diff --git a/app/src/components/layout/Sidebar.test.tsx b/app/src/components/layout/Sidebar.test.tsx
index 5800c91..be5c22c 100644
--- a/app/src/components/layout/Sidebar.test.tsx
+++ b/app/src/components/layout/Sidebar.test.tsx
@@ -8,6 +8,9 @@ vi.mock("../../store/appState", () => ({
selector({
sidebarView: "projects",
setSidebarView: vi.fn(),
+ sidebarCollapsed: false,
+ setSidebarCollapsed: vi.fn(),
+ toggleSidebarCollapsed: vi.fn(),
})
),
}));
diff --git a/app/src/components/layout/Sidebar.tsx b/app/src/components/layout/Sidebar.tsx
index 2eb6ac6..2c3bbfc 100644
--- a/app/src/components/layout/Sidebar.tsx
+++ b/app/src/components/layout/Sidebar.tsx
@@ -1,15 +1,100 @@
+import type { ReactNode } from "react";
import { useShallow } from "zustand/react/shallow";
import { useAppState } from "../../store/appState";
import ProjectList from "../projects/ProjectList";
import McpPanel from "../mcp/McpPanel";
import SettingsPanel from "../settings/SettingsPanel";
+type SidebarView = "projects" | "mcp" | "settings";
+
+const RAIL_ICONS: { view: SidebarView; label: string; icon: ReactNode }[] = [
+ {
+ view: "projects",
+ label: "Projects",
+ icon: (
+
+ ),
+ },
+ {
+ view: "mcp",
+ label: "MCP",
+ icon: (
+
+ ),
+ },
+ {
+ view: "settings",
+ label: "Settings",
+ icon: (
+
+ ),
+ },
+];
+
export default function Sidebar() {
- const { sidebarView, setSidebarView } = useAppState(
- useShallow(s => ({ sidebarView: s.sidebarView, setSidebarView: s.setSidebarView }))
+ const { sidebarView, setSidebarView, sidebarCollapsed, setSidebarCollapsed, toggleSidebarCollapsed } = useAppState(
+ useShallow(s => ({
+ sidebarView: s.sidebarView,
+ setSidebarView: s.setSidebarView,
+ sidebarCollapsed: s.sidebarCollapsed,
+ setSidebarCollapsed: s.setSidebarCollapsed,
+ toggleSidebarCollapsed: s.toggleSidebarCollapsed,
+ }))
);
- const tabCls = (view: typeof sidebarView) =>
+ if (sidebarCollapsed) {
+ const railBtn = (view: SidebarView, label: string, icon: ReactNode) => {
+ const active = sidebarView === view;
+ return (
+
+ );
+ };
+
+ return (
+
+
+
+ {RAIL_ICONS.map(({ view, label, icon }) => railBtn(view, label, icon))}
+
+
+ );
+ }
+
+ const tabCls = (view: SidebarView) =>
`flex-1 px-3 py-2 text-sm font-medium transition-colors ${
sidebarView === view
? "text-[var(--accent)] border-b-2 border-[var(--accent)]"
@@ -29,6 +114,16 @@ export default function Sidebar() {
+
{/* Content */}
diff --git a/app/src/store/appState.ts b/app/src/store/appState.ts
index 37de139..bf9cdb0 100644
--- a/app/src/store/appState.ts
+++ b/app/src/store/appState.ts
@@ -1,6 +1,24 @@
import { create } from "zustand";
import type { Project, TerminalSession, AppSettings, UpdateInfo, ImageUpdateInfo, McpServer } from "../lib/types";
+const SIDEBAR_COLLAPSED_KEY = "triple-c.sidebar.collapsed";
+
+function loadSidebarCollapsed(): boolean {
+ try {
+ return localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === "1";
+ } catch {
+ return false;
+ }
+}
+
+function persistSidebarCollapsed(value: boolean) {
+ try {
+ localStorage.setItem(SIDEBAR_COLLAPSED_KEY, value ? "1" : "0");
+ } catch {
+ // ignore — storage may be unavailable
+ }
+}
+
interface AppState {
// Projects
projects: Project[];
@@ -28,6 +46,9 @@ interface AppState {
setTerminalHasSelection: (has: boolean) => void;
sidebarView: "projects" | "mcp" | "settings";
setSidebarView: (view: "projects" | "mcp" | "settings") => void;
+ sidebarCollapsed: boolean;
+ setSidebarCollapsed: (collapsed: boolean) => void;
+ toggleSidebarCollapsed: () => void;
dockerAvailable: boolean | null;
setDockerAvailable: (available: boolean | null) => void;
imageExists: boolean | null;
@@ -106,6 +127,17 @@ export const useAppState = create((set) => ({
setTerminalHasSelection: (has) => set({ terminalHasSelection: has }),
sidebarView: "projects",
setSidebarView: (view) => set({ sidebarView: view }),
+ sidebarCollapsed: loadSidebarCollapsed(),
+ setSidebarCollapsed: (collapsed) => {
+ persistSidebarCollapsed(collapsed);
+ set({ sidebarCollapsed: collapsed });
+ },
+ toggleSidebarCollapsed: () =>
+ set((state) => {
+ const next = !state.sidebarCollapsed;
+ persistSidebarCollapsed(next);
+ return { sidebarCollapsed: next };
+ }),
dockerAvailable: null,
setDockerAvailable: (available) => set({ dockerAvailable: available }),
imageExists: null,