feat: canonical workspace tab IDs with URL routing and draft agent tabs

Rewrite workspace tab management to use stable tab IDs (agent_, terminal_, draft_, file_) with
dedicated /tab/[tabId] routes, replace separate file-tabs store with unified workspace-tabs store,
support multiple concurrent draft agent tabs, and detect text files by content instead of extension allowlist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2026-03-03 20:11:55 +07:00
co-authored by Claude Opus 4.6
parent 97f0f4b266
commit 73eabd2eea
17 changed files with 1732 additions and 768 deletions
@@ -11,6 +11,7 @@ import {
parseHostDraftRouteFromPathname,
parseHostWorkspaceAgentRouteFromPathname,
parseHostWorkspaceFileRouteFromPathname,
parseHostWorkspaceTabRouteFromPathname,
parseHostWorkspaceTerminalRouteFromPathname,
parseHostWorkspaceRouteFromPathname,
} from "./host-routes";
@@ -73,6 +74,15 @@ describe("workspace route parsing", () => {
});
});
it("parses workspace route for /tab targets", () => {
expect(
parseHostWorkspaceRouteFromPathname("/h/local/workspace/L3RtcC9yZXBv/tab/draft_abc123")
).toEqual({
serverId: "local",
workspaceId: "/tmp/repo",
});
});
it("parses workspace file route", () => {
const encodedPath = encodeFilePathForPathSegment("src/index.ts");
expect(
@@ -110,6 +120,18 @@ describe("workspace route parsing", () => {
});
});
it("parses workspace tab route", () => {
expect(
parseHostWorkspaceTabRouteFromPathname(
"/h/local/workspace/L3RtcC9yZXBv/tab/draft_abc123"
)
).toEqual({
serverId: "local",
workspaceId: "/tmp/repo",
tabId: "draft_abc123",
});
});
it("still parses legacy percent-encoded workspace routes", () => {
expect(
parseHostWorkspaceAgentRouteFromPathname(
+66
View File
@@ -231,6 +231,10 @@ export function parseHostWorkspaceRouteFromPathname(
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);
@@ -344,6 +348,55 @@ export function parseHostWorkspaceTerminalRouteFromPathname(
return { serverId, workspaceId, terminalId };
}
export function parseHostWorkspaceTabRouteFromPathname(
pathname: string
): { serverId: string; workspaceId: string; tabId: 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 tabMarker = "/tab/";
const tabIdx = pathname.lastIndexOf(tabMarker);
if (tabIdx < 0 || tabIdx <= workspaceIdStart) {
return null;
}
const rawWorkspaceId = pathname.slice(workspaceIdStart, tabIdx).replace(/\/+$/, "");
const workspaceId = decodeWorkspaceIdFromPathSegment(rawWorkspaceId);
if (!workspaceId) {
return null;
}
const tabIdStart = tabIdx + tabMarker.length;
const tabIdEnd = pathname.indexOf("/", tabIdStart);
const rawTabId =
tabIdEnd < 0 ? pathname.slice(tabIdStart) : pathname.slice(tabIdStart, tabIdEnd);
const tabId = trimNonEmpty(decodeSegment(rawTabId));
if (!tabId) {
return null;
}
return { serverId, workspaceId, tabId };
}
export function parseHostWorkspaceFileRouteFromPathname(
pathname: string
): { serverId: string; workspaceId: string; filePath: string } | null {
@@ -435,6 +488,19 @@ export function buildHostWorkspaceTerminalRoute(
return `${base}/terminal/${encodeSegment(normalizedTerminalId)}`;
}
export function buildHostWorkspaceTabRoute(
serverId: string,
workspaceId: string,
tabId: string
): string {
const base = buildHostWorkspaceRoute(serverId, workspaceId);
const normalizedTabId = trimNonEmpty(tabId);
if (base === "/" || !normalizedTabId) {
return "/";
}
return `${base}/tab/${encodeSegment(normalizedTabId)}`;
}
export function buildHostWorkspaceFileRoute(
serverId: string,
workspaceId: string,