import { normalizeIdentityPath } from '@features/recent-projects/main/infrastructure/identity/normalizeIdentityPath'; import { WorktreeGrouper } from '@main/services/discovery/WorktreeGrouper'; import { getProjectsBasePath } from '@main/utils/pathDecoder'; import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath'; import type { LoggerPort } from '@features/recent-projects/core/application/ports/LoggerPort'; import type { RecentProjectsSourcePort, RecentProjectsSourceResult, } from '@features/recent-projects/core/application/ports/RecentProjectsSourcePort'; import type { RecentProjectCandidate } from '@features/recent-projects/core/domain/models/RecentProjectCandidate'; import type { ServiceContext } from '@main/services'; import type { RepositoryGroup, Worktree } from '@main/types'; function selectPreferredWorktree(worktrees: readonly Worktree[]): Worktree | undefined { return worktrees.find((worktree) => worktree.isMainWorktree) ?? worktrees[0]; } function toCandidate(repo: RepositoryGroup): RecentProjectCandidate | null { const selectableWorktrees = repo.worktrees.filter( (worktree) => !isEphemeralProjectPath(worktree.path) ); if (!selectableWorktrees.length || !repo.mostRecentSession) { return null; } const preferredWorktree = selectPreferredWorktree(selectableWorktrees); if (!preferredWorktree) { return null; } return { identity: repo.identity?.id ?? `path:${normalizeIdentityPath(preferredWorktree.path)}`, displayName: repo.name, primaryPath: preferredWorktree.path, associatedPaths: selectableWorktrees.map((worktree) => worktree.path), lastActivityAt: repo.mostRecentSession, providerIds: ['anthropic'], sourceKind: 'claude', openTarget: { type: 'existing-worktree', repositoryId: repo.id, worktreeId: preferredWorktree.id, }, branchName: preferredWorktree.gitBranch, filesystemState: preferredWorktree.filesystemState ?? 'available', }; } export class ClaudeRecentProjectsSourceAdapter implements RecentProjectsSourcePort { readonly #localWorktreeGrouper = new WorktreeGrouper(getProjectsBasePath()); constructor( private readonly getActiveContext: () => ServiceContext, private readonly logger: LoggerPort ) {} async list(): Promise { const activeContext = this.getActiveContext(); const groups = activeContext.type === 'local' ? await this.#groupLocalProjects(activeContext) : await activeContext.projectScanner.scanWithWorktreeGrouping(); const candidates = groups .map((group) => toCandidate(group)) .filter((candidate): candidate is RecentProjectCandidate => candidate !== null); this.logger.info('claude recent-projects source loaded', { count: candidates.length, contextId: activeContext.id, }); return { candidates, degraded: false, }; } async #groupLocalProjects(activeContext: ServiceContext): Promise { try { const projects = await activeContext.projectScanner.scan(); return await this.#localWorktreeGrouper.groupByRepository(projects); } catch (error) { this.logger.warn('claude recent-projects fell back to simplified grouping', { error: error instanceof Error ? error.message : String(error), }); return activeContext.projectScanner.scanWithWorktreeGrouping(); } } }