Rename (owner ask): clicking the header title opens edit mode with the whole title selected — typing replaces, arrows move inside. Enter or a click away commits (PUT /api/chats/:id); Escape and an empty commit discard. renameChat joins the ChatStore port; like saveSettings it is NOT conversation activity — updatedAt stays put so sidebar order never churns on a rename. The sidebar patch is instant: the header dispatches lh:chat-renamed and SessionsSidebar folds it into its snapshot (the 2.5s poll is only a backstop). Sidebar right-click rename is DELIBERATELY deferred: it needs a real context-menu system, which gets its own pass later (owner decision, 2026-09-17). Graph chrome: - The extraction-coverage button moves between search and import; the gear keeps the far-right corner. Coverage still renders on an EMPTY graph (that is exactly when it matters). - The no-match search note no longer shifts its siblings: the chrome row is items-start (every button is 44px tall, so top alignment pins the row while the search container grows downward). - Popovers dismiss properly: pointerdown anywhere outside the control's root (button + panel) closes it, and so does Escape. Two panels can never be open at once — the sibling chrome buttons are outside the open root, so pointerdown closes the first before the click opens the second. New shared hook components/popover-dismiss.ts, wired into both the settings gear and the coverage control. Gates: typecheck, eslint, 498/498 (+2 rename store tests), build. Live-verified: select-all/focus/Enter/Escape/empty/blur round-trip, updatedAt untouched, sidebar instant patch; chrome order + frozen positions under a no-match query; popover close via canvas, search input, Esc, mutual exclusion, toggle intact.
31 lines
1.4 KiB
TypeScript
31 lines
1.4 KiB
TypeScript
import { getChat, isTurnRunning, stores } from "@/src/server";
|
|
|
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
const { id } = await params;
|
|
const chat = getChat(id);
|
|
if (!chat) return Response.json({ error: "Not found" }, { status: 404 });
|
|
// turnInFlight lets a returning client re-attach to a running turn
|
|
// (chat-handler write-ahead + registry — see src/server/turn-liveness.ts).
|
|
return Response.json({ ...chat, turnInFlight: isTurnRunning(id) });
|
|
}
|
|
|
|
/**
|
|
* Rename (2026-09-17): the header's click-to-edit title commits here.
|
|
* Validation is a single rule — a non-empty string after trim (the store's
|
|
* "Untitled session" floor is never reached from this route). updatedAt is
|
|
* deliberately NOT bumped: a rename must not reorder the sidebar (same
|
|
* rule as the settings PUT).
|
|
*/
|
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
const { id } = await params;
|
|
if (!getChat(id)) return Response.json({ error: "Not found" }, { status: 404 });
|
|
const body = (await req.json().catch(() => null)) as { title?: unknown } | null;
|
|
if (typeof body?.title !== "string" || body.title.trim() === "") {
|
|
return Response.json({ error: "title must be a non-empty string" }, { status: 400 });
|
|
}
|
|
const title = body.title.trim();
|
|
const record = stores.chatStore.renameChat(id, title);
|
|
return Response.json({ title: record.title });
|
|
}
|
|
|