# Proof audit, pass 1: Read image preview lifecycle

PR [#27954](https://git.cafe/pingdotgg/t3code/pull/27954), issue [#27946](https://git.cafe/pingdotgg/t3code/issues/27946). Reviewed head [f9e4eae](https://github.com/pingdotgg/t3code/commit/f9e4eae7d71f4abbdd80dbe66e4406a7a5e65727) against merge-base [80a2351](https://github.com/pingdotgg/t3code/commit/80a2351fcb909034e65a512dfabeb3cdfd367762). Checkout: `/home/ubuntu/wt/pr-27954`. Pass 1 is source derivation, not visual verification. No new captures, browsers, providers, servers, emulators, or leases were started. No source files or forge state were changed.

## Findings first

1. **Proof blocker: the race requires a recording and asset-request evidence.** An expanded pending Read must refer to a file that does not yet exist; that exact item then completes after the file exists, without reloading or clearing the asset cache. A completed image screenshot alone passes on main when the row was first opened after completion. Frozen pending images help comparison, but do not replace the continuous transition. Separate provider runs with different image content, path lengths, or grouping are poor pixel comparisons.
2. **Two distinct callers need proof.** Web and desktop use `WorkEntryLogRow` via `SimpleWorkEntryRow`; native mobile uses `ThreadWorkLogRow` and a different image renderer. A 390 CSS-pixel browser is narrow web, not native mobile. The required web grid is 1280×800 and 390×844, each light and dark; native needs one named OS/device at an actual phone logical size, in light and dark. Desktop has no separate changed branch and may be declared source-covered by the shared web renderer; do not claim a desktop runtime pass from browser evidence.
3. **The guard affects both explicit and fallback paths, including existing files.** It returns before path validation and the read-action/detail fallback. It hides an already-existing image during an in-progress Read too. Normal Read rows retain their path text and can still expand. In unusual data with no other detail, web expandability can change because it includes `viewedImage`; mobile expandability is precomputed independently of this helper.
4. **The stock replay showcase is not a reproducer.** The dev replay agent uses ACP. At this head its adapter does not populate `viewedImagePath`, while both clients' projected `dynamic_tool` entries omit `detail`. Naming an ACP tool “Read image.png” is insufficient. Public `orchestration.dispatchCommand` has no arbitrary provider-item injection command. A real Claude turn can seed the real app, subject to provider availability and tool ordering. Deterministic provider-ingestion/Claude replay or transport/component fixtures need to be labelled harness evidence until integrated and observed; no ready-made race driver was found or run in this pass.
5. **Check the right asset.** Success must come from the Read path's `media-file` resource, not a separate `tool-output-image` result, markdown image, Files panel, lightbox-only asset, or a second Read. Compare the same path and same item within each run; use fresh isolated client caches for base versus head.
6. **Current scope is small.** The complete diff changes only the helper (one early return plus comment) and adds a focused explicit-path pending/completed test. No component markup, contract, adapter, CSS, or theme code changes. No new source defect was established in this proof-planning pass. Existing PR screenshots/checks were not accepted as coverage of this new matrix; PR metadata reports 41 successful checks, which is not visual proof.

## Source-derived render inventory

| Boundary | Every relevant branch / input axis | Collapse for this change |
|---|---|---|
| Shared helper | `toolLifecycleStatus === inProgress` versus every other/absent status; explicit trimmed image path; invalid explicit path falling through to detail; read action plus valid single-line image detail; no candidate; CR/LF; non-image extension; blank values | R01/R02 explicit path, R04 existing file, R05 detail fallback, R06 invalid/no candidate, R07 terminal controls. Non-image formats and CR/LF are rejected, not loading/error states. All supported image suffixes share the same guard; do not cross-product codecs. |
| Status adapters | Both clients map wire `pending`, `running`, `waiting` to `inProgress`; `cancelled`/`interrupted` to `stopped`; `idle`, `completed`, `failed` keep their names. Undefined/`declined` are possible shared presentation values, not distinct current typed wire statuses | One pending/running fixture is visually sufficient for the guard; record wire status and presentation status. Verify mappings by source, not three recordings of equivalent inputs. Idle is intentionally not suppressed by this diff. |
| Web callers | `SimpleWorkEntryRow` -> `WorkEntryLogRow` from direct `WorkGroupSection` entries (optional `displayLabel`, standalone toggle callback) and from virtualized `ExpandedWorkGroupEntries` (group view state preserves expanded IDs). Notification link fallback also calls it | R01 normal live group, R03 completed lone-call/direct entry, R04 multi-entry group and remount. Active neutral tool entries are filtered outside the expanded group, so open the work-group disclosure before looking for the Read row. Notification/approval/error early returns cannot produce this Read-path preview under normal data; no capture multiplication for them. |
| Web props/context | Row `workEntry`, `workspaceRoot`, `displayLabel`; context `threadRef`, image expand callback, timestamp format, group state. Explicit path needs `threadRef` and `resolveViewedImageAsset` returning WorkspaceFile/environment media; root affects relative resolution and labels | R01 short relative/in-workspace image; R04 long absolute path outside workspace. X02 covers absent thread context/unresolvable source. Thread ID/environment route is required in real captures. Timestamps fixed/masked in derived diffs only. |
| Web collapsed/expanded | Collapsed mounts no preview image. Expanded resolved image uses `WorkLogDetails kind=media` and `ChatMarkdownAssetImage`, max height 16rem. Read path text is independent `plainOutput`. Other branches build expanded text or show inspector. `canExpandProjectedItem` may depend on image, plain output, question answer, projected detail or fetch flag | R01/R02 open throughout and reopen; R03 first open only after completion; R04 long path and portrait image. R05 legacy body deduplication. X02 isolates image-only/no-other-detail shape. |
| Native callers | `ThreadFeed` -> `ThreadWorkLog`: zero activities returns null; proposed-plan, subagent and all-reasoning groups have specialized paths; ordinary activities map rows directly or use virtualized `ThreadWorkGroupList` | R01 ordinary single row, R04 grouped 2+ rows. Zero count and the specialized non-Read branches are source exclusions, not missing Read captures. |
| Native props/context | `ThreadFeedActivity` contains projected item, work entry, `canExpand`, `fetchesDetail`, summary, live/group stand-in, full/copy text; `expandedRows`, `copiedRowId`, row sizing, group scroll state, colors and theme; `renderImage` resolves asset using thread ID/root | R01 normal live row/group, R03 lone-call group stand-in, R04 expanded multi-entry group, long text and list recycling. Copy feedback and row interaction can be recorded in R04. Default text size is sufficient: no font-size branch changes. |
| Native expanded body | `expanded` plus any reasoning/full detail/call/fetched output/viewed path/output images/question answer opens body. Read path renders above a capped text ScrollView. Output fallback is `No output.` only when fetched output has no text, output images are empty, and helper path is null. Fetch can be loading/error/expired; call output has other branches | R01 holds pending with no output then completes; R09 records image loading. X01 lists independent detail/result-image controls. Do not mistake an output fallback for a workspace image placeholder. |
| Image subcomponents | Asset URL Loading / Failure / Success; decoded-image load/error; known versus unknown dimensions; landscape versus portrait scaling; filename caption; missing-file-access error detail; web 16rem cap versus native available-width sizing | R01 landscape success + R09 loading; R04 portrait/long path; R08 actual absent/corrupt image error. Restricted connection message is a distinct unchanged permission state (X01). |
| Interactions | Web row click, Enter/Space toggle, work-group open/close/scroll/reopen; image click opens preview; preview close returns to row. Native tap toggles, long press copies, group expand/scroll/reopen, image tap opens native preview, dismiss returns; media action menus unchanged | R01 toggle/reopen and preview round trip; R03 delayed first expansion; R04 group/scroll/copy. Every such fixture is **recording**. Save still frames as supplementary before/after/diff evidence. No new Settings, palette, or keybinding entry point exists for this helper. |
| Platform/connection | Web locally served and hosted/remote use same renderer; desktop wraps it. Native iOS/Android use same caller with Expo image/native preview. Preview URL access routes to entry environment | Capture one real native OS and state which; other OS and desktop are source-covered, not runtime-verified. At least one R01 run must use a browser/native client connected to the isolated server with real authenticated `assets.createUrl`; a remote native connection can satisfy the remote path. No full local/relay/tunnel cross-product: this diff changes no routing. |

Sources (pinned to reviewed head): [helper](https://github.com/pingdotgg/t3code/blob/f9e4eae7d71f4abbdd80dbe66e4406a7a5e65727/packages/client-runtime/src/work-log/presentation.ts#L358), [web row](https://github.com/pingdotgg/t3code/blob/f9e4eae7d71f4abbdd80dbe66e4406a7a5e65727/apps/web/src/components/chat/TimelineWorkEntryRow.tsx#L295), [web parent paths](https://github.com/pingdotgg/t3code/blob/f9e4eae7d71f4abbdd80dbe66e4406a7a5e65727/apps/web/src/components/chat/TimelineWorkGroupRows.tsx#L74), [web expanded body](https://github.com/pingdotgg/t3code/blob/f9e4eae7d71f4abbdd80dbe66e4406a7a5e65727/apps/web/src/components/chat/TimelineWorkEntryRow.logic.ts#L92), [web path text](https://github.com/pingdotgg/t3code/blob/f9e4eae7d71f4abbdd80dbe66e4406a7a5e65727/apps/web/src/components/chat/MessagesTimeline.logic.ts#L206), [native row](https://github.com/pingdotgg/t3code/blob/f9e4eae7d71f4abbdd80dbe66e4406a7a5e65727/apps/mobile/src/features/threads/thread-work-log.tsx#L1032), [native row construction](https://github.com/pingdotgg/t3code/blob/f9e4eae7d71f4abbdd80dbe66e4406a7a5e65727/apps/mobile/src/lib/threadActivity.ts#L790), [native asset resolution](https://github.com/pingdotgg/t3code/blob/f9e4eae7d71f4abbdd80dbe66e4406a7a5e65727/apps/mobile/src/features/threads/ThreadFeed.tsx#L3016).

## Named fixture matrix

Capture profiles: **WL** web 1280×800 light; **WD** web 1280×800 dark; **NL** web 390×844 light; **ND** web 390×844 dark; **ML/MD** actual native phone light/dark (record device, OS, app build, logical viewport; use 390×844 when available, otherwise record the actual size). “Pair” means matching base/head evidence. A recording may cover multiple rows; do not rerun every row in every cell.

| ID / named fixture | Exact data / visually distinct state | Diff effect | Medium | Minimum capture cells | Seed into real app or labelled harness | Pass-1 status |
|---|---|---|---|---|---|---|
| **R01 — Pending Read, file absent, row open** | One target Claude `dynamic_tool` inside the normal live work group, `toolName=Read`, input `{file_path: P}`, valid explicit `viewedImagePath=P`, wire pending/running/waiting, presentation inProgress; 0 output images, no result text. Open the work-group disclosure and then the target Read row. Same turn has delayed writer. P points to an absent 320×200 PNG target. | **Direct**: base mounts preview and can cache failure; head shows path/details, no workspace preview/loading/error frame and no `media-file(P)` request. | **recording**; pending is time-dependent even if frozen | **Pair WL, WD, NL, ND, ML, MD**. Continue each into R02. | S1 real Claude; or S2 deterministic service/Claude replay harness. Do not use stock ACP showcase. | **MISSING — capture required** |
| **R02 — Same Read completes, no reload** | Exactly R01 item ID/thread/path. Writer creates fixed PNG bytes before Read result. Update same item to completed. Keep row open; image goes through normal load and shows expected pixels. Collapse/reopen; web click image then close, native tap preview then dismiss. | **Indirect race result**: base remains unavailable if its earlier lookup failed; head first requests the asset now and shows it. Terminal helper return itself is unchanged. | **recording**, continuous with R01; stills insufficient | **Pair WL, WD, NL, ND, ML, MD**, same sessions as R01. Frames: pending and finished; diff for each matching frame. | Continue S1/S2; preserve cache and item identity. Log `media-file` request start and item/file milestones. | **MISSING — capture required** |
| **R03 — Closed until finished; first open succeeds** | Fresh Read/path/cache. Leave row closed while file absent and call inProgress. File written; same Read completed; only then open. 320×200 PNG. | **Control, unchanged**: both base and head succeed. Distinguishes the race from a screenshot that would already pass on main. | **recording** (first-expansion interaction and ordering) | **Pair WL + ML**. May be another row in the R01/R02 recording. | S1/S2, new item and unique target path; never prime its asset by opening Files/lightbox first. | **MISSING — capture required** |
| **R04 — Existing tall image, long paths, mixed group** | 2+ tool entries in an opened virtualized group: one completed landscape Read, one inProgress Read of an already-existing 200×800 portrait PNG at a long absolute path outside workspace, plus non-image read or Bash. Long label/path; 1 versus several read paths can use the same group. Expand pending row, complete it, scroll out/back, close/reopen group. Native long-press a row to copy then return to preview. | **Direct**: existing pending image is deliberately hidden until completion; sibling completed image remains. Covers group layout, long text, portrait sizing, standalone versus grouped mount behavior. | **recording** | **Pair ND + MD**. Include the short-path target row from R01 as comparison; use R03 for completed lone-call/direct rendering. | S1 existing-file Read may finish too quickly: use S2 gate if needed, label harness. Paths outside project still resolve through media-file access; do not seed into the lead's workspace. | **MISSING — capture required** |
| **R05 — Legacy detail-only image candidate** | Presentation entry lacks `viewedImagePath`, has read action (`requestKind=file-read` or Read tool classification), `detail=P` (trimmed, single line, image suffix). InProgress -> completed; same body/open state. Include normal input path so row can expand. | **Direct**: early return also gates detail fallback. Web body deduplication sees null while pending; completion resolves preview. This data is not produced by the current normal dynamic-tool projection. | **recording**, **labelled harness** | **Pair WL web renderer + ML native renderer**; no need for all width/theme cells. | S3 presentation/feed harness using production row renderer. Current public RPC cannot add `detail` to this projection. Do not present it as a real Claude session. | **MISSING — harness capture required** |
| **R06 — No candidate, non-image and invalid-path controls** | Text Read `notes.txt`; separate empty/absent candidate; invalid explicit candidate and detail with CR/LF or non-image suffix; include a valid detail fallback separately in R05. Ordinary short and long path text, no preview on either revision. | **Unchanged**: gate must not create image placeholders or remove ordinary Read text. Blank entries/group count zero render nothing, source-covered. | **recording** if opened/toggled; a frozen control tableau may supply supplementary stills | **Pair ND + MD**, can share R04 capture thread; synthetic invalid values require S3 | S1 for normal text Read, S3 for invalid/empty fields rejected earlier by extraction/schema. Label synthetic portions. | **MISSING — control capture required** |
| **R07 — Finished failure/stopped still allows existing image** | Expanded explicit-path Read with valid existing PNG; terminal failed item with short error text, and stopped/cancelled or interrupted item with no output. Completed success already in R02. | **Unchanged**: failed/stopped do not enter guard. Failure icon/text differ from neutral/success. Idle/undefined/declined use same helper branch; idle/undefined need no separate visual gate unless a different caller is identified. | **recording** for row expansion; frozen terminal images supplementary | **Pair WD + MD**. Two status rows may be adjacent in one recording. | S1 actual failure/interrupt when obtainable; S2 fixture with exact terminal status and P. Idle/undefined/declined mapping is source coverage, not a new typed wire fixture. | **MISSING — control capture required** |
| **R08 — Genuine unavailable image after completion** | Expanded failed/completed Read with explicit valid PNG path that stays absent (or corrupt bytes), with actual error detail. File never appears. | **Unchanged terminal error**: unavailable frame should still occur after finishing; no claim that fix masks real missing/undecodable assets. | **recording** pending -> terminal error | **Pair ND + MD**, can share R06/R07 capture run | S1 Read a permanently absent/corrupt unique target, or S2. Record resulting `media-file` failure; do not manufacture a fake success. | **MISSING — control capture required** |
| **R09 — First permitted asset load** | R02 completion with known landscape dimensions or unknown dimensions until decode. First `assets.createUrl`/image bytes withheld briefly by an explicit fixture gate, then released; optional decode-failure release continues R08. | **Affected mount timing**: head must not confuse normal post-completion Loading with a pending tool preview. Actual loader visual differs from unavailable. | **recording** (request and decode lifetime) | **WL + MD head**, normally excerpts of R02. If normal load is too quick, use labelled S2/S3 gate. | S1 if observable, otherwise S2/S3 controlled image response. Do not assert a loader existed from a finished still. | **MISSING — capture/excerpt required** |
| **X01 — Independent inspector/result-image/permission branches** | `outputOmitted`/summarized input false versus true; detail Loading / failure / expired / empty / text; 0 versus 1+ `tool-output-image` resources; no-file-access pairing yields distinct explanatory error; reasoning/question/notification/approval/error-special rows. | **No newly changed guard branch**. Native empty fetched result depends on helper path and is already sampled in R01/R02; result images remain independent. | Source inventory; any exercised transition is **recording** | **No extra mandatory capture** for this one-branch diff. Do not use these assets to satisfy R02. | Real output/detail fetch or restricted-scope pairing; synthetic error states require labelled S2/S3. | **SCOPED OUT of extra capture; inspected in source** |
| **X02 — No resolvable asset/context or image-only expandability** | Web threadRef absent; candidate ending in image suffix but classified non-WorkspaceFile; root undefined/relative handling; entry with no label/plain text/tool data/detail except a candidate so web chevron can disappear while pending. Native independently constructed canExpand false. | Direct guard can remove image-only expansion; real chat has thread ref and ordinary Read input/path text. Resolver returns null on both revisions for non-workspace source. | **labelled harness recording** if exercised | **No extra mandatory capture** unless lead claims support for these synthetic contexts. R05 covers supported legacy shape. | S3 only for absent context/contradictory feed shape; unavailable through normal real-chat route. | **SCOPED OUT of extra capture; inspected in source** |

## Minimum capture bundle

The matrix does not require a full cross-product of every row. Minimum is:

| Profile | Required row coverage | Artifact |
|---|---|---|
| WL | R01/R02/R03, plus R09 excerpt | Matching base/head continuous recording, pending+finished still pairs and derived diffs |
| WD | R01/R02/R07 | Same |
| NL | R01/R02 | Same |
| ND | R01/R02/R04/R06/R08 | Same; controls can be adjacent rows in one run |
| ML | R01/R02/R03 | Native recording with OS/device/app build stated; base/head matched frames and diffs |
| MD | R01/R02/R04/R06/R07/R08, R09 excerpt | Same; remote server asset route if device is remote |
| H-WL | R05 | Labelled web production-renderer harness recording and matching frames/diffs |
| H-ML | R05 | Labelled native production-renderer harness recording and matching frames/diffs |

R06 invalid-shape portions can use the corresponding harness recording instead of contaminating a real-app run. R04/R07 may require gated service fixtures; label their provenance. A real app capture of R01/R02 is still necessary to claim the bug fixed end to end. A mobile web screenshot or React Native web harness cannot substitute for ML/MD.

## Seeding recipes for the capturer

**S1: real app, Claude provider.** Use the lead's isolated dev server/data, not live T3 home. In web use `/$environmentId/$threadId`; on native open that same environment/thread. Theme is the client's light/dark setting; use actual viewport resizing for web. Through the app or `thread start` CLI/public thread RPC, ask the configured cheapest available Claude model to emit one assistant message with (a) a delayed PNG writer and (b) Read of the same target. Existing PR reproduction used a roughly 40-second delayed writer. Use a fresh timestamped scratch run directory and byte-identical image content on base/head. Expand the live work group, then the target Read row, only after the provider has emitted that pending Read and before file creation. Confirm `viewedImagePath` and actual lifecycle in projection. If it serializes the calls into separate messages, finishes before opening, or never emits the path, reject that attempt. Do not call a provider failure a visual pass. Provider availability on this VM was not tested by this read-only pass.

**S2: deterministic service/Claude replay fixture, labelled harness until integrated.** Reuse the repository's `ClaudeOrchestratorReplayHarness` / `ClaudeAgentSdkQueryRunner` replay layer, or a standalone isolated server-runtime driver with `ProviderEventIngestorV2.ingestNormalized`. Seed the normal project/thread/run/provider session first, with the run active so pending tool entries remain visible in the client's active group. A pending item attached only to a stopped run can be filtered out and is not equivalent. Emit a `turn_item.updated` dynamic_tool event with stable item ID and `toolName: Read`, `input: {file_path: P}`, `viewedImagePath: P`, `status: running`, proper thread/run/node/provider IDs and timestamps. Gate completion on an explicit driver signal. With P absent, let client open row, capture, and observe asset-request counts. Write the PNG and wait for successful write/close, then emit a second event for the SAME item with `status: completed`, `completedAt`, new `updatedAt`, and realistic output. Await committed events/drain effects, not arbitrary sleeps. R04 keeps P present before first event; R07/R08 vary terminal status and file availability. Never send fabricated provider events as public client commands: that contract does not accept them. The driver must participate in the serving runtime/event stream, or expose a labelled fixture transport; a second process writing SQLite behind a running server is not sufficient. This recipe is derived, not implemented or smoke-tested here.

S2 source hooks: [provider-ingestion example](https://github.com/pingdotgg/t3code/blob/f9e4eae7d71f4abbdd80dbe66e4406a7a5e65727/apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts#L890), [Claude replay harness](https://github.com/pingdotgg/t3code/blob/f9e4eae7d71f4abbdd80dbe66e4406a7a5e65727/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts#L2816), [public command schema](https://github.com/pingdotgg/t3code/blob/f9e4eae7d71f4abbdd80dbe66e4406a7a5e65727/packages/contracts/src/orchestrationV2.ts#L2866), [Claude path assignment](https://github.com/pingdotgg/t3code/blob/f9e4eae7d71f4abbdd80dbe66e4406a7a5e65727/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts#L5655), [ACP dynamic-tool assignment](https://github.com/pingdotgg/t3code/blob/f9e4eae7d71f4abbdd80dbe66e4406a7a5e65727/packages/provider-acp/src/server/adapter.ts#L3925).

**S3: presentation/feed harness.** Supply the production web row contexts or native `ThreadWorkLog` with deliberately shaped presentation entries/feed activities. Preserve actual image component and asset transport where possible. For R05 omit explicit viewedImagePath and set valid image detail plus read classification. For invalid data deliberately bypass extraction and label that fact. Rerender the same keyed entry from inProgress to completed under driver control, recording the transition. A static reproduction of the helper's return value or props is not visual evidence. Native harness means native runtime; a browser render of native components is a different surface. No such ready-made harness was built in this pass.

## Evidence acceptance / handoff

Keep a manifest with fixture ID, profile, base/head SHA, runtime provenance (real Claude / service replay / presentation harness), same-item/path milestone order, source file dimensions and hash, asset resource kind and request outcomes, and artifact URLs. Remove auth tokens, pairing URLs and private paths from material before upload. Record `media-file(P)` request count: zero while head is inProgress, then first request after completion; on base show failed early request and persistent unavailable result without reload. Report output-image assets separately.

Extract labelled Before/After frames from the continuous recordings for pending and completed. Produce a pixel/overlay diff for each matched frame pair; identical PNG bytes, matching viewport/theme/scroll/disclosure state, and a fixed or consistently masked timestamp region make this meaningful. Preserve raw frames/recordings alongside annotated diffs, and label masks. If an image changes between separate provider runs, rerun a deterministic fixture instead of treating the content difference as proof. A pending freeze is supplemental, never enough for R01/R02.

The lead should replace stale verification prose with a concise **Evidence** section linking the matrix, each covered row/profile, videos, Before/After images and diff images, plus honest exclusions (desktop/other native OS/harness-only rows). Do not claim all platforms from shared code alone. This pass performs no PR edits/comments.

Status at handoff: R01–R09 await capture/excerpts; X01/X02 are explicit source-scoped exclusions. Focused test execution, browser verification, mobile verification and provider availability were not run. Checkout remained clean. Available disk was 16 GB at start and latest check, above the 5 GB stop threshold.

FRICTION: none. The stock-replay limitation is a source-derived fixture constraint, not a failed capture attempt. No owned runtime processes require teardown.

VERDICT: INSUFFICIENT R01–R09 captures/recordings, matching before-after diffs, and Evidence section remain outstanding.
