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.
5.2 KiB
5.2 KiB
Task 03 — The shared page (anonymous read-only)
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
/shared/<token> renders the shared conversation for anyone — read-only, zero controls, nothing the share shouldn't expose — through the same record shape the chat uses.
Work
frontend/shared.html(new) — the page scaffold ashistory.htmldoes it: the head (charset, viewport,assets/styles.css), the skip link, the identical shared header (the nav with all the admin-only links hidden — a guest never sees them — and the auth pair with?next=/on the Sign-in link: a guest signing in from a shared page returns to the app root; note this in a comment), then<main id="main">carrying the#steering-panelsection +#steering-announcer(the shared panel ships on every page — phase 34) and the page content:- an
<h1 id="shared-title">(JS-filled with the shared chat's title; static fallback text "Shared conversation"); - a
.shared-noteline — "Shared via Brain of Reese — read-only." (the brand resolves through thewindow.BOR_BRANDconvention like the other pages); - the messages section —
<section class="messages" id="messages" aria-label="Shared conversation">using the same.msg/.bubble/.thinking/.tool-callsstructure as the chat page, so the existing CSS applies unchanged (the shell maps to the 46rem centered chat column — reuse the.chat-shellclass or a.shared-shellthat maps to the same width rule, per the PLAN §7 column contract); - the invalid-state block
#shared-invalid(hidden by default): "This share link is invalid or was revoked." - the app-footer (version span, like the other pages).
- Scripts:
<script src="assets/brand.js"></script>(classic, first) +<script src="assets/markdown.js"></script>(the classic renderer, asindex.htmlloads it) +<script type="module" src="/assets/shared.js"></script>. Nodocument-modal.js, no composer, no Save/Share/Retry/Tune markup at all (owner-locked: zero controls). No-CDN rule holds (local assets only).
- an
frontend/assets/shared.js(new) — a module:- read the token from
location.pathname(the last path segment of/shared/<token>; a malformed/missing token → show#shared-invalidimmediately, no fetch). await initSharedHeader()(the header works for guests — whoami anonymous, the admin links stay hidden), thenGET /api/shared/<token>:- 200 → set the
h1to the title; render every message through a localrenderSharedMessage(m)reusing the chat's record shape: user → the.msg.userbubble; brain → the.msg.brainbubble with the optional thinking block (collapsed — the phase-17 restore convention), the tool lines, theis-deflectedclass, the stopped note (the ~8-lineappendStoppedNotemarkup duplicated locally — the per-page duplication house style), the deflection's "Maybe try" chips as plain<span class="suggestion-chip">text (not buttons — a guest tapping a chip has nowhere to go; owner-locked zero controls), and the source chips as plain text<span>(owner-locked: guests cannot open documents — the documents API is admin-only; nohref, no modal wiring). - 404/other → show
#shared-invalid(the title keeps its fallback), no data rendered, no error banner.
- 200 → set the
- markdown through the global
renderMarkdown(escape-first — the stored payloads are raw text, so the renderer's XSS safety applies unchanged).
- read the token from
frontend/assets/styles.css—.shared-note(the muted meta line under the h1); the static-chip treatment scoped to the shared page (e.g..shared-shell .suggestion-chip { pointer-events: none; cursor: default; }— or a distinct.chip-staticclass if cleaner; the interactive chips' styles elsewhere stay untouched); the invalid-state styling (a centered muted block); the shared shell's 46rem column mapping; the ≤640px responsive behavior (the phase-07 contract).- Frontend source pins (house pattern): the token parse (a malformed path → no fetch, the invalid state shows); the 404 → invalid state (no data render); zero interactive controls (pin:
renderSharedMessagenever callsrenderChips/appendTuneButton/appendRetryButton, and the rendered shared messages contain no<button/<form— i.e. chips are spans, source chips carry nohref);shared.htmldoes not referencedocument-modal.jsor a composer.
- ASSUMPTION (owner-locked 2026-08-29): the shared page shows the full conversation (thinking collapsed) read-only; zero interactive controls; the "Maybe try" chips are plain text; the source chips are plain text (no document access for guests).
Testing & Quality
- Unit: source pins as above; full suite green.
- Coverage: >90% on
app/(unchanged — frontend-only task).
Completion Criteria
GET /shared/<token>(the task-01 route) serves the page; a fresh anonymous browser renders the conversation read-only.- A wrong/revoked token shows the invalid state; no control exists anywhere on the page.
uv run pytestgreen;uv run ruff check . && uv run pyrightclean.