Files
brain-of-reese/.agents/phases/complete/55_save_share_ux/00_phase.md
T
ducoterra dbf2af26c6 refactor(agents): migrate .agent/ planning tree to .agents/
Standardize on the .agents/ directory (shared with project skills):
phases/, user_stories/, reports/, screenshots/, validate.sh, and
phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves
history; runtime artifacts move alongside).

Updates every reference in AGENTS.md, README.md, .gitignore, app
docstrings, and test story headers. Historical KB content in data/
and the runtime pipeline.log transcript are left untouched.
2026-09-05 10:57:07 -04:00

8.7 KiB
Raw Blame History

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/<token> 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/<token> 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/<id>, and POST /api/chats/<id>/share all succeed; the same guest gets 403 on GET /api/chats, GET /api/chats/<id>, DELETE /api/chats/<id>, and POST /api/chats/<id>/unshare (unshare still revokes — the public /api/shared/<token> 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/<id>, and POST /api/chats/<id>/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/<id> (detail), DELETE, and POST /<id>/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=<id> 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

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"