5.1 KiB
Phase 111 — Chat error banner: a real Retry button (TODO L1)
Source: TODO.md L3–21 — "L1 — Chat error banner: 'Try again' is plain text, not a button (2026-09-15, brain-of-reese interactive test)"
Story: n/a (interactive-test follow-up fix; extends the phase-06 loading-feedback and phase-49/53 retry assets).
Context: frontend/index.html:112 renders #kb-banner (chat page only — the Sources/document pages do not render it) with #kb-banner-text; frontend/assets/app.js — showErrorBanner(detail) (L2084) writes ${detail} ${ERROR_HINT} as plain text; ERROR_HINT (L362) begins "Try again — …" so "Try again" reads as a clickable action but is not. retryLastTurn(wrap) (L2131) re-asks the last question in place (phase 49); #stale-regenerate (index.html:134, handler at app.js ~L1898) is the existing banner-button → retryLastTurn pattern.
Objective
Give the chat-view error banner a real Retry control after a failed/dropped turn: the banner shows a visible Retry button that re-runs the last question without re-typing (reusing the phase-49 redo-in-place and the stale-banner button pattern). The banner text stops mimicking a button, and every existing showErrorBanner caller (share, save-doc, stale chat) keeps working text-only — the Retry button appears only on failed chat turns.
Dependencies
110_fix_sse_db_pool_exhaustion(complete) — pipeline predecessor (execution order) only; no code dependency (this phase touchesfrontend/index.html,frontend/assets/app.js,frontend/assets/styles.css, and frontend unit tests).
Design (shared by all tasks — the executor reads this, not the chat)
- Banner button (task 01): add
<button type="button" class="banner-retry" id="banner-retry" hidden>inside#kb-banner(after#kb-banner-text), mirroring the#stale-regeneratemarkup (same refresh SVG + visible "Retry" label). Hidden by default;showErrorBanner(detail, opts)gains an optional second arg — when the caller flags the error as a failed chat turn (the UI state-machine path at app.js:1281if (state === UI_STATE.error) showErrorBanner(errorDetail)), the button is revealed and wired toretryLastTurn(lastBrainWrap)— the same last-brain-bubble targeting the#stale-regeneratehandler uses. No retryable brain bubble → no button. - Copy:
ERROR_HINTbecomes "If this persists, check the LLM is reachable." — the "Try again —" prefix moves to the button (the text must no longer read as a fake control). - Non-turn callers (share failures L1857/L1870/L1892, save-doc L735/L742, stale L1620/L1950/L1962, …) pass no opts → text-only banner, no button — no behavior change for them.
- CSS:
.banner-retryinstyles.cssreuses the.stale-regeneratepill look (same component family); the banner keepsrole="alert". - NOT touched: the per-answer Retry pill (phase 49), the stale banner,
retryLastTurnitself, the server, and the RAG/document views (no#kb-bannerthere — no split needed, ASSUMPTION in task 01).
Tasks
01_banner_retry_button.md— banner Retry button markup + handler + hint-copy fix.02_banner_retry_tests.md— frontend unit tests for the button's presence/handler + stale hint-copy assertions updated.
Testing & Quality
- Unit:
tests/unit/test_frontend_banner_retry.py(new, task 02) — house-style source assertions:#banner-retryexists in the#kb-bannermarkup (hidden by default,type="button");showErrorBannerwires the click →retryLastTurn; the button is revealed only on the turn-error path;clearErrorBannerre-hides it;ERROR_HINTno longer starts with "Try again". - E2E: no new file — the turn-error path is exercised by the existing
tests/e2e/test_llm_retry.pyandtests/e2e/test_smoke.pysuites, which must stay green (no banner behavior change for non-turn callers). - Coverage: >90% on
app/(validate.sh gate; the frontend JS is pinned by the source-assertion unit tests — no app/ code changes in this phase).
Completion Criteria
- After a dropped/failed chat turn,
#kb-bannershows a visible Retry button; clicking it re-runs the last question without re-typing. - Share/save-doc/stale-chat errors show a text-only banner (no button) — unchanged.
uv run pytestgreen;uv run pytest --cov=app --cov-report=term-missingTOTAL >90%;uv run ruff check . && uv run pyrightclean.- One
--no-gpg-signcommit; phase dir moved to.agents/phases/complete/by the pipeline gate.
Locked decisions
- A1 — Retry = re-ask the last question in place via the existing
retryLastTurn(owner-confirmed 2026-09-14, roadmap confirmation). No new retry mechanism; the phase-49 redo-in-place is reused. - A2 — the button is offered only on the UI state-machine's turn-error path (a dropped/failed chat turn); all other banner callers stay text-only (owner-confirmed 2026-09-14). Matches L1's acceptance ("after a dropped/failed turn …").
Commit
git add frontend/ tests/ .agents/phases/ && git commit --no-gpg-sign -m "fix(ui): give the chat error banner a real Retry button that re-asks the last question"