Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.PHONY: dev build lint format test test-watch test-e2e build-release clean

dev: ## Start dev mode with isolated DB (safe to run alongside prod app)
xattr -cr node_modules/electron/dist/Electron.app 2>/dev/null || true
E2E_DATA_DIR=$(HOME)/.codez-dev npm run dev

build:
Expand Down
129 changes: 86 additions & 43 deletions src/renderer/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useSessionShortcuts } from "../../hooks/useChordShortcuts";
import { useRepoStore } from "../../stores/repoStore";
import { useSessionStore } from "../../stores/sessionStore";
import { useThemeStore } from "../../stores/themeStore";
import { NewSessionDialog } from "./NewSessionDialog";
import { SessionListItem, type SessionListItemProps } from "./SessionListItem";
import { WorktreeDeleteDialog } from "./WorktreeDeleteDialog";
Expand Down Expand Up @@ -34,6 +35,9 @@ export function Sidebar() {

const reorderSessions = useSessionStore((state) => state.reorderSessions);

const collapsed = useThemeStore((state) => state.sidebarCollapsed);
const toggleSidebar = useThemeStore((state) => state.toggleSidebar);

const [archiveOpen, setArchiveOpen] = useState(false);
const [metaHeld, setMetaHeld] = useState(false);
const repoPickerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -175,62 +179,83 @@ export function Sidebar() {
]);

return (
<aside className="w-60 border-r border-white/[0.06] bg-transparent flex flex-col">
<aside
className={`${collapsed ? "w-10" : "w-60"} border-r border-white/[0.06] bg-transparent flex flex-col transition-[width] duration-200 ease-in-out overflow-hidden`}
>
{/* Draggable title bar + header */}
<div className="h-12 flex items-center px-4 [-webkit-app-region:drag]">
<span className="text-[13px] font-medium text-text-muted ml-16 flex-1">Codez</span>
<div className="[-webkit-app-region:no-drag]" ref={repoPickerRef}>
<div className="h-12 flex items-center px-2 [-webkit-app-region:drag] shrink-0">
{!collapsed && (
<span className="text-[13px] font-medium text-text-muted ml-16 flex-1 whitespace-nowrap overflow-hidden">
Codez
</span>
)}
<div
className={`flex items-center gap-1 [-webkit-app-region:no-drag] ${collapsed ? "mx-auto" : ""}`}
ref={repoPickerRef}
>
{!collapsed && (
<button
type="button"
onClick={handleNewSessionClick}
className="w-6 h-6 flex items-center justify-center rounded-md text-text-muted hover:text-text-primary hover:bg-white/10 transition-colors"
title="New session"
>
<PlusIcon />
</button>
)}
<button
type="button"
onClick={handleNewSessionClick}
onClick={toggleSidebar}
className="w-6 h-6 flex items-center justify-center rounded-md text-text-muted hover:text-text-primary hover:bg-white/10 transition-colors"
title="New session"
title={collapsed ? "Expand sidebar (⌘B)" : "Collapse sidebar (⌘B)"}
>
<PlusIcon />
<CollapseIcon collapsed={collapsed} />
</button>
</div>
</div>

{/* Session list — sorted by user-defined order */}
<div className="flex-1 overflow-y-auto px-1.5 py-1 space-y-0.5">
{sessions.length === 0 ? (
<div className="flex items-center justify-center h-32">
<p className="text-xs text-text-muted">
{repos.length === 0 ? "Add a folder to get started" : "No sessions yet"}
</p>
</div>
) : (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={sessions.map((s) => s.id)} strategy={verticalListSortingStrategy}>
{sessions.map((session, index) => (
<SortableSessionItem
key={session.id}
session={session}
isActive={session.id === activeSessionId}
onClick={() => setActiveSession(session.id)}
onArchive={() => handleArchiveSession(session)}
branchName={session.branchName ?? branches.get(session.repoPath)}
shortcutNumber={metaHeld && index < 9 ? index + 1 : null}
/>
))}
</SortableContext>
</DndContext>
)}
{!collapsed && (
<div className="flex-1 overflow-y-auto px-1.5 py-1 space-y-0.5">
{sessions.length === 0 ? (
<div className="flex items-center justify-center h-32">
<p className="text-xs text-text-muted">
{repos.length === 0 ? "Add a folder to get started" : "No sessions yet"}
</p>
</div>
) : (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={sessions.map((s) => s.id)} strategy={verticalListSortingStrategy}>
{sessions.map((session, index) => (
<SortableSessionItem
key={session.id}
session={session}
isActive={session.id === activeSessionId}
onClick={() => setActiveSession(session.id)}
onArchive={() => handleArchiveSession(session)}
branchName={session.branchName ?? branches.get(session.repoPath)}
shortcutNumber={metaHeld && index < 9 ? index + 1 : null}
/>
))}
</SortableContext>
</DndContext>
)}

{/* Add folder link — only when no repos exist */}
{repos.length === 0 && (
<button
type="button"
onClick={addRepoViaDialog}
className="w-full text-left px-3 py-1.5 text-xs text-text-muted hover:text-accent transition-colors [-webkit-app-region:no-drag]"
>
+ Add folder...
</button>
)}
</div>
{/* Add folder link — only when no repos exist */}
{repos.length === 0 && (
<button
type="button"
onClick={addRepoViaDialog}
className="w-full text-left px-3 py-1.5 text-xs text-text-muted hover:text-accent transition-colors [-webkit-app-region:no-drag]"
>
+ Add folder...
</button>
)}
</div>
)}

{/* Archive accordion */}
{archivedSessions.length > 0 && (
{!collapsed && archivedSessions.length > 0 && (
<div className="border-t border-white/[0.06]">
<button
type="button"
Expand Down Expand Up @@ -306,6 +331,24 @@ function PlusIcon() {
);
}

function CollapseIcon({ collapsed }: { collapsed: boolean }) {
return (
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={`transition-transform ${collapsed ? "rotate-180" : ""}`}
>
<polyline points="15 18 9 12 15 6" />
</svg>
);
}

function ChevronIcon({ open }: { open: boolean }) {
return (
<svg
Expand Down
12 changes: 11 additions & 1 deletion src/renderer/hooks/useGlobalShortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ export function isNewSessionShortcut(event: KeyboardEvent): boolean {
return event.key === "n" && event.metaKey && !event.shiftKey && !event.altKey && !event.ctrlKey;
}

export function isSidebarToggleShortcut(event: KeyboardEvent): boolean {
return event.key === "b" && event.metaKey && !event.shiftKey && !event.altKey && !event.ctrlKey;
}

export function useGlobalShortcuts(): void {
const toggleSettings = useThemeStore((state) => state.toggleSettings);
const toggleSidebar = useThemeStore((state) => state.toggleSidebar);
const setPendingNewSessionRepo = useSessionStore((state) => state.setPendingNewSessionRepo);
const addRepoViaDialog = useRepoStore((state) => state.addRepoViaDialog);

Expand All @@ -30,9 +35,14 @@ export function useGlobalShortcuts(): void {
setPendingNewSessionRepo(repo);
}
}

if (isSidebarToggleShortcut(event)) {
event.preventDefault();
toggleSidebar();
}
}

window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSettings, setPendingNewSessionRepo, addRepoViaDialog]);
}, [toggleSettings, toggleSidebar, setPendingNewSessionRepo, addRepoViaDialog]);
}
7 changes: 7 additions & 0 deletions src/renderer/stores/themeStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,28 @@ export function applyThemeToElement(theme: ThemeDefinition, element: HTMLElement
interface ThemeState {
activeThemeId: ThemeId;
settingsOpen: boolean;
sidebarCollapsed: boolean;

toggleSettings: () => void;
closeSettings: () => void;
toggleSidebar: () => void;
setTheme: (id: ThemeId) => void;
loadTheme: () => Promise<void>;
}

export const useThemeStore = create<ThemeState>((set) => ({
activeThemeId: DEFAULT_THEME_ID,
settingsOpen: false,
sidebarCollapsed: false,

toggleSettings: () => {
set((state) => ({ settingsOpen: !state.settingsOpen }));
},

toggleSidebar: () => {
set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed }));
},

closeSettings: () => {
set({ settingsOpen: false });
},
Expand Down
Loading