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.
14 lines
557 B
TypeScript
14 lines
557 B
TypeScript
import { NewSession } from "@/components/new-session";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
/**
|
|
* The draft view behind "+ New session" (owner, 2026-09-13): NO session is
|
|
* created until the first message is sent — the view is a sessionless empty
|
|
* chat; on send it creates the chat (title = that message) and redirects to
|
|
* the real page, whose initialPrompt path delivers the message as turn 1.
|
|
* Static segment beats the dynamic [id] route — "new" is never a chat id.
|
|
*/
|
|
export default function NewSessionPage() {
|
|
return <NewSession />;
|
|
} |