diff --git a/apps/desktop/CLAUDE.md b/apps/desktop/CLAUDE.md
index a0ddf2220c6..904ffffd6ca 100644
--- a/apps/desktop/CLAUDE.md
+++ b/apps/desktop/CLAUDE.md
@@ -1,7 +1,11 @@
+# Implementation details
For Electron interprocess communnication, ALWAYS use trpc as defined in `src/lib/trpc`
Please use alias as defined in `tsconfig.json` when possible
+Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
-Please follow the clean code rules:
+
+
+# Code quality
```
Code is clean if it can be understood easily – by everyone on the team. Clean code can be read and enhanced by a developer other than its original author. With understandability comes readability, changeability, extensibility and maintainability.
_____________________________________
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 916a6754b20..889c4e1346d 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -7,7 +7,7 @@
"resources": "src/resources",
"author": {
"name": "Superset",
- "email": "hi@superset.com"
+ "email": "hi@superset.sh"
},
"scripts": {
"start": "electron-vite preview",
@@ -48,20 +48,21 @@
"framer-motion": "^12.23.24",
"http-proxy": "^1.18.1",
"lowdb": "^7.0.1",
- "lucide-react": "^0.553.0",
"node-pty": "1.1.0-beta30",
"react": "^19.1.1",
"react-arborist": "^3.4.3",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.1.1",
+ "react-icons": "^5.5.0",
"react-mosaic-component": "^6.1.1",
"react-resizable-panels": "^3.0.6",
"react-router-dom": "^7.8.2",
"react-syntax-highlighter": "^16.1.0",
"superjson": "^2.2.5",
"tailwind-merge": "^2.6.0",
- "trpc-electron": "^0.1.2"
+ "trpc-electron": "^0.1.2",
+ "zustand": "^5.0.8"
},
"devDependencies": {
"@biomejs/biome": "^2.2.6",
diff --git a/apps/desktop/src/renderer/components/ZustandExample.tsx b/apps/desktop/src/renderer/components/ZustandExample.tsx
new file mode 100644
index 00000000000..81feb9fc12f
--- /dev/null
+++ b/apps/desktop/src/renderer/components/ZustandExample.tsx
@@ -0,0 +1,69 @@
+import { useCount, useExampleStore, useText } from "renderer/stores";
+
+/**
+ * Example component demonstrating Zustand usage patterns
+ *
+ * This component shows:
+ * 1. Using selector hooks for optimized re-renders
+ * 2. Accessing actions from the store
+ * 3. Direct store access for multiple values
+ */
+export function ZustandExample() {
+ // Optimized selectors - only re-renders when specific values change
+ const count = useCount();
+ const text = useText();
+
+ // Access actions directly from the store
+ const increment = useExampleStore((state) => state.increment);
+ const decrement = useExampleStore((state) => state.decrement);
+ const setText = useExampleStore((state) => state.setText);
+ const reset = useExampleStore((state) => state.reset);
+
+ return (
+
+
Zustand Example
+
+ {/* Counter Example */}
+
+
Counter: {count}
+
+
+ Increment
+
+
+ Decrement
+
+
+
+
+ {/* Text Input Example */}
+
+
Text: {text}
+
setText(e.target.value)}
+ placeholder="Type something..."
+ className="px-3 py-2 border rounded w-full"
+ />
+
+
+ {/* Reset Button */}
+
+ Reset All
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/lib/utils.ts b/apps/desktop/src/renderer/lib/utils.ts
deleted file mode 100644
index ac680b303c9..00000000000
--- a/apps/desktop/src/renderer/lib/utils.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { type ClassValue, clsx } from "clsx";
-import { twMerge } from "tailwind-merge";
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
-}
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx
deleted file mode 100644
index af6ee29d7fe..00000000000
--- a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-export function Sidebar() {
- return (
-
- );
-}
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/index.ts b/apps/desktop/src/renderer/screens/main/components/Sidebar/index.ts
deleted file mode 100644
index 7ad39439cfb..00000000000
--- a/apps/desktop/src/renderer/screens/main/components/Sidebar/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { Sidebar } from "./Sidebar";
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/index.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/index.tsx
new file mode 100644
index 00000000000..98519f47cf0
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/index.tsx
@@ -0,0 +1,65 @@
+import { motion } from "framer-motion";
+import { useSidebarStore } from "renderer/stores";
+
+export function Sidebar() {
+ const { isSidebarOpen } = useSidebarStore();
+
+ return (
+
+
+
+ {/* Add navigation items here */}
+
+
Navigation
+
+
+ Dashboard
+
+
+ Projects
+
+
+ Settings
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/SidebarControl.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/SidebarControl.tsx
new file mode 100644
index 00000000000..8eb7b431af3
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/TopBar/SidebarControl.tsx
@@ -0,0 +1,23 @@
+import { Button } from "@superset/ui/button";
+import { HiMiniBars3, HiMiniBars3BottomLeft } from "react-icons/hi2";
+import { useSidebarStore } from "renderer/stores";
+
+export function SidebarControl() {
+ const { isSidebarOpen, toggleSidebar } = useSidebarStore();
+
+ return (
+
+ {isSidebarOpen ? (
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/AddTabButton.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/AddTabButton.tsx
new file mode 100644
index 00000000000..51f9946bb72
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/AddTabButton.tsx
@@ -0,0 +1,18 @@
+import { Button } from "@superset/ui/button";
+import { useTabsStore } from "renderer/stores/tabs";
+
+export function AddTabButton() {
+ const { addTab } = useTabsStore();
+
+ return (
+
+ +
+
+ );
+}
diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/TabItem.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/TabItem.tsx
new file mode 100644
index 00000000000..064c74aa84c
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/TabItem.tsx
@@ -0,0 +1,102 @@
+import { Button } from "@superset/ui/button";
+import { cn } from "@superset/ui/utils";
+import { useDrag, useDrop } from "react-dnd";
+import { HiMiniXMark } from "react-icons/hi2";
+import { useTabsStore } from "renderer/stores/tabs";
+
+const TAB_TYPE = "TAB";
+
+interface TabItemProps {
+ id: string;
+ title: string;
+ isActive: boolean;
+ index: number;
+ width: number;
+ onMouseEnter?: () => void;
+ onMouseLeave?: () => void;
+}
+
+export function TabItem({
+ id,
+ title,
+ isActive,
+ index,
+ width,
+ onMouseEnter,
+ onMouseLeave,
+}: TabItemProps) {
+ const { setActiveTab, removeTab, reorderTabs } = useTabsStore();
+
+ const [{ isDragging }, drag] = useDrag(
+ () => ({
+ type: TAB_TYPE,
+ item: { id, index },
+ collect: (monitor) => ({
+ isDragging: monitor.isDragging(),
+ }),
+ }),
+ [id, index],
+ );
+
+ const [, drop] = useDrop({
+ accept: TAB_TYPE,
+ hover: (item: { id: string; index: number }) => {
+ if (item.index !== index) {
+ reorderTabs(item.index, index);
+ item.index = index;
+ }
+ },
+ });
+
+ return (
+
+ {/* Active tab bottom border overlay */}
+ {isActive &&
}
+
+ {/* Main tab button */}
+
{
+ drag(drop(node));
+ }}
+ onClick={() => setActiveTab(id)}
+ onMouseEnter={onMouseEnter}
+ onMouseLeave={onMouseLeave}
+ className={`
+ flex items-center gap-0.5 rounded-t-md transition-all w-full shrink-0 pr-6 pl-3 h-[80%]
+ ${
+ isActive
+ ? "text-foreground bg-sidebar"
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/30"
+ }
+ ${isDragging ? "opacity-30" : "opacity-100"}
+ `}
+ style={{ cursor: isDragging ? "grabbing" : "grab" }}
+ >
+
+ {title}
+
+
+
+
{
+ e.stopPropagation();
+ removeTab(id);
+ }}
+ className={cn(
+ "mt-1 absolute right-1 top-1/2 -translate-y-1/2 size-5 ",
+ isActive ? "opacity-90" : "opacity-0 group-hover:opacity-90",
+ )}
+ aria-label="Close tab"
+ >
+
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/index.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/index.tsx
new file mode 100644
index 00000000000..39ef8a7de36
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/index.tsx
@@ -0,0 +1,121 @@
+import { Separator } from "@superset/ui/separator";
+import { Fragment, useEffect, useRef, useState } from "react";
+import { useTabsStore } from "renderer/stores/tabs";
+import { AddTabButton } from "./AddTabButton";
+import { TabItem } from "./TabItem";
+
+const MIN_TAB_WIDTH = 60;
+const MAX_TAB_WIDTH = 240;
+const ADD_BUTTON_WIDTH = 48;
+
+export function Tabs() {
+ const { tabs, activeTabId } = useTabsStore();
+ const containerRef = useRef(null);
+ const scrollRef = useRef(null);
+ const [showStartFade, setShowStartFade] = useState(false);
+ const [showEndFade, setShowEndFade] = useState(false);
+ const [tabWidth, setTabWidth] = useState(MAX_TAB_WIDTH);
+ const [hoveredTabId, setHoveredTabId] = useState(null);
+
+ useEffect(() => {
+ const checkScroll = () => {
+ if (!scrollRef.current) return;
+
+ const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current;
+ setShowStartFade(scrollLeft > 0);
+ setShowEndFade(scrollLeft < scrollWidth - clientWidth - 1);
+ };
+
+ const updateTabWidth = () => {
+ if (!containerRef.current) return;
+
+ const containerWidth = containerRef.current.offsetWidth;
+ const availableWidth = containerWidth - ADD_BUTTON_WIDTH;
+
+ // Calculate width: fill available space but respect min/max
+ const calculatedWidth = Math.max(
+ MIN_TAB_WIDTH,
+ Math.min(MAX_TAB_WIDTH, availableWidth / tabs.length),
+ );
+ setTabWidth(calculatedWidth);
+ };
+
+ checkScroll();
+ updateTabWidth();
+
+ const scrollElement = scrollRef.current;
+ if (scrollElement) {
+ scrollElement.addEventListener("scroll", checkScroll);
+ }
+
+ window.addEventListener("resize", updateTabWidth);
+
+ return () => {
+ if (scrollElement) {
+ scrollElement.removeEventListener("scroll", checkScroll);
+ }
+ window.removeEventListener("resize", updateTabWidth);
+ };
+ }, [tabs]);
+
+ return (
+
+
+
+ {tabs.map((tab, index) => {
+ const nextTab = tabs[index + 1];
+ const isActive = tab.id === activeTabId;
+ const isNextActive = nextTab?.id === activeTabId;
+ const isHovered = tab.id === hoveredTabId;
+ const isNextHovered = nextTab?.id === hoveredTabId;
+ const separatorOpacity =
+ !isActive && !isNextActive && !isHovered && !isNextHovered
+ ? 100
+ : 0;
+
+ return (
+
+
+ setHoveredTabId(tab.id)}
+ onMouseLeave={() => setHoveredTabId(null)}
+ />
+
+ {index < tabs.length - 1 && (
+
+
+
+ )}
+
+ );
+ })}
+
+
+ {/* Fade effects for scroll indication */}
+ {showStartFade && (
+
+ )}
+ {showEndFade && (
+
+ )}
+
+
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/WindowControls.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/WindowControls.tsx
index 555813d8e66..272bdd77acb 100644
--- a/apps/desktop/src/renderer/screens/main/components/TopBar/WindowControls.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/TopBar/WindowControls.tsx
@@ -1,4 +1,4 @@
-import { Minus, Square, X } from "lucide-react";
+import { HiMiniMinus, HiMiniStop, HiMiniXMark } from "react-icons/hi2";
import { trpc } from "renderer/lib/trpc";
export function WindowControls() {
@@ -25,21 +25,21 @@ export function WindowControls() {
className="h-full w-12 flex items-center justify-center hover:bg-accent transition-colors"
onClick={handleMinimize}
>
-
+
-
+
-
+
);
diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/index.ts b/apps/desktop/src/renderer/screens/main/components/TopBar/index.ts
deleted file mode 100644
index 7cc3ff0450b..00000000000
--- a/apps/desktop/src/renderer/screens/main/components/TopBar/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { TopBar } from "./TopBar";
diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/TopBar.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/index.tsx
similarity index 60%
rename from apps/desktop/src/renderer/screens/main/components/TopBar/TopBar.tsx
rename to apps/desktop/src/renderer/screens/main/components/TopBar/index.tsx
index d1141845867..12814cf85c9 100644
--- a/apps/desktop/src/renderer/screens/main/components/TopBar/TopBar.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/TopBar/index.tsx
@@ -1,20 +1,24 @@
import { trpc } from "renderer/lib/trpc";
+import { SidebarControl } from "./SidebarControl";
+import { Tabs } from "./Tabs";
import { WindowControls } from "./WindowControls";
export function TopBar() {
const { data: platform } = trpc.window.getPlatform.useQuery();
const isMac = platform === "darwin";
return (
-
+
-
Superset
+
+
+
+
-
Middle section
{!isMac && }
diff --git a/apps/desktop/src/renderer/stores/README.md b/apps/desktop/src/renderer/stores/README.md
new file mode 100644
index 00000000000..e9aa8c2caed
--- /dev/null
+++ b/apps/desktop/src/renderer/stores/README.md
@@ -0,0 +1,191 @@
+# Zustand Stores
+
+This directory contains Zustand stores for React state management in the renderer process.
+
+## Quick Start
+
+```typescript
+// 1. Create a store
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+
+interface MyState {
+ value: string;
+ setValue: (value: string) => void;
+}
+
+export const useMyStore = create
()(
+ devtools(
+ (set) => ({
+ value: '',
+ setValue: (value) => set({ value }),
+ }),
+ { name: 'MyStore' }
+ )
+);
+
+// 2. Use in component
+import { useMyStore } from 'renderer/stores';
+
+function MyComponent() {
+ const value = useMyStore((state) => state.value);
+ const setValue = useMyStore((state) => state.setValue);
+
+ return setValue(e.target.value)} />;
+}
+```
+
+## Best Practices
+
+### 1. TypeScript First
+Always define interfaces for your stores:
+```typescript
+interface UserState {
+ user: User | null;
+ setUser: (user: User) => void;
+ clearUser: () => void;
+}
+```
+
+### 2. Use Selectors for Performance
+Create selector hooks to prevent unnecessary re-renders:
+```typescript
+// In store file
+export const useUser = () => useUserStore((state) => state.user);
+export const useUserName = () => useUserStore((state) => state.user?.name);
+
+// In component - only re-renders when name changes
+const userName = useUserName();
+```
+
+### 3. DevTools Integration
+Always use devtools middleware in development:
+```typescript
+export const useMyStore = create()(
+ devtools(
+ (set) => ({ /* store definition */ }),
+ { name: 'MyStore' } // Shows in Redux DevTools
+ )
+);
+```
+
+### 4. Persistence (Optional)
+Use persist middleware for localStorage persistence:
+```typescript
+import { persist } from 'zustand/middleware';
+
+export const useMyStore = create()(
+ devtools(
+ persist(
+ (set) => ({ /* store definition */ }),
+ { name: 'my-store-key' } // localStorage key
+ ),
+ { name: 'MyStore' }
+ )
+);
+```
+
+### 5. Organize Actions
+Group related actions together:
+```typescript
+export const useTaskStore = create()(
+ devtools((set) => ({
+ tasks: [],
+
+ // Add actions
+ addTask: (task) => set((state) => ({
+ tasks: [...state.tasks, task]
+ })),
+
+ // Update actions
+ updateTask: (id, updates) => set((state) => ({
+ tasks: state.tasks.map(t => t.id === id ? { ...t, ...updates } : t)
+ })),
+
+ // Remove actions
+ removeTask: (id) => set((state) => ({
+ tasks: state.tasks.filter(t => t.id !== id)
+ })),
+
+ // Bulk actions
+ clearCompleted: () => set((state) => ({
+ tasks: state.tasks.filter(t => !t.completed)
+ })),
+ }))
+);
+```
+
+### 6. Async Actions
+Handle async operations within actions:
+```typescript
+export const useDataStore = create()(
+ devtools((set) => ({
+ data: null,
+ loading: false,
+ error: null,
+
+ fetchData: async () => {
+ set({ loading: true, error: null });
+ try {
+ const data = await api.getData();
+ set({ data, loading: false });
+ } catch (error) {
+ set({ error, loading: false });
+ }
+ },
+ }))
+);
+```
+
+### 7. Slice Pattern for Large Stores
+Split large stores into slices:
+```typescript
+interface UserSlice {
+ user: User | null;
+ setUser: (user: User) => void;
+}
+
+interface SettingsSlice {
+ theme: 'light' | 'dark';
+ setTheme: (theme: 'light' | 'dark') => void;
+}
+
+type AppState = UserSlice & SettingsSlice;
+
+const createUserSlice = (set): UserSlice => ({
+ user: null,
+ setUser: (user) => set({ user }),
+});
+
+const createSettingsSlice = (set): SettingsSlice => ({
+ theme: 'light',
+ setTheme: (theme) => set({ theme }),
+});
+
+export const useAppStore = create()(
+ devtools(
+ (...a) => ({
+ ...createUserSlice(...a),
+ ...createSettingsSlice(...a),
+ })
+ )
+);
+```
+
+## When to Use Zustand vs tRPC
+
+- **Zustand**: Local UI state (sidebar open/closed, active tab, theme preferences)
+- **tRPC**: Server state and IPC communication with Electron main process
+- **Combine them**: Use tRPC queries to fetch data, store UI state in Zustand
+
+## Debugging
+
+1. Install Redux DevTools browser extension
+2. Stores with `devtools()` middleware will appear in the extension
+3. You can inspect state changes, time-travel, and dispatch actions
+
+## Resources
+
+- [Zustand Documentation](https://zustand.docs.pmnd.rs/)
+- [TypeScript Guide](https://zustand.docs.pmnd.rs/guides/typescript)
+- [Persisting Store](https://zustand.docs.pmnd.rs/integrations/persisting-store-data)
diff --git a/apps/desktop/src/renderer/stores/example-store.ts b/apps/desktop/src/renderer/stores/example-store.ts
new file mode 100644
index 00000000000..38567b35339
--- /dev/null
+++ b/apps/desktop/src/renderer/stores/example-store.ts
@@ -0,0 +1,54 @@
+import { create } from "zustand";
+import { devtools } from "zustand/middleware";
+
+/**
+ * Example store demonstrating Zustand usage patterns
+ *
+ * Features:
+ * - TypeScript type safety
+ * - DevTools integration for debugging
+ * - Optional persistence (commented out by default)
+ */
+
+interface ExampleState {
+ // State
+ count: number;
+ text: string;
+
+ // Actions
+ increment: () => void;
+ decrement: () => void;
+ setText: (text: string) => void;
+ reset: () => void;
+}
+
+const initialState = {
+ count: 0,
+ text: "",
+};
+
+export const useExampleStore = create()(
+ devtools(
+ // Uncomment persist middleware if you need localStorage persistence
+ // persist(
+ (set) => ({
+ ...initialState,
+
+ increment: () => set((state) => ({ count: state.count + 1 })),
+ decrement: () => set((state) => ({ count: state.count - 1 })),
+ setText: (text: string) => set({ text }),
+ reset: () => set(initialState),
+ }),
+ // {
+ // name: "example-store", // localStorage key
+ // }
+ // ),
+ {
+ name: "ExampleStore", // DevTools name
+ },
+ ),
+);
+
+// Selectors for optimized re-renders
+export const useCount = () => useExampleStore((state) => state.count);
+export const useText = () => useExampleStore((state) => state.text);
diff --git a/apps/desktop/src/renderer/stores/index.ts b/apps/desktop/src/renderer/stores/index.ts
new file mode 100644
index 00000000000..7443ba32acf
--- /dev/null
+++ b/apps/desktop/src/renderer/stores/index.ts
@@ -0,0 +1,8 @@
+/**
+ * Zustand stores index
+ *
+ * Export all stores from this file for easy importing
+ */
+
+export * from "./sidebar-state";
+export * from "./tabs";
diff --git a/apps/desktop/src/renderer/stores/sidebar-state.ts b/apps/desktop/src/renderer/stores/sidebar-state.ts
new file mode 100644
index 00000000000..36c276a8dc7
--- /dev/null
+++ b/apps/desktop/src/renderer/stores/sidebar-state.ts
@@ -0,0 +1,25 @@
+import { create } from "zustand";
+import { devtools } from "zustand/middleware";
+
+interface SidebarState {
+ isSidebarOpen: boolean;
+ toggleSidebar: () => void;
+ setSidebarOpen: (open: boolean) => void;
+}
+
+export const useSidebarStore = create()(
+ devtools(
+ (set) => ({
+ isSidebarOpen: true,
+
+ toggleSidebar: () => {
+ set((state) => ({ isSidebarOpen: !state.isSidebarOpen }));
+ },
+
+ setSidebarOpen: (open) => {
+ set({ isSidebarOpen: open });
+ },
+ }),
+ { name: "SidebarStore" },
+ ),
+);
diff --git a/apps/desktop/src/renderer/stores/tabs.ts b/apps/desktop/src/renderer/stores/tabs.ts
new file mode 100644
index 00000000000..e1dc5fe8cf4
--- /dev/null
+++ b/apps/desktop/src/renderer/stores/tabs.ts
@@ -0,0 +1,71 @@
+import { create } from "zustand";
+import { devtools } from "zustand/middleware";
+
+export interface Tab {
+ id: string;
+ title: string;
+}
+
+interface TabsState {
+ tabs: Tab[];
+ activeTabId: string | null;
+
+ addTab: () => void;
+ removeTab: (id: string) => void;
+ setActiveTab: (id: string) => void;
+ reorderTabs: (startIndex: number, endIndex: number) => void;
+}
+
+const createNewTab = (): Tab => ({
+ id: `tab-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
+ title: "New Tab",
+});
+
+export const useTabsStore = create()(
+ devtools(
+ (set) => ({
+ tabs: [{ id: "tab-1", title: "Home" }],
+ activeTabId: "tab-1",
+
+ addTab: () => {
+ const newTab = createNewTab();
+ set((state) => ({
+ tabs: [...state.tabs, newTab],
+ activeTabId: newTab.id,
+ }));
+ },
+
+ removeTab: (id) => {
+ set((state) => {
+ const tabs = state.tabs.filter((tab) => tab.id !== id);
+ if (tabs.length === 0) {
+ const newTab = createNewTab();
+ return { tabs: [newTab], activeTabId: newTab.id };
+ }
+
+ if (id === state.activeTabId) {
+ const closedIndex = state.tabs.findIndex((tab) => tab.id === id);
+ const nextTab = tabs[closedIndex] || tabs[closedIndex - 1];
+ return { tabs, activeTabId: nextTab.id };
+ }
+
+ return { tabs };
+ });
+ },
+
+ setActiveTab: (id) => {
+ set({ activeTabId: id });
+ },
+
+ reorderTabs: (startIndex, endIndex) => {
+ set((state) => {
+ const tabs = [...state.tabs];
+ const [removed] = tabs.splice(startIndex, 1);
+ tabs.splice(endIndex, 0, removed);
+ return { tabs };
+ });
+ },
+ }),
+ { name: "TabsStore" },
+ ),
+);
diff --git a/apps/desktop/src/shared/ipc-channels/index.ts b/apps/desktop/src/shared/ipc-channels/index.ts
index 02d1ae0acd6..7eb575807cf 100644
--- a/apps/desktop/src/shared/ipc-channels/index.ts
+++ b/apps/desktop/src/shared/ipc-channels/index.ts
@@ -12,8 +12,8 @@ import type { TabChannels } from "./tab";
import type { TerminalChannels } from "./terminal";
import type { UiChannels } from "./ui";
import type { WindowChannels } from "./window";
-import type { WorktreeChannels } from "./worktree";
import type { WorkspaceChannels } from "./workspace";
+import type { WorktreeChannels } from "./worktree";
// Re-export shared types
export type {
diff --git a/apps/desktop/src/shared/ipc-channels/ui.ts b/apps/desktop/src/shared/ipc-channels/ui.ts
index d25823a11e5..e580c7390f2 100644
--- a/apps/desktop/src/shared/ipc-channels/ui.ts
+++ b/apps/desktop/src/shared/ipc-channels/ui.ts
@@ -2,8 +2,8 @@
* UI-related IPC channels for Desktop app
*/
+import type { MosaicNode, Tab } from "../types";
import type { IpcResponse, NoRequest } from "./types";
-import type { Tab, MosaicNode } from "../types";
export interface UiChannels {
// Workspace UI state
diff --git a/bun.lock b/bun.lock
index a224c8b5621..e60ff3d47d9 100644
--- a/bun.lock
+++ b/bun.lock
@@ -94,13 +94,13 @@
"framer-motion": "^12.23.24",
"http-proxy": "^1.18.1",
"lowdb": "^7.0.1",
- "lucide-react": "^0.553.0",
"node-pty": "1.1.0-beta30",
"react": "^19.1.1",
"react-arborist": "^3.4.3",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.1.1",
+ "react-icons": "^5.5.0",
"react-mosaic-component": "^6.1.1",
"react-resizable-panels": "^3.0.6",
"react-router-dom": "^7.8.2",
@@ -108,6 +108,7 @@
"superjson": "^2.2.5",
"tailwind-merge": "^2.6.0",
"trpc-electron": "^0.1.2",
+ "zustand": "^5.0.8",
},
"devDependencies": {
"@biomejs/biome": "^2.2.6",
@@ -355,11 +356,12 @@
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
- "lucide-react": "^0.553.0",
+ "react-icons": "^5.5.0",
"react-resizable-panels": "^3.0.6",
"tailwind-merge": "^2.6.0",
},
@@ -890,6 +892,8 @@
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
+ "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
+
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
@@ -3066,6 +3070,8 @@
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
+ "react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="],
+
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"react-medium-image-zoom": ["react-medium-image-zoom@5.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-BsE+EnFVQzFIlyuuQrZ9iTwyKpKkqdFZV1ImEQN573QPqGrIUuNni7aF+sZwDcxlsuOMayCr6oO/PZR/yJnbRg=="],
@@ -3794,6 +3800,8 @@
"@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+ "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
+
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@react-three/drei/cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="],
diff --git a/packages/ui/package.json b/packages/ui/package.json
index eb01e898a89..c02a1404edf 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -19,6 +19,7 @@
"./resizable": "./src/components/resizable.tsx",
"./label": "./src/components/label.tsx",
"./select": "./src/components/select.tsx",
+ "./separator": "./src/components/separator.tsx",
"./textarea": "./src/components/textarea.tsx",
"./utils": "./src/lib/utils.ts",
"./hooks": "./src/hooks/index.ts",
@@ -35,11 +36,12 @@
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
- "lucide-react": "^0.553.0",
+ "react-icons": "^5.5.0",
"react-resizable-panels": "^3.0.6",
"tailwind-merge": "^2.6.0"
},
diff --git a/packages/ui/src/components/context-menu.tsx b/packages/ui/src/components/context-menu.tsx
index d186942f0b1..53a55fb9c57 100644
--- a/packages/ui/src/components/context-menu.tsx
+++ b/packages/ui/src/components/context-menu.tsx
@@ -1,5 +1,5 @@
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
-import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
+import { HiMiniCheck, HiMiniChevronRight, HiMiniStop } from "react-icons/hi2";
import type * as React from "react";
import { cn } from "../lib/utils";
@@ -76,7 +76,7 @@ function ContextMenuSubTrigger({
{...props}
>
{children}
-
+
);
}
@@ -156,7 +156,7 @@ function ContextMenuCheckboxItem({
>
-
+
{children}
@@ -180,7 +180,7 @@ function ContextMenuRadioItem({
>
-
+
{children}
diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx
index 20a846728c9..0680ed61b4f 100644
--- a/packages/ui/src/components/dialog.tsx
+++ b/packages/ui/src/components/dialog.tsx
@@ -1,5 +1,5 @@
import * as DialogPrimitive from "@radix-ui/react-dialog";
-import { XIcon } from "lucide-react";
+import { HiMiniXMark } from "react-icons/hi2";
import type * as React from "react";
import { cn } from "../lib/utils";
@@ -70,7 +70,7 @@ function DialogContent({
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
-
+
Close
)}
diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu.tsx
index 9a1ea6a2752..43d40d3794c 100644
--- a/packages/ui/src/components/dropdown-menu.tsx
+++ b/packages/ui/src/components/dropdown-menu.tsx
@@ -1,5 +1,5 @@
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
-import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
+import { HiMiniCheck, HiMiniChevronRight, HiMiniStop } from "react-icons/hi2";
import type * as React from "react";
import { cn } from "../lib/utils";
@@ -103,7 +103,7 @@ function DropdownMenuCheckboxItem({
>
-
+
{children}
@@ -138,7 +138,7 @@ function DropdownMenuRadioItem({
>
-
+
{children}
@@ -220,7 +220,7 @@ function DropdownMenuSubTrigger({
{...props}
>
{children}
-
+
);
}
diff --git a/packages/ui/src/components/resizable.tsx b/packages/ui/src/components/resizable.tsx
index 8dc97d79ea7..a4225e4cad4 100644
--- a/packages/ui/src/components/resizable.tsx
+++ b/packages/ui/src/components/resizable.tsx
@@ -1,4 +1,4 @@
-import { GripVerticalIcon } from "lucide-react";
+import { HiMiniEllipsisVertical } from "react-icons/hi2";
import type * as React from "react";
import * as ResizablePrimitive from "react-resizable-panels";
@@ -44,7 +44,7 @@ function ResizableHandle({
>
{withHandle && (
-
+
)}
diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx
index 65d9c6b2750..205251792d5 100644
--- a/packages/ui/src/components/select.tsx
+++ b/packages/ui/src/components/select.tsx
@@ -1,7 +1,7 @@
"use client";
import * as SelectPrimitive from "@radix-ui/react-select";
-import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
+import { HiMiniCheck, HiMiniChevronDown, HiMiniChevronUp } from "react-icons/hi2";
import type * as React from "react";
import { cn } from "../lib/utils";
@@ -44,7 +44,7 @@ function SelectTrigger({
>
{children}
-
+
);
@@ -116,7 +116,7 @@ function SelectItem({
>
-
+
{children}
@@ -150,7 +150,7 @@ function SelectScrollUpButton({
)}
{...props}
>
-
+
);
}
@@ -168,7 +168,7 @@ function SelectScrollDownButton({
)}
{...props}
>
-
+
);
}
diff --git a/packages/ui/src/components/separator.tsx b/packages/ui/src/components/separator.tsx
new file mode 100644
index 00000000000..d57e7e2272f
--- /dev/null
+++ b/packages/ui/src/components/separator.tsx
@@ -0,0 +1,25 @@
+import * as React from "react";
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
+import { cn } from "../lib/utils";
+
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Separator };