feat: unify workspace routing through tab-based URLs and remove legacy per-type routes

This commit is contained in:
Mohamed Boudra
2026-03-03 21:14:15 +07:00
parent 74ac17f910
commit f3796e2ba5
25 changed files with 204 additions and 569 deletions
+20 -66
View File
@@ -1,7 +1,9 @@
import { describe, expect, it } from "vitest";
import {
buildHostWorkspaceAgentTabRoute,
buildHostWorkspaceFileTabRoute,
buildHostWorkspaceRoute,
buildHostWorkspaceFileRoute,
buildHostWorkspaceTerminalTabRoute,
decodeFilePathFromPathSegment,
decodeWorkspaceIdFromPathSegment,
encodeFilePathForPathSegment,
@@ -9,10 +11,7 @@ import {
parseHostAgentDraftRouteFromPathname,
parseHostAgentRouteFromPathname,
parseHostDraftRouteFromPathname,
parseHostWorkspaceAgentRouteFromPathname,
parseHostWorkspaceFileRouteFromPathname,
parseHostWorkspaceTabRouteFromPathname,
parseHostWorkspaceTerminalRouteFromPathname,
parseHostWorkspaceRouteFromPathname,
} from "./host-routes";
@@ -24,9 +23,7 @@ describe("parseHostAgentDraftRouteFromPathname", () => {
});
it("parses encoded server id", () => {
expect(
parseHostAgentDraftRouteFromPathname("/h/team%20host/new")
).toEqual({
expect(parseHostAgentDraftRouteFromPathname("/h/team%20host/new")).toEqual({
serverId: "team host",
});
});
@@ -66,9 +63,7 @@ describe("workspace route parsing", () => {
});
it("parses workspace route", () => {
expect(
parseHostWorkspaceRouteFromPathname("/h/local/workspace/L3RtcC9yZXBv")
).toEqual({
expect(parseHostWorkspaceRouteFromPathname("/h/local/workspace/L3RtcC9yZXBv")).toEqual({
serverId: "local",
workspaceId: "/tmp/repo",
});
@@ -83,48 +78,9 @@ describe("workspace route parsing", () => {
});
});
it("parses workspace file route", () => {
const encodedPath = encodeFilePathForPathSegment("src/index.ts");
expect(
parseHostWorkspaceFileRouteFromPathname(
`/h/local/workspace/L3RtcC9yZXBv/file/${encodedPath}`
)
).toEqual({
serverId: "local",
workspaceId: "/tmp/repo",
filePath: "src/index.ts",
});
});
it("parses workspace agent route", () => {
expect(
parseHostWorkspaceAgentRouteFromPathname(
"/h/local/workspace/L3RtcC9yZXBv/agent/agent-1"
)
).toEqual({
serverId: "local",
workspaceId: "/tmp/repo",
agentId: "agent-1",
});
});
it("parses workspace terminal route", () => {
expect(
parseHostWorkspaceTerminalRouteFromPathname(
"/h/local/workspace/L3RtcC9yZXBv/terminal/term-1"
)
).toEqual({
serverId: "local",
workspaceId: "/tmp/repo",
terminalId: "term-1",
});
});
it("parses workspace tab route", () => {
expect(
parseHostWorkspaceTabRouteFromPathname(
"/h/local/workspace/L3RtcC9yZXBv/tab/draft_abc123"
)
parseHostWorkspaceTabRouteFromPathname("/h/local/workspace/L3RtcC9yZXBv/tab/draft_abc123")
).toEqual({
serverId: "local",
workspaceId: "/tmp/repo",
@@ -132,27 +88,25 @@ describe("workspace route parsing", () => {
});
});
it("still parses legacy percent-encoded workspace routes", () => {
expect(
parseHostWorkspaceAgentRouteFromPathname(
"/h/local/workspace/%2Ftmp%2Frepo/agent/agent-1"
)
).toEqual({
serverId: "local",
workspaceId: "/tmp/repo",
agentId: "agent-1",
});
it("builds base64url workspace routes", () => {
expect(buildHostWorkspaceRoute("local", "/tmp/repo")).toBe("/h/local/workspace/L3RtcC9yZXBv");
});
it("builds base64url workspace routes", () => {
expect(buildHostWorkspaceRoute("local", "/tmp/repo")).toBe(
"/h/local/workspace/L3RtcC9yZXBv"
it("builds workspace agent tab routes", () => {
expect(buildHostWorkspaceAgentTabRoute("local", "/tmp/repo", "agent-1")).toBe(
"/h/local/workspace/L3RtcC9yZXBv/tab/agent_agent-1"
);
});
it("builds base64url workspace file routes", () => {
expect(buildHostWorkspaceFileRoute("local", "/tmp/repo", "src/index.ts")).toBe(
`/h/local/workspace/L3RtcC9yZXBv/file/${encodeFilePathForPathSegment("src/index.ts")}`
it("builds workspace terminal tab routes", () => {
expect(buildHostWorkspaceTerminalTabRoute("local", "/tmp/repo", "term-1")).toBe(
"/h/local/workspace/L3RtcC9yZXBv/tab/terminal_term-1"
);
});
it("builds workspace file tab routes", () => {
expect(buildHostWorkspaceFileTabRoute("local", "/tmp/repo", "src/index.ts")).toBe(
"/h/local/workspace/L3RtcC9yZXBv/tab/file_src%2Findex.ts"
);
});
});
+29 -179
View File
@@ -61,6 +61,21 @@ function normalizeWorkspaceId(value: string): string {
return value.trim().replace(/\\/g, "/").replace(/\/+$/, "");
}
function buildWorkspaceAgentTabId(agentId: string): string | null {
const normalized = trimNonEmpty(agentId);
return normalized ? `agent_${normalized}` : null;
}
function buildWorkspaceTerminalTabId(terminalId: string): string | null {
const normalized = trimNonEmpty(terminalId);
return normalized ? `terminal_${normalized}` : null;
}
function buildWorkspaceFileTabId(filePath: string): string | null {
const normalized = trimNonEmpty(filePath);
return normalized ? `file_${normalized.replace(/\\/g, "/")}` : null;
}
export function encodeWorkspaceIdForPathSegment(workspaceId: string): string {
const normalized = trimNonEmpty(workspaceId);
if (!normalized) {
@@ -222,22 +237,9 @@ export function parseHostWorkspaceRouteFromPathname(
const workspaceIdStart = serverIdEnd + workspacePrefix.length;
let workspaceIdEnd = pathname.length;
const agentIdx = pathname.lastIndexOf("/agent/");
if (agentIdx >= 0 && agentIdx > workspaceIdStart) {
workspaceIdEnd = Math.min(workspaceIdEnd, agentIdx);
}
const terminalIdx = pathname.lastIndexOf("/terminal/");
if (terminalIdx >= 0 && terminalIdx > workspaceIdStart) {
workspaceIdEnd = Math.min(workspaceIdEnd, terminalIdx);
}
const tabIdx = pathname.lastIndexOf("/tab/");
if (tabIdx >= 0 && tabIdx > workspaceIdStart) {
workspaceIdEnd = Math.min(workspaceIdEnd, tabIdx);
}
const fileIdx = pathname.lastIndexOf("/file/");
if (fileIdx >= 0 && fileIdx > workspaceIdStart) {
workspaceIdEnd = Math.min(workspaceIdEnd, fileIdx);
workspaceIdEnd = tabIdx;
}
const rawWorkspaceId = pathname.slice(workspaceIdStart, workspaceIdEnd).replace(/\/+$/, "");
@@ -248,106 +250,6 @@ export function parseHostWorkspaceRouteFromPathname(
return { serverId, workspaceId };
}
export function parseHostWorkspaceAgentRouteFromPathname(
pathname: string
): { serverId: string; workspaceId: string; agentId: string } | null {
const prefix = "/h/";
if (!pathname.startsWith(prefix)) {
return null;
}
const serverIdStart = prefix.length;
const serverIdEnd = pathname.indexOf("/", serverIdStart);
if (serverIdEnd < 0) {
return null;
}
const rawServerId = pathname.slice(serverIdStart, serverIdEnd);
const serverId = trimNonEmpty(decodeSegment(rawServerId));
if (!serverId) {
return null;
}
const workspacePrefix = "/workspace/";
if (!pathname.startsWith(workspacePrefix, serverIdEnd)) {
return null;
}
const workspaceIdStart = serverIdEnd + workspacePrefix.length;
const agentMarker = "/agent/";
const agentIdx = pathname.lastIndexOf(agentMarker);
if (agentIdx < 0 || agentIdx <= workspaceIdStart) {
return null;
}
const rawWorkspaceId = pathname.slice(workspaceIdStart, agentIdx).replace(/\/+$/, "");
const workspaceId = decodeWorkspaceIdFromPathSegment(rawWorkspaceId);
if (!workspaceId) {
return null;
}
const agentIdStart = agentIdx + agentMarker.length;
const agentIdEnd = pathname.indexOf("/", agentIdStart);
const rawAgentId =
agentIdEnd < 0 ? pathname.slice(agentIdStart) : pathname.slice(agentIdStart, agentIdEnd);
const agentId = trimNonEmpty(decodeSegment(rawAgentId));
if (!agentId) {
return null;
}
return { serverId, workspaceId, agentId };
}
export function parseHostWorkspaceTerminalRouteFromPathname(
pathname: string
): { serverId: string; workspaceId: string; terminalId: string } | null {
const prefix = "/h/";
if (!pathname.startsWith(prefix)) {
return null;
}
const serverIdStart = prefix.length;
const serverIdEnd = pathname.indexOf("/", serverIdStart);
if (serverIdEnd < 0) {
return null;
}
const rawServerId = pathname.slice(serverIdStart, serverIdEnd);
const serverId = trimNonEmpty(decodeSegment(rawServerId));
if (!serverId) {
return null;
}
const workspacePrefix = "/workspace/";
if (!pathname.startsWith(workspacePrefix, serverIdEnd)) {
return null;
}
const workspaceIdStart = serverIdEnd + workspacePrefix.length;
const terminalMarker = "/terminal/";
const terminalIdx = pathname.lastIndexOf(terminalMarker);
if (terminalIdx < 0 || terminalIdx <= workspaceIdStart) {
return null;
}
const rawWorkspaceId = pathname.slice(workspaceIdStart, terminalIdx).replace(/\/+$/, "");
const workspaceId = decodeWorkspaceIdFromPathSegment(rawWorkspaceId);
if (!workspaceId) {
return null;
}
const terminalIdStart = terminalIdx + terminalMarker.length;
const terminalIdEnd = pathname.indexOf("/", terminalIdStart);
const rawTerminalId =
terminalIdEnd < 0
? pathname.slice(terminalIdStart)
: pathname.slice(terminalIdStart, terminalIdEnd);
const terminalId = trimNonEmpty(decodeSegment(rawTerminalId));
if (!terminalId) {
return null;
}
return { serverId, workspaceId, terminalId };
}
export function parseHostWorkspaceTabRouteFromPathname(
pathname: string
): { serverId: string; workspaceId: string; tabId: string } | null {
@@ -397,55 +299,6 @@ export function parseHostWorkspaceTabRouteFromPathname(
return { serverId, workspaceId, tabId };
}
export function parseHostWorkspaceFileRouteFromPathname(
pathname: string
): { serverId: string; workspaceId: string; filePath: string } | null {
const prefix = "/h/";
if (!pathname.startsWith(prefix)) {
return null;
}
const serverIdStart = prefix.length;
const serverIdEnd = pathname.indexOf("/", serverIdStart);
if (serverIdEnd < 0) {
return null;
}
const rawServerId = pathname.slice(serverIdStart, serverIdEnd);
const serverId = trimNonEmpty(decodeSegment(rawServerId));
if (!serverId) {
return null;
}
const workspacePrefix = "/workspace/";
if (!pathname.startsWith(workspacePrefix, serverIdEnd)) {
return null;
}
const workspaceIdStart = serverIdEnd + workspacePrefix.length;
const fileMarker = "/file/";
const fileIdx = pathname.lastIndexOf(fileMarker);
if (fileIdx < 0 || fileIdx <= workspaceIdStart) {
return null;
}
const rawWorkspaceId = pathname.slice(workspaceIdStart, fileIdx).replace(/\/+$/, "");
const workspaceId = decodeWorkspaceIdFromPathSegment(rawWorkspaceId);
if (!workspaceId) {
return null;
}
const filePathStart = fileIdx + fileMarker.length;
const filePathEnd = pathname.indexOf("/", filePathStart);
const rawFilePath =
filePathEnd < 0 ? pathname.slice(filePathStart) : pathname.slice(filePathStart, filePathEnd);
const filePath = decodeFilePathFromPathSegment(rawFilePath);
if (!filePath) {
return null;
}
return { serverId, workspaceId, filePath };
}
export function buildHostWorkspaceRoute(
serverId: string,
workspaceId: string
@@ -462,30 +315,28 @@ export function buildHostWorkspaceRoute(
return `/h/${encodeSegment(normalizedServerId)}/workspace/${encodeSegment(encodedWorkspaceId)}`;
}
export function buildHostWorkspaceAgentRoute(
export function buildHostWorkspaceAgentTabRoute(
serverId: string,
workspaceId: string,
agentId: string
): string {
const base = buildHostWorkspaceRoute(serverId, workspaceId);
const normalizedAgentId = trimNonEmpty(agentId);
if (base === "/" || !normalizedAgentId) {
const tabId = buildWorkspaceAgentTabId(agentId);
if (!tabId) {
return "/";
}
return `${base}/agent/${encodeSegment(normalizedAgentId)}`;
return buildHostWorkspaceTabRoute(serverId, workspaceId, tabId);
}
export function buildHostWorkspaceTerminalRoute(
export function buildHostWorkspaceTerminalTabRoute(
serverId: string,
workspaceId: string,
terminalId: string
): string {
const base = buildHostWorkspaceRoute(serverId, workspaceId);
const normalizedTerminalId = trimNonEmpty(terminalId);
if (base === "/" || !normalizedTerminalId) {
const tabId = buildWorkspaceTerminalTabId(terminalId);
if (!tabId) {
return "/";
}
return `${base}/terminal/${encodeSegment(normalizedTerminalId)}`;
return buildHostWorkspaceTabRoute(serverId, workspaceId, tabId);
}
export function buildHostWorkspaceTabRoute(
@@ -501,17 +352,16 @@ export function buildHostWorkspaceTabRoute(
return `${base}/tab/${encodeSegment(normalizedTabId)}`;
}
export function buildHostWorkspaceFileRoute(
export function buildHostWorkspaceFileTabRoute(
serverId: string,
workspaceId: string,
filePath: string
): string {
const base = buildHostWorkspaceRoute(serverId, workspaceId);
const encodedFilePath = encodeFilePathForPathSegment(filePath);
if (base === "/" || !encodedFilePath) {
const tabId = buildWorkspaceFileTabId(filePath);
if (!tabId) {
return "/";
}
return `${base}/file/${encodeSegment(encodedFilePath)}`;
return buildHostWorkspaceTabRoute(serverId, workspaceId, tabId);
}
export function buildHostAgentDetailRoute(
@@ -521,7 +371,7 @@ export function buildHostAgentDetailRoute(
): string {
const normalizedWorkspaceId = trimNonEmpty(workspaceId);
if (normalizedWorkspaceId) {
return buildHostWorkspaceAgentRoute(
return buildHostWorkspaceAgentTabRoute(
serverId,
normalizedWorkspaceId,
agentId
@@ -77,7 +77,7 @@ describe("resolveSelectedAgentForNewAgent", () => {
it("prefers the agent in the current route", () => {
expect(
resolveSelectedAgentForNewAgent({
pathname: "/h/srv-1/workspace/%2Frepo/agent/agent-2",
pathname: "/h/srv-1/workspace/%2Frepo/tab/agent_agent-2",
selectedAgentId: "srv-9:agent-9",
})
).toEqual({
+7 -4
View File
@@ -2,7 +2,7 @@ import type { CheckoutStatusPayload } from "@/hooks/use-checkout-status-query";
import {
buildHostAgentDraftRoute,
parseHostAgentRouteFromPathname,
parseHostWorkspaceAgentRouteFromPathname,
parseHostWorkspaceTabRouteFromPathname,
} from "@/utils/host-routes";
export function parseAgentKey(
@@ -27,9 +27,12 @@ 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 };
const workspaceTabRoute = parseHostWorkspaceTabRouteFromPathname(input.pathname);
if (workspaceTabRoute?.tabId?.startsWith("agent_")) {
const agentId = workspaceTabRoute.tabId.slice("agent_".length).trim();
if (agentId) {
return { serverId: workspaceTabRoute.serverId, agentId };
}
}
return (
parseHostAgentRouteFromPathname(input.pathname) ??