Convert the three unchecked TODO.md items into an executable phase roadmap (Protocol B, appended after phase 92): - 93_theme_semantic_completion (TODO L3): the ok/err/accent state families become Theme-tab-controlled (B3 revised, owner permission 2026-09-10) + surface panels behind every page head - 94_ls_tree_drilldown (TODO L4): ls becomes a source -> folder -> file tree with sync-time lite-model folder summaries; controlled tool-calling battery as the accuracy/performance gate - 95_read_truncation_cap (TODO L5): read capped at BOR_READ_MAX_CHARS (128k chars ~= 32k tokens, spec'd on the 128k-token minimum context), LLM-visible truncation notice pointing at grep, new tool_result SSE event (A15 extension) + the visible UI marker Owner decisions (B3 / A7 scope / A15) are recorded in the phase files; .agents/PLAN.md is being redone separately per the owner.
10 KiB
Phase 93 — Theme completion: semantic state colors become tab-controlled + readable page heads
Source: TODO.md L3 — "I created a black/white/gray theme for brain of reese and found multiple cases of color still in the UI which tells me the customization is not complete. Screenshots are in the theme_fixes/ folder. Note the green text "Theme saved", the red "Revoked" tag, the red "Stale" tag, The green "Local" tag, The yellow "Listing documents" and "Reading" tool calls. Also the header and description of each page needs a background - the grid makes it hard to read."
Story: n/a (owner TODO item — theme-customization completion on 91_admin_theme_tab / 92_theme_save_and_coverage).
Context: The admin Theme tab (phases 91/92) persists the 9 identity variables (bg, surface, ink, ink_soft, line, grid_line, brand, brand_soft, brand_ink) in the single-row ui_settings table (app/models.py UiSettings), resolves them in app/core/theming.py (BUILTIN_COLORS / COLOR_FIELDS / effective_settings), serves them via GET/PUT /api/ui-settings (app/api/ui_settings.py), injects them pre-paint as <style id="bor-theme">:root{…}</style> with a matching CSP hash (theme_style_tag / theme_csp_hash / app/core/caching.py), and edits them live in frontend/assets/theme.js (the FIELDS array drives pickers, live preview, PUT body, contrast warnings). Phase 92 removed every hardcoded color literal, but the semantic state families are raw :root variables the tab cannot reach — --ok-bg/--ok-ink (the green "Theme saved." result text, the green LOCAL badge .git-source-kind.is-local), --err-bg/--err-ink/--err-line (the rose Stale / Revoked pills .stale-pill), --accent-bg/--accent-ink/--accent-line (the amber agent tool-call lines .tool-call), built-in values at frontend/assets/styles.css L29–36. Separately, every page's h1 + description (.page-head) sits directly on the background grid texture and is hard to read (all 8 screenshots in theme_fixes/).
Objective
Make theme customization complete: a monochrome (black/white/gray) theme can be saved with zero residual color, because the three semantic state families become Theme-tab-controlled (17 palette variables total) with NULL = built-in (the default theme renders byte-identical to today); and every page's header + description block gets a solid surface background so the grid never fights the heading text. State stays honest: every state element keeps its text label (the "text + color, never color alone" house rule), so a grayscale theme conveys state by words, not hues.
Owner-permitted decision recorded here (PLAN.md is being redone by the owner)
- B3 revised (owner permission 2026-09-10, TODO.md L3): the semantic families
--ok-*,--err-*,--accent-*become storable and Theme-tab-controlled (the 2026-09-09 lock that they "are NOT identity … not configurable from the tab" is lifted).NULL = built-in, so the built-in theme's look and all existing AA ratios are unchanged; only the tab's reachable surface grows. - Header panel design (owner-permitted by the same TODO line — "needs a background"): a solid
var(--surface)panel behind each page head, not a full-bleed band (ASSUMPTION in task 03).
Dependencies
92_theme_save_and_coverage(complete) — the whole phase builds on it: theui_settingsrow + resolver + admin API, the pre-paint injection + CSP hash inapp/core/caching.py, the#view-themeform (frontend/index.html) + editor (frontend/assets/theme.js), and the E2E suitestests/e2e/test_admin_theme_tab.py/test_theme_save_and_coverage.py/test_configurable_brand.py(all stay green — a no-op theme must remain byte-identical).
Design (shared by all tasks — the executor reads this, not the chat)
17-variable palette (tasks 01, 02)
The 9 identity variables are untouched. 8 semantic variables join the storable palette, column names mirroring the CSS variables:
| Column / field | CSS var | Built-in (from styles.css :root) |
UI role |
|---|---|---|---|
ok_bg |
--ok-bg |
#10241b |
success pill/text background ("Theme saved.") |
ok_ink |
--ok-ink |
#6ee7a8 |
success text |
err_bg |
--err-bg |
#2d0a0a |
Stale/Revoked pill background |
err_ink |
--err-ink |
#fca5a5 |
Stale/Revoked pill text |
err_line |
--err-line |
#ef4444 |
Stale/Revoked pill border (decorative — no contrast duty) |
accent_bg |
--accent-bg |
#2b2110 |
deflection banner / tool-line background |
accent_ink |
--accent-ink |
#fbbf24 |
tool-call line text ("Listing…", "Reading…") |
accent_line |
--accent-line |
#f59e0b |
tool-line left border / deflection border (decorative) |
Server plumbing mirrors the phase-92 grid_line pattern exactly: migration 0016 (after 0015_grid_line.py), 8 nullable String(7) columns, BUILTIN_COLORS extended (the :root-drift test in tests/unit/test_theming.py parses the built-ins from styles.css — no new hardcoded palette copy), COLOR_FIELDS order = the 9 identity vars then the 8 semantic vars (structural first, brand middle, state last). Everything downstream is COLOR_FIELDS-driven and picks the 8 up with zero logic change: effective_settings, theme_style_tag (the pre-paint tag gains 8 declarations when any var is non-default; the byte-identical no-op contract holds — the tag is still "" only when all 17 equal their built-ins), theme_csp_hash (recomputed per theme, runtime), the app/core/caching.py injection, and the COLOR_FIELDS-driven loops in app/api/ui_settings.py.
Contrast pairs 5 → 8 (task 02)
The tab's client-side AA warnings (theme.js PAIRS, L184) gain three ink-on-bg pairs: ok_ink on ok_bg, err_ink on err_bg, accent_ink on accent_bg. The two _line vars stay excluded (decorative borders, no contrast duty — same rule as --line/--grid-line). The authoritative table lives in app/core/theming.py's docstring — extend it there and in theme.js's pair list together.
Page-head panel (task 03)
.page-head (the shell's standard frame: h1 + description — styles.css L1536 ff.) gets background: var(--surface), padding, border: 1px solid var(--line) and the house card radius, on EVERY page: the seven shell views (#view-chat, #view-tuning, #view-rag, #view-git-sources, #view-history, #view-tokens, #view-theme — one shared rule covers them) plus the standalone pages' header blocks (frontend/login.html, frontend/document.html, frontend/shared.html, frontend/doc-edit.html — audit each; apply the same panel treatment to whatever class carries their h1 + lede, including the shared-page head that reuses the .page-head h1 size per the styles.css L3189 note). The panel must not break: the #view-history .page-head flex row (title left, stale pill right — styles.css L2550), the mobile wrap of .page-head-row (L4221), or any existing header/nav/responsive E2E.
Monochrome E2E (task 04)
One dedicated story suite: as admin, PUT /api/ui-settings an all-gray 17-color theme (every channel R=G=B; pairs still ≥ 4.5:1), then assert per element that the computed color is grayscale and the text state label is still present: "Theme saved." result (#theme-result), a Stale pill (History — seed an out-of-generation saved chat, the phase-53 pattern in tests/e2e/test_stale_saved_chats.py), a Revoked pill (Tokens — create + revoke via the UI), the LOCAL badge (Git sources — register a local-directory source, the tests/e2e/test_local_directory_sources.py pattern), and a .tool-call line (Chat — a mock-LLM turn that executes a tool, the tests/e2e/test_agent_document_tools.py pattern). Plus: every page head's computed background-color is non-transparent, and a fresh page.goto of each page paints the gray palette pre-paint (the #bor-theme tag carries all 17 when non-default).
Tasks
01_semantic_columns.md— migration 0016 +UiSettingscolumns +BUILTIN_COLORS/effective_settings+ schema + API validation02_theme_tab_state_section.md—#view-themeform fields +theme.jsFIELDS/PAIRS(5→8 pairs) + live preview/Reset coverage03_page_head_background.md—.page-head(and standalone-page) surface panel across every page04_e2e_monochrome_theme.md— dedicated Playwright suite: full-gray theme → zero residual color, state text intact, heads readable
Testing & Quality
- Unit/integration: resolver round-trip for all 17 (NULL=clear, built-in→NULL normalization, 422 on bad hex names the field); pre-paint tag + CSP hash with the 17 vars (non-default subset, no-op byte-identical); drift test covers the 17;
PUT/GET /api/ui-settingsintegration for the 8 new fields; existing theming suites (tests/unit/test_theming.py,test_ui_settings.py,test_caching.py,tests/integration/test_ui_settings_api.py,test_security_headers.py) stay green. - Coverage: >90% on
app/(uv run pytest --cov=app --cov-report=term-missing). - E2E:
uv run pytest tests/e2e/test_theme_semantic_completion.py -v --no-covin isolation; the existing theme suites (test_admin_theme_tab.py,test_theme_save_and_coverage.py,test_configurable_brand.py,test_dark_tech_theme.py,test_header_consistency.py,test_nav_*, responsive) stay green in isolation. - Lint/types:
uv run ruff check . && uv run pyright.
Completion Criteria
- An all-gray 17-color theme saved from the tab renders with no colored element anywhere (verified by the new E2E's computed-style assertions) while Stale/Revoked/Local/Tool state text remains visible
- Default (no row / all NULL) deployment serves byte-identical HTML (no
#bor-themetag) and the built-in theme's 8 semantic look is unchanged - Every page head (7 shell views + 4 standalone pages) has a solid surface background over the grid
- test suite green, coverage >90%, ruff + pyright clean, dedicated E2E green in isolation
- no behavior change in completed phases (phases 01–92 suites green)
- one atomic Conventional Commit,
--no-gpg-sign(e.g.feat(theme): make semantic state colors tab-controlled and give page heads a surface panel)