feat(frontend): gate new sidebar changes behind NEW_SIDEBAR feature flag

Add LaunchDarkly feature flag to allow gradual rollout of the redesigned
sidebar. When disabled, the original sidebar behavior is preserved.
This commit is contained in:
abhi1992002
2026-04-15 16:51:24 +05:30
parent c370285e75
commit 1471d9f6cb
5 changed files with 234 additions and 95 deletions

View File

@@ -10,6 +10,8 @@ import dynamic from "next/dynamic";
import { useCallback, useEffect, useRef, useState } from "react";
import { ChatContainer } from "./components/ChatContainer/ChatContainer";
import { DeleteChatDialog } from "./components/DeleteChatDialog/DeleteChatDialog";
import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer";
import { MobileHeader } from "./components/MobileHeader/MobileHeader";
import { NotificationBanner } from "./components/NotificationBanner/NotificationBanner";
import { NotificationDialog } from "./components/NotificationDialog/NotificationDialog";
import { RateLimitResetDialog } from "./components/RateLimitResetDialog/RateLimitResetDialog";
@@ -88,8 +90,16 @@ export function CopilotPage() {
hasMoreMessages,
isLoadingMore,
loadMore,
// Mobile
// Mobile drawer
isMobile,
isDrawerOpen,
sessions,
isLoadingSessions,
handleOpenDrawer,
handleCloseDrawer,
handleDrawerOpenChange,
handleSelectSession,
handleNewChat,
// Delete functionality
sessionToDelete,
isDeleting,
@@ -119,6 +129,7 @@ export function CopilotPage() {
const isBillingEnabled = useGetFlag(Flag.ENABLE_PLATFORM_PAYMENT);
const isArtifactsEnabled = useGetFlag(Flag.ARTIFACTS);
const isNewSidebar = useGetFlag(Flag.NEW_SIDEBAR);
const { credits, fetchCredits } = useCredits({ fetchInitialCredits: true });
const hasInsufficientCredits =
credits !== null && resetCost != null && credits < resetCost;
@@ -155,6 +166,9 @@ export function CopilotPage() {
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{isMobile && !isNewSidebar && (
<MobileHeader onOpenDrawer={handleOpenDrawer} />
)}
<NotificationBanner />
{isDryRun && (
<div className="flex items-center justify-center gap-1.5 bg-amber-50 px-3 py-1.5 text-xs font-medium text-amber-800">
@@ -206,6 +220,18 @@ export function CopilotPage() {
onCancel={handleCancelDelete}
/>
)}
{isMobile && !isNewSidebar && (
<MobileDrawer
isOpen={isDrawerOpen}
sessions={sessions}
currentSessionId={sessionId}
isLoading={isLoadingSessions}
onSelectSession={handleSelectSession}
onNewChat={handleNewChat}
onClose={handleCloseDrawer}
onOpenChange={handleDrawerOpenChange}
/>
)}
<NotificationDialog />
<RateLimitResetDialog
isOpen={!!rateLimitMessage && hasUsage && (resetCost ?? 0) > 0}

View File

@@ -28,8 +28,10 @@ import { useCopilotUIStore } from "@/app/(platform)/copilot/store";
import { Button } from "@/components/atoms/Button/Button";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { DeleteChatDialog } from "@/app/(platform)/copilot/components/DeleteChatDialog/DeleteChatDialog";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
export function ChatSessionList() {
const isNewSidebar = useGetFlag(Flag.NEW_SIDEBAR);
const isMobile = useIsMobile();
const pathname = usePathname();
const router = useRouter();
@@ -223,6 +225,11 @@ export function ChatSessionList() {
return (
<>
{!isNewSidebar && (
<div className="flex flex-col px-3 pb-4">
<span className="text-sm font-medium text-zinc-600">All tasks</span>
</div>
)}
<div className="flex flex-col gap-5">
{isLoadingSessions ? (
<div className="flex min-h-[30rem] items-center justify-center py-4">

View File

@@ -43,6 +43,7 @@ import { UsageLimits } from "@/app/(platform)/copilot/components/UsageLimits/Usa
import { NotificationToggle } from "@/app/(platform)/copilot/components/ChatSidebar/components/NotificationToggle/NotificationToggle";
import { AgentActivityDropdown } from "@/components/layout/Navbar/components/AgentActivityDropdown/AgentActivityDropdown";
import { useTallyPopup } from "@/components/molecules/TallyPoup/useTallyPopup";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
interface Props {
dynamicContent?: ReactNode;
@@ -54,6 +55,7 @@ export function AppSidebar({ dynamicContent }: Props) {
const pathname = usePathname();
const { state: tallyState } = useTallyPopup();
const { isLoggedIn } = useSupabase();
const isNewSidebar = useGetFlag(Flag.NEW_SIDEBAR);
const [loadingHref, setLoadingHref] = useState<string | null>(null);
const [isTasksOpen, setIsTasksOpen] = useState(true);
@@ -64,36 +66,64 @@ export function AppSidebar({ dynamicContent }: Props) {
const homeHref = "/copilot";
const navLinks = [
{
name: "Search",
href: "/marketplace/search",
icon: MagnifyingGlass,
testId: "sidebar-link-search",
showWhenCollapsed: true,
},
{
name: "Workflows",
href: "/library",
icon: Books,
testId: "sidebar-link-workflows",
showWhenCollapsed: true,
},
{
name: "Explore",
href: "/marketplace",
icon: ShoppingBag,
testId: "sidebar-link-marketplace",
showWhenCollapsed: false,
},
{
name: "Builder",
href: "/build",
icon: PenNibStraight,
testId: "sidebar-link-build",
showWhenCollapsed: false,
},
];
const navLinks = isNewSidebar
? [
{
name: "Search",
href: "/marketplace/search",
icon: MagnifyingGlass,
testId: "sidebar-link-search",
showWhenCollapsed: true,
},
{
name: "Workflows",
href: "/library",
icon: Books,
testId: "sidebar-link-workflows",
showWhenCollapsed: true,
},
{
name: "Explore",
href: "/marketplace",
icon: ShoppingBag,
testId: "sidebar-link-marketplace",
showWhenCollapsed: false,
},
{
name: "Builder",
href: "/build",
icon: PenNibStraight,
testId: "sidebar-link-build",
showWhenCollapsed: false,
},
]
: [
{
name: "Workflows",
href: "/library",
icon: Books,
testId: "sidebar-link-workflows",
showWhenCollapsed: true,
},
{
name: "Explore",
href: "/marketplace",
icon: ShoppingBag,
testId: "sidebar-link-marketplace",
showWhenCollapsed: true,
},
{
name: "Builder",
href: "/build",
icon: PenNibStraight,
testId: "sidebar-link-build",
showWhenCollapsed: true,
},
];
const filteredNavLinks = isNewSidebar
? navLinks.filter((link) => !isCollapsed || link.showWhenCollapsed)
: navLinks;
function isActive(href: string) {
if (href === homeHref) {
@@ -120,12 +150,7 @@ export function AppSidebar({ dynamicContent }: Props) {
)}
>
{!isCollapsed && (
<motion.div
className="flex w-full items-center justify-between"
initial={{ opacity: 0, y: -6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, ease: [0.25, 0.46, 0.45, 0.94] }}
>
<div className="flex w-full items-center justify-between">
<Link href={homeHref}>
<IconAutoGPTLogo className="h-7 w-auto" />
</Link>
@@ -140,9 +165,9 @@ export function AppSidebar({ dynamicContent }: Props) {
</TooltipContent>
</Tooltip>
</div>
</motion.div>
</div>
)}
{isCollapsed && (
{isCollapsed && isNewSidebar && (
<div className="relative flex flex-col items-center">
<Link
href={homeHref}
@@ -153,86 +178,105 @@ export function AppSidebar({ dynamicContent }: Props) {
<SidebarTrigger className="absolute inset-0 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100 hover:bg-sidebar-accent [&>svg]:!size-5" />
</div>
)}
{isCollapsed && !isNewSidebar && (
<div className="flex flex-col items-center">
<Link href={homeHref}>
<IconAutoGPTLogoMinimal className="h-6 w-6" />
</Link>
</div>
)}
</SidebarHeader>
{/* Navigation links */}
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<div>
<SidebarMenu className={cn(isCollapsed && "gap-3")}>
<SidebarMenu className={cn(isCollapsed && "gap-3")}>
{isCollapsed && !isNewSidebar && (
<SidebarMenuItem>
<SidebarMenuButton
asChild
isActive={isActive(homeHref)}
tooltip="New Task"
tooltip="Open sidebar"
className="py-5"
>
<SidebarTrigger className="hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&>svg]:!size-5" />
</SidebarMenuButton>
</SidebarMenuItem>
)}
{isCollapsed && !isNewSidebar && (
<SidebarMenuItem>
<AgentActivityDropdown />
</SidebarMenuItem>
)}
<SidebarMenuItem>
<SidebarMenuButton
asChild
isActive={isActive(homeHref)}
tooltip="New Task"
className={cn(
"!rounded-xl py-5 data-[active=true]:!bg-zinc-200 data-[active=true]:!font-normal",
!isCollapsed && "gap-3",
)}
>
<Link
href="/copilot"
data-testid="sidebar-link-new-task"
onClick={() =>
!isActive(homeHref) && setLoadingHref(homeHref)
}
>
{loadingHref === homeHref && isCollapsed ? (
<CircleNotch className="!size-5 animate-spin text-zinc-600" />
) : (
<NotePencil className="!size-5" weight="regular" />
)}
{!isCollapsed && <span className="flex-1">New Task</span>}
{loadingHref === homeHref && !isCollapsed && (
<CircleNotch className="!size-4 animate-spin text-zinc-600" />
)}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
{filteredNavLinks.map((link) => (
<SidebarMenuItem key={link.name}>
<SidebarMenuButton
asChild
isActive={isActive(link.href)}
tooltip={link.name}
className={cn(
"!rounded-xl py-5 data-[active=true]:!bg-zinc-200 data-[active=true]:!font-normal",
!isCollapsed && "gap-3",
)}
>
<Link
href="/copilot"
data-testid="sidebar-link-new-task"
href={link.href}
data-testid={link.testId}
onClick={() =>
!isActive(homeHref) && setLoadingHref(homeHref)
!isActive(link.href) && setLoadingHref(link.href)
}
>
{loadingHref === homeHref && isCollapsed ? (
{loadingHref === link.href && isCollapsed ? (
<CircleNotch className="!size-5 animate-spin text-zinc-600" />
) : (
<NotePencil className="!size-5" weight="regular" />
<link.icon className="!size-5" weight="regular" />
)}
{!isCollapsed && <span className="flex-1">New Task</span>}
{loadingHref === homeHref && !isCollapsed && (
{!isCollapsed && (
<span className="flex-1">{link.name}</span>
)}
{loadingHref === link.href && !isCollapsed && (
<CircleNotch className="!size-4 animate-spin text-zinc-600" />
)}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
{navLinks
.filter((link) => !isCollapsed || link.showWhenCollapsed)
.map((link) => (
<SidebarMenuItem key={link.name}>
<SidebarMenuButton
asChild
isActive={isActive(link.href)}
tooltip={link.name}
className={cn(
"!rounded-xl py-5 data-[active=true]:!bg-zinc-200 data-[active=true]:!font-normal",
!isCollapsed && "gap-3",
)}
>
<Link
href={link.href}
data-testid={link.testId}
onClick={() =>
!isActive(link.href) && setLoadingHref(link.href)
}
>
{loadingHref === link.href && isCollapsed ? (
<CircleNotch className="!size-5 animate-spin text-zinc-600" />
) : (
<link.icon className="!size-5" weight="regular" />
)}
{!isCollapsed && (
<span className="flex-1">{link.name}</span>
)}
{loadingHref === link.href && !isCollapsed && (
<CircleNotch className="!size-4 animate-spin text-zinc-600" />
)}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</div>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
{dynamicContent && (
<SidebarGroup className="flex-1 overflow-hidden">
{!isCollapsed && (
{!isCollapsed && isNewSidebar && (
<>
<button
onClick={() => setIsTasksOpen((prev) => !prev)}
@@ -275,6 +319,11 @@ export function AppSidebar({ dynamicContent }: Props) {
</AnimatePresence>
</>
)}
{!isCollapsed && !isNewSidebar && (
<SidebarGroupContent className="h-full overflow-y-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{dynamicContent}
</SidebarGroupContent>
)}
</SidebarGroup>
)}
</SidebarContent>
@@ -297,17 +346,19 @@ export function AppSidebar({ dynamicContent }: Props) {
Usage
</TooltipContent>
</Tooltip>
{!isCollapsed && (
{(!isNewSidebar || !isCollapsed) && (
<Tooltip>
<TooltipTrigger asChild>
<div className="[&_button]:!flex [&_button]:!size-8 [&_button]:items-center [&_button]:justify-center [&_button]:!rounded-xl [&_button]:!p-0 [&_button]:transition-colors [&_button]:hover:bg-sidebar-accent [&_button_svg]:!size-5">
<NotificationToggle />
</div>
</TooltipTrigger>
<TooltipContent side="top">Notifications</TooltipContent>
<TooltipContent side={isCollapsed ? "right" : "top"}>
Notifications
</TooltipContent>
</Tooltip>
)}
{!isCollapsed && !tallyState.isFormVisible && (
{(!isNewSidebar || !isCollapsed) && !tallyState.isFormVisible && (
<Tooltip>
<TooltipTrigger asChild>
<button
@@ -337,7 +388,9 @@ export function AppSidebar({ dynamicContent }: Props) {
<ChatCircleDots className="!size-5" />
</button>
</TooltipTrigger>
<TooltipContent side="top">Feedback</TooltipContent>
<TooltipContent side={isCollapsed ? "right" : "top"}>
Feedback
</TooltipContent>
</Tooltip>
)}
{!isCollapsed && <div className="flex-1" />}

View File

@@ -2,17 +2,20 @@
import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
import { okData } from "@/app/api/helpers";
import { IconType } from "@/components/__legacy__/ui/icons";
import { PreviewBanner } from "@/components/layout/Navbar/components/PreviewBanner/PreviewBanner";
import { SidebarTrigger } from "@/components/ui/sidebar";
import { isLogoutInProgress } from "@/lib/autogpt-server-api/helpers";
import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { environment } from "@/services/environment";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { List } from "@phosphor-icons/react";
import { AccountMenu } from "./components/AccountMenu/AccountMenu";
import { LoginButton } from "./components/LoginButton";
import { MobileNavBar } from "./components/MobileNavbar/MobileNavBar";
import { Wallet } from "./components/Wallet/Wallet";
import { getAccountMenuItems } from "./helpers";
import { getAccountMenuItems, loggedInLinks } from "./helpers";
export function Navbar() {
const { user, isLoggedIn, isUserLoading } = useSupabase();
@@ -21,6 +24,7 @@ export function Navbar() {
const dynamicMenuItems = getAccountMenuItems(user?.role);
const previewBranchName = environment.getPreviewStealingDev();
const logoutInProgress = isLogoutInProgress();
const isNewSidebar = useGetFlag(Flag.NEW_SIDEBAR);
const { data: profile, isLoading: isProfileLoading } = useGetV2GetUserProfile(
{
@@ -35,6 +39,12 @@ export function Navbar() {
const isLoadingProfile = isProfileLoading || isUserLoading;
const shouldShowPreviewBanner = Boolean(isLoggedIn && previewBranchName);
const actualLoggedInLinks = [
{ name: "Home", href: "/copilot" },
{ name: "Agents", href: "/library" },
...loggedInLinks,
];
if (isUserLoading) {
return null;
}
@@ -67,8 +77,8 @@ export function Navbar() {
</div>
) : null}
{/* Mobile top bar: credits + hamburger on right */}
{isLoggedIn && isSmallScreen ? (
{/* Mobile top bar: new sidebar trigger + wallet (new) or MobileNavBar (old) */}
{isLoggedIn && isSmallScreen && isNewSidebar ? (
<div className="fixed right-0 top-0 z-50 flex items-center gap-2 px-3 py-2">
<SidebarTrigger className="flex size-10 items-center justify-center rounded-full border border-zinc-200 bg-white [&>svg]:!size-5">
<List className="!size-5" weight="bold" />
@@ -76,6 +86,47 @@ export function Navbar() {
<Wallet />
</div>
) : null}
{isLoggedIn && isSmallScreen && !isNewSidebar ? (
<div className="fixed right-0 top-2 z-50 flex items-center gap-0">
<Wallet />
<MobileNavBar
userName={profile?.username}
menuItemGroups={[
{
groupName: "Navigation",
items: actualLoggedInLinks
.map((link) => {
return {
icon:
link.href === "/marketplace"
? IconType.Marketplace
: link.href === "/build"
? IconType.Builder
: link.href === "/copilot"
? IconType.Chat
: link.href === "/library"
? IconType.Library
: link.href === "/monitor"
? IconType.Library
: IconType.LayoutDashboard,
text: link.name,
href: link.href,
};
})
.filter((item) => item !== null) as Array<{
icon: IconType;
text: string;
href: string;
}>,
},
...dynamicMenuItems,
]}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url ?? ""}
/>
</div>
) : null}
</>
);
}

View File

@@ -11,6 +11,7 @@ export enum Flag {
ARTIFACTS = "artifacts",
CHAT_MODE_OPTION = "chat-mode-option",
BUILDER_CHAT_PANEL = "builder-chat-panel",
NEW_SIDEBAR = "new-sidebar",
}
const isPwMockEnabled = process.env.NEXT_PUBLIC_PW_TEST === "true";
@@ -22,6 +23,7 @@ const defaultFlags = {
[Flag.ARTIFACTS]: false,
[Flag.CHAT_MODE_OPTION]: false,
[Flag.BUILDER_CHAT_PANEL]: false,
[Flag.NEW_SIDEBAR]: false,
};
type FlagValues = typeof defaultFlags;