Fix removed host routes getting stuck connecting

This commit is contained in:
Mohamed Boudra
2026-06-09 18:06:48 +07:00
parent c5bcec5c71
commit e53d26699f
6 changed files with 115 additions and 12 deletions

View File

@@ -180,6 +180,18 @@ export async function goBackInSettings(page: Page): Promise<void> {
await page.getByRole("button", { name: "Back", exact: true }).click();
}
export async function closeCompactSettings(page: Page): Promise<void> {
await goBackInSettings(page);
await expect(page).not.toHaveURL(/\/settings(\/|$)/);
}
export async function removeCurrentHostFromSettings(page: Page): Promise<void> {
await page.getByTestId("host-page-remove-host-button").click();
await expect(page.getByTestId("remove-host-confirm-modal")).toBeVisible();
await page.getByTestId("remove-host-confirm").click();
await expect(page).toHaveURL(/\/settings$/);
}
export async function expectSettingsBackButton(page: Page): Promise<void> {
await expect(page.getByRole("button", { name: "Back", exact: true })).toBeVisible();
}

View File

@@ -1,7 +1,9 @@
import { test, expect } from "./fixtures";
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
import { gotoAppShell, openSettings } from "./helpers/app";
import { getE2EDaemonPort } from "./helpers/daemon-port";
import {
closeCompactSettings,
openSettingsSection,
expectSettingsHeader,
openAddHostFlow,
@@ -31,9 +33,18 @@ import {
selectSettingsHost,
expectSettingsHostPickerLabel,
openSettingsHostSection,
removeCurrentHostFromSettings,
} from "./helpers/settings";
import { getServerId } from "./helpers/server-id";
async function openWorkspace(
page: import("@playwright/test").Page,
workspace: { workspaceId: string },
) {
await page.goto(buildHostWorkspaceRoute(getServerId(), workspace.workspaceId));
await expect(page.getByTestId("menu-button")).toBeVisible();
}
test.describe("Settings sidebar navigation", () => {
test("clicking a sidebar section updates the URL and renders the section", async ({ page }) => {
await gotoAppShell(page);
@@ -187,4 +198,20 @@ test.describe("Settings — compact master-detail", () => {
await openSettingsHostSection(page, secondaryServerId, "connections");
});
test("removing the last active host returns to welcome after settings closes", async ({
page,
withWorkspace,
}) => {
const workspace = await withWorkspace({ prefix: "remove-host-compact-" });
await openWorkspace(page, workspace);
await openCompactSettings(page);
await openSettingsHostSection(page, getServerId(), "host");
await removeCurrentHostFromSettings(page);
await closeCompactSettings(page);
await expect(page).toHaveURL(/\/welcome$/);
await expect(page.getByTestId("welcome-direct-connection")).toBeVisible();
});
});

View File

@@ -84,7 +84,6 @@ import { resolveActiveHost } from "@/utils/active-host";
import { toggleDesktopSidebarsWithCheckoutIntent } from "@/utils/desktop-sidebar-toggle";
import {
buildHostRootRoute,
mapPathnameToServer,
parseHostAgentRouteFromPathname,
parseServerIdFromPathname,
parseWorkspaceOpenIntent,
@@ -811,7 +810,6 @@ function OpenProjectListener() {
}
function AppWithSidebar({ children }: { children: ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const params = useGlobalSearchParams<{ open?: string | string[] }>();
const hosts = useHosts();
@@ -820,16 +818,6 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
const shouldShowAppChrome =
storeReady && activeServerId !== null && hosts.some((host) => host.serverId === activeServerId);
useEffect(() => {
if (!activeServerId || hosts.length === 0) {
return;
}
if (hosts.some((host) => host.serverId === activeServerId)) {
return;
}
router.replace(mapPathnameToServer(pathname, hosts[0].serverId));
}, [activeServerId, hosts, pathname, router]);
// Parse selectedAgentKey directly from pathname
// useLocalSearchParams doesn't update when navigating between same-pattern routes
const selectedAgentKey = useMemo(() => {

View File

@@ -0,0 +1,25 @@
import { Redirect, Slot, useLocalSearchParams } from "expo-router";
import { HostRouteBootstrapBoundary } from "@/components/host-route-bootstrap-boundary";
import { useHosts } from "@/runtime/host-runtime";
import { resolveKnownHostRoute } from "@/utils/host-routes";
export default function HostRouteLayout() {
return (
<HostRouteBootstrapBoundary>
<KnownHostRoute />
</HostRouteBootstrapBoundary>
);
}
function KnownHostRoute() {
const params = useLocalSearchParams<{ serverId?: string | string[] }>();
const hosts = useHosts();
const routeServerId = typeof params.serverId === "string" ? params.serverId : null;
const resolution = resolveKnownHostRoute({ routeServerId, hosts });
if (resolution.kind === "redirect") {
return <Redirect href={resolution.href} />;
}
return <Slot />;
}

View File

@@ -16,6 +16,7 @@ import {
parseHostWorkspaceOpenIntentFromPathname,
parseHostWorkspaceRouteFromPathname,
parseWorkspaceOpenIntent,
resolveKnownHostRoute,
} from "./host-routes";
describe("parseHostAgentRouteFromPathname", () => {
@@ -190,3 +191,32 @@ describe("host settings section slugs", () => {
expect(normalizeHostSectionSlug("daemon")).toBe("host");
});
});
describe("resolveKnownHostRoute", () => {
it("renders when the route host is still saved", () => {
expect(
resolveKnownHostRoute({
routeServerId: "srv-current",
hosts: [{ serverId: "srv-current" }, { serverId: "srv-next" }],
}),
).toEqual({ kind: "render" });
});
it("sends removed host routes to the next saved host home", () => {
expect(
resolveKnownHostRoute({
routeServerId: "srv-removed",
hosts: [{ serverId: "srv-next" }],
}),
).toEqual({ kind: "redirect", href: "/h/srv-next/open-project" });
});
it("sends host routes to welcome when no hosts are saved", () => {
expect(
resolveKnownHostRoute({
routeServerId: "srv-removed",
hosts: [],
}),
).toEqual({ kind: "redirect", href: "/welcome" });
});
});

View File

@@ -355,6 +355,27 @@ export function buildHostOpenProjectRoute(serverId: string) {
return `${base}/open-project` as const;
}
export type KnownHostRouteResolution =
| { kind: "render" }
| { kind: "redirect"; href: ReturnType<typeof buildHostOpenProjectRoute> | "/welcome" };
export function resolveKnownHostRoute(input: {
routeServerId: string | null | undefined;
hosts: readonly { serverId: string }[];
}): KnownHostRouteResolution {
const routeServerId = trimNonEmpty(input.routeServerId);
if (routeServerId && input.hosts.some((host) => host.serverId === routeServerId)) {
return { kind: "render" };
}
const fallbackServerId = input.hosts[0]?.serverId;
if (fallbackServerId) {
return { kind: "redirect", href: buildHostOpenProjectRoute(fallbackServerId) };
}
return { kind: "redirect", href: "/welcome" };
}
export function buildHostNewWorkspaceRoute(
serverId: string,
sourceDirectory?: string,