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
+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,