# Phase 49 — Retry the Last Answer (Redo) **Source:** `TODO.md` L4 — "Need a retry button to retry the last answer, like a redo button" **Story:** n/a (TODO-derived — owner roadmap confirmation 2026-08-29) **Context:** `frontend/assets/app.js` — `handleSend` owns the whole turn (user-bubble append + conversation push → fetch/SSE → save points), `appendTuneButton` is the house pattern for a per-bubble meta-row action (reuses the `.msg-meta` row, the `role=list` → `role=listitem` rule, the admin gate), `conversation`/`rememberBrainTurn` hold the persisted records, and phase 48's Stop button means the last brain bubble can be a **stopped partial** — the natural retry candidate. ## Objective A **Retry** button on the last brain answer re-asks the preceding question in place — the old answer is removed (DOM + persisted record), the fresh answer streams into its place, and the conversation never duplicates the question. ## Dependencies - `48_stop_generation` (todo) — the button/state-machine work it builds on (the meta-row button family, the in-flight guard) and the stopped partials it makes retryable. - `14_chat_persistence` (complete) — the conversation records the retry edits in place. ## Tasks 1. `01_retry_button.md` — the `runTurn` extraction + the Retry button + redo-in-place semantics + CSS. 2. `02_e2e_retry_answer.md` — the story Playwright suite + regressions + commit. ## Testing & Quality - Frontend source pins (house pattern): the `runTurn(text, {reask})` extraction (reask skips the user append/push), last-bubble-only Retry management, the redo-in-place record edit, the in-flight no-op. - Coverage: **>90%** on `app/` (unchanged — frontend-only phase). - E2E (mandatory, A16): `tests/e2e/test_retry_answer.py`, run in isolation. ## Completion Criteria - [ ] The last brain bubble (completed, deflected, empty-fallback, or stopped partial — admin and anonymous alike) carries a Retry button; earlier brain bubbles do not. - [ ] Clicking Retry removes the old answer (DOM + `bor.chat.v1` record), re-sends the preceding question without duplicating it, and streams the fresh answer into its place; the new answer carries the Retry button (it is the new last). - [ ] While a turn is in flight, Retry does nothing (no double turn). - [ ] `uv run pytest` green; coverage TOTAL unchanged (>90%). - [ ] `uv run pytest tests/e2e/test_retry_answer.py -v --no-cov` green in isolation (DB up). - [ ] Regression E2E suites green in isolation: `test_chat_rag.py`, `test_chat_persistence.py`, `test_stop_generation.py`. - [ ] `uv run ruff check . && uv run pyright` clean. - [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`. ## Locked decisions - **Owner-locked (2026-08-29, roadmap confirmation):** redo-in-place (the old answer is replaced, the question is not duplicated); only the **last** brain bubble is retryable; Retry is available to **all** visitors (chat is public — unlike Tune, which is admin-only); inert while a turn is in flight. - **A10 untouched** — retry is a fresh `POST /api/chat` (the API stays stateless). - **A16/A17 honoured** — one story E2E suite, one atomic commit. ## Commit ```bash git add -A .agent/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(chat): retry the last answer — redo-in-place Retry button on the latest brain bubble" ```