## Phase 111 Completion Report
**Implemented/Verified:**
- `#kb-banner` contains a `<button type="button" class="banner-retry" id="banner-retry">` (hidden by default, Retry label + SVG)
- `showErrorBanner(detail, opts)` reveals the button only when `opts.retryable` is true AND `lastBrainWrap` exists
- Turn-error path passes `{ retryable: true }`; all non-turn callers (share, save-doc, stale) remain text-only
- `clearErrorBanner()` re-hides the button
- `ERROR_HINT` changed from "Try again — …" to "If this persists, check the LLM is reachable."
- `.banner-retry` CSS styled as a pill (matching `.stale-regenerate` family)
- 12 source-assertion unit tests in `tests/unit/test_frontend_banner_retry.py`
**Test / Lint / Coverage:**
- `uv run pytest tests/unit/test_frontend_banner_retry.py -v --no-cov` → 12 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 2362 passed, 99% coverage
- `uv run ruff check .` → All checks passed
- `uv run pyright` → 0 errors
- `tests/e2e/test_llm_retry.py` → 4 passed (in isolation)
- `tests/e2e/test_smoke.py` → 3 passed (in isolation)
**Completion Criteria:**
- ✅ Retry button visible after failed chat turn, re-runs last question
- ✅ Non-turn callers show text-only banner (no button)
- ✅ pytest green, coverage >90%, ruff + pyright clean
- ✅ Phase dir to be moved by pipeline gate
**Next pending phase:** `112_honesty_gate_weak_hits`
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"