Resizable sidebar
This commit is contained in:
parent
68f09660e9
commit
027593b350
@ -38,6 +38,8 @@ type ChatSessionsSidebarProps = {
|
||||
mobile?: boolean;
|
||||
/** Close the mobile drawer. */
|
||||
onClose?: () => void;
|
||||
/** Fixed width in px when not mobile (overrides default 260). */
|
||||
width?: number;
|
||||
};
|
||||
|
||||
/** Format a timestamp into a human-readable relative time string. */
|
||||
@ -123,6 +125,7 @@ export function ChatSessionsSidebar({
|
||||
onSelectSubagent,
|
||||
mobile,
|
||||
onClose,
|
||||
width: widthProp,
|
||||
}: ChatSessionsSidebarProps) {
|
||||
const [hoveredId, setHoveredId] = useState<string | null>(null);
|
||||
|
||||
@ -160,11 +163,12 @@ export function ChatSessionsSidebar({
|
||||
// Group sessions: today, yesterday, this week, this month, older
|
||||
const grouped = groupSessions(sessions);
|
||||
|
||||
const width = mobile ? "280px" : (widthProp ?? 260);
|
||||
const sidebar = (
|
||||
<aside
|
||||
className={`flex flex-col h-full flex-shrink-0 ${mobile ? "drawer-right" : "border-l"}`}
|
||||
style={{
|
||||
width: mobile ? "280px" : 260,
|
||||
width: typeof width === "number" ? `${width}px` : width,
|
||||
borderColor: "var(--color-border)",
|
||||
background: "var(--color-surface)",
|
||||
}}
|
||||
|
||||
@ -10,12 +10,21 @@ export type ProfileInfo = {
|
||||
hasConfig: boolean;
|
||||
};
|
||||
|
||||
export type ProfileSwitcherTriggerProps = {
|
||||
isOpen: boolean;
|
||||
onClick: () => void;
|
||||
activeProfile: string;
|
||||
switching: boolean;
|
||||
};
|
||||
|
||||
type ProfileSwitcherProps = {
|
||||
onProfileSwitch?: () => void;
|
||||
onCreateWorkspace?: () => void;
|
||||
/** When set, this renders instead of the default button; dropdown still opens below. */
|
||||
trigger?: (props: ProfileSwitcherTriggerProps) => React.ReactNode;
|
||||
};
|
||||
|
||||
export function ProfileSwitcher({ onProfileSwitch, onCreateWorkspace }: ProfileSwitcherProps) {
|
||||
export function ProfileSwitcher({ onProfileSwitch, onCreateWorkspace, trigger }: ProfileSwitcherProps) {
|
||||
const [profiles, setProfiles] = useState<ProfileInfo[]>([]);
|
||||
const [activeProfile, setActiveProfile] = useState("default");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@ -78,35 +87,51 @@ export function ProfileSwitcher({ onProfileSwitch, onCreateWorkspace }: ProfileS
|
||||
|
||||
// Don't show the switcher if there's only one profile and no way to create more
|
||||
const showSwitcher = profiles.length > 0;
|
||||
if (!showSwitcher) {return null;}
|
||||
const handleToggle = () => {
|
||||
if (showSwitcher) { setIsOpen((o) => !o); }
|
||||
};
|
||||
|
||||
if (!trigger && !showSwitcher) { return null; }
|
||||
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
disabled={switching}
|
||||
className="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs transition-colors hover:bg-[var(--color-surface-hover)] disabled:opacity-50"
|
||||
style={{ color: "var(--color-text-secondary)" }}
|
||||
title="Switch workspace profile"
|
||||
>
|
||||
{/* Workspace icon */}
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
||||
</svg>
|
||||
<span className="truncate max-w-[120px]">
|
||||
{activeProfile === "default" ? "Default" : activeProfile}
|
||||
</span>
|
||||
<svg
|
||||
className={`w-3 h-3 transition-transform ${isOpen ? "rotate-180" : ""}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
<div
|
||||
className={`relative ${trigger ? "flex-1 min-w-0" : ""}`}
|
||||
ref={dropdownRef}
|
||||
>
|
||||
{trigger ? (
|
||||
trigger({
|
||||
isOpen,
|
||||
onClick: handleToggle,
|
||||
activeProfile,
|
||||
switching,
|
||||
})
|
||||
) : (
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
disabled={switching}
|
||||
className="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs transition-colors hover:bg-[var(--color-surface-hover)] disabled:opacity-50"
|
||||
style={{ color: "var(--color-text-secondary)" }}
|
||||
title="Switch workspace profile"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{/* Workspace icon */}
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
||||
</svg>
|
||||
<span className="truncate max-w-[120px]">
|
||||
{activeProfile === "default" ? "Default" : activeProfile}
|
||||
</span>
|
||||
<svg
|
||||
className={`w-3 h-3 transition-transform ${isOpen ? "rotate-180" : ""}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
{showSwitcher && isOpen && (
|
||||
<div
|
||||
className="absolute left-0 top-full mt-1 w-64 rounded-lg overflow-hidden z-50"
|
||||
style={{
|
||||
|
||||
@ -41,6 +41,8 @@ type WorkspaceSidebarProps = {
|
||||
onClose?: () => void;
|
||||
/** Active workspace profile name (null = default). */
|
||||
activeProfile?: string | null;
|
||||
/** Fixed width in px when not mobile (overrides default 260). */
|
||||
width?: number;
|
||||
/** Called after the user switches to a different profile. */
|
||||
onProfileSwitch?: () => void;
|
||||
};
|
||||
@ -401,15 +403,17 @@ export function WorkspaceSidebar({
|
||||
onClose,
|
||||
activeProfile,
|
||||
onProfileSwitch,
|
||||
width: widthProp,
|
||||
}: WorkspaceSidebarProps) {
|
||||
const isBrowsing = browseDir != null;
|
||||
const [showCreateWorkspace, setShowCreateWorkspace] = useState(false);
|
||||
const width = mobile ? "280px" : (widthProp ?? 260);
|
||||
|
||||
const sidebar = (
|
||||
<aside
|
||||
className={`flex flex-col h-screen flex-shrink-0 ${mobile ? "drawer-left" : "border-r"}`}
|
||||
style={{
|
||||
width: mobile ? "280px" : "260px",
|
||||
width: typeof width === "number" ? `${width}px` : width,
|
||||
background: "var(--color-surface)",
|
||||
borderColor: "var(--color-border)",
|
||||
}}
|
||||
@ -466,7 +470,7 @@ export function WorkspaceSidebar({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void onGoToChat?.()}
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 cursor-pointer transition-opacity"
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0 cursor-pointer transition-opacity"
|
||||
style={{
|
||||
background: "var(--color-accent-light)",
|
||||
color: "var(--color-accent)",
|
||||
@ -480,50 +484,59 @@ export function WorkspaceSidebar({
|
||||
<polyline points="9 22 9 12 15 12 15 22" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className="text-sm font-medium truncate"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
>
|
||||
{orgName || "Workspace"}
|
||||
</div>
|
||||
<div
|
||||
className="text-[11px] flex items-center gap-1"
|
||||
style={{
|
||||
color: "var(--color-text-muted)",
|
||||
}}
|
||||
>
|
||||
<span>Ironclaw</span>
|
||||
{activeProfile && activeProfile !== "default" && (
|
||||
<span
|
||||
className="px-1 py-0.5 rounded text-[10px]"
|
||||
style={{
|
||||
background: "var(--color-accent-light)",
|
||||
color: "var(--color-accent)",
|
||||
}}
|
||||
<ProfileSwitcher
|
||||
onProfileSwitch={onProfileSwitch}
|
||||
onCreateWorkspace={() => setShowCreateWorkspace(true)}
|
||||
trigger={({ isOpen, onClick, activeProfile: profileName, switching }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={switching}
|
||||
className="flex-1 min-w-0 w-full flex items-center justify-between gap-2 text-left rounded-lg py-1.5 px-2 transition-colors hover:bg-(--color-surface-hover) disabled:opacity-50"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
title="Switch workspace profile"
|
||||
>
|
||||
<div className="min-w-0 truncate">
|
||||
<div
|
||||
className="text-sm font-medium truncate"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
>
|
||||
{orgName || "Workspace"}
|
||||
</div>
|
||||
<div
|
||||
className="text-[11px] flex items-center gap-1 truncate"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
<span>Ironclaw</span>
|
||||
{profileName && profileName !== "default" && (
|
||||
<span
|
||||
className="px-1 py-0.5 rounded text-[10px] shrink-0"
|
||||
style={{
|
||||
background: "var(--color-accent-light)",
|
||||
color: "var(--color-accent)",
|
||||
}}
|
||||
>
|
||||
{profileName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 shrink-0 transition-transform ${isOpen ? "rotate-180" : ""}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{activeProfile}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Profile switcher — only in workspace mode */}
|
||||
{!isBrowsing && (
|
||||
<div
|
||||
className="px-3 py-1.5 border-b"
|
||||
style={{ borderColor: "var(--color-border)" }}
|
||||
>
|
||||
<ProfileSwitcher
|
||||
onProfileSwitch={onProfileSwitch}
|
||||
onCreateWorkspace={() => setShowCreateWorkspace(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create workspace dialog */}
|
||||
<CreateWorkspaceDialog
|
||||
isOpen={showCreateWorkspace}
|
||||
|
||||
@ -142,6 +142,72 @@ function rawFileUrl(path: string): string {
|
||||
return `/api/workspace/raw-file?path=${encodeURIComponent(path)}`;
|
||||
}
|
||||
|
||||
const LEFT_SIDEBAR_MIN = 200;
|
||||
const LEFT_SIDEBAR_MAX = 480;
|
||||
const RIGHT_SIDEBAR_MIN = 260;
|
||||
const RIGHT_SIDEBAR_MAX = 600;
|
||||
const STORAGE_LEFT = "ironclaw-workspace-left-sidebar-width";
|
||||
const STORAGE_RIGHT = "ironclaw-workspace-right-sidebar-width";
|
||||
|
||||
function clamp(n: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, n));
|
||||
}
|
||||
|
||||
/** Vertical resize handle; uses cursor position so the handle follows the mouse (no stuck-at-limit). */
|
||||
function ResizeHandle({
|
||||
mode,
|
||||
containerRef,
|
||||
min,
|
||||
max,
|
||||
onResize,
|
||||
}: {
|
||||
mode: "left" | "right";
|
||||
containerRef: React.RefObject<HTMLElement | null>;
|
||||
min: number;
|
||||
max: number;
|
||||
onResize: (width: number) => void;
|
||||
}) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const onMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
const move = (ev: MouseEvent) => {
|
||||
const el = containerRef.current;
|
||||
if (!el) {return;}
|
||||
const rect = el.getBoundingClientRect();
|
||||
const width =
|
||||
mode === "left"
|
||||
? ev.clientX - rect.left
|
||||
: rect.right - ev.clientX;
|
||||
onResize(clamp(width, min, max));
|
||||
};
|
||||
const up = () => {
|
||||
setIsDragging(false);
|
||||
document.removeEventListener("mousemove", move);
|
||||
document.removeEventListener("mouseup", up);
|
||||
document.body.style.removeProperty("user-select");
|
||||
document.body.style.removeProperty("cursor");
|
||||
};
|
||||
document.body.style.setProperty("user-select", "none");
|
||||
document.body.style.setProperty("cursor", "col-resize");
|
||||
document.addEventListener("mousemove", move);
|
||||
document.addEventListener("mouseup", up);
|
||||
},
|
||||
[containerRef, mode, min, max, onResize],
|
||||
);
|
||||
const showHover = isDragging || undefined;
|
||||
return (
|
||||
<div
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
onMouseDown={onMouseDown}
|
||||
className={`shrink-0 w-1 cursor-col-resize flex justify-center transition-colors ${showHover ? "bg-blue-600/30" : "hover:bg-blue-600/30"}`}
|
||||
style={{ minWidth: 4 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Find a node in the tree by exact path. */
|
||||
function findNode(
|
||||
tree: TreeNode[],
|
||||
@ -229,6 +295,8 @@ function WorkspacePageInner() {
|
||||
const chatRef = useRef<ChatPanelHandle>(null);
|
||||
// Compact (file-scoped) chat panel ref for sidebar drag-and-drop
|
||||
const compactChatRef = useRef<ChatPanelHandle>(null);
|
||||
// Root layout ref for resize handle position (handle follows cursor)
|
||||
const layoutRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Live-reactive tree via SSE watcher (with browse-mode support)
|
||||
const {
|
||||
@ -308,6 +376,26 @@ function WorkspacePageInner() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [chatSessionsOpen, setChatSessionsOpen] = useState(false);
|
||||
|
||||
// Resizable sidebar widths (desktop only; persisted in localStorage)
|
||||
const [leftSidebarWidth, setLeftSidebarWidth] = useState(() => {
|
||||
if (typeof window === "undefined") {return 260;}
|
||||
const v = window.localStorage.getItem(STORAGE_LEFT);
|
||||
const n = v ? parseInt(v, 10) : NaN;
|
||||
return Number.isFinite(n) ? clamp(n, LEFT_SIDEBAR_MIN, LEFT_SIDEBAR_MAX) : 260;
|
||||
});
|
||||
const [rightSidebarWidth, setRightSidebarWidth] = useState(() => {
|
||||
if (typeof window === "undefined") {return 320;}
|
||||
const v = window.localStorage.getItem(STORAGE_RIGHT);
|
||||
const n = v ? parseInt(v, 10) : NaN;
|
||||
return Number.isFinite(n) ? clamp(n, RIGHT_SIDEBAR_MIN, RIGHT_SIDEBAR_MAX) : 320;
|
||||
});
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(STORAGE_LEFT, String(leftSidebarWidth));
|
||||
}, [leftSidebarWidth]);
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(STORAGE_RIGHT, String(rightSidebarWidth));
|
||||
}, [rightSidebarWidth]);
|
||||
|
||||
// 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(() => {
|
||||
@ -919,8 +1007,13 @@ function WorkspacePageInner() {
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div className="flex h-screen" style={{ background: "var(--color-bg)" }} onClick={handleContainerClick}>
|
||||
{/* Sidebar — static on desktop, drawer overlay on mobile */}
|
||||
<div
|
||||
ref={layoutRef}
|
||||
className="flex h-screen"
|
||||
style={{ background: "var(--color-bg)" }}
|
||||
onClick={handleContainerClick}
|
||||
>
|
||||
{/* Left sidebar — static on desktop (resizable), drawer overlay on mobile */}
|
||||
{isMobile ? (
|
||||
sidebarOpen && (
|
||||
<WorkspaceSidebar
|
||||
@ -945,24 +1038,36 @@ function WorkspacePageInner() {
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<WorkspaceSidebar
|
||||
tree={enhancedTree}
|
||||
activePath={activePath}
|
||||
onSelect={handleNodeSelect}
|
||||
onRefresh={refreshTree}
|
||||
orgName={context?.organization?.name}
|
||||
loading={treeLoading}
|
||||
browseDir={browseDir}
|
||||
parentDir={effectiveParentDir}
|
||||
onNavigateUp={handleNavigateUp}
|
||||
onGoHome={handleGoHome}
|
||||
onFileSearchSelect={handleFileSearchSelect}
|
||||
workspaceRoot={workspaceRoot}
|
||||
onGoToChat={handleGoToChat}
|
||||
onExternalDrop={handleSidebarExternalDrop}
|
||||
activeProfile={activeProfile}
|
||||
onProfileSwitch={handleProfileSwitch}
|
||||
/>
|
||||
<>
|
||||
<div className="flex shrink-0 flex-col" style={{ width: leftSidebarWidth }}>
|
||||
<WorkspaceSidebar
|
||||
tree={enhancedTree}
|
||||
activePath={activePath}
|
||||
onSelect={handleNodeSelect}
|
||||
onRefresh={refreshTree}
|
||||
orgName={context?.organization?.name}
|
||||
loading={treeLoading}
|
||||
browseDir={browseDir}
|
||||
parentDir={effectiveParentDir}
|
||||
onNavigateUp={handleNavigateUp}
|
||||
onGoHome={handleGoHome}
|
||||
onFileSearchSelect={handleFileSearchSelect}
|
||||
workspaceRoot={workspaceRoot}
|
||||
onGoToChat={handleGoToChat}
|
||||
onExternalDrop={handleSidebarExternalDrop}
|
||||
activeProfile={activeProfile}
|
||||
onProfileSwitch={handleProfileSwitch}
|
||||
width={leftSidebarWidth}
|
||||
/>
|
||||
</div>
|
||||
<ResizeHandle
|
||||
mode="left"
|
||||
containerRef={layoutRef}
|
||||
min={LEFT_SIDEBAR_MIN}
|
||||
max={LEFT_SIDEBAR_MAX}
|
||||
onResize={setLeftSidebarWidth}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
@ -1127,26 +1232,38 @@ function WorkspacePageInner() {
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<ChatSessionsSidebar
|
||||
sessions={sessions}
|
||||
activeSessionId={activeSessionId}
|
||||
activeSessionTitle={activeSessionTitle}
|
||||
streamingSessionIds={streamingSessionIds}
|
||||
subagents={subagents}
|
||||
activeSubagentKey={activeSubagentKey}
|
||||
onSelectSession={(sessionId) => {
|
||||
setActiveSessionId(sessionId);
|
||||
setActiveSubagentKey(null);
|
||||
void chatRef.current?.loadSession(sessionId);
|
||||
}}
|
||||
onNewSession={() => {
|
||||
setActiveSessionId(null);
|
||||
setActiveSubagentKey(null);
|
||||
void chatRef.current?.newSession();
|
||||
router.replace("/workspace", { scroll: false });
|
||||
}}
|
||||
onSelectSubagent={handleSelectSubagent}
|
||||
/>
|
||||
<>
|
||||
<ResizeHandle
|
||||
mode="right"
|
||||
containerRef={layoutRef}
|
||||
min={RIGHT_SIDEBAR_MIN}
|
||||
max={RIGHT_SIDEBAR_MAX}
|
||||
onResize={setRightSidebarWidth}
|
||||
/>
|
||||
<div className="flex shrink-0 flex-col" style={{ width: rightSidebarWidth }}>
|
||||
<ChatSessionsSidebar
|
||||
sessions={sessions}
|
||||
activeSessionId={activeSessionId}
|
||||
activeSessionTitle={activeSessionTitle}
|
||||
streamingSessionIds={streamingSessionIds}
|
||||
subagents={subagents}
|
||||
activeSubagentKey={activeSubagentKey}
|
||||
onSelectSession={(sessionId) => {
|
||||
setActiveSessionId(sessionId);
|
||||
setActiveSubagentKey(null);
|
||||
void chatRef.current?.loadSession(sessionId);
|
||||
}}
|
||||
onNewSession={() => {
|
||||
setActiveSessionId(null);
|
||||
setActiveSubagentKey(null);
|
||||
void chatRef.current?.newSession();
|
||||
router.replace("/workspace", { scroll: false });
|
||||
}}
|
||||
onSelectSubagent={handleSelectSubagent}
|
||||
width={rightSidebarWidth}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
@ -1176,21 +1293,30 @@ function WorkspacePageInner() {
|
||||
|
||||
{/* Chat sidebar (file/folder-scoped) — hidden for reserved paths, hidden on mobile */}
|
||||
{!isMobile && fileContext && showChatSidebar && (
|
||||
<aside
|
||||
className="flex-shrink-0 border-l"
|
||||
style={{
|
||||
width: 380,
|
||||
borderColor: "var(--color-border)",
|
||||
background: "var(--color-bg)",
|
||||
}}
|
||||
>
|
||||
<ChatPanel
|
||||
ref={compactChatRef}
|
||||
compact
|
||||
fileContext={fileContext}
|
||||
onFileChanged={handleFileChanged}
|
||||
<>
|
||||
<ResizeHandle
|
||||
mode="right"
|
||||
containerRef={layoutRef}
|
||||
min={RIGHT_SIDEBAR_MIN}
|
||||
max={RIGHT_SIDEBAR_MAX}
|
||||
onResize={setRightSidebarWidth}
|
||||
/>
|
||||
</aside>
|
||||
<aside
|
||||
className="flex-shrink-0 border-l flex flex-col"
|
||||
style={{
|
||||
width: rightSidebarWidth,
|
||||
borderColor: "var(--color-border)",
|
||||
background: "var(--color-bg)",
|
||||
}}
|
||||
>
|
||||
<ChatPanel
|
||||
ref={compactChatRef}
|
||||
compact
|
||||
fileContext={fileContext}
|
||||
onFileChanged={handleFileChanged}
|
||||
/>
|
||||
</aside>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user