diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a1a00c7..731b9208 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,6 +45,9 @@ jobs: node-version: 22 cache: pnpm + - name: Restore pnpm node-gyp executable bit + run: find "$(dirname "$(command -v pnpm)")/store" -path '*/node-gyp/gyp/gyp_main.py' -exec chmod +x {} \; 2>/dev/null || true + - name: Install dependencies run: pnpm install --frozen-lockfile @@ -336,6 +339,9 @@ jobs: with: python-version: '3.11' + - name: Restore pnpm node-gyp executable bit + run: find "$(dirname "$(command -v pnpm)")/store" -path '*/node-gyp/gyp/gyp_main.py' -exec chmod +x {} \; 2>/dev/null || true + - name: Install dependencies run: pnpm install --frozen-lockfile @@ -452,6 +458,9 @@ jobs: with: python-version: '3.11' + - name: Restore pnpm node-gyp executable bit + run: find "$(dirname "$(command -v pnpm)")/store" -path '*/node-gyp/gyp/gyp_main.py' -exec chmod +x {} \; 2>/dev/null || true + - name: Install dependencies run: pnpm install --frozen-lockfile @@ -573,6 +582,9 @@ jobs: sudo apt-get update sudo apt-get install -y libarchive-tools rpm xvfb + - name: Restore pnpm node-gyp executable bit + run: find "$(dirname "$(command -v pnpm)")/store" -path '*/node-gyp/gyp/gyp_main.py' -exec chmod +x {} \; 2>/dev/null || true + - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/src/main/index.ts b/src/main/index.ts index 5c735e63..985f579e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -927,7 +927,7 @@ let shutdownComplete = false; const startupTimers = new Set>(); const SHUTDOWN_STEP_TIMEOUT_MS = 5_000; -const STARTUP_RECOVERY_DELAY_MS = 60_000; +const STARTUP_RECOVERY_DELAY_MS = 10_000; const STARTUP_CLI_WARMUP_DELAY_MS = 90_000; const STARTUP_BACKGROUND_SERVICE_DELAY_MS = 5_000; const STARTUP_RECOVERY_CONCURRENCY = 1; diff --git a/src/renderer/components/sidebar/DateGroupedSessions.tsx b/src/renderer/components/sidebar/DateGroupedSessions.tsx index 5c539408..83ffad22 100644 --- a/src/renderer/components/sidebar/DateGroupedSessions.tsx +++ b/src/renderer/components/sidebar/DateGroupedSessions.tsx @@ -274,24 +274,26 @@ export const DateGroupedSessions = memo((): React.JSX.Element => { // Loading guards in the store actions prevent duplicate IPC calls // when the centralized init chain has already started a fetch. const repositoryGroupsLoading = useStore((s) => s.repositoryGroupsLoading); + const repositoryGroupsInitialized = useStore((s) => s.repositoryGroupsInitialized); const repositoryGroupsError = useStore((s) => s.repositoryGroupsError); const projectsLoading = useStore((s) => s.projectsLoading); + const projectsInitialized = useStore((s) => s.projectsInitialized); const projectsError = useStore((s) => s.projectsError); useEffect(() => { if ( viewMode === 'grouped' && - repositoryGroups.length === 0 && + !repositoryGroupsInitialized && !repositoryGroupsLoading && !repositoryGroupsError ) { void fetchRepositoryGroups(); - } else if (viewMode === 'flat' && projects.length === 0 && !projectsLoading && !projectsError) { + } else if (viewMode === 'flat' && !projectsInitialized && !projectsLoading && !projectsError) { void fetchProjects(); } }, [ viewMode, - repositoryGroups.length, - projects.length, + repositoryGroupsInitialized, + projectsInitialized, repositoryGroupsLoading, repositoryGroupsError, projectsLoading, diff --git a/src/renderer/components/sidebar/GlobalTaskList.tsx b/src/renderer/components/sidebar/GlobalTaskList.tsx index db7d61bb..ce45c50b 100644 --- a/src/renderer/components/sidebar/GlobalTaskList.tsx +++ b/src/renderer/components/sidebar/GlobalTaskList.tsx @@ -206,10 +206,12 @@ export const GlobalTaskList = memo(function GlobalTaskList({ softDeleteTask, projects, projectsLoading, + projectsInitialized, projectsError, viewMode, repositoryGroups, repositoryGroupsLoading, + repositoryGroupsInitialized, repositoryGroupsError, teams, provisioningRuns, @@ -226,10 +228,12 @@ export const GlobalTaskList = memo(function GlobalTaskList({ softDeleteTask: s.softDeleteTask, projects: s.projects, projectsLoading: s.projectsLoading, + projectsInitialized: s.projectsInitialized, projectsError: s.projectsError, viewMode: s.viewMode, repositoryGroups: s.repositoryGroups, repositoryGroupsLoading: s.repositoryGroupsLoading, + repositoryGroupsInitialized: s.repositoryGroupsInitialized, repositoryGroupsError: s.repositoryGroupsError, teams: s.teams, provisioningRuns: s.provisioningRuns, @@ -457,22 +461,22 @@ export const GlobalTaskList = memo(function GlobalTaskList({ useEffect(() => { if ( viewMode === 'grouped' && - repositoryGroups.length === 0 && + !repositoryGroupsInitialized && !repositoryGroupsLoading && !repositoryGroupsError ) { void fetchRepositoryGroups(); - } else if (viewMode === 'flat' && projects.length === 0 && !projectsLoading && !projectsError) { + } else if (viewMode === 'flat' && !projectsInitialized && !projectsLoading && !projectsError) { void fetchProjects(); } }, [ fetchProjects, fetchRepositoryGroups, - projects.length, projectsError, + projectsInitialized, projectsLoading, - repositoryGroups.length, repositoryGroupsError, + repositoryGroupsInitialized, repositoryGroupsLoading, viewMode, ]); diff --git a/src/renderer/store/slices/projectSlice.ts b/src/renderer/store/slices/projectSlice.ts index 010427a8..6e74b25d 100644 --- a/src/renderer/store/slices/projectSlice.ts +++ b/src/renderer/store/slices/projectSlice.ts @@ -19,6 +19,7 @@ export interface ProjectSlice { projects: Project[]; selectedProjectId: string | null; projectsLoading: boolean; + projectsInitialized: boolean; projectsError: string | null; // Actions @@ -35,6 +36,7 @@ export const createProjectSlice: StateCreator = projects: [], selectedProjectId: null, projectsLoading: false, + projectsInitialized: false, projectsError: null, // Fetch all projects from main process @@ -48,7 +50,7 @@ export const createProjectSlice: StateCreator = const sorted = [...projects].sort( (a, b) => (b.mostRecentSession ?? 0) - (a.mostRecentSession ?? 0) ); - set({ projects: sorted, projectsLoading: false }); + set({ projects: sorted, projectsLoading: false, projectsInitialized: true }); } catch (error) { set({ projectsError: error instanceof Error ? error.message : 'Failed to fetch projects', diff --git a/src/renderer/store/slices/repositorySlice.ts b/src/renderer/store/slices/repositorySlice.ts index 1eb6af45..0d439c68 100644 --- a/src/renderer/store/slices/repositorySlice.ts +++ b/src/renderer/store/slices/repositorySlice.ts @@ -39,6 +39,7 @@ export interface RepositorySlice { selectedRepositoryId: string | null; selectedWorktreeId: string | null; repositoryGroupsLoading: boolean; + repositoryGroupsInitialized: boolean; repositoryGroupsError: string | null; viewMode: 'flat' | 'grouped'; @@ -62,6 +63,7 @@ export const createRepositorySlice: StateCreator= 2000) { logger.warn(`fetchRepositoryGroups slow ms=${ms} count=${groups.length}`); diff --git a/test/renderer/components/sidebar/GlobalTaskList.test.ts b/test/renderer/components/sidebar/GlobalTaskList.test.ts index e0651516..01294f1d 100644 --- a/test/renderer/components/sidebar/GlobalTaskList.test.ts +++ b/test/renderer/components/sidebar/GlobalTaskList.test.ts @@ -15,6 +15,7 @@ interface StoreState { softDeleteTask: ReturnType; projects: { path: string; name: string; sessions: unknown[]; totalSessions?: number }[]; projectsLoading: boolean; + projectsInitialized: boolean; projectsError: string | null; viewMode: 'flat' | 'grouped'; repositoryGroups: { @@ -24,6 +25,7 @@ interface StoreState { worktrees: { path: string }[]; }[]; repositoryGroupsLoading: boolean; + repositoryGroupsInitialized: boolean; repositoryGroupsError: string | null; teams: (Pick & Partial)[]; provisioningRuns: Record; @@ -216,10 +218,12 @@ describe('GlobalTaskList project grouping', () => { storeState.softDeleteTask = vi.fn(() => Promise.resolve(undefined)); storeState.projects = []; storeState.projectsLoading = false; + storeState.projectsInitialized = false; storeState.projectsError = null; storeState.viewMode = 'flat'; storeState.repositoryGroups = []; storeState.repositoryGroupsLoading = false; + storeState.repositoryGroupsInitialized = false; storeState.repositoryGroupsError = null; storeState.teams = [{ teamName: 'alpha-team', displayName: 'Alpha Team' }]; storeState.provisioningRuns = {}; @@ -310,6 +314,43 @@ describe('GlobalTaskList project grouping', () => { }); }); + it('does not refetch repository groups after an empty grouped result is initialized', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + storeState.viewMode = 'grouped'; + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render(React.createElement(GlobalTaskList)); + await flushMicrotasks(); + }); + + expect(storeState.fetchRepositoryGroups).toHaveBeenCalledTimes(1); + + storeState.repositoryGroupsLoading = true; + await act(async () => { + notifyStoreUpdate(); + await flushMicrotasks(); + }); + + storeState.repositoryGroupsLoading = false; + storeState.repositoryGroupsInitialized = true; + storeState.repositoryGroups = []; + await act(async () => { + notifyStoreUpdate(); + await flushMicrotasks(); + }); + + expect(storeState.fetchRepositoryGroups).toHaveBeenCalledTimes(1); + + await act(async () => { + root.unmount(); + await flushMicrotasks(); + }); + }); + it('shows five tasks first, then expands and collapses with Show more and Show less', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.globalTasks = Array.from({ length: 6 }, (_, index) => makeTask(index + 1));