feat: session rename + graph chrome polish (popovers dismiss, no-shift no-match, coverage reseat)
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.
This commit is contained in:
@@ -54,6 +54,10 @@ export interface ChatStore {
|
||||
/** Persist the per-chat model/thinking settings (the model control).
|
||||
* Replace-whole: the picker sends the complete settings object. */
|
||||
saveSettings(id: string, settings: ChatSettings): void;
|
||||
/** Rename a session (the header's click-to-edit title, 2026-09-17).
|
||||
* Like saveSettings: a rename is NOT conversation activity — updatedAt
|
||||
* stays put, so sidebar order never churns on a rename. */
|
||||
renameChat(id: string, title: string): ChatRecord;
|
||||
}
|
||||
|
||||
// ─── GraphStore — the append-only evidence ledgers (data/graph) ──────────────
|
||||
|
||||
@@ -116,6 +116,19 @@ function saveSettings(id: string, settings: ChatSettings): void {
|
||||
fs.writeFileSync(chatFile(id), JSON.stringify(record, null, "\t"), "utf-8");
|
||||
}
|
||||
|
||||
/** Rename a session (the header's click-to-edit title, 2026-09-17). Like
|
||||
* saveSettings, a rename is NOT conversation activity: updatedAt stays put
|
||||
* so sidebar order never churns. Same fallback as createChat — the empty
|
||||
* string becomes "Untitled session" (the API gate rejects it first; this
|
||||
* is the honest floor for any other caller). */
|
||||
function renameChat(id: string, title: string): ChatRecord {
|
||||
const record = getChat(id);
|
||||
if (!record) throw new Error(`Chat not found: ${id}`);
|
||||
record.title = title.trim() || "Untitled session";
|
||||
fs.writeFileSync(chatFile(id), JSON.stringify(record, null, "\t"), "utf-8");
|
||||
return record;
|
||||
}
|
||||
|
||||
/** Persist the session plan tree (plan tools call this in execute). */
|
||||
function saveTree(id: string, tree: PlanTree): void {
|
||||
const record = getChat(id);
|
||||
@@ -133,6 +146,7 @@ export const chatStore: ChatStore = {
|
||||
saveMessages,
|
||||
saveTree,
|
||||
saveSettings,
|
||||
renameChat,
|
||||
appendTurnUsage,
|
||||
appendExtractionUsage,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user