# Phase 65 — Chat Action Cluster to the Pinned Bottom **Source:** `TODO.md` L3 — "Move the new chat and share button to the tune/retry/save doc cluster area so it's always at the bottom of the screen and easily accessble. Make the button clusters look better, neater, more aligned" **Story:** n/a (TODO-derived — owner roadmap confirmation 2026-09-01) **Context:** Verified layout inventory (audited 2026-09-01, line numbers as of this writing): - `frontend/index.html` — the `.chat-actions` row (New chat + Share pills, ONE wrapper) sits at the **TOP** of the 46rem chat column: the comment block runs L126–148, the row div is L149, positioned between `#steering-announcer` (L129) and `
` (L193). The pinned composer form follows the messages at L225. In any real conversation the row scrolls off-screen the moment the reader is at the bottom — the exact defect this phase removes. - `frontend/assets/styles.css` — `.chat-actions` (L433: horizontal row, `align-items: center`, `gap: 0.6rem`, intrinsic pill widths); `.new-chat-btn` (L301) / `.share-chat-btn` (L335) — solid brand pill family, `min-height: 44px`, `border-radius: 999px`; `.composer` (L1226) — `position: sticky; bottom: env(safe-area-inset-bottom, 0)`, no z-index (the phase-52 pin, owner-locked 2026-08-30); `.messages` — `flex: 1 1 auto` + `min-height: 200px` (the grow that rests the bottom at the screen on short chats); the per-answer meta cluster: `.msg-meta` (L641), `.tune-btn` (L694), `.retry-btn` (L723), `.save-as-doc-btn` (L756) — ghost pill family, `min-height: 44px`, 14px icons, `margin(-inline-start): auto` pushes Tune/Retry/Save-as-doc to the row's **right edge** ("Save as doc" is the bottom-right action of every completed brain bubble — the app's action corner). The ≤640px block (L3049) flips `.chat-actions` to a full-width vertical stack. - `frontend/assets/header.js` L336–339 — binds `#new-chat-btn` and dispatches the `bor:new-chat` window event; `frontend/assets/app.js` L1700 listens and resets the conversation (it owns the in-flight-turn guard); `app.js` L246/L2067 binds `#share-chat-btn` to `shareCurrentChat` (the empty-chat no-op writes `Nothing to share yet.` to `#send-status`). All bindings are id-based and **position-independent** — the move is pure HTML/CSS. - `frontend/assets/app.js` `copyShareLinkWithFallback` (L1424) appends the clipboard-fallback field to the **composer** (not the row) — unaffected by the move. - Existing suites that constrain the design: `tests/e2e/test_pinned_composer.py` (the `#composer` computed `position` must stay `sticky`; the composer box flush with the viewport bottom at EVERY scroll position inside the sticky range; settles back into flow above the footer at the document bottom; NO z-index on the composer; the empty chat must not be scrollable, `sh <= ch + 1`); `tests/e2e/test_save_share_ux.py::test_action_row_layout` (ONE `.chat-actions`, DOM order New chat → Share, horizontal row at 1280px with intrinsic pill widths, full-width stack at 390px, no horizontal overflow at 360px — all RELATIVE geometry, nothing pins the row's vertical position); `tests/e2e/test_chat_persistence.py` (`#new-chat-btn` ≥44px, click resets the conversation, `New chat started` in `#send-status`). - `.agents/` is tracked and committed (AGENTS.md rule 8) — the phase commit stages `.agents/` + `frontend/` + `tests/`. ## Objective The New chat + Share pills live in the **bottom cluster** of the chat column — the same screen zone as the tune/retry/save-doc meta actions — and are **always at the bottom of the screen** (pinned with the composer, visible even while scrolled up reading a long conversation). The two button clusters read as neat, aligned sets: the bottom row hugs the column's right edge, mirroring the right-aligned "Save as doc" corner above it. ## Dependencies - `64_sync_upload_progress` (todo, preceding — no functional dependency; ordering by number) ## Tasks 1. `01_move_chat_actions_to_bottom.md` — relocate the `.chat-actions` row (buttons + comments byte-identical) to the last child of `.chat-shell`, directly above the composer; in-flow at first. 2. `02_pin_bottom_cluster.md` — wrap row + composer in a sticky `.chat-bottom` unit (A1): the cluster is pinned at the viewport bottom at every scroll position and settles into flow above the footer at the document bottom. 3. `03_align_button_clusters.md` — right-align the bottom row to the column's right edge (A2); verify the five action pills share one geometry (44px targets, 999px radius, global focus-visible). 4. `04_e2e_bottom_chat_actions.md` — the dedicated Playwright suite `tests/e2e/test_bottom_chat_actions.py` (run in isolation): pinned at every scroll position, resting at the screen bottom, settled above the footer, geometry + DOM order, mobile stack, both buttons still work. 5. `05_regressions_and_commit.md` — regression E2E matrix, full `validate.sh`, one atomic commit. ## Testing & Quality - E2E (mandatory, house rule — one file per phase, run in isolation): `tests/e2e/test_bottom_chat_actions.py` against the shared conftest server (DB up, mock LLM, seeded KB — the `test_pinned_composer.py` patterns for overflow conversations and scroll measurement). - Frontend-only phase: `app/` is untouched, so the coverage gate stays satisfied at its current level — `validate.sh` (unit + integration >90% + ruff + pyright) must still pass end to end. - Regression E2E (each in isolation): `test_pinned_composer.py`, `test_save_share_ux.py`, `test_chat_persistence.py`, `test_share_chat.py`, `test_chat_history.py`, `test_smoke.py`. ## Completion Criteria - [ ] The `.chat-actions` row is the bottom cluster of `.chat-shell` — above the composer, inside the sticky `.chat-bottom` — with DOM order New chat → Share preserved and all ids/aria-labels intact. - [ ] Over-viewport conversation, scrolled to the top: the row is fully visible, pinned at the viewport bottom above the composer, at every scroll position inside the sticky range; at the document bottom the cluster settles into flow above the footer (no overlap). - [ ] Empty chat at 1280×800: the row rests at the bottom of the screen, the document is not scrollable. - [ ] Desktop: the row's right edge aligns with the column's right edge (A2); ≤640px: full-width vertical stack; no horizontal overflow at 360px. - [ ] `#new-chat-btn` click resets the conversation (`New chat started` in `#send-status`); `#share-chat-btn` on an empty chat is a no-op (`Nothing to share yet.`) — the bindings survived the move. - [ ] `uv run pytest` green; coverage TOTAL >90%; `uv run ruff check . && uv run pyright` clean. - [ ] `uv run pytest tests/e2e/test_bottom_chat_actions.py -v --no-cov` green in isolation (DB up); the six regression E2E suites green in isolation. - [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`. ## Locked decisions - **Owner-locked (2026-09-01, roadmap confirmation, A1):** *pinned bottom cluster* — a `.chat-bottom` wrapper (`position: sticky; bottom: env(safe-area-inset-bottom, 0)`, NO z-index) groups `.chat-actions` + `#composer` as one sticky unit, so the pills are literally always at the bottom of the screen — the rejected alternative (plain in-flow row) would scroll away while reading up a long conversation. The `#composer`'s own sticky declaration STAYS (redundant inside the wrapper — it cannot shift within its containing block — but `test_pinned_composer.py` pins the computed style `sticky`). - **Owner-locked (2026-09-01, roadmap confirmation, A2):** *right-aligned bottom row* — the New chat + Share row hugs the column's RIGHT edge on desktop (so the bottom-right reads as one aligned action column with the right-aligned "Save as doc" buttons above); the ≤640px stack stays full-width (alignment is moot when stretched). ## Commit ```bash git add .agents/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(web): move the chat action cluster to the pinned bottom and align the button sets" ```