fix(mobile): read file-backed image drafts before enabling them - #9713
Conversation
Import the mobile changes from Wout Stiens' composer draft persistence work. Store new image bytes in owned files. Keep metadata in drafts, queued messages, and incoming shares. Read base64 only for old-server sends. The web changes from the source commit are not included. Original-Commit: 5a157c0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The review-comment sheet holds picked images in local state until submit copies them into the thread draft. With file-backed images, removing an attachment or dismissing the sheet dropped the state while the copied file stayed in the attachment directory. Removal and unmount now route through the reference-checked cleanup, so submitted files stay owned by the draft and abandoned ones are deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dropping the picker's base64 export also dropped the only source of provider-supported bytes for HEIC-family originals: the picker's file copy stays HEIC on both its fast and slow paths, so those photos were rejected as unsupported. The picker exports JPEG base64 again, used only as the fallback for unsupported originals and landed once in the owned attachment directory; supported formats still copy their original file and nothing base64 is persisted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
iOS always transcodes the base64 export to JPEG, but Android's quality-1 export is the raw original, so an Android HEIC pick would have shipped HEIC bytes labeled image/jpeg. The fallback now requires the JPEG magic number in the export and otherwise rejects the photo as unsupported, matching the pre-change Android behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep current inline image creation and v3 outbox writes while accepting file-backed image records and v4 queued messages. A compatible native baseline must include the readers before the new writers can ship. Keep file-backed uploads, previews, cleanup ownership, and old-server inline sends. Require a saved image to have a file URI or inline bytes. Extend existing restore and lease tests to file-backed images. Created with GPT-6 Astra (preview) in Codex.
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a contained mobile attachment-lifecycle fix that adds file-backed image reading, URI rebasing, retention, and cancellation handling while preserving existing inline writers and outbox writes. The compatibility paths and cleanup behavior are covered by focused tests, with no production-default, infrastructure, security, billing, or static-analysis changes. You can add or adjust custom eligibility rules. Learn more. |
Keep an image's declared MIME type even if its filename has a video extension. Recheck cancellation after reading file-backed images for an old-server inline send. Both cases failed in focused tests before the corrections. Created with GPT-6 Astra (preview) in Codex.
Move legacy image reads into attachmentUpload. Hold the existing file lease through each read and release it in finally. A removed draft can then clean up its file after the read, without breaking the send. Return abandoned if a canceled native read rejects. Add read-barrier coverage through the real ownership cleanup in the existing tests. Created with GPT-6 Astra (preview) in Codex.
## What's Changed * perf(server): use one query for buffered provider events by @t3dotgg in pingdotgg/t3code#9706 * perf(relay): avoid repeated activity decoding by @t3dotgg in pingdotgg/t3code#9708 * perf(web): stop continuous chat status animations by @t3dotgg in pingdotgg/t3code#9709 * fix(mobile): preserve saved work after storage read failures by @t3dotgg in pingdotgg/t3code#9710 * perf(web): stop replaying terminal buffers on rollover by @t3dotgg in pingdotgg/t3code#9707 * feat(web): preview pull request links by @maria-rcks in pingdotgg/t3code#9631 * perf(client): reduce thread-list update work by @t3dotgg in pingdotgg/t3code#9716 * fix(server): settle inactive threads with open PRs by @Gigioxx in pingdotgg/t3code#9610 * fix(server): bound slow-client event buffers by @t3dotgg in pingdotgg/t3code#9715 * test(server): allow either valid file-search match by @t3dotgg in pingdotgg/t3code#9720 * fix(web): match provider settings layout for disconnected devices by @flamboh in pingdotgg/t3code#9619 * fix(web): keep the slash menu above the composer when vertical space is short by @Mnigos in pingdotgg/t3code#9625 * fix(mobile): remove provider setup by @juliusmarminge in pingdotgg/t3code#9721 * perf(web): stop rendering hidden terminals by @t3dotgg in pingdotgg/t3code#9718 * fix(mobile): read file-backed image drafts before enabling them by @t3dotgg in pingdotgg/t3code#9713 * perf(server): replay only the selected thread by @t3dotgg in pingdotgg/t3code#9726 * fix(web): mute composer helper text by @jakeleventhal in pingdotgg/t3code#9654 * feat(web): unpin threads from the sidebar multi-select menu by @gsimone in pingdotgg/t3code#9651 * perf(web): reuse timeline rows while text streams by @t3dotgg in pingdotgg/t3code#9725 * fix(relay): bound stalled push requests by @t3dotgg in pingdotgg/t3code#9734 * perf(server): stop caching unused OpenCode tool parts by @t3dotgg in pingdotgg/t3code#9738 * fix(web): fold single trailing activity by @maria-rcks in pingdotgg/t3code#9739 * fix(web): show project settings for new threads by @maria-rcks in pingdotgg/t3code#9743 * perf(mobile): bound the parsed review cache by @t3dotgg in pingdotgg/t3code#9749 **Full Changelog**: pingdotgg/t3code@v0.0.39-nightly.20260904.1279...v0.0.39-nightly.20260904.1280 Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.39-nightly.20260904.1280
An OTA rollback can run older JavaScript against the same saved drafts. File-backed image writers need compatible readers in the native baseline first.
The app can now load, preview, and send inline or file-backed image drafts. Cleanup retains their files through queued messages, signed-out archives, and active previews. New attachments still use inline bytes, and outbox writes remain v3.
The later file-backed writer change must stay held until a compatible native baseline includes these readers and the storage guards from PR #9710. No native or dependency changes here.
Based on Wout Stiens' mobile work in PR #9049. Original Git authorship and co-author credit are preserved. Reader-only corrections are separate commits.
Verified with 240 focused tests, mobile typecheck, and targeted lint. Real-source checks confirm that old decoders can read newly saved inline drafts and v3 queues, while new readers accept future file-backed drafts and v4 queues. No device or native build ran.
Created with GPT-6 Astra (preview) in Codex.
Note
Medium Risk
Changes composer attachment persistence validation, send/upload paths, and file retention around queued messages and previews; regressions could drop images or fail sends after iOS container moves, though coverage is extensive.
Overview
Prepares the mobile composer to read file-backed image drafts (optional
fileUriplus optionaldataUrl) before writers switch away from inline bytes. Image schema now requires at least one of those sources; persisted images with onlypreviewUrifail decode.Preview and send paths resolve owned attachment paths against the current iOS document container, lazily read base64 for legacy inline
startTurnwhen uploads are off, and upload from disk when uploads are on—without staging cache files for file-backed images. Thumbnails and file preview use retention leases and attachment-based preview sources for file-backed images.Lifecycle treats any attachment with
fileUrilike other owned files for cleanup, upload-queue retention, and share MIME handling. Thread start and outbox drain pass onlyprepareTurnAttachmentswire payloads, not raw drafts. Outbox decoding accepts schema v4 file-backed images while writes stay on v3.Reviewed by Cursor Bugbot for commit 572bdfb. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Fix file-backed image drafts by reading files before enabling them in mobile composer
FileBackedComposerAttachmenttype andisFileBackedComposerAttachmentguard so composer images can store bytes in the app-owned attachment directory rather than only inlinecomposerImageAttachmentDataUrllazily reads file-backed image bytes, rebasing the URI against the current iOS document container, and throws an explicit attachment-unavailable error when neither inline nor file data existsuploadFileBytesuploads file-backed images directly from their owned file without base64 staging or temp-file cleanup, while legacy inline images still use a temp upload fileprepareTurnAttachmentsmakes legacy image conversion async and cancellation-aware; a canceled read now returns an abandoned result instead of surfacing an erroruse-composer-drafts.tsandcomposer-attachment-uploads.tsnow cover any attachment with afileUri, retaining file-backed images referenced by drafts, queued messages, or active leasesQueuedThreadMessageSchemaaccepts version 4 records so the outbox reader can decode queued messages containing file-backed imagesDraftComposerImageAttachmentSchemanow requires at least one offileUriordataUrl; persisted records with neither field will fail decoding. Reviewers should checkcomposer-image-schema.tsand outbox migration paths for older drafts.Macroscope summarized 572bdfb.