92 lines
3.4 KiB
TypeScript
92 lines
3.4 KiB
TypeScript
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<RecentProjectsSourceResult> {
|
|
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<RepositoryGroup[]> {
|
|
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();
|
|
}
|
|
}
|
|
}
|