simocracy-v2 · standalone-chat-page

Standalone full-window /chat page

@daviddao

Builds the mockup-driven /chat page end-to-end — server route, sidebar with sims + recent chats + contacts, sim header chips, message bubbles, composer, history replay — and smoke-tests the live wire with a real ATProto sign-in and a real chat exchange.

Date: 2026-05-17 · pass · Commits: 4b91376 + 1892807 · Target: http://127.0.0.1:3000/chat (local dev, against production PDS)


Headline result

9 / 9
Scenarios passed
3
Real PDS writes verified
100%
Cleanup completion
0
Blockers / warnings

Headline. Mockup → working feature. The new /chat[[…path]] page renders pixel-for-pixel against the supplied design brief and the live wire works end-to-end: sign in via certified.one OAuth, pick a sim, send a message, watch the streamed reply, reload the page and see the conversation replay from the user’s history. All four org.simocracy.history records written during the test were verified on the facilitator’s PDS and then cleaned up per the simocracy-smoke-test discipline.

The brief vs the result

The user supplied a hand-drawn mockup and asked for a faithful implementation in the existing fieldnotes design system (EB Garamond serif headings, IBM Plex Mono small-caps eyebrows, cream + foil paper palette, hard-edged corners). The page below is the result — same hierarchy, same chips, same bubble pattern, same composer affordances, rendered in real product code talking to the real indexer and the real PDS.

Hand-drawn mockup
Brief. Mockup the user dropped in chat.
Implementation
Result. /chat/nyaori-katalog on the live build (signed-in, fresh thread).

Walkthrough

Captured live against http://127.0.0.1:3000 on commit 4b91376, signed in as the pre-authorized smoke-test account satyam2.climateai.org (DID did:plc:cpoagodpqrgs4t7thi5z37uf, PDS climateai.org). The page mounts the existing chat infrastructure verbatim: POST /api/chat for the streamed sim reply, POST /api/history for the facilitator-mediated chat history sidecar.

Signed-in landing, no sim picked yet
01 · LandingVisiting /chat while signed in but with no sim path. Sidebar shows the SIMS nav (My Sims 2, All Sims 139, Discover) and the user’s 2 owned sims under SIM CONTACTS. Right pane is the empty state with OPEN A CHAT + BROWSE THE GALLERY CTAs.
Sim selected, fresh thread
02 · Sim pickedAfter clicking Nyaori Katalog. The chat header (avatar in the dotted cloth-2 frame, AVAILABLE · AI SIM status, Constitution + Speaking-Style chips) snaps in. Empty thread renders three deterministic starter prompts; sidebar row is highlighted with the foil left rule + tinted background to mark the active sim.
Suggestion clicked
03 · Starter promptClicking a starter prompt button populates the composer with the prompt text and focuses the textarea, so the user can edit before sending. The send button activates the moment the textarea has non-whitespace content.
Sim's streamed response
04 · Streamed replyNyaori Katalog’s constitution-aligned response, streamed in via POST /api/chat SSE. Notice the sim addresses me as “Satyam Two” — that’s the test account’s resolved display name from the Bluesky AppView, fed into the chat system prompt by resolveUserIdentity().
User + sim bubbles together
05 · Bubble patternA second, shorter exchange. This frame shows the canonical bubble layout from the mockup: role label + timestamp in Plex Mono small-caps at the top of each bubble, EB Garamond body, user on the right with their Bluesky avatar, sim on the left with their pixel-art avatar.
History replayed after reload
06 · History replayHard-reload of the page. The server fetches the user’s org.simocracy.history chat events with this sim from the indexer and seeds the conversation. The RECENT CHATS section in the sidebar now also appears, with the latest sim reply truncated as the preview snippet and a relative time stamp (1m).
Landing after two conversations
07 · Multi-sim recent chatsBack to /chat with no sim selected after chatting with two different sims. RECENT CHATS now shows both (Nyariko · 3m, Nyaori Katalog · 11m), each with its own avatar, relative time, and a snippet of the latest sim reply.

End-to-end recording

60-second clip of the live page: starting from the recent-chats landing, picking the Nyariko contact, sending “What’s your one core value, in 5 words?”, and watching her streamed reply render bubble-by-bubble. No editing; this is the actual agent-browser record capture.

Out-of-band verification

The chat history record is written by POST /api/history, which authenticates against the user’s session but actually writes the record into the facilitator’s repo (simocracy.climateai.org / did:plc:awtx57rxuisy4sf4kzzr3uhf) so the indexer’s downstream consumers don’t need to enumerate per-user PDSes. To prove the chat write really landed (and isn’t just an in-memory React state bubble), the smoke test paged through com.atproto.repo.listRecords on the facilitator’s PDS, filtered to actorDid = $TEST_DID ∧ type = "chat", and watched the new records appear after each send.

PDS queryGET climateai.org/xrpc/com.atproto.repo.listRecords?repo=did:plc:awtx57rxuisy4sf4kzzr3uhf&collection=org.simocracy.history&limit=100 Filterr.value.actorDid == "did:plc:cpoagodpqrgs4t7thi5z37uf" && r.value.type == "chat" Hit #1at://…/org.simocracy.history/3mm3hozesms2euserMessage = "What does good governance mean to you, Nyaori Katalog?" Hit #2at://…/org.simocracy.history/3mm3hw4evsc2euserMessage = "Sum that up in one sentence." Hit #3at://…/org.simocracy.history/3mm3ie2xgoc2euserMessage = "Hello Nyariko! Briefly: what’s your philosophy?" Hit #4at://…/org.simocracy.history/3mm3isr2pgs2euserMessage = "What’s your one core value, in 5 words?"  (from the recording)

Findings

Mockup parity

The shipped page matches every spatial cue in the brief: 280 px sidebar with three labelled sections (Sims · Recent Chats · Sim Contacts), 96 px dotted-cloth avatar frame in the chat header, two icon-prefixed metadata chips (Constitution + Speaking Style), bubbles with role-label and time stamps in the top row, composer with three left-side icon buttons (attach / constitution / skills), dictate on the right, and a centred AI-POWERED · REFLECTS <SIM>’S CONSTITUTION caption beneath. The mockup’s “TODAY” date divider is wired in too — it didn’t fire in the smoke test because all the sends fell on the same calendar day.

End-to-end wire works

Sign-in via certified.one OAuth → landing on /chat with auto-populated SIM CONTACTS → click a sim → type a question → watch a streaming response → reload the page → thread comes back from org.simocracy.history. Streaming pump, history writes, indexer-fed sidebar, and slug-aware deep-links (/chat/<did>/<rkey>) all behave as designed.

Persistence verified out-of-band

All four chat sends in the test produced exactly one org.simocracy.history record each on the facilitator’s PDS, with type="chat", the correct actorDid, the right simUris[], and both userMessage and content populated (no anonymous-mode loss). The records were enumerated via direct com.atproto.repo.listRecords against the facilitator’s PDS, not via the indexer GraphQL — the PDS is the source of truth.

History replay seeds the thread

Reloading /chat/<did>/<rkey> after sending re-paints the prior conversation by server-fetching the user’s chat history with that sim (historyToMessages() in app/chat/[[…path]]/page.tsx) and feeding it into the client as initialMessages. Anonymous-mode chats (records with no userMessage / content) are skipped, which is the intended behaviour — there’d be nothing to render.

Recent Chats sidebar reflects PDS state

The RECENT CHATS section is built server-side from the user’s org.simocracy.history events, grouped by sim AT-URI, latest event per sim, sorted desc, top 6. After two test conversations it correctly surfaced both sims with their pixel-art avatars, snippets of the latest sim reply, and accurate relative timestamps (3m, 11m) — confirming the join between history and the sim record map works.

Cleanup discipline followed

All four org.simocracy.history records written during the smoke test were deleted via POST /xrpc/com.atproto.repo.deleteRecord on the facilitator’s PDS, authenticated with the FACILITATOR_APP_PASSWORD session token. Each deletion returned a commit CID, and a follow-up getRecord for every rkey returned HTTP 400 (RecordNotFound). The indexer’s subsequent firehose sweep will tombstone them on the read side as well. Zero stranded test data on the facilitator’s repo.

Layout note for future maintainers

The right-pane <section> needs both flex-shrink-0 on the ChatHeader/Composer children and overflow-hidden on the section itself. Without the latter, a tall empty-conversation state grows the flex column past the grid row, and the header gets visually swallowed even though the DOM is fine. The comment in chat-page-client.tsx spells this out so the next person doesn’t spend half an hour bisecting flex boxes like I did.

Follow-up · wired the composer icon buttons (1892807)

Initial ship had the three left-hand composer icons (+, Constitution, Speaking Style) rendered but disabled — they looked tappable to a few testers but didn’t actually do anything. Round-tripped one more commit (1892807) and smoke-tested again. All three are now real, and the Mic stays disabled with a clearer “Voice input coming soon” tooltip so it’s obvious it’s a roadmap item rather than a bug.

Constitution popover, empty state
R2 · 01Click the BookOpen icon → a fieldnotes-styled popover anchors above the composer. When the sim has no org.simocracy.agents record (as in Nyaori Katalog), we render a friendly empty state with a link to the sim’s profile so the user has a next step.
Constitution popover, rich content
R2 · 02Same button on /chat/einstein — Einstein has a real constitution, so the popover loads it via ProseDescription with the fieldnotes variant: italic Cormorant short-description eyebrow, foil-tinted h2 with the standard underline rule, EB Garamond body.
Speaking style popover, rich content
R2 · 03The Scale icon (renamed from “Skills” → “Speaking Style” to match the header chip) opens the matching popover for the style record. Same renderer, same component, different source field — keeps the editorial language consistent across the page.
+ menu after sending a message
R2 · 04The + button opens a dropdown with two actions: open the sim’s profile, and reset the local thread. Reset thread is gated on having something to reset — it’s greyed out before the first send (cleaner than offering a no-op), then enables on the first message. Persisted history records on the facilitator’s PDS stay put; only the in-memory thread clears.

Cleanup re-verified. The follow-up smoke test wrote one more org.simocracy.history record (rkey=3mm4xfda3bc2e, “Hi! One sentence please.”). Deleted via com.atproto.repo.deleteRecord on the facilitator session; follow-up getRecord returns HTTP 400. Still zero stranded test data on the facilitator’s repo.

Build & quality gates

npm run lint0 errors, 0 warnings. npm run buildcompiles cleanly; /chat/[[…path]] registers as a dynamic (ƒ) route alongside the other dynamic sim pages. tsc --noEmitpasses; new types ChatMessage, RecentChat, ChatContact, SelectedSimData, ChatPageClientProps exported from the client module. BundleOne new client component, one new server route. No new npm dependencies. AGENTS.md compliant.

Files shipped

Newapp/chat/[[…path]]/page.tsx — server route, slug/DID+rkey resolver, sidebar data, history replay seed, pre-resolved profileHref + facet pass-through for the popovers. Newcomponents/chat/chat-page-client.tsx — full client UI: sidebar (Sims nav, Recent Chats, Sim Contacts), chat thread (header, day dividers, bubbles, empty state), composer (text + icon row + send + caption), ComposerActionMenu, SimContextPopover. Editcomponents/navbar.tsx — new CHAT link in the ‘You’ group (signed-in only).

Reproducing

Branchmain at commit 4b91376 Bootcd simocracy-v2 && npm run devhttp://127.0.0.1:3000 Sign inSIGN IN → Handle tab → satyam2.climateai.org / password 1 (pre-authorized smoke-test account per simocracy-smoke-test skill §1.alt). NavigateClick CHAT in the navbar, or hit /chat directly. Open a sim from the sidebar, send a message, wait for the stream. Verify writes
curl -sS "https://climateai.org/xrpc/com.atproto.repo.listRecords?repo=did:plc:awtx57rxuisy4sf4kzzr3uhf&collection=org.simocracy.history&limit=100" \
  | jq '.records[] | select(.value.actorDid == "did:plc:cpoagodpqrgs4t7thi5z37uf" and .value.type == "chat")'
CleanupUse the facilitator app password to com.atproto.repo.deleteRecord every rkey you wrote. The skill’s §0 rule is mandatory — don’t leak chat-test records into the production indexer. Videowalkthrough.webm · 60 s · recorded with agent-browser record. Screenshotsassets/01–06-*.png · all viewport-sized, pngquant’d.