refactor: centralize git and pr store refresh
This commit is contained in:
@@ -15,9 +15,8 @@ import { useSync } from '@/sync/use-sync';
|
||||
import { useSessionPrefetch } from './sidebar/hooks/useSessionPrefetch';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import type { GitHubPullRequestStatus } from '@/lib/api/types';
|
||||
import { getSafeStorage } from '@/stores/utils/safeStorage';
|
||||
import { useGitStore } from '@/stores/useGitStore';
|
||||
import { useGitStore, useGitAllBranches, useGitRepoStatusMap } from '@/stores/useGitStore';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { NewWorktreeDialog } from './NewWorktreeDialog';
|
||||
@@ -37,7 +36,7 @@ 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 { getGitHubPrStatusKey, usePrVisualSummaryByKeys, useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
|
||||
import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog';
|
||||
import { UpdateDialog } from '@/components/ui/UpdateDialog';
|
||||
import { SessionGroupSection } from './sidebar/SessionGroupSection';
|
||||
@@ -48,7 +47,6 @@ import { SidebarProjectsList } from './sidebar/SidebarProjectsList';
|
||||
import { SessionNodeItem } from './sidebar/SessionNodeItem';
|
||||
import { useUpdateStore } from '@/stores/useUpdateStore';
|
||||
import { listProjectWorktrees } from '@/lib/worktrees/worktreeManager';
|
||||
import { checkIsGitRepository } from '@/lib/gitApi';
|
||||
import type { WorktreeMetadata } from '@/types/worktree';
|
||||
import type { SortableDragHandleProps } from './sidebar/sortableItems';
|
||||
import {
|
||||
@@ -71,6 +69,8 @@ import {
|
||||
normalizePath,
|
||||
} from './sidebar/utils';
|
||||
import { refreshGlobalSessions, useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||
|
||||
const PROJECT_COLLAPSE_STORAGE_KEY = 'oc.sessions.projectCollapse';
|
||||
const GROUP_ORDER_STORAGE_KEY = 'oc.sessions.groupOrder';
|
||||
@@ -105,46 +105,6 @@ type PrIndicator = {
|
||||
} | null;
|
||||
};
|
||||
|
||||
const getPrVisualState = (status: GitHubPullRequestStatus | null): PrVisualState | null => {
|
||||
const pr = status?.pr;
|
||||
if (!pr) {
|
||||
return null;
|
||||
}
|
||||
if (pr.state === 'merged') {
|
||||
return 'merged';
|
||||
}
|
||||
if (pr.state === 'closed') {
|
||||
return 'closed';
|
||||
}
|
||||
if (pr.draft) {
|
||||
return 'draft';
|
||||
}
|
||||
const checksFailed = status?.checks?.state === 'failure';
|
||||
const mergeableState = typeof pr.mergeableState === 'string' ? pr.mergeableState : '';
|
||||
const notMergeable = pr.mergeable === false || mergeableState === 'blocked' || mergeableState === 'dirty';
|
||||
if (checksFailed || notMergeable) {
|
||||
return 'blocked';
|
||||
}
|
||||
return 'open';
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
interface SessionSidebarProps {
|
||||
mobileVariant?: boolean;
|
||||
onSessionSelected?: (sessionId: string) => void;
|
||||
@@ -304,7 +264,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
sessionSearchContainerRef,
|
||||
});
|
||||
|
||||
const gitDirectories = useGitStore((state) => state.directories);
|
||||
const gitBranches = useGitAllBranches();
|
||||
|
||||
const sync = useSync();
|
||||
const syncSessions = useSessions();
|
||||
@@ -335,7 +295,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
const availableWorktreesByProject = useSessionUIStore((state) => state.availableWorktreesByProject);
|
||||
const getSessionsByDirectory = useSessionUIStore((state) => state.getSessionsByDirectory);
|
||||
const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft);
|
||||
const prStatusEntries = useGitHubPrStatusStore((state) => state.entries);
|
||||
const updateStore = useUpdateStore();
|
||||
|
||||
const sessions = React.useMemo(
|
||||
@@ -365,7 +324,9 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
const projectPath = normalizePath(project.path);
|
||||
if (!projectPath) return;
|
||||
try {
|
||||
const isGitRepo = await checkIsGitRepository(projectPath);
|
||||
// Use store-cached isGitRepo when available; fall back to direct check for initial worktree discovery
|
||||
const cachedIsGitRepo = useGitStore.getState().directories.get(projectPath)?.isGitRepo;
|
||||
const isGitRepo = cachedIsGitRepo ?? await import('@/lib/gitApi').then(m => m.checkIsGitRepository(projectPath));
|
||||
if (!isGitRepo) return;
|
||||
const worktrees = await listProjectWorktrees({ id: project.id, path: projectPath });
|
||||
if (cancelled || worktrees.length === 0) return;
|
||||
@@ -487,7 +448,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
homeDirectory,
|
||||
worktreeMetadata,
|
||||
pinnedSessionIds,
|
||||
gitDirectories,
|
||||
gitBranches,
|
||||
isVSCode,
|
||||
});
|
||||
|
||||
@@ -891,11 +852,22 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
}>;
|
||||
}, [projects]);
|
||||
|
||||
const normalizedProjectPaths = React.useMemo(
|
||||
() => normalizedProjects.map((project) => project.normalizedPath),
|
||||
[normalizedProjects],
|
||||
);
|
||||
|
||||
const { github } = useRuntimeAPIs();
|
||||
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
|
||||
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
|
||||
const gitRepoStatus = useGitRepoStatusMap(normalizedProjectPaths);
|
||||
const ensurePrStatusEntry = useGitHubPrStatusStore((state) => state.ensureEntry);
|
||||
const setPrStatusParams = useGitHubPrStatusStore((state) => state.setParams);
|
||||
const refreshPrStatusTargets = useGitHubPrStatusStore((state) => state.refreshTargets);
|
||||
|
||||
useProjectRepoStatus({
|
||||
projects,
|
||||
normalizedProjects,
|
||||
normalizePath,
|
||||
gitDirectories,
|
||||
gitRepoStatus,
|
||||
setProjectRepoStatus,
|
||||
setProjectRootBranches,
|
||||
});
|
||||
@@ -1162,6 +1134,91 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
}));
|
||||
}, [isVSCode, hasSessionSearchQuery, recentSessionIds, sectionsForRender]);
|
||||
|
||||
const prLookupKeys = React.useMemo(() => {
|
||||
const keys = new Set<string>();
|
||||
sectionsForSidebarRender.forEach((section) => {
|
||||
section.groups.forEach((group) => {
|
||||
const directory = normalizePath(group.directory ?? null);
|
||||
const branch = group.branch?.trim() || gitBranches.get(directory || '')?.trim();
|
||||
if (!directory || !branch) {
|
||||
return;
|
||||
}
|
||||
keys.add(getGitHubPrStatusKey(directory, branch));
|
||||
});
|
||||
});
|
||||
return [...keys];
|
||||
}, [gitBranches, sectionsForSidebarRender]);
|
||||
|
||||
const prVisualSummaryMap = usePrVisualSummaryByKeys(prLookupKeys);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!githubAuthChecked || !githubAuthStatus?.connected || !github) {
|
||||
return;
|
||||
}
|
||||
|
||||
const missingTargets: Array<{ directory: string; branch: string; remoteName?: string | null }> = [];
|
||||
|
||||
sectionsForSidebarRender.forEach((section) => {
|
||||
if (collapsedProjects.has(section.project.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
section.groups.forEach((group) => {
|
||||
const directory = normalizePath(group.directory ?? null);
|
||||
const branch = group.branch?.trim() || gitBranches.get(directory || '')?.trim();
|
||||
if (!directory || !branch) {
|
||||
return;
|
||||
}
|
||||
const key = getGitHubPrStatusKey(directory, branch);
|
||||
const entry = useGitHubPrStatusStore.getState().entries[key];
|
||||
if (!entry || !entry.isInitialStatusResolved) {
|
||||
missingTargets.push({ directory, branch });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (missingTargets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uniqueTargets = new Map<string, { directory: string; branch: string; remoteName?: string | null }>();
|
||||
missingTargets.forEach((target) => {
|
||||
const key = getGitHubPrStatusKey(target.directory, target.branch, target.remoteName ?? null);
|
||||
if (!uniqueTargets.has(key)) {
|
||||
uniqueTargets.set(key, target);
|
||||
}
|
||||
});
|
||||
|
||||
uniqueTargets.forEach((target, key) => {
|
||||
ensurePrStatusEntry(key);
|
||||
setPrStatusParams(key, {
|
||||
directory: target.directory,
|
||||
branch: target.branch,
|
||||
remoteName: target.remoteName ?? null,
|
||||
canShow: true,
|
||||
github,
|
||||
githubAuthChecked,
|
||||
githubConnected: githubAuthStatus.connected,
|
||||
});
|
||||
});
|
||||
|
||||
void refreshPrStatusTargets([...uniqueTargets.values()], {
|
||||
force: true,
|
||||
silent: true,
|
||||
markInitialResolved: true,
|
||||
});
|
||||
}, [
|
||||
collapsedProjects,
|
||||
ensurePrStatusEntry,
|
||||
github,
|
||||
githubAuthChecked,
|
||||
githubAuthStatus?.connected,
|
||||
gitBranches,
|
||||
refreshPrStatusTargets,
|
||||
sectionsForSidebarRender,
|
||||
setPrStatusParams,
|
||||
]);
|
||||
|
||||
const desktopHeaderActionButtonClass =
|
||||
'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';
|
||||
const mobileHeaderActionButtonClass =
|
||||
@@ -1280,55 +1337,24 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
|
||||
const prVisualStateByDirectoryBranch = React.useMemo(() => {
|
||||
const result = new Map<string, PrIndicator>();
|
||||
|
||||
Object.values(prStatusEntries).forEach((entry) => {
|
||||
const directory = normalizePath(entry.params?.directory ?? entry.identity?.directory ?? null);
|
||||
const branch = entry.params?.branch?.trim() ?? entry.identity?.branch?.trim();
|
||||
if (!directory || !branch) {
|
||||
return;
|
||||
}
|
||||
const state = getPrVisualState(entry.status ?? null);
|
||||
const pr = entry.status?.pr;
|
||||
if (!state || !pr?.number) {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
const existing = result.get(key);
|
||||
if (!existing || getPrVisualPriority(nextIndicator.visualState) > getPrVisualPriority(existing.visualState)) {
|
||||
result.set(key, nextIndicator);
|
||||
}
|
||||
});
|
||||
|
||||
for (const [key, summary] of prVisualSummaryMap) {
|
||||
result.set(key, {
|
||||
visualState: summary.visualState as PrVisualState,
|
||||
number: summary.number,
|
||||
url: summary.url,
|
||||
state: summary.prState as 'open' | 'closed' | 'merged',
|
||||
draft: summary.draft,
|
||||
title: summary.title,
|
||||
base: summary.base,
|
||||
head: summary.head,
|
||||
checks: summary.checks as PrIndicator['checks'],
|
||||
canMerge: summary.canMerge,
|
||||
mergeableState: summary.mergeableState,
|
||||
repo: summary.repo,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}, [prStatusEntries]);
|
||||
}, [prVisualSummaryMap]);
|
||||
|
||||
const renderGroupSessions = React.useCallback(
|
||||
(group: SessionGroup, groupKey: string, projectId?: string | null, hideGroupLabel?: boolean, dragHandleProps?: SortableDragHandleProps | null, compactBodyPadding?: boolean) => (
|
||||
|
||||
Reference in New Issue
Block a user