agent-ecosystem/src/renderer/store/slices/repositorySlice.ts
iliya a30727d3b0 feat: enhance team file handling and logging improvements
- 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
2026-03-03 22:00:11 +02:00

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();
}
},
});