Make New Workspace an app-wide screen (#1746)

* fix(app): make new workspace global

New Workspace now opens as an app-wide screen. Host and project context can still preselect the form through query params, but the route itself no longer lives under a host.

* fix(app): simplify new workspace selection
This commit is contained in:
Mohamed Boudra
2026-06-26 15:50:02 +08:00
committed by GitHub
parent 14a91d889c
commit 3fbd82664d
14 changed files with 140 additions and 89 deletions
+20 -14
View File
@@ -1,10 +1,10 @@
import { describe, expect, it } from "vitest";
import {
buildHostAgentDetailRoute,
buildHostNewWorkspaceRoute,
buildHostRootRoute,
buildHostWorkspaceOpenRoute,
buildHostWorkspaceRoute,
buildNewWorkspaceRoute,
buildOpenProjectRoute,
resolveKnownHostRoute,
buildSessionsRoute,
@@ -149,19 +149,6 @@ describe("workspace route parsing", () => {
).toBe("/h/local/workspace/b64_L3RtcC9yZXBv");
});
it("builds a global new workspace route without a source directory", () => {
expect(buildHostNewWorkspaceRoute("local")).toBe("/h/local/new");
});
it("builds a project shortcut new workspace route with initial project context", () => {
expect(
buildHostNewWorkspaceRoute("local", "/repo/project", {
displayName: "Project",
projectId: "project-1",
}),
).toBe("/h/local/new?dir=%2Frepo%2Fproject&name=Project&projectId=project-1");
});
it("round-trips URL-safe IDs through encode/decode", () => {
const ids = ["1", "40", "164", "9999", "workspace-1", "opaque_id.v2~test"];
for (const id of ids) {
@@ -216,6 +203,25 @@ describe("global routes", () => {
it("buildSessionsRoute returns the all-host Sessions route", () => {
expect(buildSessionsRoute()).toBe("/sessions");
});
it("buildNewWorkspaceRoute returns the all-host New Workspace route", () => {
expect(buildNewWorkspaceRoute()).toBe("/new");
});
it("buildNewWorkspaceRoute accepts an initial host", () => {
expect(buildNewWorkspaceRoute({ serverId: "local" })).toBe("/new?serverId=local");
});
it("buildNewWorkspaceRoute accepts initial project context", () => {
expect(
buildNewWorkspaceRoute({
serverId: "local",
sourceDirectory: "/repo/project",
displayName: "Project",
projectId: "project-1",
}),
).toBe("/new?serverId=local&dir=%2Frepo%2Fproject&name=Project&projectId=project-1");
});
});
describe("host settings section slugs", () => {
+33 -26
View File
@@ -414,6 +414,39 @@ export function buildOpenProjectRoute() {
return "/open-project" as const;
}
interface NewWorkspaceRouteOptions {
serverId?: string;
sourceDirectory?: string;
displayName?: string;
projectId?: string;
}
function buildNewWorkspaceSearch(options: NewWorkspaceRouteOptions): string {
const params = new URLSearchParams();
const serverId = trimNonEmpty(options.serverId);
if (serverId) {
params.set("serverId", serverId);
}
if (options.sourceDirectory) {
params.set("dir", options.sourceDirectory);
}
if (options.displayName) {
params.set("name", options.displayName);
}
if (options.projectId) {
params.set("projectId", options.projectId);
}
return params.toString();
}
export function buildNewWorkspaceRoute(options: NewWorkspaceRouteOptions = {}) {
const query = buildNewWorkspaceSearch(options);
if (!query) {
return "/new" as const;
}
return `/new?${query}` as const;
}
export type KnownHostRouteResolution =
| { kind: "render" }
| { kind: "redirect"; href: ReturnType<typeof buildOpenProjectRoute> | "/welcome" };
@@ -434,32 +467,6 @@ export function resolveKnownHostRoute(input: {
return { kind: "redirect", href: "/welcome" };
}
export function buildHostNewWorkspaceRoute(
serverId: string,
sourceDirectory?: string,
options?: { displayName?: string; projectId?: string },
) {
const base = buildHostRootRoute(serverId);
if (base === "/") {
return "/" as const;
}
const params = new URLSearchParams();
if (sourceDirectory) {
params.set("dir", sourceDirectory);
}
if (options?.displayName) {
params.set("name", options.displayName);
}
if (options?.projectId) {
params.set("projectId", options.projectId);
}
const query = params.toString();
if (!query) {
return `${base}/new` as const;
}
return `${base}/new?${query}` as const;
}
export const SETTINGS_SECTION_SLUGS = [
"general",
"daemon",
@@ -40,7 +40,7 @@ describe("buildWorkspaceArchiveRedirectRoute", () => {
archivedWorkspaceId: "/repo/.paseo/worktrees/feature",
workspaces,
}),
).toBe("/h/server-1/new?dir=%2Frepo&name=Project&projectId=project-1");
).toBe("/new?serverId=server-1&dir=%2Frepo&name=Project&projectId=project-1");
});
it("redirects to the new workspace route when no sibling workspace target exists", () => {
@@ -58,7 +58,7 @@ describe("buildWorkspaceArchiveRedirectRoute", () => {
archivedWorkspaceId: "/repo/.paseo/worktrees/feature",
workspaces,
}),
).toBe("/h/server-1/new?dir=%2Frepo&name=Project&projectId=project-1");
).toBe("/new?serverId=server-1&dir=%2Frepo&name=Project&projectId=project-1");
});
it("redirects to the new workspace route instead of another workspace", () => {
@@ -78,7 +78,7 @@ describe("buildWorkspaceArchiveRedirectRoute", () => {
archivedWorkspaceId: "/notes",
workspaces,
}),
).toBe("/h/server-1/new?dir=%2Fnotes&name=Project&projectId=notes");
).toBe("/new?serverId=server-1&dir=%2Fnotes&name=Project&projectId=notes");
});
});
@@ -136,6 +136,6 @@ describe("redirectIfArchivingActiveWorkspace", () => {
),
).toBe(true);
expect(routes).toEqual(["/h/server-1/new?dir=%2Frepo&name=Project&projectId=project-1"]);
expect(routes).toEqual(["/new?serverId=server-1&dir=%2Frepo&name=Project&projectId=project-1"]);
});
});
@@ -1,6 +1,6 @@
import type { Href } from "expo-router";
import type { WorkspaceDescriptor } from "@/stores/session-store";
import { buildHostNewWorkspaceRoute, buildHostRootRoute } from "@/utils/host-routes";
import { buildHostRootRoute, buildNewWorkspaceRoute } from "@/utils/host-routes";
import { resolveWorkspaceRouteId } from "@/utils/workspace-identity";
export function buildWorkspaceArchiveRedirectRoute(input: {
@@ -23,7 +23,9 @@ export function buildWorkspaceArchiveRedirectRoute(input: {
return buildHostRootRoute(input.serverId);
}
return buildHostNewWorkspaceRoute(input.serverId, sourceDirectory, {
return buildNewWorkspaceRoute({
serverId: input.serverId,
sourceDirectory,
displayName: archivedWorkspace.projectDisplayName,
projectId: archivedWorkspace.projectId,
});