Files
brain-of-reese/.agents/phases/complete/170_edit_bubble_sizing/00_phase.md
T
ducoterra c4f098d0c0
Build and Push Containers / build-and-push-app (push) Successful in 1m42s
Build and Push Containers / build-and-push-db (push) Successful in 13s
phase: 170_edit_bubble_sizing
All verification complete. Final report:

**Phase 170 — final verification pass (all 3 tasks were in `complete/`):**

**Verified implemented:**
- Editor renders inside the bubble: `bubble.replaceChildren(form)`, `display:none` rule deleted, `form.style.minWidth` seeded with pre-edit width, `restoreBubble()` (markdown + image re-prepend) before `closeEditor()` on Cancel/Escape, `closeEditor()` reordered before submit re-render
- Label: `editSaveBtn.textContent = "Send"` (app.js L1241)

**Gates (all 100% green):**
- `uv run pytest` → 4066 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 99% on `app/` (gate >90%)
- `uv run ruff check . && uv run pyright` → clean (0 errors, 0 warnings)
- `uv run pytest tests/e2e/test_edit_bubble_sizing.py -v --no-cov` (in isolation) → 6/6 passed
- Regression set in isolation: `test_edit_and_resend.py` 7✓, `test_retry_answer.py` 4✓, `test_failed_turn_retry.py` 4✓, `test_chat_persistence.py` 4✓

**Completion criteria:** in-bubble editor + width pin ✅ · "Send" label + cancel restore + trim re-ask + "Question too long" driver case ✅ · suite/coverage/lint ✅ · E2E + regression isolation ✅ · phase dir moved to `complete/` ✅ (atomic commit is the harness's to make — changes left in the working tree per protocol)

**Documentation:** README.md reviewed in full — edit section already aligned to phase 170 (in-bubble editor + "Send"), pages table matches actual routes, all 29 `.env.example` vars documented, setup commands accurate; AGENTS.md reviewed — all referenced files/commands exist. Both verified consistent — no changes needed.

**Notable decisions:** none — verification pass only, no code changes required.

**Next pending phase:** `171_admin_settings_layout`
2026-10-10 03:25:40 -04:00

8.9 KiB
Raw Blame History

Phase 170 — Edit bubble: the editor lives inside the user bubble, "Send" re-asks

Source: TODO.md L3 — "the edit bubble is tiny compared to the message bubble. When you click "edit" it collapses down to this super small text field. It should stay the same size as the user message bubble. Also the save button should read "send" rather than "save" because you're not saving it you're re-sending the message." Story: n/a (TODO-derived, owner roadmap confirmation 2026-10-10) Context: frontend/assets/app.js — openEditor(wrap, toggleBtn) (L1210–1357: label + textarea[rows=3][maxlength=4000] prefilled through .value, editSaveBtn "Save" L1238 + cancel, closeEditor() L1252 (form.remove + .is-editing remove + focus), the submit handler L1265–1345 (in-flight guard, empty guard, replace-in-place, tail truncation + DOM drop, lastBrainWrap = null, bubble.innerHTML = renderMarkdown(text) + the .msg-image re-prepend L1322–1325, saveConversation() + persistConversation(), markLastRetryable(), closeEditor() L1337, runTurn(text, { reask: true })), the open-tail L1355–1357 (classList.add("is-editing") + body.appendChild(form) + ta.focus())), appendEditButton / markLastEditable (L793–871, the phase-159 single-target discipline), tests/unit/test_frontend_edit_message.py (house source pins: editSaveBtn.textContent = "Save" L348; the .is-editing add L365 / remove L387 / ordering L372; the body.appendChild(form) pin near L699), tests/e2e/test_edit_and_resend.py (the phase-159 suite: page.click(".edit-form .edit-save") L337 — class-based, so the label rename touches only its docstrings; the composer's #send-label "Send" pins L312/L533 are a different control), frontend/assets/styles.css — .msg.user .bubble.is-editing { display: none; } (L1411), .edit-form (L1412–1420 — its own --surface box + border + padding), .edit-form label (L1421–1425), .edit-form textarea (L1426–1436, resize: vertical, min-height: 2.6rem), .edit-form-actions (L1437), .edit-save (L1438–1455, incl. :disabled { opacity: 0.6; cursor: wait; } L1454), .edit-cancel (L1456–1469), .edit-error (L1470–1479), .bubble (L501–508), .msg.user .bubble (L592–596 — the --brand fill + --bg text).

Objective

Clicking Edit no longer swaps the user bubble for a shrunken form: the editor lives inside the bubble itself (inheriting its box, background, and padding, so it keeps the bubble's dimensions) and the submit button reads Send — the action re-sends the question, it does not save a note. Everything else of the phase-159 flow (prefill, empty guard, tail truncation, the reask re-ask, one-editor-at-a-time, image survival, the in-flight guards) stays behavior-identical.

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

  • The form IS the bubble's content (LOCKED A1). openEditor stops appending the form as a sibling of .bubble in .msg-body and stops hiding the bubble. It keeps the .msg-image child in a closure variable, keeps bubble.classList.add("is-editing") (L1355 — the state class survives, the unit pins keep it), and replaces body.appendChild(form) (L1356) with the form appended into the bubble, the rendered markdown cleared first. The bubble keeps its own --brand fill, border, padding, and width — "the same size as the user message bubble" holds by construction (the box IS the box), with zero measurement/matching code. .msg.user .bubble.is-editing { display: none; } (styles.css L1411) is deleted — the bubble stays visible while the editor is open.
  • The form is transparent. .edit-form drops its own background / border / padding (the bubble provides them) and keeps display:flex; flex-direction:column; gap: 0.5rem. The label and the action text take the bubble's --bg-family ink (contrast documented in a comment — the B5 text-state rule); the textarea keeps its own readable --surface fill + --ink text (an edit field on a --brand bubble) and gains width: 100%. rows=3 and maxlength=4000 are untouched — the outer box is the bubble's, the textarea is the inner field.
  • Cancel = restore the markdown. closeEditor() keeps exactly three jobs: form.remove(), bubble.classList.remove("is-editing"), toggleBtn.focus() — no content change. The cancel branch (Escape + the Cancel button) gains a restore step: bubble.innerHTML = renderMarkdown(conversation[editIdx].text) + re-prepend the saved image (the mirror of the submit re-render L1322–1325), then closeEditor().
  • Submit ordering. The submit handler currently re-renders (bubble.innerHTML = renderMarkdown(text) L1322) before closeEditor() (L1337). With the form inside the bubble, closeEditor() (which detaches the form) must run before the innerHTML re-render; the rest of the handler — truncation, lastBrainWrap = null / leavePartialIndex reset, persistence, markLastRetryable(), the in-flight re-check, runTurn(text, { reask: true }) — is byte-untouched.
  • Label (LOCKED A2). editSaveBtn.textContent = "Send" (app.js L1238) — matching the composer's #send-label "Send" and the "Cancel" sibling. The phase-168 "Save settings" button is a different control and keeps its name.
  • NOT touched: appendEditButton / markLastEditable, runTurn, retryLastTurn, handleSend, rememberBrainTurn, the record-truncation + persistence path, MAX_QUESTION_CHARS / updateCharCount, the composer (the unsent draft survives the edit), the SSE contract (A15), the shared-chat page, app/ server code (A10 stateless holds).

Dependencies

  • 159_edit_and_resend_message (complete) — the inline editor + re-ask flow this phase re-houses and re-labels.
  • 166_hide_empty_answer_bubble (complete) — .msg .bubble:empty { display: none } (styles.css L524): while the editor is open the bubble holds the form (never :empty), and on cancel the re-rendered markdown is never empty (min_length=1 pinned client- and server-side) — no interaction, the pin stays green.

Tasks

  1. 01_editor_inside_bubble.md — openEditor builds the form inside .bubble (transparent form, display:none deleted), the cancel re-render, the submit ordering; the unit pins updated.
  2. 02_save_label_send.md — the submit label "Save" → "Send" (app.js L1238), the unit pin, the phase-159 E2E docstring alignment.
  3. 03_e2e_edit_bubble.md — tests/e2e/test_edit_bubble_sizing.py (the phase's dedicated Playwright suite, AGENTS rule 4), the regression set, the full gates, the atomic commit.

Testing & Quality

  • Unit: tests/unit/test_frontend_edit_message.py (house source pins, updated in tasks 01/02): the form is appended to the bubble (not .msg-body), the display:none rule's absence pinned in the CSS, .is-editing still added (L365) / removed (L387), the cancel restore (markdown re-render + image re-prepend) and the submit ordering (form detached before the re-render), editSaveBtn.textContent = "Send", and runTurn / retryLastTurn / handleSend byte-unchanged.
  • E2E (AGENTS rule 4, task 03): tests/e2e/test_edit_bubble_sizing.py, run in isolation — the size/label contract plus the regression flows.
  • Coverage: >90% on app/ (this phase changes no app/ code — the gate must stay green, not drop).
  • Lint/types: uv run ruff check . && uv run pyright.

Completion Criteria

  • Clicking Edit on the last user bubble shows the editor inside the bubble: the bubble is visible (computed display ≠ none), keeps its --brand fill, and its box width is unchanged by the edit (the E2E bounding-box pin).
  • The submit button reads "Send"; Cancel / Escape restore the rendered question; trim + Send re-asks with the truncated shape and the "Question too long" driver case still resolves.
  • uv run pytest green, app/ coverage >90% (uv run pytest --cov=app --cov-report=term-missing), ruff + pyright clean.
  • uv run pytest tests/e2e/test_edit_bubble_sizing.py -v --no-cov green in isolation; regression set green in isolation: test_edit_and_resend.py, test_retry_answer.py, test_failed_turn_retry.py, test_chat_persistence.py.
  • One atomic --no-gpg-sign Conventional Commit; the phase dir moves to .agents/phases/complete/.

Locked decisions (owner-confirmed 2026-10-10, roadmap confirmation)

  • A1 — the editor lives inside the bubble: "stay the same size as the user message bubble" = the editor occupies the bubble element itself (inherits its box/styling), not a pixel-matched sibling form. rows=3 + maxlength=4000 stay.
  • A2 — the label is "Send" (capital S, matching the composer's #send-label "Send" and "Cancel"); only the edit-form label changes — the phase-168 "Save settings" button keeps its name.

Commit

git add frontend/ tests/ .agents/phases/ && git commit --no-gpg-sign -m "feat(chat): edit the last question inside its bubble and re-send it with Send"