Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion client/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "lucide-react";
import { useStore, AgentStatus } from "../stores/useStore";
import { Terminal } from "./Terminal";
import { useResizable } from "../hooks/useResizable";

const statusConfig: Record<AgentStatus, { label: string; color: string }> = {
running: { label: "Running", color: "#22C55E" },
Expand Down Expand Up @@ -56,8 +57,16 @@ export function Sidebar() {
nodes,
setNewSessionModalOpen,
setNewSessionForNodeId,
sidebarWidthPercent,
setSidebarWidthPercent,
} = useStore();

const { isDragging, handleMouseDown } = useResizable({
minPercent: 20,
maxPercent: 60,
onResize: setSidebarWidthPercent,
});

const session = selectedNodeId ? sessions.get(selectedNodeId) : null;
const node = selectedNodeId ? nodes.find(n => n.id === selectedNodeId) : null;

Expand Down Expand Up @@ -108,8 +117,16 @@ export function Sidebar() {
animate={{ x: 0, opacity: 1 }}
exit={{ x: "100%", opacity: 0 }}
transition={{ type: "spring", stiffness: 400, damping: 40 }}
className="fixed right-0 top-14 bottom-0 w-full max-w-lg z-50 flex flex-col bg-canvas-dark border-l border-border"
className="fixed right-0 top-14 bottom-0 z-50 flex flex-col bg-canvas-dark border-l border-border"
style={{ width: `${sidebarWidthPercent}vw` }}
>
{/* Drag handle */}
<div
onMouseDown={handleMouseDown}
className={`absolute left-0 top-0 bottom-0 w-1.5 cursor-col-resize transition-colors z-10 ${
isDragging ? "bg-violet-500" : "hover:bg-violet-500/40"
}`}
/>
{/* Header */}
<div className="flex-shrink-0 px-4 py-3 border-b border-border">
<div className="flex items-center gap-3">
Expand Down
66 changes: 66 additions & 0 deletions client/src/hooks/useResizable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useState, useCallback, useEffect } from "react";

interface UseResizableOptions {
minPercent?: number;
maxPercent?: number;
onResize?: (percent: number) => void;
}

interface UseResizableReturn {
isDragging: boolean;
handleMouseDown: (e: React.MouseEvent) => void;
}

export function useResizable({
minPercent = 20,
maxPercent = 60,
onResize,
}: UseResizableOptions = {}): UseResizableReturn {
const [isDragging, setIsDragging] = useState(false);

const handleMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
setIsDragging(true);
}, []);

useEffect(() => {
if (!isDragging) return;

const handleMouseMove = (e: MouseEvent) => {
// Calculate percentage: sidebar is on the right, so width = distance from mouse to right edge
const percent = ((window.innerWidth - e.clientX) / window.innerWidth) * 100;
const clampedPercent = Math.min(maxPercent, Math.max(minPercent, percent));
onResize?.(clampedPercent);
};

const handleMouseUp = () => {
setIsDragging(false);
};

document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);

return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [isDragging, minPercent, maxPercent, onResize]);

// Prevent text selection and set cursor globally during drag
useEffect(() => {
if (isDragging) {
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
} else {
document.body.style.cursor = "";
document.body.style.userSelect = "";
}

return () => {
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
}, [isDragging]);

return { isDragging, handleMouseDown };
}
17 changes: 17 additions & 0 deletions client/src/stores/useStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ export interface AgentSession {
currentTool?: string;
}

const getInitialSidebarWidth = (): number => {
if (typeof window === 'undefined') return 35;
const stored = localStorage.getItem('openui:sidebarWidthPercent');
if (stored) {
const parsed = parseFloat(stored);
if (!isNaN(parsed) && parsed >= 20 && parsed <= 60) return parsed;
}
return 35; // default ~512px equivalent on 1440px screens
};

interface AppState {
// Config
launchCwd: string;
Expand Down Expand Up @@ -62,6 +72,8 @@ interface AppState {
setSelectedNodeId: (id: string | null) => void;
sidebarOpen: boolean;
setSidebarOpen: (open: boolean) => void;
sidebarWidthPercent: number;
setSidebarWidthPercent: (percent: number) => void;
addAgentModalOpen: boolean;
setAddAgentModalOpen: (open: boolean) => void;
newSessionModalOpen: boolean;
Expand Down Expand Up @@ -123,6 +135,11 @@ export const useStore = create<AppState>((set) => ({
setSelectedNodeId: (id) => set({ selectedNodeId: id }),
sidebarOpen: false,
setSidebarOpen: (open) => set({ sidebarOpen: open }),
sidebarWidthPercent: getInitialSidebarWidth(),
setSidebarWidthPercent: (percent) => {
localStorage.setItem('openui:sidebarWidthPercent', String(percent));
set({ sidebarWidthPercent: percent });
},
addAgentModalOpen: false,
setAddAgentModalOpen: (open) => set({ addAgentModalOpen: open }),
newSessionModalOpen: false,
Expand Down