Remove the blanket .agent/ gitignore so the phase roadmap, user stories, reports, and PLAN.md are versioned with the code. Only runtime artifacts (.agent/phase-sessions/, .agent/pipeline.log) remain ignored. Update AGENTS.md git protocol rule to match.
4.8 KiB
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
frontend/assets/app.js:- Extract
runTurn(text, { reask = false })fromhandleSend: everything from thesetUiState(thinking)/armTurnTimeoutpoint through thefinallysettle moves intorunTurn; thereaskflag skips (a) theaddMessage("user", …)append and (b) theconversation.push({ who: "user", … })+saveConversation()(the question is already in both).handleSendkeeps the form-level pre-work — the!textguard, the in-flight guard that callsstopTurn()(phase 48),clearErrorBanner(), the input clear + autoGrow — then appends + persists the user message and callsrunTurn(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 houseappendTuneButtonpattern for the meta row (reuse the.msg-metarow;role=list→role=listitemwhere 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 handlerretryLastTurn(wrap):- in-flight guard: if
uiStateis thinking/streaming → no-op (owner-locked). - find the record: the last brain record in
conversation(and thatwrapis 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 freshdonemust never resurrect the replaced answer — what the user saw, the removed answer, is what is stored; the question remains); removewrapfrom the DOM; locate the preceding user record'stext(the record immediately before the popped one — invariant: every brain record follows its user record); callrunTurn(text, { reask: true }). NosendStatusbanner; no scroll (phase-42 contract — no auto-scroll; the fresh bubble lands where the old one was).
- in-flight guard: if
- Last-bubble-only management: a helper
markLastRetryable()— removes any existing.retry-btnfrom 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: ondone(afterappendTuneButton), 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 ofrestoreConversation()(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.mdL4).
- Extract
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.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).- Frontend source pins (house pattern, extend
tests/unit/test_frontend_feedback.pyor a sibling): therunTurnsignature + thereaskskips (no user append/push when reask);appendRetryButton's no-admin-gate + one-per-bubble;markLastRetryable's remove-then-append; the pop → save → rerun order inretryLastTurn; 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.pyE2E green in isolation). - Retry on a completed / deflected / stopped answer redoes in place (unit-pinned now, E2E in task 02).
uv run pytestgreen;uv run ruff check . && uv run pyrightclean.