Collapsible sidebar

This commit is contained in:
Mark 2026-02-19 23:15:39 -08:00
parent 5da7d46a49
commit 6c6289eb2e
6 changed files with 98 additions and 10 deletions

View File

@ -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) =>

View File

@ -1549,12 +1549,6 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
</svg>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom">
<DropdownMenuItem
onSelect={() => setRawView((v) => !v)}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="16 18 22 12 16 6" /><polyline points="8 6 2 12 8 18" /></svg>
{rawView ? "Rendered view" : "Raw view"}
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onSelect={() => onDeleteSession(currentSessionId)}

View File

@ -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)",
}}
>
<div className="min-w-0 flex-1">
<div className="min-w-0 flex-1 flex items-center gap-1.5">
{onCollapse && (
<button
type="button"
onClick={onCollapse}
className="p-1 rounded-md shrink-0 transition-colors hover:bg-black/5"
style={{ color: "var(--color-text-muted)" }}
title="Hide chat sidebar (⌘⇧B)"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M15 3v18" />
</svg>
</button>
)}
<span
className="text-xs font-medium truncate block"
style={{ color: "var(--color-text)" }}

View File

@ -46,6 +46,8 @@ type WorkspaceSidebarProps = {
width?: number;
/** Called after the user switches to a different profile. */
onProfileSwitch?: () => 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 && (
<button
type="button"
onClick={onCollapse}
className="p-1 rounded-md shrink-0 transition-colors hover:bg-stone-200 dark:hover:bg-stone-700"
style={{ color: "var(--color-text-muted)" }}
title="Hide sidebar (⌘B)"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M9 3v18" />
</svg>
</button>
)}
</div>
{/* Create workspace dialog */}

View File

@ -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);

View File

@ -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 && (
<div
className="flex shrink-0 flex-col relative"
style={{ width: leftSidebarWidth, minWidth: leftSidebarWidth }}
@ -1323,11 +1344,31 @@ function WorkspacePageInner() {
activeProfile={activeProfile}
onProfileSwitch={handleProfileSwitch}
width={leftSidebarWidth}
onCollapse={() => setLeftSidebarCollapsed(true)}
/>
</div>
)}
</>
)}
{/* Expand left sidebar button (shown when collapsed) */}
{!isMobile && leftSidebarCollapsed && (
<div className="shrink-0 flex flex-col items-center pt-2.5 px-1.5">
<button
type="button"
onClick={() => setLeftSidebarCollapsed(false)}
className="p-1.5 rounded-md transition-colors hover:bg-black/5"
style={{ color: "var(--color-text-muted)" }}
title="Show sidebar (⌘B)"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M9 3v18" />
</svg>
</button>
</div>
)}
{/* Main content */}
<main className="flex-1 flex flex-col min-w-0 overflow-hidden" style={{ background: "var(--color-main-bg)" }}>
{/* Mobile top bar — always visible on mobile */}
@ -1497,6 +1538,7 @@ function WorkspacePageInner() {
)
) : (
<>
{!rightSidebarCollapsed && (
<div
className="flex shrink-0 flex-col relative"
style={{ width: rightSidebarWidth, minWidth: rightSidebarWidth, background: "var(--color-sidebar-bg)" }}
@ -1536,10 +1578,28 @@ function WorkspacePageInner() {
onSelectSubagent={handleSelectSubagent}
onDeleteSession={handleDeleteSession}
onRenameSession={handleRenameSession}
onCollapse={() => setRightSidebarCollapsed(true)}
width={rightSidebarWidth}
/>
)}
</div>
)}
{rightSidebarCollapsed && (
<div className="shrink-0 flex flex-col items-center pt-2.5 px-1.5">
<button
type="button"
onClick={() => setRightSidebarCollapsed(false)}
className="p-1.5 rounded-md transition-colors hover:bg-black/5"
style={{ color: "var(--color-text-muted)" }}
title="Show chat sidebar (⌘⇧B)"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M15 3v18" />
</svg>
</button>
</div>
)}
</>
)}
</>
@ -1569,7 +1629,7 @@ function WorkspacePageInner() {
</div>
{/* Chat sidebar (file/folder-scoped) — hidden for reserved paths, hidden on mobile */}
{!isMobile && fileContext && showChatSidebar && (
{!isMobile && fileContext && showChatSidebar && !rightSidebarCollapsed && (
<>
<aside
className="flex-shrink-0 border-l flex flex-col relative"