diff --git a/.agents/phases/todo/116_modal_scrollbar_theme/00_phase.md b/.agents/phases/complete/116_modal_scrollbar_theme/00_phase.md similarity index 100% rename from .agents/phases/todo/116_modal_scrollbar_theme/00_phase.md rename to .agents/phases/complete/116_modal_scrollbar_theme/00_phase.md diff --git a/.agents/phases/todo/116_modal_scrollbar_theme/01_themed_scrollbar.md b/.agents/phases/complete/116_modal_scrollbar_theme/01_themed_scrollbar.md similarity index 100% rename from .agents/phases/todo/116_modal_scrollbar_theme/01_themed_scrollbar.md rename to .agents/phases/complete/116_modal_scrollbar_theme/01_themed_scrollbar.md diff --git a/.agents/phases/todo/116_modal_scrollbar_theme/02_scrollbar_e2e_check.md b/.agents/phases/complete/116_modal_scrollbar_theme/02_scrollbar_e2e_check.md similarity index 100% rename from .agents/phases/todo/116_modal_scrollbar_theme/02_scrollbar_e2e_check.md rename to .agents/phases/complete/116_modal_scrollbar_theme/02_scrollbar_e2e_check.md diff --git a/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__00_phase.a1.err b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__00_phase.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__00_phase.a1.md b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__00_phase.a1.md new file mode 100644 index 0000000..352c772 --- /dev/null +++ b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__00_phase.a1.md @@ -0,0 +1,21 @@ +All verification complete — every gate re-run independently and green. Final report: + +**Phase 116 final verification pass — all green (work was already in the tree; verified, nothing to fix)** + +- **Verified** `styles.css`: `--scrollbar-thumb`/`--scrollbar-track` tokens in `:root` (color-mix derivations — follow the saved theme, not in BUILTIN_COLORS; contrast 3.73:1 ≥ 3:1, recomputed independently) + scoped rules `.doc-modal .doc-raw` (overflow-x already owned by the element) +- **Verified** E2E test 8 `test_raw_code_block_themed_scrollbar`: overflow, scroll action, computed-style theme assertions, screenshot +- **Screenshots visually confirmed**: headed `doc_modal_scrollbar_themed_1280.png` shows muted rounded thumb on dark track, no native light-gray bar; headless suite shot records scrolled state (headless Chromium paints no bar UI — documented in test docstring) + +**Test / lint / coverage (re-run by me):** +- `uv run pytest tests/e2e/test_document_viewer.py -v --no-cov` → **8 passed** (isolation) +- `uv run pytest --cov=app --cov-report=term` → **2457 passed, app/ 99%** (>90% ✓) +- `uv run pytest tests/e2e/test_dark_tech_theme.py -v --no-cov` → **6 passed** (no token collision) +- `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings + +**Completion criteria:** +1. Modal scrolls horizontally, scrollbar themed, screenshot shows no native bar — ✅ +2. No global scrollbar change (all new selectors under `.doc-modal`; grep-confirmed), regressions green — ✅ +3. pytest green, coverage >90%, e2e isolated green, ruff+pyright clean — ✅ +4. `--no-gpg-sign` commit + phase move — harness job (left uncommitted in working tree, per rules) + +**No defects found; no deviations. Next pending phase:** none — `todo/` contains only phase 116. diff --git a/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__00_phase.a1.validate b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__00_phase.a1.validate new file mode 100644 index 0000000..6b977c2 --- /dev/null +++ b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__00_phase.a1.validate @@ -0,0 +1,103 @@ +........................................................................ [ 2%] +........................................................................ [ 5%] +........................................................................ [ 8%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 20%] +........................................................................ [ 23%] +........................................................................ [ 26%] +........................................................................ [ 29%] +........................................................................ [ 32%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 41%] +........................................................................ [ 43%] +........................................................................ [ 46%] +........................................................................ [ 49%] +........................................................................ [ 52%] +........................................................................ [ 55%] +........................................................................ [ 58%] +........................................................................ [ 61%] +........................................................................ [ 64%] +........................................................................ [ 67%] +........................................................................ [ 70%] +........................................................................ [ 73%] +........................................................................ [ 76%] +........................................................................ [ 79%] +........................................................................ [ 82%] +........................................................................ [ 84%] +........................................................................ [ 87%] +........................................................................ [ 90%] +........................................................................ [ 93%] +........................................................................ [ 96%] +........................................................................ [ 99%] +......... [100%] +=============================== warnings summary =============================== +.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain-of-reese/.venv/lib64/python3.14/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.14.7-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 224 1 99% +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 156 1 99% +app/api/git_sources.py 232 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 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 210 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 66 0 100% +app/models.py 128 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 317 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 206 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 123 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 215 3 99% +app/rag/llm.py 244 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 192 3 98% +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 24 0 100% +app/schemas.py 329 0 100% +-------------------------------------------------- +TOTAL 4147 16 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 diff --git a/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__01_themed_scrollbar.a1.err b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__01_themed_scrollbar.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__01_themed_scrollbar.a1.md b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__01_themed_scrollbar.a1.md new file mode 100644 index 0000000..8ed077e --- /dev/null +++ b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__01_themed_scrollbar.a1.md @@ -0,0 +1,18 @@ +Task 01 complete. + +**Implemented** +- `frontend/assets/styles.css` — added `--scrollbar-thumb` / `--scrollbar-track` tokens to the `:root` block, derived via `color-mix()` from identity tokens (same pattern as phase-92 derived state colors: not stored, not tab-controlled, not in `BUILTIN_COLORS` — every saved theme repaints them) +- Added the scoped rules on `.doc-modal .doc-raw` (the `pre` that owns its `overflow-x: auto` + `white-space: pre`): `scrollbar-width: thin` + `scrollbar-color` (Firefox) and the `::-webkit-scrollbar{height:8px}` / `-track` / `-thumb{bg, radius:4px}` pseudos (Chromium) — no global restyle + +**Results** +- `uv run pytest --cov=app --cov-report=term` → 2457 passed, 99% coverage on `app/` (gate >90%) +- `uv run pytest tests/e2e/test_dark_tech_theme.py -v --no-cov` → 6 passed (in isolation) +- `uv run pytest tests/e2e/test_document_viewer.py -v --no-cov` → 7 passed (regression) +- `uv run ruff check . && uv run pyright` → All checks passed / 0 errors +- Headless-Chromium sanity check: computed `scrollbar-color` = thumb `#796b6b` on track `#170e0e` (3.7:1 ≥ 3:1 non-text AA), 8px webkit bar, 4px radius, pre scrolls (1598 > 1056) + +**Decisions** +- Track = `color-mix(surface 75%, bg)` (near the `.doc-raw` bg, slightly recessed); thumb = `color-mix(ink-soft 60%, surface)` for a muted ≥3:1 thumb (50% measured 2.97:1 — rejected) +- No `app/` code change; no line-wrap (locked A1 honored) + +**Next pending task:** `02_scrollbar_e2e_check.md` (E2E scroll check + screenshot) diff --git a/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__01_themed_scrollbar.a1.validate b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__01_themed_scrollbar.a1.validate new file mode 100644 index 0000000..6b977c2 --- /dev/null +++ b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__01_themed_scrollbar.a1.validate @@ -0,0 +1,103 @@ +........................................................................ [ 2%] +........................................................................ [ 5%] +........................................................................ [ 8%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 20%] +........................................................................ [ 23%] +........................................................................ [ 26%] +........................................................................ [ 29%] +........................................................................ [ 32%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 41%] +........................................................................ [ 43%] +........................................................................ [ 46%] +........................................................................ [ 49%] +........................................................................ [ 52%] +........................................................................ [ 55%] +........................................................................ [ 58%] +........................................................................ [ 61%] +........................................................................ [ 64%] +........................................................................ [ 67%] +........................................................................ [ 70%] +........................................................................ [ 73%] +........................................................................ [ 76%] +........................................................................ [ 79%] +........................................................................ [ 82%] +........................................................................ [ 84%] +........................................................................ [ 87%] +........................................................................ [ 90%] +........................................................................ [ 93%] +........................................................................ [ 96%] +........................................................................ [ 99%] +......... [100%] +=============================== warnings summary =============================== +.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain-of-reese/.venv/lib64/python3.14/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.14.7-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 224 1 99% +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 156 1 99% +app/api/git_sources.py 232 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 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 210 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 66 0 100% +app/models.py 128 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 317 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 206 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 123 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 215 3 99% +app/rag/llm.py 244 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 192 3 98% +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 24 0 100% +app/schemas.py 329 0 100% +-------------------------------------------------- +TOTAL 4147 16 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 diff --git a/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__02_scrollbar_e2e_check.a1.err b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__02_scrollbar_e2e_check.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__02_scrollbar_e2e_check.a1.md b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__02_scrollbar_e2e_check.a1.md new file mode 100644 index 0000000..aab23bf --- /dev/null +++ b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__02_scrollbar_e2e_check.a1.md @@ -0,0 +1,15 @@ +Task 02 complete — all gates green. + +**Implemented** +- `tests/e2e/test_document_viewer.py` — new test 8 `test_raw_code_block_themed_scrollbar`: seeds a synthetic `notes/long-line.txt` with a ~935-char `quest::say(…)` line (fixture KB checked first — longest raw line is 91 chars ≈ 742px < ~1058px pre client width, so no fixture doc overflows; choice noted in docstring), then asserts real overflow (`scrollWidth > clientWidth`), `overflow-x: auto` + `scrollbar-width: thin`, computed `scrollbar-color` = the `--scrollbar-thumb`/`--scrollbar-track` theme pair, `scrollBy(300)` moves `scrollLeft`, and writes a screenshot to `.agents/screenshots/`. + +**Results** +- `uv run pytest tests/e2e/test_document_viewer.py -v --no-cov` → 8 passed (in isolation) +- `uv run pytest tests/e2e/test_dark_tech_theme.py -v --no-cov` → 6 passed +- `uv run pytest --cov=app --cov-report=term` → 2457 passed, 99% coverage (gate >90%) +- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings + +**Notable decisions** +- Headless Chromium (151) paints no scrollbars at all (verified: no pixels in the bar strip), so the suite's screenshot (`doc_modal_scrollbar_1280.png`) records the scrolled modal state, while the themed bar itself is asserted via computed style and recorded in a one-off **headed** capture `doc_modal_scrollbar_themed_1280.png` (visually verified: muted rounded thumb mid-track on the dark recessed track, no native light-gray bar). The temp headed-capture test was deleted after use. + +**Next pending task:** none in this phase — both task files of `116_modal_scrollbar_theme` are done; the phase is ready for the harness commit/move. diff --git a/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__02_scrollbar_e2e_check.a1.validate b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__02_scrollbar_e2e_check.a1.validate new file mode 100644 index 0000000..6b977c2 --- /dev/null +++ b/.agents/reports/116_modal_scrollbar_theme/116_modal_scrollbar_theme__02_scrollbar_e2e_check.a1.validate @@ -0,0 +1,103 @@ +........................................................................ [ 2%] +........................................................................ [ 5%] +........................................................................ [ 8%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 20%] +........................................................................ [ 23%] +........................................................................ [ 26%] +........................................................................ [ 29%] +........................................................................ [ 32%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 41%] +........................................................................ [ 43%] +........................................................................ [ 46%] +........................................................................ [ 49%] +........................................................................ [ 52%] +........................................................................ [ 55%] +........................................................................ [ 58%] +........................................................................ [ 61%] +........................................................................ [ 64%] +........................................................................ [ 67%] +........................................................................ [ 70%] +........................................................................ [ 73%] +........................................................................ [ 76%] +........................................................................ [ 79%] +........................................................................ [ 82%] +........................................................................ [ 84%] +........................................................................ [ 87%] +........................................................................ [ 90%] +........................................................................ [ 93%] +........................................................................ [ 96%] +........................................................................ [ 99%] +......... [100%] +=============================== warnings summary =============================== +.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain-of-reese/.venv/lib64/python3.14/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.14.7-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 224 1 99% +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 156 1 99% +app/api/git_sources.py 232 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 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 210 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 66 0 100% +app/models.py 128 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 317 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 206 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 123 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 215 3 99% +app/rag/llm.py 244 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 192 3 98% +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 24 0 100% +app/schemas.py 329 0 100% +-------------------------------------------------- +TOTAL 4147 16 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 diff --git a/.agents/screenshots/doc_modal_scrollbar_1280.png b/.agents/screenshots/doc_modal_scrollbar_1280.png new file mode 100644 index 0000000..1429699 Binary files /dev/null and b/.agents/screenshots/doc_modal_scrollbar_1280.png differ diff --git a/.agents/screenshots/doc_modal_scrollbar_themed_1280.png b/.agents/screenshots/doc_modal_scrollbar_themed_1280.png new file mode 100644 index 0000000..a566c4b Binary files /dev/null and b/.agents/screenshots/doc_modal_scrollbar_themed_1280.png differ diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css index de425a1..081337e 100644 --- a/frontend/assets/styles.css +++ b/frontend/assets/styles.css @@ -35,6 +35,19 @@ --err-ink: #fca5a5; /* 9.3:1 on --err-bg */ --err-line: #ef4444; /* 4.6:1 on --err-bg (UI boundary, not text) */ + /* Scrollbar tokens (phase 116, task 01) — the document modal's raw + code block scrolls horizontally (locked A1: code never wraps), so + its scrollbar must match the theme instead of the browser-native + light bar. Like the derived state colors above, COMPUTED in :root + from the identity palette via color-mix(): NOT stored, NOT + tab-controlled, NOT in BUILTIN_COLORS (theming.py) — every saved + theme repaints them. Muted thumb on a near-code-background track + (the .doc-raw background is --surface); the thumb holds >= 3:1 + non-text contrast against the track (WCAG 2.1 AA — 3.7:1 at the + built-in palette). */ + --scrollbar-thumb: color-mix(in srgb, var(--ink-soft) 60%, var(--surface)); + --scrollbar-track: color-mix(in srgb, var(--surface) 75%, var(--bg)); + --radius: 10px; --radius-sm: 6px; --shadow: 0 1px 2px rgb(0 0 0 / 0.30), 0 4px 16px rgb(0 0 0 / 0.35); @@ -4330,6 +4343,26 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } overflow: auto; padding: 1rem 1.25rem 1.5rem; } +/* Themed horizontal scrollbar for the modal's raw code block (phase 116, + task 01): the .doc-raw pre already owns its overflow (its own + `overflow-x: auto` + `white-space: pre` — locked A1, code never + wraps), so the native light-gray bar a long line reveals is the pre's + own. Both engine families are themed, scoped to THIS modal only (no + global scrollbar restyle — the TODO's scope): scrollbar-color / -width + for Firefox, the ::-webkit-scrollbar pseudos for Chromium (height 8px + = the horizontal bar; the pre does not scroll vertically). The tokens + follow the saved theme via the :root color-mix derivation; the muted + thumb keeps >= 3:1 non-text contrast on the near-surface track. */ +.doc-modal .doc-raw { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); +} +.doc-modal .doc-raw::-webkit-scrollbar { height: 8px; } +.doc-modal .doc-raw::-webkit-scrollbar-track { background: var(--scrollbar-track); } +.doc-modal .doc-raw::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: 4px; +} .doc-modal-loading { margin: 1.5rem auto; text-align: center; color: var(--ink-soft); } /* No motion under reduced motion (same pattern as the phase-25 layers). */ diff --git a/tests/e2e/test_document_viewer.py b/tests/e2e/test_document_viewer.py index 348b890..9d94b7b 100644 --- a/tests/e2e/test_document_viewer.py +++ b/tests/e2e/test_document_viewer.py @@ -29,6 +29,10 @@ Test → story mapping (Playwright Mapping Rule): inner content). 7. ``test_modal_theme_and_no_cdn`` — dark page background, the panel on the Phase-08 surface colour, every asset same-origin or ``data:``. +8. ``test_raw_code_block_themed_scrollbar`` — phase 116: the raw code + block (``pre.doc-raw``) overflows horizontally, a scroll action moves + it, the themed scrollbar tokens are in effect via computed style, and + a screenshot records the themed (not native light-gray) bar. """ from __future__ import annotations @@ -51,6 +55,7 @@ from e2e.auth_helpers import login REPO = Path(__file__).resolve().parents[2] FIXTURES = REPO / "tests" / "fixtures" / "docs" +SCREENSHOTS = REPO / ".agents" / "screenshots" # house convention for visual records QUESTION = "How is my Kubernetes cluster set up?" @@ -119,6 +124,33 @@ def _drill(page: Page, *names: str) -> None: page.click(f'#folders-tbody a.folder-link:text-is("{name}")') +def _seed_long_line_doc() -> None: + """Seed the synthetic phase-116 fixture (see the test docstring): + a non-markdown doc whose one long line is deliberately longer than + the modal width — the shape of the TODO's quest .pl ``quest::say`` + line. Direct DB seed, the ``test_modal_xss_safe`` pattern (the + viewer is database-only; no chunks needed).""" + long_line = "quest::say(\"" + ("the quick brown fox jumps over the lazy dog " * 20) + "\")" + with SessionLocal() as db: + db.add( + Document( + source="docs", + path="notes/long-line.txt", + full_path="/tmp/long-line.txt", + title="Long Line Fixture", + content=( + "# long-line-fixture: the line below is deliberately\n" + "# longer than the document modal's width (phase 116).\n" + + long_line + + "\n" + ), + content_hash="e" * 64, + indexed_at=datetime.now(UTC), + ) + ) + db.commit() + + # --------------------------------------------------------------------------- # 1. Chat source chip → SAME-PAGE modal (no new tab) # --------------------------------------------------------------------------- @@ -456,3 +488,112 @@ def test_modal_theme_and_no_cdn( assert refs, "expected local asset references on the chat page" for ref in refs: assert ref.startswith(app_url) or ref.startswith("data:"), f"non-local: {ref}" + + +# --------------------------------------------------------------------------- +# 8. Phase 116: the raw code block scrolls horizontally with a themed +# scrollbar (task 01's scoped rules) — never the native light-gray bar +# --------------------------------------------------------------------------- + + +def test_raw_code_block_themed_scrollbar( + page: Page, app_url: str, mock_llm: int, db_ready: None +) -> None: + """The modal's ``pre.doc-raw`` overflows horizontally, a horizontal + scroll action actually moves it, and task 01's themed scrollbar rules + are in effect (the screenshot is the visual record of the themed bar). + + Document choice (the task says to check the fixture KB first): no + fixture doc qualifies — the longest RAW (non-markdown) fixture line is + 91 chars (``homelab/networking/static-dns.json``) ≈ 742px at the + 0.85rem mono font, well inside the ≈1058px pre client width at the + 1280px fixture viewport (panel 1100px − content padding − pre border). + So this test seeds a synthetic doc instead (the + ``test_modal_xss_safe`` direct-seed pattern): one ≈935-char + ``quest::say(...)`` line — the shape of the TODO's observed quest + .pl line — which guarantees real horizontal overflow. + + Screenshot note: headless Chromium paints NO scrollbars at all + (frame UI is omitted — verified on Chromium 151), so the suite's + screenshot (``doc_modal_scrollbar_1280.png``) is the record of the + scrolled modal state (long line mid-content = overflow + working + scroll, no native bar); the themed bar itself is asserted via + computed style here and recorded in the headed capture + ``doc_modal_scrollbar_themed_1280.png`` (phase 116, task 02). + """ + _reset_db(mock_llm, seed=True) + _seed_long_line_doc() + + login(page, app_url) + _drill(page, "docs", "notes") + row = page.locator("#docs-tbody tr", has_text="long-line.txt") + expect(row).to_have_count(1) + row.locator("td:nth-child(2) a.doc-link").click() + + expect(page.locator(".doc-modal")).to_be_visible() + expect(page.locator("#doc-modal-title")).to_have_text("Long Line Fixture") + pre = page.locator("#doc-modal-content pre.doc-raw") + expect(pre).to_have_count(1) + + # The overflow is real: the long line makes the content wider than the + # box (no line-wrap — locked A1, phase 116). + dims = pre.evaluate("el => ({sw: el.scrollWidth, cw: el.clientWidth})") + assert dims["sw"] > dims["cw"], f"no horizontal overflow: {dims}" + + # The overflow is SCROLLED, not clipped: the element owns its + # overflow-x (task 01's comment in styles.css) and the themed rules + # are in effect — scrollbar-width: thin + scrollbar-color as a + # two-colour pair (the ::-webkit-scrollbar pseudos are not exposed via + # computed style; that half is the screenshot's job). + themed = page.evaluate( + """() => { + const el = document.querySelector('#doc-modal-content pre.doc-raw'); + const cs = getComputedStyle(el); + const probe = (name) => { + const s = document.createElement('span'); + const hidden = 'position:absolute;visibility:hidden;'; + s.style.cssText = hidden + 'background:var(' + name + ')'; + document.body.appendChild(s); + const bg = getComputedStyle(s).backgroundColor; + s.remove(); + return bg; + }; + return { + overflowX: cs.overflowX, + width: cs.scrollbarWidth, + color: cs.scrollbarColor, + thumb: probe('--scrollbar-thumb'), + track: probe('--scrollbar-track'), + }; + }""" + ) + assert themed["overflowX"] == "auto", f"overflow-x is {themed['overflowX']}, not auto" + assert themed["width"] == "thin", f"scrollbar-width is {themed['width']}, not thin" + # The tokens are color-mix() derivations, so Chromium serializes the + # used values as color(srgb …) (plain rgb(…) or #hex also accepted). + colors = re.findall( + r"#[0-9a-fA-F]{3,8}\b|rgb\([^)]*\)|color\([^)]*\)", themed["color"] + ) + assert len(colors) == 2, f"scrollbar-color {themed['color']!r} is not a two-colour pair" + assert colors[0] == themed["thumb"], ( + f"scrollbar thumb {colors[0]} is not the theme's --scrollbar-thumb {themed['thumb']}" + ) + assert colors[1] == themed["track"], ( + f"scrollbar track {colors[1]} is not the theme's --scrollbar-track {themed['track']}" + ) + + # A horizontal scroll action actually moves it (no clipping — the + # rest of the line is reachable). The pre is freshly rendered, so + # scrollLeft starts at 0. + before = pre.evaluate("el => el.scrollLeft") + assert before == 0, f"expected scrollLeft 0 before the action, got {before}" + pre.evaluate("el => el.scrollBy({left: 300, behavior: 'instant'})") + after = pre.evaluate("el => el.scrollLeft") + assert after > 250, f"scrollBy(300) only moved to {after}" + + # Visual record (house convention): the bar at the pre's bottom is the + # themed one — scroll to mid-content so the thumb sits mid-track. + pre.evaluate("el => el.scrollTo({left: el.scrollWidth / 2, behavior: 'instant'})") + shot = SCREENSHOTS / "doc_modal_scrollbar_1280.png" + page.screenshot(path=str(shot)) + assert shot.exists() and shot.stat().st_size > 0