phase: 143_mobile_keyboard_after_answer
Build and Push Containers / build-and-push-app (push) Successful in 2m32s
Build and Push Containers / build-and-push-db (push) Successful in 13s

**Phase 143 final verification pass — 100% green, no code changes needed (all work verified in-tree)**

- **Verified implementation:** `COARSE_POINTER` constant (L395, L391-twin `typeof`-guarded shape) + gated finally-block focus `if (!COARSE_POINTER) input.focus({ preventScroll: true })` in `frontend/assets/app.js`; SETTLE lines/anchor untouched; extended unit pin in `test_frontend_scroll.py`; dedicated two-context E2E `tests/e2e/test_mobile_keyboard_focus.py`
- **C1 desktop byte-identical refocus:** ✅ unit pin passes (gated shape keeps `preventScroll`); desktop E2E asserts composer active; `test_loading_feedback.py` 5 passed (phase-06 regression)
- **C2 mobile keyboard stays down:** ✅ `test_mobile_turn_end_keeps_keyboard_down` passes — `pointer: coarse` sanity assert first, composer NOT active after done
- **C3 gates:** ✅ `uv run pytest --cov=app --cov-report=term-missing` → 3359 passed, `app/` TOTAL 99% (>90%); `uv run pytest tests/e2e/test_mobile_keyboard_focus.py -v --no-cov` → 2 passed in isolation; `uv run ruff check .` clean; `uv run pyright` 0 errors
- **C4 no regression:** ✅ full suite green (all frontend source pins: char counter, send flow, never-scroll); `test_stop_generation.py` 3 passed (single settle)
- **Docs:** README.md fully reviewed — all env vars cross-checked against `app/config.py` defaults, routes/endpoints/tools verified in code; AGENTS.md rules + quick reference verified against repo. Consistent — no changes needed
- **No live-infrastructure changes** (dev Postgres container already running)
- **Next pending phase:** `144_modal_back_button`
This commit is contained in:
2026-10-01 07:03:15 -04:00
parent aa65d2d691
commit ba4841a811
18 changed files with 817 additions and 6 deletions
@@ -0,0 +1,10 @@
**Phase 143 final verification pass — 100% green, no code changes needed (all work verified in-tree)**
- **Verified implementation:** `COARSE_POINTER` constant (L395, L391-twin `typeof`-guarded shape) + gated finally-block focus `if (!COARSE_POINTER) input.focus({ preventScroll: true })` in `frontend/assets/app.js`; SETTLE lines/anchor untouched; extended unit pin in `test_frontend_scroll.py`; dedicated two-context E2E `tests/e2e/test_mobile_keyboard_focus.py`
- **C1 desktop byte-identical refocus:** ✅ unit pin passes (gated shape keeps `preventScroll`); desktop E2E asserts composer active; `test_loading_feedback.py` 5 passed (phase-06 regression)
- **C2 mobile keyboard stays down:** ✅ `test_mobile_turn_end_keeps_keyboard_down` passes — `pointer: coarse` sanity assert first, composer NOT active after done
- **C3 gates:** ✅ `uv run pytest --cov=app --cov-report=term-missing` → 3359 passed, `app/` TOTAL 99% (>90%); `uv run pytest tests/e2e/test_mobile_keyboard_focus.py -v --no-cov` → 2 passed in isolation; `uv run ruff check .` clean; `uv run pyright` 0 errors
- **C4 no regression:** ✅ full suite green (all frontend source pins: char counter, send flow, never-scroll); `test_stop_generation.py` 3 passed (single settle)
- **Docs:** README.md fully reviewed — all env vars cross-checked against `app/config.py` defaults, routes/endpoints/tools verified in code; AGENTS.md rules + quick reference verified against repo. Consistent — no changes needed
- **No live-infrastructure changes** (dev Postgres container already running)
- **Next pending phase:** `144_modal_back_button`
@@ -0,0 +1,118 @@
........................................................................ [ 2%]
........................................................................ [ 4%]
........................................................................ [ 6%]
........................................................................ [ 8%]
........................................................................ [ 10%]
........................................................................ [ 12%]
........................................................................ [ 15%]
........................................................................ [ 17%]
........................................................................ [ 19%]
........................................................................ [ 21%]
........................................................................ [ 23%]
........................................................................ [ 25%]
........................................................................ [ 27%]
........................................................................ [ 30%]
........................................................................ [ 32%]
........................................................................ [ 34%]
........................................................................ [ 36%]
........................................................................ [ 38%]
........................................................................ [ 40%]
........................................................................ [ 42%]
........................................................................ [ 45%]
........................................................................ [ 47%]
........................................................................ [ 49%]
........................................................................ [ 51%]
........................................................................ [ 53%]
........................................................................ [ 55%]
........................................................................ [ 57%]
........................................................................ [ 60%]
........................................................................ [ 62%]
........................................................................ [ 64%]
........................................................................ [ 66%]
........................................................................ [ 68%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 75%]
........................................................................ [ 77%]
........................................................................ [ 79%]
........................................................................ [ 81%]
........................................................................ [ 83%]
........................................................................ [ 85%]
........................................................................ [ 87%]
........................................................................ [ 90%]
........................................................................ [ 92%]
........................................................................ [ 94%]
........................................................................ [ 96%]
........................................................................ [ 98%]
............................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 268 1 99%
app/api/chat_images.py 50 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 179 1 99%
app/api/git_sources.py 259 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 183 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 264 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 68 0 100%
app/models.py 133 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 422 1 99%
app/rag/archive_upload.py 134 0 100%
app/rag/chunker.py 220 4 98%
app/rag/doc_dates.py 18 0 100%
app/rag/folder_summaries.py 134 0 100%
app/rag/git_sources.py 56 0 100%
app/rag/importer.py 705 9 99%
app/rag/llm.py 246 1 99%
app/rag/md_images.py 63 0 100%
app/rag/overview.py 71 0 100%
app/rag/pdf.py 124 0 100%
app/rag/prompts.py 154 0 100%
app/rag/retriever.py 248 3 99%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 58 0 100%
app/schemas.py 424 0 100%
--------------------------------------------------
TOTAL 5498 22 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
@@ -0,0 +1,18 @@
Task 01 complete. Report:
**Implemented**
- `frontend/assets/app.js` — added `COARSE_POINTER` module constant beside the `reducedMotion`/`SCROLL` pair (~L393), same `typeof matchMedia` guard shape, with the phase-143 comment (coarse-pointer state of this device; keyboard-pop gate).
- `frontend/assets/app.js` — finally block's focus line is now `if (!COARSE_POINTER) input.focus({ preventScroll: true });` with the full phase-143 comment (desktop courtesy, §7.1 tap target, phase-42 never-scroll + single-settle unchanged); the old "Focus back…" comment was folded in (no duplicate). Anchor comment and all SETTLE lines untouched.
- `tests/unit/test_frontend_scroll.py::test_turn_end_focus_does_not_scroll` — extended: asserts the GATED shape in the finally block, that no bare ungated `input.focus(` line remains, and that `COARSE_POINTER` is bound from `matchMedia("(pointer: coarse)")` (L391-twin regex); docstring updated to the phase-143 pin's job.
**Verification (all 100% green)**
- `uv run pytest` (unit+integration, exit 0); touched files: 18/18 passed
- `uv run pytest --cov=app` → TOTAL 99% (>90% gate)
- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings
- `tests/unit/test_pinned_composer.py::test_turn_end_focus_still_uses_preventscroll` still green (gated line keeps the `preventScroll` substring)
**Docs:** no doc impact (README/AGENTS.md don't document the focus-back behavior; the UX note lives in the phase files).
**Decisions:** constant placed after the `SCROLL` line to keep the reducedMotion/SCROLL pair contiguous; wrapped to match the file's two-line house style for the long media-query line.
**Next pending task:** `.agents/phases/todo/143_mobile_keyboard_after_answer/02_e2e_mobile_focus.md` (two-context Playwright E2E).
@@ -0,0 +1,118 @@
........................................................................ [ 2%]
........................................................................ [ 4%]
........................................................................ [ 6%]
........................................................................ [ 8%]
........................................................................ [ 10%]
........................................................................ [ 12%]
........................................................................ [ 15%]
........................................................................ [ 17%]
........................................................................ [ 19%]
........................................................................ [ 21%]
........................................................................ [ 23%]
........................................................................ [ 25%]
........................................................................ [ 27%]
........................................................................ [ 30%]
........................................................................ [ 32%]
........................................................................ [ 34%]
........................................................................ [ 36%]
........................................................................ [ 38%]
........................................................................ [ 40%]
........................................................................ [ 42%]
........................................................................ [ 45%]
........................................................................ [ 47%]
........................................................................ [ 49%]
........................................................................ [ 51%]
........................................................................ [ 53%]
........................................................................ [ 55%]
........................................................................ [ 57%]
........................................................................ [ 60%]
........................................................................ [ 62%]
........................................................................ [ 64%]
........................................................................ [ 66%]
........................................................................ [ 68%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 75%]
........................................................................ [ 77%]
........................................................................ [ 79%]
........................................................................ [ 81%]
........................................................................ [ 83%]
........................................................................ [ 85%]
........................................................................ [ 87%]
........................................................................ [ 90%]
........................................................................ [ 92%]
........................................................................ [ 94%]
........................................................................ [ 96%]
........................................................................ [ 98%]
............................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 268 1 99%
app/api/chat_images.py 50 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 179 1 99%
app/api/git_sources.py 259 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 183 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 264 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 68 0 100%
app/models.py 133 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 422 1 99%
app/rag/archive_upload.py 134 0 100%
app/rag/chunker.py 220 4 98%
app/rag/doc_dates.py 18 0 100%
app/rag/folder_summaries.py 134 0 100%
app/rag/git_sources.py 56 0 100%
app/rag/importer.py 705 9 99%
app/rag/llm.py 246 1 99%
app/rag/md_images.py 63 0 100%
app/rag/overview.py 71 0 100%
app/rag/pdf.py 124 0 100%
app/rag/prompts.py 154 0 100%
app/rag/retriever.py 248 3 99%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 58 0 100%
app/schemas.py 424 0 100%
--------------------------------------------------
TOTAL 5498 22 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
@@ -0,0 +1,16 @@
All gates green — the rejection was a harness child-process issue, not a code failure; the working-tree changes were intact and verified.
**Implemented (unchanged from prior attempt, re-verified)**
- `frontend/assets/app.js` — `COARSE_POINTER` module constant beside `reducedMotion`/`SCROLL` (typeof-guarded `matchMedia("(pointer: coarse)")` twin, phase-143 comment); finally-block focus line gated: `if (!COARSE_POINTER) input.focus({ preventScroll: true });` — SETTLE lines, anchor comment, and desktop `preventScroll` contract untouched; old focus comment folded into the phase-143 comment.
- `tests/unit/test_frontend_scroll.py::test_turn_end_focus_does_not_scroll` — extended pin: gated shape asserted, no bare ungated `input.focus(` in the finally block, `COARSE_POINTER` binding regex, docstring updated.
**Verification (full re-run, 100% green)**
- `uv run pytest --cov=app --cov-report=term-missing` → **3359 passed**, 0 failed; `app/` TOTAL **99%** (>90% gate)
- `uv run ruff check .` → All checks passed
- `uv run pyright` → 0 errors, 0 warnings
**Docs:** no doc impact (README.md/AGENTS.md contain no focus-back/composer-behavior statements to update).
**Decisions:** none new; prior placement/wrapping choices kept (constant after `SCROLL` line, two-line house style).
**Next pending task:** `.agents/phases/todo/143_mobile_keyboard_after_answer/02_e2e_mobile_focus.md` (two-context desktop/mobile Playwright E2E).
@@ -0,0 +1,118 @@
........................................................................ [ 2%]
........................................................................ [ 4%]
........................................................................ [ 6%]
........................................................................ [ 8%]
........................................................................ [ 10%]
........................................................................ [ 12%]
........................................................................ [ 15%]
........................................................................ [ 17%]
........................................................................ [ 19%]
........................................................................ [ 21%]
........................................................................ [ 23%]
........................................................................ [ 25%]
........................................................................ [ 27%]
........................................................................ [ 30%]
........................................................................ [ 32%]
........................................................................ [ 34%]
........................................................................ [ 36%]
........................................................................ [ 38%]
........................................................................ [ 40%]
........................................................................ [ 42%]
........................................................................ [ 45%]
........................................................................ [ 47%]
........................................................................ [ 49%]
........................................................................ [ 51%]
........................................................................ [ 53%]
........................................................................ [ 55%]
........................................................................ [ 57%]
........................................................................ [ 60%]
........................................................................ [ 62%]
........................................................................ [ 64%]
........................................................................ [ 66%]
........................................................................ [ 68%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 75%]
........................................................................ [ 77%]
........................................................................ [ 79%]
........................................................................ [ 81%]
........................................................................ [ 83%]
........................................................................ [ 85%]
........................................................................ [ 87%]
........................................................................ [ 90%]
........................................................................ [ 92%]
........................................................................ [ 94%]
........................................................................ [ 96%]
........................................................................ [ 98%]
............................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 268 1 99%
app/api/chat_images.py 50 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 179 1 99%
app/api/git_sources.py 259 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 183 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 264 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 68 0 100%
app/models.py 133 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 422 1 99%
app/rag/archive_upload.py 134 0 100%
app/rag/chunker.py 220 4 98%
app/rag/doc_dates.py 18 0 100%
app/rag/folder_summaries.py 134 0 100%
app/rag/git_sources.py 56 0 100%
app/rag/importer.py 705 9 99%
app/rag/llm.py 246 1 99%
app/rag/md_images.py 63 0 100%
app/rag/overview.py 71 0 100%
app/rag/pdf.py 124 0 100%
app/rag/prompts.py 154 0 100%
app/rag/retriever.py 248 3 99%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 58 0 100%
app/schemas.py 424 0 100%
--------------------------------------------------
TOTAL 5498 22 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
@@ -0,0 +1,10 @@
All gates green. Final report:
**Phase 143, task 02 — dedicated two-context mobile/desktop keyboard-focus E2E**
- **Implemented:** `tests/e2e/test_mobile_keyboard_focus.py` — house-convention E2E (per-module truncate/re-import, `login` helper, phase-06 done-state waits) with a shared `turn_to_completion(page)` driver; `test_desktop_turn_end_refocuses_composer` (default 1280×800 fine-pointer context → `document.activeElement` IS `#message-input`, the preserved phase-06 contract) and `test_mobile_turn_end_keeps_keyboard_down` (360×800 `isMobile`+`hasTouch` context → `pointer: coarse` sanity assert **first**, then composer NOT active after a completed turn — keyboard stays down).
- **Tests:** `uv run pytest tests/e2e/test_mobile_keyboard_focus.py -v --no-cov` → **2 passed** (in isolation); regression `uv run pytest tests/e2e/test_loading_feedback.py -v --no-cov` → **5 passed** (phase-06, pins the desktop refocus); `uv run pytest --cov=app --cov-report=term-missing` → **3359 passed**, `app/` coverage **99%** (>90%); task-01 pin `tests/unit/test_frontend_scroll.py` → 9 passed.
- **Lint/types:** `uv run ruff check .` → All checks passed (removed one unused import flagged on first run); `uv run pyright` → 0 errors, 0 warnings.
- **Docs:** no doc impact — purely additive test file; the isolation run command matches the already-documented `test_<story>.py` convention.
- **Decisions:** focus compared in-page via `document.activeElement ===` (Element handles aren't identity-comparable in Python); desktop test includes a `pointer: fine` control sanity so the two contexts differ in exactly one variable.
- **Next pending task:** none in phase 143 — task 02 was the last; the phase can be archived.
@@ -0,0 +1,118 @@
........................................................................ [ 2%]
........................................................................ [ 4%]
........................................................................ [ 6%]
........................................................................ [ 8%]
........................................................................ [ 10%]
........................................................................ [ 12%]
........................................................................ [ 15%]
........................................................................ [ 17%]
........................................................................ [ 19%]
........................................................................ [ 21%]
........................................................................ [ 23%]
........................................................................ [ 25%]
........................................................................ [ 27%]
........................................................................ [ 30%]
........................................................................ [ 32%]
........................................................................ [ 34%]
........................................................................ [ 36%]
........................................................................ [ 38%]
........................................................................ [ 40%]
........................................................................ [ 42%]
........................................................................ [ 45%]
........................................................................ [ 47%]
........................................................................ [ 49%]
........................................................................ [ 51%]
........................................................................ [ 53%]
........................................................................ [ 55%]
........................................................................ [ 57%]
........................................................................ [ 60%]
........................................................................ [ 62%]
........................................................................ [ 64%]
........................................................................ [ 66%]
........................................................................ [ 68%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 75%]
........................................................................ [ 77%]
........................................................................ [ 79%]
........................................................................ [ 81%]
........................................................................ [ 83%]
........................................................................ [ 85%]
........................................................................ [ 87%]
........................................................................ [ 90%]
........................................................................ [ 92%]
........................................................................ [ 94%]
........................................................................ [ 96%]
........................................................................ [ 98%]
............................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 268 1 99%
app/api/chat_images.py 50 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 179 1 99%
app/api/git_sources.py 259 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 183 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 264 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 68 0 100%
app/models.py 133 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 422 1 99%
app/rag/archive_upload.py 134 0 100%
app/rag/chunker.py 220 4 98%
app/rag/doc_dates.py 18 0 100%
app/rag/folder_summaries.py 134 0 100%
app/rag/git_sources.py 56 0 100%
app/rag/importer.py 705 9 99%
app/rag/llm.py 246 1 99%
app/rag/md_images.py 63 0 100%
app/rag/overview.py 71 0 100%
app/rag/pdf.py 124 0 100%
app/rag/prompts.py 154 0 100%
app/rag/retriever.py 248 3 99%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 58 0 100%
app/schemas.py 424 0 100%
--------------------------------------------------
TOTAL 5498 22 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
+14 -4
View File
@@ -390,6 +390,10 @@ const FAILED_TURN_TEXT =
const reducedMotion =
typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
const SCROLL = reducedMotion ? "auto" : "smooth";
/* Phase 143 (TODO L5): the coarse-pointer state of THIS device (touch
* phones/tablets) — the keyboard-pop gate for the turn-end focus-back. */
const COARSE_POINTER =
typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches;
/* Thinking-window follow-the-tail contract (owner direction
* 2026-08-27, `TODO.md` L7): the scratchpad autoscrolls to its live
@@ -3486,10 +3490,16 @@ async function runTurn(text, { reask = false, image = null } = {}) {
turnAbort = null; // phase 48: the turn's abort owner is spent
if (uiState !== UI_STATE.idle) setUiState(UI_STATE.idle);
if (stoppedByUser) sendStatus.textContent = "Answer stopped.";
// Focus back for the next question, but never move the viewport —
// the page never auto-scrolls (phase 42), so a user reading earlier
// content stays where they are.
input.focus({ preventScroll: true });
// Phase 143 (TODO L5): the focus-back is a DESKTOP courtesy — on a
// coarse pointer (a phone/tablet) focusing the composer pops the
// virtual keyboard over the completed answer, which is exactly what
// the owner found annoying. Fine pointers keep the immediate
// refocus (type the follow-up without moving a hand); a coarse
// pointer stays unfocused — the composer is a ≥44px tap target
// (PLAN §7.1), so the next question starts with a tap. The
// never-scroll contract (phase 42) and the single-settle rule are
// unchanged on every device — only the focus call is gated.
if (!COARSE_POINTER) input.focus({ preventScroll: true });
}
}
+248
View File
@@ -0,0 +1,248 @@
"""Phase 143 E2E (Playwright): the mobile keyboard stays DOWN after an
answer completes — one file, two device contexts.
Owner complaint (TODO.md L5): "mobile keyboard pops back up after answer
is complete and that's annoying." The fix (task 01) gates the turn's
single settle focus-back in ``frontend/assets/app.js`` on
``matchMedia("(pointer: coarse)")``: fine pointers (desktops) keep the
phase-06 "always focus back" courtesy — the follow-up question is typed
without moving a hand — while coarse pointers (phones/tablets) stay
unfocused so the virtual keyboard does not pop back over the completed
answer; the composer is a ≥44px tap target (PLAN §7.1), so the next
question starts with a tap.
This file is the browser-visible proof on BOTH devices, in ONE variable:
* ``test_desktop_turn_end_refocuses_composer`` — the DEFAULT Chromium
context (1280×800, no touch — ``pointer: fine``): a completed turn
leaves ``document.activeElement`` on the composer's textarea
(``#message-input``) — the phase-06/42 contract (refocus,
``preventScroll``) preserved on desktop.
* ``test_mobile_turn_end_keeps_keyboard_down`` — a mobile-emulated
context (360×800, ``isMobile`` + ``hasTouch`` — the ≤640px CSS
breakpoint applies AND Chromium's touch emulation flips the media
feature to ``pointer: coarse``): FIRST the sanity assert that
``matchMedia("(pointer: coarse)").matches`` IS true (if a future
Playwright/Chromium change breaks the emulation, this line fails
loudly and names why, instead of the focus assertion failing
mysteriously), THEN a completed turn leaves the composer UNFOCUSED —
the keyboard stays down.
Both contexts run against the SAME fixture KB (the per-module
truncate/re-import, the house E2E convention — suites run in isolation)
and the SAME on-topic question the chat story suites use; the mock LLM
answers immediately, so the done state settles fast.
Run in isolation (mock LLM; DB up: ``podman compose up -d db``):
uv run pytest tests/e2e/test_mobile_keyboard_focus.py -v --no-cov
"""
from __future__ import annotations
import asyncio
import os
import threading
from pathlib import Path
from typing import Any
from playwright.sync_api import Browser, BrowserContext, Page, ViewportSize, expect
from sqlalchemy import text
from app.config import Settings
from app.db import SessionLocal
from app.rag.importer import ImportSummary, import_sources
from app.rag.llm import LLMClient
from e2e.auth_helpers import login
REPO = Path(__file__).resolve().parents[2]
FIXTURES = REPO / "tests" / "fixtures" / "docs"
MOCK_PORT = int(os.environ.get("E2E_MOCK_PORT", "8901"))
# The same on-topic question the chat story suites (phase 06 loading
# feedback) use — the fixture KB grounds it and the mock answers
# immediately (no warm-up marker), so both contexts settle fast.
ON_TOPIC = "How is my Kubernetes cluster set up?"
# The composer's textarea — the element app.js's turn `finally` block
# focuses back on fine pointers (task 01's `#message-input` pin), and
# the `login` helper already types questions into.
COMPOSER = "#message-input"
# The typing indicator is itself a .msg.brain — exclude its bubble.
ANSWER = ".msg.brain .bubble:not(.typing)"
# The mobile context: a 360×800 phone (the ≤640px CSS breakpoint
# applies — 360 is the phase-88 suite's narrow owner-device edge) with
# isMobile + hasTouch — Chromium's CDP touch emulation is what makes
# ``matchMedia("(pointer: coarse)")`` match (the test's sanity assert
# proves it is doing its job).
MOBILE_VIEWPORT: ViewportSize = {"width": 360, "height": 800}
# --------------------------------------------------------------------------
# KB seeding (same pattern as the phase 06 story suite — truncate +
# re-import the fixture docs per test, so the file runs in isolation)
# --------------------------------------------------------------------------
async def _import_fixtures(mock_port: int) -> ImportSummary:
kwargs: dict[str, Any] = {"_env_file": None, "llm_base_url": f"http://127.0.0.1:{mock_port}/v1"}
settings = Settings(**kwargs) # pyright: ignore[reportCallIssue]
return await import_sources([FIXTURES], LLMClient(settings))
def _run_in_thread(coro: Any) -> Any:
"""Run a coroutine on a worker thread.
Playwright's sync API keeps an asyncio loop running on the test thread,
so ``asyncio.run`` cannot be called directly from a test body.
"""
box: dict[str, Any] = {}
def runner() -> None:
try:
box["value"] = asyncio.run(coro)
except BaseException as e: # noqa: BLE001 — re-raised on the test thread
box["error"] = e
t = threading.Thread(target=runner)
t.start()
t.join()
if "error" in box:
raise box["error"]
return box["value"]
def _reset_db(mock_port: int, seed: bool) -> ImportSummary | None:
"""Truncate the KB (and query log), then optionally re-import fixtures."""
with SessionLocal() as db:
db.execute(text("TRUNCATE chunks, documents, query_log"))
db.commit()
if not seed:
return None
return _run_in_thread(_import_fixtures(mock_port))
# --------------------------------------------------------------------------
# The shared turn driver + device contexts
# --------------------------------------------------------------------------
def turn_to_completion(page: Page) -> None:
"""Ask the on-topic question and wait for the DONE state.
The same settle wait the chat story suites (phase 06) use: the
answer bubble has streamed in AND the send button is back to its
idle contract (enabled, "Send" label — phase 48). By the time the
label reads "Send", the turn's ``finally`` block — the single
settle, including the phase-143 gated focus-back — has already run
(the ``setUiState(idle)`` that flips the label is the line just
above the focus call).
"""
page.fill(COMPOSER, ON_TOPIC)
page.click("#send-btn")
page.locator(ANSWER).wait_for(state="visible", timeout=30_000)
expect(page.locator("#send-label")).to_have_text("Send", timeout=30_000)
expect(page.locator("#send-btn")).to_be_enabled()
def composer_is_active(page: Page) -> bool:
"""``document.activeElement === #message-input`` (compared IN the
page — Element handles are not identity-comparable in Python)."""
return page.evaluate(
"() => document.activeElement === document.querySelector('#message-input')"
)
def _mobile_page(browser: Browser) -> tuple[BrowserContext, Page]:
"""A fresh 360×800 MOBILE page (``isMobile`` + ``hasTouch``): the
context whose touch emulation makes Chromium report
``pointer: coarse`` (the conftest ``page`` fixture is a 1280×800
fine-pointer page — the desktop control). The caller closes page +
context in ``finally``."""
context = browser.new_context(
is_mobile=True, has_touch=True, viewport=MOBILE_VIEWPORT
)
return context, context.new_page()
# --------------------------------------------------------------------------
# 1. Desktop (fine pointer): the preserved "always focus back" contract
# --------------------------------------------------------------------------
def test_desktop_turn_end_refocuses_composer(
page: Page, app_url: str, mock_llm: int, db_ready: None
) -> None:
"""The regression guard (phase 06): in the DEFAULT Chromium context
— no touch, ``pointer: fine`` — a completed turn (the done state:
answer streamed, send button back to the idle "Send" contract)
leaves the composer FOCUSED — ``document.activeElement`` IS
``#message-input`` — the follow-up question is typed immediately.
The never-scroll ``preventScroll`` argument itself is source-pinned
by the unit pin (tests/unit/test_frontend_scroll.py) and is
unobservable from the focus assert (it suppresses viewport motion,
not focus)."""
summary = _reset_db(mock_llm, seed=True)
assert summary is not None and summary.added == 13 # A9 formats (phase 47 added quadlet+j2)
page.set_default_timeout(30_000)
login(page, app_url, next="/")
# Control sanity: this context really is a FINE pointer (the pair
# of contexts differs in exactly one variable — the device).
assert not page.evaluate("matchMedia('(pointer: coarse)').matches"), (
"the desktop control context must report pointer: fine — the "
"conftest page is a no-touch 1280x800 Chromium context"
)
turn_to_completion(page)
assert composer_is_active(page), (
"desktop (fine pointer): after a turn ends, the composer must be "
"the active element again — the phase-06 'always focus back' "
"contract is desktop-only, never removed"
)
# --------------------------------------------------------------------------
# 2. Mobile (coarse pointer): the keyboard stays down
# --------------------------------------------------------------------------
def test_mobile_turn_end_keeps_keyboard_down(
browser: Browser, app_url: str, mock_llm: int, db_ready: None
) -> None:
"""The owner's fix, browser-visible: in a 360×800 mobile context
(``isMobile`` + ``hasTouch`` → ``pointer: coarse``), a completed
turn leaves the composer UNFOCUSED — ``document.activeElement`` is
NOT ``#message-input`` — so the virtual keyboard stays DOWN over
the completed answer (the composer's ≥44px tap target, PLAN §7.1,
starts the next question). The first assert proves the emulation
is doing its job: if a future Playwright/Chromium change stops
flipping the media feature, THIS line fails loudly and names why,
instead of the focus assert failing mysteriously."""
summary = _reset_db(mock_llm, seed=True)
assert summary is not None and summary.added == 13
context, page = _mobile_page(browser)
try:
page.set_default_timeout(30_000)
# Emulation sanity FIRST (see the docstring): the touch
# emulation must have flipped the media feature.
assert page.evaluate("matchMedia('(pointer: coarse)').matches"), (
"the mobile context (isMobile + hasTouch, 360x800) must "
"report pointer: coarse — Chromium's touch emulation is not "
"flipping the media feature; the focus assert below would "
"be meaningless (fail this line, not the focus one)"
)
login(page, app_url, next="/")
turn_to_completion(page)
assert not composer_is_active(page), (
"mobile (coarse pointer): after a turn ends, the composer "
"must NOT be re-focused — the focus-back is a desktop "
"courtesy, and re-focusing pops the virtual keyboard over "
"the completed answer (the owner's complaint, TODO L5)"
)
finally:
page.close()
context.close()
+29 -2
View File
@@ -214,12 +214,39 @@ def test_turn_end_focus_does_not_scroll() -> None:
"""The turn-end focus-back (phase 06's "always focus back") must not
move the viewport: focusing the composer while the user is scrolled
up would yank them to the bottom at the moment the turn ends.
preventScroll keeps the keyboard flow without the scroll."""
preventScroll keeps the keyboard flow without the scroll.
Phase 143 (TODO L5): the focus-back is now GATED on pointer type —
`if (!COARSE_POINTER) input.focus({ preventScroll: true })`. On a
coarse pointer (phone/tablet) focusing the composer pops the
virtual keyboard over the completed answer, so the composer stays
unfocused there (the ≥44px tap target starts the next question).
This pin's job: the focus-back exists, is preventScroll (the
phase-06/42 contract, byte-preserved on desktop), and is
coarse-gated — a bare ungated `input.focus(...)` line in the
finally block is a failure, as a missing or mistyped
`COARSE_POINTER` constant is."""
js = _js()
# Phase 48: the settle line gained the user-stop terminal (stop →
# idle, no banner) — the focus-back contract is unchanged.
finally_idx = js.find("// done | error | stop → idle: always settle, always focus back")
assert finally_idx != -1, "the turn's finally block must exist"
block = js[finally_idx : js.find("\n}", finally_idx)]
assert 'input.focus({ preventScroll: true })' in block
# Phase 143: the gated shape — the focus call exists, keeps
# preventScroll, and is wrapped in the coarse-pointer gate.
assert "if (!COARSE_POINTER) input.focus({ preventScroll: true })" in block
# A bare, ungated focus line (the pre-phase-143 shape) is gone:
# every focus call in the block rides the gate.
focus_lines = [ln for ln in block.splitlines() if "input.focus(" in ln]
assert focus_lines, "the turn-end focus-back must still exist"
assert all("if (!COARSE_POINTER)" in ln for ln in focus_lines), (
"the turn-end focus-back must be coarse-pointer gated (phase 143)"
)
assert "input.focus()" not in block
# The L391-twin constant: the same typeof-guarded matchMedia shape
# as reducedMotion, bound to COARSE_POINTER.
assert re.search(
r'const COARSE_POINTER\s*=\s*typeof matchMedia === "function"'
r' && matchMedia\("\(pointer: coarse\)"\)\.matches;',
js,
)