feat(mobile): queue a message while its attachment is still uploading#10404

merged1
main
t3code/queue-with-uploads
openedSep 6, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 3d ago

Problem

A connected composer disabled Send until every attachment had finished uploading. On a slow link that meant sitting and watching a progress ring, even though the offline path already knew how to queue the message and let the outbox drain finish the upload later. Reported from the field: a video at 60% with a greyed-out button.

Fix

composerAttachmentUploadBlockReason now only blocks on a failed upload, since the outbox drain would hit the same failure and the user has to retry or remove the file first. An upload still in flight no longer blocks: the message queues, and the drain's prepareQueuedMessageAttachments reuses the finished upload (or re-sends the local bytes if the pending upload expired).

  • New-task screen: when connected but an attachment is still uploading, handleStart takes the outbox branch instead of calling createProjectThread. The button switches to the tray icon and its accessibility label reads "Queue task, sends when uploads finish". The worktree-setup shimmer no longer shows for a queued task.
  • Thread composer: every send already goes through the outbox, so removing the gate is enough; the button label now reads "Queue" while an upload is in flight, matching the offline and already-queued cases.
  • New helper composerAttachmentsStillUploading for the label logic, alongside the (now narrower) block reason.

Verification

apps/mobile: vp test run src/lib/composerAttachmentUploadQueue.test.ts (9 passed; the block-reason test now pins that in-flight does not block and failed does). vpr typecheck passes. Lint on touched files shows only pre-existing warnings (15, same as base).

Before / after โ€” new-task composer with a 45 MB video mid-upload

Before: the send button is greyed out with accessibility label "Attachment still uploading" while the video is at 25%. After: the button is enabled with the tray icon while the video is at 30%

After tapping Queue mid-upload

The task lands in the Unsent section as "Sends on reconnect". About 75 seconds later the drain delivered it; the server's projection shows the created thread, the user message with the 47 MB file attachment claimed, and the agent's reply.

The Unsent section shows the queued task with Sends on reconnect while the upload continues

Captured on an iPhone 17 Pro simulator against a disposable backend behind a 150 KB/s throttling proxy so the upload was slow enough to observe; the "before" is main with the same seeded draft.

Surfaces

  • Entry points: new-task composer (Home compose, project row, Draft rows) and the thread composer.
  • Clients: mobile only. Web has no offline outbox; server untouched.
  • Reverse state: a failed upload still blocks with the existing "Retry or remove" affordance.

Model: Claude Fable 5 ยท Harness: Claude Code in T3 Code

๐Ÿค– Generated with Claude Code

Added 1 commit

  • feat(mobile): queue a message while its attachment is still uploading071c390
Synced from GitHub

Thread transfer impact

โœ… Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+83 B (+0.6%)15.1 KiBโœ…
CodexThread snapshot wire7.0 KiB7.0 KiB+4 B (+0.1%)7.3 KiBโœ…
CodexLive turn WebSocket wire6.5 KiB6.6 KiB+79 B (+1.2%)7.8 KiBโœ…
CodexLive turn WebSocket decoded57.0 KiB57.1 KiB+88 B (+0.2%)66.4 KiBโœ…
CodexLive turn messages810+2 (+25.0%)21โœ…
ClaudeTotal thread wire13.6 KiB13.5 KiBโˆ’75 B (โˆ’0.5%)15.1 KiBโœ…
ClaudeThread snapshot wire7.0 KiB7.0 KiB0 B (0.0%)7.3 KiBโœ…
ClaudeLive turn WebSocket wire6.5 KiB6.5 KiBโˆ’75 B (โˆ’1.1%)7.8 KiBโœ…
ClaudeLive turn WebSocket decoded57.8 KiB57.8 KiBโˆ’88 B (โˆ’0.1%)66.4 KiBโœ…
ClaudeLive turn messages97โˆ’2 (โˆ’22.2%)21โœ…

Baseline: 7ac93e3 ยท PR result: 071c390 ยท Source CI: success

Scenario and decoded snapshot size

10 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.

  • Codex decoded thread snapshot: 113.9 KiB
  • Claude decoded thread snapshot: 114.6 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Synced from GitHub

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable โ€” This PR changes production submission behavior so messages and tasks can be queued while attachments upload, then dispatched later by the outbox. The implementation is focused and covered by targeted tests, but the new user-facing queueing workflow and altered dispatch timing merit human confirmation.

Not approved because:

  • Monthly spending limit reached (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings. You can add or adjust custom eligibility rules. Learn more.