Files
brain-of-reese/.agents/phases/complete/51_share_chat/02_share_ui.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

4.6 KiB

Task 02 — Share button (chat) + the History share column

Phase: 51_share_chat · Source: TODO.md:6 — "Need a way to share a chat with a link so others can see it anonymously." Story: n/a (TODO-derived)

Objective

Two share entry points for the admin: the Share button on the chat page (an unsaved conversation is saved + shared in one action) and a Share column on the History table (create link / copy / unshare) — both copy the link with a clipboard + inline-link fallback.

Work

  1. frontend/index.html — beside #save-chat-btn, #share-chat-btn (the same ghost-pill family: an inline link/share SVG + the label "Share", aria-label="Share chat", ships hidden — revealed for admin exactly like Save); a comment documenting the save-then-share contract (2026-08-29, TODO.md L6).
  2. app/schemas.py + app/api/chats.py (the small server side of this task): SavedChatCreate gains share: bool = False; the create route, when share is true, sets share_token = uuid.uuid4() in the same commit and the response carries share_url. SavedChatOut and SavedChatRow each gain share_url: str | None (None → absent from the JSON) — the list endpoint populates it, so the History column renders from GET /api/chats without a second fetch.
  3. frontend/assets/app.js:
    • shareCurrentChat() — the #share-chat-btn handler (the same empty-conversation no-op guard as Save): if currentChatId is set → POST /api/chats/<id>/share; else → POST /api/chats with { messages: conversation, share: true } → currentChatId = created.id (one action saves and shares — owner-locked). On success: copy the absolute URL of share_url — navigator.clipboard.writeText(...) in a try; on success the live region reads "Share link copied."; on failure (a non-secure http origin rejects the clipboard) render the inline fallback: a transient link field (an <a> styled as a select-on-focus field, carrying the full URL) near the status line + the live region "Share link ready — copy it from the field." (owner-locked fallback). On 403/5xx/network: showErrorBanner with an actionable line.
    • Reveal on boot: #share-chat-btn joins the same admin-reveal block as #save-chat-btn.
    • Header comment: the share contract (2026-08-29, TODO.md L6).
  4. frontend/history.html + frontend/assets/history.js — the Share column between Updated and the Actions/Delete cell (the th + the per-row cell; the table stays full-width):
    • row already shared (share_url present): a Copy button (the same clipboard + fallback helper — the per-page duplication house style: history.js keeps its own ~10-line copy of the helper rather than a new shared module) and an Unshare button (inline two-step, the phase-50 Delete-confirm pattern: "Unshare? [Yes] [No]" → POST /api/chats/<id>/unshare → the cell re-renders to the unshared state + the live region).
    • row unshared: a Create link button → POST /api/chats/<id>/share → the cell re-renders to the shared state (Copy + Unshare) and the link is offered for copying (same fallback pattern).
  5. frontend/assets/styles.css — .share-chat-btn (the pill family), the .history-share cell buttons (the Tune/Retry-family ghost buttons, ≥44px comfortable, focus-visible), .share-link-fallback (the inline link field — input-like look, select-on-focus), reusing the phase-50 .history-confirm styles for the unshare two-step.
  6. tests/integration/test_chats_api.py — extend: create-with-share (the response carries share_url matching the token shape and the row is immediately publicly readable), create-without-share (no share_url), the list rows carry share_url when shared and absent otherwise, GET /{chat_id} carries it too.
  7. Frontend source pins (house pattern): the shareCurrentChat branches (linked → POST share; unlinked → create-with-share + currentChatId set); the clipboard try → fallback element on rejection; the History cell's three states (unshared / shared / confirming-unshare) + the two-step unshare; #share-chat-btn ships hidden and reveals only for admin.
  • ASSUMPTION (owner-locked 2026-08-29): the Share button saves + shares an unsaved conversation in one action; the clipboard copy has the inline-link fallback; unshare is inline two-step.

Testing & Quality

  • Integration: as above; full suite green.
  • Coverage: >90% on app/ (the create-with-share branch covered).

Completion Criteria

  • Admin: the Share button works on a saved (linked) and an unsaved conversation; the History column creates / copies / unshares per row.
  • uv run pytest green; uv run ruff check . && uv run pyright clean.