simocracy-v2 · standalone-chat-page
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.
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 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.
/chat/nyaori-katalog on the live build (signed-in, fresh thread).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.
/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.
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.
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().
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).
/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.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.
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.
GET 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/3mm3hozesms2e — userMessage = "What does good governance mean to you, Nyaori Katalog?"
Hit #2at://…/org.simocracy.history/3mm3hw4evsc2e — userMessage = "Sum that up in one sentence."
Hit #3at://…/org.simocracy.history/3mm3ie2xgoc2e — userMessage = "Hello Nyariko! Briefly: what’s your philosophy?"
Hit #4at://…/org.simocracy.history/3mm3isr2pgs2e — userMessage = "What’s your one core value, in 5 words?" (from the recording)
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.
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.
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.
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.
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.
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.
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.
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.
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.
/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.
+ 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.
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.
app/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).
main at commit 4b91376
Bootcd simocracy-v2 && npm run dev → http://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 writescurl -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.