phase: 166_hide_empty_answer_bubble
**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:
@@ -36,8 +36,13 @@ thinking frame) is REPLACED by the per-run timeline (D1/D5): each
|
||||
contiguous thinking run streams into its OWN block, and the first frame
|
||||
of a new kind closes the previous run. The SSE stream is unchanged, so
|
||||
this turn renders ONE ``.tool-calls-disclosure`` + TWO ``.thinking``
|
||||
blocks (arrival order, all ABOVE the answer bubble — D2), open in their
|
||||
live windows and folded at rest. The loader pins
|
||||
blocks (arrival order) — wrapped since phase 161 in the ONE
|
||||
``details.reasoning`` container ABOVE the answer bubble (D1), the
|
||||
container open from creation until the settle fold (D2). The
|
||||
2026-10-06 D5 refinement (owner, shipped in ``690fd98``): the per-run
|
||||
blocks are PLAIN DIVS — always visible inside the container, never
|
||||
themselves collapsible (the container is the ONE disclosure). The
|
||||
loader pins
|
||||
(``test_loader_visible_from_send_through_the_tool_gap``,
|
||||
``test_loader_a11y_and_reduced_motion``) stay unchanged — the
|
||||
never-stale loader contract (D16) is intact.
|
||||
@@ -53,15 +58,17 @@ context, ``_reset_db`` reseeds the KB deterministically):
|
||||
the phase-87 elapsed counter in the tool gap, and ``#send-status``
|
||||
carries a state text (not empty) throughout.
|
||||
* ``test_thinking_runs_open_in_live_window_with_visible_loader`` —
|
||||
THE reported symptom's state, re-pinned for phase 158 (D5 replaces
|
||||
phase-109 D15 — a NEW block instead of a re-open): the post-delta
|
||||
thinking frames open a per-run ``.thinking`` block (exactly one
|
||||
block, open + non-empty ``.thinking-text`` while the bubble already
|
||||
carries call 1's content — the run starts AFTER the first delta) with
|
||||
the loader STILL visible (the frozen window is gone — the user sees
|
||||
the new thinking in its own live block); the terminal state is clean
|
||||
(loader hidden, the scratchpad holds TWO ``.thinking`` blocks —
|
||||
EACH carrying its own round's text — BOTH folded at rest, the bubble
|
||||
THE reported symptom's state, re-pinned for phase 158 + the phase-161
|
||||
D5 refinement (a NEW plain-div run instead of a re-open; the runs
|
||||
never fold, the ONE container does): while the
|
||||
``details.reasoning`` container is OPEN, exactly one ``.thinking``
|
||||
run is VISIBLE with non-empty ``.thinking-text`` while the bubble
|
||||
already carries call 1's content — the run starts AFTER the first
|
||||
delta — with the loader STILL visible (the frozen window is gone —
|
||||
the user sees the new thinking in its own live run); the terminal
|
||||
state is clean (loader hidden, the container folded at the settle —
|
||||
zero nested ``<details>`` inside it — the scratchpad holds TWO
|
||||
``.thinking`` runs EACH carrying its own round's text, the bubble
|
||||
carries BOTH answers, the send button reads "Send" not "Stop",
|
||||
``#send-status`` back to the idle shape).
|
||||
* ``test_loader_a11y_and_reduced_motion`` — after a full turn: the
|
||||
@@ -223,25 +230,32 @@ def test_thinking_runs_open_in_live_window_with_visible_loader(
|
||||
loader = page.locator("#turn-loader")
|
||||
|
||||
# THE reported symptom's state (phase 158 D5 replaces phase-109
|
||||
# D15 — the post-delta thinking frames open a NEW per-run block
|
||||
# instead of re-opening the first delta's block): exactly ONE
|
||||
# ``.thinking`` block exists, it is OPEN with non-empty text WHILE
|
||||
# the answer bubble already carries call 1's content — the first
|
||||
# thinking run starts AFTER the first delta (the owner's "thinking
|
||||
# after responding" repro), and the loader is STILL visible (the
|
||||
# frozen window is gone — the user sees the new thinking in its
|
||||
# own live block).
|
||||
# D15 — the post-delta thinking frames open a NEW per-run block,
|
||||
# and the phase-161 D5 refinement (owner 2026-10-06, 690fd98) made
|
||||
# the runs PLAIN DIVS inside the ONE details.reasoning container —
|
||||
# always visible, the CONTAINER is the disclosure): while the
|
||||
# container is OPEN, exactly ONE ``.thinking`` run exists and
|
||||
# paints non-empty text WHILE the answer bubble already carries
|
||||
# call 1's content — the first thinking run starts AFTER the first
|
||||
# delta (the owner's "thinking after responding" repro), and the
|
||||
# loader is STILL visible (the frozen window is gone — the user
|
||||
# sees the new thinking in its own live run).
|
||||
page.wait_for_function(
|
||||
"""() => {
|
||||
const b = document.querySelectorAll('.msg.brain .thinking');
|
||||
const cont = document.querySelector(
|
||||
'.msg.brain details.reasoning[open]');
|
||||
if (!cont) return false;
|
||||
const b = cont.querySelectorAll('.thinking');
|
||||
const t = b.length === 1 ? b[0].querySelector('.thinking-text') : null;
|
||||
return b.length === 1 && !!b[0].open && !!t
|
||||
&& t.textContent.trim().length > 0;
|
||||
return b.length === 1 && !!t && t.textContent.trim().length > 0;
|
||||
}""",
|
||||
timeout=30_000,
|
||||
)
|
||||
expect(blocks).to_have_count(1)
|
||||
expect(blocks).to_have_attribute("open", "")
|
||||
expect(blocks.first).to_be_visible()
|
||||
expect(
|
||||
page.locator(".msg.brain details.reasoning").first
|
||||
).to_have_attribute("open", "")
|
||||
expect(blocks.nth(0).locator(".thinking-text")).to_contain_text(SENTINEL_THOUGHT)
|
||||
# The run is genuinely AFTER a delta: call 1's content is already
|
||||
# in the bubble ...
|
||||
@@ -265,11 +279,12 @@ def test_thinking_runs_open_in_live_window_with_visible_loader(
|
||||
# reasoning streams — separate blocks, not one lump) ...
|
||||
expect(blocks.nth(0).locator(".thinking-text")).to_contain_text(SENTINEL_THOUGHT)
|
||||
expect(blocks.nth(1).locator(".thinking-text")).to_contain_text(SENTINEL_FINAL_THOUGHT)
|
||||
# ... and BOTH runs are FOLDED at rest (D5 — the per-run
|
||||
# fold-at-rest; the phase-109 symptom — the frozen chat — stays
|
||||
# fixed: a run is open in its live window, never re-opened after
|
||||
# done) ...
|
||||
expect(page.locator(".msg.brain .thinking[open]")).to_have_count(0)
|
||||
# ... and the ONE container FOLDED at the settle (phase 161 D2 —
|
||||
# the container is the ONLY disclosure; the inner runs are plain
|
||||
# divs, never collapsible: zero nested <details>; the phase-109
|
||||
# symptom — the frozen chat — stays fixed) ...
|
||||
expect(page.locator(".msg.brain details.reasoning[open]")).to_have_count(0)
|
||||
expect(page.locator(".msg.brain details.reasoning details")).to_have_count(0)
|
||||
# ... the bubble carries BOTH answers (call 1 + call 2) ...
|
||||
expect(bubble).to_contain_text(SENTINEL_FIRST)
|
||||
expect(bubble).to_contain_text(SENTINEL_FINAL)
|
||||
|
||||
Reference in New Issue
Block a user