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
6.9 KiB
Phase 130 — Chat answer clusters: chips row, images row, bottom-right actions cluster
Source: TODO.md L6 — "The chat output is getting messy. I think it's because the source-chip, source-image, tune-btn, retry-btn, and save-as-doc-btn end up in the same div which causes them to push each other in weird ways. Those three buttons should be in a button cluster in the bottom right. We don't need to display the source-chip for images, just the image itself is fine. source-chips should show up before images in their own cluster, then images in their own cluster to keep things organized."
Story: n/a (TODO-derived phase, Protocol B append).
Context: frontend/assets/app.js — appendSources (~L1561: builds the FIRST .msg-meta row, interleaving a .source-image figure after each chip for refs carrying image_url), appendRelated (~L1618: the "Nearby docs" row, appended LAST among meta rows), appendSourceImageFigure (~L1660: the compact figure; on img error it removes the figure and the chip remains), appendAnswerImage (~L1735: the .answer-image figure — appended by callers right BEFORE appendSources), appendTuneButton (~L484, admin-gated), appendRetryButton (~L598), appendSaveAsDocButton (~L793, admin + docs-repo gated; margin-inline-start: auto pushes it right), markLastRetryable (~L620: re-appends the Retry button on the last bubble and re-appends the save button after it — "the auto margins split the free space"), appendStoppedNote / appendFailedNote (~L520/~L560: join the first .msg-meta row); frontend/assets/styles.css — the .msg-meta, .source-chip, .source-image, .tune-btn, .retry-btn, .save-as-doc-btn rules (grep each — the auto-margin rules live here); frontend/index.html — no chat markup changes expected (the bubble DOM is JS-built); E2E image-KB fixture patterns in tests/e2e/test_chat_image_questions.py / test_image_documents.py (the mock's IMAGE_DESCRIPTION_MODE canned vision reply makes image docs retrievable).
Objective
A completed brain bubble's metadata reads as organized clusters, top to bottom: the answer text → the answer figure (phase 124, unchanged) → the source chips row (text documents only — an image document gets NO chip, just its figure) → the source images row (all retrieved image figures) → the "Nearby docs" row (unchanged) → the actions row (a right-aligned cluster holding Tune / Retry / Save-as-doc and the Stopped/Failed notes) — never one soup div where buttons push chips around.
Dependencies
129_sync_chunk_progress(todo) — pipeline order only; code dependencies are the completed chat-render contract (phases 26/49/59/113/120/122/124).
Design (shared by all tasks — the executor reads this, not the chat)
- Row structure per brain bubble (top → bottom): (1) answer text; (2)
.answer-imagefigure — the phase-124 position is UNCHANGED (below the text, before the source rows — A3); (3).msg-metachips row (role=list,aria-label="Sources") — one.source-chipper NON-image ref; refs withimage_urlcontribute NO chip (ASSUMPTION A3 — the figure is the citation for image docs); (4).msg-meta.msg-imagesimages row (role=list,aria-label="Source images") — one.source-imagefigure per image ref, in ref order; the row is absent when there are no image refs; (5).msg-meta.related-docs— the phase-113 row, UNCHANGED in content, placed after the images row (ASSUMPTION A4); (6).msg-meta.msg-actionsactions row (role=list,aria-label="Answer actions") — the ONLY row the action buttons + the stopped/failed notes join (ASSUMPTION A7). - Call order at every render site (the
donebranch, the empty-answer fallback, the stop finalize, the phase-14 restore path): answer image →appendSources(now builds the chips row AND the images row) →appendRelated→ the action buttons/notes. The current "buttons claim the FIRST.msg-metarow" behavior is REPLACED — the buttons get-or-create.msg-actionsinstead. - Image-load failure (ASSUMPTION A3):
appendSourceImageFigure'simgerror handler replaces the figure IN PLACE with a plain.source-chipanchor for that document (samedocumentUrlhref, same modal click behavior, the label as text) — the citation survives a lost byte copy; no broken-image icon. - Actions row: one get-or-create
.msg-actionsper bubble (the house pattern). DOM order = Tune, Retry, Save-as-doc (each present per its gate — non-admins see only Retry; a stopped bubble sees the note + Retry). The note (appendStoppedNote/appendFailedNote) joins this row.markLastRetryable's re-append logic moves the Retry button into.msg-actionsand DROPS the save-button re-append hack (row-level alignment replaces the per-buttonmargin-inline-start: auto— CSS task). ARIA:role=list+role=listitemon every child (the existing convention). - CSS:
.msg-actions { justify-content: flex-end; }(the right-aligned cluster — "bottom right" = the bubble's last row, right edge); remove the per-button auto margins (.save-as-doc-btn'smargin-inline-start: auto+ the tune/retry auto margins themarkLastRetryablecomment names); the chips row keeps its wrap;.msg-imageswraps with the.msg-metagap; the row ORDER is DOM order (no positioning tricks); mobile: rows stack naturally, the actions row stays right-aligned.
Tasks
01_sources_clusters.md—appendSources/figure split: chips-only row + images-only row, no chip for image docs, the in-place chip fallback.02_actions_cluster.md— the.msg-actionsrow: the three buttons + the notes,markLastRetryablerework, ARIA kept.03_css_clusters.md— the cluster CSS (actions right-aligned, auto margins removed, images row).04_e2e_clusters.md— dedicated Playwright E2E, run in isolation.
Testing & Quality
- Unit: the frontend unit suite — the pinned behaviors that move (the button-row target, the no-chip-for-images rule, the fallback) — inspect
tests/unit/test_frontend_*.pyfor the chat-render pins and update them. - E2E: one dedicated Playwright file, run in isolation, image-KB fixture (text doc + image doc both cited).
- Coverage: >90% on new/modified code (frontend: unit pins + E2E;
app/untouched this phase).
Completion Criteria
- a bubble citing a text doc + an image doc renders: chips row (text chip only, NO chip for the image) → images row (the figure) → actions row (bottom right) — asserted in the DOM, not the pixels
- Tune/Retry/Save-as-doc sit in ONE
.msg-actionsrow, right-aligned; non-admin sees only Retry; the restore path renders the identical structure - an image whose bytes 404 renders as its plain chip (the citation survives)
uv run pytest tests/e2e/test_chat_answer_clusters.py -v --no-covgreen in isolation- no behavior change in completed phases (chips still open the modal, the related row, the answer figure's position, the ARIA names)