aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--renderer/src/App.tsx70
-rw-r--r--renderer/src/components/Header.tsx81
-rw-r--r--renderer/src/styles/globals.css28
-rw-r--r--src/main/index.ts4
-rw-r--r--src/main/ipc/handlers.ts6
-rw-r--r--src/main/preload.ts2
6 files changed, 169 insertions, 22 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 />
diff --git a/renderer/src/components/Header.tsx b/renderer/src/components/Header.tsx
index b4faa6e..a435519 100644
--- a/renderer/src/components/Header.tsx
+++ b/renderer/src/components/Header.tsx
@@ -1,4 +1,4 @@
1import React from "react"; 1import React, { useState } from "react";
2import type { Project, Session, Phase } from "../types"; 2import type { Project, Session, Phase } from "../types";
3 3
4type Theme = "dark" | "light"; 4type Theme = "dark" | "light";
@@ -14,6 +14,7 @@ interface HeaderProps {
14 onCreateSession: () => void; 14 onCreateSession: () => void;
15 onDeleteProject?: (id: string) => void; 15 onDeleteProject?: (id: string) => void;
16 onDeleteSession?: (id: string) => void; 16 onDeleteSession?: (id: string) => void;
17 onRenameSession?: (id: string, name: string) => void;
17 theme: Theme; 18 theme: Theme;
18 onToggleTheme: () => void; 19 onToggleTheme: () => void;
19} 20}
@@ -37,6 +38,7 @@ export function Header({
37 onCreateSession, 38 onCreateSession,
38 onDeleteProject, 39 onDeleteProject,
39 onDeleteSession, 40 onDeleteSession,
41 onRenameSession,
40 theme, 42 theme,
41 onToggleTheme, 43 onToggleTheme,
42}: HeaderProps) { 44}: HeaderProps) {
@@ -54,6 +56,32 @@ export function Header({
54 } 56 }
55 }; 57 };
56 58
59 const [isRenamingSession, setIsRenamingSession] = useState(false);
60 const [renameValue, setRenameValue] = useState("");
61 // Guard against double-commit (onKeyDown Enter → unmount → onBlur)
62 const renameCommitted = React.useRef(false);
63
64 const startRename = () => {
65 if (!selectedSession) return;
66 renameCommitted.current = false;
67 setRenameValue(selectedSession.name);
68 setIsRenamingSession(true);
69 };
70
71 const commitRename = () => {
72 if (renameCommitted.current) return;
73 renameCommitted.current = true;
74 if (selectedSession && onRenameSession && renameValue.trim()) {
75 onRenameSession(selectedSession.id, renameValue.trim());
76 }
77 setIsRenamingSession(false);
78 };
79
80 const cancelRename = () => {
81 renameCommitted.current = true; // prevent blur from committing after cancel
82 setIsRenamingSession(false);
83 };
84
57 return ( 85 return (
58 <header className="header"> 86 <header className="header">
59 <div className="header-left"> 87 <div className="header-left">
@@ -88,21 +116,44 @@ export function Header({
88 116
89 {selectedProject && ( 117 {selectedProject && (
90 <> 118 <>
91 <select 119 {isRenamingSession ? (
92 value={selectedSession?.id || ""} 120 <input
93 onChange={(e) => { 121 autoFocus
94 const session = sessions.find((s) => s.id === e.target.value); 122 value={renameValue}
95 onSelectSession(session || null); 123 onChange={(e) => setRenameValue(e.target.value)}
96 }} 124 onKeyDown={(e) => {
97 > 125 if (e.key === "Enter") commitRename();
98 <option value="">Select Session...</option> 126 if (e.key === "Escape") cancelRename();
99 {sessions.map((s) => ( 127 }}
100 <option key={s.id} value={s.id}> 128 onBlur={commitRename}
101 {s.name} 129 className="session-rename-input"
102 </option> 130 />
103 ))} 131 ) : (
104 </select> 132 <select
133 value={selectedSession?.id || ""}
134 onChange={(e) => {
135 const session = sessions.find((s) => s.id === e.target.value);
136 onSelectSession(session || null);
137 }}
138 >
139 <option value="">Select Session...</option>
140 {sessions.map((s) => (
141 <option key={s.id} value={s.id}>
142 {s.name}
143 </option>
144 ))}
145 </select>
146 )}
105 <button onClick={onCreateSession}>+ Session</button> 147 <button onClick={onCreateSession}>+ Session</button>
148 {selectedSession && onRenameSession && !isRenamingSession && (
149 <button
150 onClick={startRename}
151 className="btn-rename"
152 title="Rename session"
153 >
154 ✏️
155 </button>
156 )}
106 {selectedSession && onDeleteSession && ( 157 {selectedSession && onDeleteSession && (
107 <button 158 <button
108 onClick={handleDeleteSession} 159 onClick={handleDeleteSession}
diff --git a/renderer/src/styles/globals.css b/renderer/src/styles/globals.css
index 97b7bb8..f141538 100644
--- a/renderer/src/styles/globals.css
+++ b/renderer/src/styles/globals.css
@@ -111,6 +111,34 @@ body {
111 border-color: var(--danger); 111 border-color: var(--danger);
112} 112}
113 113
114.header button.btn-rename {
115 background: transparent;
116 border: 1px solid var(--border);
117 padding: 5px 8px;
118 font-size: 13px;
119}
120
121.header button.btn-rename:hover {
122 background: var(--bg-tertiary);
123}
124
125.session-rename-input {
126 padding: 5px 10px;
127 background: var(--bg-tertiary);
128 border: 1px solid var(--accent);
129 border-radius: 2px;
130 color: var(--text-primary);
131 font-size: 12px;
132 font-family: inherit;
133 min-width: 140px;
134 outline: none;
135 box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2);
136}
137
138html[data-theme="light"] .session-rename-input {
139 box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
140}
141
114/* Theme toggle */ 142/* Theme toggle */
115.theme-toggle { 143.theme-toggle {
116 font-size: 11px; 144 font-size: 11px;
diff --git a/src/main/index.ts b/src/main/index.ts
index f0b23f7..a7bed00 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -1,4 +1,4 @@
1import { app, BrowserWindow } from "electron"; 1import { app, BrowserWindow, Menu } from "electron";
2import path from "node:path"; 2import path from "node:path";
3import { getDb, closeDb } from "./db"; 3import { getDb, closeDb } from "./db";
4import { registerIpcHandlers } from "./ipc/handlers"; 4import { registerIpcHandlers } from "./ipc/handlers";
@@ -41,6 +41,8 @@ function createWindow() {
41} 41}
42 42
43app.whenReady().then(() => { 43app.whenReady().then(() => {
44 Menu.setApplicationMenu(null);
45
44 // Initialize database 46 // Initialize database
45 getDb(); 47 getDb();
46 48
diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts
index d9beaf0..145c6e2 100644
--- a/src/main/ipc/handlers.ts
+++ b/src/main/ipc/handlers.ts
@@ -31,7 +31,11 @@ export function registerIpcHandlers(mainWindow: BrowserWindow): void {
31 } 31 }
32 sessions.deleteSession(id); 32 sessions.deleteSession(id);
33 }); 33 });
34 34
35 ipcMain.handle("sessions:rename", (_, id: string, name: string) => {
36 sessions.updateSession(id, { name });
37 });
38
35 ipcMain.handle("sessions:get", (_, id: string) => sessions.getSession(id)); 39 ipcMain.handle("sessions:get", (_, id: string) => sessions.getSession(id));
36 40
37 // Messages 41 // Messages
diff --git a/src/main/preload.ts b/src/main/preload.ts
index 2c228dd..f377639 100644
--- a/src/main/preload.ts
+++ b/src/main/preload.ts
@@ -15,6 +15,7 @@ export interface ClaudeFlowAPI {
15 createSession: (projectId: string, name: string) => Promise<Session>; 15 createSession: (projectId: string, name: string) => Promise<Session>;
16 deleteSession: (id: string) => Promise<void>; 16 deleteSession: (id: string) => Promise<void>;
17 getSession: (id: string) => Promise<Session | undefined>; 17 getSession: (id: string) => Promise<Session | undefined>;
18 renameSession: (id: string, name: string) => Promise<void>;
18 19
19 // Messages 20 // Messages
20 listMessages: (sessionId: string) => Promise<Message[]>; 21 listMessages: (sessionId: string) => Promise<Message[]>;
@@ -70,6 +71,7 @@ const api: ClaudeFlowAPI = {
70 ipcRenderer.invoke("sessions:create", projectId, name), 71 ipcRenderer.invoke("sessions:create", projectId, name),
71 deleteSession: (id) => ipcRenderer.invoke("sessions:delete", id), 72 deleteSession: (id) => ipcRenderer.invoke("sessions:delete", id),
72 getSession: (id) => ipcRenderer.invoke("sessions:get", id), 73 getSession: (id) => ipcRenderer.invoke("sessions:get", id),
74 renameSession: (id, name) => ipcRenderer.invoke("sessions:rename", id, name),
73 75
74 // Messages 76 // Messages
75 listMessages: (sessionId) => ipcRenderer.invoke("messages:list", sessionId), 77 listMessages: (sessionId) => ipcRenderer.invoke("messages:list", sessionId),