Files
brain-of-reese/.agents/phases/complete/49_retry_answer/01_retry_button.md
T
ducoterra dbf2af26c6 refactor(agents): migrate .agent/ planning tree to .agents/
Standardize on the .agents/ directory (shared with project skills):
phases/, user_stories/, reports/, screenshots/, validate.sh, and
phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves
history; runtime artifacts move alongside).

Updates every reference in AGENTS.md, README.md, .gitignore, app
docstrings, and test story headers. Historical KB content in data/
and the runtime pipeline.log transcript are left untouched.
2026-09-05 10:57:07 -04:00

4.8 KiB

Task 01 — The Retry button (redo in place)

Phase: 49_retry_answer · Source: TODO.md:4 — "Need a retry button to retry the last answer, like a redo button" Story: n/a (TODO-derived)

Objective

handleSend is split so a question can be re-run without re-adding it, and a Retry button on the last brain bubble performs that redo: old answer gone (DOM + storage), fresh answer streaming into its place.

Work

  1. frontend/assets/app.js:
    • Extract runTurn(text, { reask = false }) from handleSend: everything from the setUiState(thinking) / armTurnTimeout point through the finally settle moves into runTurn; the reask flag skips (a) the addMessage("user", …) append and (b) the conversation.push({ who: "user", … }) + saveConversation() (the question is already in both). handleSend keeps the form-level pre-work — the !text guard, the in-flight guard that calls stopTurn() (phase 48), clearErrorBanner(), the input clear + autoGrow — then appends + persists the user message and calls runTurn(text, { reask }). The turn-local resets (acc, thinkingAcc, sawThinking, sawDone, toolAcc, stoppedByUser, turnAbort) stay turn-scoped exactly as phase 48 left them. No behavior drift for the normal send path: a plain send must produce byte-identical DOM/SSE/persistence behavior to today (the regression suite is the proof).
    • appendRetryButton(wrap) — the house appendTuneButton pattern for the meta row (reuse the .msg-meta row; role=list → role=listitem where the row is a list; one per bubble; an inline redo-glyph SVG + "Retry" text — the text is the accessible name). Not admin-gated (owner-locked: all visitors). Click handler retryLastTurn(wrap):
      • in-flight guard: if uiState is thinking/streaming → no-op (owner-locked).
      • find the record: the last brain record in conversation (and that wrap is the rendered wrap of that record — the button only ever sits on the last bubble, but the guard keeps a stale click harmless).
      • redo in place: pop the brain record from conversation; saveConversation() immediately (a crash between the pop and the fresh done must never resurrect the replaced answer — what the user saw, the removed answer, is what is stored; the question remains); remove wrap from the DOM; locate the preceding user record's text (the record immediately before the popped one — invariant: every brain record follows its user record); call runTurn(text, { reask: true }). No sendStatus banner; no scroll (phase-42 contract — no auto-scroll; the fresh bubble lands where the old one was).
    • Last-bubble-only management: a helper markLastRetryable() — removes any existing .retry-btn from every rendered .msg-meta, then appends the Retry button to the last brain bubble (only when it has a preceding user record — always true in practice). Call sites: on done (after appendTuneButton), on the empty-answer fallback path, on the stop finalize path (phase 48 — the stopped partial is the prime retry candidate), and once at the end of restoreConversation() (after all records are rendered). startNewChat's list reset removes everything anyway — no change there.
    • Header comment: note the Retry contract (2026-08-29, TODO.md L4).
  2. frontend/assets/styles.css — .retry-btn: the exact visual family of .tune-btn (same size/spacing/focus-visible/hover; ≥44px comfortable via the meta-row padding, as Tune has) with the redo glyph in the phase-08 palette (ink-soft → ink on hover), so the two meta actions read as a pair.
  3. frontend/index.html — no markup change (the button is JS-injected like Tune); update the messages-section comment to mention the meta-row actions (Tune — admin; Retry — everyone).
  4. Frontend source pins (house pattern, extend tests/unit/test_frontend_feedback.py or a sibling): the runTurn signature + the reask skips (no user append/push when reask); appendRetryButton's no-admin-gate + one-per-bubble; markLastRetryable's remove-then-append; the pop → save → rerun order in retryLastTurn; the in-flight no-op.
  • ASSUMPTION (owner-locked 2026-08-29): redo-in-place — the old answer is replaced (DOM + persisted record), the question is re-sent without duplication; only the last brain bubble carries the button; available to all visitors; inert while a turn is in flight.

Testing & Quality

  • Unit: source pins as above; full suite green.
  • Coverage: >90% on app/ (unchanged — frontend-only).

Completion Criteria

  • The plain-send path is byte-identical to pre-task behavior (regression: test_chat_rag.py E2E green in isolation).
  • Retry on a completed / deflected / stopped answer redoes in place (unit-pinned now, E2E in task 02).
  • uv run pytest green; uv run ruff check . && uv run pyright clean.