# feat\(mobile\): queue a message while its attachment is still uploading · PR \#10404 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/10404)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: merged

Source repository: pingdotgg/t3code

Source branch: t3code/queue-with-uploads

Target branch: main

Head commit: 071c390a9a28712879f1f8e0b944cb32adbe27da

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/10404)

[Commits](https://git.cafe/pingdotgg/t3code/pull/10404/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/10404/diffs?format=markdown)

## Description

---

## 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%](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/a1847ef815dea2c7/queue-upload-before-after.png)

### 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](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/507407c7affee966/queue-upload-queued.png)

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](https://claude.com/claude-code)

