diff options
| author | bndw <ben@bdw.to> | 2026-02-28 19:34:02 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-28 19:34:02 -0800 |
| commit | 283013c09d4855529e846951a1e090f0f16030a8 (patch) | |
| tree | e78c84c85b07806770faee99af7280d0a83eadc8 /renderer/src/App.tsx | |
| parent | 9a636af9090b122db2e55737fca3e78550aab9df (diff) | |
feat: auto session naming
Diffstat (limited to 'renderer/src/App.tsx')
| -rw-r--r-- | renderer/src/App.tsx | 70 |
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 | ||
| 9 | const api = window.api; | 9 | const 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 | */ | ||
| 17 | function 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 | |||
| 11 | type Theme = "dark" | "light"; | 43 | type Theme = "dark" | "light"; |
| 12 | 44 | ||
| 13 | export function App() { | 45 | export 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 | /> |
