feat: the sessionless draft view — "+ New session" creates nothing

The sidebar button no longer POSTs a record (no more "Untitled session"
litter): it opens /chat/new, a sessionless draft view visually identical
to a fresh chat (same header + probe chip, the same ConversationEmptyState,
the same composer blob + stats dock). The session comes to exist only
when the learner sends the first message — and that message becomes its
title (startDraftSession: POST /api/chats {title: msg} → PUT the lifted
pre-chat settings → redirect /chat/<id>?prompt=<msg>, whose initialPrompt
path delivers the message as turn 1 — the home composer's proven contract).

- app/chat/new/page.tsx: the static draft route (static beats [id]).
- components/new-session.tsx: DraftShell (pure render seam) + NewSession
  (router wiring); the model picker's empty-chatId branch defers its save
  and lifts the selection, so pre-chat model/thinking rides the creation.
- components/sidebar.tsx: newSession is a plain router.push("/chat/new").
- tests/render-new-session.test.tsx: 6 pins — the create-on-send contract
  (title = message, settings PUT, no-op on empty text) + the shell render.

Live-verified end to end: button → draft (0 creations), pre-chat Qwen
pick, send → exactly one session titled with the message, settings
persisted, redirect + turn 1 running. Gates 431/431.
This commit is contained in:
amosblomqvist
2026-09-14 09:06:48 +03:00
parent 7f001cdb46
commit 0f6e36f85f
4 changed files with 271 additions and 7 deletions
+6 -7
View File
@@ -186,13 +186,12 @@ function SessionsSidebar() {
const pathname = usePathname();
const router = useRouter();
/* New session: one POST, then hello. Search: a client-side filter over
the poll snapshot — no server round trip. Both were visual-only in
the wave-5 mock; wiring each is a single tiny handler. */
const newSession = useCallback(async () => {
const res = await fetch("/api/chats", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
const d = (await res.json().catch(() => null)) as { id?: string } | null;
if (d?.id) router.push(`/chat/${d.id}`);
/* New session (owner, 2026-09-13): NO POST — the button opens the
sessionless draft view; the session is created only when the first
message is sent (and that message becomes its title). Search: a
client-side filter over the poll snapshot — no server round trip. */
const newSession = useCallback(() => {
router.push("/chat/new");
}, [router]);
const shown = useMemo(() => {