This repository has been archived on 2026-08-23. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
pi-web/src/client/src/controllers/workspaceController.ts
T

104 lines
5.4 KiB
TypeScript

import { api as defaultApi, type Project, type Workspace } from "../api";
import { resetWorkspaceScopedState } from "../appState";
import { mergeCachedNewSessions } from "../cachedNewSessions";
import { machineProjectKey } from "../machineKeys";
import { selectedMachineId, type GetState, type RouteTarget, type SetState, type UpdateUrl } from "./types";
import type { SessionController } from "./sessionController";
import { InMemoryWorkspaceSelectionMemory, selectPreferredWorkspace, type WorkspaceSelectionMemory } from "./workspaceSelection";
export interface WorkspaceControllerDependencies {
api?: Pick<typeof defaultApi, "sessions" | "workspaces">;
}
export class WorkspaceController {
private readonly api: Pick<typeof defaultApi, "sessions" | "workspaces">;
constructor(
private readonly getState: GetState,
private readonly setState: SetState,
private readonly updateUrl: UpdateUrl,
private readonly sessions: Pick<SessionController, "clearActiveSession" | "preferredSession" | "selectSession">,
private readonly workspaceSelection: WorkspaceSelectionMemory = new InMemoryWorkspaceSelectionMemory(),
deps: WorkspaceControllerDependencies = {},
) {
this.api = deps.api ?? defaultApi;
}
clearSelection(options?: { updateUrl?: boolean | undefined }) {
this.sessions.clearActiveSession();
this.setState({ selectedProject: undefined, selectedWorkspace: undefined, workspaces: [], isLoadingWorkspaces: false, ...resetWorkspaceScopedState() });
if (options?.updateUrl !== false) this.updateUrl();
}
forgetProject(projectId: string): void {
this.workspaceSelection.forgetProject(machineProjectKey(selectedMachineId(this.getState()), projectId));
const workspacesByProjectId = Object.fromEntries(Object.entries(this.getState().workspacesByProjectId).filter(([candidate]) => candidate !== projectId));
this.setState({ workspacesByProjectId });
}
async selectProject(project: Project, target?: RouteTarget) {
this.sessions.clearActiveSession();
this.setState({ selectedProject: project, selectedWorkspace: undefined, workspaces: [], isLoadingWorkspaces: true, ...resetWorkspaceScopedState() });
try {
const machineId = selectedMachineId(this.getState());
const workspaces = await api.workspaces(project.id, machineId);
this.setState({ workspaces, workspacesByProjectId: { ...this.getState().workspacesByProjectId, [project.id]: workspaces }, isLoadingWorkspaces: false });
const workspace = selectPreferredWorkspace(workspaces, { targetWorkspaceId: target?.workspaceId, latestWorkspaceId: this.workspaceSelection.latestWorkspaceId(machineProjectKey(machineId, project.id)) });
if (workspace) await this.selectWorkspace(workspace, { sessionId: target?.sessionId, updateUrl: target?.updateUrl });
else if (target?.updateUrl !== false) this.updateUrl();
} catch (error) {
this.setState({ error: String(error), isLoadingWorkspaces: false });
}
}
async selectWorkspace(workspace: Workspace, target?: { sessionId?: string | undefined; updateUrl?: boolean | undefined }) {
const machineId = selectedMachineId(this.getState());
this.workspaceSelection.rememberWorkspace({ ...workspace, projectId: machineProjectKey(machineId, workspace.projectId) });
this.sessions.clearActiveSession();
this.setState({ selectedWorkspace: workspace, isLoadingWorkspaces: false, ...resetWorkspaceScopedState() });
try {
const sessions = mergeCachedNewSessions(workspace.path, await api.sessions(workspace.path, machineId), machineId);
this.setState({ sessions });
const session = this.sessions.preferredSession(workspace.path, sessions, target?.sessionId);
if (session) await this.sessions.selectSession(session, { updateUrl: target?.updateUrl });
else if (target?.updateUrl !== false) this.updateUrl();
} catch (error) {
this.setState({ error: String(error) });
}
}
async refreshProjectWorkspaces(projectId: string): Promise<Workspace[]> {
const project = this.getState().projects.find((candidate) => candidate.id === projectId);
if (project === undefined) throw new Error("Project not found");
const workspaces = await this.api.workspaces(project.id);
this.applyProjectWorkspaces(project.id, workspaces);
return workspaces;
}
async refreshAfterWorkspaceDeleted(projectId: string, workspaceId: string): Promise<void> {
const workspaces = await this.refreshProjectWorkspaces(projectId);
const state = this.getState();
if (state.selectedProject?.id !== projectId || state.selectedWorkspace?.id !== workspaceId) return;
const fallback = selectFallbackWorkspace(workspaces);
if (fallback !== undefined) await this.selectWorkspace(fallback);
else this.clearSelection();
}
private applyProjectWorkspaces(projectId: string, workspaces: Workspace[]): void {
const state = this.getState();
const workspacesByProjectId = { ...state.workspacesByProjectId, [projectId]: workspaces };
if (state.selectedProject?.id === projectId) this.setState({ workspaces, workspacesByProjectId });
else this.setState({ workspacesByProjectId });
}
}
export function canDeleteWorkspace(workspace: Workspace | undefined): boolean {
return workspace !== undefined && workspace.isGitWorktree && !workspace.isMain;
}
function selectFallbackWorkspace(workspaces: Workspace[]): Workspace | undefined {
return workspaces.find((workspace) => workspace.isMain) ?? workspaces[0];
}