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`
8.9 KiB
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).
openEditorstops appending the form as a sibling of.bubblein.msg-bodyand stops hiding the bubble. It keeps the.msg-imagechild in a closure variable, keepsbubble.classList.add("is-editing")(L1355 — the state class survives, the unit pins keep it), and replacesbody.appendChild(form)(L1356) with the form appended into the bubble, the rendered markdown cleared first. The bubble keeps its own--brandfill, 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-formdrops its ownbackground/border/padding(the bubble provides them) and keepsdisplay: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--surfacefill +--inktext (an edit field on a--brandbubble) and gainswidth: 100%.rows=3andmaxlength=4000are 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), thencloseEditor(). - Submit ordering. The submit handler currently re-renders (
bubble.innerHTML = renderMarkdown(text)L1322) beforecloseEditor()(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/leavePartialIndexreset, 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=1pinned client- and server-side) — no interaction, the pin stays green.
Tasks
01_editor_inside_bubble.md—openEditorbuilds the form inside.bubble(transparent form,display:nonedeleted), the cancel re-render, the submit ordering; the unit pins updated.02_save_label_send.md— the submit label "Save" → "Send" (app.js L1238), the unit pin, the phase-159 E2E docstring alignment.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), thedisplay:nonerule's absence pinned in the CSS,.is-editingstill 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", andrunTurn/retryLastTurn/handleSendbyte-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 noapp/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--brandfill, and its box width is unchanged by the edit (the E2E bounding-box pin). - The submit button reads "Send"; Cancel /
Escaperestore the rendered question; trim + Send re-asks with the truncated shape and the "Question too long" driver case still resolves. uv run pytestgreen,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-covgreen 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-signConventional 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=4000stay. - 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"