feat: draggable panel resize handles and remove extensions divider

Add ResizeHandle component for drag-to-resize side panels.
ChatListPanel (180–400px) and RightPanel (200–480px) widths
are persisted via localStorage. Also remove the border line
below the Extensions page tab selector.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-10 12:57:37 +08:00
1 parent 7b64948cb4
commit da07f1bd1b
5 files changed
+112 -7

No files matched your search

+1 -1
View File
@@ -31,7 +31,7 @@ function ExtensionsPageInner() {
return (
<div className="flex h-full flex-col">
<div className="border-b border-border/50 px-6 pt-4 pb-0">
<div className="px-6 pt-4 pb-0">
<h1 className="text-xl font-semibold mb-3">Extensions</h1>
<Tabs value={tab} onValueChange={(v) => setTab(v as ExtTab)}>
<TabsList>
+44 -2
View File
@@ -6,10 +6,16 @@ import { TooltipProvider } from "@/components/ui/tooltip";
import { NavRail } from "./NavRail";
import { ChatListPanel } from "./ChatListPanel";
import { RightPanel } from "./RightPanel";
import { ResizeHandle } from "./ResizeHandle";
import { UpdateDialog } from "./UpdateDialog";
import { PanelContext, type PanelContent } from "@/hooks/usePanel";
import { UpdateContext, type UpdateInfo } from "@/hooks/useUpdate";
const CHATLIST_MIN = 180;
const CHATLIST_MAX = 400;
const RIGHTPANEL_MIN = 200;
const RIGHTPANEL_MAX = 480;
const LG_BREAKPOINT = 1024;
const CHECK_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours
const DISMISSED_VERSION_KEY = "codepilot_dismissed_update_version";
@@ -19,6 +25,36 @@ export function AppShell({ children }: { children: React.ReactNode }) {
const [chatListOpen, setChatListOpenRaw] = useState(false);
// Panel width state with localStorage persistence
const [chatListWidth, setChatListWidth] = useState(() => {
if (typeof window === "undefined") return 240;
return parseInt(localStorage.getItem("codepilot_chatlist_width") || "240");
});
const [rightPanelWidth, setRightPanelWidth] = useState(() => {
if (typeof window === "undefined") return 288;
return parseInt(localStorage.getItem("codepilot_rightpanel_width") || "288");
});
const handleChatListResize = useCallback((delta: number) => {
setChatListWidth((w) => Math.min(CHATLIST_MAX, Math.max(CHATLIST_MIN, w + delta)));
}, []);
const handleChatListResizeEnd = useCallback(() => {
setChatListWidth((w) => {
localStorage.setItem("codepilot_chatlist_width", String(w));
return w;
});
}, []);
const handleRightPanelResize = useCallback((delta: number) => {
setRightPanelWidth((w) => Math.min(RIGHTPANEL_MAX, Math.max(RIGHTPANEL_MIN, w - delta)));
}, []);
const handleRightPanelResizeEnd = useCallback(() => {
setRightPanelWidth((w) => {
localStorage.setItem("codepilot_rightpanel_width", String(w));
return w;
});
}, []);
// Panel state
const isChatRoute = pathname.startsWith("/chat/") || pathname === "/chat";
const isChatDetailRoute = pathname.startsWith("/chat/");
@@ -165,7 +201,10 @@ export function AppShell({ children }: { children: React.ReactNode }) {
hasUpdate={updateInfo?.updateAvailable ?? false}
skipPermissionsActive={skipPermissionsActive}
/>
<ChatListPanel open={chatListOpen} />
<ChatListPanel open={chatListOpen} width={chatListWidth} />
{chatListOpen && (
<ResizeHandle side="left" onResize={handleChatListResize} onResizeEnd={handleChatListResizeEnd} />
)}
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
{/* Electron draggable title bar region */}
<div
@@ -174,7 +213,10 @@ export function AppShell({ children }: { children: React.ReactNode }) {
/>
<main className="relative flex-1 overflow-hidden">{children}</main>
</div>
{isChatDetailRoute && <RightPanel />}
{isChatDetailRoute && panelOpen && (
<ResizeHandle side="right" onResize={handleRightPanelResize} onResizeEnd={handleRightPanelResizeEnd} />
)}
{isChatDetailRoute && <RightPanel width={rightPanelWidth} />}
</div>
<UpdateDialog />
</TooltipProvider>
+3 -2
View File
@@ -21,6 +21,7 @@ import type { ChatSession } from "@/types";
interface ChatListPanelProps {
open: boolean;
width?: number;
}
function formatRelativeTime(dateStr: string): string {
@@ -69,7 +70,7 @@ const MODE_BADGE_CONFIG = {
ask: { label: "Ask", className: "bg-green-500/10 text-green-500" },
} as const;
export function ChatListPanel({ open }: ChatListPanelProps) {
export function ChatListPanel({ open, width }: ChatListPanelProps) {
const pathname = usePathname();
const router = useRouter();
const { streamingSessionId, pendingApprovalSessionId } = usePanel();
@@ -150,7 +151,7 @@ export function ChatListPanel({ open }: ChatListPanelProps) {
if (!open) return null;
return (
<aside className="hidden h-full w-60 shrink-0 flex-col overflow-hidden bg-sidebar lg:flex">
<aside className="hidden h-full shrink-0 flex-col overflow-hidden bg-sidebar lg:flex" style={{ width: width ?? 240 }}>
{/* Header - extra top padding for macOS traffic lights */}
<div className="flex h-12 shrink-0 items-center justify-between px-3 mt-5 pl-6">
<span className="text-[13px] font-semibold tracking-tight text-sidebar-foreground">
+58
View File
@@ -0,0 +1,58 @@
"use client";
import { useCallback, useRef } from "react";
import { cn } from "@/lib/utils";
interface ResizeHandleProps {
side: "left" | "right";
onResize: (delta: number) => void;
onResizeEnd?: () => void;
}
export function ResizeHandle({ side, onResize, onResizeEnd }: ResizeHandleProps) {
const isDragging = useRef(false);
const startX = useRef(0);
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
isDragging.current = true;
startX.current = e.clientX;
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
const handleMouseMove = (ev: MouseEvent) => {
if (!isDragging.current) return;
const delta = ev.clientX - startX.current;
startX.current = ev.clientX;
onResize(delta);
};
const handleMouseUp = () => {
isDragging.current = false;
document.body.style.cursor = "";
document.body.style.userSelect = "";
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
onResizeEnd?.();
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[onResize, onResizeEnd]
);
return (
<div
onMouseDown={handleMouseDown}
className={cn(
"group relative z-10 flex w-1 shrink-0 cursor-col-resize items-center justify-center",
side === "left" ? "-ml-0.5" : "-mr-0.5"
)}
>
<div className="h-full w-px bg-transparent transition-colors duration-150 group-hover:bg-border" />
</div>
);
}
+6 -2
View File
@@ -14,7 +14,11 @@ import { usePanel } from "@/hooks/usePanel";
import { FileTree } from "@/components/project/FileTree";
import { FilePreview } from "@/components/project/FilePreview";
export function RightPanel() {
interface RightPanelProps {
width?: number;
}
export function RightPanel({ width }: RightPanelProps) {
const { panelOpen, setPanelOpen, workingDirectory, sessionId, sessionTitle, setSessionTitle } = usePanel();
const [previewPath, setPreviewPath] = useState<string | null>(null);
const [isEditingName, setIsEditingName] = useState(false);
@@ -93,7 +97,7 @@ export function RightPanel() {
};
return (
<aside className="hidden h-full w-72 shrink-0 flex-col overflow-hidden bg-background lg:flex">
<aside className="hidden h-full shrink-0 flex-col overflow-hidden bg-background lg:flex" style={{ width: width ?? 288 }}>
{/* Header */}
<div className="flex h-10 shrink-0 items-center justify-between px-4">
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Chat Info</span>