Collapsible sidebar
This commit is contained in:
parent
5da7d46a49
commit
6c6289eb2e
@ -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) =>
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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)" }}
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user