diff --git a/apps/web/app/api/web-sessions/[id]/route.ts b/apps/web/app/api/web-sessions/[id]/route.ts index e9c6f4f0620..cdc94aa69ae 100644 --- a/apps/web/app/api/web-sessions/[id]/route.ts +++ b/apps/web/app/api/web-sessions/[id]/route.ts @@ -64,6 +64,30 @@ export async function GET( return Response.json({ id, messages }); } +/** PATCH /api/web-sessions/[id] — update session metadata (e.g. rename). */ +export async function PATCH( + request: Request, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params; + let body: { title?: string }; + try { + body = await request.json(); + } catch { + return Response.json({ error: "Invalid JSON" }, { status: 400 }); + } + const sessions = readIndex(); + const session = sessions.find((s) => s.id === id); + if (!session) { + return Response.json({ error: "Session not found" }, { status: 404 }); + } + if (typeof body.title === "string") { + session.title = body.title; + } + writeIndex(sessions); + return Response.json({ ok: true, session }); +} + /** DELETE /api/web-sessions/[id] — remove a web chat session and its messages. */ export async function DELETE( _request: Request, diff --git a/apps/web/app/components/workspace/chat-sessions-sidebar.tsx b/apps/web/app/components/workspace/chat-sessions-sidebar.tsx index e9b0d356c77..3c2ab15181d 100644 --- a/apps/web/app/components/workspace/chat-sessions-sidebar.tsx +++ b/apps/web/app/components/workspace/chat-sessions-sidebar.tsx @@ -49,6 +49,8 @@ type ChatSessionsSidebarProps = { width?: number; /** Called when the user deletes a session from the sidebar menu. */ onDeleteSession?: (sessionId: string) => void; + /** Called when the user renames a session from the sidebar menu. */ + onRenameSession?: (sessionId: string, newTitle: string) => void; /** When true, show a loader instead of empty state (e.g. initial sessions fetch). */ loading?: boolean; }; @@ -154,12 +156,15 @@ export function ChatSessionsSidebar({ onNewSession, onSelectSubagent, onDeleteSession, + onRenameSession, mobile, onClose, width: widthProp, loading = false, }: ChatSessionsSidebarProps) { const [hoveredId, setHoveredId] = useState(null); + const [renamingId, setRenamingId] = useState(null); + const [renameValue, setRenameValue] = useState(""); const handleSelect = useCallback( (id: string) => { @@ -184,6 +189,19 @@ export function ChatSessionsSidebar({ [onDeleteSession], ); + const handleStartRename = useCallback((sessionId: string, currentTitle: string) => { + setRenamingId(sessionId); + setRenameValue(currentTitle || ""); + }, []); + + const handleCommitRename = useCallback(() => { + if (renamingId && renameValue.trim()) { + onRenameSession?.(renamingId, renameValue.trim()); + } + setRenamingId(null); + setRenameValue(""); + }, [renamingId, renameValue, onRenameSession]); + // Index subagents by parent session ID const subagentsByParent = useMemo(() => { const map = new Map(); @@ -288,6 +306,23 @@ export function ChatSessionsSidebar({ : "transparent", }} > + {renamingId === session.id ? ( +
{ e.preventDefault(); handleCommitRename(); }} + > + setRenameValue(e.target.value)} + onBlur={handleCommitRename} + onKeyDown={(e) => { if (e.key === "Escape") { setRenamingId(null); setRenameValue(""); } }} + autoFocus + className="w-full text-xs font-medium px-1 py-0.5 rounded outline-none border" + style={{ color: "var(--color-text)", background: "var(--color-surface)", borderColor: "var(--color-border)" }} + /> +
+ ) : ( + )} {onDeleteSession && (
@@ -343,6 +379,12 @@ export function ChatSessionsSidebar({ + handleStartRename(session.id, session.title)} + > + + Rename + handleDeleteSession(session.id)} diff --git a/apps/web/app/workspace/page.tsx b/apps/web/app/workspace/page.tsx index f17d5cd8e99..83b2e3b077f 100644 --- a/apps/web/app/workspace/page.tsx +++ b/apps/web/app/workspace/page.tsx @@ -533,6 +533,18 @@ function WorkspacePageInner() { [activeSessionId, sessions, fetchSessions], ); + const handleRenameSession = useCallback( + async (sessionId: string, newTitle: string) => { + await fetch(`/api/web-sessions/${sessionId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ title: newTitle }), + }); + void fetchSessions(); + }, + [fetchSessions], + ); + // Poll for active (streaming) agent runs so the sidebar can show indicators. useEffect(() => { let cancelled = false; @@ -1448,6 +1460,7 @@ function WorkspacePageInner() { onSubagentClick={handleSubagentClickFromChat} onFilePathClick={handleFilePathClickFromChat} onDeleteSession={handleDeleteSession} + onRenameSession={handleRenameSession} compact={isMobile} /> )} @@ -1477,6 +1490,7 @@ function WorkspacePageInner() { }} onSelectSubagent={handleSelectSubagent} onDeleteSession={handleDeleteSession} + onRenameSession={handleRenameSession} mobile onClose={() => setChatSessionsOpen(false)} /> @@ -1521,6 +1535,7 @@ function WorkspacePageInner() { }} onSelectSubagent={handleSelectSubagent} onDeleteSession={handleDeleteSession} + onRenameSession={handleRenameSession} width={rightSidebarWidth} /> )}