Converts the TODO list into the phased-execution roadmap (Protocol B append): incremental folder summaries on sync (127), the mobile sync label with the pinned (n/m) count (128), per-chunk embed progress in the sync label (129), chat answer clusters with the bottom-right actions row (130), read(offset/limit) line ranges for huge documents (131), visible chat image-upload feedback (132), and the garbage-PDF text gate with the page-screenshot vision fallback (133 — includes the diagnosed root cause of the hnb guide's pre-phase-126 binary import). Each phase carries its dedicated in-isolation Playwright suite; TODO.md is cleared (items now live in .agents/phases/todo/). LLM-Generated: true
7.1 KiB
Phase 129 — The sync label shows what chunk we're on out of how many
Source: TODO.md L5 — "The Syncing… name/of/file/here (2/3) needs to show what chunk we're on out of how many" (ASSUMPTION A6, owner-confirmed 2026-09-27: "chunk" = the current file's EMBEDDING CHUNKS; position reported at BATCH granularity — n = first chunk of the in-flight token-budgeted embed batch — because the transport embeds batches per HTTP call and per-chunk calls would be a latency regression; the summary-chunk embed is excluded from the counter; label Syncing… <file> (2/3) · chunk 14/38).
Story: n/a (TODO-derived phase, Protocol B append; a NEW feature — distinct from phase 128's mobile fix, per the owner).
Context: app/rag/llm.py — LLMClient.embed(texts) (L346: iterates self._batch_texts(texts) — token-budgeted batches, BOR_EMBED_BATCH_SIZE cap, _REQUEST_TOKEN_BUDGET ≈ 2800 chars), _embed_batch (L289: the halving backstop on _TooLarge), self.embed_batches counter (L249); app/rag/importer.py — import_sources (L408, the per-file progress(source, rel, done, total) hook fired BEFORE each _index_file), _index_file (L647 — text branch → _replace_content_chunks, image branch → _index_image_file, PDF branch → _index_pdf_file), _replace_content_chunks (L603 — chunks the content, llm.embed([c.content for c in doc.chunks]) in ONE call, token-cap re-chunk retry loop), _store_summary (embeds the ONE summary chunk — EXCLUDED from the counter); app/api/sync.py — SyncStatus (the phase-64/98 progress fields: current_file, files_done/total, phase, current_summary, summaries_done/total; _run_sync's _hook closure convention; terminal-state clearing); frontend/assets/sources.js — the phase-128 part builders (setSyncLabel, .sync-label-main / .sync-label-count); tests/e2e/slow_llm.py + tests/e2e/test_sync_summary_visibility.py — the slow-proxy sampling pattern.
Objective
While a file is being imported, the sync status (endpoint + button label) shows the file's embedding-chunk position: Syncing… <file> (2/3) · chunk 14/38 — 14 = the first chunk of the embed batch in flight, 38 = the file's total content chunks. The position climbs through the file's batches and is cleared when the file (or the run) moves on. Everything that passes no callback stays a zero-cost no-op (the CLI, the chat path).
Dependencies
128_sync_label_mobile(todo) — the label's part structure (.sync-label-main+ pinned count span) is the carrier; this phase adds the pinned chunk span.
Design (shared by all tasks — the executor reads this, not the chat)
- The embed hook (task 01):
LLMClient.embed(texts, on_progress: Callable[[int, int], None] | None = None)— before EACH top-level batch request,on_progress(first_1based, len(texts))wherefirst_1based= 1 + the cumulative chunk counts of the already-sent batches.None(every existing caller — the chat retrieval path,embed_one, the summarizer, the CLI) is a zero-cost guarded no-op. The_embed_batchhalving backstop does NOT sub-report — the top-level loop reports batch starts; a halved batch means the position is approximate (document in the docstring; the backstop is rare by construction). - Importer plumbing (task 02):
import_sources(..., on_chunk_progress: Callable[[int, int], None] | None = None)— a new keyword-only param, plumbed:import_sources→_index_file(..., on_chunk=...)(all three branches: text, image, PDF — the PDF branch's content embed is the same_replace_content_chunkscall; the image branch's ONE description-chunk embed reports1/1— acceptable, it IS the file's only chunk) →_replace_content_chunks(..., on_chunk=...)→llm.embed([...], on_progress=on_chunk). The token-cap re-chunk retry re-embeds with the NEW (smaller-target) chunk list — the hook naturally reports against the new total (no special case)._store_summary's single summary-chunk embed NEVER receives the hook (the counter is content chunks only — no1/1noise after the real progress ends). Thescripts/import_docs.pyCLI passes nothing — byte-identical. - Sync status (task 03):
SyncStatusgainscurrent_chunk: int = 0andchunks_total: int = 0(documented: the current file's embed-batch position — 0/0 before the first batch and in terminal states)._run_sync: reset to 0/0 at run start (with the other progress fields); a closure_chunk_hook(chunk, total)(the file's_hookconvention) sets them; passed ason_chunk_progresstoimport_sources. CLEARED (0/0) when the import phase ends — set at thephase = "overview"andphase = "summaries"transitions AND in both terminal states (thecurrent_fileclearing points — same convention, same spots).GET /api/sync/statusexposes both fields (0/0 idle — the dataclass defaults). TheSyncStatusdocstring + module docstring get the chunk-position paragraph (the phase-64/98 style). - UI (task 04):
sources.js— the import-phase label gains a THIRD pinned part.sync-label-chunk(markup inindex.html's#sync-label,hiddenby default): whenstatus.chunks_total > 0the import fall-through emits· chunk ${status.current_chunk}/${status.chunks_total}into it; every other phase/state leaves it hidden/empty (the summaries phase has its own(n/m)— no chunk part; the prelude has no file yet). The UNTRUNCATED concatenation (main + count + chunk part) rides the buttontitle+#sync-result(A4). The pinned unit tests (tests/unit/test_frontend_sync_upload.py) gain the chunk-part pins.
Tasks
01_llm_embed_hook.md—LLMClient.embed's optional per-batchon_progress.02_importer_chunk_progress.md—import_sources→_index_file→_replace_content_chunks→embedplumbing.03_sync_status_chunk_fields.md—SyncStatusfields, the closure, clearing, the endpoint.04_ui_chunk_part.md— the label's chunk part + unit pins.05_e2e_chunk_progress.md— dedicated Playwright E2E, run in isolation.
Testing & Quality
- Unit/integration: the embed hook's call sequence (batch boundaries,
Noneno-op, halving-approximate documented); the importer plumbing (hook fires per file with its own total; the CLI path unchanged; the summary-chunk embed never fires it); the sync status (fields in the endpoint JSON, reset per run, cleared at phase transitions + terminal, 0/0 idle). - Coverage: >90% on new/modified code (
app/). - E2E: one dedicated Playwright file, run in isolation, slow-LLM sampling.
Completion Criteria
GET /api/sync/statusreportscurrent_chunkclimbing 1 → N withchunks_total= the file's true content-chunk count while a file imports (E2E, sampled)- the rendered mobile/desktop label shows
· chunk n/mduring the import phase only; the untruncated text ridestitle+#sync-result uv run pytest --cov=app --cov-report=term-missinggreen,app/coverage >90%uv run pytest tests/e2e/test_sync_chunk_progress.py -v --no-covgreen in isolation- no behavior change in completed phases (chat embed path, CLI import, the phase-64/98 label contract minus the new part)