- Added support for a new worker in the team file system to list teams, improving performance and reliability in team management. - Introduced event loop lag monitoring in various IPC handlers to track and log slow operations, enhancing observability. - Implemented caching for CLI installation status to reduce redundant calls and improve responsiveness. - Updated project and team data services to include total session counts, optimizing data handling and performance. - Enhanced error handling and logging across multiple services to provide clearer insights into performance issues and failures. Made-with: Cursor
166 lines
5.3 KiB
TypeScript
166 lines
5.3 KiB
TypeScript
/**
|
|
* Repository slice - manages repository grouping state (worktree support).
|
|
*/
|
|
|
|
import { api } from '@renderer/api';
|
|
import { createLogger } from '@shared/utils/logger';
|
|
|
|
import { getSessionResetState } from '../utils/stateResetHelpers';
|
|
|
|
import type { AppState } from '../types';
|
|
import type { RepositoryGroup } from '@renderer/types/data';
|
|
import type { StateCreator } from 'zustand';
|
|
|
|
const logger = createLogger('Store:repository');
|
|
const FETCH_REPOSITORY_GROUPS_TIMEOUT_MS = 30_000;
|
|
|
|
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
const timeoutPromise = new Promise<T>((_resolve, reject) => {
|
|
timeoutId = setTimeout(
|
|
() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),
|
|
timeoutMs
|
|
);
|
|
});
|
|
try {
|
|
return await Promise.race([promise, timeoutPromise]);
|
|
} finally {
|
|
if (timeoutId) clearTimeout(timeoutId);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Slice Interface
|
|
// =============================================================================
|
|
|
|
export interface RepositorySlice {
|
|
// State
|
|
repositoryGroups: RepositoryGroup[];
|
|
selectedRepositoryId: string | null;
|
|
selectedWorktreeId: string | null;
|
|
repositoryGroupsLoading: boolean;
|
|
repositoryGroupsError: string | null;
|
|
viewMode: 'flat' | 'grouped';
|
|
|
|
// Actions
|
|
fetchRepositoryGroups: () => Promise<void>;
|
|
selectRepository: (repositoryId: string) => void;
|
|
selectWorktree: (worktreeId: string) => void;
|
|
setViewMode: (mode: 'flat' | 'grouped') => void;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Slice Creator
|
|
// =============================================================================
|
|
|
|
export const createRepositorySlice: StateCreator<AppState, [], [], RepositorySlice> = (
|
|
set,
|
|
get
|
|
) => ({
|
|
// Initial state
|
|
repositoryGroups: [],
|
|
selectedRepositoryId: null,
|
|
selectedWorktreeId: null,
|
|
repositoryGroupsLoading: false,
|
|
repositoryGroupsError: null,
|
|
viewMode: 'grouped', // Default to grouped view
|
|
|
|
// Fetch all repository groups (projects grouped by git repo)
|
|
fetchRepositoryGroups: async () => {
|
|
// Guard: prevent concurrent fetches (component mount + centralized init chain)
|
|
if (get().repositoryGroupsLoading) return;
|
|
const startedAt = Date.now();
|
|
set({ repositoryGroupsLoading: true, repositoryGroupsError: null });
|
|
try {
|
|
const groups = await withTimeout(
|
|
api.getRepositoryGroups(),
|
|
FETCH_REPOSITORY_GROUPS_TIMEOUT_MS,
|
|
'get-repository-groups'
|
|
);
|
|
// Already sorted by most recent session in the scanner
|
|
set({ repositoryGroups: groups, repositoryGroupsLoading: false });
|
|
const ms = Date.now() - startedAt;
|
|
if (ms >= 2000) {
|
|
logger.warn(`fetchRepositoryGroups slow ms=${ms} count=${groups.length}`);
|
|
}
|
|
} catch (error) {
|
|
const ms = Date.now() - startedAt;
|
|
logger.warn(
|
|
`fetchRepositoryGroups failed ms=${ms}: ${error instanceof Error ? error.message : String(error)}`
|
|
);
|
|
set({
|
|
repositoryGroupsError:
|
|
error instanceof Error ? error.message : 'Failed to fetch repository groups',
|
|
repositoryGroupsLoading: false,
|
|
});
|
|
}
|
|
},
|
|
|
|
// Select a repository group and auto-select a worktree
|
|
selectRepository: (repositoryId: string) => {
|
|
const { repositoryGroups } = get();
|
|
const repo = repositoryGroups.find((r) => r.id === repositoryId);
|
|
|
|
if (!repo) {
|
|
logger.warn('Repository not found:', repositoryId);
|
|
return;
|
|
}
|
|
|
|
// Auto-select worktree:
|
|
// 1. Prefer the "Default" worktree (isMainWorktree = true)
|
|
// 2. Otherwise, select the first worktree (already sorted by most recent)
|
|
const defaultWorktree = repo.worktrees.find((w) => w.isMainWorktree);
|
|
const worktreeToSelect = defaultWorktree ?? repo.worktrees[0];
|
|
|
|
if (worktreeToSelect) {
|
|
set({
|
|
selectedRepositoryId: repositoryId,
|
|
selectedWorktreeId: worktreeToSelect.id,
|
|
selectedProjectId: worktreeToSelect.id,
|
|
activeProjectId: worktreeToSelect.id,
|
|
sidebarCollapsed: false, // Ensure session list is visible when a project is selected
|
|
...getSessionResetState(),
|
|
});
|
|
// Fetch sessions for this worktree
|
|
void get().fetchSessionsInitial(worktreeToSelect.id);
|
|
} else {
|
|
// No worktrees available (shouldn't happen normally)
|
|
set({
|
|
selectedRepositoryId: repositoryId,
|
|
selectedWorktreeId: null,
|
|
...getSessionResetState(),
|
|
});
|
|
}
|
|
},
|
|
|
|
// Select a worktree within a repository group
|
|
selectWorktree: (worktreeId: string) => {
|
|
set({
|
|
selectedWorktreeId: worktreeId,
|
|
selectedProjectId: worktreeId,
|
|
activeProjectId: worktreeId,
|
|
...getSessionResetState(),
|
|
});
|
|
|
|
// Fetch sessions for this worktree
|
|
void get().fetchSessionsInitial(worktreeId);
|
|
},
|
|
|
|
// Toggle between flat and grouped view modes
|
|
setViewMode: (mode: 'flat' | 'grouped') => {
|
|
set({
|
|
viewMode: mode,
|
|
selectedRepositoryId: null,
|
|
selectedWorktreeId: null,
|
|
selectedProjectId: null,
|
|
...getSessionResetState(),
|
|
});
|
|
|
|
// Fetch the appropriate data for the new mode
|
|
if (mode === 'grouped') {
|
|
void get().fetchRepositoryGroups();
|
|
} else {
|
|
void get().fetchProjects();
|
|
}
|
|
},
|
|
});
|