# Proof audit, pass 1 — existing evidence after recaptures

Findings

1. **One combination remains insufficient: F4-Android at a compact 360×800 viewport with 1.5× text.** The supplied Android stress pair is genuinely dark, enlarged, scrollable and dismissible, but its native viewport is 411×923. It proves the larger-phone case. Add only a compact before/after recording pair showing initial message, scroll to `last-readable-segment.png`, OK, and a settled thread. The same pair can supply compact-width coverage; do not redo the ordinary Android light/dark rows solely to duplicate width coverage.
2. **All other affected fixture outcomes are covered by acceptable existing recordings.** The checked-path, resource-path and pathless messages match the code; both platform gallery policies are visible; custom callback, video and attachment fallback paths preserve their prior output. No reviewed settled image shows duplicated elements, a stale overlay, a spinner or permanently clipped message. Scroll viewport edges and normal iOS glass translucency are not capture defects.
3. **No retake is needed for dismissal in F4-iOS or V2.** Uniformly sampled frames initially miss their sparse tails. The actual last decoded frames show a clean thread after OK. F11 and D1 likewise return to their thread; D2 returns to Chats, which the current PR body correctly says. D1 records successful Try again recovery. Current-head guard recordings do not need duplicate before recordings: the visible guard output is intentionally unchanged; changed message families already have paired proof.

Scope and method

Read-only audit of PR [#32994](https://git.cafe/pingdotgg/t3code/pull/32994), branch `fix/issue-32977`, the supplied local merged head, diffed against `origin/main`. Public capture after revision is [c4238adea1](https://git.cafe/pingdotgg/t3code/commit/c4238adea165b4d853298ce8b8b0d51f91863b53); before is [cec8d73a1f](https://git.cafe/pingdotgg/t3code/commit/cec8d73a1f0fd3bd9a48df88640436e73a04afa3). The four changed production files are byte-identical between the public after and local merged head, and between the capture before and current `origin/main`. This makes the supplied old/new behaviors relevant to the audited diff. Revision attribution comes from the supplied capture sheets/scripts, checked against source and observed messages.

Derived the branches and caller collapse from source before reading the previous fixture matrix. Downloaded all 29 selected stills and 29 selected MP4s in the current PR body, all 18 current Diff0/DiffT images, and five relevant supplemental stills from its sheets. Visually inspected the stills and decoded recording sequences. Checked exact starts/tails for long/sparse clips rather than relying on evenly spaced samples. Read the reproduction scripts without executing them. The before clips show the reconnect text; after changed-copy clips show the intended specific error, with the same fixture background. The inspected diff images are consistent with message/dialog geometry plus status-bar changes. These are existing captures, not a new live client test.

Capture selection

Use existing iOS 402×874 and Android 411×923 default-text light/dark pairs for normal rendering. Constrained-height/width stress is a separate fixture: iOS 375×667 at Accessibility Large (2.143×), Android 360×800 at 1.5× text, one theme each. The latter is the sole gap. There is no source breakpoint requiring every row at every width or both themes. An existing 411×923 stress capture is accepted as supplemental, not substituted for the compact combination. Do not multiply rows by provider, transport, tablet, or caller when they use the same resolver and default dialog. No web/desktop production files or wire contracts change. Local/remote/relay/tunnel all go through the same prepared-connection URL resolution here.

Every fixture with a tap, async resolution, page/scroll, close, retry, navigation or authorization age is marked **recording**; stills alone are insufficient. The minimum is the affected rows below. F6 is visually subsumed by F5. U1–U6 enumerate unaffected branches and impose no new capture requirement. Their “missing” statuses mean no separate capture is claimed, not a request to generate more evidence.

Source-derived render and branch inventory

- [FilePreviewSession.tsx](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/components/ui/FilePreviewSession.tsx): Only production callers are the iOS and Android FilePreviewModal wrappers. iOS passes progressiveGallery=true; Android false. Remote resolution is shared by image/PDF/document before viewer choice. Typed error selects checkedPath ?? resource.path, then truthiness selects path text vs error.message. Other error/null becomes generic reconnect text. Custom onOpenError receives cause and suppresses default alert; resolution closes the session. A failed nonselected iOS image instead settles a failed slot. Selected failure is fatal on both platforms. URI/local inputs bypass this changed remote branch. Undefined URI means pending; null means failed. Android waits for every URI; iOS presents slots immediately. Selected index, frozen/deduplicated image-only group, one/many and abort/lease lifecycle are accounted for in F9/U1/U2/U6.
- [VideoPreviewModal.tsx](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/components/ui/VideoPreviewModal.tsx): All six caller files listed below feed null/local/direct/remote media. Only iOS remote media uses changed standalone refresh: pre-ready rejection catches to null, connected/None connection select distinct advice. After ready, native failure reauthorizes and suppresses rejection, then gives playback/save-share guidance and closes. Native listener, audio ownership, local lease and cleanup branches are unchanged. Android imports the unchanged hook instead.
- [assets.ts](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/state/assets.ts): Standalone refresh is called only by FilePreviewSession, the two iOS video sites, readAttachmentText and freshAssetUrlFamily. Initial missing connection returns null; query Failure now throws its squashed error; successful query re-reads current connection and can return null if lost. Fresh family catches failure to null, preserving generic unavailable copy; success stamps authorizedAt. Null environment/resource, initial/loading/waiting/success/error, revision Retry and session-generation refresh are accounted for by D1/U4. useAssetUrlState/useRefreshAssetUrl and project favicon consumers do not call this changed standalone path.
- [attachmentDocument.ts](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/lib/attachmentDocument.ts): useAttachmentDocument has one production consumer: AttachmentFileScreen. Local readableStream bypasses mint; remote text re-mints only when authorization is null or older than 5 minutes; rejected refresh catches to null and generic reconnect. Fresh URL fetch and stale text read are distinct D1/D2 paths. needsText is text/Markdown or HTML Source. Remote/local URL, rendered/source, delimiter table, content empty/truncated, loading/error, size Range and revision no-cache, no environment, retry/share, and native viewer selection are U4 except changed failure guards D1/D2.

[iOS Dialog](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/components/ui/Dialog.tsx) delegates to native Alert. [Android Dialog](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/components/ui/Dialog.android.tsx) uses a separate Compose AlertDialog, themed/scaled title/body and verticalScroll for long message text; its action remains separate from message scrolling. This is why platform, themes, long text and constrained native viewport are fixture dimensions. Neither dialog implementation changes in this diff.


| File-preview caller | Sources / layout and error handling | Collapsed fixtures |
|---|---|---|
| [ThreadFeed](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/threads/ThreadFeed.tsx) | Markdown chips/embeds, media-file or attachment/tool-output resources, direct URI; image gallery; default alert. | F1–F5, F7–F10; U1–U3/U6 |
| [FileMarkdownPreview](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/files/FileMarkdownPreview.tsx) | Thread media-file or draft-workspace-file; direct URI; captured mode disallows workspace resolution. | F1–F5/F7/F8; U2/U3 |
| [ComposerContextSheet](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/components/ComposerContextSheet.tsx) | Remote captured attachment, local file-backed input or inline URI; image/PDF, context galleries; default alert. | F7/F9/F10; U2/U3/U6 |
| [ComposerContextAttachment](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/components/ComposerContextAttachment.tsx) | Already resolved image URI; remote/local actionsSource changes sharing, not resolver. | U2/U3 |
| [ThreadComposer](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/threads/ThreadComposer.tsx) | Draft local image/PDF or inline URI, composer gallery; close restores focus. | U1/U2/U3/U6 |
| [QuestionAttachments](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/threads/QuestionAttachments.tsx) | Local draft/inline URI attachments; document navigation to ThreadAttachment. | U1/U2/U3; D1/D2 downstream |
| [ReviewCommentComposerSheet](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx) | Local draft image/inline URI via ComposerAttachmentStrip. | U1/U2/U6 |
| [PullRequestMarkdown](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx) | Credentialed github-media resource when host/context matches, otherwise direct URL; grouped images. | F7/F9/F10; U2/U6 |
| [PullRequestMedia](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx) | Two file preview mounts: media image and explicit image attachment; github-media or direct URI, gallery; default alert. | F7/F9/F10; U2/U6 |
| [WorkspaceFileImagePreview](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx) | Already resolved image URI; unavailable inline image blocks open. | U2 |
| [ThreadFilesRouteScreen](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx) | Open PDF action passes already resolved URI; remote resource retained only in actionsSource. | U2/U3 |
| [AttachmentFileScreen](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/files/AttachmentFileScreen.tsx) | Remote attachment/local/URI; custom onOpenError maps to inline generic native error; reader modes. | F11/D1/D2; U2–U4 |
| [ZipArchiveView](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/files/ZipArchiveView.tsx) | Extracted entry URI with custom inline error callback; bypasses remote refresh. | U2–U4 |
| [MermaidCodeBlock](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/native/MermaidCodeBlock.tsx) | Generated image URI and explicitly empty gallery. | U2/U6 |

VideoPreviewModal direct callers: [ThreadFeed.tsx](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/threads/ThreadFeed.tsx), [ThreadComposer.tsx](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/threads/ThreadComposer.tsx), [QuestionAttachments.tsx](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/threads/QuestionAttachments.tsx), [ComposerContextSheet.tsx](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/components/ComposerContextSheet.tsx), [FileMarkdownPreview.tsx](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/files/FileMarkdownPreview.tsx), [PullRequestMedia.tsx](https://git.cafe/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx). ThreadComposer/QuestionAttachments supply local draft video; ComposerContextSheet supplies local or remote attachment; the other callers supply direct or remote media. They collapse into V1/V2/U5. ThreadMarkdownVideo and Android VideoPreviewModal use the unchanged useRefreshAssetUrl hook and therefore do not acquire the new rejection contract.

Fixture matrix

| Fixture | Data / visually distinct branch | Diff effect | Minimum viewport × theme × row / medium | Real-app seed or labelled harness | Existing evidence inspected | Status |
|---|---|---|---|---|---|---|
| F1-light | Selected missing image; connected; typed not-found with multiline absolute checkedPath. | Changed: close pending viewer; path replaces reconnect copy. | iOS 402×874 × light, default text; before/after **recording**; tap → alert → OK → thread. | ThreadFeed: synthetic replay reply with absent `v2/extended/duo-color-3-extended-FINAL.png`; keep present `docs/in-project.png` as connection control. | [before still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/5abb72d5161527db/F1-ios-light-before-20261110T0836.png) · [before recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1117bfcc8d20d068/F1-ios-light-before-20261110T0836.mp4)<br>[after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e945dfd83cfacd8d/F1-ios-light-after-20261110T0828.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/44198de6d18e5f39/F1-ios-light-after-20261110T0828.mp4) | covered — correct theme, readable complete path; old/new copy and clean dismissal visible. |
| F1-dark | Selected missing image; connected; typed not-found with multiline absolute checkedPath. | Changed: close pending viewer; path replaces reconnect copy. | iOS 402×874 × dark, default text; before/after **recording**; tap → alert → OK → thread. | ThreadFeed: synthetic replay reply with absent `v2/extended/duo-color-3-extended-FINAL.png`; keep present `docs/in-project.png` as connection control. | [before still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d16d6c0f31390b54/F1-ios-dark-before-20261110T0836.png) · [before recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/4174148255256055/F1-ios-dark-before-20261110T0836.mp4)<br>[after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f31a56a2bec58ce7/F1-ios-dark-after-20261110T0839.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/fc96942bf21d2eed/F1-ios-dark-after-20261110T0839.mp4) | covered — correct theme, readable complete path; old/new copy and clean dismissal visible. |
| F2-light | Same selected failure, Android nonprogressive session and Compose dialog. | Changed copy through Android renderer. | Android 411×923 × light, default text; before/after **recording**; tap → alert → OK → scroll thread. | Same synthetic thread/data; Android client; set app and OS appearance together. | [before still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/8b6bdb069550986c/F2-android-light-before-20261110T0855.png) · [before recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d219e7f37ee952be/F2-android-light-before-20261110T0855.mp4)<br>[after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/af6c5ee71b3bed11/F2-android-light-after-final-20261110T0853.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f0569aef22570f2c/F2-android-light-after-final-20261110T0853.mp4) | covered — larger-phone platform/theme proof accepted; no ghosted or stale dialog after dismissal. Compact width is assessed separately in F4-Android. |
| F2-dark | Same selected failure, Android nonprogressive session and Compose dialog. | Changed copy through Android renderer. | Android 411×923 × dark, default text; before/after **recording**; tap → alert → OK → scroll thread. | Same synthetic thread/data; Android client; set app and OS appearance together. | [before still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/626c7fd40f5cbc7e/F2-android-dark-before-20261110T1010.png) · [before recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d6b995c2ae1dda45/F2-android-dark-before-20261110T1010.mp4)<br>[after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/fad9dfead63fcc29/F2-android-dark-after-20261110T1012.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/0755fb7d3e38b8f5/F2-android-dark-after-20261110T1012.mp4) | covered — larger-phone platform/theme proof accepted; no ghosted or stale dialog after dismissal. Compact width is assessed separately in F4-Android. |
| F3 | No checkedPath; resource.path=`missing.png`; short single-line message. | Changed: resource path fallback. | iOS 402×874 × light; before/after **recording**. | Real draft-workspace file from FileMarkdownPreview can omit checkedPath on current server; alternatively labelled actual-session harness throwing typed error with path and no checkedPath. Existing capture uses harness. | [before still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ca36c7a19cd2f759/F3-ios-light-before-native-harness-20261110T0920.png) · [before recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d4f65500667a5172/F3-ios-light-before-native-harness-20261110T0920.mp4)<br>[after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/50474e6661d88356/F3-ios-light-after-native-harness-20261110T0846.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/9b3da79fe7061bff/F3-ios-light-after-native-harness-20261110T0846.mp4) | covered — old reconnect → `The file was not found at missing.png.`; harness is correctly labelled; clean exit. |
| F4-iOS | Near-limit missing path, long unbroken segments, spaces/é/資料; narrow screen plus accessibility text. | Changed dynamic length makes native alert scroll. | iOS 375×667 × light, Accessibility Large 2.143×; before/after **recording**; show initial body, scroll to final filename, OK, thread. | Missing path near 1024-character resource limit with filesystem-safe segments; labelled typed-error harness is acceptable when host path setup is impractical. Existing path is about 900 characters including checked prefix. | [before still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/834cadf579355255/F4-smallios-light-before-20261110T1002.png) · [before recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/73c4a49ecaa2284c/F4-smallios-light-before-20261110T1002.mp4)<br>[after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/3f9f64b729ccb517/F4-smallios-light-after-20261110T1006.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1902ee013afdc242/F4-smallios-light-after-20261110T1006.mp4)<br>[scrolled end](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/db149f72245b7269/F4-smallios-light-after-20261110T1006-end.png) | covered — final filename readable and OK reachable. Full last decoded frame at 158.532 s confirms clean return to thread; sparse-frame clip must be checked through its end. |
| F4-Android | Same near-limit path on compact Android, enlarged text. | Changed dynamic length; Android verticalScroll, title and confirm button must remain usable under both width and height constraints. | Android **360×800** × dark, text 1.5×; before/after **recording**; initial body → scroll to last filename → OK → settled thread. | Same labelled long-path harness in real Android app; compact emulator/device or verified native logical-size override, followed by app relaunch. Record actual Dimensions and fontScale; browser resizing is not equivalent. | [before still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/8bedcc57668aa0c5/F4-before-20261110T1044.png) · [before recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/b1e97359b9578667/F4-before-20261110T1044.mp4)<br>[after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/48f9577a4bcdca19/F4-after-20261110T1048.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/4447f11cac50a462/F4-after-20261110T1048.mp4)<br>[existing scrolled end at 411×923](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/bb81f0c460830c71/path-end.png) | insufficient — existing Pixel 9 pair is clean and scrolls/dismisses correctly, but is 411×923 at 1.5×, not compact 360×800. Add only this compact before/after pair; retain existing pair as supplementary proof. |
| F5 | Typed not-found with neither checkedPath nor path; tool-output-image resource. | Changed: error.message fallback (`Tool output image was not found.`). | iOS 402×874 × light; before/after **recording**. | Seed persisted tool-output image then make stored image unavailable if still tappable; otherwise labelled actual session + native dialog harness with tool-output-image resource. Existing capture is harness. | [before still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/09bc935e5e83ef27/F5-ios-light-before-20261110T1001.png) · [before recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/b07540cd203755fc/F5-ios-light-before-20261110T1001.mp4)<br>[after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/754d3898f95e3248/F5-ios-light-after-20261110T1009.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1f2020207cc819fb/F5-ios-light-after-20261110T1009.mp4) | covered — correct pathless copy, single alert, no reconnect advice; old/new flow and dismissal visible. |
| F6 | checkedPath empty string with pathful resource; alternate pathless not-found messages. | Boundary: empty string wins ??, then falls to error.message; same short-message layout as F5. | —; collapsed into F5 for visual minimum. **recording** if separately captured. | Labelled harness only for empty checkedPath; current server normally sends nonempty path or omits it. | No separate capture; F5 covers the visual family. | covered — visual layout is subsumed by F5; no additional capture required. No separate empty-string logic proof is claimed. |
| F7-server | Connected non-not-found refusal: missing attachment / authorization / network error. | Guard: generic reconnect + moved/deleted advice preserved. | iOS 402×874 × light; current-head **recording**; tap → alert → OK. | Use a disposable missing attachment or denied read scope. Existing labelled harness redirects refresh to real server with absent attachment, preserving session/dialog. | [after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/292a70c8c7937f64/F7-ios-generic-real-attachment-refusal-20261110T0849.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e5e4e535974a0ab5/F7-ios-generic-real-attachment-refusal-20261110T0849.mp4) | covered — real generic server refusal at controlled resource, original text retained; no checked path; clean dismissal. |
| F7-no-connection | No prepared connection initially, or connection disappears after mint. | Guard: null result keeps same generic alert. | iOS 402×874 × light; current-head **recording**. | Load thread then disconnect and tap if allowed; otherwise labelled session-boundary harness returning null. Existing capture is harness, not proof of a physical network drop. | [after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/601003ed87710b73/F7-ios-disconnected-native-harness-final-20261110T0855.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/724bb9b5920e23cf/F7-ios-disconnected-native-harness-final-20261110T0855.mp4) | covered — correct null-result alert and return; labelled harness is sufficient for this rendering row. |
| F8 | Selected missing remote PDF; document kind uses same pre-viewer resolver. | Changed: checked-path alert before native viewer mounts. | iOS 402×874 × light; before/after **recording**. | Synthetic thread with absent `docs/missing.pdf`; tap its chip. No duplicate document-kind fixture needed because resolver failure occurs before viewer selection. | [before still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/669e3394ad79cdb3/F8-ios-light-before-20261110T0921.png) · [before recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/332538dc692dfb86/F8-ios-light-before-20261110T0921.mp4)<br>[after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1e5dc33419273736/F8-ios-light-after-20261110T0844.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ca591348d93fd64a/F8-ios-light-after-20261110T0844.mp4) | covered — missing.pdf path, correct old/new copy; no native-opening or Save/share fallback overlay; clean dismissal. |
| F9-iOS | Three images A good / B missing / C good; select A, B fails nonselected. | Guard: progressive gallery retains A, settles B to failed slot; C remains usable. | iOS 402×874 × light; current-head **recording**; open A → page B → C → close. | Replay message with three image embeds; files A/C exist, B absent. Session freezes and deduplicates this image-only group. | [after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/aa33b91fd0ec91ca/F9-ios-light-after-final-20261110T091823Z.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f623e85a54682c7f/F9-ios-light-after-final-20261110T091823Z.mp4)<br>[selected A](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e438ef932583d26e/F9-ios-light-after-final-20261110T091823Z-selected.png) · [C](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/af576d4b5d2a06cd/F9-ios-light-after-final-20261110T091823Z-third.png) | covered — A cyan, B explicit failed slot, C orange, then clean thread. Transitional swipe frames are not stale overlays. |
| F9-Android | Same group; nonselected B fails before all URLs resolve. | Changed sibling alert copy: nonprogressive session closes instead of showing a partial gallery. | Android 411×923 × light; current-head **recording**; tap A → B-path alert → OK → thread. | Same three-image message and files as F9-iOS. | [after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/7a191481683febfe/F9-android-light-after-final-20261110T0856.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/b138b5079b0d27d8/F9-android-light-after-final-20261110T0856.mp4) | covered — actual missing sibling filename in alert, no stuck image modal, return/scroll works. F2 already supplies paired proof of the same changed Android alert selector; no extra before capture needed. |
| F10 | Selected existing remote image; normal successful mint. | Guard: opens, closes and reopens; no new failure. | iOS 402×874 × light; current-head **recording**; open → close → reopen → close. | Existing `docs/in-project.png` on same prepared connection as F1. | [after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/0a12f947a8de5508/F10-ios-light-after-final-20261110T0859.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/2401ba4a1e536c02/F10-ios-light-after-final-20261110T0859.mp4) | covered — present image opens twice and both exits return to same thread. Success logic is unchanged; no additional success cross-product needed. |
| F11 | Remote native-opening resolution fails, custom onOpenError supplied. | Guard: original cause reaches callback; caller shows inline native error instead of default alert. | iOS 402×874 × light; current-head **recording**; navigate → failure → inline body → Back. | Upload actual attachment, navigate ThreadAttachment with its metadata; allow initial URL then fail standalone native-open refresh. Existing labelled harness retains real AttachmentFileScreen callback/body. | [after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/966080bd0a15b434/F11-ios-native-callback-harness-20261110T0912.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/587ed4bc328ec439/F11-ios-native-callback-harness-20261110T0912.mp4) | covered — single `No preview for this file` body with generic connection guidance and Try again; no default path alert. Exact tail confirms Back to thread. |
| V1-connected | iOS remote video URL mint rejects while prepared connection exists; ready=false. | Guard: `Could not load this video. Check the connection and try again.` | iOS 402×874 × light; current-head **recording**. | Missing `docs/missing.mp4` chip on connected synthetic thread. | [after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c1f022f59d929ad4/V1-ios-connected-after-20261110T0902.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ed949e1ecd5f608f/V1-ios-connected-after-20261110T0902.mp4) | covered — correct connected-video copy and clean OK dismissal; no file-path alert. |
| V1-disconnected | iOS video, no prepared connection; null refresh; ready=false. | Guard: reconnect/open-video-again advice. | iOS 402×874 × light; current-head **recording**. | Disconnect after load or labelled NativeVideoPreview harness providing None connection and null refresh. Existing capture uses harness. | [after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/767dd5260610d935/V1-ios-disconnected-native-harness-20261110T0909.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/6e763abad9a5702d/V1-ios-disconnected-native-harness-20261110T0909.mp4) | covered — distinct disconnected copy, clean dismissal; no physical network-drop claim. |
| V2 | iOS first mint succeeds, native presentVideo fails after ready, background remint rejects too. | Guard: one playback/save-or-share guidance alert; reauthorization rejection swallowed. | iOS 402×874 × light; current-head **recording**; trigger → alert → OK → thread. | Labelled harness: actual VideoPreviewModal, controlled successful first mint, failing native presentVideo, rejected second mint. Normal UI/RPC cannot deterministically force this double failure. | [after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c64c04c568ff834a/V2-ios-native-failure-harness-final-20261110T091602Z.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d255c4c733e4b7b5/V2-ios-native-failure-harness-final-20261110T091602Z.mp4) | covered — proper post-ready copy, no duplicate alert/player; last decoded frame confirms OK returned to thread. Harness bypasses AVKit playback itself, as labelled. |
| D1 | Attachment fresh URL mint fails; remote fresh-URL atom catches rejection; restore bytes and retry. | Guard: inline File unavailable + reconnect + Try again, then real contents. | iOS 402×874 × light; current-head **recording**; navigate → error → restore → Try again → recovered text → Back. | Upload disposable evidence.txt via app/RPC; move its disposable bytes out before ThreadAttachment navigation, restore before retry. Use actual attachmentId/name/mimeType/sizeBytes/environmentId. | [after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/19acd15a512d5020/D1-ios-light-after-20261110T0905.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e432ce3d03526e6f/D1-ios-light-after-20261110T0905.mp4)<br>[recovered contents](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/110564e897d7e718/D1-ios-light-after-20261110T0905-recovered.png) | covered — actual inline error followed by synthetic text `Retry restored these bytes.`; exact tail returns to thread. |
| D2 | Attachment needs text; signed URL authorizedAt older than five minutes (or null), re-mint rejects. | Guard: reader catch keeps generic reconnect inline error; never renders stale contents. | iOS 402×874 × light; current-head **recording (time-dependent)**; navigate → stale authorization remint error → Back. | Actual ThreadAttachment reader with labelled hook harness providing a fixed six-minute-old URL, then typed failure in standalone refresh; real alternative enter Source mode after authorization ages. No delay is needed in a harness. | [after still](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/2256024670543527/D2-ios-stale-text-harness-valid-20261110T090826Z.png) · [after recording](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d38cb769a1cf0a3a/D2-ios-stale-text-harness-valid-20261110T090826Z.mp4) | covered — correct inline reconnect body without spinner/stale text; starts and ends on Chats, matching corrected PR body. No retry recovery claim here; D1 proves the retry path. |
| U1 | source=null; route blur/unmount or explicit close during pending work; changed source identity. | Unchanged: null render, abort ignores late failures, native cleanup/leases released, keyboard dismissed. | —; **recording** if separately captured. | Actual navigation during deferred request, or labelled controllable-promise harness. | No dedicated pending-abort capture; completed exits are visible in F1/F9/F10. | missing — pending-abort variant unaffected; no additional capture required. |
| U2 | Direct uri, inline/data/generated/extracted URI; local file-backed attachment success/failure. | Unchanged: bypass remote refresh; local failure says attach again; local leases disposed. | —; **recording** if separately captured. | Composer draft/question/review strip, Mermaid expansion, ZIP entry, direct Markdown URL; inaccessible local file for failure. | No dedicated local/direct capture. | missing — bypasses changed remote failure contract; no additional capture required. |
| U3 | Successful PDF/document; viewer-native error; custom/default callback; supplied/synthesized PDF actionsSource. | Unchanged: iOS Quick Look; Android PDF modal or opening card; Cancel/Save or share actions; share success/failure. | —; **recording** if separately captured. | Present PDF/document through chat/Files/attachment/ZIP; unsupported native format; save/share from menu. | No dedicated native/share capture. | missing — unrelated successful/native error branches unaffected; F8 covers changed pre-viewer rejection. |
| U4 | Attachment pending/success; no environment/resource; local vs remote; empty/truncated text; source/Markdown/table/HTML/audio/archive/native/unsupported. | Unchanged: loading vs content vs error, Partial file notice, rendered/source toggle, wrap/copy/share/retry controls; Range/Cache-Control variants affect reads. | —; **recording** if separately captured. | ThreadAttachment/NewTaskAttachment with TXT/MD/CSV/TSV/HTML/audio/ZIP, zero bytes and >1 MiB; source mode and optional environment. | D1 shows representative remote text success, but not this full unchanged matrix. | missing — remaining modes and empty/loading/count variants unaffected; no additional capture required. |
| U5 | Video source=null; direct/local/normal remote success; audio ownership, pause/play, close; Android playback states. | Unchanged: local lease lifecycle, native presentation/audio handoff, Android player/error/actions. Android uses unchanged useRefreshAssetUrl hook. | —; **recording** if separately captured. | Draft local video, direct URI, existing remote clip; Android video/open/play/close. | No dedicated playback success capture. | missing — no new capture required; V1/V2 cover changed standalone-call guards on iOS. |
| U6 | Gallery []→selected singleton, 1 vs 3, selected first/middle, duplicate/nonimage filtering, all-success delayed URLs; metadata name/MIME/sourceIdentifier/srcFragment/actionsSource. | Unchanged: frozen image-only group; iOS progressive updates, Android waits for all; zoom/page/action header. Metadata affects successful viewers, not new failure body. | —; **recording** if separately captured. | Actual gallery or labelled deferred-URL harness; duplicates/nonimages and selected item absent from group; zoom/page while next URL arrives. | F9 covers failure group and page transitions; no dedicated all-success/deferred matrix. | missing — unaffected variants; no additional capture required. |

Seeding handoff

Reuse an isolated environment and the existing synthetic project/thread. The provided reproduction archive seeds via `project add`, free `devReplay` showcase turns and app-created attachments; inspect/adapt its paths before use. ThreadFeed is the minimal representative for default-alert failures. Three image embeds add the separate platform sibling behavior. Prefer normal app navigation; actual attachment route is `ThreadAttachment`, taking environmentId/threadId/attachmentId/name/mimeType/sizeBytes and optional draftKey. `NewTaskAttachment` takes the draft equivalent. Workspace file routes are `ThreadFile` and `NewTaskFile`; no separate capture is needed for their already-resolved URI paths.

Settings app appearance and OS appearance must agree; device/native text settings supply stress scale. For the only requested addition, retain the existing long-path data and before/after revisions, verify native logical 360×800 and fontScale 1.5 after relaunch, and record a clear starting thread, initial alert, scroll to complete final filename, OK and settled thread. A labelled native runtime harness is acceptable; a browser mock is not. No additional paid provider turn is needed.

Coverage limits and cleanup

No tests, servers, clients, devices, leases or background services were started. No worktree was created; no source edits, pushes, PR edits or comments were made. All audit logs, downloads and derived frames remained under the designated scratch directory. Worktree cleanliness was rechecked. Initial free disk was 109 GB. The full report is uploaded before scratch removal.

FRICTION:
- tool | Python urllib returned HTTP 403 for public evidence, and Pillow was unavailable; curl and ImageMagick/ffmpeg completed download and inspection | Use curl plus installed ImageMagick/ffmpeg for evidence review on this host.

VERDICT: INSUFFICIENT F4-Android compact 360×800, dark, 1.5× text
