phase: 143_mobile_keyboard_after_answer
**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:
+14
-4
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user