mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-24 05:28:46 +02:00
Add the backend contract and session-domain support for 'Branch from here': - new branchSession IPC method and sessions:branch channel - branchOrigin metadata on SessionRecord - session branching helper that truncates the transcript at a chosen user message and clears runtime state - AryxAppService branching flow with scratchpad directory support - persistence normalization and regression coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
134 lines
4.8 KiB
TypeScript
134 lines
4.8 KiB
TypeScript
import { mkdir } from 'node:fs/promises';
|
|
|
|
import { createBuiltinPatterns, resolvePatternGraph } from '@shared/domain/pattern';
|
|
import type { PatternDefinition } from '@shared/domain/pattern';
|
|
import { isScratchpadProject, mergeScratchpadProject } from '@shared/domain/project';
|
|
import { normalizeDiscoveredToolingState } from '@shared/domain/discoveredTooling';
|
|
import { normalizeProjectCustomizationState } from '@shared/domain/projectCustomization';
|
|
import { normalizeSessionRunRecords } from '@shared/domain/runTimeline';
|
|
import { normalizeSessionBranchOrigin, type SessionRecord } from '@shared/domain/session';
|
|
import {
|
|
normalizeSessionToolingSelection,
|
|
normalizeWorkspaceSettings,
|
|
} from '@shared/domain/tooling';
|
|
import {
|
|
applyDefaultToolApprovalPolicy,
|
|
normalizePendingApprovalState,
|
|
normalizeSessionApprovalSettings,
|
|
} from '@shared/domain/approval';
|
|
import { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
|
|
import { nowIso } from '@shared/utils/ids';
|
|
|
|
import {
|
|
getScratchpadDirectoryPath,
|
|
getScratchpadSessionPath,
|
|
getWorkspaceFilePath,
|
|
} from '@main/persistence/appPaths';
|
|
import { readJsonFile, writeJsonFile } from '@main/persistence/jsonStore';
|
|
|
|
function mergePatterns(existingPatterns: PatternDefinition[]): PatternDefinition[] {
|
|
const builtinTimestamp = nowIso();
|
|
const builtinPatterns = createBuiltinPatterns(builtinTimestamp);
|
|
const builtinIds = new Set(builtinPatterns.map((pattern) => pattern.id));
|
|
const existingMap = new Map(existingPatterns.map((pattern) => [pattern.id, pattern]));
|
|
|
|
const mergedBuiltins = builtinPatterns.map((builtin) => {
|
|
const existing = existingMap.get(builtin.id);
|
|
if (!existing) {
|
|
return builtin;
|
|
}
|
|
|
|
return {
|
|
...existing,
|
|
availability: builtin.availability,
|
|
unavailabilityReason: builtin.unavailabilityReason,
|
|
mode: builtin.mode,
|
|
};
|
|
});
|
|
|
|
const customPatterns = existingPatterns.filter((pattern) => !builtinIds.has(pattern.id));
|
|
return [...mergedBuiltins, ...customPatterns];
|
|
}
|
|
|
|
export class WorkspaceRepository {
|
|
readonly filePath = getWorkspaceFilePath();
|
|
readonly scratchpadPath = getScratchpadDirectoryPath();
|
|
|
|
async load(): Promise<WorkspaceState> {
|
|
await mkdir(this.scratchpadPath, { recursive: true });
|
|
|
|
const stored = await readJsonFile<WorkspaceState>(this.filePath);
|
|
if (!stored) {
|
|
const seededBase = createWorkspaceSeed();
|
|
const projects = mergeScratchpadProject([], this.scratchpadPath);
|
|
const seeded: WorkspaceState = {
|
|
...seededBase,
|
|
projects,
|
|
selectedProjectId: projects[0]?.id,
|
|
};
|
|
await this.save(seeded);
|
|
return seeded;
|
|
}
|
|
|
|
const projects = mergeScratchpadProject(
|
|
(stored.projects ?? []).map((project) => ({
|
|
...project,
|
|
discoveredTooling: normalizeDiscoveredToolingState(project.discoveredTooling),
|
|
customization: normalizeProjectCustomizationState(project.customization),
|
|
})),
|
|
this.scratchpadPath,
|
|
);
|
|
const sessions = await Promise.all((stored.sessions ?? []).map(async (session): Promise<SessionRecord> => {
|
|
const normalizedSession: SessionRecord = {
|
|
...session,
|
|
branchOrigin: normalizeSessionBranchOrigin(session.branchOrigin),
|
|
runs: normalizeSessionRunRecords(session.runs),
|
|
tooling: normalizeSessionToolingSelection(session.tooling),
|
|
approvalSettings: normalizeSessionApprovalSettings(session.approvalSettings),
|
|
...normalizePendingApprovalState({
|
|
pendingApproval: session.pendingApproval,
|
|
pendingApprovalQueue: session.pendingApprovalQueue,
|
|
}),
|
|
};
|
|
if (!isScratchpadProject(normalizedSession.projectId)) {
|
|
return normalizedSession;
|
|
}
|
|
|
|
const cwd = normalizedSession.cwd ?? getScratchpadSessionPath(normalizedSession.id);
|
|
await mkdir(cwd, { recursive: true });
|
|
return {
|
|
...normalizedSession,
|
|
cwd,
|
|
};
|
|
}));
|
|
const settings = normalizeWorkspaceSettings(stored.settings);
|
|
|
|
const workspace: WorkspaceState = {
|
|
...stored,
|
|
patterns: mergePatterns(stored.patterns ?? []).map((pattern) => ({
|
|
...pattern,
|
|
approvalPolicy: applyDefaultToolApprovalPolicy(pattern.approvalPolicy),
|
|
graph: resolvePatternGraph(pattern),
|
|
})),
|
|
projects,
|
|
sessions,
|
|
settings,
|
|
selectedProjectId: projects.some((project) => project.id === stored.selectedProjectId)
|
|
? stored.selectedProjectId
|
|
: projects[0]?.id,
|
|
lastUpdatedAt: stored.lastUpdatedAt ?? nowIso(),
|
|
};
|
|
|
|
await this.save(workspace);
|
|
return workspace;
|
|
}
|
|
|
|
async save(workspace: WorkspaceState): Promise<void> {
|
|
const { mcpProbingServerIds: _mcpProbingServerIds, ...persistedWorkspace } = workspace;
|
|
await writeJsonFile(this.filePath, {
|
|
...persistedWorkspace,
|
|
lastUpdatedAt: nowIso(),
|
|
});
|
|
}
|
|
}
|