# Phase 55 — Save by Default, Share Anonymously **Source:** `TODO.md` L3–L6 — "Share chat should work anonymously without login" / "Save shouldn't be a button, every chat should be saved by default" / "Need feedback (probably dropdown notification toast) to show share worked" / "New Chat and Share buttons should only be vertically stacked when in mobile, otherwise they should be horizontally next to each other" **Story:** n/a (TODO-derived — owner roadmap confirmation 2026-08-31) **Context:** Phase 50 stores conversations in `saved_chats` through an explicit Save pill — the `saved_chats` CRUD lives under `/api/chats` in `app/api/chats.py` with a **router-wide** `dependencies=[Depends(require_admin)]` (line ~72), and the pill ships hidden, revealed at boot only when whoami says admin (`frontend/assets/app.js` boot IIFE, line ~1712). Phase 51 added save-then-share (`POST /api/chats` with `share: true` mints the 128-bit `uuid4` token in the same commit; idempotent `POST /{id}/share`) and the public read-only `/shared/` page (already anonymous). The conversation itself lives in localStorage under `bor.chat.v1` (`{ v: 1, messages: [...] }` — **no row link is persisted**; `currentChatId` is module-scope only, so a reload unlinks the conversation). The chat-page pills (`#new-chat-btn`, `#save-chat-btn`, `#share-chat-btn`) are direct children of `.chat-shell` (a vertical flex column, `styles.css` line ~424), so they stack at **every** width. Feedback today is status-line text only (`#send-status`, the live region inside the composer's send button). Phase 53 (todo, preceding) stamps `sources_version` on save and re-saves the linked row after a stale Regenerate — its re-save must keep working once the Save pill is gone. ## Objective Every conversation on the chat page saves itself (no Save button), **any** visitor can turn the current conversation into a public link without signing in (with a visible toast confirming the share), and the New chat / Share pills sit horizontally on desktop, stacking vertically only on mobile. ## Dependencies - `53_stale_saved_chats` (todo, preceding) — the save-point machinery and the linked-row re-save (stale Regenerate's auto re-save must keep working after the Save pill is removed; task 02 re-points it at the shared upsert helper if it landed as a direct call). - `54_asset_cache_bust_revalidation` (todo, preceding) — the static-bundle / cache-busting contract the frontend changes ride on (the `?v=` rewrite picks up the changed `styles.css`/`app.js` automatically). - `50_chat_history` + `51_share_chat` (complete) — the `saved_chats` rows, the `/api/chats` surface, the save-then-share contract, the History page, the public `/shared/` page. ## Tasks 1. `01_anonymous_save_share_api.md` — open the save/share write surface to anonymous visitors (list/detail/delete/unshare stay admin-only). 2. `02_auto_save_default.md` — retire the Save pill; every conversation auto-upserts at the existing save points and the row link survives reloads (no duplicate rows). 3. `03_share_for_everyone.md` — ship the Share pill visible to all visitors; neutral error copy. 4. `04_share_toast.md` — the top-right slide-down toast confirming a successful share (both success paths). 5. `05_chat_actions_layout.md` — the `.chat-actions` row: horizontal on desktop, stacked at ≤640px. 6. `06_e2e_save_share_ux.md` — the story Playwright suite + regressions + the atomic commit. ## Testing & Quality - Integration: `tests/integration/test_chats_api.py` — anonymous create (incl. save-then-share), anonymous update, anonymous share (idempotent); guest 403s on list/detail/delete/unshare; the existing admin pins stay green. - Frontend source pins (house style, `tests/unit/test_save_chat_ui.py` / `test_history_page.py` pattern): `app.js` (headless upsert helper, save-point triggers, `chatId` in the `bor.chat.v1` record, no Save-pill wiring, no admin-gated Share reveal, toast helper), `index.html` (no `#save-chat-btn`, `#share-chat-btn` not `hidden`, the `.chat-actions` wrapper), `styles.css` (`.save-chat-btn` gone, `.chat-actions` base + ≤640px rules, `.toast` + reduced-motion). - **Existing E2E pin adaptation (contract change):** `tests/e2e/test_chat_history.py` (the file-local `_save()` helper + the admin Save-pill assertions + the anonymous "absent" block) and `tests/e2e/test_share_chat.py` (the Save-click step in `test_share_from_history_and_unshare`) — the Save pill is replaced by waiting for the auto-saved row via the admin API; assertion edits limited to the new contract (tasks 02/03). - Coverage: **>90%** on `app/` (validate.sh gate). - E2E (mandatory, A16): `tests/e2e/test_save_share_ux.py`, run in isolation. ## Completion Criteria - [ ] Without a session: `POST /api/chats`, `POST /api/chats` with `share: true`, `PUT /api/chats/`, and `POST /api/chats//share` all succeed; the same guest gets 403 on `GET /api/chats`, `GET /api/chats/`, `DELETE /api/chats/`, and `POST /api/chats//unshare` (unshare still revokes — the public `/api/shared/` read 404s afterwards). - [ ] The chat page has **no Save control** at any width; a signed-out visitor who sends one question produces exactly one `saved_chats` row (auto-title, both messages); the next brain answer updates the SAME row; a reload at `/` keeps the link (the next message does not create a second row); "New chat" unlinks (a fresh conversation creates a fresh row on its first message). - [ ] A signed-out visitor sees the Share pill; clicking it on a non-empty conversation yields the public link (clipboard or the inline fallback field) **and** a top-right toast; the link opens read-only in a fresh anonymous context; an admin unshare from the History page revokes it. - [ ] Desktop (>640px): the New chat and Share pills share one horizontal row (Share right of New chat); ≤640px: stacked vertically (New chat above Share); no horizontal overflow at 360px. - [ ] `uv run pytest` green; coverage TOTAL >90%. - [ ] `uv run pytest tests/e2e/test_save_share_ux.py -v --no-cov` green in isolation (DB up). - [ ] Regression E2E suites green in isolation: `test_share_chat.py`, `test_chat_history.py`, `test_chat_persistence.py`, `test_stale_saved_chats.py`, `test_smoke.py`. - [ ] `uv run ruff check . && uv run pyright` clean. - [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`. ## Locked decisions - **Owner-locked (2026-08-31, roadmap confirmation):** 1. **A1 — the write surface is public.** `POST /api/chats` (create, incl. save-then-share), `PUT /api/chats/`, and `POST /api/chats//share` require **no session**. Row ids stay unguessable `uuid4` — the same trust model as the share token (the token IS the credential, phase 51). The management surface stays admin-only: `GET /api/chats` (list), `GET /api/chats/` (detail), `DELETE`, and `POST //unshare` — the owner's History surface. Guest chats appear in the admin's History (saved by default, per L4). **This supersedes the phase-50 owner lock "save/history is admin-only".** 2. **A2 — auto-save contract.** Triggers: the first user message creates the row (auto-title as today); every brain-done save point and the pagehide partial update it. A failed auto-save **never blocks the conversation** — a one-line status note only (no error banner), retried at the next save point. Successful auto-saves are silent (the History page is the visible proof; the toast is reserved for share, per L5). 3. **A3 — `/?chat=` boot restore stays admin-only** (History "Open"); guests keep the localStorage restore exactly as today. 4. **A4 — the toast.** Top-right, slides down, auto-dismisses ~4s, a single instance (a new toast replaces a pending one). **Visual only** (`aria-hidden`) — the existing `#send-status` live region remains the a11y announcer (no double screen-reader read). Shown on BOTH share-success paths (clipboard copied / fallback field). Never on failure (the error banner is the failure UI). 5. **A5 — the layout.** A `.chat-actions` wrapper around New chat + Share; horizontal row on desktop, vertical stack at the existing 640px breakpoint; pill order New chat → Share in both orientations; the existing ≤640px pill rules (padding, icon/label handling, the `.chat-shell` label overrides) stay and apply to the stacked pills. - **A10 honoured** — `/api/chat` stays stateless; auto-save writes the `saved_chats` row, not the chat endpoint. - **A16/A17 honoured** — one dedicated story E2E suite, one atomic commit. ## Commit ```bash git add -A app/ frontend/ tests/ && git add -f .agents/phases/todo/55_save_share_ux .agents/phases/complete && git commit --no-gpg-sign -m "feat(chat): save by default + share anonymously — auto-saved chats, guest-facing Share, success toast, action row" ```