feat: projects → workspaces → tabs
This commit is contained in:
@@ -2,18 +2,22 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
parseHostAgentDraftRouteFromPathname,
|
||||
parseHostAgentRouteFromPathname,
|
||||
parseHostDraftRouteFromPathname,
|
||||
parseHostWorkspaceAgentRouteFromPathname,
|
||||
parseHostWorkspaceTerminalRouteFromPathname,
|
||||
parseHostWorkspaceRouteFromPathname,
|
||||
} from "./host-routes";
|
||||
|
||||
describe("parseHostAgentDraftRouteFromPathname", () => {
|
||||
it("parses draft route server id", () => {
|
||||
expect(parseHostAgentDraftRouteFromPathname("/h/local/agent")).toEqual({
|
||||
expect(parseHostAgentDraftRouteFromPathname("/h/local/new")).toEqual({
|
||||
serverId: "local",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses encoded server id", () => {
|
||||
expect(
|
||||
parseHostAgentDraftRouteFromPathname("/h/team%20host/agent")
|
||||
parseHostAgentDraftRouteFromPathname("/h/team%20host/new")
|
||||
).toEqual({
|
||||
serverId: "team host",
|
||||
});
|
||||
@@ -24,6 +28,14 @@ describe("parseHostAgentDraftRouteFromPathname", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseHostDraftRouteFromPathname", () => {
|
||||
it("parses /new draft routes", () => {
|
||||
expect(parseHostDraftRouteFromPathname("/h/local/new")).toEqual({
|
||||
serverId: "local",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseHostAgentRouteFromPathname", () => {
|
||||
it("continues parsing detail routes", () => {
|
||||
expect(parseHostAgentRouteFromPathname("/h/local/agent/abc123")).toEqual({
|
||||
@@ -32,3 +44,38 @@ describe("parseHostAgentRouteFromPathname", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("workspace route parsing", () => {
|
||||
it("parses workspace route", () => {
|
||||
expect(
|
||||
parseHostWorkspaceRouteFromPathname("/h/local/workspace/%2Ftmp%2Frepo")
|
||||
).toEqual({
|
||||
serverId: "local",
|
||||
workspaceId: "/tmp/repo",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses workspace agent route", () => {
|
||||
expect(
|
||||
parseHostWorkspaceAgentRouteFromPathname(
|
||||
"/h/local/workspace/%2Ftmp%2Frepo/agent/agent-1"
|
||||
)
|
||||
).toEqual({
|
||||
serverId: "local",
|
||||
workspaceId: "/tmp/repo",
|
||||
agentId: "agent-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses workspace terminal route", () => {
|
||||
expect(
|
||||
parseHostWorkspaceTerminalRouteFromPathname(
|
||||
"/h/local/workspace/%2Ftmp%2Frepo/terminal/term-1"
|
||||
)
|
||||
).toEqual({
|
||||
serverId: "local",
|
||||
workspaceId: "/tmp/repo",
|
||||
terminalId: "term-1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ export function parseHostAgentRouteFromPathname(
|
||||
export function parseHostAgentDraftRouteFromPathname(
|
||||
pathname: string
|
||||
): { serverId: string } | null {
|
||||
const match = pathname.match(/^\/h\/([^/]+)\/agent\/?$/);
|
||||
const match = pathname.match(/^\/h\/([^/]+)\/(?:agent|new)\/?$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
@@ -73,17 +73,151 @@ export function parseHostAgentDraftRouteFromPathname(
|
||||
}
|
||||
|
||||
export function buildHostAgentDraftRoute(serverId: string): string {
|
||||
return buildHostDraftRoute(serverId);
|
||||
}
|
||||
|
||||
export function parseHostDraftRouteFromPathname(
|
||||
pathname: string
|
||||
): { serverId: string } | null {
|
||||
const match = pathname.match(/^\/h\/([^/]+)\/new\/?$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const encodedServerId = match[1];
|
||||
if (!encodedServerId) {
|
||||
return null;
|
||||
}
|
||||
const serverId = trimNonEmpty(decodeSegment(encodedServerId));
|
||||
if (!serverId) {
|
||||
return null;
|
||||
}
|
||||
return { serverId };
|
||||
}
|
||||
|
||||
export function buildHostDraftRoute(serverId: string): string {
|
||||
const normalized = trimNonEmpty(serverId);
|
||||
if (!normalized) {
|
||||
return "/";
|
||||
}
|
||||
return `/h/${encodeSegment(normalized)}/agent`;
|
||||
return `/h/${encodeSegment(normalized)}/new`;
|
||||
}
|
||||
|
||||
export function parseHostWorkspaceRouteFromPathname(
|
||||
pathname: string
|
||||
): { serverId: string; workspaceId: string } | null {
|
||||
const match = pathname.match(/^\/h\/([^/]+)\/workspace\/([^/]+)(?:\/|$)/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const [, encodedServerId, encodedWorkspaceId] = match;
|
||||
if (!encodedServerId || !encodedWorkspaceId) {
|
||||
return null;
|
||||
}
|
||||
const serverId = trimNonEmpty(decodeSegment(encodedServerId));
|
||||
const workspaceId = trimNonEmpty(decodeSegment(encodedWorkspaceId));
|
||||
if (!serverId || !workspaceId) {
|
||||
return null;
|
||||
}
|
||||
return { serverId, workspaceId };
|
||||
}
|
||||
|
||||
export function parseHostWorkspaceAgentRouteFromPathname(
|
||||
pathname: string
|
||||
): { serverId: string; workspaceId: string; agentId: string } | null {
|
||||
const match = pathname.match(
|
||||
/^\/h\/([^/]+)\/workspace\/([^/]+)\/agent\/([^/]+)(?:\/|$)/
|
||||
);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const [, encodedServerId, encodedWorkspaceId, encodedAgentId] = match;
|
||||
if (!encodedServerId || !encodedWorkspaceId || !encodedAgentId) {
|
||||
return null;
|
||||
}
|
||||
const serverId = trimNonEmpty(decodeSegment(encodedServerId));
|
||||
const workspaceId = trimNonEmpty(decodeSegment(encodedWorkspaceId));
|
||||
const agentId = trimNonEmpty(decodeSegment(encodedAgentId));
|
||||
if (!serverId || !workspaceId || !agentId) {
|
||||
return null;
|
||||
}
|
||||
return { serverId, workspaceId, agentId };
|
||||
}
|
||||
|
||||
export function parseHostWorkspaceTerminalRouteFromPathname(
|
||||
pathname: string
|
||||
): { serverId: string; workspaceId: string; terminalId: string } | null {
|
||||
const match = pathname.match(
|
||||
/^\/h\/([^/]+)\/workspace\/([^/]+)\/terminal\/([^/]+)(?:\/|$)/
|
||||
);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const [, encodedServerId, encodedWorkspaceId, encodedTerminalId] = match;
|
||||
if (!encodedServerId || !encodedWorkspaceId || !encodedTerminalId) {
|
||||
return null;
|
||||
}
|
||||
const serverId = trimNonEmpty(decodeSegment(encodedServerId));
|
||||
const workspaceId = trimNonEmpty(decodeSegment(encodedWorkspaceId));
|
||||
const terminalId = trimNonEmpty(decodeSegment(encodedTerminalId));
|
||||
if (!serverId || !workspaceId || !terminalId) {
|
||||
return null;
|
||||
}
|
||||
return { serverId, workspaceId, terminalId };
|
||||
}
|
||||
|
||||
export function buildHostWorkspaceRoute(
|
||||
serverId: string,
|
||||
workspaceId: string
|
||||
): string {
|
||||
const normalizedServerId = trimNonEmpty(serverId);
|
||||
const normalizedWorkspaceId = trimNonEmpty(workspaceId);
|
||||
if (!normalizedServerId || !normalizedWorkspaceId) {
|
||||
return "/";
|
||||
}
|
||||
return `/h/${encodeSegment(normalizedServerId)}/workspace/${encodeSegment(
|
||||
normalizedWorkspaceId
|
||||
)}`;
|
||||
}
|
||||
|
||||
export function buildHostWorkspaceAgentRoute(
|
||||
serverId: string,
|
||||
workspaceId: string,
|
||||
agentId: string
|
||||
): string {
|
||||
const base = buildHostWorkspaceRoute(serverId, workspaceId);
|
||||
const normalizedAgentId = trimNonEmpty(agentId);
|
||||
if (base === "/" || !normalizedAgentId) {
|
||||
return "/";
|
||||
}
|
||||
return `${base}/agent/${encodeSegment(normalizedAgentId)}`;
|
||||
}
|
||||
|
||||
export function buildHostWorkspaceTerminalRoute(
|
||||
serverId: string,
|
||||
workspaceId: string,
|
||||
terminalId: string
|
||||
): string {
|
||||
const base = buildHostWorkspaceRoute(serverId, workspaceId);
|
||||
const normalizedTerminalId = trimNonEmpty(terminalId);
|
||||
if (base === "/" || !normalizedTerminalId) {
|
||||
return "/";
|
||||
}
|
||||
return `${base}/terminal/${encodeSegment(normalizedTerminalId)}`;
|
||||
}
|
||||
|
||||
export function buildHostAgentDetailRoute(
|
||||
serverId: string,
|
||||
agentId: string
|
||||
agentId: string,
|
||||
workspaceId?: string
|
||||
): string {
|
||||
const normalizedWorkspaceId = trimNonEmpty(workspaceId);
|
||||
if (normalizedWorkspaceId) {
|
||||
return buildHostWorkspaceAgentRoute(
|
||||
serverId,
|
||||
normalizedWorkspaceId,
|
||||
agentId
|
||||
);
|
||||
}
|
||||
const normalizedServerId = trimNonEmpty(serverId);
|
||||
const normalizedAgentId = trimNonEmpty(agentId);
|
||||
if (!normalizedServerId || !normalizedAgentId) {
|
||||
@@ -127,5 +261,14 @@ export function mapPathnameToServer(
|
||||
if (suffix.startsWith("agents")) {
|
||||
return `${base}/agents`;
|
||||
}
|
||||
return `${base}/agent`;
|
||||
if (suffix.startsWith("new")) {
|
||||
return `${base}/new`;
|
||||
}
|
||||
if (suffix.startsWith("workspace/")) {
|
||||
return `${base}/${suffix}`;
|
||||
}
|
||||
if (suffix.startsWith("agent/")) {
|
||||
return `${base}/${suffix}`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
|
||||
describe("buildNewAgentRoute", () => {
|
||||
it("falls back to host-scoped draft route when no working directory is provided", () => {
|
||||
expect(buildNewAgentRoute("srv-1", undefined)).toBe("/h/srv-1/agent");
|
||||
expect(buildNewAgentRoute("srv-1", " ")).toBe("/h/srv-1/agent");
|
||||
expect(buildNewAgentRoute("srv-1", undefined)).toBe("/h/srv-1/new");
|
||||
expect(buildNewAgentRoute("srv-1", " ")).toBe("/h/srv-1/new");
|
||||
});
|
||||
|
||||
it("encodes the working directory query parameter", () => {
|
||||
expect(buildNewAgentRoute("srv-1", "/Users/me/dev/paseo")).toBe(
|
||||
"/h/srv-1/agent?workingDir=%2FUsers%2Fme%2Fdev%2Fpaseo"
|
||||
"/h/srv-1/new?workingDir=%2FUsers%2Fme%2Fdev%2Fpaseo"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -77,7 +77,7 @@ describe("resolveSelectedAgentForNewAgent", () => {
|
||||
it("prefers the agent in the current route", () => {
|
||||
expect(
|
||||
resolveSelectedAgentForNewAgent({
|
||||
pathname: "/h/srv-1/agent/agent-2",
|
||||
pathname: "/h/srv-1/workspace/%2Frepo/agent/agent-2",
|
||||
selectedAgentId: "srv-9:agent-9",
|
||||
})
|
||||
).toEqual({
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CheckoutStatusPayload } from "@/hooks/use-checkout-status-query";
|
||||
import {
|
||||
buildHostAgentDraftRoute,
|
||||
parseHostAgentRouteFromPathname,
|
||||
parseHostWorkspaceAgentRouteFromPathname,
|
||||
} from "@/utils/host-routes";
|
||||
|
||||
export function parseAgentKey(
|
||||
@@ -26,6 +27,10 @@ export function resolveSelectedAgentForNewAgent(input: {
|
||||
pathname: string;
|
||||
selectedAgentId?: string;
|
||||
}): { serverId: string; agentId: string } | null {
|
||||
const workspaceRoute = parseHostWorkspaceAgentRouteFromPathname(input.pathname);
|
||||
if (workspaceRoute) {
|
||||
return { serverId: workspaceRoute.serverId, agentId: workspaceRoute.agentId };
|
||||
}
|
||||
return (
|
||||
parseHostAgentRouteFromPathname(input.pathname) ??
|
||||
parseAgentKey(input.selectedAgentId)
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("buildNotificationRoute", () => {
|
||||
});
|
||||
|
||||
it("falls back to host-scoped draft route when only serverId is present", () => {
|
||||
expect(buildNotificationRoute({ serverId: "srv-only" })).toBe("/h/srv-only/agent");
|
||||
expect(buildNotificationRoute({ serverId: "srv-only" })).toBe("/h/srv-only/new");
|
||||
});
|
||||
|
||||
it("falls back to root when no server id is present", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
buildHostDraftRoute,
|
||||
buildHostAgentDetailRoute,
|
||||
buildHostAgentDraftRoute,
|
||||
} from "@/utils/host-routes";
|
||||
|
||||
type NotificationData = Record<string, unknown> | null | undefined;
|
||||
@@ -33,7 +33,7 @@ export function buildNotificationRoute(data: NotificationData): string {
|
||||
return buildHostAgentDetailRoute(serverId, agentId);
|
||||
}
|
||||
if (serverId) {
|
||||
return buildHostAgentDraftRoute(serverId);
|
||||
return buildHostDraftRoute(serverId);
|
||||
}
|
||||
return "/";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user