Protocol B append (phases 146-149) for the four TODO items: the search tool line missing the grep's document scope (146), sync-only- what-changed via a raw-file byte hash checked before extraction and vision (147), pictorial PDFs showing their rendered pages in the sources viewer instead of page descriptions (148), and the sync detail moved to a text box under the button with the button as a spinner (149). TODO.md cleared (items now live in .agents/phases/ todo/). LLM-Generated: true
9.2 KiB
Phase 149 — sync detail lives in a text box under the button; the button is just the spinner
Source: TODO.md L6 — "The "Sync Sources" button now has too much detail and you can't see all of it - it gets cut off. I get that this was the solution to show information on mobile, but can't we just show that information in a nice text box under the button instead to give us more breathing room? Then the button can just be used as a loading spinner."
Story: n/a (TODO-derived phase, Protocol B append).
Context: the Sources page (frontend/index.html, the view-rag section): #sync-btn (L465 — the pill: .sync-icon + .sync-label with the three PARTS — .sync-label-main (the shrinks-to-fit prefix + file/folder, the part CSS ellipsizes), .sync-label-count (the pinned "(n/m)"), .sync-label-chunk (the phase-129 pinned "· chunk n/m")) + #sync-cancel-btn (L477, the phase-134 Cancel control) in the .page-head-row; #sync-result (L500 — the role="status" aria-live announcer: the LIVE untruncated label while a sync runs, the settle counts after); #sync-error-banner (L502). The single label writer: frontend/assets/sources.js setSyncLabel(mainText, countText, chunkText) (L759 — writes the parts, then the UNTRUNCATED full label rides the button 810–850: "Syncing… (n/m) · chunk n/m", "Writing KB overview…", "Summarizing folders… (n/m)"), the idle/settle labels, the 202/409 click, the re-attach). The running state: title + #sync-result — the A4 announcer contract; EVERY label state goes through it: the running builders fmtSyncLabel/fmtSyncPhaseLabel (Lsources.js ~L909 adds .is-spinning to the icon (the CSS spin at 1s — styles.css L2200, with the prefers-reduced-motion: none kill at L2202) and ~L928 removes it on settle; the label's ellipsis lives in styles.css (L2216–2239 — .sync-label-main min-width 0 + ellipsis; the mobile media query at L5516+ makes the button full-width and the label fill the pill). The owner's complaint: the FULL live label (a long source/relative/path + counts + chunk) is clipped in the pill even on desktop — the mobile solution (stuffing the parts in the button) is now the permanent surface.
Locked (owner delegation 2026-09-30, chat — "pick the best choices"): while a sync/upload run is in flight, the button shows ONLY the spinning icon (the label parts hidden — the button IS the loading spinner, exactly as the owner asked; its aria-label carries the full live label for AT), and a NEW bordered text box directly under the button row (#sync-progress) shows the SAME full untruncated live label — wrapping, mono, breathing room — hidden at idle/settle. #sync-result REMAINS the sole aria-live announcer (the box is presentational, aria-hidden — one live region, no double announce); the settle counts line still lands on #sync-result exactly as today, and the idle "Sync sources" label returns to the button when the run ends. The box appears for BOTH run kinds (sync + the phase-90 upload "Importing…" run — the shared writer covers both) and re-attaches (a page load mid-run reveals it, the never-stale contract).
Objective
The live sync detail (file, position, chunk, phase labels) is fully visible in a calm text box under the button at every viewport, the button reduces to a clean spinner while running, and the accessibility contract (one live region, the button's accessible name, focus-visible, contrast) is preserved.
Dependencies
148_pdf_pages_in_sources(todo) — pipeline order only; no shared code (this phase touches the Sources-page sync UI inindex.html/sources.js/styles.css; 148 touches the doc viewer + the importer/docs API).
Design (shared by all tasks — the executor reads this, not the chat)
- The box (task 01):
#sync-progress— a<div class="sync-progress" hidden aria-hidden="true">with an inner<span class="sync-progress-text" id="sync-progress-text"></span>, placed inindex.htmlBETWEEN the.page-headblock and#sync-result(directly under the button row — the owner's "under the button").sources.js: element constants beside the existing ones (~L707–714);setSyncLabel(the SINGLE writer — the invariant: every label state, running or not, flows through here) writes the full label into the box's text AND reveals the box for the RUNNING label states (the running builders' outputs + the re-attach running entry) and HIDES it for every non-running state (idle "Sync sources", the settle "Synced HH:MM" + counts, the 202/409 clicks, the error state) — the reveal/hide decision lives at thesetSyncLabelcallsites via a smallrunningflag parameter (or a dedicatedsetSyncProgress(full, running)called from the same sites — the executor picks the minimal clean shape; the CONTRACT is: the box is visible iff a run is in flight, and its text is exactly the full labelsetSyncLabelcomputes). The button:#sync-btngainsis-runningclass while in flight (added where.is-spinningis added, ~L909; removed where it is removed, ~L928 — including the error settle and the 409 path); itsaria-labelis set to the full live label while running (the icon-only button needs an accessible name — thetitlealready carries it for hover, the aria-label for AT) and restored to"Sync sources"on every settle. The Cancel button is untouched (it already rides the running state). - The styling (task 02):
styles.css—.sync-progress: the bordered panel (1px border in the theme border var, the theme surface/background var, the house radius, mono text invar(--mono)— the.sync-label-count/.sync-resultmono language: a position reads as data,overflow-wrap: anywherefor the longsource/relative/path(it must WRAP, never clip — the whole point), a comfortable min-height (~2 lines' breathing room, no jump when empty→full because it is hidden when idle), margin below the button row, inside the existing 72rem centered container (the page already rides it — no new width logic);.sync-btn.is-running .sync-label { display: none; }(the pill shrinks to an icon-only control — the width adjusts naturally; the.sync-btn:disabled/cursor behavior while running is preserved as today); the mobile media query (L5516+): the button stays full-width, the box full-width, the label-hidden rule identical (no mobile special-case — the box REPLACES the old mobile-stuffing reason, which is exactly what the owner asked);prefers-reduced-motionuntouched (the box is static text; the spinner kill already exists); contrast AA in both themes (the ink vars — the muted.sync-resultlanguage is ≥4.5:1 already; the box reuses it); focus-visible on the button + Cancel unchanged. - The E2E (task 03): the
slow_llmproxy idiom (thetest_sync_chunk_progress.pyrecipe — a module app withBOR_LLM_BASE_URL= thetests/e2e/slow_llm.pydelay proxy in front of the mock, ~1.5s per LLM call, the fixture repo with a few docs so the import phase has many polls of observable running state) — observe the in-flight state deterministically.
Tasks
01_progress_box_wiring.md— the box markup + thesetSyncLabelwiring (reveal/hide contract) + the button'sis-running+ aria-label.02_progress_box_styles.md— the panel + the icon-only running button + mobile + the AA/motion gates.03_e2e_progress_box.md— the dedicated E2E: in-flight (spinner-only button + full untruncated box), settled (box hidden, counts line, label restored).
Testing & Quality
- E2E: one dedicated Playwright file, run in isolation, mock-only (slow proxy).
- Frontend pins: the pre-existing sync E2Es (label parts, chunk progress, mobile label, cancel, summary visibility, upload progress) pass UNCHANGED in their settled/idle assertions; their RUNNING-state assertions that pinned the button label text are updated to the box (the executor finds them via the
sync-labelselectors and migrates the expectation to#sync-progress— the A4 announcer (#sync-result) is asserted unchanged). - Coverage: n/a (frontend phase —
app/coverage stays >90%).
Completion Criteria
- while a sync (or upload) run is in flight: the button shows only the spinning icon (no visible label text, at any viewport), its
aria-labelcarries the full live label, and#sync-progressis visible with the EXACT full untruncated label (a longsource/relative/pathwraps — never clipped) - at settle/idle: the box is hidden, the button shows "Sync sources" (the settle "Synced HH:MM" + counts line on
#sync-resultis byte-identical to pre-phase), and the label parts are restored #sync-resultremains the sole aria-live region (the box isaria-hidden); a page load mid-run re-attaches with the box visible (the never-stale contract)- WCAG: focus-visible on the button + Cancel, contrast ≥4.5:1 both themes,
prefers-reduced-motionrespected (the spinner kill intact) uv run pytestgreen (all pre-existing E2Es — running-state label pins migrated to the box, settled-state pins untouched);uv run pytest tests/e2e/test_sync_progress_box.py -v --no-covgreen in isolation;uv run ruff check . && uv run pyrightclean- no behavior change in completed phases (the cancel control, the 409 double-trigger, the error banner/modal, the upload run's status, the sync status API)