# fix\(web\): pasted images show their upload tile instantly · PR \#23832 · pingdotgg/t3code

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

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

Visibility: public

State: merged

Source repository: pingdotgg/t3code

Source branch: t3/instant-image-paste-feedback

Target branch: main

Head commit: 3b5b33831afbfd8ae4eb875f097dc26bac4743bc

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

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

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

## Description

---

## Problem

Pasting an image showed nothing for a noticeable beat before its tile appeared at 0%, worst on remote environments. Pasted images were downscaled and re-encoded (every retina screenshot since #19364) before they joined the draft, so neither the tile nor its upload progress existed until that finished.

## Change

**Pasted images get a tile at 0% immediately, showing the original image, and keep it through compression without a flash.**

- A placeholder tile appears the moment you paste, using the source file's thumbnail and the same markup and key as the final tile.
- When compression finishes, the compressed file inherits the source's thumbnail and replaces the placeholder in the same render.
- Thumbnails that have already settled render on first paint, so tiles never flash back to the file name.
- While an image is preparing, the composer stays expanded, so the placeholder can't be hidden by the resting layout while it's blocking send.
- The placeholders replace the non-reactive `pendingImageCompressionsRef` counter, so attachment slot reservations and the "Still compressing" send guard now read the same state the strip renders.

**Important files:**

`apps/web/src/components/chat/preparingComposerImages.ts`
  - New per-draft store of images still compressing. Feeds the strip and the slot and send guards.

`apps/web/src/components/chat/useComposerAttachmentIntake.ts`
  - Registers placeholders before compressing, reuses their ids for the draft images, and removes them in the same tick as the draft insert.

`apps/web/src/components/chat/ComposerAttachmentStrip.tsx`
  - Renders placeholder tiles that mirror the image tile, including the 0% label.

`apps/web/src/lib/imageCompression.ts`
  - Adds a synchronous settled-thumbnail read and thumbnail inheritance from source to compressed file. Skips thumbnailing sources over the compressible size cap.

`apps/web/src/components/chat/ComposerImageThumbnail.tsx`
  - Paints a settled thumbnail on first render instead of the fallback.

`apps/web/src/components/chat/ChatComposer.tsx`, `useComposerContextImport.ts`
  - Swap the old counter ref for the store, and keep the composer expanded while images are preparing.

Paste, drag and drop, the attach button, and question attachments all share this intake path. Mobile uploads at send time through its own flow and is unchanged.

## Scope and approval

Maintainer-requested fix for an obvious responsiveness bug in the existing paste flow. No new behavior beyond showing the in-progress image earlier.

## Verification

- `vp run check:changed`: format, lint, knip, typecheck, and tests all pass.
- Manually verified by @bmdavis419 on a tailnet dev server against a remote environment: pasting large retina screenshots shows the tile at 0% instantly with the image, and the swap to the compressed image has no visible flash.
- No recording attached yet.

Opus 5.5 via Claude Code in T3 Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


## Takeover

Kept @bmdavis419's per-draft preparing state and thumbnail reuse; unified preparing/ready tile rendering and added removal without resurrection.
Cancelled failures no longer replace newer thread errors. Contributor verification above is historical; takeover proof is for [3b5b338](https://github.com/pingdotgg/t3code/commit/3b5b33831afbfd8ae4eb875f097dc26bac4743bc).
Initial thumbnail decoding can show the filename fallback; once decoded, the recorded ready swap preserves the thumbnail node without another fallback.

## Evidence

Before [23bd753](https://github.com/pingdotgg/t3code/commit/23bd753806eb9db5a315fbf9ac6563d355f20b3a) → After [3b5b338](https://github.com/pingdotgg/t3code/commit/3b5b33831afbfd8ae4eb875f097dc26bac4743bc), fresh isolated loopback app, synthetic clipboard/files, native X11 recordings. Controlled encode-result holds are disclosed separately from natural runs.

| Matrix row | Result |
| --- | --- |
| One image / slow retina | Source tile during preparation; same thumbnail node through ready swap. |
| Several / overlap | Four resolve once; completion-based reordering disclosed. |
| Attachment limit | 99 ready + 1 preparing reserves 100; extras rejected; removal frees capacity. |
| Send while compressing | Submit button and Enter show compression guard; no post-release acceptance claim. |
| Cancel/remove | No resurrection; ready removal and retry work. |
| Compression failure | Bad tile clears, good sibling survives; cancelled failure preserves newer error. |
| Non-image paste | Text/HTML/file/folded text/literal handling retained, no ghost image tile. |
| Additional entrypoints | New-draft navigation, picker and drop recorded. |

| Viewport | Before | After |
| --- | --- | --- |
| 1440 light | ![before-1440.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/433228fdf31a3c26/before-1440.png) | ![after-1440.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/71831a0327b13d2c/after-1440.png) |
| 390 dark (responsive web) | ![before-390.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/da50e5c36b41587c/before-390.png) | ![after-390.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ad08e8079183efbd/after-390.png) |

![abs-diff-1440.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c51713ff93567bbd/abs-diff-1440.png)
![highlighted-overlay-1440.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/afba798d5e89d18b/highlighted-overlay-1440.png)
![abs-diff-390.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/8e39397ef72c3997/abs-diff-390.png)
![highlighted-overlay-390.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c3a3ce68b08d9c08/highlighted-overlay-390.png)
![before-after-390.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/a9a1caa8a78d6b2d/before-after-390.png)
[matrix.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/5f5978fb2eea52d0/matrix.md)
[proof-audit-2.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/4a1df09bcd4f83b6/proof-audit-2.md)
[evidence-23832.tar.gz](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/0bb142e8803c1906/evidence-23832.tar.gz)
[SHA256SUMS](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/31b0cfbd3b391966/SHA256SUMS)
[verification.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f598d50ba3034450/verification.md)

Before recording: [barrier.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/a0204cf4aa0cd788/barrier.mp4)
After recording: [barrier.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/0ade548866b73549/barrier.mp4)

SHA256 bundle: 24658bdbf2d00c6d87987913b153f77d836648d4e82c79c018a958c0f37e5715. Per-file hashes are in the linked manifest. Upload Markdown above is the verbatim output of `ghdrop upload -m`.

<details><summary>Verification log</summary>

Two fresh full-diff review rounds, Codex GPT-6.1 Sol + Claude Opus 5.5: all MERGE. A bot cancellation-error finding between rounds was confirmed and fixed; both second-round reviewers read the entire final diff. No unresolved code blockers. Main merged by merge commit; original branch and contributor commits retained.
Tests: existing imageCompression 18→19 (new test fails with inheritance disabled); 22 focused tests pass on the final head. Web typecheck and scoped lint/format pass on final code. CI: 31 reported checks green, none pending at final gate; current native conflict check is mergeable.
What else changed: preparing row intentionally expands composer and shifts centered heading. Idle/ready layouts otherwise match; minor elapsed-label/thumbnail pixel differences and overlap ordering disclosed in matrix. No upload-complete, speed, GPU or peak-memory claim.
Unverified: accepted post-release sends, existing stopped-thread paths, pending questions, discard-all cancellation, OS-native clipboard/Electron shell, native mobile, remote/relay/tunnel. Narrow evidence is web, not native mobile. Proof audit: SUFFICIENT (all eight rows; independent rebuild passed).
Model/harness: tests/implementation Claude Opus 5.5 via Claude Code; reviewers GPT-6.1 Sol via Codex and Claude Opus 5.5 via Claude Code; lead/evidence/proof GPT-6.1 Sol via Codex in T3 Code.

</details>

