Files
brain-of-reese/.agents/phases/complete/132_image_upload_feedback/00_phase.md
T
ducoterra 4cb85e5baa
Build and Push Containers / build-and-push-app (push) Successful in 2m25s
Build and Push Containers / build-and-push-db (push) Successful in 17s
phase: 132_image_upload_feedback
All verification complete. Final report:

**Phase 132 (image upload feedback) — final verification pass: 100% green**

- **Verified** all 3 tasks' implementations in the working tree: `index.html` status part (hidden-by-default, arc + "Uploading image…", pinned order), `app.js` `enterUploadState`/`exitUploadState` (entered after `attachUpload` guard pre-await, exited on BOTH resolution paths, `setUiState` untouched, idle "Send" restore byte-identical), `styles.css` `.is-uploading` rules (house `spin` keyframes, reduced-motion block, existing `:disabled` Send language), unit pins, E2E suite with route-delayed upload window.
- **Completion criteria** — (1) mid-flight spinner + "Uploading image…", remove + Send disabled with "Uploading…": ✅ E2E-asserted; (2) second Send press = no second upload (request count exactly 1): ✅; (3) success clears + turn proceeds (bubble carries image); failure clears + banner + text/attachment remain: ✅; (4) `uv run pytest tests/e2e/test_image_upload_feedback.py -v --no-cov`: ✅ 3 passed (18.5 s, in isolation); (5) no behavior change in completed phases: ✅ (full suite green, A5 path-never-bytes re-pinned in E2E).
- **Full suite**: `uv run pytest --cov=app --cov-report=term-missing` → **3108 passed**, `app/` coverage **99%** (>90% gate); `uv run ruff check .` → clean; `uv run pyright` → 0 errors.
- **Documentation**: README.md fully reviewed (setup, pages, all `.env.example` vars + every default vs `app/config.py`, tools incl. phase-131 `read(offset,limit)`, image/PDF/sync/deploy sections) and AGENTS.md fully reviewed (rules, quick-reference commands, gitignore/gpg claims) — **verified consistent; no changes needed**. Phase-132 README bullet already in the working tree; CSS `:root` contrast-note corrections (15.3:1 / 8.2:1) re-verified mathematically correct (comments only).
- **Decisions**: none — all task work was already implemented and correct; no fixes required.
- **Next pending phase**: `133_pdf_screenshot_fallback`.
2026-09-28 13:45:31 -04:00

5.6 KiB
Raw Blame History

Phase 132 — Chat image upload: visible "uploading" feedback

Source: TODO.md L8 — "Uploading an image in the chat provides no feedback to the user. The user clicks the paperclick, selects and image, presses 'send' and then nothing happens. No loading bar, no 'uploading image', no indication. This results in the user spamming 'send' over and over because they think it's frozen. We need to visually show the user's image is uploading." (A5, owner-confirmed 2026-09-27: the affordance = a spinner + "Uploading image…" in the preview strip AND the Send button disabled with the label "Uploading…" — the double-spam is blocked visually, not just by the internal guard.) Story: n/a (TODO-derived phase, Protocol B append). Context: frontend/assets/app.js — handleSend (~L2700: the attachedImage branch — the attachUpload double-fire guard, await uploadAttachedImage(attachedImage.file) with NO visual state between the await's start and its resolution, then image = { path, src, alt } + runTurn), uploadAttachedImage (~L2641: the POST /api/chat-images multipart — every failure path is an out-of-turn banner + null), showAttachPreview / clearAttachedImage (~L2680: the strip's reveal/clear — static markup, only textContent/src move), setUiState (the turn-state owner — NOT used for the upload: the upload is a PRE-turn operation, uiState stays idle, the turn loader must not spin during the upload); frontend/index.html L280–322 — the static attach markup (#attach-preview with img + .attach-preview-name + #attach-remove; #send-btn with #send-label); frontend/assets/styles.css — the .attach-preview* / .send-btn rules (the existing :disabled styles); tests/unit/test_frontend_sync_upload.py or a sibling — the pinned frontend JS unit pattern.

Objective

Between the user pressing Send and the upload resolving, the UI proves the image is moving: the preview strip shows a spinner + "Uploading image…" (the remove button disabled), and the Send button is disabled with the label "Uploading…". When the upload resolves — success proceeds to the turn exactly as today; failure clears the state and keeps the existing banner + the typed text + the attachment for the retry. No frozen-looking gap, no visible double-send.

Dependencies

  • 131_read_line_ranges (todo) — pipeline order only; code dependencies are the completed phase-123 attach/upload contract.

Design (shared by all tasks — the executor reads this, not the chat)

  • State scope (task 01): the uploading state is the attachedImage branch of handleSend — from just after the attachUpload = true guard until the attachUpload = false line (success AND failure paths — the helper never throws). setUiState is NOT touched (the upload is pre-turn; uiState stays idle; the turn's own thinking/streaming feedback starts only when runTurn does). The attachUpload guard stays exactly as is (the visual state reinforces it — the disabled Send is the user-facing half).
  • Preview strip (task 01): the static markup gains ONE status element inside #attach-preview: <span class="attach-preview-status" hidden><svg spinner…></svg><span class="attach-preview-status-text">Uploading image…</span></span>. The JS sets attachPreview.classList.add("is-uploading") + un-hides the status + attachRemove.disabled = true on entry; on exit it removes the class + hides the status + attachRemove.disabled = false. The thumbnail + filename stay visible (the user watches THEIR image uploading).
  • Send button (task 01): sendBtn.disabled = true + sendLabel.textContent = "Uploading…" on entry; on exit (both paths) sendBtn.disabled = false + sendLabel.textContent = "Send" — restored BEFORE the success path calls runTurn (the turn's setUiState("thinking") then takes over the button as usual) and BEFORE the failure path returns (the banner + editable input + the still-attached image).
  • Reduced motion (task 02): the spinner animation is still under prefers-reduced-motion (the house @media (prefers-reduced-motion: reduce) block pattern).

Tasks

  1. 01_upload_state.md — the static markup + the uploading state in handleSend (enter/exit on both paths).
  2. 02_css_upload_state.md — the strip's uploading styles + the disabled Send + reduced motion.
  3. 03_e2e_upload_feedback.md — dedicated Playwright E2E, run in isolation.

Testing & Quality

  • Unit: the pinned frontend JS suite — the state transitions (the strip's class/status/remove-disabled, the Send's disabled/label) on entry, on success exit, on failure exit (the banner path).
  • E2E: one dedicated Playwright file, run in isolation, with a route-delayed upload (Playwright page.route delaying POST /api/chat-images) so the in-flight window is observable.
  • Coverage: >90% on new/modified code (frontend: unit pins + E2E; app/ untouched this phase).

Completion Criteria

  • while the upload is in flight: the strip shows the spinner + "Uploading image…", the remove button is disabled, the Send button is disabled with "Uploading…" (E2E-asserted mid-flight)
  • a second Send press during the flight does nothing (no second upload request — E2E request count)
  • success: the indicator clears and the turn proceeds exactly as pre-phase (the user bubble carries the image); failure: the indicator clears, the banner shows, the text + attachment remain
  • uv run pytest tests/e2e/test_image_upload_feedback.py -v --no-cov green in isolation
  • no behavior change in completed phases (the phase-123 upload contract, the A5 path-never-bytes rule, the out-of-turn banner)