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:
@@ -159,7 +159,7 @@ export async function openNewWorkspaceComposer(
|
|||||||
await expect(button).toBeVisible({ timeout: 30_000 });
|
await expect(button).toBeVisible({ timeout: 30_000 });
|
||||||
await button.click();
|
await button.click();
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/h\/[^/]+\/new(?:\?.*)?$/, {
|
await expect(page).toHaveURL(/\/new(?:\?.*)?$/, {
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@ export async function openNewWorkspaceComposer(
|
|||||||
export async function openGlobalNewWorkspaceComposer(page: Page): Promise<void> {
|
export async function openGlobalNewWorkspaceComposer(page: Page): Promise<void> {
|
||||||
await page.getByTestId("sidebar-global-new-workspace").click();
|
await page.getByTestId("sidebar-global-new-workspace").click();
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/h\/[^/]+\/new(?:\?.*)?$/, {
|
await expect(page).toHaveURL(/\/new(?:\?.*)?$/, {
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
openGlobalNewWorkspaceComposer,
|
openGlobalNewWorkspaceComposer,
|
||||||
openNewWorkspaceComposer,
|
openNewWorkspaceComposer,
|
||||||
} from "./helpers/new-workspace";
|
} from "./helpers/new-workspace";
|
||||||
|
import { getE2EDaemonPort } from "./helpers/daemon-port";
|
||||||
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
|
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
|
||||||
|
import { seedSavedSettingsHosts } from "./helpers/settings";
|
||||||
import { getServerId } from "./helpers/server-id";
|
import { getServerId } from "./helpers/server-id";
|
||||||
import { waitForSidebarHydration } from "./helpers/workspace-ui";
|
import { waitForSidebarHydration } from "./helpers/workspace-ui";
|
||||||
|
|
||||||
@@ -38,6 +40,19 @@ test.describe("New workspace entry points", () => {
|
|||||||
const seeded: SeededWorkspace = await seedWorkspace({ repoPrefix: "entry-global-button-" });
|
const seeded: SeededWorkspace = await seedWorkspace({ repoPrefix: "entry-global-button-" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await seedSavedSettingsHosts(page, [
|
||||||
|
{
|
||||||
|
serverId: getServerId(),
|
||||||
|
label: "localhost",
|
||||||
|
endpoint: `127.0.0.1:${getE2EDaemonPort()}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverId: "secondary-new-workspace-host",
|
||||||
|
label: "Secondary host",
|
||||||
|
endpoint: "127.0.0.1:9",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
await gotoAppShell(page);
|
await gotoAppShell(page);
|
||||||
await waitForSidebarHydration(page);
|
await waitForSidebarHydration(page);
|
||||||
await expect(
|
await expect(
|
||||||
@@ -48,11 +63,12 @@ test.describe("New workspace entry points", () => {
|
|||||||
await expect(globalButton).toBeVisible({ timeout: 30_000 });
|
await expect(globalButton).toBeVisible({ timeout: 30_000 });
|
||||||
|
|
||||||
await openGlobalNewWorkspaceComposer(page);
|
await openGlobalNewWorkspaceComposer(page);
|
||||||
|
await expect(page.getByTestId("host-chooser")).toHaveCount(0);
|
||||||
|
|
||||||
// The screen is up: its project picker trigger is the canonical landmark.
|
|
||||||
await expect(page.getByTestId("new-workspace-project-picker-trigger")).toBeVisible({
|
await expect(page.getByTestId("new-workspace-project-picker-trigger")).toBeVisible({
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
|
await expect(page.getByTestId("host-picker-trigger")).toBeVisible({ timeout: 30_000 });
|
||||||
} finally {
|
} finally {
|
||||||
await seeded.cleanup();
|
await seeded.cleanup();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -878,7 +878,11 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
|
|||||||
const routeHasKnownHost =
|
const routeHasKnownHost =
|
||||||
routeServerId !== null && hosts.some((host) => host.serverId === routeServerId);
|
routeServerId !== null && hosts.some((host) => host.serverId === routeServerId);
|
||||||
const shouldShowAppChrome =
|
const shouldShowAppChrome =
|
||||||
storeReady && (pathname === "/open-project" || pathname === "/sessions" || routeHasKnownHost);
|
storeReady &&
|
||||||
|
(pathname === "/open-project" ||
|
||||||
|
pathname === "/new" ||
|
||||||
|
pathname === "/sessions" ||
|
||||||
|
routeHasKnownHost);
|
||||||
|
|
||||||
// Parse selectedAgentKey directly from pathname
|
// Parse selectedAgentKey directly from pathname
|
||||||
// useLocalSearchParams doesn't update when navigating between same-pattern routes
|
// useLocalSearchParams doesn't update when navigating between same-pattern routes
|
||||||
@@ -933,6 +937,7 @@ function RootStack() {
|
|||||||
<Stack.Screen name="settings/[section]" />
|
<Stack.Screen name="settings/[section]" />
|
||||||
<Stack.Screen name="settings/projects/index" />
|
<Stack.Screen name="settings/projects/index" />
|
||||||
<Stack.Screen name="settings/projects/[projectKey]" />
|
<Stack.Screen name="settings/projects/[projectKey]" />
|
||||||
|
<Stack.Screen name="new" />
|
||||||
<Stack.Screen name="open-project" />
|
<Stack.Screen name="open-project" />
|
||||||
<Stack.Screen name="sessions" />
|
<Stack.Screen name="sessions" />
|
||||||
<Stack.Screen name="pair-scan" />
|
<Stack.Screen name="pair-scan" />
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ function KnownHostRoute() {
|
|||||||
<Stack.Screen name="agent/[agentId]" options={AGENT_SCREEN_OPTIONS} />
|
<Stack.Screen name="agent/[agentId]" options={AGENT_SCREEN_OPTIONS} />
|
||||||
<Stack.Screen name="sessions" />
|
<Stack.Screen name="sessions" />
|
||||||
<Stack.Screen name="open-project" />
|
<Stack.Screen name="open-project" />
|
||||||
<Stack.Screen name="new" />
|
|
||||||
<Stack.Screen name="settings" />
|
<Stack.Screen name="settings" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useLocalSearchParams } from "expo-router";
|
|||||||
import { HostRouteBootstrapBoundary } from "@/components/host-route-bootstrap-boundary";
|
import { HostRouteBootstrapBoundary } from "@/components/host-route-bootstrap-boundary";
|
||||||
import { NewWorkspaceScreen } from "@/screens/new-workspace-screen";
|
import { NewWorkspaceScreen } from "@/screens/new-workspace-screen";
|
||||||
|
|
||||||
export default function HostNewWorkspaceRoute() {
|
export default function NewWorkspaceRoute() {
|
||||||
const params = useLocalSearchParams<{
|
const params = useLocalSearchParams<{
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
dir?: string;
|
dir?: string;
|
||||||
@@ -13,10 +13,17 @@ export default function HostNewWorkspaceRoute() {
|
|||||||
const sourceDirectory = typeof params.dir === "string" ? params.dir : undefined;
|
const sourceDirectory = typeof params.dir === "string" ? params.dir : undefined;
|
||||||
const displayName = typeof params.name === "string" ? params.name : undefined;
|
const displayName = typeof params.name === "string" ? params.name : undefined;
|
||||||
const projectId = typeof params.projectId === "string" ? params.projectId : undefined;
|
const projectId = typeof params.projectId === "string" ? params.projectId : undefined;
|
||||||
|
const screenKey = JSON.stringify([
|
||||||
|
serverId,
|
||||||
|
sourceDirectory ?? null,
|
||||||
|
displayName ?? null,
|
||||||
|
projectId ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HostRouteBootstrapBoundary>
|
<HostRouteBootstrapBoundary>
|
||||||
<NewWorkspaceScreen
|
<NewWorkspaceScreen
|
||||||
|
key={screenKey}
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
sourceDirectory={sourceDirectory}
|
sourceDirectory={sourceDirectory}
|
||||||
displayName={displayName}
|
displayName={displayName}
|
||||||
@@ -30,7 +30,6 @@ import { useIsCompactFormFactor } from "@/constants/layout";
|
|||||||
import { isWeb } from "@/constants/platform";
|
import { isWeb } from "@/constants/platform";
|
||||||
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
|
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
|
||||||
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
||||||
import { useHostChooser } from "@/hosts/host-chooser";
|
|
||||||
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||||
import { useSidebarShortcutModel } from "@/hooks/use-sidebar-shortcut-model";
|
import { useSidebarShortcutModel } from "@/hooks/use-sidebar-shortcut-model";
|
||||||
import {
|
import {
|
||||||
@@ -52,7 +51,7 @@ import { useWindowControlsPadding } from "@/utils/desktop-window";
|
|||||||
import { canCloseLeftSidebarGesture } from "@/utils/sidebar-animation-state";
|
import { canCloseLeftSidebarGesture } from "@/utils/sidebar-animation-state";
|
||||||
import {
|
import {
|
||||||
buildOpenProjectRoute,
|
buildOpenProjectRoute,
|
||||||
buildHostNewWorkspaceRoute,
|
buildNewWorkspaceRoute,
|
||||||
buildSessionsRoute,
|
buildSessionsRoute,
|
||||||
buildSettingsAddHostRoute,
|
buildSettingsAddHostRoute,
|
||||||
buildSettingsHostSectionRoute,
|
buildSettingsHostSectionRoute,
|
||||||
@@ -166,7 +165,6 @@ export const LeftSidebar = memo(function LeftSidebar({
|
|||||||
}, [isRevalidating, isManualRefresh]);
|
}, [isRevalidating, isManualRefresh]);
|
||||||
|
|
||||||
const openProjectPicker = useOpenProjectPicker();
|
const openProjectPicker = useOpenProjectPicker();
|
||||||
const chooseHost = useHostChooser();
|
|
||||||
|
|
||||||
const handleOpenProjectMobile = useCallback(() => {
|
const handleOpenProjectMobile = useCallback(() => {
|
||||||
showMobileAgent();
|
showMobileAgent();
|
||||||
@@ -178,13 +176,8 @@ export const LeftSidebar = memo(function LeftSidebar({
|
|||||||
}, [openProjectPicker]);
|
}, [openProjectPicker]);
|
||||||
|
|
||||||
const handleNewWorkspaceNavigate = useCallback(() => {
|
const handleNewWorkspaceNavigate = useCallback(() => {
|
||||||
chooseHost({
|
router.push(buildNewWorkspaceRoute());
|
||||||
title: "Choose host",
|
}, []);
|
||||||
onChooseHost: (serverId) => {
|
|
||||||
router.push(buildHostNewWorkspaceRoute(serverId));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, [chooseHost]);
|
|
||||||
|
|
||||||
const handleSettingsMobile = useCallback(() => {
|
const handleSettingsMobile = useCallback(() => {
|
||||||
showMobileAgent();
|
showMobileAgent();
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ import { getHostRuntimeStore, useHosts } from "@/runtime/host-runtime";
|
|||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { useProjectIconDataByProjectKey } from "@/projects/project-icons";
|
import { useProjectIconDataByProjectKey } from "@/projects/project-icons";
|
||||||
import {
|
import {
|
||||||
buildHostNewWorkspaceRoute,
|
buildNewWorkspaceRoute,
|
||||||
buildProjectSettingsRoute,
|
buildProjectSettingsRoute,
|
||||||
parseHostWorkspaceRouteFromPathname,
|
parseHostWorkspaceRouteFromPathname,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
@@ -992,7 +992,9 @@ function NewWorkspaceGhostRow({
|
|||||||
const handlePress = useCallback(() => {
|
const handlePress = useCallback(() => {
|
||||||
onWorkspacePress?.();
|
onWorkspacePress?.();
|
||||||
router.navigate(
|
router.navigate(
|
||||||
buildHostNewWorkspaceRoute(worktreeTarget.serverId, worktreeTarget.iconWorkingDir, {
|
buildNewWorkspaceRoute({
|
||||||
|
serverId: worktreeTarget.serverId,
|
||||||
|
sourceDirectory: worktreeTarget.iconWorkingDir,
|
||||||
displayName,
|
displayName,
|
||||||
projectId: project.projectKey,
|
projectId: project.projectKey,
|
||||||
}) as Href,
|
}) as Href,
|
||||||
@@ -1287,7 +1289,9 @@ function ProjectHeaderRow({
|
|||||||
}
|
}
|
||||||
onWorkspacePress?.();
|
onWorkspacePress?.();
|
||||||
router.navigate(
|
router.navigate(
|
||||||
buildHostNewWorkspaceRoute(worktreeTarget.serverId, worktreeTarget.iconWorkingDir, {
|
buildNewWorkspaceRoute({
|
||||||
|
serverId: worktreeTarget.serverId,
|
||||||
|
sourceDirectory: worktreeTarget.iconWorkingDir,
|
||||||
displayName,
|
displayName,
|
||||||
projectId: project.projectKey,
|
projectId: project.projectKey,
|
||||||
}) as Href,
|
}) as Href,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
|||||||
import type { KeyboardActionId } from "@/keyboard/keyboard-action-dispatcher";
|
import type { KeyboardActionId } from "@/keyboard/keyboard-action-dispatcher";
|
||||||
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { buildHostNewWorkspaceRoute } from "@/utils/host-routes";
|
import { buildNewWorkspaceRoute } from "@/utils/host-routes";
|
||||||
import { projectDisplayNameFromProjectId } from "@/utils/project-display-name";
|
import { projectDisplayNameFromProjectId } from "@/utils/project-display-name";
|
||||||
|
|
||||||
const WORKTREE_NEW_ACTIONS: readonly KeyboardActionId[] = ["worktree.new"];
|
const WORKTREE_NEW_ACTIONS: readonly KeyboardActionId[] = ["worktree.new"];
|
||||||
@@ -14,7 +14,7 @@ export function useActiveWorktreeNewAction() {
|
|||||||
const serverId = selection?.serverId ?? null;
|
const serverId = selection?.serverId ?? null;
|
||||||
const workspaceId = selection?.workspaceId ?? null;
|
const workspaceId = selection?.workspaceId ?? null;
|
||||||
|
|
||||||
const workingDir = useSessionStore((state) => {
|
const activeGitWorkspace = useSessionStore((state) => {
|
||||||
if (!serverId || !workspaceId) {
|
if (!serverId || !workspaceId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -22,31 +22,30 @@ export function useActiveWorktreeNewAction() {
|
|||||||
if (!workspace || workspace.projectKind !== "git") {
|
if (!workspace || workspace.projectKind !== "git") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return workspace.projectRootPath;
|
return workspace;
|
||||||
});
|
});
|
||||||
|
|
||||||
const displayName = useSessionStore((state) => {
|
const workingDir = activeGitWorkspace?.projectRootPath ?? null;
|
||||||
if (!serverId || !workspaceId) {
|
const projectId = activeGitWorkspace?.projectId ?? null;
|
||||||
return null;
|
const displayName = activeGitWorkspace
|
||||||
}
|
? activeGitWorkspace.projectDisplayName ||
|
||||||
const workspace = state.sessions[serverId]?.workspaces?.get(workspaceId);
|
projectDisplayNameFromProjectId(activeGitWorkspace.projectId)
|
||||||
if (!workspace || workspace.projectKind !== "git") {
|
: null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return workspace.projectDisplayName || projectDisplayNameFromProjectId(workspace.projectId);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handle = useCallback(() => {
|
const handle = useCallback(() => {
|
||||||
if (!serverId || !workingDir) {
|
if (!serverId || !workingDir) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
router.navigate(
|
router.navigate(
|
||||||
buildHostNewWorkspaceRoute(serverId, workingDir, {
|
buildNewWorkspaceRoute({
|
||||||
|
serverId,
|
||||||
|
sourceDirectory: workingDir,
|
||||||
displayName: displayName ?? undefined,
|
displayName: displayName ?? undefined,
|
||||||
|
projectId: projectId ?? undefined,
|
||||||
}) as never,
|
}) as never,
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}, [serverId, workingDir, displayName]);
|
}, [serverId, workingDir, displayName, projectId]);
|
||||||
|
|
||||||
useKeyboardActionHandler({
|
useKeyboardActionHandler({
|
||||||
handlerId: "worktree-new-active",
|
handlerId: "worktree-new-active",
|
||||||
|
|||||||
@@ -2,27 +2,29 @@ import { useCallback } from "react";
|
|||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||||
import type { KeyboardActionId } from "@/keyboard/keyboard-action-dispatcher";
|
import type { KeyboardActionId } from "@/keyboard/keyboard-action-dispatcher";
|
||||||
|
import { useHosts } from "@/runtime/host-runtime";
|
||||||
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
||||||
import { buildHostNewWorkspaceRoute } from "@/utils/host-routes";
|
import { buildNewWorkspaceRoute } from "@/utils/host-routes";
|
||||||
|
|
||||||
const WORKSPACE_NEW_ACTIONS: readonly KeyboardActionId[] = ["workspace.new"];
|
const WORKSPACE_NEW_ACTIONS: readonly KeyboardActionId[] = ["workspace.new"];
|
||||||
|
|
||||||
export function useGlobalNewWorkspaceAction() {
|
export function useGlobalNewWorkspaceAction() {
|
||||||
const selection = useActiveWorkspaceSelection();
|
const selection = useActiveWorkspaceSelection();
|
||||||
const serverId = selection?.serverId ?? null;
|
const serverId = selection?.serverId ?? null;
|
||||||
|
const hosts = useHosts();
|
||||||
|
|
||||||
const handle = useCallback(() => {
|
const handle = useCallback(() => {
|
||||||
if (!serverId) {
|
if (hosts.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
router.navigate(buildHostNewWorkspaceRoute(serverId) as never);
|
router.navigate(buildNewWorkspaceRoute(serverId ? { serverId } : undefined) as never);
|
||||||
return true;
|
return true;
|
||||||
}, [serverId]);
|
}, [hosts.length, serverId]);
|
||||||
|
|
||||||
useKeyboardActionHandler({
|
useKeyboardActionHandler({
|
||||||
handlerId: "workspace-new-global",
|
handlerId: "workspace-new-global",
|
||||||
actions: WORKSPACE_NEW_ACTIONS,
|
actions: WORKSPACE_NEW_ACTIONS,
|
||||||
enabled: serverId !== null,
|
enabled: hosts.length > 0,
|
||||||
priority: 0,
|
priority: 0,
|
||||||
handle,
|
handle,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -976,11 +976,24 @@ function submitWorkspaceDraft(input: SubmitDraftInput): void {
|
|||||||
function useNewWorkspaceHostSelector(initialServerId: string) {
|
function useNewWorkspaceHostSelector(initialServerId: string) {
|
||||||
const allHosts = useHosts();
|
const allHosts = useHosts();
|
||||||
const allServerIds = useMemo(() => allHosts.map((h) => h.serverId), [allHosts]);
|
const allServerIds = useMemo(() => allHosts.map((h) => h.serverId), [allHosts]);
|
||||||
const [selectedServerId, setSelectedServerId] = useState(initialServerId);
|
const lastWorkspaceSelection = useLastWorkspaceSelection();
|
||||||
|
const normalizedInitialServerId = initialServerId.trim();
|
||||||
|
const routeInitialServerId = allServerIds.includes(normalizedInitialServerId)
|
||||||
|
? normalizedInitialServerId
|
||||||
|
: null;
|
||||||
|
const fallbackServerId =
|
||||||
|
lastWorkspaceSelection && allServerIds.includes(lastWorkspaceSelection.serverId)
|
||||||
|
? lastWorkspaceSelection.serverId
|
||||||
|
: (allServerIds[0] ?? "");
|
||||||
|
const [manualServerId, setManualServerId] = useState<string | null>(null);
|
||||||
const [hostPickerOpen, setHostPickerOpen] = useState(false);
|
const [hostPickerOpen, setHostPickerOpen] = useState(false);
|
||||||
|
const selectedServerId =
|
||||||
|
manualServerId && allServerIds.includes(manualServerId)
|
||||||
|
? manualServerId
|
||||||
|
: (routeInitialServerId ?? fallbackServerId);
|
||||||
|
|
||||||
const handleSelectHost = useCallback((id: string) => {
|
const handleSelectHost = useCallback((id: string) => {
|
||||||
setSelectedServerId(id);
|
setManualServerId(id);
|
||||||
setHostPickerOpen(false);
|
setHostPickerOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -996,9 +1009,7 @@ function useNewWorkspaceHostSelector(initialServerId: string) {
|
|||||||
allHosts,
|
allHosts,
|
||||||
allServerIds,
|
allServerIds,
|
||||||
selectedServerId,
|
selectedServerId,
|
||||||
setSelectedServerId,
|
|
||||||
hostPickerOpen,
|
hostPickerOpen,
|
||||||
setHostPickerOpen,
|
|
||||||
handleSelectHost,
|
handleSelectHost,
|
||||||
handleHostPickerOpenChange,
|
handleHostPickerOpenChange,
|
||||||
openHostPicker,
|
openHostPicker,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
buildHostAgentDetailRoute,
|
buildHostAgentDetailRoute,
|
||||||
buildHostNewWorkspaceRoute,
|
|
||||||
buildHostRootRoute,
|
buildHostRootRoute,
|
||||||
buildHostWorkspaceOpenRoute,
|
buildHostWorkspaceOpenRoute,
|
||||||
buildHostWorkspaceRoute,
|
buildHostWorkspaceRoute,
|
||||||
|
buildNewWorkspaceRoute,
|
||||||
buildOpenProjectRoute,
|
buildOpenProjectRoute,
|
||||||
resolveKnownHostRoute,
|
resolveKnownHostRoute,
|
||||||
buildSessionsRoute,
|
buildSessionsRoute,
|
||||||
@@ -149,19 +149,6 @@ describe("workspace route parsing", () => {
|
|||||||
).toBe("/h/local/workspace/b64_L3RtcC9yZXBv");
|
).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", () => {
|
it("round-trips URL-safe IDs through encode/decode", () => {
|
||||||
const ids = ["1", "40", "164", "9999", "workspace-1", "opaque_id.v2~test"];
|
const ids = ["1", "40", "164", "9999", "workspace-1", "opaque_id.v2~test"];
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
@@ -216,6 +203,25 @@ describe("global routes", () => {
|
|||||||
it("buildSessionsRoute returns the all-host Sessions route", () => {
|
it("buildSessionsRoute returns the all-host Sessions route", () => {
|
||||||
expect(buildSessionsRoute()).toBe("/sessions");
|
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", () => {
|
describe("host settings section slugs", () => {
|
||||||
|
|||||||
@@ -414,6 +414,39 @@ export function buildOpenProjectRoute() {
|
|||||||
return "/open-project" as const;
|
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 =
|
export type KnownHostRouteResolution =
|
||||||
| { kind: "render" }
|
| { kind: "render" }
|
||||||
| { kind: "redirect"; href: ReturnType<typeof buildOpenProjectRoute> | "/welcome" };
|
| { kind: "redirect"; href: ReturnType<typeof buildOpenProjectRoute> | "/welcome" };
|
||||||
@@ -434,32 +467,6 @@ export function resolveKnownHostRoute(input: {
|
|||||||
return { kind: "redirect", href: "/welcome" };
|
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 = [
|
export const SETTINGS_SECTION_SLUGS = [
|
||||||
"general",
|
"general",
|
||||||
"daemon",
|
"daemon",
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ describe("buildWorkspaceArchiveRedirectRoute", () => {
|
|||||||
archivedWorkspaceId: "/repo/.paseo/worktrees/feature",
|
archivedWorkspaceId: "/repo/.paseo/worktrees/feature",
|
||||||
workspaces,
|
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", () => {
|
it("redirects to the new workspace route when no sibling workspace target exists", () => {
|
||||||
@@ -58,7 +58,7 @@ describe("buildWorkspaceArchiveRedirectRoute", () => {
|
|||||||
archivedWorkspaceId: "/repo/.paseo/worktrees/feature",
|
archivedWorkspaceId: "/repo/.paseo/worktrees/feature",
|
||||||
workspaces,
|
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", () => {
|
it("redirects to the new workspace route instead of another workspace", () => {
|
||||||
@@ -78,7 +78,7 @@ describe("buildWorkspaceArchiveRedirectRoute", () => {
|
|||||||
archivedWorkspaceId: "/notes",
|
archivedWorkspaceId: "/notes",
|
||||||
workspaces,
|
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);
|
).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 { Href } from "expo-router";
|
||||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
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";
|
import { resolveWorkspaceRouteId } from "@/utils/workspace-identity";
|
||||||
|
|
||||||
export function buildWorkspaceArchiveRedirectRoute(input: {
|
export function buildWorkspaceArchiveRedirectRoute(input: {
|
||||||
@@ -23,7 +23,9 @@ export function buildWorkspaceArchiveRedirectRoute(input: {
|
|||||||
return buildHostRootRoute(input.serverId);
|
return buildHostRootRoute(input.serverId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildHostNewWorkspaceRoute(input.serverId, sourceDirectory, {
|
return buildNewWorkspaceRoute({
|
||||||
|
serverId: input.serverId,
|
||||||
|
sourceDirectory,
|
||||||
displayName: archivedWorkspace.projectDisplayName,
|
displayName: archivedWorkspace.projectDisplayName,
|
||||||
projectId: archivedWorkspace.projectId,
|
projectId: archivedWorkspace.projectId,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user