feat(app): redirect to next workspace after archiving

This commit is contained in:
Mohamed Boudra
2026-03-20 13:08:28 +07:00
parent 70fb5f5bfd
commit 77fb74c188
5 changed files with 427 additions and 15 deletions
@@ -0,0 +1,76 @@
import { describe, expect, it } from "vitest";
import {
buildWorkspaceArchiveRedirectRoute,
resolveWorkspaceArchiveRedirectWorkspaceId,
} from "@/utils/workspace-archive-navigation";
import type { WorkspaceDescriptor } from "@/stores/session-store";
function workspace(
input: Partial<WorkspaceDescriptor> & Pick<WorkspaceDescriptor, "id">
): WorkspaceDescriptor {
return {
id: input.id,
projectId: input.projectId ?? "project-1",
projectDisplayName: input.projectDisplayName ?? "Project",
projectRootPath: input.projectRootPath ?? "/repo",
projectKind: input.projectKind ?? "git",
workspaceKind: input.workspaceKind ?? "worktree",
name: input.name ?? input.id,
status: input.status ?? "done",
activityAt: input.activityAt ?? null,
diffStat: input.diffStat ?? null,
};
}
describe("resolveWorkspaceArchiveRedirectWorkspaceId", () => {
it("redirects an archived worktree to the visible local checkout for the same project", () => {
const workspaces = [
workspace({ id: "/repo", workspaceKind: "local_checkout", name: "main" }),
workspace({ id: "/repo/.paseo/worktrees/feature", name: "feature" }),
];
expect(
resolveWorkspaceArchiveRedirectWorkspaceId({
archivedWorkspaceId: "/repo/.paseo/worktrees/feature",
workspaces,
})
).toBe("/repo");
});
it("falls back to the project root path when the root checkout is not in the visible workspace list", () => {
const workspaces = [
workspace({
id: "/repo/.paseo/worktrees/feature",
name: "feature",
projectRootPath: "/repo",
}),
];
expect(
resolveWorkspaceArchiveRedirectWorkspaceId({
archivedWorkspaceId: "/repo/.paseo/worktrees/feature",
workspaces,
})
).toBe("/repo");
});
it("falls back to the host root route when no alternate workspace target exists", () => {
const workspaces = [
workspace({
id: "/notes",
projectId: "notes",
projectRootPath: "/notes",
projectKind: "non_git",
workspaceKind: "directory",
}),
];
expect(
buildWorkspaceArchiveRedirectRoute({
serverId: "server-1",
archivedWorkspaceId: "/notes",
workspaces,
})
).toBe("/h/server-1");
});
});
@@ -0,0 +1,67 @@
import type { WorkspaceDescriptor } from "@/stores/session-store";
import { buildHostRootRoute, buildHostWorkspaceRoute } from "@/utils/host-routes";
function trimNonEmpty(value: string | null | undefined): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
export function resolveWorkspaceArchiveRedirectWorkspaceId(input: {
archivedWorkspaceId: string;
workspaces: Iterable<WorkspaceDescriptor>;
}): string | null {
const archivedWorkspaceId = trimNonEmpty(input.archivedWorkspaceId);
if (!archivedWorkspaceId) {
return null;
}
const workspaces = Array.from(input.workspaces);
const archivedWorkspace =
workspaces.find((workspace) => workspace.id === archivedWorkspaceId) ?? null;
if (!archivedWorkspace) {
return null;
}
const sameProjectWorkspaces = workspaces.filter(
(workspace) => workspace.projectId === archivedWorkspace.projectId
);
const rootCheckoutWorkspace =
sameProjectWorkspaces.find(
(workspace) =>
workspace.workspaceKind === "local_checkout" &&
workspace.id !== archivedWorkspace.id
) ?? null;
if (rootCheckoutWorkspace) {
return rootCheckoutWorkspace.id;
}
const fallbackProjectRootPath = trimNonEmpty(archivedWorkspace.projectRootPath);
if (fallbackProjectRootPath && fallbackProjectRootPath !== archivedWorkspace.id) {
return fallbackProjectRootPath;
}
const siblingWorkspace =
sameProjectWorkspaces.find((workspace) => workspace.id !== archivedWorkspace.id) ?? null;
return siblingWorkspace?.id ?? null;
}
export function buildWorkspaceArchiveRedirectRoute(input: {
serverId: string;
archivedWorkspaceId: string;
workspaces: Iterable<WorkspaceDescriptor>;
}): string {
const redirectWorkspaceId = resolveWorkspaceArchiveRedirectWorkspaceId({
archivedWorkspaceId: input.archivedWorkspaceId,
workspaces: input.workspaces,
});
if (!redirectWorkspaceId) {
return buildHostRootRoute(input.serverId);
}
return buildHostWorkspaceRoute(input.serverId, redirectWorkspaceId);
}