fix(sidebar): persist projects/workspaces and add remove project menu

Stop auto-archiving workspaces when all their agents are archived —
only archive when the directory no longer exists on disk. This prevents
projects from disappearing when users archive their last agent.

Add a kebab dropdown menu to all project rows (desktop) with a
"Remove project" action that archives all workspaces in the project.
This commit is contained in:
Mohamed Boudra
2026-04-03 21:33:05 +07:00
parent 1ae2f9280e
commit 176942bbf1
4 changed files with 132 additions and 240 deletions
-4
View File
@@ -1363,10 +1363,6 @@ export class Session {
);
const staleWorkspaceIds = await detectStaleWorkspaces({
activeWorkspaces,
agentRecords: (await this.agentStorage.list()).map((agent) => ({
cwd: agent.cwd,
archivedAt: agent.archivedAt ?? null,
})),
checkDirectoryExists: async (cwd) => {
try {
await stat(cwd);
@@ -24,7 +24,6 @@ describe("detectStaleWorkspaces", () => {
createWorkspaceRecord("/tmp/existing"),
createWorkspaceRecord("/tmp/missing"),
],
agentRecords: [],
checkDirectoryExists,
});
@@ -32,41 +31,21 @@ describe("detectStaleWorkspaces", () => {
expect(checkDirectoryExists.mock.calls).toEqual([["/tmp/existing"], ["/tmp/missing"]]);
});
test("returns workspace ids when all matching agents are archived", async () => {
test("keeps workspaces whose directories exist even when all agents are archived", async () => {
const staleWorkspaceIds = await detectStaleWorkspaces({
activeWorkspaces: [createWorkspaceRecord("/tmp/repo"), createWorkspaceRecord("/tmp/other")],
agentRecords: [
{
cwd: "/tmp/repo",
archivedAt: "2026-03-02T00:00:00.000Z",
},
{
cwd: "/tmp/other",
archivedAt: null,
},
],
checkDirectoryExists: async () => true,
});
expect(Array.from(staleWorkspaceIds)).toEqual(["/tmp/repo"]);
expect(Array.from(staleWorkspaceIds)).toEqual([]);
});
test("keeps workspaces with no agents or at least one active agent", async () => {
test("keeps workspaces with no agents when directory exists", async () => {
const staleWorkspaceIds = await detectStaleWorkspaces({
activeWorkspaces: [
createWorkspaceRecord("/tmp/active"),
createWorkspaceRecord("/tmp/no-agents"),
],
agentRecords: [
{
cwd: "/tmp/active",
archivedAt: "2026-03-02T00:00:00.000Z",
},
{
cwd: "/tmp/active/../active",
archivedAt: null,
},
],
checkDirectoryExists: async () => true,
});
@@ -6,14 +6,8 @@ import type { PersistedWorkspaceRecord } from "./workspace-registry.js";
export type PersistedProjectKind = "git" | "non_git";
export type PersistedWorkspaceKind = "local_checkout" | "worktree" | "directory";
export type StaleWorkspaceAgentRecord = {
cwd: string;
archivedAt: string | null;
};
export type DetectStaleWorkspacesInput = {
activeWorkspaces: PersistedWorkspaceRecord[];
agentRecords: StaleWorkspaceAgentRecord[];
checkDirectoryExists: (cwd: string) => Promise<boolean>;
};
@@ -143,28 +137,11 @@ export async function detectStaleWorkspaces(
input: DetectStaleWorkspacesInput,
): Promise<Set<string>> {
const staleWorkspaceIds = new Set<string>();
const cwdsWithActiveAgents = new Set<string>();
const cwdsWithAnyAgent = new Set<string>();
for (const agent of input.agentRecords) {
const normalizedCwd = normalizeWorkspaceId(agent.cwd);
cwdsWithAnyAgent.add(normalizedCwd);
if (!agent.archivedAt) {
cwdsWithActiveAgents.add(normalizedCwd);
}
}
for (const workspace of input.activeWorkspaces) {
const dirExists = await input.checkDirectoryExists(workspace.cwd);
if (!dirExists) {
staleWorkspaceIds.add(workspace.workspaceId);
continue;
}
const hasAgents = cwdsWithAnyAgent.has(workspace.workspaceId);
const hasActiveAgents = cwdsWithActiveAgents.has(workspace.workspaceId);
if (hasAgents && !hasActiveAgents) {
staleWorkspaceIds.add(workspace.workspaceId);
}
}