Add a global New workspace picker (#1324)

* Add global New workspace project picker

* Clean up New workspace picker state
This commit is contained in:
Mohamed Boudra
2026-06-04 15:16:41 +08:00
committed by GitHub
parent 0b62e3bd7f
commit 38f8179bc1
28 changed files with 1392 additions and 424 deletions
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
buildHostAgentDetailRoute,
buildHostNewWorkspaceRoute,
buildHostRootRoute,
buildHostWorkspaceOpenRoute,
buildHostWorkspaceRoute,
@@ -120,6 +121,19 @@ describe("workspace route parsing", () => {
);
});
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) {
+9 -3
View File
@@ -357,7 +357,7 @@ export function buildHostOpenProjectRoute(serverId: string) {
export function buildHostNewWorkspaceRoute(
serverId: string,
sourceDirectory: string,
sourceDirectory?: string,
options?: { displayName?: string; projectId?: string },
) {
const base = buildHostRootRoute(serverId);
@@ -365,14 +365,20 @@ export function buildHostNewWorkspaceRoute(
return "/" as const;
}
const params = new URLSearchParams();
params.set("dir", sourceDirectory);
if (sourceDirectory) {
params.set("dir", sourceDirectory);
}
if (options?.displayName) {
params.set("name", options.displayName);
}
if (options?.projectId) {
params.set("projectId", options.projectId);
}
return `${base}/new?${params.toString()}` as const;
const query = params.toString();
if (!query) {
return `${base}/new` as const;
}
return `${base}/new?${query}` as const;
}
export const SETTINGS_SECTION_SLUGS = [
+47 -16
View File
@@ -1,4 +1,6 @@
import type { WorkspaceDescriptor } from "@/stores/session-store";
import { buildHostProjectList, type HostProjectListItem } from "@/projects/host-project-model";
import { buildWorkspaceStructureProjects } from "@/projects/workspace-structure";
export interface WorkspaceSummary {
id: string;
@@ -61,6 +63,31 @@ interface ProjectGroup {
hostsByServerId: Map<string, HostGroup>;
}
function findProjectCustomName(
workspaces: WorkspaceDescriptor[],
projectKey: string,
): { customName: string; displayName: string } | null {
for (const workspace of workspaces) {
if (workspace.projectId === projectKey && workspace.projectCustomName) {
return {
customName: workspace.projectCustomName,
displayName: workspace.projectDisplayName,
};
}
}
return null;
}
function buildHostProjectEntries(host: ProjectHost): HostProjectListItem[] {
return buildHostProjectList({
serverId: host.serverId,
projects: buildWorkspaceStructureProjects({
serverId: host.serverId,
workspaces: host.workspaces,
}),
});
}
function deriveGithubUrl(projectKey: string): string | undefined {
const match = projectKey.match(GITHUB_PROJECT_KEY_PATTERN);
if (!match) {
@@ -134,33 +161,37 @@ export function buildProjects(input: BuildProjectsInput): BuildProjectsResult {
const groups = new Map<string, ProjectGroup>();
for (const host of input.hosts) {
for (const workspace of host.workspaces) {
const projectKey = workspace.projectId;
let group = groups.get(projectKey);
const hostProjects = buildHostProjectEntries(host);
for (const hostProject of hostProjects) {
const customName = findProjectCustomName(host.workspaces, hostProject.projectKey);
let group = groups.get(hostProject.projectKey);
if (!group) {
group = {
projectKey,
projectName: workspace.projectDisplayName,
projectCustomName: workspace.projectCustomName ?? null,
projectKey: hostProject.projectKey,
projectName: customName?.displayName ?? hostProject.projectName,
projectCustomName: customName?.customName ?? null,
hostsByServerId: new Map(),
};
groups.set(projectKey, group);
} else if (workspace.projectCustomName && !group.projectCustomName) {
group.projectCustomName = workspace.projectCustomName;
group.projectName = workspace.projectDisplayName;
groups.set(hostProject.projectKey, group);
} else if (customName && !group.projectCustomName) {
group.projectCustomName = customName.customName;
group.projectName = customName.displayName;
}
let hostGroup = group.hostsByServerId.get(host.serverId);
if (!hostGroup) {
hostGroup = {
if (!group.hostsByServerId.has(host.serverId)) {
group.hostsByServerId.set(host.serverId, {
serverId: host.serverId,
serverName: host.serverName,
isOnline: host.isOnline,
workspaces: [],
};
group.hostsByServerId.set(host.serverId, hostGroup);
});
}
}
for (const workspace of host.workspaces) {
const group = groups.get(workspace.projectId);
const hostGroup = group?.hostsByServerId.get(host.serverId);
if (!hostGroup) continue;
hostGroup.workspaces.push(workspace);
}
}
@@ -32,11 +32,13 @@ function workspace(overrides: Partial<SidebarWorkspaceEntry> = {}): SidebarWorks
}
function project(overrides: Partial<SidebarProjectEntry> = {}): SidebarProjectEntry {
const projectKind = overrides.projectKind ?? "git";
return {
projectKey: "project-1",
projectName: "paseo",
projectKind: "git",
projectKind,
iconWorkingDir: "/repo",
canCreateWorktree: overrides.canCreateWorktree ?? projectKind === "git",
workspaces: [workspace()],
...overrides,
};
@@ -37,7 +37,7 @@ export function buildSidebarProjectRowModel(input: {
kind: "workspace_link",
workspace: flattenedWorkspace,
chevron: null,
trailingAction: input.project.projectKind === "git" ? "new_worktree" : "none",
trailingAction: input.project.canCreateWorktree ? "new_worktree" : "none",
};
}
@@ -51,6 +51,6 @@ export function buildSidebarProjectRowModel(input: {
return {
kind: "project_section",
chevron,
trailingAction: input.project.projectKind === "git" ? "new_worktree" : "none",
trailingAction: input.project.canCreateWorktree ? "new_worktree" : "none",
};
}
@@ -46,6 +46,7 @@ function project(projectKey: string, workspaces: SidebarWorkspaceEntry[]): Sideb
projectName: projectKey,
projectKind: "git",
iconWorkingDir: workspaces[0]?.workspaceDirectory ?? "",
canCreateWorktree: true,
workspaces,
};
}