refactor: centralize git and pr store refresh
This commit is contained in:
@@ -52,7 +52,7 @@ import { generateBranchSlug } from '@/lib/git/branchNameGenerator';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { rankBranchesForQuery } from '@/lib/worktrees/branchSearch';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { useGitBranches, useGitStore } from '@/stores/useGitStore';
|
||||
import { useGitBranches, useGitStore, useGitLoadingBranches } from '@/stores/useGitStore';
|
||||
import { GitHubIntegrationDialog } from './GitHubIntegrationDialog';
|
||||
import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip';
|
||||
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
|
||||
@@ -237,7 +237,7 @@ export function NewWorktreeDialog({
|
||||
|
||||
// Use cached branches from Git store (instant if already fetched)
|
||||
const branches = useGitBranches(projectDirectory);
|
||||
const isLoadingBranches = useGitStore((state) => state.isLoadingBranches);
|
||||
const isLoadingBranches = useGitLoadingBranches(projectDirectory);
|
||||
const fetchBranches = useGitStore((state) => state.fetchBranches);
|
||||
|
||||
// Compute local and remote branch lists (same pattern as GitView)
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -1,79 +1,59 @@
|
||||
import React from 'react';
|
||||
import { checkIsGitRepository } from '@/lib/gitApi';
|
||||
import { mapWithConcurrency } from '@/lib/concurrency';
|
||||
import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
|
||||
import { mapWithConcurrency } from '@/lib/concurrency';
|
||||
import { useGitStore } from '@/stores/useGitStore';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
|
||||
type Project = { id: string; path: string; normalizedPath: string };
|
||||
|
||||
type DirectoryState = { status?: { current?: string | null } | null };
|
||||
|
||||
type Args = {
|
||||
projects: Array<{ id: string; path: string }>;
|
||||
normalizedProjects: Project[];
|
||||
normalizePath: (value?: string | null) => string | null;
|
||||
gitDirectories: Map<string, DirectoryState>;
|
||||
gitRepoStatus: Map<string, { isGitRepo: boolean | null; branch: string | null }>;
|
||||
setProjectRepoStatus: React.Dispatch<React.SetStateAction<Map<string, boolean | null>>>;
|
||||
setProjectRootBranches: React.Dispatch<React.SetStateAction<Map<string, string>>>;
|
||||
};
|
||||
|
||||
export const useProjectRepoStatus = (args: Args): void => {
|
||||
const {
|
||||
projects,
|
||||
normalizedProjects,
|
||||
normalizePath,
|
||||
gitDirectories,
|
||||
gitRepoStatus,
|
||||
setProjectRepoStatus,
|
||||
setProjectRootBranches,
|
||||
} = args;
|
||||
|
||||
const { git } = useRuntimeAPIs();
|
||||
const ensureStatus = useGitStore((state) => state.ensureStatus);
|
||||
|
||||
// Derive repo status from centralized Git store
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
const normalized = projects
|
||||
.map((project) => ({ id: project.id, path: normalizePath(project.path) }))
|
||||
.filter((project): project is { id: string; path: string } => Boolean(project.path));
|
||||
|
||||
setProjectRepoStatus(new Map());
|
||||
|
||||
if (normalized.length === 0) {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
if (!git || normalizedProjects.length === 0) {
|
||||
setProjectRepoStatus(new Map());
|
||||
return;
|
||||
}
|
||||
|
||||
void mapWithConcurrency(normalized, 2, async (project) => {
|
||||
try {
|
||||
const result = await checkIsGitRepository(project.path);
|
||||
if (!cancelled) {
|
||||
setProjectRepoStatus((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(project.id, result);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setProjectRepoStatus((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(project.id, null);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
// Trigger ensureStatus for each project to populate store
|
||||
normalizedProjects.forEach((project) => {
|
||||
void ensureStatus(project.normalizedPath, git);
|
||||
});
|
||||
}, [normalizedProjects, git, ensureStatus, setProjectRepoStatus]);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [normalizePath, projects, setProjectRepoStatus]);
|
||||
// Read isGitRepo from the store-populated state
|
||||
React.useEffect(() => {
|
||||
const next = new Map<string, boolean | null>();
|
||||
normalizedProjects.forEach((project) => {
|
||||
next.set(project.id, gitRepoStatus.get(project.normalizedPath)?.isGitRepo ?? null);
|
||||
});
|
||||
setProjectRepoStatus(next);
|
||||
}, [normalizedProjects, gitRepoStatus, setProjectRepoStatus]);
|
||||
|
||||
const projectGitBranchesKey = React.useMemo(() => {
|
||||
return normalizedProjects
|
||||
.map((project) => {
|
||||
const dirState = gitDirectories.get(project.normalizedPath);
|
||||
return `${project.id}:${dirState?.status?.current ?? ''}`;
|
||||
const branch = gitRepoStatus.get(project.normalizedPath)?.branch ?? '';
|
||||
return `${project.id}:${branch}`;
|
||||
})
|
||||
.join('|');
|
||||
}, [normalizedProjects, gitDirectories]);
|
||||
}, [normalizedProjects, gitRepoStatus]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
@@ -15,7 +15,7 @@ type Args = {
|
||||
homeDirectory: string | null;
|
||||
worktreeMetadata: Map<string, WorktreeMetadata>;
|
||||
pinnedSessionIds: Set<string>;
|
||||
gitDirectories: Map<string, { status?: { current?: string | null } | null }>;
|
||||
gitBranches: Map<string, string | null>;
|
||||
isVSCode: boolean;
|
||||
};
|
||||
|
||||
@@ -196,7 +196,7 @@ export const useSessionGrouping = (args: Args) => {
|
||||
|
||||
sortedWorktrees.forEach((meta) => {
|
||||
const directory = normalizePath(meta.path) ?? meta.path;
|
||||
const currentBranch = args.gitDirectories.get(directory)?.status?.current?.trim() || null;
|
||||
const currentBranch = args.gitBranches.get(directory)?.trim() || null;
|
||||
const metadataBranch = meta.branch?.trim() || null;
|
||||
const shouldSyncLabelWithBranch = Boolean(
|
||||
currentBranch && metadataBranch && meta.label && normalizeForBranchComparison(meta.label) === normalizeForBranchComparison(metadataBranch),
|
||||
@@ -234,7 +234,7 @@ export const useSessionGrouping = (args: Args) => {
|
||||
|
||||
return groups;
|
||||
},
|
||||
[args.homeDirectory, args.worktreeMetadata, args.pinnedSessionIds, args.gitDirectories, args.isVSCode],
|
||||
[args.homeDirectory, args.worktreeMetadata, args.pinnedSessionIds, args.gitBranches, args.isVSCode],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user