From 6c6289eb2e4b4bceebe06615caf62d1c88d82ab0 Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 19 Feb 2026 23:15:39 -0800 Subject: [PATCH] Collapsible sidebar --- apps/web/app/components/chain-of-thought.tsx | 2 +- apps/web/app/components/chat-panel.tsx | 6 -- .../workspace/chat-sessions-sidebar.tsx | 19 +++++- .../workspace/workspace-sidebar.tsx | 17 +++++ apps/web/app/globals.css | 2 +- apps/web/app/workspace/page.tsx | 62 ++++++++++++++++++- 6 files changed, 98 insertions(+), 10 deletions(-) diff --git a/apps/web/app/components/chain-of-thought.tsx b/apps/web/app/components/chain-of-thought.tsx index 2a6b0919ec6..5f7fa9d32fe 100644 --- a/apps/web/app/components/chain-of-thought.tsx +++ b/apps/web/app/components/chain-of-thought.tsx @@ -566,7 +566,7 @@ function groupToolSteps(tools: ToolPart[]): VisualItem[] { /* ─── Main component ─── */ export function ChainOfThought({ parts, isStreaming }: { parts: ChainPart[]; isStreaming?: boolean }) { - const [isOpen, setIsOpen] = useState(true); + const [isOpen, setIsOpen] = useState(!!isStreaming); const isActive = parts.some( (p) => diff --git a/apps/web/app/components/chat-panel.tsx b/apps/web/app/components/chat-panel.tsx index 8f225d9b4f9..d246cf20781 100644 --- a/apps/web/app/components/chat-panel.tsx +++ b/apps/web/app/components/chat-panel.tsx @@ -1549,12 +1549,6 @@ export const ChatPanel = forwardRef( - setRawView((v) => !v)} - > - - {rawView ? "Rendered view" : "Raw view"} - onDeleteSession(currentSessionId)} diff --git a/apps/web/app/components/workspace/chat-sessions-sidebar.tsx b/apps/web/app/components/workspace/chat-sessions-sidebar.tsx index 3c2ab15181d..24d16a5dba0 100644 --- a/apps/web/app/components/workspace/chat-sessions-sidebar.tsx +++ b/apps/web/app/components/workspace/chat-sessions-sidebar.tsx @@ -51,6 +51,8 @@ type ChatSessionsSidebarProps = { onDeleteSession?: (sessionId: string) => void; /** Called when the user renames a session from the sidebar menu. */ onRenameSession?: (sessionId: string, newTitle: string) => void; + /** Called when the user clicks the collapse/hide sidebar button. */ + onCollapse?: () => void; /** When true, show a loader instead of empty state (e.g. initial sessions fetch). */ loading?: boolean; }; @@ -157,6 +159,7 @@ export function ChatSessionsSidebar({ onSelectSubagent, onDeleteSession, onRenameSession, + onCollapse, mobile, onClose, width: widthProp, @@ -459,7 +462,21 @@ export function ChatSessionsSidebar({ background: "color-mix(in srgb, var(--color-sidebar-bg) 80%, transparent)", }} > -
+
+ {onCollapse && ( + + )} void; + /** Called when the user clicks the collapse/hide sidebar button. */ + onCollapse?: () => void; }; function HomeIcon() { @@ -403,6 +405,7 @@ export function WorkspaceSidebar({ activeProfile, onProfileSwitch, width: widthProp, + onCollapse, }: WorkspaceSidebarProps) { const isBrowsing = browseDir != null; const [showCreateWorkspace, setShowCreateWorkspace] = useState(false); @@ -525,6 +528,20 @@ export function WorkspaceSidebar({ /> )} + {onCollapse && ( + + )}
{/* Create workspace dialog */} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 6093330f383..08d6b5ed1f0 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -113,7 +113,7 @@ /* Glassmorphism */ --color-glass: rgba(22, 22, 21, 0.72); --color-glass-border: rgba(255, 255, 255, 0.06); - --color-bg-glass: rgba(12, 12, 11, 0.8); + --color-bg-glass: rgba(22, 22, 21, 0.8); /* Object type chips */ --color-chip-object: rgba(59, 130, 246, 0.12); diff --git a/apps/web/app/workspace/page.tsx b/apps/web/app/workspace/page.tsx index 83b2e3b077f..303a070eddf 100644 --- a/apps/web/app/workspace/page.tsx +++ b/apps/web/app/workspace/page.tsx @@ -429,6 +429,10 @@ function WorkspacePageInner() { const [sidebarOpen, setSidebarOpen] = useState(false); const [chatSessionsOpen, setChatSessionsOpen] = useState(false); + // Sidebar collapse state (desktop only). + const [leftSidebarCollapsed, setLeftSidebarCollapsed] = useState(false); + const [rightSidebarCollapsed, setRightSidebarCollapsed] = useState(false); + // Resizable sidebar widths (desktop only; persisted in localStorage). // Use static defaults so server and client match on first render (avoid hydration mismatch). const [leftSidebarWidth, setLeftSidebarWidth] = useState(260); @@ -452,6 +456,22 @@ function WorkspacePageInner() { window.localStorage.setItem(STORAGE_RIGHT, String(rightSidebarWidth)); }, [rightSidebarWidth]); + // Keyboard shortcuts: Cmd+B = toggle left sidebar, Cmd+Shift+B = toggle right sidebar + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "b") { + e.preventDefault(); + if (e.shiftKey) { + setRightSidebarCollapsed((v) => !v); + } else { + setLeftSidebarCollapsed((v) => !v); + } + } + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, []); + // Derive file context for chat sidebar directly from activePath (stable across loading). // Exclude reserved virtual paths (~chats, ~cron, etc.) where file-scoped chat is irrelevant. const fileContext = useMemo(() => { @@ -1294,6 +1314,7 @@ function WorkspacePageInner() { ) ) : ( <> + {!leftSidebarCollapsed && (
setLeftSidebarCollapsed(true)} />
+ )} )} + {/* Expand left sidebar button (shown when collapsed) */} + {!isMobile && leftSidebarCollapsed && ( +
+ +
+ )} + {/* Main content */}
{/* Mobile top bar — always visible on mobile */} @@ -1497,6 +1538,7 @@ function WorkspacePageInner() { ) ) : ( <> + {!rightSidebarCollapsed && (
setRightSidebarCollapsed(true)} width={rightSidebarWidth} /> )}
+ )} + {rightSidebarCollapsed && ( +
+ +
+ )} )} @@ -1569,7 +1629,7 @@ function WorkspacePageInner() {
{/* Chat sidebar (file/folder-scoped) — hidden for reserved paths, hidden on mobile */} - {!isMobile && fileContext && showChatSidebar && ( + {!isMobile && fileContext && showChatSidebar && !rightSidebarCollapsed && ( <>