phase: 99_kb_tree_table_and_back_nav
Build and Push Containers / build-and-push-app (push) Successful in 1m55s
Build and Push Containers / build-and-push-db (push) Successful in 12s

All verification is complete and green. Final report:

**Phase 99 — final verification pass (all 3 tasks already in `complete/`)**

- Verified the one-line Description clamp: `.kb-desc-cell` flex wrapper + `.kb-desc-text` ellipsis triad + `nowrap` column + in-cell Edit button + hover `title` + untouched level block (source diffs match D1; unit pins present)
- Verified the back-button breadcrumb nav: `applyTarget(target, push)` state-only `pushState`, no-duplicate gate, popstate adopt/reset, `bor:view-refresh` alignment before `loadTree()`, anonymous gate listener-free, router.js untouched (D2)
- `uv run pytest --cov=app`: **2180 passed**, coverage **99%** (>90% gate)
- `uv run ruff check .`: clean; `uv run pyright`: **0 errors, 0 warnings**
- `uv run pytest tests/e2e/test_kb_tree_nav.py -v --no-cov` (isolated, DB up): **4/4 passed** (back-walk, jump-then-back, fresh-nav/active-reclick, measured clamp incl. row-height parity ±4px, computed styles, full text in DOM/title/level block)
- Regression suites isolated: `test_kb_tree.py` 8✓, `test_ls_tree_drilldown.py` 3✓, `test_navbar_refresh.py` 7✓, plus `test_sync_summary_visibility.py` 3✓ (marker pin updated to `kb-desc-text kb-summary-pending` — the sanctioned class-pair change) and `test_edit_summaries.py` 4✓
- Criteria 1–4: **met** (each as above). Criterion 5 (atomic commit): left to the harness per executor rules — no `git add`/`commit` run; all changes left in the working tree
- No defects found in prior phases; no deviations. Stray `__pycache__/test_zz_smoke_tree_ui.*.pyc` is a leftover (no `.py` on disk) — inert, not touched
- Next pending phase: **none** — `todo/` will be empty once the harness moves this phase
This commit is contained in:
2026-09-13 01:57:40 -04:00
parent f665a83b1a
commit 8476dc1e07
22 changed files with 1755 additions and 53 deletions
@@ -0,0 +1,70 @@
# Phase 99 — Catalog table polish: one-line descriptions + the browser back button walks the breadcrumb
**Source:** Owner request (chat, 2026-09-12) — "The description columns of the table cause the rows to grow way too much in height. Just provide the first line that fits and cut it off with '...'. The user can click the folder and see the full description at the top anyway, so that's not a huge deal. Leave the edit button in the column, that's a nice touch. Clicking 'back' in the browser should take you back up the directory breadcrumbs."
**Story:** n/a (owner request — the drill-down catalog on `97_kb_tree_catalog`; the shell router on `76_spa_nav_shell` / `77_navbar_refresh`).
**Context:** The RAG view's ONE folders/sources table (`#folders-table`, `#folders-tbody`) renders `makeDescCell` rows: a Description `<td>` holding a text `<span>` (the stored description — a 1–3 sentence `lite` paragraph, or the phase-98 `Summary pending` marker) + the ALWAYS-present `Edit` button (`wireDescriptionEdit` swaps the cell's children for the inline textarea on edit). CSS: `.kb-folders-table td:nth-child(3) { white-space: normal; min-width: 18rem; max-width: 44rem; }` — the paragraph WRAPS, so a long description grows the row to many lines (the owner's "rows grow way too much in height"). The level block (`#kb-level`) shows the current directory's FULL description when drilled in (the owner's "see the full description at the top" — it stays). Navigation is purely client-side: `goTo(target)` sets the module `current = {source, folder}` and re-renders — source-row clicks, folder-link clicks, and breadcrumb-segment clicks (`crumbSegment`, `href="#"` + preventDefault) all call `goTo` with NO history entry, so the browser back button leaves the RAG view entirely instead of popping one breadcrumb level. The shell router (`frontend/assets/router.js`) pushes `{ view: name }` state on navbar switches and its `popstate` handler reads ONLY `window.location.pathname` (a state-only push that keeps the URL therefore cannot disturb it); the phase-77 `bor:view-refresh` CustomEvent re-runs a view's load on re-show (a switch back, an active-link re-click — which pushes NOTHING, or back/forward onto the view).
## Objective
Long folder descriptions stop stretching the catalog rows — the Description cell shows the first line that fits, ellipsized, with the Edit button kept in the cell (full text on hover; the full description remains at the top of the level when you click in) — and the browser back/forward buttons walk the directory breadcrumb: every drill (source, folder, or breadcrumb jump) is a history entry, so Back pops one level up, Forward re-descends, and Back at the top level leaves the RAG view exactly as today.
## Dependencies
- `97_kb_tree_catalog` (complete) — the tree UI (`makeDescCell`, `wireDescriptionEdit`, `renderLevel`, the breadcrumb, `goTo`), the level block.
- `96_oneshot_resilience` / `98_sync_summary_visibility` (todo/complete) — queue order only; if 98 lands first, the `Summary pending` marker text must remain clamped/consistent (a one-line marker — no behavior coupling, the cell restructure in task 01 simply wraps whatever the span contains).
- `76_spa_nav_shell` / `77_navbar_refresh` (complete) — the router's pushState/popstate/refresh contracts this phase integrates with (untouched).
## Decisions recorded here (owner review — PLAN.md is being redone by the owner)
- **D1 — the clamp is visual only:** the Description cell clamps to ONE line — `white-space: nowrap` + `overflow: hidden` + `text-overflow: ellipsis` on the text span, the cell a fixed one-line height (the row height becomes independent of description length). The full text stays in the DOM (the accessible name is unchanged — the ellipsis is CSS-only) AND on hover via the span's `title` (the house pattern — the tokens label cell and the folder-link `title`s do the same). The Edit button stays in the cell (owner: "that's a nice touch") — the cell is a flex row: text flexes + ellipsizes, the button is `flex: 0 0 auto`. The level block's `.kb-level p` keeps the FULL unclamped description (owner: the full text at the top is the escape hatch).
- **D2 — the drill state becomes the history state:** every `goTo` (source-row click, folder-link click, breadcrumb-segment click) does `history.pushState({ view: "rag", kb: target }, "")` — a STATE-ONLY entry (the URL stays the shell's current path; no new route, no URL contract change — the phase-76 deep-link surface is untouched). `popstate`: the entry's `state.kb` (when present) is adopted WITHOUT pushing; an entry without `kb` (the boot entry, a router view entry, any foreign state) resets the drill to the TOP level. The phase-77 `bor:view-refresh` (re-show) aligns `current` with `history.state` the same way — so a fresh nav visit to RAG (the router's entry carries no `kb`) starts at the top level, while an active-link re-click (no pushState — the current drilled entry's state is still on top) keeps the drill. Back at the top level = leave the RAG view (the router's existing pathname-based switch — UNCHANGED). The router module is not modified: its popstate reads the pathname only, and a state-only push never changes it.
- **D3 — the `renderLevel`/`loadTree` machinery is otherwise untouched:** the never-stale `resetVanishedLocation` (post-refetch), the `loadSeq` race token, the re-entrant render, and the anonymous gate all stand; the history integration lives in `goTo` + two listeners (popstate, and the refresh listener's alignment step).
## Design (shared by all tasks — the executor reads this, not the chat)
### The one-line clamp (task 01)
- `frontend/assets/sources.js` — `makeDescCell`: the `<td>`'s content becomes ONE flex wrapper `div.kb-desc-cell` holding the text `<span>` (class `kb-desc-text`) and the Edit button (today's two children, re-homed). `wireDescriptionEdit`'s row `container` becomes the WRAPPER (the editor swap then fills the wrapper: `replaceChildren(editor, actions, status)` inside it — the textarea's existing `width: 100%` keeps filling the cell; verify the open-editor layout in the phase-97 E2E regression, task 03). The text span always carries `title` = the full description text (set whenever non-empty — the hover escape hatch, D1). The marker case (phase 98, if landed) is unaffected: the marker span content/class is set as today inside the wrapper.
- `frontend/assets/styles.css`:
- `.kb-folders-table td:nth-child(3)` — `white-space: nowrap` (was `normal`); keep `min-width: 18rem; max-width: 44rem;` (the column width is the clamp's budget).
- `.kb-desc-cell { display: flex; align-items: center; gap: 0.4rem; min-width: 0; }` (the gap replaces the button's `margin-left: 0.4rem` — remove that rule's effect by the wrapper's gap; keep the level-block button rule untouched).
- `.kb-desc-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }` (the `min-width: 0` is what lets the flex item shrink — the house comment pattern, cf. `.sync-label`).
- `.kb-summary-edit` stays `flex: 0 0 auto` (already) — the button never wraps to a second line.
- The open-editor case: `.kb-desc-cell .kb-summary-editor` must keep `width: 100%` behavior as a flex item (add `flex: 1 1 auto; min-width: 0;` if needed) — the editor fills the cell, Save/Cancel below it (the existing `.kb-summary-actions` margin).
- The level block (`.kb-level p`) is UNTOUCHED (full text, D1).
### The back button (task 02)
- `frontend/assets/sources.js`:
- `goTo(target)` → the two-step `applyTarget(target, push)`: when `push` is true, `history.pushState({ view: "rag", kb: target }, "")` FIRST (a no-op if `target` equals `current` — never push duplicate entries: a re-click of the current row/segment is skipped), then set `current` + `renderLevel()`. Source-row / folder-link / breadcrumb-segment click handlers call `applyTarget(target, true)`.
- `window.addEventListener("popstate", …)` (registered once at mount, in the admin branch next to the refresh listener — the anonymous gate never installs it): `const kb = event.state && event.state.kb;` → `kb` present → `applyTarget(kb, false)`; absent → `applyTarget({ source: null, folder: null }, false)`. Rendering a currently-hidden view is harmless (the router's own popstate listener, registered earlier, has already switched the visible view for foreign entries — the pathname did not change for rag-internal entries, so no switch happens and this listener's render IS the visible one).
- The `bor:view-refresh` listener (re-show): BEFORE `loadTree()`, align `current` with `history.state` — `history.state && history.state.kb` present → adopt it (the active-link re-click case: the drilled entry is still on top → the drill survives); absent → reset to the top level (a fresh nav entry carries no `kb`). Then the existing `loadTree()` render.
- Boot: `current` starts at the top level; NO pushState on boot (the browser's own entry is the base — the phase-76 boot contract).
- `resetVanishedLocation` (the post-refetch never-stale reset) stays as the DOM-side backstop (a re-fetch after a gap where the current level vanished still resets — it does not push, which is correct: it follows the history state).
- Module docstring: the phase-99 history-integration contract (D2) — the push/adopt/reset rules.
- `frontend/assets/router.js` — UNCHANGED (its popstate reads the pathname; the state-only pushes are invisible to it). No new route, no `_shell_routes` change.
### The E2E (task 03)
`tests/e2e/test_kb_tree_nav.py` (new; the temp-local-source seeding + sync pattern from `test_ls_tree_drilldown.py` — seed a source with a NESTED folder `one/two` + ≥ 2 docs per relevant folder so the mock stores descriptions; admin login via `auth_helpers`; the long manual description for the clamp assertion is created through `PATCH /api/folders/summary` with the admin cookie — the phase-97 pattern):
1. **`test_back_button_walks_the_breadcrumb`** — full navigation chain: load `/` (chat), click the RAG nav link (the router's pushState entry), click the source row (push #1), click folder `one` (push #2), click folder `two` (push #3) — the breadcrumb shows `Knowledge base / <src> / one / two` (last segment `aria-current`). `page.goBack()` → the level is `one` (level title `<src>/one`, `two` listed as a subfolder, the breadcrumb's current segment = `one`); `page.goBack()` → the source root (`<src>`, breadcrumb current = the source); `page.goBack()` → the TOP level (source rows, breadcrumb hidden); `page.goBack()` → the CHAT view is visible (the router contract — Back at the top level leaves the view) and the RAG view is hidden; `page.goForward()` → back to the top level; `page.goForward(); page.goForward(); page.goForward()` → `one` → then the folder level `two` again (the whole chain replays).
2. **`test_breadcrumb_jump_then_back`** — at `one/two`, click the `Knowledge base` breadcrumb segment (a jump: pushes a top-level entry) → the top level renders; `page.goBack()` → returns to `one/two` (the jump's origin — standard history semantics, pinned).
3. **`test_fresh_nav_visit_starts_at_the_top`** — drill to `one/two`, leave via the Tuning nav link, click the RAG nav link again (a fresh router entry, no `kb` state) → the RAG view shows the TOP level (the D2 alignment); drill to `one`, re-click the ACTIVE RAG link (no pushState) → the drill SURVIVES (still at `one` after the refresh re-render).
4. **`test_description_cell_clamps_to_one_line`** — via the admin cookie `PATCH /api/folders/summary` set a long manual description (≥ 4 wrapped lines worth, e.g. 300 chars) on folder `one`; at the source level: the `one` row's Description cell — the cell's `<td>` bounding-box height equals a short-description row's height (± 4 px — both one line), the text span's computed `text-overflow` is `ellipsis` and `white-space` is `nowrap`, the span's `title` carries the FULL text, and the Edit button is visible IN the same cell (not wrapped below). Click `one` → the level block's `#kb-level-summary` textContent is the FULL long text (unclamped at the top — the owner's escape hatch).
5. Regression sweep (each in isolation, `--no-cov`): `test_kb_tree.py` (the EDITOR flow now lives in the restructured cell — the critical one), `test_ls_tree_drilldown.py`, `test_navbar_refresh.py` (the refresh contract the alignment step hooks). Fix ONLY pins broken by the additive changes — no asserted behavior changes elsewhere.
6. Full gates + commit: `uv run pytest` green; coverage >90%; `uv run ruff check . && uv run pyright` clean; move `99_kb_tree_table_and_back_nav` → `complete/`; one atomic commit:
```bash
git add -A .agents/ frontend/ tests/ && git commit --no-gpg-sign -m "fix(ui): clamp catalog descriptions to one line and walk the breadcrumb with the browser back button"
```
## Tasks
1. `01_description_line_clamp.md` — the Description cell's one-line clamp (flex wrapper + ellipsis + hover title) with the Edit button kept
2. `02_breadcrumb_history_nav.md` — the drill state becomes the history state (pushState per drill, popstate adopt/reset, refresh alignment)
3. `03_e2e_tree_nav.md` — `tests/e2e/test_kb_tree_nav.py` + the regression sweep + the atomic commit
## Testing & Quality
- Unit: `tests/unit/test_kb_tree_ui.py` extensions — the clamp structure (the `.kb-desc-cell` wrapper, the `kb-desc-text` span + its `title`, the button inside the wrapper, the editor container = the wrapper) and the history wiring (`pushState` on drill, the popstate adopt/reset handler, the refresh alignment, the no-duplicate-push rule) + `styles.css` pins (`.kb-desc-cell` / `.kb-desc-text` declarations, `white-space: nowrap` on the cell, the editor's flex fill).
- E2E (mandatory, A16): `uv run pytest tests/e2e/test_kb_tree_nav.py -v --no-cov` in isolation; `test_kb_tree.py` + `test_ls_tree_drilldown.py` + `test_navbar_refresh.py` green in isolation.
- Coverage: **>90%** on `app/` (untouched this phase — the full-suite gate still runs; the frontend pins + E2E are the test layer).
- Lint/types: `uv run ruff check . && uv run pyright`.
## Completion Criteria
- [ ] a long stored description renders ONE line in the catalog row (row height independent of description length, source-pinned + E2E-measured), ellipsized, with the full text on hover and in the DOM, and the Edit button in the same cell; the level block shows the full text
- [ ] Back pops one breadcrumb level at a time (drill → … → top), Forward re-descends, Back at the top leaves the RAG view (router contract intact), a breadcrumb jump + Back returns to the jump's origin, a fresh nav visit starts at the top, and an active-link re-click keeps the drill
- [ ] `uv run pytest` green; coverage >90%; ruff + pyright clean
- [ ] `uv run pytest tests/e2e/test_kb_tree_nav.py -v --no-cov` green in isolation (DB up: `podman compose up -d db`); the regression suites green in isolation
- [ ] one atomic Conventional Commit, `--no-gpg-sign`
@@ -0,0 +1,31 @@
# Task 01 — The Description cell clamps to one line (Edit button kept, full text on hover)
**Phase:** `99_kb_tree_table_and_back_nav` · **Story:** n/a (owner request)
## Objective
A long stored description no longer grows its catalog row: the Description cell shows the first line that fits, ellipsized, with the Edit button in the same cell and the full text on hover (and in the DOM).
## Work
1. `frontend/assets/sources.js` — `makeDescCell` (D1, `00_phase.md`):
- The `<td>`'s content becomes ONE wrapper `div.kb-desc-cell` holding the text `<span class="kb-desc-text">` and the Edit `<button>` (today's two children, re-homed; the td itself gets no new class).
- The text span ALWAYS carries `title` = the full description text when non-empty (the hover escape hatch; the marker case — phase 98's `Summary pending` span, if landed — keeps its own marker title, never overridden by this rule: set the hover title only for real description text).
- `wireDescriptionEdit`'s ROW `container` becomes the WRAPPER (the level-block editor is unaffected — its container is still `.kb-level-body`): the open/close swaps happen inside the wrapper, so the `<td>` always keeps the wrapper and the cell layout survives the editor swap.
- Module docstring note: the cell's flex structure (text flexes + ellipsizes, button fixed).
2. `frontend/assets/styles.css` (the `.kb-folders-table` / phase-97 tree region):
- `.kb-folders-table td:nth-child(3)` — `white-space: nowrap` (was `normal`); keep `min-width: 18rem; max-width: 44rem;` (the column's width is the clamp's budget) and the font-size.
- `.kb-desc-cell { display: flex; align-items: center; gap: 0.4rem; min-width: 0; }` — and the row-cell button's spacing comes from this gap: adjust the existing `.kb-folders-table .kb-summary-edit { margin-left: 0.4rem; }` (drop the margin — the gap replaces it; the LEVEL-block rule `.kb-level-body .kb-summary-edit { margin-top: 0.5rem; }` is untouched).
- `.kb-desc-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }` (the `min-width: 0` is what lets the flex item shrink — comment it, the `.sync-label` house pattern).
- The open-editor case: the `.kb-summary-editor` inside the flex wrapper must keep filling the cell — add `.kb-desc-cell .kb-summary-editor { flex: 1 1 auto; min-width: 0; }` if the existing `width: 100%` does not (verify against the phase-97 editor E2E, task 03); `.kb-summary-actions` / `.kb-summary-status` stack below it as today.
- NO change to `.kb-level p` (the level block's full description — D1), no new hue (phase-92 invariant), `:focus-visible` via the global rule (the button is unchanged).
3. Source pins (`tests/unit/test_kb_tree_ui.py` + its styles.css pins): the wrapper + span classes, the `title` on non-empty text (and NOT on the marker/empty case), the editor container = the wrapper, the CSS declarations (`.kb-desc-cell` flex + gap, `.kb-desc-text` ellipsis triad + `min-width: 0`, `white-space: nowrap` on `td:nth-child(3)`, the margin removal).
## Testing & Quality
- Unit: the source pins above are this task's test layer (`app/` untouched).
- Coverage: **>90%** on this task's new/modified code (frontend pins cover the JS; the measured-height proof is task 03's E2E).
- Note: do NOT add an E2E file here — the clamp's browser proof lands in task 03.
## Completion Criteria
- [ ] the cell renders text + Edit button in one flex row; long text ellipsizes instead of wrapping (CSS-pinned); the full text is in the DOM + the span's `title`
- [ ] the inline editor still opens/saves/cancels from the restructured cell (source-pinned; the phase-97 E2E regression in task 03 proves it in a browser)
- [ ] full test suite green, coverage >90%
- [ ] no behavior change in completed work (the level block's full description, the marker cell — if phase 98 landed — untouched)
@@ -0,0 +1,29 @@
# Task 02 — The browser back button walks the breadcrumb (drill state = history state)
**Phase:** `99_kb_tree_table_and_back_nav` · **Story:** n/a (owner request)
## Objective
Every catalog drill (source, folder, or breadcrumb jump) becomes a history entry — Back pops one level up, Forward re-descends, Back at the top level leaves the RAG view (the router contract), a fresh nav visit starts at the top, and an active-link re-click keeps the drill.
## Work
1. `frontend/assets/sources.js` (D2/D3, `00_phase.md`):
- `applyTarget(target, push)`: set `current = { source: target.source, folder: target.folder }` + `renderLevel()`. When `push` is true, FIRST `history.pushState({ view: "rag", kb: target }, "")` — but SKIP the push when `target` is deep-equal to `current` (a re-click of the current row/segment pushes no duplicate entry). `goTo(target)` becomes `applyTarget(target, true)` — the existing call sites (source-row click, folder-link click, `crumbSegment` click) keep calling `goTo`.
- A `window` `popstate` listener (registered ONCE at mount, in the ADMIN branch next to the refresh listener — the anonymous gate installs neither): read `event.state && event.state.kb` → present: `applyTarget(kb, false)`; absent: `applyTarget({ source: null, folder: null }, false)`. (Rendering a currently-hidden view is harmless — the router's earlier-registered popstate listener owns view switches for foreign entries; for rag-internal entries the pathname never changed, so this listener's render is the visible one.)
- The `bor:view-refresh` listener (the phase-77 re-show): BEFORE `loadTree()`, align with `history.state` — `history.state && history.state.kb` present → `applyTarget(kb, false)` (the active-link re-click case: no pushState happened, the drilled entry is still on top → the drill survives the refresh); absent → `applyTarget({ source: null, folder: null }, false)` (a fresh nav entry carries no `kb` → top level).
- Boot: `current` stays the top-level initial value; NO pushState on boot (the browser's own entry is the base).
- `resetVanishedLocation` (the post-refetch never-stale reset) is UNCHANGED — it still resets `current` without pushing when a re-fetch finds the drilled level gone (it follows whatever the history state says; no new entry).
- Module docstring: the phase-99 history contract — the push/adopt/reset rules + the URL-stays-put note (state-only entries; the phase-76 deep-link surface untouched).
2. `frontend/assets/router.js` — UNCHANGED (its popstate reads the pathname only; state-only pushes are invisible to it). Verify by running its unit suite (`tests/unit/test_frontend_router.py`) untouched.
3. Source pins (`tests/unit/test_kb_tree_ui.py`): the `pushState` call with `{ view: "rag", kb: target }` (and the no-URL-change — the second arg is `""` / no third arg), the duplicate-push skip, the popstate handler's adopt/reset branch (the `event.state.kb` read), the refresh alignment branch (`history.state` read), and that the anonymous branch installs no popstate listener.
## Testing & Quality
- Unit: the source pins above are this task's test layer (`app/` untouched).
- Coverage: **>90%** on this task's new/modified code (frontend pins cover the JS; the browser proof is task 03's E2E).
- Note: do NOT add an E2E file here — the navigation proof lands in task 03.
## Completion Criteria
- [ ] drill clicks push state-only history entries (no URL change, no duplicate on re-click); Back/Forward adopt the entry's `kb` without pushing; a `kb`-less entry resets to the top (source-pinned)
- [ ] a fresh nav visit to the RAG view aligns to the top level; an active-link re-click keeps the current drill (source-pinned)
- [ ] `router.js` and its unit suite are untouched; the anonymous gate still fetches nothing and installs no listeners
- [ ] full test suite green, coverage >90%
- [ ] no behavior change in completed work (the phase-76/77 router contracts green — `test_frontend_router.py`)
@@ -0,0 +1,29 @@
# Task 03 — The dedicated E2E: back-walks-the-breadcrumb + the measured clamp + regressions + commit
**Phase:** `99_kb_tree_table_and_back_nav` · **Story:** n/a (owner request)
## Objective
Pin both owner-visible contracts in a browser: the browser Back/Forward buttons walk the directory breadcrumb level by level, and a long description renders exactly one row-line (Edit button in-cell, full text on hover + at the top).
## Work
1. `tests/e2e/test_kb_tree_nav.py` (new — the phase's dedicated A16 suite, run in isolation). Fixtures: `app_server` + `mock_llm` + `db_ready` (the `conftest` pattern); admin login via `tests/e2e/auth_helpers.py`; the temp-local-source seeding + `POST /api/sync` pattern from `test_ls_tree_drilldown.py` — seed a source with a NESTED folder `one/two` and ≥ 2 docs per folder (so the mock `FOLDER_SUMMARY_MODE` stores deterministic descriptions at every level). An autouse cleanup removes the temp source's KB rows + `folder_summaries` rows (the phase-96 cleanup pattern).
- **`test_back_button_walks_the_breadcrumb`** — load `/` (chat), click the RAG nav link (the router's pushState entry), click the source row (push #1), click folder `one` (push #2), click folder `two` (push #3): the breadcrumb renders `Knowledge base / <src> / one / two` (last segment `aria-current="page"`). Then, exactly one level per press: `page.goBack()` → level `one` (level title `<src>/one`, `two` listed as a subfolder, breadcrumb current = `one`); `page.goBack()` → the source root (title `<src>`, breadcrumb current = the source); `page.goBack()` → the TOP level (the source rows, breadcrumb hidden); `page.goBack()` → the CHAT view visible + the RAG view hidden (the router contract — Back at the top leaves the view); `page.goForward()` → the top level again; three more `page.goForward()`s → the source root → `one` → `two` (the whole chain replays, current segment correct at each step).
- **`test_breadcrumb_jump_then_back`** — drill to `one/two`; click the `Knowledge base` breadcrumb segment (a jump — pushes a top-level entry) → the top level renders; `page.goBack()` → back at `one/two` (the jump's origin — standard history semantics, pinned).
- **`test_fresh_nav_visit_starts_at_the_top`** — drill to `one/two`; navigate to Tuning (nav link); click the RAG nav link again (a fresh router entry — no `kb` state) → the RAG view shows the TOP level (the D2 alignment, after the refresh re-render); then drill to `one` and re-click the ACTIVE RAG nav link (no pushState) → the refresh re-render KEEPS the drill (still at `one` — the active re-click contract).
- **`test_description_cell_clamps_to_one_line`** — with the admin cookie, `PATCH /api/folders/summary` sets a LONG manual description (≥ 4 wrapped lines worth — e.g. 300 chars) on folder `one` (the phase-97 endpoint); at the source level: the `one` row's Description `<td>` bounding-box height equals a short-description row's height (± 4 px — both one line); the `.kb-desc-text` span's computed style: `text-overflow: ellipsis`, `white-space: nowrap`; the span's `title` attribute carries the FULL long text; the Edit button is visible IN the same cell (its bounding box shares the cell's row — not wrapped below). Click `one` → the level block's `#kb-level-summary` textContent is the FULL long text (unclamped at the top — the owner's escape hatch).
2. Regression sweep (each in isolation, `--no-cov`, DB up): `test_kb_tree.py` (the EDITOR flow now lives in the restructured cell — the critical regression), `test_ls_tree_drilldown.py`, `test_navbar_refresh.py` (the refresh contract the alignment step hooks). Fix ONLY pins broken by the additive changes — no asserted behavior changes elsewhere.
3. Full gates + commit: `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run ruff check . && uv run pyright` clean; move `99_kb_tree_table_and_back_nav` → `.agents/phases/complete/`; one atomic commit:
```bash
git add -A .agents/ frontend/ tests/ && git commit --no-gpg-sign -m "fix(ui): clamp catalog descriptions to one line and walk the breadcrumb with the browser back button"
```
## Testing & Quality
- E2E (mandatory, A16): `uv run pytest tests/e2e/test_kb_tree_nav.py -v --no-cov` green in isolation.
- Coverage: **>90%** on `app/` (the full-suite gate; `app/` is untouched this phase).
- Lint/types: `uv run ruff check . && uv run pyright`.
## Completion Criteria
- [ ] the four E2E tests pass in isolation (the back/forward walk, the jump, the fresh-visit/re-click alignment, the measured one-line clamp)
- [ ] the regression suites pass in isolation (the phase-97 editor flow green in the restructured cell)
- [ ] full suite green, coverage >90%, ruff + pyright clean
- [ ] phase dir moved to `complete/`, one atomic `--no-gpg-sign` Conventional Commit