2025-12-07 19:32:53 +02:00
|
|
|
import React from 'react';
|
2026-01-03 13:39:59 +02:00
|
|
|
import type { Session } from '@opencode-ai/sdk/v2';
|
2026-01-19 02:50:40 +02:00
|
|
|
import { toast } from '@/components/ui';
|
2026-02-11 20:07:44 +02:00
|
|
|
import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell } from '@/lib/desktop';
|
2026-02-11 23:53:37 -08:00
|
|
|
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
|
2025-12-07 19:32:53 +02:00
|
|
|
import { sessionEvents } from '@/lib/sessionEvents';
|
2026-03-06 17:22:59 +02:00
|
|
|
import { formatDirectoryName, cn } from '@/lib/utils';
|
2025-12-07 19:32:53 +02:00
|
|
|
import { useSessionStore } from '@/stores/useSessionStore';
|
|
|
|
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
2026-01-06 21:31:04 +02:00
|
|
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
2025-12-21 20:18:51 +02:00
|
|
|
import { useUIStore } from '@/stores/useUIStore';
|
2026-01-08 01:48:42 +02:00
|
|
|
import { useConfigStore } from '@/stores/useConfigStore';
|
2026-03-06 17:22:59 +02:00
|
|
|
import type { GitHubPullRequestStatus } from '@/lib/api/types';
|
2025-12-07 19:32:53 +02:00
|
|
|
import { getSafeStorage } from '@/stores/utils/safeStorage';
|
2026-03-03 00:20:15 +02:00
|
|
|
import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
|
2026-02-08 16:51:05 -08:00
|
|
|
import { useGitStore } from '@/stores/useGitStore';
|
2026-02-11 23:53:37 -08:00
|
|
|
import { useDeviceInfo } from '@/lib/device';
|
2026-01-08 17:59:36 +02:00
|
|
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
2026-03-03 00:20:15 +02:00
|
|
|
import { NewWorktreeDialog } from './NewWorktreeDialog';
|
2026-02-11 23:53:37 -08:00
|
|
|
import { ProjectNotesTodoPanel } from './ProjectNotesTodoPanel';
|
2026-02-21 06:09:55 +07:00
|
|
|
import { useSessionFoldersStore } from '@/stores/useSessionFoldersStore';
|
2026-03-02 23:08:11 +00:00
|
|
|
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
|
2026-03-06 17:22:59 +02:00
|
|
|
import { useArchivedAutoFolders } from './sidebar/hooks/useArchivedAutoFolders';
|
|
|
|
|
import { useSessionSidebarSections } from './sidebar/hooks/useSessionSidebarSections';
|
|
|
|
|
import { useProjectSessionSelection } from './sidebar/hooks/useProjectSessionSelection';
|
|
|
|
|
import { useGroupOrdering } from './sidebar/hooks/useGroupOrdering';
|
|
|
|
|
import { useSessionGrouping } from './sidebar/hooks/useSessionGrouping';
|
|
|
|
|
import { useSessionSearchEffects } from './sidebar/hooks/useSessionSearchEffects';
|
|
|
|
|
import { useSessionPrefetch } from './sidebar/hooks/useSessionPrefetch';
|
|
|
|
|
import { useDirectoryStatusProbe } from './sidebar/hooks/useDirectoryStatusProbe';
|
|
|
|
|
import { useSessionActions } from './sidebar/hooks/useSessionActions';
|
|
|
|
|
import { useSidebarPersistence } from './sidebar/hooks/useSidebarPersistence';
|
|
|
|
|
import { useProjectRepoStatus } from './sidebar/hooks/useProjectRepoStatus';
|
|
|
|
|
import { useProjectSessionLists } from './sidebar/hooks/useProjectSessionLists';
|
|
|
|
|
import { useSessionFolderCleanup } from './sidebar/hooks/useSessionFolderCleanup';
|
|
|
|
|
import { useStickyProjectHeaders } from './sidebar/hooks/useStickyProjectHeaders';
|
|
|
|
|
import { useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
|
|
|
|
|
import { SessionGroupSection } from './sidebar/SessionGroupSection';
|
|
|
|
|
import { SidebarHeader } from './sidebar/SidebarHeader';
|
|
|
|
|
import { SidebarProjectsList } from './sidebar/SidebarProjectsList';
|
|
|
|
|
import { SessionNodeItem } from './sidebar/SessionNodeItem';
|
|
|
|
|
import {
|
|
|
|
|
FolderDeleteConfirmDialog,
|
|
|
|
|
SessionDeleteConfirmDialog,
|
|
|
|
|
type DeleteFolderConfirmState,
|
|
|
|
|
type DeleteSessionConfirmState,
|
|
|
|
|
} from './sidebar/ConfirmDialogs';
|
|
|
|
|
import { type SessionGroup, type SessionNode } from './sidebar/types';
|
|
|
|
|
import {
|
|
|
|
|
compareSessionsByPinnedAndTime,
|
|
|
|
|
formatProjectLabel,
|
|
|
|
|
normalizePath,
|
|
|
|
|
} from './sidebar/utils';
|
2026-02-07 03:57:46 +02:00
|
|
|
|
2026-01-06 21:31:04 +02:00
|
|
|
const PROJECT_COLLAPSE_STORAGE_KEY = 'oc.sessions.projectCollapse';
|
2026-02-07 03:57:46 +02:00
|
|
|
const GROUP_ORDER_STORAGE_KEY = 'oc.sessions.groupOrder';
|
|
|
|
|
const GROUP_COLLAPSE_STORAGE_KEY = 'oc.sessions.groupCollapse';
|
|
|
|
|
const PROJECT_ACTIVE_SESSION_STORAGE_KEY = 'oc.sessions.activeSessionByProject';
|
2025-12-07 19:32:53 +02:00
|
|
|
const SESSION_EXPANDED_STORAGE_KEY = 'oc.sessions.expandedParents';
|
2026-02-16 14:15:19 +02:00
|
|
|
const SESSION_PINNED_STORAGE_KEY = 'oc.sessions.pinned';
|
2025-12-07 19:32:53 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
type PrVisualState = 'draft' | 'open' | 'blocked' | 'merged' | 'closed';
|
|
|
|
|
|
|
|
|
|
type PrIndicator = {
|
|
|
|
|
visualState: PrVisualState;
|
|
|
|
|
number: number;
|
|
|
|
|
url: string | null;
|
|
|
|
|
state: 'open' | 'closed' | 'merged';
|
|
|
|
|
draft: boolean;
|
|
|
|
|
title: string | null;
|
|
|
|
|
base: string | null;
|
|
|
|
|
head: string | null;
|
|
|
|
|
checks: {
|
|
|
|
|
state: 'success' | 'failure' | 'pending' | 'unknown';
|
|
|
|
|
total: number;
|
|
|
|
|
success: number;
|
|
|
|
|
failure: number;
|
|
|
|
|
pending: number;
|
|
|
|
|
} | null;
|
|
|
|
|
canMerge: boolean | null;
|
|
|
|
|
mergeableState: string | null;
|
|
|
|
|
repo: {
|
|
|
|
|
owner: string;
|
|
|
|
|
repo: string;
|
|
|
|
|
} | null;
|
2026-02-23 03:56:38 +07:00
|
|
|
};
|
|
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const getPrVisualState = (status: GitHubPullRequestStatus | null): PrVisualState | null => {
|
|
|
|
|
const pr = status?.pr;
|
|
|
|
|
if (!pr) {
|
2025-12-07 19:32:53 +02:00
|
|
|
return null;
|
|
|
|
|
}
|
2026-03-06 17:22:59 +02:00
|
|
|
if (pr.state === 'merged') {
|
|
|
|
|
return 'merged';
|
2026-02-16 14:15:19 +02:00
|
|
|
}
|
2026-03-06 17:22:59 +02:00
|
|
|
if (pr.state === 'closed') {
|
|
|
|
|
return 'closed';
|
2026-03-02 23:08:11 +00:00
|
|
|
}
|
2026-03-06 17:22:59 +02:00
|
|
|
if (pr.draft) {
|
|
|
|
|
return 'draft';
|
2026-03-02 23:08:11 +00:00
|
|
|
}
|
2026-03-06 17:22:59 +02:00
|
|
|
const checksFailed = status?.checks?.state === 'failure';
|
2026-03-11 23:52:31 +02:00
|
|
|
const mergeableState = typeof pr.mergeableState === 'string' ? pr.mergeableState : '';
|
|
|
|
|
const notMergeable = pr.mergeable === false || mergeableState === 'blocked' || mergeableState === 'dirty';
|
2026-03-06 17:22:59 +02:00
|
|
|
if (checksFailed || notMergeable) {
|
|
|
|
|
return 'blocked';
|
2026-03-02 23:08:11 +00:00
|
|
|
}
|
2026-03-06 17:22:59 +02:00
|
|
|
return 'open';
|
2026-03-02 23:08:11 +00:00
|
|
|
};
|
|
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const getPrVisualPriority = (state: PrVisualState): number => {
|
|
|
|
|
switch (state) {
|
|
|
|
|
case 'open':
|
|
|
|
|
return 5;
|
|
|
|
|
case 'blocked':
|
|
|
|
|
return 4;
|
|
|
|
|
case 'draft':
|
|
|
|
|
return 3;
|
|
|
|
|
case 'merged':
|
|
|
|
|
return 2;
|
|
|
|
|
case 'closed':
|
|
|
|
|
return 1;
|
|
|
|
|
default:
|
|
|
|
|
return 0;
|
2026-02-23 03:56:38 +07:00
|
|
|
}
|
2026-01-06 21:31:04 +02:00
|
|
|
};
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
interface SessionSidebarProps {
|
|
|
|
|
mobileVariant?: boolean;
|
2025-12-13 16:34:17 +02:00
|
|
|
onSessionSelected?: (sessionId: string) => void;
|
|
|
|
|
allowReselect?: boolean;
|
|
|
|
|
hideDirectoryControls?: boolean;
|
2026-02-16 14:15:19 +02:00
|
|
|
hideProjectSelector?: boolean;
|
2026-01-01 20:39:05 +01:00
|
|
|
showOnlyMainWorkspace?: boolean;
|
2025-12-07 19:32:53 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-13 16:34:17 +02:00
|
|
|
export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|
|
|
|
mobileVariant = false,
|
|
|
|
|
onSessionSelected,
|
|
|
|
|
allowReselect = false,
|
|
|
|
|
hideDirectoryControls = false,
|
2026-02-25 14:32:23 +02:00
|
|
|
hideProjectSelector = true,
|
2026-01-01 20:39:05 +01:00
|
|
|
showOnlyMainWorkspace = false,
|
2025-12-13 16:34:17 +02:00
|
|
|
}) => {
|
2026-03-02 23:08:11 +00:00
|
|
|
const [isSessionSearchOpen, setIsSessionSearchOpen] = React.useState(false);
|
|
|
|
|
const [sessionSearchQuery, setSessionSearchQuery] = React.useState('');
|
|
|
|
|
const sessionSearchContainerRef = React.useRef<HTMLDivElement | null>(null);
|
|
|
|
|
const sessionSearchInputRef = React.useRef<HTMLInputElement | null>(null);
|
2025-12-07 19:32:53 +02:00
|
|
|
const [editingId, setEditingId] = React.useState<string | null>(null);
|
|
|
|
|
const [editTitle, setEditTitle] = React.useState('');
|
2026-02-06 02:07:50 +02:00
|
|
|
const [editingProjectId, setEditingProjectId] = React.useState<string | null>(null);
|
|
|
|
|
const [editProjectTitle, setEditProjectTitle] = React.useState('');
|
2025-12-07 19:32:53 +02:00
|
|
|
const [expandedParents, setExpandedParents] = React.useState<Set<string>>(new Set());
|
|
|
|
|
const [directoryStatus, setDirectoryStatus] = React.useState<Map<string, 'unknown' | 'exists' | 'missing'>>(
|
|
|
|
|
() => new Map(),
|
|
|
|
|
);
|
|
|
|
|
const safeStorage = React.useMemo(() => getSafeStorage(), []);
|
2026-01-06 21:31:04 +02:00
|
|
|
const [collapsedProjects, setCollapsedProjects] = React.useState<Set<string>>(new Set());
|
2026-01-07 12:08:53 +02:00
|
|
|
|
2026-01-06 21:31:04 +02:00
|
|
|
const [projectRepoStatus, setProjectRepoStatus] = React.useState<Map<string, boolean | null>>(new Map());
|
2025-12-07 19:32:53 +02:00
|
|
|
const [expandedSessionGroups, setExpandedSessionGroups] = React.useState<Set<string>>(new Set());
|
2026-01-06 21:31:04 +02:00
|
|
|
const [hoveredProjectId, setHoveredProjectId] = React.useState<string | null>(null);
|
2026-03-03 00:20:15 +02:00
|
|
|
const [newWorktreeDialogOpen, setNewWorktreeDialogOpen] = React.useState(false);
|
2026-02-11 23:53:37 -08:00
|
|
|
const [projectNotesPanelOpen, setProjectNotesPanelOpen] = React.useState(false);
|
2026-01-16 17:45:37 +01:00
|
|
|
const [openMenuSessionId, setOpenMenuSessionId] = React.useState<string | null>(null);
|
2026-02-21 06:09:55 +07:00
|
|
|
const [renamingFolderId, setRenamingFolderId] = React.useState<string | null>(null);
|
|
|
|
|
const [renameFolderDraft, setRenameFolderDraft] = React.useState('');
|
2026-03-06 17:22:59 +02:00
|
|
|
const [deleteSessionConfirm, setDeleteSessionConfirm] = React.useState<DeleteSessionConfirmState>(null);
|
|
|
|
|
const [deleteFolderConfirm, setDeleteFolderConfirm] = React.useState<DeleteFolderConfirmState>(null);
|
2026-02-16 14:15:19 +02:00
|
|
|
const [pinnedSessionIds, setPinnedSessionIds] = React.useState<Set<string>>(() => {
|
|
|
|
|
try {
|
|
|
|
|
const raw = getSafeStorage().getItem(SESSION_PINNED_STORAGE_KEY);
|
|
|
|
|
if (!raw) {
|
|
|
|
|
return new Set();
|
|
|
|
|
}
|
|
|
|
|
const parsed = JSON.parse(raw) as string[];
|
|
|
|
|
return new Set(Array.isArray(parsed) ? parsed.filter((item) => typeof item === 'string') : []);
|
|
|
|
|
} catch {
|
|
|
|
|
return new Set();
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-02-07 03:57:46 +02:00
|
|
|
const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(() => {
|
|
|
|
|
try {
|
|
|
|
|
const raw = getSafeStorage().getItem(GROUP_COLLAPSE_STORAGE_KEY);
|
|
|
|
|
if (!raw) {
|
|
|
|
|
return new Set();
|
|
|
|
|
}
|
|
|
|
|
const parsed = JSON.parse(raw) as string[];
|
|
|
|
|
return new Set(Array.isArray(parsed) ? parsed.filter((item) => typeof item === 'string') : []);
|
|
|
|
|
} catch {
|
|
|
|
|
return new Set();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const [groupOrderByProject, setGroupOrderByProject] = React.useState<Map<string, string[]>>(() => {
|
|
|
|
|
try {
|
|
|
|
|
const raw = getSafeStorage().getItem(GROUP_ORDER_STORAGE_KEY);
|
|
|
|
|
if (!raw) {
|
|
|
|
|
return new Map();
|
|
|
|
|
}
|
|
|
|
|
const parsed = JSON.parse(raw) as Record<string, string[]>;
|
|
|
|
|
const next = new Map<string, string[]>();
|
|
|
|
|
Object.entries(parsed).forEach(([projectId, order]) => {
|
|
|
|
|
if (Array.isArray(order)) {
|
|
|
|
|
next.set(projectId, order.filter((item) => typeof item === 'string'));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return next;
|
|
|
|
|
} catch {
|
|
|
|
|
return new Map();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const [activeSessionByProject, setActiveSessionByProject] = React.useState<Map<string, string>>(() => {
|
|
|
|
|
try {
|
|
|
|
|
const raw = getSafeStorage().getItem(PROJECT_ACTIVE_SESSION_STORAGE_KEY);
|
|
|
|
|
if (!raw) {
|
|
|
|
|
return new Map();
|
|
|
|
|
}
|
|
|
|
|
const parsed = JSON.parse(raw) as Record<string, string>;
|
|
|
|
|
const next = new Map<string, string>();
|
|
|
|
|
Object.entries(parsed).forEach(([projectId, sessionId]) => {
|
|
|
|
|
if (typeof sessionId === 'string' && sessionId.length > 0) {
|
|
|
|
|
next.set(projectId, sessionId);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return next;
|
|
|
|
|
} catch {
|
|
|
|
|
return new Map();
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-02-25 14:32:23 +02:00
|
|
|
|
2026-02-07 03:57:46 +02:00
|
|
|
const [isProjectRenameInline, setIsProjectRenameInline] = React.useState(false);
|
|
|
|
|
const [projectRenameDraft, setProjectRenameDraft] = React.useState('');
|
|
|
|
|
const [projectRootBranches, setProjectRootBranches] = React.useState<Map<string, string>>(new Map());
|
2026-01-06 21:31:04 +02:00
|
|
|
const projectHeaderSentinelRefs = React.useRef<Map<string, HTMLDivElement | null>>(new Map());
|
|
|
|
|
const ignoreIntersectionUntil = React.useRef<number>(0);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
|
2026-01-06 21:31:04 +02:00
|
|
|
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
2025-12-07 19:32:53 +02:00
|
|
|
const setDirectory = useDirectoryStore((state) => state.setDirectory);
|
|
|
|
|
|
2026-01-06 21:31:04 +02:00
|
|
|
const projects = useProjectsStore((state) => state.projects);
|
|
|
|
|
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
|
|
|
|
|
const addProject = useProjectsStore((state) => state.addProject);
|
|
|
|
|
const removeProject = useProjectsStore((state) => state.removeProject);
|
|
|
|
|
const setActiveProjectIdOnly = useProjectsStore((state) => state.setActiveProjectIdOnly);
|
2026-02-06 02:07:50 +02:00
|
|
|
const renameProject = useProjectsStore((state) => state.renameProject);
|
2026-01-06 21:31:04 +02:00
|
|
|
|
2025-12-21 20:18:51 +02:00
|
|
|
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
2026-03-02 02:11:33 +02:00
|
|
|
const openContextPanelTab = useUIStore((state) => state.openContextPanelTab);
|
2026-02-11 23:53:37 -08:00
|
|
|
const deviceInfo = useDeviceInfo();
|
2025-12-21 20:18:51 +02:00
|
|
|
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
|
2026-01-01 14:59:51 +02:00
|
|
|
const openMultiRunLauncher = useUIStore((state) => state.openMultiRunLauncher);
|
2026-02-21 05:46:15 +07:00
|
|
|
const notifyOnSubtasks = useUIStore((state) => state.notifyOnSubtasks);
|
2026-02-24 03:28:30 +02:00
|
|
|
const showDeletionDialog = useUIStore((state) => state.showDeletionDialog);
|
|
|
|
|
const setShowDeletionDialog = useUIStore((state) => state.setShowDeletionDialog);
|
2026-01-08 01:48:42 +02:00
|
|
|
const settingsAutoCreateWorktree = useConfigStore((state) => state.settingsAutoCreateWorktree);
|
|
|
|
|
|
2026-03-02 23:08:11 +00:00
|
|
|
const debouncedSessionSearchQuery = useDebouncedValue(sessionSearchQuery, 120);
|
|
|
|
|
const normalizedSessionSearchQuery = React.useMemo(
|
|
|
|
|
() => debouncedSessionSearchQuery.trim().toLowerCase(),
|
|
|
|
|
[debouncedSessionSearchQuery],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const hasSessionSearchQuery = normalizedSessionSearchQuery.length > 0;
|
|
|
|
|
|
2026-02-21 06:09:55 +07:00
|
|
|
// Session Folders store
|
|
|
|
|
const collapsedFolderIds = useSessionFoldersStore((state) => state.collapsedFolderIds);
|
2026-03-06 17:22:59 +02:00
|
|
|
const foldersMap = useSessionFoldersStore((state) => state.foldersMap);
|
2026-02-21 06:09:55 +07:00
|
|
|
const getFoldersForScope = useSessionFoldersStore((state) => state.getFoldersForScope);
|
|
|
|
|
const createFolder = useSessionFoldersStore((state) => state.createFolder);
|
|
|
|
|
const renameFolder = useSessionFoldersStore((state) => state.renameFolder);
|
|
|
|
|
const deleteFolder = useSessionFoldersStore((state) => state.deleteFolder);
|
|
|
|
|
const addSessionToFolder = useSessionFoldersStore((state) => state.addSessionToFolder);
|
|
|
|
|
const removeSessionFromFolder = useSessionFoldersStore((state) => state.removeSessionFromFolder);
|
|
|
|
|
const toggleFolderCollapse = useSessionFoldersStore((state) => state.toggleFolderCollapse);
|
|
|
|
|
const cleanupSessions = useSessionFoldersStore((state) => state.cleanupSessions);
|
|
|
|
|
const getSessionFolderId = useSessionFoldersStore((state) => state.getSessionFolderId);
|
|
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
useSessionSearchEffects({
|
|
|
|
|
isSessionSearchOpen,
|
|
|
|
|
setIsSessionSearchOpen,
|
|
|
|
|
sessionSearchInputRef,
|
|
|
|
|
sessionSearchContainerRef,
|
|
|
|
|
});
|
2026-03-02 23:08:11 +00:00
|
|
|
|
2026-02-08 16:51:05 -08:00
|
|
|
const gitDirectories = useGitStore((state) => state.directories);
|
|
|
|
|
|
2026-01-06 21:31:04 +02:00
|
|
|
const sessions = useSessionStore((state) => state.sessions);
|
2026-03-06 17:22:59 +02:00
|
|
|
const archivedSessions = useSessionStore((state) => state.archivedSessions);
|
2026-01-06 21:31:04 +02:00
|
|
|
const sessionsByDirectory = useSessionStore((state) => state.sessionsByDirectory);
|
2025-12-07 19:32:53 +02:00
|
|
|
const currentSessionId = useSessionStore((state) => state.currentSessionId);
|
2026-02-11 19:28:22 +02:00
|
|
|
const newSessionDraftOpen = useSessionStore((state) => Boolean(state.newSessionDraft?.open));
|
2025-12-07 19:32:53 +02:00
|
|
|
const setCurrentSession = useSessionStore((state) => state.setCurrentSession);
|
2026-02-25 14:32:23 +02:00
|
|
|
const loadMessages = useSessionStore((state) => state.loadMessages);
|
2025-12-07 19:32:53 +02:00
|
|
|
const updateSessionTitle = useSessionStore((state) => state.updateSessionTitle);
|
|
|
|
|
const shareSession = useSessionStore((state) => state.shareSession);
|
|
|
|
|
const unshareSession = useSessionStore((state) => state.unshareSession);
|
|
|
|
|
const sessionMemoryState = useSessionStore((state) => state.sessionMemoryState);
|
2026-02-05 01:59:49 +02:00
|
|
|
const sessionStatus = useSessionStore((state) => state.sessionStatus);
|
2026-02-07 03:57:46 +02:00
|
|
|
const sessionAttentionStates = useSessionStore((state) => state.sessionAttentionStates);
|
2026-01-06 21:31:04 +02:00
|
|
|
const permissions = useSessionStore((state) => state.permissions);
|
2025-12-07 19:32:53 +02:00
|
|
|
const worktreeMetadata = useSessionStore((state) => state.worktreeMetadata);
|
2026-01-06 21:31:04 +02:00
|
|
|
const availableWorktreesByProject = useSessionStore((state) => state.availableWorktreesByProject);
|
|
|
|
|
const getSessionsByDirectory = useSessionStore((state) => state.getSessionsByDirectory);
|
2026-01-08 00:29:20 +02:00
|
|
|
const openNewSessionDraft = useSessionStore((state) => state.openNewSessionDraft);
|
2026-03-06 17:22:59 +02:00
|
|
|
const prStatusEntries = useGitHubPrStatusStore((state) => state.entries);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
2026-02-05 01:59:49 +02:00
|
|
|
const tauriIpcAvailable = React.useMemo(() => isTauriShell(), []);
|
|
|
|
|
const isDesktopShellRuntime = React.useMemo(() => isDesktopShell(), []);
|
|
|
|
|
|
|
|
|
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
|
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const {
|
|
|
|
|
buildGroupSearchText,
|
|
|
|
|
filterSessionNodesForSearch,
|
|
|
|
|
buildGroupedSessions,
|
|
|
|
|
} = useSessionGrouping({
|
|
|
|
|
homeDirectory,
|
|
|
|
|
worktreeMetadata,
|
|
|
|
|
pinnedSessionIds,
|
|
|
|
|
gitDirectories,
|
2026-03-12 23:45:45 +02:00
|
|
|
isVSCode,
|
2026-03-06 17:22:59 +02:00
|
|
|
});
|
2025-12-07 19:32:53 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const { scheduleCollapsedProjectsPersist } = useSidebarPersistence({
|
|
|
|
|
isVSCode,
|
|
|
|
|
safeStorage,
|
|
|
|
|
keys: {
|
|
|
|
|
sessionExpanded: SESSION_EXPANDED_STORAGE_KEY,
|
|
|
|
|
projectCollapse: PROJECT_COLLAPSE_STORAGE_KEY,
|
|
|
|
|
sessionPinned: SESSION_PINNED_STORAGE_KEY,
|
|
|
|
|
groupOrder: GROUP_ORDER_STORAGE_KEY,
|
|
|
|
|
projectActiveSession: PROJECT_ACTIVE_SESSION_STORAGE_KEY,
|
|
|
|
|
groupCollapse: GROUP_COLLAPSE_STORAGE_KEY,
|
|
|
|
|
},
|
|
|
|
|
sessions,
|
|
|
|
|
pinnedSessionIds,
|
|
|
|
|
setPinnedSessionIds,
|
|
|
|
|
groupOrderByProject,
|
|
|
|
|
activeSessionByProject,
|
|
|
|
|
collapsedGroups,
|
|
|
|
|
setExpandedParents,
|
|
|
|
|
setCollapsedProjects,
|
|
|
|
|
});
|
2026-02-16 14:15:19 +02:00
|
|
|
|
|
|
|
|
const togglePinnedSession = React.useCallback((sessionId: string) => {
|
|
|
|
|
setPinnedSessionIds((prev) => {
|
|
|
|
|
const next = new Set(prev);
|
|
|
|
|
if (next.has(sessionId)) {
|
|
|
|
|
next.delete(sessionId);
|
|
|
|
|
} else {
|
|
|
|
|
next.add(sessionId);
|
|
|
|
|
}
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const sortedSessions = React.useMemo(() => {
|
2026-02-24 03:28:30 +02:00
|
|
|
return [...sessions].sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds));
|
|
|
|
|
}, [sessions, pinnedSessionIds]);
|
2026-02-16 14:15:19 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
useSessionPrefetch({
|
|
|
|
|
currentSessionId,
|
|
|
|
|
sortedSessions,
|
|
|
|
|
loadMessages,
|
|
|
|
|
});
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
const childrenMap = React.useMemo(() => {
|
|
|
|
|
const map = new Map<string, Session[]>();
|
|
|
|
|
sortedSessions.forEach((session) => {
|
|
|
|
|
const parentID = (session as Session & { parentID?: string | null }).parentID;
|
|
|
|
|
if (!parentID) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const collection = map.get(parentID) ?? [];
|
|
|
|
|
collection.push(session);
|
|
|
|
|
map.set(parentID, collection);
|
|
|
|
|
});
|
2026-02-24 03:28:30 +02:00
|
|
|
map.forEach((list) => list.sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds)));
|
2025-12-07 19:32:53 +02:00
|
|
|
return map;
|
2026-02-24 03:28:30 +02:00
|
|
|
}, [sortedSessions, pinnedSessionIds]);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
useDirectoryStatusProbe({
|
|
|
|
|
sortedSessions,
|
|
|
|
|
projects,
|
|
|
|
|
directoryStatus,
|
|
|
|
|
setDirectoryStatus,
|
|
|
|
|
});
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
const emptyState = (
|
|
|
|
|
<div className="py-6 text-center text-muted-foreground">
|
|
|
|
|
<p className="typography-ui-label font-semibold">No sessions yet</p>
|
|
|
|
|
<p className="typography-meta mt-1">Create your first session to start coding.</p>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-06 02:07:50 +02:00
|
|
|
const handleSaveProjectEdit = React.useCallback(() => {
|
|
|
|
|
if (editingProjectId && editProjectTitle.trim()) {
|
|
|
|
|
renameProject(editingProjectId, editProjectTitle.trim());
|
|
|
|
|
setEditingProjectId(null);
|
|
|
|
|
setEditProjectTitle('');
|
|
|
|
|
}
|
|
|
|
|
}, [editingProjectId, editProjectTitle, renameProject]);
|
|
|
|
|
|
|
|
|
|
const handleCancelProjectEdit = React.useCallback(() => {
|
|
|
|
|
setEditingProjectId(null);
|
|
|
|
|
setEditProjectTitle('');
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
const deleteSession = useSessionStore((state) => state.deleteSession);
|
|
|
|
|
const deleteSessions = useSessionStore((state) => state.deleteSessions);
|
2026-03-06 17:22:59 +02:00
|
|
|
const archiveSession = useSessionStore((state) => state.archiveSession);
|
|
|
|
|
const archiveSessions = useSessionStore((state) => state.archiveSessions);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const {
|
|
|
|
|
copiedSessionId,
|
|
|
|
|
handleSessionSelect,
|
|
|
|
|
handleSessionDoubleClick,
|
|
|
|
|
handleSaveEdit,
|
|
|
|
|
handleCancelEdit,
|
|
|
|
|
handleShareSession,
|
|
|
|
|
handleCopyShareUrl,
|
|
|
|
|
handleUnshareSession,
|
|
|
|
|
handleDeleteSession,
|
|
|
|
|
confirmDeleteSession,
|
|
|
|
|
} = useSessionActions({
|
|
|
|
|
activeProjectId,
|
|
|
|
|
currentDirectory,
|
|
|
|
|
currentSessionId,
|
|
|
|
|
mobileVariant,
|
|
|
|
|
allowReselect,
|
|
|
|
|
onSessionSelected,
|
|
|
|
|
isSessionSearchOpen,
|
|
|
|
|
sessionSearchQuery,
|
|
|
|
|
setSessionSearchQuery,
|
|
|
|
|
setIsSessionSearchOpen,
|
|
|
|
|
setActiveProjectIdOnly,
|
|
|
|
|
setDirectory,
|
|
|
|
|
setActiveMainTab,
|
|
|
|
|
setSessionSwitcherOpen,
|
|
|
|
|
setCurrentSession,
|
|
|
|
|
updateSessionTitle,
|
|
|
|
|
shareSession,
|
|
|
|
|
unshareSession,
|
|
|
|
|
deleteSession,
|
|
|
|
|
deleteSessions,
|
|
|
|
|
archiveSession,
|
|
|
|
|
archiveSessions,
|
|
|
|
|
childrenMap,
|
|
|
|
|
showDeletionDialog,
|
|
|
|
|
setDeleteSessionConfirm,
|
|
|
|
|
deleteSessionConfirm,
|
|
|
|
|
setEditingId,
|
|
|
|
|
setEditTitle,
|
|
|
|
|
editingId,
|
|
|
|
|
editTitle,
|
|
|
|
|
});
|
2026-02-23 03:56:38 +07:00
|
|
|
|
|
|
|
|
const confirmDeleteFolder = React.useCallback(() => {
|
|
|
|
|
if (!deleteFolderConfirm) return;
|
|
|
|
|
const { scopeKey, folderId } = deleteFolderConfirm;
|
|
|
|
|
setDeleteFolderConfirm(null);
|
|
|
|
|
deleteFolder(scopeKey, folderId);
|
|
|
|
|
}, [deleteFolderConfirm, deleteFolder]);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
const handleOpenDirectoryDialog = React.useCallback(() => {
|
2026-02-11 20:07:44 +02:00
|
|
|
if (!tauriIpcAvailable || !isDesktopLocalOriginActive()) {
|
2025-12-07 19:32:53 +02:00
|
|
|
sessionEvents.requestDirectoryDialog();
|
2026-02-05 01:59:49 +02:00
|
|
|
return;
|
2025-12-07 19:32:53 +02:00
|
|
|
}
|
2026-02-05 01:59:49 +02:00
|
|
|
|
|
|
|
|
import('@/lib/desktop')
|
|
|
|
|
.then(({ requestDirectoryAccess }) => requestDirectoryAccess(''))
|
|
|
|
|
.then((result) => {
|
|
|
|
|
if (result.success && result.path) {
|
|
|
|
|
const added = addProject(result.path, { id: result.projectId });
|
|
|
|
|
if (!added) {
|
|
|
|
|
toast.error('Failed to add project', {
|
|
|
|
|
description: 'Please select a valid directory.',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else if (result.error && result.error !== 'Directory selection cancelled') {
|
|
|
|
|
toast.error('Failed to select directory', {
|
|
|
|
|
description: result.error,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
console.error('Desktop: Error selecting directory:', error);
|
|
|
|
|
toast.error('Failed to select directory');
|
|
|
|
|
});
|
|
|
|
|
}, [addProject, tauriIpcAvailable]);
|
2026-01-06 21:31:04 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const toggleParent = React.useCallback((sessionId: string) => {
|
|
|
|
|
setExpandedParents((prev) => {
|
|
|
|
|
const next = new Set(prev);
|
|
|
|
|
if (next.has(sessionId)) {
|
|
|
|
|
next.delete(sessionId);
|
|
|
|
|
} else {
|
|
|
|
|
next.add(sessionId);
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
safeStorage.setItem(SESSION_EXPANDED_STORAGE_KEY, JSON.stringify(Array.from(next)));
|
|
|
|
|
} catch { /* ignored */ }
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}, [safeStorage]);
|
2026-02-07 03:57:46 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const createFolderAndStartRename = React.useCallback(
|
|
|
|
|
(scopeKey: string, parentId?: string | null) => {
|
|
|
|
|
if (!scopeKey) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-02-07 03:57:46 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
if (parentId && collapsedFolderIds.has(parentId)) {
|
|
|
|
|
toggleFolderCollapse(parentId);
|
|
|
|
|
}
|
2026-02-07 03:57:46 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const newFolder = createFolder(scopeKey, 'New folder', parentId);
|
|
|
|
|
setRenamingFolderId(newFolder.id);
|
|
|
|
|
setRenameFolderDraft(newFolder.name);
|
|
|
|
|
return newFolder;
|
2026-01-06 21:31:04 +02:00
|
|
|
},
|
2026-03-06 17:22:59 +02:00
|
|
|
[collapsedFolderIds, toggleFolderCollapse, createFolder],
|
2026-01-06 21:31:04 +02:00
|
|
|
);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
const toggleGroupSessionLimit = React.useCallback((groupId: string) => {
|
|
|
|
|
setExpandedSessionGroups((prev) => {
|
|
|
|
|
const next = new Set(prev);
|
|
|
|
|
if (next.has(groupId)) {
|
|
|
|
|
next.delete(groupId);
|
|
|
|
|
} else {
|
|
|
|
|
next.add(groupId);
|
|
|
|
|
}
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-01-06 21:31:04 +02:00
|
|
|
const toggleProject = React.useCallback((projectId: string) => {
|
|
|
|
|
// Ignore intersection events for a short period after toggling
|
|
|
|
|
ignoreIntersectionUntil.current = Date.now() + 150;
|
|
|
|
|
setCollapsedProjects((prev) => {
|
|
|
|
|
const next = new Set(prev);
|
|
|
|
|
if (next.has(projectId)) {
|
|
|
|
|
next.delete(projectId);
|
|
|
|
|
} else {
|
|
|
|
|
next.add(projectId);
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
safeStorage.setItem(PROJECT_COLLAPSE_STORAGE_KEY, JSON.stringify(Array.from(next)));
|
|
|
|
|
} catch { /* ignored */ }
|
2026-02-05 01:59:49 +02:00
|
|
|
|
|
|
|
|
// Persist collapse state to server settings (web + desktop local/remote).
|
|
|
|
|
if (!isVSCode) {
|
|
|
|
|
scheduleCollapsedProjectsPersist(next);
|
|
|
|
|
}
|
2026-01-06 21:31:04 +02:00
|
|
|
return next;
|
|
|
|
|
});
|
2026-02-05 01:59:49 +02:00
|
|
|
}, [isVSCode, safeStorage, scheduleCollapsedProjectsPersist]);
|
2026-01-06 21:31:04 +02:00
|
|
|
|
|
|
|
|
const normalizedProjects = React.useMemo(() => {
|
|
|
|
|
return projects
|
|
|
|
|
.map((project) => ({
|
|
|
|
|
...project,
|
|
|
|
|
normalizedPath: normalizePath(project.path),
|
|
|
|
|
}))
|
|
|
|
|
.filter((project) => Boolean(project.normalizedPath)) as Array<{
|
|
|
|
|
id: string;
|
|
|
|
|
path: string;
|
|
|
|
|
label?: string;
|
|
|
|
|
normalizedPath: string;
|
|
|
|
|
}>;
|
|
|
|
|
}, [projects]);
|
|
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
useProjectRepoStatus({
|
|
|
|
|
projects,
|
|
|
|
|
normalizedProjects,
|
|
|
|
|
normalizePath,
|
|
|
|
|
gitDirectories,
|
|
|
|
|
setProjectRepoStatus,
|
|
|
|
|
setProjectRootBranches,
|
|
|
|
|
});
|
2026-02-08 16:51:05 -08:00
|
|
|
|
2026-02-23 03:56:38 +07:00
|
|
|
const isSessionsLoading = useSessionStore((state) => state.isLoading);
|
2026-03-06 17:22:59 +02:00
|
|
|
useSessionFolderCleanup({
|
|
|
|
|
isSessionsLoading,
|
|
|
|
|
sessions,
|
|
|
|
|
archivedSessions,
|
|
|
|
|
normalizedProjects,
|
|
|
|
|
isVSCode,
|
|
|
|
|
availableWorktreesByProject,
|
|
|
|
|
cleanupSessions,
|
|
|
|
|
});
|
2026-02-21 06:09:55 +07:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const { getSessionsForProject, getArchivedSessionsForProject } = useProjectSessionLists({
|
|
|
|
|
isVSCode,
|
|
|
|
|
sessions,
|
|
|
|
|
archivedSessions,
|
|
|
|
|
sessionsByDirectory,
|
|
|
|
|
getSessionsByDirectory,
|
|
|
|
|
availableWorktreesByProject,
|
|
|
|
|
});
|
2026-01-06 21:31:04 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
useArchivedAutoFolders({
|
|
|
|
|
normalizedProjects,
|
|
|
|
|
sessions,
|
|
|
|
|
archivedSessions,
|
|
|
|
|
availableWorktreesByProject,
|
|
|
|
|
isVSCode,
|
|
|
|
|
isSessionsLoading,
|
|
|
|
|
foldersMap,
|
|
|
|
|
createFolder,
|
|
|
|
|
addSessionToFolder,
|
|
|
|
|
cleanupSessions,
|
|
|
|
|
});
|
2026-01-06 21:31:04 +02:00
|
|
|
|
2026-02-16 14:15:19 +02:00
|
|
|
// Keep last-known repo status to avoid UI jiggling during project switch
|
|
|
|
|
const lastRepoStatusRef = React.useRef(false);
|
|
|
|
|
if (activeProjectId && projectRepoStatus.has(activeProjectId)) {
|
|
|
|
|
lastRepoStatusRef.current = Boolean(projectRepoStatus.get(activeProjectId));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const {
|
|
|
|
|
projectSections,
|
|
|
|
|
groupSearchDataByGroup,
|
|
|
|
|
sectionsForRender,
|
|
|
|
|
searchMatchCount,
|
|
|
|
|
} = useSessionSidebarSections({
|
|
|
|
|
normalizedProjects,
|
|
|
|
|
activeProjectId,
|
|
|
|
|
getSessionsForProject,
|
|
|
|
|
getArchivedSessionsForProject,
|
|
|
|
|
availableWorktreesByProject,
|
|
|
|
|
projectRepoStatus,
|
|
|
|
|
projectRootBranches,
|
|
|
|
|
lastRepoStatus: lastRepoStatusRef.current,
|
|
|
|
|
buildGroupedSessions,
|
2026-03-02 23:08:11 +00:00
|
|
|
hasSessionSearchQuery,
|
|
|
|
|
normalizedSessionSearchQuery,
|
2026-03-06 17:22:59 +02:00
|
|
|
filterSessionNodesForSearch,
|
2026-03-02 23:08:11 +00:00
|
|
|
buildGroupSearchText,
|
|
|
|
|
getFoldersForScope,
|
2026-03-06 17:22:59 +02:00
|
|
|
});
|
2026-03-02 23:08:11 +00:00
|
|
|
|
|
|
|
|
const searchEmptyState = (
|
|
|
|
|
<div className="py-6 text-center text-muted-foreground">
|
|
|
|
|
<p className="typography-ui-label font-semibold">No matching sessions</p>
|
|
|
|
|
<p className="typography-meta mt-1">Try a different title, branch, folder, or path.</p>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-07 03:57:46 +02:00
|
|
|
const activeProjectForHeader = React.useMemo(
|
|
|
|
|
() => normalizedProjects.find((project) => project.id === activeProjectId) ?? normalizedProjects[0] ?? null,
|
|
|
|
|
[normalizedProjects, activeProjectId],
|
|
|
|
|
);
|
2026-02-11 23:53:37 -08:00
|
|
|
const activeProjectRefForHeader = React.useMemo(
|
|
|
|
|
() => (activeProjectForHeader
|
|
|
|
|
? {
|
|
|
|
|
id: activeProjectForHeader.id,
|
|
|
|
|
path: activeProjectForHeader.normalizedPath,
|
|
|
|
|
}
|
|
|
|
|
: null),
|
|
|
|
|
[activeProjectForHeader],
|
|
|
|
|
);
|
2026-02-07 03:57:46 +02:00
|
|
|
|
|
|
|
|
const activeProjectIsRepo = React.useMemo(
|
|
|
|
|
() => (activeProjectForHeader ? Boolean(projectRepoStatus.get(activeProjectForHeader.id)) : false),
|
|
|
|
|
[activeProjectForHeader, projectRepoStatus],
|
|
|
|
|
);
|
2026-02-16 14:15:19 +02:00
|
|
|
// Only flip to false once the new project's status is actually resolved (present in map)
|
|
|
|
|
const stableActiveProjectIsRepo = activeProjectForHeader && projectRepoStatus.has(activeProjectForHeader.id)
|
|
|
|
|
? activeProjectIsRepo
|
|
|
|
|
: lastRepoStatusRef.current;
|
2026-02-11 23:53:37 -08:00
|
|
|
const reserveHeaderActionsSpace = Boolean(activeProjectForHeader);
|
|
|
|
|
const useMobileNotesPanel = mobileVariant || deviceInfo.isMobile;
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (!activeProjectForHeader) {
|
|
|
|
|
setProjectNotesPanelOpen(false);
|
|
|
|
|
}
|
|
|
|
|
}, [activeProjectForHeader]);
|
2026-02-07 03:57:46 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const { currentSessionDirectory } = useProjectSessionSelection({
|
|
|
|
|
projectSections,
|
2026-02-07 03:57:46 +02:00
|
|
|
activeProjectId,
|
|
|
|
|
activeSessionByProject,
|
2026-03-06 17:22:59 +02:00
|
|
|
setActiveSessionByProject,
|
2026-02-07 03:57:46 +02:00
|
|
|
currentSessionId,
|
|
|
|
|
handleSessionSelect,
|
2026-02-11 22:11:53 +02:00
|
|
|
isVSCode,
|
2026-02-11 19:28:22 +02:00
|
|
|
newSessionDraftOpen,
|
2026-02-07 03:57:46 +02:00
|
|
|
mobileVariant,
|
|
|
|
|
openNewSessionDraft,
|
|
|
|
|
setActiveMainTab,
|
|
|
|
|
setSessionSwitcherOpen,
|
2026-03-06 17:22:59 +02:00
|
|
|
sessions,
|
|
|
|
|
worktreeMetadata,
|
|
|
|
|
});
|
2026-02-07 03:57:46 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const { getOrderedGroups } = useGroupOrdering(groupOrderByProject);
|
2026-02-07 03:57:46 +02:00
|
|
|
|
|
|
|
|
const handleStartInlineProjectRename = React.useCallback(() => {
|
|
|
|
|
if (!activeProjectForHeader) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setProjectRenameDraft(formatProjectLabel(
|
|
|
|
|
activeProjectForHeader.label?.trim()
|
|
|
|
|
|| formatDirectoryName(activeProjectForHeader.normalizedPath, homeDirectory)
|
|
|
|
|
|| activeProjectForHeader.normalizedPath,
|
|
|
|
|
));
|
|
|
|
|
setIsProjectRenameInline(true);
|
|
|
|
|
}, [activeProjectForHeader, homeDirectory]);
|
|
|
|
|
|
|
|
|
|
const handleSaveInlineProjectRename = React.useCallback(() => {
|
|
|
|
|
if (!activeProjectForHeader) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const trimmed = projectRenameDraft.trim();
|
|
|
|
|
if (!trimmed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
renameProject(activeProjectForHeader.id, trimmed);
|
|
|
|
|
setIsProjectRenameInline(false);
|
|
|
|
|
}, [activeProjectForHeader, projectRenameDraft, renameProject]);
|
|
|
|
|
|
2026-02-25 14:32:23 +02:00
|
|
|
const desktopHeaderActionButtonClass =
|
2026-03-03 22:23:21 +00:00
|
|
|
'inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md leading-none text-foreground hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 disabled:cursor-not-allowed';
|
2026-02-25 14:32:23 +02:00
|
|
|
const mobileHeaderActionButtonClass =
|
2026-03-03 22:23:21 +00:00
|
|
|
'inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md leading-none text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 disabled:cursor-not-allowed';
|
2026-02-25 14:32:23 +02:00
|
|
|
const headerActionButtonClass = mobileVariant ? mobileHeaderActionButtonClass : desktopHeaderActionButtonClass;
|
|
|
|
|
const headerActionIconClass = 'h-4.5 w-4.5';
|
|
|
|
|
const addProjectButtonClass = cn(
|
2026-03-03 22:23:21 +00:00
|
|
|
'inline-flex cursor-pointer items-center justify-center rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 disabled:cursor-not-allowed',
|
2026-02-25 14:32:23 +02:00
|
|
|
mobileVariant
|
|
|
|
|
? 'h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50'
|
|
|
|
|
: 'h-8 w-8 text-foreground hover:bg-interactive-hover',
|
|
|
|
|
!isDesktopShellRuntime && 'bg-transparent hover:bg-sidebar/40',
|
|
|
|
|
);
|
2026-01-06 21:31:04 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const stuckProjectHeaders = useStickyProjectHeaders({
|
|
|
|
|
isDesktopShellRuntime,
|
|
|
|
|
projectSections,
|
|
|
|
|
projectHeaderSentinelRefs,
|
|
|
|
|
});
|
2026-01-01 14:24:57 +02:00
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
const renderSessionNode = React.useCallback(
|
2026-03-06 17:22:59 +02:00
|
|
|
(
|
|
|
|
|
node: SessionNode,
|
|
|
|
|
depth = 0,
|
|
|
|
|
groupDirectory?: string | null,
|
|
|
|
|
projectId?: string | null,
|
|
|
|
|
archivedBucket = false,
|
|
|
|
|
): React.ReactNode => (
|
|
|
|
|
<SessionNodeItem
|
|
|
|
|
node={node}
|
|
|
|
|
depth={depth}
|
|
|
|
|
groupDirectory={groupDirectory}
|
|
|
|
|
projectId={projectId}
|
|
|
|
|
archivedBucket={archivedBucket}
|
|
|
|
|
directoryStatus={directoryStatus}
|
|
|
|
|
sessionMemoryState={sessionMemoryState as Map<string, { isZombie?: boolean }>}
|
|
|
|
|
currentSessionId={currentSessionId}
|
|
|
|
|
pinnedSessionIds={pinnedSessionIds}
|
|
|
|
|
expandedParents={expandedParents}
|
|
|
|
|
hasSessionSearchQuery={hasSessionSearchQuery}
|
|
|
|
|
normalizedSessionSearchQuery={normalizedSessionSearchQuery}
|
|
|
|
|
sessionAttentionStates={sessionAttentionStates as Map<string, { needsAttention?: boolean }>}
|
|
|
|
|
notifyOnSubtasks={notifyOnSubtasks}
|
|
|
|
|
sessionStatus={sessionStatus as Map<string, { type?: string }> | undefined}
|
|
|
|
|
permissions={permissions as Map<string, unknown[]>}
|
|
|
|
|
editingId={editingId}
|
|
|
|
|
setEditingId={setEditingId}
|
|
|
|
|
editTitle={editTitle}
|
|
|
|
|
setEditTitle={setEditTitle}
|
|
|
|
|
handleSaveEdit={handleSaveEdit}
|
|
|
|
|
handleCancelEdit={handleCancelEdit}
|
|
|
|
|
toggleParent={toggleParent}
|
|
|
|
|
handleSessionSelect={handleSessionSelect}
|
|
|
|
|
handleSessionDoubleClick={handleSessionDoubleClick}
|
|
|
|
|
togglePinnedSession={togglePinnedSession}
|
|
|
|
|
handleShareSession={handleShareSession}
|
|
|
|
|
copiedSessionId={copiedSessionId}
|
|
|
|
|
handleCopyShareUrl={handleCopyShareUrl}
|
|
|
|
|
handleUnshareSession={handleUnshareSession}
|
|
|
|
|
openMenuSessionId={openMenuSessionId}
|
|
|
|
|
setOpenMenuSessionId={setOpenMenuSessionId}
|
|
|
|
|
renamingFolderId={renamingFolderId}
|
|
|
|
|
getFoldersForScope={getFoldersForScope}
|
|
|
|
|
getSessionFolderId={getSessionFolderId}
|
|
|
|
|
removeSessionFromFolder={removeSessionFromFolder}
|
|
|
|
|
addSessionToFolder={addSessionToFolder}
|
|
|
|
|
createFolderAndStartRename={createFolderAndStartRename}
|
|
|
|
|
openContextPanelTab={openContextPanelTab}
|
|
|
|
|
handleDeleteSession={handleDeleteSession}
|
|
|
|
|
mobileVariant={mobileVariant}
|
|
|
|
|
renderSessionNode={renderSessionNode}
|
|
|
|
|
/>
|
|
|
|
|
),
|
2025-12-07 19:32:53 +02:00
|
|
|
[
|
|
|
|
|
directoryStatus,
|
|
|
|
|
sessionMemoryState,
|
|
|
|
|
currentSessionId,
|
2026-03-06 17:22:59 +02:00
|
|
|
pinnedSessionIds,
|
|
|
|
|
expandedParents,
|
2026-03-02 23:08:11 +00:00
|
|
|
hasSessionSearchQuery,
|
|
|
|
|
normalizedSessionSearchQuery,
|
2026-03-06 17:22:59 +02:00
|
|
|
sessionAttentionStates,
|
|
|
|
|
notifyOnSubtasks,
|
|
|
|
|
sessionStatus,
|
|
|
|
|
permissions,
|
2025-12-07 19:32:53 +02:00
|
|
|
editingId,
|
2026-03-06 17:22:59 +02:00
|
|
|
setEditingId,
|
2025-12-07 19:32:53 +02:00
|
|
|
editTitle,
|
2026-03-06 17:22:59 +02:00
|
|
|
setEditTitle,
|
2025-12-07 19:32:53 +02:00
|
|
|
handleSaveEdit,
|
|
|
|
|
handleCancelEdit,
|
|
|
|
|
toggleParent,
|
|
|
|
|
handleSessionSelect,
|
2026-02-11 10:08:15 -08:00
|
|
|
handleSessionDoubleClick,
|
2026-02-16 14:15:19 +02:00
|
|
|
togglePinnedSession,
|
2025-12-07 19:32:53 +02:00
|
|
|
handleShareSession,
|
2026-03-06 17:22:59 +02:00
|
|
|
copiedSessionId,
|
2025-12-07 19:32:53 +02:00
|
|
|
handleCopyShareUrl,
|
|
|
|
|
handleUnshareSession,
|
2026-01-16 17:45:37 +01:00
|
|
|
openMenuSessionId,
|
2026-03-06 17:22:59 +02:00
|
|
|
setOpenMenuSessionId,
|
2026-02-21 06:09:55 +07:00
|
|
|
renamingFolderId,
|
|
|
|
|
getFoldersForScope,
|
|
|
|
|
getSessionFolderId,
|
|
|
|
|
removeSessionFromFolder,
|
2026-03-06 17:22:59 +02:00
|
|
|
addSessionToFolder,
|
2026-02-24 03:28:30 +02:00
|
|
|
createFolderAndStartRename,
|
2026-03-02 02:11:33 +02:00
|
|
|
openContextPanelTab,
|
2026-03-06 17:22:59 +02:00
|
|
|
handleDeleteSession,
|
|
|
|
|
mobileVariant,
|
2025-12-07 19:32:53 +02:00
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const toggleCollapsedGroup = React.useCallback((key: string) => {
|
|
|
|
|
setCollapsedGroups((prev) => {
|
|
|
|
|
const next = new Set(prev);
|
|
|
|
|
if (next.has(key)) next.delete(key);
|
|
|
|
|
else next.add(key);
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
2026-03-02 23:08:11 +00:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const prVisualStateByDirectoryBranch = React.useMemo(() => {
|
|
|
|
|
const result = new Map<string, PrIndicator>();
|
2026-02-21 06:09:55 +07:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
Object.values(prStatusEntries).forEach((entry) => {
|
2026-03-11 23:52:31 +02:00
|
|
|
const directory = normalizePath(entry.params?.directory ?? entry.identity?.directory ?? null);
|
|
|
|
|
const branch = entry.params?.branch?.trim() ?? entry.identity?.branch?.trim();
|
2026-03-06 17:22:59 +02:00
|
|
|
if (!directory || !branch) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const state = getPrVisualState(entry.status ?? null);
|
|
|
|
|
const pr = entry.status?.pr;
|
|
|
|
|
if (!state || !pr?.number) {
|
|
|
|
|
return;
|
2026-03-02 23:08:11 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
const key = `${directory}::${branch}`;
|
|
|
|
|
const nextIndicator: PrIndicator = {
|
|
|
|
|
visualState: state,
|
|
|
|
|
number: pr.number,
|
|
|
|
|
url: typeof pr.url === 'string' && pr.url.trim().length > 0 ? pr.url : null,
|
|
|
|
|
state: pr.state,
|
|
|
|
|
draft: Boolean(pr.draft),
|
|
|
|
|
title: typeof pr.title === 'string' && pr.title.trim().length > 0 ? pr.title : null,
|
|
|
|
|
base: typeof pr.base === 'string' && pr.base.trim().length > 0 ? pr.base : null,
|
|
|
|
|
head: typeof pr.head === 'string' && pr.head.trim().length > 0 ? pr.head : null,
|
|
|
|
|
checks: entry.status?.checks
|
|
|
|
|
? {
|
|
|
|
|
state: entry.status.checks.state,
|
|
|
|
|
total: entry.status.checks.total,
|
|
|
|
|
success: entry.status.checks.success,
|
|
|
|
|
failure: entry.status.checks.failure,
|
|
|
|
|
pending: entry.status.checks.pending,
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
canMerge: typeof entry.status?.canMerge === 'boolean' ? entry.status.canMerge : null,
|
|
|
|
|
mergeableState: typeof pr.mergeableState === 'string' ? pr.mergeableState : null,
|
|
|
|
|
repo: entry.status?.repo
|
|
|
|
|
? {
|
|
|
|
|
owner: entry.status.repo.owner,
|
|
|
|
|
repo: entry.status.repo.repo,
|
|
|
|
|
}
|
|
|
|
|
: null,
|
2026-02-23 03:56:38 +07:00
|
|
|
};
|
2026-03-06 17:22:59 +02:00
|
|
|
const existing = result.get(key);
|
|
|
|
|
if (!existing || getPrVisualPriority(nextIndicator.visualState) > getPrVisualPriority(existing.visualState)) {
|
|
|
|
|
result.set(key, nextIndicator);
|
2026-02-11 19:28:22 +02:00
|
|
|
}
|
2026-03-06 17:22:59 +02:00
|
|
|
});
|
2026-02-11 19:28:22 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
return result;
|
|
|
|
|
}, [prStatusEntries]);
|
|
|
|
|
|
|
|
|
|
const renderGroupSessions = React.useCallback(
|
|
|
|
|
(group: SessionGroup, groupKey: string, projectId?: string | null, hideGroupLabel?: boolean) => (
|
|
|
|
|
<SessionGroupSection
|
|
|
|
|
group={group}
|
|
|
|
|
groupKey={groupKey}
|
|
|
|
|
projectId={projectId}
|
|
|
|
|
hideGroupLabel={hideGroupLabel}
|
|
|
|
|
hasSessionSearchQuery={hasSessionSearchQuery}
|
|
|
|
|
normalizedSessionSearchQuery={normalizedSessionSearchQuery}
|
|
|
|
|
groupSearchDataByGroup={groupSearchDataByGroup}
|
|
|
|
|
expandedSessionGroups={expandedSessionGroups}
|
|
|
|
|
collapsedGroups={collapsedGroups}
|
|
|
|
|
hideDirectoryControls={hideDirectoryControls}
|
|
|
|
|
getFoldersForScope={getFoldersForScope}
|
|
|
|
|
collapsedFolderIds={collapsedFolderIds}
|
|
|
|
|
toggleFolderCollapse={toggleFolderCollapse}
|
|
|
|
|
renameFolder={renameFolder}
|
|
|
|
|
deleteFolder={deleteFolder}
|
|
|
|
|
showDeletionDialog={showDeletionDialog}
|
|
|
|
|
setDeleteFolderConfirm={setDeleteFolderConfirm}
|
|
|
|
|
renderSessionNode={renderSessionNode}
|
|
|
|
|
currentSessionDirectory={currentSessionDirectory}
|
|
|
|
|
projectRepoStatus={projectRepoStatus}
|
|
|
|
|
lastRepoStatus={lastRepoStatusRef.current}
|
|
|
|
|
toggleGroupSessionLimit={toggleGroupSessionLimit}
|
|
|
|
|
mobileVariant={mobileVariant}
|
|
|
|
|
activeProjectId={activeProjectId}
|
|
|
|
|
setActiveProjectIdOnly={setActiveProjectIdOnly}
|
|
|
|
|
setActiveMainTab={setActiveMainTab}
|
|
|
|
|
setSessionSwitcherOpen={setSessionSwitcherOpen}
|
|
|
|
|
openNewSessionDraft={openNewSessionDraft}
|
|
|
|
|
addSessionToFolder={addSessionToFolder}
|
|
|
|
|
createFolderAndStartRename={createFolderAndStartRename}
|
|
|
|
|
renamingFolderId={renamingFolderId}
|
|
|
|
|
renameFolderDraft={renameFolderDraft}
|
|
|
|
|
setRenameFolderDraft={setRenameFolderDraft}
|
|
|
|
|
setRenamingFolderId={setRenamingFolderId}
|
|
|
|
|
pinnedSessionIds={pinnedSessionIds}
|
|
|
|
|
prVisualStateByDirectoryBranch={prVisualStateByDirectoryBranch}
|
|
|
|
|
onToggleCollapsedGroup={toggleCollapsedGroup}
|
|
|
|
|
/>
|
|
|
|
|
),
|
2026-02-07 03:57:46 +02:00
|
|
|
[
|
2026-03-02 23:08:11 +00:00
|
|
|
hasSessionSearchQuery,
|
|
|
|
|
normalizedSessionSearchQuery,
|
|
|
|
|
groupSearchDataByGroup,
|
2026-03-06 17:22:59 +02:00
|
|
|
expandedSessionGroups,
|
|
|
|
|
collapsedGroups,
|
|
|
|
|
hideDirectoryControls,
|
|
|
|
|
getFoldersForScope,
|
|
|
|
|
collapsedFolderIds,
|
|
|
|
|
toggleFolderCollapse,
|
|
|
|
|
renameFolder,
|
|
|
|
|
deleteFolder,
|
|
|
|
|
showDeletionDialog,
|
|
|
|
|
renderSessionNode,
|
2026-02-07 03:57:46 +02:00
|
|
|
currentSessionDirectory,
|
|
|
|
|
projectRepoStatus,
|
|
|
|
|
toggleGroupSessionLimit,
|
2026-03-06 17:22:59 +02:00
|
|
|
mobileVariant,
|
2026-02-07 03:57:46 +02:00
|
|
|
activeProjectId,
|
2026-02-25 14:32:23 +02:00
|
|
|
setActiveProjectIdOnly,
|
2026-02-07 03:57:46 +02:00
|
|
|
setActiveMainTab,
|
|
|
|
|
setSessionSwitcherOpen,
|
|
|
|
|
openNewSessionDraft,
|
2026-02-23 03:56:38 +07:00
|
|
|
addSessionToFolder,
|
2026-03-06 17:22:59 +02:00
|
|
|
createFolderAndStartRename,
|
2026-02-21 06:09:55 +07:00
|
|
|
renamingFolderId,
|
|
|
|
|
renameFolderDraft,
|
2026-02-23 03:56:38 +07:00
|
|
|
pinnedSessionIds,
|
2026-03-06 17:22:59 +02:00
|
|
|
prVisualStateByDirectoryBranch,
|
|
|
|
|
toggleCollapsedGroup,
|
|
|
|
|
],
|
2026-01-06 21:31:04 +02:00
|
|
|
);
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
return (
|
|
|
|
|
<div
|
2026-03-02 23:08:11 +00:00
|
|
|
ref={sessionSearchContainerRef}
|
2025-12-07 19:32:53 +02:00
|
|
|
className={cn(
|
|
|
|
|
'flex h-full flex-col text-foreground overflow-x-hidden',
|
2026-02-25 14:32:23 +02:00
|
|
|
mobileVariant ? '' : 'bg-transparent',
|
2025-12-07 19:32:53 +02:00
|
|
|
)}
|
|
|
|
|
>
|
2026-03-06 17:22:59 +02:00
|
|
|
<SidebarHeader
|
|
|
|
|
hideDirectoryControls={hideDirectoryControls}
|
|
|
|
|
hideProjectSelector={hideProjectSelector}
|
|
|
|
|
activeProjectForHeader={activeProjectForHeader}
|
|
|
|
|
homeDirectory={homeDirectory}
|
|
|
|
|
normalizedProjects={normalizedProjects}
|
|
|
|
|
activeProjectId={activeProjectId}
|
|
|
|
|
setActiveProjectIdOnly={setActiveProjectIdOnly}
|
|
|
|
|
isProjectRenameInline={isProjectRenameInline}
|
|
|
|
|
setIsProjectRenameInline={setIsProjectRenameInline}
|
|
|
|
|
handleStartInlineProjectRename={handleStartInlineProjectRename}
|
|
|
|
|
handleSaveInlineProjectRename={handleSaveInlineProjectRename}
|
|
|
|
|
projectRenameDraft={projectRenameDraft}
|
|
|
|
|
setProjectRenameDraft={setProjectRenameDraft}
|
|
|
|
|
removeProject={removeProject}
|
|
|
|
|
handleOpenDirectoryDialog={handleOpenDirectoryDialog}
|
|
|
|
|
addProjectButtonClass={addProjectButtonClass}
|
|
|
|
|
headerActionIconClass={headerActionIconClass}
|
|
|
|
|
reserveHeaderActionsSpace={reserveHeaderActionsSpace}
|
|
|
|
|
stableActiveProjectIsRepo={stableActiveProjectIsRepo}
|
|
|
|
|
useMobileNotesPanel={useMobileNotesPanel}
|
|
|
|
|
projectNotesPanelOpen={projectNotesPanelOpen}
|
|
|
|
|
setProjectNotesPanelOpen={setProjectNotesPanelOpen}
|
|
|
|
|
activeProjectRefForHeader={activeProjectRefForHeader}
|
|
|
|
|
openMultiRunLauncher={openMultiRunLauncher}
|
|
|
|
|
headerActionButtonClass={headerActionButtonClass}
|
|
|
|
|
setNewWorktreeDialogOpen={setNewWorktreeDialogOpen}
|
|
|
|
|
setActiveMainTab={setActiveMainTab}
|
|
|
|
|
isSessionSearchOpen={isSessionSearchOpen}
|
|
|
|
|
setIsSessionSearchOpen={setIsSessionSearchOpen}
|
|
|
|
|
sessionSearchInputRef={sessionSearchInputRef}
|
|
|
|
|
sessionSearchQuery={sessionSearchQuery}
|
|
|
|
|
setSessionSearchQuery={setSessionSearchQuery}
|
|
|
|
|
hasSessionSearchQuery={hasSessionSearchQuery}
|
|
|
|
|
searchMatchCount={searchMatchCount}
|
|
|
|
|
/>
|
2025-12-07 19:32:53 +02:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
<SidebarProjectsList
|
|
|
|
|
sectionsForRender={sectionsForRender}
|
|
|
|
|
projectSections={projectSections}
|
|
|
|
|
activeProjectId={activeProjectId}
|
|
|
|
|
showOnlyMainWorkspace={showOnlyMainWorkspace}
|
|
|
|
|
hasSessionSearchQuery={hasSessionSearchQuery}
|
|
|
|
|
emptyState={emptyState}
|
|
|
|
|
searchEmptyState={searchEmptyState}
|
|
|
|
|
renderGroupSessions={renderGroupSessions}
|
|
|
|
|
homeDirectory={homeDirectory}
|
|
|
|
|
collapsedProjects={collapsedProjects}
|
|
|
|
|
hideDirectoryControls={hideDirectoryControls}
|
|
|
|
|
projectRepoStatus={projectRepoStatus}
|
|
|
|
|
hoveredProjectId={hoveredProjectId}
|
|
|
|
|
setHoveredProjectId={setHoveredProjectId}
|
|
|
|
|
isDesktopShellRuntime={isDesktopShellRuntime}
|
|
|
|
|
stuckProjectHeaders={stuckProjectHeaders}
|
|
|
|
|
mobileVariant={mobileVariant}
|
|
|
|
|
toggleProject={toggleProject}
|
|
|
|
|
setActiveProjectIdOnly={setActiveProjectIdOnly}
|
|
|
|
|
setActiveMainTab={setActiveMainTab}
|
|
|
|
|
setSessionSwitcherOpen={setSessionSwitcherOpen}
|
|
|
|
|
openNewSessionDraft={openNewSessionDraft}
|
|
|
|
|
createWorktreeSession={createWorktreeSession}
|
|
|
|
|
openMultiRunLauncher={openMultiRunLauncher}
|
|
|
|
|
setEditingProjectId={setEditingProjectId}
|
|
|
|
|
setEditProjectTitle={setEditProjectTitle}
|
|
|
|
|
editingProjectId={editingProjectId}
|
|
|
|
|
editProjectTitle={editProjectTitle}
|
|
|
|
|
handleSaveProjectEdit={handleSaveProjectEdit}
|
|
|
|
|
handleCancelProjectEdit={handleCancelProjectEdit}
|
|
|
|
|
removeProject={removeProject}
|
|
|
|
|
projectHeaderSentinelRefs={projectHeaderSentinelRefs}
|
|
|
|
|
settingsAutoCreateWorktree={settingsAutoCreateWorktree}
|
|
|
|
|
getOrderedGroups={getOrderedGroups}
|
|
|
|
|
setGroupOrderByProject={setGroupOrderByProject}
|
|
|
|
|
/>
|
2026-01-06 21:31:04 +02:00
|
|
|
|
2026-03-03 00:20:15 +02:00
|
|
|
<NewWorktreeDialog
|
|
|
|
|
open={newWorktreeDialogOpen}
|
|
|
|
|
onOpenChange={setNewWorktreeDialogOpen}
|
|
|
|
|
onWorktreeCreated={(worktreePath, options) => {
|
|
|
|
|
setActiveMainTab('chat');
|
|
|
|
|
if (mobileVariant) {
|
2026-02-06 02:29:26 -08:00
|
|
|
setSessionSwitcherOpen(false);
|
|
|
|
|
}
|
2026-03-03 00:20:15 +02:00
|
|
|
if (options?.sessionId) {
|
|
|
|
|
setCurrentSession(options.sessionId);
|
|
|
|
|
return;
|
2026-02-09 13:25:42 +02:00
|
|
|
}
|
2026-03-03 00:20:15 +02:00
|
|
|
openNewSessionDraft({ directoryOverride: worktreePath });
|
2026-02-09 13:25:42 +02:00
|
|
|
}}
|
|
|
|
|
/>
|
2026-02-11 23:53:37 -08:00
|
|
|
|
|
|
|
|
{useMobileNotesPanel ? (
|
|
|
|
|
<MobileOverlayPanel
|
|
|
|
|
open={projectNotesPanelOpen}
|
|
|
|
|
onClose={() => setProjectNotesPanelOpen(false)}
|
|
|
|
|
title="Project notes"
|
|
|
|
|
>
|
|
|
|
|
<ProjectNotesTodoPanel
|
|
|
|
|
projectRef={activeProjectRefForHeader}
|
2026-02-16 14:15:19 +02:00
|
|
|
canCreateWorktree={stableActiveProjectIsRepo}
|
2026-02-11 23:53:37 -08:00
|
|
|
onActionComplete={() => setProjectNotesPanelOpen(false)}
|
|
|
|
|
className="p-0"
|
|
|
|
|
/>
|
|
|
|
|
</MobileOverlayPanel>
|
|
|
|
|
) : null}
|
2026-02-23 03:56:38 +07:00
|
|
|
|
2026-03-06 17:22:59 +02:00
|
|
|
<SessionDeleteConfirmDialog
|
|
|
|
|
value={deleteSessionConfirm}
|
|
|
|
|
setValue={setDeleteSessionConfirm}
|
|
|
|
|
showDeletionDialog={showDeletionDialog}
|
|
|
|
|
setShowDeletionDialog={setShowDeletionDialog}
|
|
|
|
|
onConfirm={confirmDeleteSession}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FolderDeleteConfirmDialog
|
|
|
|
|
value={deleteFolderConfirm}
|
|
|
|
|
setValue={setDeleteFolderConfirm}
|
|
|
|
|
onConfirm={confirmDeleteFolder}
|
|
|
|
|
/>
|
2025-12-07 19:32:53 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|