* fix: update sidebar store after worktree creation * fix: update sidebar store after worktree removal * fix: update sidebar store with correct key in removeProjectWorktree - Fixed key mismatch in sidebar store updates for worktree removal - Removed worktrees now properly disappear from sidebar - Aligned remove path with create path key resolution * fix: resolve sidebar stale state after folder, worktree, project, and session mutations * fix: now builds * Fix worktree sidebar store key --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
175 lines
5.7 KiB
TypeScript
175 lines
5.7 KiB
TypeScript
import React from 'react';
|
|
import type { Session } from '@opencode-ai/sdk/v2';
|
|
import type { SessionGroup, SessionNode, GroupSearchData } from '../types';
|
|
import { dedupeSessionsById, normalizePath } from '../utils';
|
|
import type { WorktreeMetadata } from '@/types/worktree';
|
|
import type { SessionFoldersMap } from '@/stores/useSessionFoldersStore';
|
|
|
|
type ProjectItem = {
|
|
id: string;
|
|
path: string;
|
|
label?: string;
|
|
normalizedPath: string;
|
|
icon?: string;
|
|
color?: string;
|
|
iconImage?: { mime: string; updatedAt: number; source: 'custom' | 'auto' };
|
|
iconBackground?: string;
|
|
};
|
|
|
|
type ProjectSection = {
|
|
project: ProjectItem;
|
|
groups: SessionGroup[];
|
|
};
|
|
|
|
type Args = {
|
|
normalizedProjects: ProjectItem[];
|
|
getSessionsForProject: (project: { normalizedPath: string }) => Session[];
|
|
getArchivedSessionsForProject: (project: { normalizedPath: string }) => Session[];
|
|
availableWorktreesByProject: Map<string, WorktreeMetadata[]>;
|
|
projectRepoStatus: Map<string, boolean | null>;
|
|
projectRootBranches: Map<string, string | null>;
|
|
lastRepoStatus: boolean;
|
|
buildGroupedSessions: (
|
|
sessions: Session[],
|
|
projectRoot: string,
|
|
availableWorktrees: WorktreeMetadata[],
|
|
rootBranch: string | null,
|
|
isRepo: boolean,
|
|
) => SessionGroup[];
|
|
hasSessionSearchQuery: boolean;
|
|
normalizedSessionSearchQuery: string;
|
|
filterSessionNodesForSearch: (nodes: SessionNode[], query: string) => SessionNode[];
|
|
buildGroupSearchText: (group: SessionGroup) => string;
|
|
foldersMap: SessionFoldersMap;
|
|
};
|
|
|
|
export const useSessionSidebarSections = (args: Args) => {
|
|
const {
|
|
normalizedProjects,
|
|
getSessionsForProject,
|
|
getArchivedSessionsForProject,
|
|
availableWorktreesByProject,
|
|
projectRepoStatus,
|
|
projectRootBranches,
|
|
lastRepoStatus,
|
|
buildGroupedSessions,
|
|
hasSessionSearchQuery,
|
|
normalizedSessionSearchQuery,
|
|
filterSessionNodesForSearch,
|
|
buildGroupSearchText,
|
|
foldersMap,
|
|
} = args;
|
|
|
|
const projectSections = React.useMemo<ProjectSection[]>(() => {
|
|
return normalizedProjects.map((project) => {
|
|
const projectSessions = dedupeSessionsById([
|
|
...getSessionsForProject(project),
|
|
...getArchivedSessionsForProject(project),
|
|
]);
|
|
const worktreesForProject = availableWorktreesByProject.get(project.normalizedPath) ?? [];
|
|
const isRepo = projectRepoStatus.has(project.id)
|
|
? Boolean(projectRepoStatus.get(project.id))
|
|
: lastRepoStatus;
|
|
const groups = buildGroupedSessions(
|
|
projectSessions,
|
|
project.normalizedPath,
|
|
worktreesForProject,
|
|
projectRootBranches.get(project.id) ?? null,
|
|
isRepo,
|
|
);
|
|
return { project, groups };
|
|
});
|
|
}, [
|
|
normalizedProjects,
|
|
getSessionsForProject,
|
|
getArchivedSessionsForProject,
|
|
availableWorktreesByProject,
|
|
projectRepoStatus,
|
|
lastRepoStatus,
|
|
buildGroupedSessions,
|
|
projectRootBranches,
|
|
]);
|
|
|
|
const visibleProjectSections = React.useMemo(() => {
|
|
return projectSections;
|
|
}, [projectSections]);
|
|
|
|
const groupSearchDataByGroup = React.useMemo(() => {
|
|
const result = new WeakMap<SessionGroup, GroupSearchData>();
|
|
if (!hasSessionSearchQuery) {
|
|
return result;
|
|
}
|
|
|
|
const countNodes = (nodes: SessionNode[]): number => nodes.reduce((total, node) => total + 1 + countNodes(node.children), 0);
|
|
|
|
visibleProjectSections.forEach((section) => {
|
|
section.groups.forEach((group) => {
|
|
const filteredNodes = filterSessionNodesForSearch(group.sessions, normalizedSessionSearchQuery);
|
|
const matchedSessionCount = countNodes(filteredNodes);
|
|
const groupMatches = buildGroupSearchText(group).includes(normalizedSessionSearchQuery);
|
|
const scopeKey = normalizePath(group.directory ?? null);
|
|
const scopeFolders = scopeKey ? (foldersMap[scopeKey] ?? []) : [];
|
|
const folderNameMatchCount = scopeFolders.filter((folder) => folder.name.toLowerCase().includes(normalizedSessionSearchQuery)).length;
|
|
|
|
result.set(group, {
|
|
filteredNodes,
|
|
matchedSessionCount,
|
|
folderNameMatchCount,
|
|
groupMatches,
|
|
hasMatch: groupMatches || matchedSessionCount > 0 || folderNameMatchCount > 0,
|
|
});
|
|
});
|
|
});
|
|
|
|
return result;
|
|
}, [
|
|
hasSessionSearchQuery,
|
|
visibleProjectSections,
|
|
filterSessionNodesForSearch,
|
|
normalizedSessionSearchQuery,
|
|
buildGroupSearchText,
|
|
foldersMap,
|
|
]);
|
|
|
|
const searchableProjectSections = React.useMemo(() => {
|
|
if (!hasSessionSearchQuery) {
|
|
return visibleProjectSections;
|
|
}
|
|
|
|
return visibleProjectSections
|
|
.map((section) => ({
|
|
...section,
|
|
groups: section.groups.filter((group) => groupSearchDataByGroup.get(group)?.hasMatch === true),
|
|
}))
|
|
.filter((section) => section.groups.length > 0);
|
|
}, [hasSessionSearchQuery, visibleProjectSections, groupSearchDataByGroup]);
|
|
|
|
const sectionsForRender = hasSessionSearchQuery ? searchableProjectSections : visibleProjectSections;
|
|
|
|
const searchMatchCount = React.useMemo(() => {
|
|
if (!hasSessionSearchQuery) {
|
|
return 0;
|
|
}
|
|
|
|
return sectionsForRender.reduce((total, section) => {
|
|
return total + section.groups.reduce((groupTotal, group) => {
|
|
const data = groupSearchDataByGroup.get(group);
|
|
if (!data) {
|
|
return groupTotal;
|
|
}
|
|
const metadataMatches = data.folderNameMatchCount + (data.groupMatches ? 1 : 0);
|
|
return groupTotal + data.matchedSessionCount + metadataMatches;
|
|
}, 0);
|
|
}, 0);
|
|
}, [hasSessionSearchQuery, sectionsForRender, groupSearchDataByGroup]);
|
|
|
|
return {
|
|
projectSections,
|
|
visibleProjectSections,
|
|
groupSearchDataByGroup,
|
|
searchableProjectSections,
|
|
sectionsForRender,
|
|
searchMatchCount,
|
|
};
|
|
};
|