chore(agent): phase roadmap from TODO.md — 6 phases (111–116): banner retry, honesty gate, chip quality, embed length, draft discard, modal scrollbar
Build and Push Containers / build-and-push-app (push) Successful in 16s
Build and Push Containers / build-and-push-db (push) Successful in 13s

This commit is contained in:
2026-09-14 22:07:32 -04:00
parent 2b75f3cc85
commit f37c517590
25 changed files with 754 additions and 208 deletions
@@ -0,0 +1,42 @@
# 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 touches `frontend/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-regenerate` markup (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:1281 `if (state === UI_STATE.error) showErrorBanner(errorDetail)`), the button is revealed and wired to `retryLastTurn(lastBrainWrap)` — the same last-brain-bubble targeting the `#stale-regenerate` handler uses. No retryable brain bubble → no button.
- **Copy:** `ERROR_HINT` becomes "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-retry` in `styles.css` reuses the `.stale-regenerate` pill look (same component family); the banner keeps `role="alert"`.
- **NOT touched:** the per-answer Retry pill (phase 49), the stale banner, `retryLastTurn` itself, the server, and the RAG/document views (no `#kb-banner` there — no split needed, ASSUMPTION in task 01).
## Tasks
1. `01_banner_retry_button.md` — banner Retry button markup + handler + hint-copy fix.
2. `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-retry` exists in the `#kb-banner` markup (hidden by default, `type="button"`); `showErrorBanner` wires the click → `retryLastTurn`; the button is revealed only on the turn-error path; `clearErrorBanner` re-hides it; `ERROR_HINT` no longer starts with "Try again".
- E2E: no new file — the turn-error path is exercised by the existing `tests/e2e/test_llm_retry.py` and `tests/e2e/test_smoke.py` suites, 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-banner` shows 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 pytest` green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL >90%; `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; 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
```bash
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"
```
@@ -0,0 +1,34 @@
# Task 01 — Banner Retry button: markup, handler, hint-copy fix
**Phase:** `111_chat_banner_retry` · **Source:** `TODO.md:3–18` — "L1 — Chat error banner: 'Try again' is plain text, not a button … `showErrorBanner()` (`frontend/assets/app.js`, `ERROR_HINT` ~line 362) renders `${detail} ${ERROR_HINT}` as **plain text** into `#kb-banner-text` … Suggested fix: give the chat-view error banner a real Retry control that re-asks the last question (the stale-chat banner already has the pattern: `#stale-regenerate` → `retryLastTurn`; the phase-49 Retry pill asset exists). Keep `#kb-banner` dual-use working for the RAG view, or split the two banners if the RAG view's banner has different recovery semantics."
## Objective
The chat error banner gets a real Retry button (turn failures only) that re-asks the last question through the existing `retryLastTurn` redo-in-place; the hint copy stops mimicking the button.
## Work
1. `frontend/index.html` — inside `#kb-banner` (L112), add after `<span id="kb-banner-text">`:
```html
<button type="button" class="banner-retry" id="banner-retry" hidden>
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
<span>Retry</span>
</button>
```
(the same refresh SVG the `#stale-regenerate` button at L134 uses). Add a comment block: revealed only for failed chat turns (task 01 of this phase); hidden for every other banner caller.
2. `frontend/assets/app.js`:
- `ERROR_HINT` (L362): change to `"If this persists, check the LLM is reachable."` (the action moves to the button).
- `showErrorBanner(detail, opts = {})` (L2084): keep the single-arg behavior byte-identical; when `opts.retryable` is true AND a retryable last brain bubble exists (the same lastBrainWrap lookup the `#stale-regenerate` handler at ~L1898 uses), unhide `#banner-retry` and bind its click **once** to `() => retryLastTurn(lastBrainWrap)`; re-binding on every reveal must be guarded (one listener per button lifetime). `clearErrorBanner()` re-hides the button.
- The turn-error path (L1281 `if (state === UI_STATE.error) showErrorBanner(errorDetail)`): pass `{ retryable: true }`. Every other caller (L735, L742, L1620, L1857, L1870, L1892, L1950, L1962, …) is left unchanged.
- If `retryLastTurn` would no-op (no retryable bubble), do not reveal the button — reveal only when a bubble exists.
3. `frontend/assets/styles.css` — `.banner-retry`: same pill treatment as `.stale-regenerate` (color, border, hover, `focus-visible` ring per the theme), laid out inline after the banner text (the `.kb-banner` flex row + gap already handles spacing).
4. ASSUMPTION: no banner split — `#kb-banner` exists only in `frontend/index.html` (the chat page); `document.html`/the Sources pages do not render it, so "keep dual-use" is trivially satisfied and the RAG view is untouched.
5. ASSUMPTION: the button is revealed only on the UI state-machine's turn-error path (a dropped/failed chat turn) — non-turn errors (share, save-doc, stale chat) stay text-only (locked A2).
## Testing & Quality
- Unit: `tests/unit/test_frontend_banner_retry.py` (added by task 02 — this task ships the code, task 02 ships the pin).
- Coverage: n/a (frontend) — the validate.sh `app/` gate must stay green.
## Completion Criteria
- [ ] `#kb-banner` contains `#banner-retry` (hidden by default); a failed chat turn reveals it; clicking re-asks the last question without re-typing.
- [ ] `ERROR_HINT` no longer contains "Try again".
- [ ] No non-turn call site passes `retryable` (grep the call sites).
- [ ] `uv run pytest` green; `uv run ruff check . && uv run pyright` clean.
@@ -0,0 +1,23 @@
# Task 02 — Unit tests for the banner Retry button
**Phase:** `111_chat_banner_retry` · **Source:** `TODO.md:19–21` — "Acceptance: after a dropped/failed turn, the banner shows a visible Retry button that re-runs the last question without re-typing; unit test for the banner's button presence/handler in the frontend test suite."
## Objective
Pin the banner contract in the frontend unit suite: the button's presence, its handler, its reveal condition, and the new hint copy.
## Work
1. `tests/unit/test_frontend_banner_retry.py` (new) — house-style source assertions (pattern: `tests/unit/test_frontend_feedback.py`):
- `frontend/index.html`: `#banner-retry` exists inside the `#kb-banner` block, `hidden` by default, `type="button"`, with a visible "Retry" label.
- `frontend/assets/app.js`: `showErrorBanner` binds the click handler to `retryLastTurn`; the `UI_STATE.error` turn path passes the retryable flag; `clearErrorBanner` re-hides the button.
- `ERROR_HINT` does not start with "Try again".
2. Grep the whole `tests/` tree for the old hint copy (`Try again — if this persists`) and update any stale assertion (loading-feedback and banner-related frontend tests).
3. Run the full unit + integration suite — no regressions (in particular `tests/unit/test_frontend_*.py`).
## Testing & Quality
- Unit: the new file above (≥4 assertions across markup/handler/reveal/copy).
- Coverage: **>90%** on `app/` unchanged (no app/ code touched by this phase).
## Completion Criteria
- [ ] `uv run pytest tests/unit/test_frontend_banner_retry.py -v` green.
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL >90%; `uv run ruff check . && uv run pyright` clean.
- [ ] No test anywhere asserts the old "Try again — …" hint.