From 283013c09d4855529e846951a1e090f0f16030a8 Mon Sep 17 00:00:00 2001 From: bndw Date: Sat, 28 Feb 2026 19:34:02 -0800 Subject: feat: auto session naming --- renderer/src/App.tsx | 70 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 5 deletions(-) (limited to 'renderer/src/App.tsx') diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx index f7ba41d..19f6284 100644 --- a/renderer/src/App.tsx +++ b/renderer/src/App.tsx @@ -8,6 +8,38 @@ import "./styles/globals.css"; const api = window.api; +/** + * Derive a short session name (≤30 chars) from research.md content. + * Looks for the first non-empty body line under ## Overview. + * Falls back to the first non-heading line in the document. + * Returns null if nothing usable is found. + */ +function extractSessionName(content: string): string | null { + const truncate = (s: string) => + s.length > 30 ? s.slice(0, 28) + "\u2026" : s; + + // Primary: first sentence of ## Overview body + const overviewMatch = content.match(/##\s+Overview\s*\n([\s\S]*?)(?=\n##|\n---)/); + if (overviewMatch) { + const firstLine = overviewMatch[1] + .split("\n") + .map((l) => l.trim()) + .find((l) => l.length > 0 && !l.startsWith("#")); + if (firstLine) { + const firstSentence = firstLine.split(/[.?!]/)[0].trim(); + if (firstSentence.length > 0) return truncate(firstSentence); + } + } + + // Fallback: first non-heading line anywhere in the document + const firstLine = content + .split("\n") + .map((l) => l.trim()) + .find((l) => l.length > 0 && !l.startsWith("#")); + if (!firstLine) return null; + return truncate(firstLine); +} + type Theme = "dark" | "light"; export function App() { @@ -125,11 +157,27 @@ export function App() { if (selectedProject && selectedSession) { const filename = selectedSession.phase === "research" ? "research.md" : "plan.md"; - api.readSessionArtifact(selectedProject.id, selectedSession.id, filename).then((content) => { - const text = content || ""; - setDocumentContent(text); - setOriginalContent(text); - }); + // Capture for the async closure + const sessionAtTrigger = selectedSession; + api + .readSessionArtifact(selectedProject.id, sessionAtTrigger.id, filename) + .then((content) => { + const text = content || ""; + setDocumentContent(text); + setOriginalContent(text); + + // Auto-name: only during research, only while name is still the default + if ( + sessionAtTrigger.phase === "research" && + /^Session \d+$/.test(sessionAtTrigger.name) && + text.length > 0 + ) { + const derived = extractSessionName(text); + if (derived) { + handleRenameSession(sessionAtTrigger.id, derived); + } + } + }); } } @@ -285,6 +333,17 @@ export function App() { } }; + const handleRenameSession = async (id: string, name: string) => { + await api.renameSession(id, name); + setSessions((prev) => + prev.map((s) => (s.id === id ? { ...s, name } : s)) + ); + // Use functional updater to avoid stale-closure issues + setSelectedSession((prev) => + prev?.id === id ? { ...prev, name } : prev + ); + }; + return (
-- cgit v1.2.3