Remove the blanket .agent/ gitignore so the phase roadmap, user stories, reports, and PLAN.md are versioned with the code. Only runtime artifacts (.agent/phase-sessions/, .agent/pipeline.log) remain ignored. Update AGENTS.md git protocol rule to match.
8.7 KiB
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 changedstyles.css/app.jsautomatically).50_chat_history+51_share_chat(complete) — thesaved_chatsrows, the/api/chatssurface, the save-then-share contract, the History page, the public/shared/<token>page.
Tasks
01_anonymous_save_share_api.md— open the save/share write surface to anonymous visitors (list/detail/delete/unshare stay admin-only).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).03_share_for_everyone.md— ship the Share pill visible to all visitors; neutral error copy.04_share_toast.md— the top-right slide-down toast confirming a successful share (both success paths).05_chat_actions_layout.md— the.chat-actionsrow: horizontal on desktop, stacked at ≤640px.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.pypattern):app.js(headless upsert helper, save-point triggers,chatIdin thebor.chat.v1record, no Save-pill wiring, no admin-gated Share reveal, toast helper),index.html(no#save-chat-btn,#share-chat-btnnothidden, the.chat-actionswrapper),styles.css(.save-chat-btngone,.chat-actionsbase + ≤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) andtests/e2e/test_share_chat.py(the Save-click step intest_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/chatswithshare: true,PUT /api/chats/<id>, andPOST /api/chats/<id>/shareall succeed; the same guest gets 403 onGET /api/chats,GET /api/chats/<id>,DELETE /api/chats/<id>, andPOST /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_chatsrow (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 pytestgreen; coverage TOTAL >90%.uv run pytest tests/e2e/test_save_share_ux.py -v --no-covgreen 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 pyrightclean.- One
--no-gpg-signcommit; phase dir moved to.agent/phases/complete/.
Locked decisions
- Owner-locked (2026-08-31, roadmap confirmation):
- A1 — the write surface is public.
POST /api/chats(create, incl. save-then-share),PUT /api/chats/<id>, andPOST /api/chats/<id>/sharerequire no session. Row ids stay unguessableuuid4— 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, andPOST /<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". - 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).
- A3 —
/?chat=<id>boot restore stays admin-only (History "Open"); guests keep the localStorage restore exactly as today. - 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-statuslive 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). - A5 — the layout. A
.chat-actionswrapper 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-shelllabel overrides) stay and apply to the stacked pills.
- A1 — the write surface is public.
- A10 honoured —
/api/chatstays stateless; auto-save writes thesaved_chatsrow, 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 .agent/phases/todo/55_save_share_ux .agent/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"