phase: 166_hide_empty_answer_bubble
Build and Push Containers / build-and-push-app (push) Successful in 1m41s
Build and Push Containers / build-and-push-db (push) Successful in 11s

**Phase 166 — final verification pass: green.** All 3 task files already in `complete/`, so this was verification + defect/doc fixing.

- **Verified:** `.msg .bubble:empty { display: none; }` exists once in the Messages block with provenance; `frontend/` diff is styles.css only (**no app.js change**); `tests/unit/test_no_empty_answer_bubble.py` 26 pins pass; phase-158/161 anchor pins (thinking_timeline + tool_states + edit_message) **75 passed**.
- **Phase E2E in isolation:** `uv run pytest tests/e2e/test_no_empty_box.py --no-cov` → **5 passed**. Mutation check (rule removed) → 2 failures, so the suite genuinely pins the defect.
- **Regression set, isolated:** thinking_timeline 6 · turn_progress_loader 3 · chat_history 5 · share_chat 4 · save_share_ux 5 · failed_turn_retry 4 · chat_answer_clusters 4 · chat_error_honesty 4 → all pass.
- **Full gates:** `uv run pytest --cov=app --cov-report=term-missing` → **3880 passed, app/ 99%** (gate >90%); `uv run ruff check .` → clean; `uv run pyright` → **0 errors**.
- **Whole E2E tree swept file-by-file (150 files, own pytest process each).** 4 suites were broken by *earlier* phases; fixed and re-run green:
  - `test_kb_tree`, `test_sync_summary_visibility`, `test_folder_summary_voice` — `td:nth-child(4) span` became a strict-mode violation when phase 156 wrapped the Edit label in `.cell-clip` → scoped to `span.kb-desc-text`.
  - `test_search_tool_scope` — pinned the pre-`690fd98` per-run `<summary>`/`open` shape → re-pinned to the ONE `details.reasoning` container + `.tool-calls-summary`.
  - `test_header_consistency` — `.doc-meta` pixel bound (`<34px`) was broken by phase 153's 44px touch-target floor at ≤640px → replaced with a shape-first single-line check that still fails a real wrap.
- **Completion criteria:** mid-turn box hidden ✅ · bubble appears at first delta, phase-130 rows below unchanged ✅ · restored/shared empty answer paints no box ✅ · no app.js change + anchors + full gates ✅ · commit/`complete/` move = harness's job (I ran no git index/history commands).
- **Docs:** README — added the phase-166 "no empty answer box mid-turn" note; fixed a **stale** Thinking lead ("collapsible Thinking blocks", pre-dated the 2026-10-06 one-disclosure refinement); QA section now states E2E isolation is mandatory (shared Postgres + fixed ports 8123/8901) with the per-file sweep loop. AGENTS.md — rule 5 UI checklist now names the phase-166 suppression rule.
- **Notable:** running all of `tests/e2e/` in one pytest session is not supported (fixture/port interleaving causes ~126 spurious failures) — documented in README. Restored the tracked `.agents/screenshots/doc_modal_scrollbar_1280.png` that `test_document_viewer.py` rewrites on every run.
- **Next pending phase:** `167_api_payload_diet`.
This commit is contained in:
2026-10-08 04:27:03 -04:00
parent b0c7aed613
commit ab9391d019
36 changed files with 2348 additions and 93 deletions
+28 -8
View File
@@ -92,6 +92,15 @@ question caps at 90 displayed characters with a `…` (phase 157) — the
full text stays in the hover tooltip, and clicking the chip submits
the full question.
**No empty answer box mid-turn** (phase 166): the bubble that will hold the
answer is created on the turn's first `thinking`/`tool` frame — it is the
DOM anchor the Reasoning container inserts itself above — but it stays
hidden until the **first answer token** lands in it, so an in-flight turn
shows the typing indicator and the Reasoning bubble only, never a padded
empty rectangle (nothing replaces the box: the existing loading feedback
is the turn's visible state). The same rule covers a restored or shared
answer whose persisted text is empty, which paints no box either.
The document's meta row (the chat modal **and** the standalone viewer at
`/document.html`) carries a **Copy path** button: one click copies the
document's `source/path` (the exact form the `read` tool, the chips, and
@@ -486,14 +495,15 @@ whole git-source refresh in one click:
## Thinking
The self-hosted `turbo` model reasons before it answers. That reasoning is
streamed with the turn as `thinking` SSE events and shown in **
collapsible "Thinking" blocks** — one block per
contiguous thinking run, in arrival order (phase 158) — all wrapped in
**one collapsible "Reasoning" bubble** above the answer bubble
(phase 161): the turn renders as a chronological timeline
streamed with the turn as `thinking` SSE events and shown as **"Thinking"
blocks** — one block per contiguous thinking run, in arrival order
(phase 158) — all wrapped in **one collapsible "Reasoning" bubble** above
the answer bubble (phase 161): the turn renders as a chronological timeline
(Thinking → Tool call (1) → Thinking → … → answer), condensed into a
single bounded live window (320 px, one scroll surface) that follows
the tail while you watch.
the tail while you watch. The Reasoning bubble is the ONE disclosure
(owner refinement, 2026-10-06): the per-run blocks inside it are plain and
never fold on their own.
The bubble is created on the turn's first thinking (or tool) frame and
stays **open** from creation until the turn settles — including a
@@ -1174,11 +1184,21 @@ uv run pyright
# Playwright E2E — install the browser once:
uv run playwright install chromium
# Each phase's E2E runs IN ISOLATION:
# Each phase's E2E runs IN ISOLATION — one file per run:
uv run pytest tests/e2e/test_import_documents.py -v --no-cov
uv run pytest tests/e2e/test_chat_rag.py -v --no-cov
# ...one file per phase in tests/e2e/ (the stories live in
# .agents/user_stories/, see .agents/phases/todo/)
# .agents/user_stories/, see .agents/phases/todo/).
#
# Isolation is REQUIRED, not a style preference: every suite shares one
# Postgres and one pair of fixed ports (the app on 8123, the mock aipi on
# 8901 — overridable with E2E_APP_PORT / E2E_MOCK_PORT), and each file
# truncates + reseeds the KB for its own fixture. Running the whole
# `tests/e2e/` directory in a single pytest session interleaves those
# fixtures and fails tests that pass file by file.
# To sweep every suite, loop over the files (one pytest process each):
for f in tests/e2e/test_*.py; do uv run pytest "$f" --no-cov -q || echo "FAIL $f"; done
```
**Retrieval regression battery.** `tests/fixtures/retrieval_battery.txt` is