aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src/App.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'renderer/src/App.tsx')
-rw-r--r--renderer/src/App.tsx70
1 files changed, 65 insertions, 5 deletions
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";
8 8
9const api = window.api; 9const api = window.api;
10 10
11/**
12 * Derive a short session name (≤30 chars) from research.md content.
13 * Looks for the first non-empty body line under ## Overview.
14 * Falls back to the first non-heading line in the document.
15 * Returns null if nothing usable is found.
16 */
17function extractSessionName(content: string): string | null {
18 const truncate = (s: string) =>
19 s.length > 30 ? s.slice(0, 28) + "\u2026" : s;
20
21 // Primary: first sentence of ## Overview body
22 const overviewMatch = content.match(/##\s+Overview\s*\n([\s\S]*?)(?=\n##|\n---)/);
23 if (overviewMatch) {
24 const firstLine = overviewMatch[1]
25 .split("\n")
26 .map((l) => l.trim())
27 .find((l) => l.length > 0 && !l.startsWith("#"));
28 if (firstLine) {
29 const firstSentence = firstLine.split(/[.?!]/)[0].trim();
30 if (firstSentence.length > 0) return truncate(firstSentence);
31 }
32 }
33
34 // Fallback: first non-heading line anywhere in the document
35 const firstLine = content
36 .split("\n")
37 .map((l) => l.trim())
38 .find((l) => l.length > 0 && !l.startsWith("#"));
39 if (!firstLine) return null;
40 return truncate(firstLine);
41}
42
11type Theme = "dark" | "light"; 43type Theme = "dark" | "light";
12 44
13export function App() { 45export function App() {
@@ -125,11 +157,27 @@ export function App() {
125 if (selectedProject && selectedSession) { 157 if (selectedProject && selectedSession) {
126 const filename = 158 const filename =
127 selectedSession.phase === "research" ? "research.md" : "plan.md"; 159 selectedSession.phase === "research" ? "research.md" : "plan.md";
128 api.readSessionArtifact(selectedProject.id, selectedSession.id, filename).then((content) => { 160 // Capture for the async closure
129 const text = content || ""; 161 const sessionAtTrigger = selectedSession;
130 setDocumentContent(text); 162 api
131 setOriginalContent(text); 163 .readSessionArtifact(selectedProject.id, sessionAtTrigger.id, filename)
132 }); 164 .then((content) => {
165 const text = content || "";
166 setDocumentContent(text);
167 setOriginalContent(text);
168
169 // Auto-name: only during research, only while name is still the default
170 if (
171 sessionAtTrigger.phase === "research" &&
172 /^Session \d+$/.test(sessionAtTrigger.name) &&
173 text.length > 0
174 ) {
175 const derived = extractSessionName(text);
176 if (derived) {
177 handleRenameSession(sessionAtTrigger.id, derived);
178 }
179 }
180 });
133 } 181 }
134 } 182 }
135 183
@@ -285,6 +333,17 @@ export function App() {
285 } 333 }
286 }; 334 };
287 335
336 const handleRenameSession = async (id: string, name: string) => {
337 await api.renameSession(id, name);
338 setSessions((prev) =>
339 prev.map((s) => (s.id === id ? { ...s, name } : s))
340 );
341 // Use functional updater to avoid stale-closure issues
342 setSelectedSession((prev) =>
343 prev?.id === id ? { ...prev, name } : prev
344 );
345 };
346
288 return ( 347 return (
289 <div className="app"> 348 <div className="app">
290 <Header 349 <Header
@@ -298,6 +357,7 @@ export function App() {
298 onCreateSession={handleCreateSession} 357 onCreateSession={handleCreateSession}
299 onDeleteProject={handleDeleteProject} 358 onDeleteProject={handleDeleteProject}
300 onDeleteSession={handleDeleteSession} 359 onDeleteSession={handleDeleteSession}
360 onRenameSession={handleRenameSession}
301 theme={theme} 361 theme={theme}
302 onToggleTheme={handleToggleTheme} 362 onToggleTheme={handleToggleTheme}
303 /> 363 />