From b6405dd6a4ba65fc5dc6746db7be7be7d0bb29f3 Mon Sep 17 00:00:00 2001 From: bndw Date: Wed, 4 Mar 2026 21:21:22 -0800 Subject: feat: replace header dropdowns with collapsible sidebar tree - Add Sidebar.tsx: project/session tree with inline rename, collapse/resize - App.tsx: load all sessions at startup, sync selectedProject on session click - Header.tsx: strip project/session UI, keep only right-side controls - globals.css: add .main-layout, sidebar, item, and activity-dot styles - Chat pane: move toggle button to left, use triangle icons matching sidebar --- renderer/src/components/Sidebar.tsx | 231 ++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 renderer/src/components/Sidebar.tsx (limited to 'renderer/src/components/Sidebar.tsx') diff --git a/renderer/src/components/Sidebar.tsx b/renderer/src/components/Sidebar.tsx new file mode 100644 index 0000000..567e731 --- /dev/null +++ b/renderer/src/components/Sidebar.tsx @@ -0,0 +1,231 @@ +import React, { useState, useRef } from "react"; +import type { Project, Session } from "../types"; +import { formatRelativeTime } from "../utils/timeFormat"; + +interface SidebarProps { + projects: Project[]; + sessions: Session[]; + selectedProject: Project | null; + selectedSession: Session | null; + onSelectProject: (project: Project) => void; + onSelectSession: (session: Session | null) => void; + onCreateProject: () => void; + onCreateSession: (projectId: string) => void; + onDeleteProject: (id: string) => void; + onDeleteSession: (id: string) => void; + onRenameSession: (id: string, name: string) => void; + loadingBySession: Record; + width: number; + collapsed: boolean; + onCollapsedChange: (collapsed: boolean) => void; +} + +export function Sidebar({ + projects, + sessions, + selectedProject, + selectedSession, + onSelectProject, + onSelectSession, + onCreateProject, + onCreateSession, + onDeleteProject, + onDeleteSession, + onRenameSession, + loadingBySession, + width, + collapsed, + onCollapsedChange, +}: SidebarProps) { + const [renamingSessionId, setRenamingSessionId] = useState(null); + const [renameValue, setRenameValue] = useState(""); + // Guard against double-commit (onKeyDown Enter → unmount → onBlur) + const renameCommitted = useRef(false); + + const startRename = (session: Session) => { + renameCommitted.current = false; + setRenameValue(session.name); + setRenamingSessionId(session.id); + }; + + const commitRename = (sessionId: string) => { + if (renameCommitted.current) return; + renameCommitted.current = true; + const trimmed = renameValue.trim(); + if (trimmed) onRenameSession(sessionId, trimmed); + setRenamingSessionId(null); + }; + + const cancelRename = () => { + renameCommitted.current = true; + setRenamingSessionId(null); + }; + + const handleDeleteProject = (project: Project) => { + if (confirm(`Delete project "${project.name}"? This cannot be undone.`)) { + onDeleteProject(project.id); + } + }; + + const handleDeleteSession = (session: Session) => { + if (confirm(`Delete session "${session.name}"? This cannot be undone.`)) { + onDeleteSession(session.id); + } + }; + + if (collapsed) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* ── Header ── */} +
+ Projects +
+ + +
+
+ + {/* ── Tree ── */} +
+ {projects.length === 0 && ( +
No projects yet
+ )} + + {projects.map((project) => { + const projectSessions = sessions.filter( + (s) => s.project_id === project.id + ); + const isSelectedProject = selectedProject?.id === project.id; + + return ( + + {/* Project row */} +
+ { + onSelectProject(project); + onSelectSession(null); + }} + title={project.path} + > + {project.name} + +
+ + +
+
+ + {/* Empty sessions hint (only under selected project) */} + {projectSessions.length === 0 && isSelectedProject && ( +
+ No sessions yet +
+ )} + + {/* Session rows */} + {projectSessions.map((session) => { + const isSelected = selectedSession?.id === session.id; + const isLoading = loadingBySession[session.id] ?? false; + const isRenaming = renamingSessionId === session.id; + + return ( +
+ {isRenaming ? ( + setRenameValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") commitRename(session.id); + if (e.key === "Escape") cancelRename(); + }} + onBlur={() => commitRename(session.id)} + /> + ) : ( + <> + onSelectSession(session)} + title={`${session.name} · ${session.phase} · ${formatRelativeTime(session.updated_at)}`} + > + {session.name} + {isLoading && ( + + )} + +
+ + +
+ + )} +
+ ); + })} +
+ ); + })} +
+
+ ); +} -- cgit v1.2.3