chore(agent): phase roadmap from TODO.md, 7 phases (127-133)
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
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# 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-image` figure — the phase-124 position is UNCHANGED (below the text, before the source rows — A3); (3) `.msg-meta` chips row (`role=list`, `aria-label="Sources"`) — one `.source-chip` per NON-image ref; refs with `image_url` contribute NO chip (ASSUMPTION A3 — the figure is the citation for image docs); (4) `.msg-meta.msg-images` images row (`role=list`, `aria-label="Source images"`) — one `.source-image` figure 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-actions` actions 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 `done` branch, 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-meta` row" behavior is REPLACED — the buttons get-or-create `.msg-actions` instead.
|
||||
- **Image-load failure (ASSUMPTION A3):** `appendSourceImageFigure`'s `img` error handler replaces the figure IN PLACE with a plain `.source-chip` anchor for that document (same `documentUrl` href, 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-actions` per 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-actions` and DROPS the save-button re-append hack (row-level alignment replaces the per-button `margin-inline-start: auto` — CSS task). ARIA: `role=list` + `role=listitem` on 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`'s `margin-inline-start: auto` + the tune/retry auto margins the `markLastRetryable` comment names); the chips row keeps its wrap; `.msg-images` wraps with the `.msg-meta` gap; the row ORDER is DOM order (no positioning tricks); mobile: rows stack naturally, the actions row stays right-aligned.
|
||||
|
||||
## Tasks
|
||||
1. `01_sources_clusters.md` — `appendSources`/figure split: chips-only row + images-only row, no chip for image docs, the in-place chip fallback.
|
||||
2. `02_actions_cluster.md` — the `.msg-actions` row: the three buttons + the notes, `markLastRetryable` rework, ARIA kept.
|
||||
3. `03_css_clusters.md` — the cluster CSS (actions right-aligned, auto margins removed, images row).
|
||||
4. `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_*.py` for 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-actions` row, 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-cov` green in isolation
|
||||
- [ ] no behavior change in completed phases (chips still open the modal, the related row, the answer figure's position, the ARIA names)
|
||||
@@ -0,0 +1,26 @@
|
||||
# Task 01 — Sources split: chips-only row, then images-only row; no chip for image docs
|
||||
|
||||
**Phase:** `130_chat_answer_clusters` · **Source:** `TODO.md:6` — "...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."
|
||||
|
||||
## Objective
|
||||
`appendSources` stops interleaving figures after chips. It builds the chips row from the NON-image refs only, then (when any image ref exists) a separate images row holding all the figures — chips cluster first, images cluster second, image documents cited by their figure alone.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/app.js` — `appendSources(wrap, sources)`:
|
||||
- Partition the refs: `imageRefs = sources.filter(s => s.image_url)`, `textRefs = sources.filter(s => !s.image_url)` (the `image_url` presence is the existing image-doc signal — phase 122).
|
||||
- CHIPS ROW: build it (the existing `.msg-meta` + `role=list` + `aria-label="Sources"` + per-chip `documentUrl`/modal click/`title`/clipped `aria-label` code) from `textRefs` ONLY. Skip the row entirely when `textRefs` is empty (an image-only citation still gets its images row + the actions row).
|
||||
- IMAGES ROW: when `imageRefs.length > 0`, build a SECOND `.msg-meta` row — add the `msg-images` class, `role=list`, `aria-label="Source images"` — and append one `appendSourceImageFigure` per image ref (in ref order). No chips in this row.
|
||||
- Preserve the existing post-build clipped-chip `aria-label` pass (the `scrollWidth > clientWidth` check) over the chips row only.
|
||||
- The row ORDER in `.msg-body` is: answer figure (appended by the caller before `appendSources`, phase 124 — unchanged) → chips row → images row. `appendRelated` (the next caller) lands after both (task 02's call-order change guarantees the actions row is last).
|
||||
2. `frontend/assets/app.js` — `appendSourceImageFigure(meta, s)`: the `img` error handler currently does `fig.remove()` (the chip remains — but there is no chip for image docs anymore). Change it to the IN-PLACE CHIP FALLBACK: build a plain `.source-chip` anchor for `s` (the same `documentUrl` href + `title` + left-click → modal handler the chips row uses; `textContent`/`aria-label` = the `source/path` label) and `replaceWith` the figure (the citation survives a lost byte copy — ASSUMPTION A3). The figure's fetch-summary swap is untouched.
|
||||
3. `frontend/assets/app.js` — `appendRelated`: the related tier's refs ALSO carry `image_url` for image docs (phase 122 — `appendSourceImageFigure(row, s)` is called there today). Keep the related row's own behavior (its chips + figures) — the related row is a separate tier and is NOT reorganized by this phase (ASSUMPTION A4: it stays where it is, below the citation clusters). Do not change `appendRelated`'s chip/figure logic.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: the frontend unit suite — inspect `tests/unit/test_frontend_*.py` for any `appendSources`/figure pins and update: no chip emitted for an `image_url` ref; the images row is built only when image refs exist; the in-place fallback replaces the figure with a chip (the figure's `img` error path).
|
||||
- Coverage: **>90%** on this task's new/modified code.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] a mixed citation (1 text + 2 image refs) renders a 1-chip row then a 2-figure row — no chip for either image
|
||||
- [ ] an image-only citation renders NO chips row and a figures-only images row
|
||||
- [ ] a figure whose bytes 404 becomes its plain chip in place (the citation survives)
|
||||
- [ ] `uv run pytest tests/unit -q` green; no behavior change in completed work
|
||||
@@ -0,0 +1,23 @@
|
||||
# Task 02 — The actions row: Tune / Retry / Save-as-doc + the notes in one bottom-right cluster
|
||||
|
||||
**Phase:** `130_chat_answer_clusters` · **Source:** `TODO.md:6` — "...Those three buttons should be in a button cluster in the bottom right."
|
||||
|
||||
## Objective
|
||||
The three action buttons (and the Stopped/Failed notes) stop joining whatever the first `.msg-meta` row happens to be. They get-or-create a dedicated `.msg-actions` row — the bubble's LAST row, right-aligned by CSS (task 03) — so chips and images can never push the buttons around.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/app.js` — a shared helper next to the button appenders: `getActionsRow(wrap)` — find or create the bubble's `.msg-meta.msg-actions` row inside `.msg-body` (`role=list`, `aria-label="Answer actions"`), the house get-or-create pattern (the same shape as the current "reuse the `.msg-meta` row when it exists" code, but scoped to the `msg-actions` class so it NEVER grabs the chips row).
|
||||
2. `frontend/assets/app.js` — `appendTuneButton` (~L484), `appendRetryButton` (~L598), `appendSaveAsDocButton` (~L793): replace their "reuse the first `.msg-meta` row" logic with `getActionsRow(wrap)`; keep everything else (the gates, the one-button-per-bubble guards, the icons, the `role=listitem` when the row is `role=list`).
|
||||
3. `frontend/assets/app.js` — `appendStoppedNote` (~L520) + `appendFailedNote` (~L560): join the actions row via `getActionsRow(wrap)` instead of the first `.msg-meta` row (ASSUMPTION A7 — the notes are answer-status markers; the actions row is their home). Keep the `failed-detail` textContent contract (no HTML from error strings).
|
||||
4. `frontend/assets/app.js` — `markLastRetryable` (~L620): the retry button is removed from EVERY bubble and re-appended — but now into `getActionsRow(lastBrainWrap)`. DELETE the save-button re-append hack (the `saveDocBtn.parentElement.appendChild(saveDocBtn)` block) — the row's `justify-content: flex-end` (task 03) right-aligns the whole cluster; DOM order inside the row is the insertion order (Tune, Retry, Save-as-doc per their gates). Update the function's comment (the "auto margins split the free space" rationale is gone).
|
||||
5. Call-order audit: at every site that renders a COMPLETED brain bubble (the `done` branch, the empty-answer fallback, the stop finalize, the phase-14 restore), confirm the order is answer figure → `appendSources` → `appendRelated` → action buttons/notes, so `.msg-actions` is the LAST row (the "bottom right" = the bubble's bottom, right edge). If a site appends actions before `appendRelated`, reorder the calls (the DOM order decides the visual order — no CSS tricks).
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: the frontend unit suite — update the pins that target the button row: the buttons/notes land in `.msg-actions` (not the chips row); `markLastRetryable` moves the Retry button into the actions row and no longer re-appends the save button.
|
||||
- Coverage: **>90%** on this task's new/modified code.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] a bubble with sources + all three buttons renders the actions row LAST, containing exactly the gated buttons (the notes when present)
|
||||
- [ ] a non-admin bubble's actions row holds only Retry (the gate unchanged)
|
||||
- [ ] the restore path renders the identical row (a saved chat re-opens with the buttons in `.msg-actions`)
|
||||
- [ ] `uv run pytest tests/unit -q` green; no behavior change in completed work
|
||||
@@ -0,0 +1,33 @@
|
||||
# Task 03 — CSS: the cluster rows (actions right-aligned, auto margins removed, images row)
|
||||
|
||||
**Phase:** `130_chat_answer_clusters` · **Source:** `TODO.md:6` — "...Those three buttons should be in a button cluster in the bottom right. ... `source-chips` should show up before images in their own cluster, then images in their own cluster to keep things organized."
|
||||
|
||||
## Objective
|
||||
The cluster layout is CSS: the actions row right-aligns its buttons as a cluster (no per-button auto margins), the images row wraps its figures like the chips row wraps chips, and the rows stack in DOM order with the existing `.msg-meta` rhythm.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/styles.css` — grep the current button-row rules (`.tune-btn`, `.retry-btn`, `.save-as-doc-btn`, and any `margin-inline-start: auto` on them — the `markLastRetryable`/phase-59 comments name the auto-margin mechanism; `.save-as-doc-btn`'s is explicit in the app.js comment at ~L653). Remove the per-button auto margins (the buttons become plain flex items of the actions row).
|
||||
2. `frontend/assets/styles.css` — add the cluster rules (place them with the other `.msg-meta` / chip / figure rules — the house grouping):
|
||||
```css
|
||||
/* Phase 130: the actions cluster — the bubble's LAST row, right-aligned.
|
||||
The row's justify does the "bottom right" (no per-button auto margins). */
|
||||
.msg-meta.msg-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
/* Phase 130: the source-IMAGES cluster — wraps like the chips row. */
|
||||
.msg-meta.msg-images {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem; /* match the .msg-meta chip gap (read the existing value) */
|
||||
}
|
||||
```
|
||||
(Match the EXISTING `.msg-meta` gap/align values — read the rule first; do not invent new spacing values that diverge from the chips row.)
|
||||
3. Audit: confirm the chips row (`.msg-meta` with `role=list`) and the related row (`.msg-meta.related-docs`) rules are untouched; the `.source-image` figure styles are untouched (they were already sized for a row context); mobile: the rows stack naturally (the `.msg-body` flex gap handles it) — verify in the mobile media query block that no rule pins the actions row left; the actions row stays right-aligned at every width.
|
||||
|
||||
## Testing & Quality
|
||||
- Visual gate: the E2E (task 04) — bounding-box assertions (the actions row's buttons' right edge ≈ the row's right edge; the row is the last `.msg-meta` in the bubble).
|
||||
- Coverage: **>90%** on new/modified code (CSS has no direct coverage — the E2E + unit pins carry it).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] the actions cluster is right-aligned by `.msg-actions` alone (the per-button auto margins are gone — grep confirms)
|
||||
- [ ] the images row wraps its figures with the chips row's gap
|
||||
- [ ] `uv run pytest` green; no behavior change in completed phases
|
||||
@@ -0,0 +1,26 @@
|
||||
# Task 04 — E2E: the answer renders as organized clusters (DOM-asserted), live + restored
|
||||
|
||||
**Phase:** `130_chat_answer_clusters` · **Source:** `TODO.md:6` — "The chat output is getting messy. ... Those three buttons should be in a button cluster in the bottom right. We don't need to display the `source-chip` for images ... `source-chips` should show up before images in their own cluster, then images in their own cluster to keep things organized."
|
||||
|
||||
## Objective
|
||||
A dedicated, mock-only Playwright suite (A16 — one per phase, run in isolation) proving the owner-visible fix in the real DOM: a grounded answer citing a text doc AND an image doc renders the chips row (text chip only, NO chip for the image) before the images row (the figure), and the buttons sit in one right-aligned `.msg-actions` row that is the bubble's last row — on the live turn and after a page reload (the phase-14 restore path).
|
||||
|
||||
## Work
|
||||
1. `tests/e2e/test_chat_answer_clusters.py` (new) — house pattern (module app fixture over the shared E2E Postgres, autouse cleanup, mock-only). KB fixture: follow `tests/e2e/test_chat_image_questions.py` / `test_image_documents.py`'s image-KB pattern — a local source with ONE text doc (the mock's grounded-answer trigger, e.g. the existing "use your tools" / read flow that cites a doc) and ONE image doc (the mock's `IMAGE_DESCRIPTION_MODE` canned description makes it retrievable; the done frame's `sources` must carry both refs — the image ref with `image_url`). Inspect those suites first and reuse their fixture builders — do not invent a new mock flow unless the existing done-frame shape cannot cite both docs in one answer (if a new mock trigger is needed, add it to `tests/e2e/mock_llm.py` in the house style: a user-message marker, a deterministic reply, a module-header docstring entry).
|
||||
Run as ADMIN (the whoami gate — the Tune + Save-as-doc buttons exist; the Retry button exists for everyone) with a configured docs repo (the `docsRepoConfigured` gate for Save-as-doc — follow the existing admin chat E2E setup).
|
||||
2. Test → observable mapping (Playwright Mapping Rule):
|
||||
- `test_answer_clusters_render_in_order` — send the question; on `done`: the LAST brain bubble's `.msg-body` direct children include, in order: the `.answer-image` (if the mock offers one — else skip) → a `.msg-meta` chips row containing EXACTLY the text doc's `.source-chip` and NO `.source-chip` for the image doc (assert by the chip's `source/path` label) → a `.msg-meta.msg-images` row containing the image's `.source-image` figure (the `img` loaded, the caption = the canned description/summary) → (the related row, if present) → a `.msg-meta.msg-actions` row that is the bubble's LAST `.msg-meta`, containing the `.tune-btn`, `.retry-btn`, `.save-as-doc-btn` (all three — admin); the actions row's buttons are right-aligned (the rightmost button's bounding-box right edge ≈ the row's right edge, slack for the row padding) and the row's bounding box is BELOW the images row's (the "bottom right" geometry).
|
||||
- `test_image_without_chip_keeps_its_citation_via_figure` — in the same bubble: the image doc is cited (the figure's `href` = its `documentUrl`, left-click opens the same-page modal — the phase-26 affordance) and there is NO `.source-chip` whose label is the image's `source/path` (the owner's exact ask).
|
||||
- `test_restore_renders_the_same_clusters` — the turn auto-saves (phase-66); reload the page; the restored last brain bubble has the IDENTICAL structure (chips row text-only, images row, `.msg-actions` last with the gated buttons — the restore path renders through the same appenders).
|
||||
- `test_non_admin_sees_retry_only_actions` — a second, non-admin browser context on a FRESH chat (no admin cookie): send the same question; the `.msg-actions` row holds ONLY the `.retry-btn` (Tune/Save gated out — the phase-16/59 gates unchanged).
|
||||
3. Header docstring: the run-in-isolation command (`uv run pytest tests/e2e/test_chat_answer_clusters.py -v --no-cov`), the mock-only gate, the fixture + mock-trigger notes, the test→observable table.
|
||||
|
||||
## Testing & Quality
|
||||
- E2E: this file IS the task's suite — run in isolation, green.
|
||||
- Coverage: the phase's >90% bar is carried by tasks 01–02's updated frontend unit pins; this task adds the in-isolation Playwright gate.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/e2e/test_chat_answer_clusters.py -v --no-cov` green in isolation (DB up: `podman compose up -d db`)
|
||||
- [ ] the DOM order assertion (chips → images → actions-last) is pinned for BOTH the live and the restored bubble
|
||||
- [ ] the suite fails against the pre-phase markup (buttons inside the chips row) — i.e. it actually pins the fix
|
||||
- [ ] no behavior change in completed phases (modal navigation, the related tier, the ARIA names)
|
||||
Reference in New Issue
Block a user