feat: redesign workspace UI — glassmorphism dropdowns, Chrome-style tabs, chat popover

- Move chat history from left sidebar to floating popover on tab bar
- Add dench-ui components (Button, Card, Input, Label, Switch) with deps
- Glassmorphism styling for all dropdowns/context menus with dark mode
- Chrome-style active tab that merges with content area
- Align sidebar header with tab bar (34px)
- Condense sidebar header to single line
- Move sidebar expand button into tab bar
- Add next-themes for proper dark mode with system preference support
- Add Tailwind v4 class-based dark mode via @custom-variant
- Add dench-ui CSS tokens (light + dark)
- Restore pointer cursor for all interactive elements
- New chat button always visible in tab bar
- "Delete this chat" label in dropdown menu

Made-with: Cursor
This commit is contained in:
Mark 2026-03-12 13:30:04 -07:00
parent 45db1bcf54
commit fbfdee21a5
17 changed files with 781 additions and 337 deletions

View File

@ -813,6 +813,8 @@ type ChatPanelProps = {
subagentLabel?: string;
/** Back button handler (subagent mode only). */
onBack?: () => void;
/** Hide the header action buttons (when they're rendered elsewhere, e.g. tab bar). */
hideHeaderActions?: boolean;
};
export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
@ -834,6 +836,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
subagentTask,
subagentLabel,
onBack,
hideHeaderActions,
},
ref,
) {
@ -2229,6 +2232,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
</h2>
)}
</div>
{!hideHeaderActions && (
<div className="flex items-center gap-1 shrink-0">
{currentSessionId && onDeleteSession && (
<DropdownMenu>
@ -2259,13 +2263,12 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
onSelect={() => onDeleteSession(currentSessionId)}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18" /><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" /><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" /></svg>
Delete
Delete this chat
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{compact && (
<button
<button
type="button"
onClick={() => handleNewSession()}
className="p-1.5 rounded-lg"
@ -2288,8 +2291,8 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
<path d="M5 12h14" />
</svg>
</button>
)}
</div>
)}
</>
)}
</header>
@ -2365,6 +2368,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
{/* Hero greeting */}
{greeting && (
<motion.h1
layout="position"
className="text-4xl md:text-5xl font-light tracking-normal font-instrument mb-10 text-center"
style={{ color: "var(--color-text)" }}
initial="hidden"
@ -2376,6 +2380,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
transition: { staggerChildren: 0.12, delayChildren: 0.2 },
},
}}
transition={{ layout: { type: "spring", stiffness: 260, damping: 30 } }}
>
{greeting.split(" ").map((word, i) => (
<motion.span
@ -2399,10 +2404,11 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
{/* Centered input bar */}
<motion.div
layout="position"
className="w-full max-w-[720px] mx-auto px-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.8, ease: [0.22, 1, 0.36, 1] }}
transition={{ duration: 0.8, delay: 0.8, ease: [0.22, 1, 0.36, 1], layout: { type: "spring", stiffness: 260, damping: 30 } }}
>
<motion.div
layout
@ -2415,10 +2421,11 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
{/* Prompt suggestion pills */}
<motion.div
layout="position"
className="mt-6 flex flex-col gap-2.5 w-full max-w-[720px] mx-auto px-4"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 1.0, ease: [0.22, 1, 0.36, 1] }}
transition={{ duration: 0.5, delay: 1.0, ease: [0.22, 1, 0.36, 1], layout: { type: "spring", stiffness: 260, damping: 30 } }}
>
<div className="flex items-center justify-center gap-2 flex-wrap">
{visiblePrompts.slice(0, 3).map((template) => {

View File

@ -0,0 +1,47 @@
"use client";
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 outline-none ring-0 border-none",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
);
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
}
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@ -0,0 +1,36 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("rounded-xl border bg-card text-card-foreground shadow", className)} {...props} />
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => <h3 ref={ref} className={cn("font-semibold leading-none tracking-tight", className)} {...props} />
);
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
);
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
));
CardFooter.displayName = "CardFooter";
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };

View File

@ -58,7 +58,7 @@ function DropdownMenuContent({
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn(
"bg-neutral-100/[0.67] border border-white backdrop-blur-md text-[var(--color-text)] z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-3xl p-1 shadow-[0_0_25px_0_rgba(0,0,0,0.16)] outline-none",
"bg-neutral-100/[0.67] dark:bg-neutral-900/[0.67] border border-white dark:border-white/10 backdrop-blur-md text-[var(--color-text)] z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-3xl p-1 shadow-[0_0_25px_0_rgba(0,0,0,0.16)] outline-none",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}

View File

@ -0,0 +1,21 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
);
});
Input.displayName = "Input";
export { Input };

View File

@ -0,0 +1,18 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View File

@ -0,0 +1,28 @@
"use client";
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };

View File

@ -452,11 +452,11 @@ export function ChatSessionsSidebar({
</div>
{/* Header overlay: backdrop blur + 80% bg; list scrolls under it */}
<div
className="absolute top-0 left-0 right-0 z-10 flex items-center justify-between border-b px-4 py-2 backdrop-blur-md"
className={`absolute top-0 left-0 right-0 z-10 flex items-center justify-between px-4 py-2 backdrop-blur-md ${embedded ? "" : "border-b"}`}
style={{
height: headerHeight,
borderColor: "var(--color-border)",
background: "color-mix(in srgb, var(--color-sidebar-bg) 80%, transparent)",
borderColor: embedded ? undefined : "var(--color-border)",
background: embedded ? "transparent" : "color-mix(in srgb, var(--color-sidebar-bg) 80%, transparent)",
}}
>
<div className="min-w-0 flex-1 flex items-center gap-1.5">

View File

@ -160,14 +160,10 @@ export function ContextMenu({ x, y, target, onAction, onClose }: ContextMenuProp
return createPortal(
<div
ref={menuRef}
className="fixed z-[9999] min-w-[200px] py-1 rounded-lg shadow-xl border"
className="fixed z-[9999] min-w-[200px] p-1 rounded-2xl bg-neutral-100/[0.67] dark:bg-neutral-900/[0.67] border border-white dark:border-white/10 backdrop-blur-md shadow-[0_0_25px_0_rgba(0,0,0,0.16)]"
style={{
left: x,
top: y,
background: "var(--color-surface)",
borderColor: "var(--color-border)",
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
animation: "contextMenuFadeIn 100ms ease-out",
}}
role="menu"
@ -188,8 +184,7 @@ export function ContextMenu({ x, y, target, onAction, onClose }: ContextMenuProp
return (
<div
key={`sep-${i}`}
className="my-1 mx-2 border-t"
style={{ borderColor: "var(--color-border)" }}
className="my-0.5 mx-1 h-px bg-neutral-400/15"
/>
);
}
@ -203,25 +198,13 @@ export function ContextMenu({ x, y, target, onAction, onClose }: ContextMenuProp
type="button"
role="menuitem"
disabled={isDisabled}
className="w-full flex items-center gap-2 px-3 py-1.5 text-[13px] text-left transition-colors"
className={`w-full flex items-center gap-2 px-2.5 py-1.5 text-[13px] text-left rounded-xl transition-all ${isDisabled ? "opacity-50" : "hover:bg-neutral-400/15"}`}
style={{
color: isDisabled
? "var(--color-text-muted)"
: menuItem.danger
? "#ef4444"
? "var(--color-error)"
: "var(--color-text)",
opacity: isDisabled ? 0.5 : 1,
cursor: isDisabled ? "default" : "pointer",
}}
onMouseEnter={(e) => {
if (!isDisabled) {
(e.currentTarget as HTMLElement).style.background = menuItem.danger
? "rgba(239, 68, 68, 0.1)"
: "var(--color-surface-hover)";
}
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.background = "transparent";
}}
onClick={() => handleItemClick(menuItem.action, isDisabled)}
>

View File

@ -199,69 +199,48 @@ export function ProfileSwitcher({
{showSwitcher && isOpen && (
<div
className="absolute left-0 top-full mt-1 w-64 rounded-lg overflow-hidden z-50"
style={{
background: "var(--color-surface-raised)",
border: "1px solid var(--color-border)",
boxShadow: "var(--shadow-lg)",
}}
className="absolute left-0 top-full mt-1.5 w-64 rounded-2xl overflow-hidden z-50 p-1 bg-neutral-100/[0.67] dark:bg-neutral-900/[0.67] border border-white dark:border-white/10 backdrop-blur-md shadow-[0_0_25px_0_rgba(0,0,0,0.16)]"
>
{/* Header */}
<div
className="px-3 py-2 text-xs font-medium"
style={{
color: "var(--color-text-muted)",
borderBottom: "1px solid var(--color-border)",
}}
className="px-2.5 py-1.5 text-[11px] font-medium"
style={{ color: "var(--color-text-muted)" }}
>
Workspaces
</div>
{/* Workspace list */}
<div className="py-1 max-h-64 overflow-y-auto">
<div className="max-h-64 overflow-y-auto">
{workspaces.map((workspace) => {
const isCurrent = workspace.name === activeWorkspace;
return (
<div key={workspace.name} className="flex items-center gap-1 px-1.5 py-0.5">
<div key={workspace.name} className="flex items-center gap-0.5">
<button
onClick={() => void handleSwitch(workspace.name)}
disabled={switching || !!deletingWorkspace}
className="flex-1 min-w-0 flex items-center gap-2 px-1.5 py-1.5 rounded text-left text-sm transition-colors hover:bg-[var(--color-surface-hover)] disabled:opacity-50"
className="flex-1 min-w-0 flex items-center gap-2 px-2.5 py-1.5 rounded-xl text-left text-sm transition-all hover:bg-neutral-400/15 disabled:opacity-50 cursor-pointer"
style={{ color: "var(--color-text)" }}
>
{/* Active indicator */}
<span
className="w-2 h-2 rounded-full flex-shrink-0"
className="w-2 h-2 rounded-full shrink-0"
style={{
background: isCurrent ? "var(--color-success)" : "transparent",
border: isCurrent ? "none" : "1px solid var(--color-border-strong)",
}}
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5">
<span className="truncate font-medium">
{workspace.name}
</span>
</div>
<div
className="text-xs truncate mt-0.5"
<span className="truncate font-medium text-[13px] block">
{workspace.name}
</span>
<span
className="text-[11px] truncate block mt-0.5"
style={{ color: "var(--color-text-muted)" }}
>
{workspace.workspaceDir
? shortenPath(workspace.workspaceDir)
: "No workspace yet"}
</div>
</span>
</div>
{isCurrent && (
<span
className="text-xs px-1.5 py-0.5 rounded"
style={{
background: "var(--color-accent-light)",
color: "var(--color-accent)",
}}
>
<span className="text-[10px] px-1.5 py-0.5 rounded-full shrink-0 bg-neutral-400/15" style={{ color: "var(--color-text-muted)" }}>
Active
</span>
)}
@ -272,28 +251,15 @@ export function ProfileSwitcher({
onClick={() => void handleDeleteWorkspace(workspace.name)}
disabled={switching || !!deletingWorkspace}
title={`Delete workspace ${workspace.name}`}
className="p-1.5 rounded transition-colors hover:bg-[var(--color-surface-hover)] disabled:opacity-50"
className="p-1.5 rounded-xl transition-all hover:bg-neutral-400/15 disabled:opacity-50 shrink-0 cursor-pointer"
style={{
color: deletingWorkspace === workspace.name
? "var(--color-text-muted)"
: "var(--color-error)",
}}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 6h18" />
<path d="M8 6V4h8v2" />
<path d="M19 6l-1 14H6L5 6" />
<path d="M10 11v6" />
<path d="M14 11v6" />
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18" /><path d="M8 6V4h8v2" /><path d="M19 6l-1 14H6L5 6" />
</svg>
</button>
)}
@ -304,7 +270,7 @@ export function ProfileSwitcher({
{actionError && (
<p
className="mx-3 mb-2 mt-1 rounded px-2 py-1 text-xs"
className="mx-2 mb-1 mt-1 rounded-xl px-2.5 py-1.5 text-xs"
style={{
background: "rgba(220, 38, 38, 0.08)",
color: "var(--color-error)",
@ -314,17 +280,16 @@ export function ProfileSwitcher({
</p>
)}
{/* Create new */}
<div style={{ borderTop: "1px solid var(--color-border)" }}>
<div className="border-t border-neutral-400/15 mt-0.5 pt-0.5">
<button
onClick={() => {
setIsOpen(false);
onCreateWorkspace?.();
}}
className="w-full flex items-center gap-2 px-3 py-2.5 text-sm transition-colors hover:bg-[var(--color-surface-hover)]"
className="w-full flex items-center gap-2 px-2.5 py-1.5 text-sm rounded-xl transition-all hover:bg-neutral-400/15 cursor-pointer"
style={{ color: "var(--color-accent)" }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 5v14" /><path d="M5 12h14" />
</svg>
New Workspace

View File

@ -14,6 +14,8 @@ type TabBarProps = {
onCloseAll: () => void;
onReorder: (fromIndex: number, toIndex: number) => void;
onTogglePin: (tabId: string) => void;
leftContent?: React.ReactNode;
rightContent?: React.ReactNode;
};
type ContextMenuState = {
@ -32,6 +34,8 @@ export function TabBar({
onCloseAll,
onReorder,
onTogglePin,
leftContent,
rightContent,
}: TabBarProps) {
const [contextMenu, setContextMenu] = useState<ContextMenuState>(null);
const [dragIndex, setDragIndex] = useState<number | null>(null);
@ -95,14 +99,22 @@ export function TabBar({
return (
<>
<div
ref={scrollRef}
className="flex items-end overflow-x-auto flex-shrink-0"
className="flex items-stretch flex-shrink-0 h-[34px]"
style={{
background: "var(--color-surface)",
borderBottom: "1px solid var(--color-border)",
scrollbarWidth: "none",
}}
>
<div
ref={scrollRef}
className="flex items-stretch overflow-x-auto flex-1 min-w-0"
style={{ scrollbarWidth: "none" }}
>
{leftContent && (
<div className="flex items-center px-1.5 shrink-0">
{leftContent}
</div>
)}
{tabs.map((tab, index) => {
const isActive = tab.id === activeTabId;
const isDragOver = dragOverIndex === index && dragIndex !== index;
@ -120,15 +132,14 @@ export function TabBar({
onDragOver={isHome ? undefined : (e) => handleDragOver(e, index)}
onDrop={isHome ? undefined : (e) => handleDrop(e, index)}
onDragEnd={isHome ? undefined : handleDragEnd}
className={`group flex items-center gap-1.5 h-[34px] text-[12.5px] font-medium cursor-pointer flex-shrink-0 relative transition-colors duration-75 select-none ${isHome ? "px-2.5" : "pl-3 pr-1.5"}`}
className={`group flex items-center gap-1.5 text-[12.5px] font-medium cursor-pointer flex-shrink-0 relative transition-colors duration-75 select-none ${isHome ? "px-2.5" : "pl-3 pr-1.5"} ${isActive ? "mb-[-1px] rounded-t-lg" : ""}`}
style={{
color: isActive ? "var(--color-text)" : "var(--color-text-muted)",
background: isActive ? "var(--color-bg)" : "transparent",
borderBottom: isActive ? "2px solid var(--color-accent)" : "2px solid transparent",
borderLeft: isDragOver && !isHome ? "2px solid var(--color-accent)" : "2px solid transparent",
background: isActive ? "var(--color-main-bg)" : "transparent",
borderBottom: isActive ? "1px solid var(--color-main-bg)" : "none",
borderLeft: isDragOver && !isHome ? "2px solid var(--color-accent)" : undefined,
opacity: dragIndex === index ? 0.5 : 1,
maxWidth: isHome ? undefined : 200,
borderRight: isHome ? "1px solid var(--color-border)" : undefined,
}}
title={isHome ? "Home (New Chat)" : undefined}
>
@ -162,24 +173,28 @@ export function TabBar({
</button>
);
})}
</div>
{rightContent && (
<div className="relative flex items-center gap-0.5 px-2 shrink-0 h-[34px]">
{rightContent}
</div>
)}
</div>
{/* Context menu */}
{contextMenu && contextTab && (
<div
className="fixed z-[9999] min-w-[180px] rounded-lg border py-1 shadow-lg"
className="fixed z-[9999] min-w-[180px] rounded-2xl p-1 bg-neutral-100/[0.67] dark:bg-neutral-900/[0.67] border border-white dark:border-white/10 backdrop-blur-md shadow-[0_0_25px_0_rgba(0,0,0,0.16)]"
style={{
left: contextMenu.x,
top: contextMenu.y,
background: "var(--color-surface)",
borderColor: "var(--color-border)",
}}
>
<ContextMenuItem
label={contextTab.pinned ? "Unpin Tab" : "Pin Tab"}
onClick={() => { onTogglePin(contextMenu.tabId); setContextMenu(null); }}
/>
<div className="h-px my-1" style={{ background: "var(--color-border)" }} />
<div className="h-px my-0.5 mx-1 bg-neutral-400/15" />
<ContextMenuItem
label="Close"
shortcut="⌘W"
@ -220,14 +235,8 @@ function ContextMenuItem({
type="button"
disabled={disabled}
onClick={onClick}
className="w-full flex items-center justify-between px-3 py-1.5 text-[12.5px] text-left transition-colors disabled:opacity-40 cursor-pointer disabled:cursor-default"
className="w-full flex items-center justify-between px-2.5 py-1.5 text-[12.5px] text-left rounded-xl transition-all disabled:opacity-40 hover:bg-neutral-400/15"
style={{ color: "var(--color-text)" }}
onMouseEnter={(e) => {
if (!disabled) (e.currentTarget as HTMLElement).style.background = "var(--color-surface-hover)";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.background = "transparent";
}}
>
<span>{label}</span>
{shortcut && (

View File

@ -1,6 +1,7 @@
"use client";
import { useEffect, useState, useRef, useCallback } from "react";
import { useTheme } from "next-themes";
import { FileManagerTree, type TreeNode } from "./file-manager-tree";
import { ProfileSwitcher } from "./profile-switcher";
import { CreateWorkspaceDialog } from "./create-workspace-dialog";
@ -105,66 +106,32 @@ function FolderOpenIcon() {
/* ─── Theme toggle ─── */
function ThemeToggle() {
const [isDark, setIsDark] = useState(false);
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
useEffect(() => {
setIsDark(document.documentElement.classList.contains("dark"));
}, []);
if (!mounted) return <div className="w-[28px] h-[28px]" />;
const toggle = () => {
const next = !isDark;
setIsDark(next);
if (next) {
document.documentElement.classList.add("dark");
localStorage.setItem("theme", "dark");
} else {
document.documentElement.classList.remove("dark");
localStorage.setItem("theme", "light");
}
};
const isDark = resolvedTheme === "dark";
return (
<button
type="button"
onClick={toggle}
onClick={() => setTheme(isDark ? "light" : "dark")}
className="p-1.5 rounded-lg"
style={{ color: "var(--color-text-muted)" }}
title={isDark ? "Switch to light mode" : "Switch to dark mode"}
>
{isDark ? (
/* Sun icon */
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.93 4.93 1.41 1.41" />
<path d="m17.66 17.66 1.41 1.41" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.34 17.66-1.41 1.41" />
<path d="m19.07 4.93-1.41 1.41" />
<path d="M12 2v2" /><path d="M12 20v2" />
<path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" />
<path d="M2 12h2" /><path d="M20 12h2" />
<path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" />
</svg>
) : (
/* Moon icon */
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
</svg>
)}
@ -467,44 +434,31 @@ export function WorkspaceSidebar({
>
{/* Header */}
<div
className="flex items-center gap-2 px-3 py-2.5 border-b"
className="flex items-center gap-2 px-3 h-[34px] border-b"
style={{ borderColor: "var(--color-border)" }}
>
{isBrowsing ? (
<>
<span
className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0"
style={{
background: "var(--color-surface-hover)",
color: "var(--color-text-muted)",
}}
>
<FolderOpenIcon />
</span>
<div className="flex-1 min-w-0">
<div
className="text-sm font-medium truncate"
<div className="flex-1 min-w-0 flex items-center gap-1.5">
<span
className="shrink-0"
style={{ color: "var(--color-text-muted)" }}
>
<FolderOpenIcon />
</span>
<span
className="text-[12px] font-medium truncate"
style={{ color: "var(--color-text)" }}
title={browseDir}
>
{dirDisplayName(browseDir)}
</div>
<div
className="text-[11px] truncate"
style={{
color: "var(--color-text-muted)",
}}
title={browseDir}
>
{browseDir}
</div>
</span>
</div>
{/* Home button to return to workspace */}
{onGoHome && (
<button
type="button"
onClick={onGoHome}
className="p-1.5 rounded-lg flex-shrink-0"
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="Return to workspace"
>
@ -514,66 +468,48 @@ export function WorkspaceSidebar({
</>
) : (
<>
<button
type="button"
onClick={() => void onGoToChat?.()}
className="w-7 h-7 rounded-lg flex items-center justify-center shrink-0 cursor-pointer transition-colors hover:bg-stone-200 dark:hover:bg-stone-700"
style={{
background: "transparent",
color: "var(--color-text-muted)",
}}
title="All Chats"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" />
</svg>
</button>
<div className="flex-1 min-w-0 px-1.5">
<div className="text-[13px] font-semibold truncate text-stone-700 dark:text-stone-200">
{orgName || "Workspace"}
</div>
<div className="mt-1">
<ProfileSwitcher
activeWorkspaceHint={activeWorkspace ?? null}
onWorkspaceSwitch={() => {
onWorkspaceChanged?.();
}}
onWorkspaceDelete={() => {
onWorkspaceChanged?.();
}}
onCreateWorkspace={() => {
setCreateWorkspaceOpen(true);
}}
trigger={({ onClick, activeWorkspace: workspaceName, switching }) => (
<button
type="button"
onClick={onClick}
disabled={switching}
className="text-[11px] flex items-center gap-1 truncate w-full transition-colors"
<div className="flex-1 min-w-0">
<ProfileSwitcher
activeWorkspaceHint={activeWorkspace ?? null}
onWorkspaceSwitch={() => {
onWorkspaceChanged?.();
}}
onWorkspaceDelete={() => {
onWorkspaceChanged?.();
}}
onCreateWorkspace={() => {
setCreateWorkspaceOpen(true);
}}
trigger={({ onClick, activeWorkspace: workspaceName, switching }) => (
<button
type="button"
onClick={onClick}
disabled={switching}
className="text-[12px] flex items-center gap-1.5 truncate w-full transition-colors font-medium rounded-md px-1.5 py-1 -mx-1.5 hover:bg-stone-200/60 dark:hover:bg-stone-700/60"
style={{ color: "var(--color-text)" }}
title="Switch workspace"
>
<span className="truncate">{orgName || "Workspace"}</span>
<span className="px-1 py-px rounded text-[10px] leading-tight shrink-0 bg-stone-200 text-stone-600 dark:bg-stone-700 dark:text-stone-300">
{workspaceName || "-"}
</span>
<svg
width="10"
height="10"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="shrink-0"
style={{ color: "var(--color-text-muted)" }}
title="Switch workspace"
>
<span>Workspace</span>
<span className="px-1 py-0.5 rounded text-[10px] shrink-0 bg-stone-200 text-stone-600 dark:bg-stone-700 dark:text-stone-300">
{workspaceName || "-"}
</span>
<svg
width="10"
height="10"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m6 9 6 6 6-6" />
</svg>
</button>
)}
/>
</div>
<path d="m6 9 6 6 6-6" />
</svg>
</button>
)}
/>
</div>
</>
)}
@ -593,83 +529,33 @@ export function WorkspaceSidebar({
)}
</div>
{/* Tab switcher */}
{hasChatProps && !isBrowsing && (
<div
className="flex px-3 pt-2 pb-1 gap-1"
>
<button
type="button"
onClick={() => setTab("files")}
className="flex-1 text-[11px] font-medium py-1.5 rounded-md transition-colors"
style={{
color: currentTab === "files" ? "var(--color-text)" : "var(--color-text-muted)",
background: currentTab === "files" ? "var(--color-surface-hover)" : "transparent",
}}
>
Files
</button>
<button
type="button"
onClick={() => setTab("chats")}
className="flex-1 text-[11px] font-medium py-1.5 rounded-md transition-colors"
style={{
color: currentTab === "chats" ? "var(--color-text)" : "var(--color-text-muted)",
background: currentTab === "chats" ? "var(--color-surface-hover)" : "transparent",
}}
>
Chats
</button>
</div>
{onFileSearchSelect && (
<FileSearch onSelect={onFileSearchSelect} />
)}
{/* Tab content */}
{currentTab === "files" || !hasChatProps ? (
<>
{onFileSearchSelect && (
<FileSearch onSelect={onFileSearchSelect} />
)}
<div className="flex-1 overflow-y-auto px-1">
{loading ? (
<div className="flex items-center justify-center py-12">
<UnicodeSpinner
name="braille"
className="text-2xl"
style={{ color: "var(--color-text-muted)" }}
/>
</div>
) : (
<FileManagerTree
tree={tree}
activePath={activePath}
onSelect={onSelect}
onRefresh={onRefresh}
parentDir={parentDir}
onNavigateUp={onNavigateUp}
browseDir={browseDir}
workspaceRoot={workspaceRoot}
onExternalDrop={onExternalDrop}
/>
)}
<div className="flex-1 overflow-y-auto px-1">
{loading ? (
<div className="flex items-center justify-center py-12">
<UnicodeSpinner
name="braille"
className="text-2xl"
style={{ color: "var(--color-text-muted)" }}
/>
</div>
</>
) : (
<ChatSessionsSidebar
sessions={chatSessions ?? []}
activeSessionId={activeChatSessionId ?? null}
activeSessionTitle={activeChatSessionTitle}
streamingSessionIds={chatStreamingSessionIds}
subagents={chatSubagents}
activeSubagentKey={chatActiveSubagentKey}
loading={chatSessionsLoading}
onSelectSession={onSelectChatSession ?? (() => {})}
onNewSession={onNewChatSession ?? (() => {})}
onSelectSubagent={onSelectChatSubagent}
onDeleteSession={onDeleteChatSession}
onRenameSession={onRenameChatSession}
embedded
/>
)}
) : (
<FileManagerTree
tree={tree}
activePath={activePath}
onSelect={onSelect}
onRefresh={onRefresh}
parentDir={parentDir}
onNavigateUp={onNavigateUp}
browseDir={browseDir}
workspaceRoot={workspaceRoot}
onExternalDrop={onExternalDrop}
/>
)}
</div>
{/* Footer */}
<div

View File

@ -1,5 +1,7 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
/* ============================================================
Theme System Light (default) & Dark (.dark)
============================================================ */
@ -70,6 +72,28 @@
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
--shadow-xl: 0 8px 32px rgba(0, 0, 0, 0.1);
/* Dench UI tokens (used by @denchhq/ui-react primitives) */
--background: 0 0% 90%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
.dark {
@ -138,6 +162,28 @@
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.4);
--shadow-xl: 0 8px 32px rgba(0, 0, 0, 0.5);
/* Dench UI tokens (used by @denchhq/ui-react primitives) */
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--radius: 0.5rem;
}
/* Disable iframe pointer events during sidebar resize so the
@ -281,6 +327,17 @@ a,
transition-timing-function: ease;
}
/* Restore pointer cursor for interactive elements (Tailwind v4 preflight resets to default) */
button:not(:disabled),
[type="button"]:not(:disabled),
[role="button"]:not(:disabled),
a[href],
select:not(:disabled),
summary,
label {
cursor: pointer !important;
}
/* ============================================================
Scrollbar
============================================================ */

View File

@ -1,5 +1,6 @@
import type { Metadata, Viewport } from "next";
import { Suspense } from "react";
import { ThemeProvider } from "next-themes";
import { getOrCreateAnonymousId } from "@/lib/telemetry";
import { PostHogProvider } from "./components/posthog-provider";
import "./globals.css";
@ -38,19 +39,15 @@ export default function RootLayout({
href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
{/* Inline script to prevent FOUC — reads localStorage or system preference */}
<script
dangerouslySetInnerHTML={{
__html: `try{if(localStorage.theme==="dark"||(!("theme" in localStorage)&&window.matchMedia("(prefers-color-scheme: dark)").matches)){document.documentElement.classList.add("dark")}else{document.documentElement.classList.remove("dark")}}catch(e){}`,
}}
/>
</head>
<body className="antialiased">
<Suspense fallback={null}>
<PostHogProvider anonymousId={anonymousId}>
{children}
</PostHogProvider>
</Suspense>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
<Suspense fallback={null}>
<PostHogProvider anonymousId={anonymousId}>
{children}
</PostHogProvider>
</Suspense>
</ThemeProvider>
</body>
</html>
);

View File

@ -43,6 +43,13 @@ import {
autoDetectViewField,
} from "@/lib/object-filters";
import { UnicodeSpinner } from "../components/unicode-spinner";
import { ChatSessionsSidebar } from "../components/workspace/chat-sessions-sidebar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../components/ui/dropdown-menu";
import { resolveActiveViewSyncDecision } from "./object-view-active-view";
import { resetWorkspaceStateOnSwitch } from "./workspace-switch";
import { TabBar } from "../components/workspace/tab-bar";
@ -501,6 +508,20 @@ function WorkspacePageInner() {
// Sidebar collapse state (desktop only).
const [leftSidebarCollapsed, setLeftSidebarCollapsed] = useState(false);
const [rightSidebarCollapsed, setRightSidebarCollapsed] = useState(false);
const [sidebarTab, setSidebarTab] = useState<"files" | "chats">("files");
const [chatPopoverOpen, setChatPopoverOpen] = useState(false);
const chatPopoverRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!chatPopoverOpen) return;
function handleClickOutside(e: MouseEvent) {
if (chatPopoverRef.current && !chatPopoverRef.current.contains(e.target as Node)) {
setChatPopoverOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [chatPopoverOpen]);
// Terminal drawer state
const [terminalOpen, setTerminalOpen] = useState(false);
@ -1829,6 +1850,8 @@ function WorkspacePageInner() {
onSelectChatSubagent={handleSelectSubagent}
onDeleteChatSession={handleDeleteSession}
onRenameChatSession={handleRenameSession}
activeTab={sidebarTab}
onTabChange={setSidebarTab}
mobile
onClose={() => setSidebarOpen(false)}
/>
@ -1888,29 +1911,14 @@ function WorkspacePageInner() {
onSelectChatSubagent={handleSelectSubagent}
onDeleteChatSession={handleDeleteSession}
onRenameChatSession={handleRenameSession}
activeTab={sidebarTab}
onTabChange={setSidebarTab}
/>
</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)" }}>
@ -1961,12 +1969,115 @@ function WorkspacePageInner() {
tabs={tabState.tabs}
activeTabId={tabState.activeTabId}
onActivate={handleTabActivate}
leftContent={leftSidebarCollapsed ? (
<button
type="button"
onClick={() => setLeftSidebarCollapsed(false)}
className="p-1.5 rounded-md transition-colors hover:bg-black/5 cursor-pointer"
style={{ color: "var(--color-text-muted)" }}
title="Show 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>
) : undefined}
onClose={handleTabClose}
onCloseOthers={handleTabCloseOthers}
onCloseToRight={handleTabCloseToRight}
onCloseAll={handleTabCloseAll}
onReorder={handleTabReorder}
onTogglePin={handleTabTogglePin}
rightContent={showMainChat ? (
<>
<div className="relative" ref={chatPopoverRef}>
<button
type="button"
onClick={() => setChatPopoverOpen((v) => !v)}
className="p-1.5 rounded-lg cursor-pointer"
style={{ color: chatPopoverOpen ? "var(--color-accent)" : "var(--color-text-muted)" }}
title="Chat history"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
</button>
{chatPopoverOpen && (
<div
className="absolute right-0 top-full mt-1.5 w-72 h-96 rounded-2xl overflow-hidden z-[9999] bg-neutral-100/[0.67] dark:bg-neutral-900/[0.67] border border-white dark:border-white/10 backdrop-blur-md shadow-[0_0_25px_0_rgba(0,0,0,0.16)] flex flex-col"
>
<ChatSessionsSidebar
sessions={sessions}
activeSessionId={activeSessionId}
activeSessionTitle={activeSessionTitle}
streamingSessionIds={streamingSessionIds}
subagents={subagents}
activeSubagentKey={activeSubagentKey}
loading={sessionsLoading}
onSelectSession={(sessionId) => {
setActiveSessionId(sessionId);
setActiveSubagentKey(null);
void chatRef.current?.loadSession(sessionId);
setChatPopoverOpen(false);
}}
onNewSession={() => {
setActiveSessionId(null);
setActiveSubagentKey(null);
void chatRef.current?.newSession();
setChatPopoverOpen(false);
}}
onSelectSubagent={(key) => {
handleSelectSubagent(key);
setChatPopoverOpen(false);
}}
onDeleteSession={handleDeleteSession}
onRenameSession={handleRenameSession}
embedded
/>
</div>
)}
</div>
{activeSessionId && (
<DropdownMenu>
<DropdownMenuTrigger
className="p-1.5 rounded-lg cursor-pointer"
style={{ color: "var(--color-text-muted)" }}
title="More options"
aria-label="More options"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="1" /><circle cx="5" cy="12" r="1" /><circle cx="19" cy="12" r="1" />
</svg>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom">
<DropdownMenuItem
variant="destructive"
onSelect={() => handleDeleteSession(activeSessionId)}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18" /><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" /><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" /></svg>
Delete this chat
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
<button
type="button"
onClick={() => {
setActiveSessionId(null);
setActiveSubagentKey(null);
void chatRef.current?.newSession();
}}
className="p-1.5 rounded-lg cursor-pointer"
style={{ color: "var(--color-text-muted)" }}
title="New chat"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 5v14" /><path d="M5 12h14" />
</svg>
</button>
</>
) : undefined}
/>
)}
@ -2043,6 +2154,7 @@ function WorkspacePageInner() {
subagentTask={activeSubagent?.task}
subagentLabel={activeSubagent?.label}
onBack={activeSubagent ? handleBackFromSubagent : undefined}
hideHeaderActions={!isMobile}
/>
</div>
</>

View File

@ -16,6 +16,9 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-table": "^8.21.3",
"@tiptap/core": "^3.19.0",
@ -45,6 +48,7 @@
"@xterm/xterm": "^6.0.0",
"ai": "^6.0.73",
"chokidar": "^5.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.34.0",
@ -54,6 +58,7 @@
"mammoth": "^1.11.0",
"monaco-editor": "^0.55.1",
"next": "^15.3.3",
"next-themes": "^0.4.6",
"node-pty": "^1.1.0",
"posthog-js": "^1.358.1",
"posthog-node": "^5.27.1",

273
pnpm-lock.yaml generated
View File

@ -75,6 +75,15 @@ importers:
'@monaco-editor/react':
specifier: ^4.7.0
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-label':
specifier: ^2.1.8
version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-slot':
specifier: ^1.2.4
version: 1.2.4(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-switch':
specifier: ^1.2.6
version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/match-sorter-utils':
specifier: ^8.19.4
version: 8.19.4
@ -162,6 +171,9 @@ importers:
chokidar:
specifier: ^5.0.0
version: 5.0.0
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
@ -189,6 +201,9 @@ importers:
next:
specifier: ^15.3.3
version: 15.5.12(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
node-pty:
specifier: ^1.1.0
version: 1.1.0
@ -2013,6 +2028,142 @@ packages:
'@quansync/fs@1.0.0':
resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
'@radix-ui/react-compose-refs@1.1.2':
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.2':
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-label@2.1.8':
resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.1.3':
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.1.4':
resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.2.3':
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-slot@1.2.4':
resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-switch@1.2.6':
resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-use-controllable-state@1.2.2':
resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-effect-event@0.0.2':
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.1':
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-previous@1.1.1':
resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-size@1.1.1':
resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@reduxjs/toolkit@2.11.2':
resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==}
peerDependencies:
@ -3745,6 +3896,9 @@ packages:
resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
engines: {node: '>=8'}
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
@ -5368,6 +5522,12 @@ packages:
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
engines: {node: '>= 0.4.0'}
next-themes@0.4.6:
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
peerDependencies:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
next-tick@0.2.2:
resolution: {integrity: sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==}
@ -9039,6 +9199,110 @@ snapshots:
dependencies:
quansync: 1.0.0
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
react: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
react: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
react: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
react: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
react: 19.2.4
optionalDependencies:
'@types/react': 19.2.14
'@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)':
dependencies:
'@standard-schema/spec': 1.1.0
@ -10956,6 +11220,10 @@ snapshots:
ci-info@4.4.0: {}
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
classnames@2.5.1: {}
cli-cursor@5.0.0:
@ -12937,6 +13205,11 @@ snapshots:
netmask@2.0.2: {}
next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
next-tick@0.2.2: {}
next@15.5.12(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):