Proposal: Chat Mode¶
Status: Shipped — Chat is the live default mode at /chat · Last updated: 2026-07-20 (status audit) · Tracking: issue #1
As-built note (2026-07-20): shipped as proposed, then evolved by later proposals — chrome/UX by
floating-glass-shell.md, transcript branching bymessage-forks.md+background-fork-runs.md, attachments bychat-attachments.md, the paid built-in provider bycredit-balance-ux.md.
Summary¶
Chat becomes PlebChat's default mode: a simple, multi-turn agent chat (ChatGPT-shaped) living at /chat, listed first in the mode selector. The mode-card welcome home is retired; / is the front door (brochure logged out, redirect to Chat logged in). Chat reuses the mode-agnostic agent runtime ($lib/ai) and Search's proven thread patterns, with device-local per-npub persistence and ?thread= deep links.
This document covers Chat v1 (issue #1 phases 1–2). The logged-out brochure build-out, demo, unified settings panel, and SEO artifacts (phases 3–6) are in proposals/brochure-settings-seo.md.
Motivation¶
"PlebChat" should feel like a chat app with specialist modes, not four equal mini-apps behind a chooser. A plain conversational agent is also the natural on-ramp: no Firecrawl key, no project setup — configure an endpoint and talk.
Design¶
Shell & routing¶
AppModegainschat, listed first. Amended 2026-07-13 (phase 3): Chat's route is/chat, not/— the root became the front door (whole-site brochure logged out, redirect to/chatlogged in) so it stays a pure marketing/SEO surface. Seeproposals/brochure-settings-seo.md. Existing modes keep/read,/search,/synthesize,/debate.- Debate's icon moves to
Scale; Chat takesMessagesSquare. - The mode store no longer persists to localStorage: deep links preserve the mode in the path, so the current mode is fully derivable from the URL (the root layout syncs it on navigation, counting
/as Chat). "Restore last mode on/" is explicitly rejected. - The ModeSelector's home-page "Select a mode" special case is retired along with the mode-card welcome.
- Logged-out
/chatis Chat's capability brochure, with the read-only seeded demo behind?demo.
Data model (DB v4)¶
The former ChatThread (Read mode's per-book chat) is renamed BookChatThread; Chat mode owns the clean name. New store chatThreads (id key, by-updated index):
interface ChatThread {
id: string; // `chat-<nanoid>` — the ?thread= deep-link token
title: string; // first message excerpt; renameable
pinned?: boolean;
messages: unknown[]; // verbatim pi AgentMessage transcript
createdAt: number;
updatedAt: number;
}
Single-record shape like SearchThread (no metadata/transcript split — the rail loads all threads anyway). Device-local, deliberately outside the sync schema, same stance as Search threads and Read chats. No per-thread agent settings in v1 (the workspace's AgentSettings shows how to add them later).
Agent¶
- One
AgentRunnerper thread (lazy map, kept across switches so concurrent runs keep streaming), copied fromsearchThreads' pattern — including the "no$statewrites insiderunnerFor()" invariant (it runs inside a$derived). - Zero tools in v1:
buildTools: () => []. Still a real agent loop, so tools can land later with no plumbing changes (the runner rebuilds model/prompt/tools everysend()). - Fixed persona prompt in
$lib/chat/prompt.ts; a user-editable system prompt waits for the unified settings panel.
Deep links¶
/chat?thread=<id> mirrored with replaceState, gated on handledInitialUrl (the same two-effect pattern as ?book= and ?project= — the mount-time mirror otherwise erases the param before it's read). Unknown or stale ids fall through silently to the hero view. The read-effect gates on chatThreads.ready, so a deep link opened while logged out survives login for free.
UI ($lib/chat/)¶
ChatView: left thread rail + main pane. Hero (tagline + input) when no thread is active; header + message list + input once one is.- Thread rail: expanded by default (ChatGPT-shaped), searchable, pinned group above date groups, inline rename, pin/unpin, confirm-to-delete (SvelteReader pattern).
- Messages render via the shared
renderMarkdown(moved to$lib/ai/markdown.tsnow that Chat is its third consumer) and$lib/ai/renderhelpers. No sources rail, no tool cards in v1. - Unconfigured-AI gate opens the unified settings surface — since 2026-07-14 the full-page
/settingsroute (the dialog andsettingsUiare gone; seesettings-page-and-chat-session-settings.md). The input-bar gear is now session settings: per-thread model + thinking level.
Deferred (tracked in issue #1, not v1)¶
Personas, attachments, tools, model-generated titles, opt-in encrypted thread sync (NIP-44 self-encrypted addressable events — design together with the workspace event-kind decision), selective sharing via child/ephemeral keys, richer brochure + demos.
Testing¶
e2e/chat.test.ts, mocked SSE endpoint (chat.completion.chunk frames), settings seeded via localStorage['plebchat-settings']:
- Logged-out
/shows the pitch; logged-in unconfigured shows the settings gate. - Chat roundtrip: bubbles, markdown, thread title, URL gains
?thread=. - Deep link: reload on
/?thread=<id>reopens the thread; stale id falls back to hero. - Rail housekeeping: rename, pin (ordering), confirm-delete.
- Shell: Chat first in selector, Debate uses Scale,
/searchstill works on refresh.
Progress¶
- Proposal doc
- Mode store: chat first,
/route, Debate icon → Scale, drop localStorage persistence - DB v4:
BookChatThreadrename +chatThreadsstore -
$lib/chat/(prompt, session, threads store, ui store, components) -
/chatroute: brochure + demo (logged out) / ChatView +?thread=(logged in);/front door -
renderMarkdownmoved to$lib/ai/markdown.ts - Playwright e2e (chat.test.ts + shell.test.ts rework)
- Phases 3–6 of issue #1 — see
proposals/brochure-settings-seo.md