app: hard-cut workspace routing to workspace-only URLs

This commit is contained in:
Mohamed Boudra
2026-03-04 22:35:30 +07:00
parent ee8e2ab691
commit 73fce89edd
22 changed files with 300 additions and 608 deletions
+7 -5
View File
@@ -47,7 +47,7 @@ import { buildNotificationRoute } from "@/utils/notification-routing";
import {
buildHostRootRoute,
parseHostAgentRouteFromPathname,
parseHostWorkspaceTabRouteFromPathname,
parseHostWorkspaceOpenIntentFromPathname,
} from "@/utils/host-routes";
import { getTauri } from "@/utils/tauri";
import { PerfDiagnosticsProvider } from "@/runtime/perf-diagnostics";
@@ -381,10 +381,12 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
// Parse selectedAgentKey directly from pathname
// useLocalSearchParams doesn't update when navigating between same-pattern routes
const selectedAgentKey = useMemo(() => {
const workspaceTab = parseHostWorkspaceTabRouteFromPathname(pathname);
if (workspaceTab?.tabId?.startsWith("agent_")) {
const agentId = workspaceTab.tabId.slice("agent_".length).trim();
return agentId ? `${workspaceTab.serverId}:${agentId}` : undefined;
const workspaceMatch = pathname.match(/^\/h\/([^/]+)\/workspace\/[^/]+(?:\/|$)/);
const workspaceServerId = workspaceMatch?.[1]?.trim() ?? "";
const openIntent = parseHostWorkspaceOpenIntentFromPathname(pathname);
if (workspaceServerId && openIntent?.kind === "agent") {
const agentId = openIntent.agentId.trim();
return agentId ? `${workspaceServerId}:${agentId}` : undefined;
}
const match = parseHostAgentRouteFromPathname(pathname);
@@ -4,7 +4,7 @@ import { useSessionStore } from "@/stores/session-store";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
import {
buildHostRootRoute,
buildHostWorkspaceAgentTabRoute,
buildHostWorkspaceAgentRoute,
} from "@/utils/host-routes";
export default function HostAgentReadyRoute() {
@@ -38,7 +38,7 @@ export default function HostAgentReadyRoute() {
if (normalizedCwd) {
redirectedRef.current = true;
router.replace(
buildHostWorkspaceAgentTabRoute(serverId, normalizedCwd, agentId) as any
buildHostWorkspaceAgentRoute(serverId, normalizedCwd, agentId) as any
);
}
}, [agentCwd, agentId, router, serverId]);
@@ -77,7 +77,7 @@ export default function HostAgentReadyRoute() {
const cwd = result?.agent?.cwd?.trim();
redirectedRef.current = true;
if (cwd) {
router.replace(buildHostWorkspaceAgentTabRoute(serverId, cwd, agentId) as any);
router.replace(buildHostWorkspaceAgentRoute(serverId, cwd, agentId) as any);
return;
}
router.replace(buildHostRootRoute(serverId) as any);
+2 -42
View File
@@ -5,30 +5,12 @@ import { useSessionStore } from "@/stores/session-store";
import { useFormPreferences } from "@/hooks/use-form-preferences";
import {
buildHostRootRoute,
buildHostWorkspaceAgentRoute,
buildHostWorkspaceRoute,
buildHostWorkspaceAgentTabRoute,
buildHostWorkspaceTabRoute,
parseHostWorkspaceTabRouteFromPathname,
} from "@/utils/host-routes";
const HOST_ROOT_REDIRECT_DELAY_MS = 300;
function readInitialNavigationPathname(): string | null {
if (Platform.OS !== "web" || typeof window === "undefined") {
return null;
}
const entry = performance.getEntriesByType("navigation")[0] as PerformanceNavigationTiming | undefined;
const name = typeof entry?.name === "string" ? entry.name : null;
if (!name) {
return null;
}
try {
return new URL(name).pathname;
} catch {
return null;
}
}
export default function HostIndexRoute() {
const router = useRouter();
const routerPathname = usePathname();
@@ -60,28 +42,6 @@ export default function HostIndexRoute() {
if (currentPathname !== rootRoute && currentPathname !== `${rootRoute}/`) {
return;
}
const initialPathname = readInitialNavigationPathname();
const initialWorkspaceTabRoute = initialPathname
? parseHostWorkspaceTabRouteFromPathname(initialPathname)
: null;
if (
initialWorkspaceTabRoute &&
initialWorkspaceTabRoute.serverId === serverId
) {
const canonicalInitialTabPath = buildHostWorkspaceTabRoute(
initialWorkspaceTabRoute.serverId,
initialWorkspaceTabRoute.workspaceId,
initialWorkspaceTabRoute.tabId
);
if (
canonicalInitialTabPath !== "/" &&
window.location.pathname !== canonicalInitialTabPath
) {
router.replace(canonicalInitialTabPath as any);
}
return;
}
}
const visibleAgents = sessionAgents
@@ -96,7 +56,7 @@ export default function HostIndexRoute() {
const primaryAgent = visibleAgents[0];
if (primaryAgent?.cwd?.trim()) {
router.replace(
buildHostWorkspaceAgentTabRoute(
buildHostWorkspaceAgentRoute(
serverId,
primaryAgent.cwd.trim(),
primaryAgent.id
@@ -1,10 +1,9 @@
import { useEffect } from "react";
import { usePathname } from "expo-router";
import { Platform } from "react-native";
import { WorkspaceScreen } from "@/screens/workspace/workspace-screen";
import {
parseHostWorkspaceOpenIntentFromPathname,
parseHostWorkspaceRouteFromPathname,
parseHostWorkspaceTabRouteFromPathname,
} from "@/utils/host-routes";
export default function HostWorkspaceLayout() {
@@ -13,29 +12,17 @@ export default function HostWorkspaceLayout() {
Platform.OS === "web" && typeof window !== "undefined"
? `${window.location.pathname}${window.location.search}${window.location.hash}`
: pathname;
const tabRoute = parseHostWorkspaceTabRouteFromPathname(resolvedPathname);
const activeRoute = tabRoute ?? parseHostWorkspaceRouteFromPathname(resolvedPathname);
const activeRoute = parseHostWorkspaceRouteFromPathname(resolvedPathname);
const serverId = activeRoute?.serverId ?? "";
const workspaceId = activeRoute?.workspaceId ?? "";
const routeTabId = tabRoute?.tabId ?? null;
useEffect(() => {
if (Platform.OS !== "web" || !tabRoute || typeof window === "undefined") {
return;
}
const params = new URLSearchParams(window.location.search);
if (!params.has("tabId") && !params.has("workspaceId") && !params.has("serverId")) {
return;
}
window.history.replaceState(window.history.state, "", window.location.pathname);
}, [resolvedPathname, tabRoute]);
const openIntent = parseHostWorkspaceOpenIntentFromPathname(resolvedPathname);
return (
<WorkspaceScreen
key={`${serverId}:${workspaceId}`}
serverId={serverId}
workspaceId={workspaceId}
routeTabId={routeTabId}
openIntent={openIntent}
/>
);
}
@@ -1,3 +0,0 @@
export default function HostWorkspaceTabRoute() {
return null;
}
+2 -2
View File
@@ -33,7 +33,7 @@ import {
startNavigationTiming,
} from "@/utils/navigation-timing";
import {
buildHostWorkspaceAgentTabRoute,
buildHostWorkspaceAgentRoute,
} from "@/utils/host-routes";
interface AgentListProps {
@@ -189,7 +189,7 @@ export function AgentList({
onAgentSelect?.();
const route: Href = buildHostWorkspaceAgentTabRoute(
const route: Href = buildHostWorkspaceAgentRoute(
serverId,
agent.cwd,
agentId
@@ -82,7 +82,7 @@ import { createMarkdownStyles } from "@/styles/markdown-styles";
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
import { getMarkdownListMarker } from "@/utils/markdown-list";
import { buildHostWorkspaceFileTabRoute } from "@/utils/host-routes";
import { buildHostWorkspaceFileRoute } from "@/utils/host-routes";
const isUserMessageItem = (item?: StreamItem) => item?.kind === "user_message";
const isToolSequenceItem = (item?: StreamItem) =>
@@ -213,7 +213,7 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
}
if (normalized.file) {
const route = buildHostWorkspaceFileTabRoute(
const route = buildHostWorkspaceFileRoute(
resolvedServerId,
workspaceId,
normalized.file
+2 -2
View File
@@ -9,7 +9,7 @@ import {
} from "@/utils/command-center-focus-restore";
import {
buildHostNewAgentRoute,
buildHostWorkspaceAgentTabRoute,
buildHostWorkspaceAgentRoute,
buildHostSettingsRoute,
parseHostAgentRouteFromPathname,
parseServerIdFromPathname,
@@ -198,7 +198,7 @@ export function useCommandCenter() {
// Don't restore focus back to the prior element after we navigate.
clearCommandCenterFocusRestoreElement();
setOpen(false);
const route: Href = buildHostWorkspaceAgentTabRoute(
const route: Href = buildHostWorkspaceAgentRoute(
agent.serverId,
agent.cwd,
agent.id
@@ -10,7 +10,7 @@ describe("keyboard-shortcut-routing", () => {
it("returns selected agent key when provided", () => {
const key = resolveSelectedOrRouteAgentKey({
selectedAgentId: "server-1:agent-1",
pathname: "/h/server-1/workspace/workspace-1/tab/draft_123",
pathname: "/h/server-1/workspace/workspace-1?open=draft%3Adraft_123",
});
expect(key).toBe("server-1:agent-1");
@@ -18,7 +18,7 @@ describe("keyboard-shortcut-routing", () => {
it("maps workspace draft tabs to the draft tab id key", () => {
const key = resolveSelectedOrRouteAgentKey({
pathname: "/h/server-1/workspace/workspace-1/tab/draft_123",
pathname: "/h/server-1/workspace/workspace-1?open=draft%3Adraft_123",
});
expect(key).toBe("server-1:draft_123");
@@ -31,7 +31,7 @@ describe("keyboard-shortcut-routing", () => {
it("allows the shortcut on selected-agent routes", () => {
const canToggle = canToggleFileExplorerShortcut({
selectedAgentId: "server-1:agent-1",
pathname: "/h/server-1/workspace/workspace-1/tab/agent_agent-1",
pathname: "/h/server-1/workspace/workspace-1?open=agent%3Aagent-1",
toggleFileExplorer,
});
@@ -47,18 +47,18 @@ describe("keyboard-shortcut-routing", () => {
expect(canToggle).toBe(true);
});
it("allows the shortcut on workspace terminal routes", () => {
it("allows the shortcut on workspace routes with intent query", () => {
const canToggle = canToggleFileExplorerShortcut({
pathname: "/h/server-1/workspace/workspace-1/tab/terminal_terminal-1",
pathname: "/h/server-1/workspace/workspace-1?open=terminal%3Aterminal-1",
toggleFileExplorer,
});
expect(canToggle).toBe(true);
});
it("allows the shortcut on workspace draft-tab routes", () => {
it("allows the shortcut on workspace draft-intent routes", () => {
const canToggle = canToggleFileExplorerShortcut({
pathname: "/h/server-1/workspace/workspace-1/tab/draft_123",
pathname: "/h/server-1/workspace/workspace-1?open=draft%3Adraft_123",
toggleFileExplorer,
});
@@ -67,7 +67,7 @@ describe("keyboard-shortcut-routing", () => {
it("blocks the shortcut when no toggle handler exists", () => {
const canToggle = canToggleFileExplorerShortcut({
pathname: "/h/server-1/workspace/workspace-1/tab/draft_123",
pathname: "/h/server-1/workspace/workspace-1?open=draft%3Adraft_123",
});
expect(canToggle).toBe(false);
@@ -1,7 +1,7 @@
import {
parseHostAgentRouteFromPathname,
parseHostWorkspaceOpenIntentFromPathname,
parseHostWorkspaceRouteFromPathname,
parseHostWorkspaceTabRouteFromPathname,
} from "@/utils/host-routes";
export function resolveSelectedOrRouteAgentKey(input: {
@@ -12,15 +12,16 @@ export function resolveSelectedOrRouteAgentKey(input: {
return input.selectedAgentId;
}
const workspaceTabRoute = parseHostWorkspaceTabRouteFromPathname(input.pathname);
if (workspaceTabRoute?.tabId) {
const tabId = workspaceTabRoute.tabId;
if (tabId.startsWith("agent_")) {
const agentId = tabId.slice("agent_".length).trim();
return agentId ? `${workspaceTabRoute.serverId}:${agentId}` : null;
const workspaceRoute = parseHostWorkspaceRouteFromPathname(input.pathname);
const workspaceOpenIntent = parseHostWorkspaceOpenIntentFromPathname(input.pathname);
if (workspaceRoute && workspaceOpenIntent) {
if (workspaceOpenIntent.kind === "agent") {
const agentId = workspaceOpenIntent.agentId.trim();
return agentId ? `${workspaceRoute.serverId}:${agentId}` : null;
}
if (tabId.startsWith("draft_")) {
return `${workspaceTabRoute.serverId}:${tabId}`;
if (workspaceOpenIntent.kind === "draft") {
const draftId = workspaceOpenIntent.draftId.trim();
return draftId ? `${workspaceRoute.serverId}:${draftId}` : null;
}
}
@@ -48,7 +48,7 @@ import type {
AgentSessionConfig,
} from '@server/server/agent/agent-sdk-types'
import { AGENT_PROVIDER_DEFINITIONS } from '@server/server/agent/provider-manifest'
import { buildHostWorkspaceAgentTabRoute } from '@/utils/host-routes'
import { buildHostWorkspaceAgentRoute } from '@/utils/host-routes'
import { useTauriDragHandlers } from '@/utils/tauri-window'
import { useKeyboardShiftStyle } from '@/hooks/use-keyboard-shift-style'
import { normalizeAgentSnapshot } from '@/utils/agent-snapshots'
@@ -1055,7 +1055,7 @@ function DraftAgentScreenContent({
const configuredWorkingDir = config.cwd.trim()
const workspaceId =
createdWorkingDir.length > 0 ? createdWorkingDir : configuredWorkingDir
const route: Href = buildHostWorkspaceAgentTabRoute(
const route: Href = buildHostWorkspaceAgentRoute(
selectedServerId,
workspaceId,
agentId
@@ -1,89 +0,0 @@
import { describe, expect, it } from "vitest";
import {
buildHostWorkspaceRoute,
buildHostWorkspaceTabRoute,
} from "@/utils/host-routes";
import { resolveWorkspaceTabCanonicalPath } from "@/screens/workspace/workspace-route-canonicalization";
describe("resolveWorkspaceTabCanonicalPath", () => {
it("canonicalizes workspace root routes to /tab/<tabId> when a tab is active", () => {
const serverId = "local";
const workspaceId = "/tmp/repo";
expect(
resolveWorkspaceTabCanonicalPath({
activeTabId: "draft_abc123",
pathname: buildHostWorkspaceRoute(serverId, workspaceId),
serverId,
workspaceId,
})
).toBe(buildHostWorkspaceTabRoute(serverId, workspaceId, "draft_abc123"));
});
it("does not skip canonical path detection due to query-string tabId", () => {
const serverId = "local";
const workspaceId = "/tmp/repo";
expect(
resolveWorkspaceTabCanonicalPath({
activeTabId: "draft_abc123",
pathname: `${buildHostWorkspaceRoute(serverId, workspaceId)}?tabId=draft_abc123`,
serverId,
workspaceId,
})
).toBe(buildHostWorkspaceTabRoute(serverId, workspaceId, "draft_abc123"));
});
it("strips redundant query params from canonical /tab routes", () => {
const serverId = "local";
const workspaceId = "/tmp/repo";
const canonicalPath = buildHostWorkspaceTabRoute(
serverId,
workspaceId,
"draft_abc123"
);
expect(
resolveWorkspaceTabCanonicalPath({
activeTabId: "draft_abc123",
pathname: `${canonicalPath}?tabId=not_used`,
serverId,
workspaceId,
})
).toBe(canonicalPath);
});
it("does nothing when the pathname is exactly canonical", () => {
const serverId = "local";
const workspaceId = "/tmp/repo";
const canonicalPath = buildHostWorkspaceTabRoute(
serverId,
workspaceId,
"draft_abc123"
);
expect(
resolveWorkspaceTabCanonicalPath({
activeTabId: "draft_abc123",
pathname: canonicalPath,
serverId,
workspaceId,
})
).toBeNull();
});
it("does not canonicalize when the pathname belongs to another workspace", () => {
const serverId = "local";
const workspaceId = "/tmp/repo";
const otherWorkspacePath = buildHostWorkspaceRoute(serverId, "/tmp/other");
expect(
resolveWorkspaceTabCanonicalPath({
activeTabId: "draft_abc123",
pathname: otherWorkspacePath,
serverId,
workspaceId,
})
).toBeNull();
});
});
@@ -1,55 +0,0 @@
import {
buildHostWorkspaceTabRoute,
parseHostWorkspaceRouteFromPathname,
} from "@/utils/host-routes";
function stripSearchAndHash(pathname: string): string {
const hashIndex = pathname.indexOf("#");
const queryIndex = pathname.indexOf("?");
const end = [hashIndex, queryIndex]
.filter((index) => index >= 0)
.reduce((min, index) => Math.min(min, index), pathname.length);
return pathname.slice(0, end);
}
type ResolveWorkspaceTabCanonicalPathInput = {
activeTabId: string | null;
pathname: string;
serverId: string;
workspaceId: string;
};
export function resolveWorkspaceTabCanonicalPath(
input: ResolveWorkspaceTabCanonicalPathInput
): string | null {
const { activeTabId, pathname, serverId, workspaceId } = input;
if (!activeTabId || !serverId || !workspaceId) {
return null;
}
const canonicalPath = buildHostWorkspaceTabRoute(serverId, workspaceId, activeTabId);
if (canonicalPath === "/") {
return null;
}
const activeWorkspaceRoute = parseHostWorkspaceRouteFromPathname(pathname);
if (
!activeWorkspaceRoute ||
activeWorkspaceRoute.serverId !== serverId ||
activeWorkspaceRoute.workspaceId !== workspaceId
) {
return null;
}
const pathOnly = stripSearchAndHash(pathname);
if (pathOnly !== canonicalPath) {
return canonicalPath;
}
// Keep workspace tab URLs clean: no query/hash source-of-truth for tab routing.
if (pathname !== canonicalPath) {
return canonicalPath;
}
return null;
}
@@ -8,7 +8,6 @@ import {
View,
} from "react-native";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { usePathname, useRouter } from "expo-router";
import * as Clipboard from "expo-clipboard";
import {
Bot,
@@ -50,9 +49,9 @@ import {
useWorkspaceTabsStore,
} from "@/stores/workspace-tabs-store";
import {
buildHostWorkspaceTabRoute,
buildWorkspaceOpenIntentParam,
type WorkspaceOpenIntent,
decodeWorkspaceIdFromPathSegment,
encodeWorkspaceIdForPathSegment,
} from "@/utils/host-routes";
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
@@ -82,7 +81,6 @@ import {
import {
deriveWorkspaceTabModel,
} from "@/screens/workspace/workspace-tab-model";
import { resolveWorkspaceTabCanonicalPath } from "@/screens/workspace/workspace-route-canonicalization";
const TERMINALS_QUERY_STALE_TIME = 5_000;
const NEW_TAB_AGENT_OPTION_ID = "__new_tab_agent__";
@@ -93,7 +91,7 @@ const EMPTY_TAB_ORDER: string[] = [];
type WorkspaceScreenProps = {
serverId: string;
workspaceId: string;
routeTabId?: string | null;
openIntent?: WorkspaceOpenIntent | null;
};
function trimNonEmpty(value: string | null | undefined): string | null {
@@ -112,38 +110,17 @@ function decodeSegment(value: string): string {
}
}
function buildWorkspaceTabHref(
serverId: string,
workspaceId: string,
tabId: string
): { pathname: string; params: { serverId: string; workspaceId: string; tabId: string } } | null {
const encodedWorkspaceId = encodeWorkspaceIdForPathSegment(workspaceId);
const trimmedTabId = tabId.trim();
const trimmedServerId = serverId.trim();
if (!encodedWorkspaceId || !trimmedTabId || !trimmedServerId) {
return null;
}
return {
pathname: "/h/[serverId]/workspace/[workspaceId]/tab/[tabId]",
params: {
serverId: trimmedServerId,
workspaceId: encodedWorkspaceId,
tabId: trimmedTabId,
},
};
}
export function WorkspaceScreen({
serverId,
workspaceId,
routeTabId,
openIntent,
}: WorkspaceScreenProps) {
return (
<ExplorerSidebarAnimationProvider>
<WorkspaceScreenContent
serverId={serverId}
workspaceId={workspaceId}
routeTabId={routeTabId}
openIntent={openIntent}
/>
</ExplorerSidebarAnimationProvider>
);
@@ -152,16 +129,10 @@ export function WorkspaceScreen({
function WorkspaceScreenContent({
serverId,
workspaceId,
routeTabId,
openIntent,
}: WorkspaceScreenProps) {
const { theme } = useUnistyles();
const toast = useToast();
const router = useRouter();
const routePathname = usePathname();
const pathname =
Platform.OS === "web" && typeof window !== "undefined"
? `${window.location.pathname}${window.location.search}${window.location.hash}`
: routePathname;
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
@@ -241,15 +212,11 @@ function WorkspaceScreenContent({
target: { kind: "terminal", terminalId: createdTerminal.id },
});
if (tabId) {
const href = buildWorkspaceTabHref(
normalizedServerId,
normalizedWorkspaceId,
tabId
);
if (!href) {
return;
}
router.replace(href as any);
useWorkspaceTabsStore.getState().focusTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
tabId,
});
}
}
},
@@ -441,23 +408,37 @@ function WorkspaceScreenContent({
const closeWorkspaceTab = useWorkspaceTabsStore((state) => state.closeTab);
const promoteDraftToAgent = useWorkspaceTabsStore((state) => state.promoteDraftToAgent);
const reorderWorkspaceTabs = useWorkspaceTabsStore((state) => state.reorderTabs);
const [pendingRouteTabId, setPendingRouteTabId] = useState<string | null>(null);
const consumedOpenIntentsRef = useRef(new Set<string>());
useEffect(() => {
const normalized = typeof routeTabId === "string" ? routeTabId.trim() : "";
if (!normalized || !persistenceKey) {
if (!openIntent || !persistenceKey) {
return;
}
if (normalized.startsWith("file_")) {
const path = normalized.slice("file_".length).trim();
if (!path) {
return;
}
const tabId = openOrFocusTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
target: { kind: "file", path },
});
const openParam = buildWorkspaceOpenIntentParam(openIntent);
if (!openParam) {
return;
}
const intentKey = `${normalizedServerId}:${normalizedWorkspaceId}:${openParam}`;
if (consumedOpenIntentsRef.current.has(intentKey)) {
return;
}
consumedOpenIntentsRef.current.add(intentKey);
if (openIntent.kind === "draft") {
const draftId = openIntent.draftId.trim();
const tabId =
draftId === "new"
? openDraftTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
draftId: generateDraftId(),
})
: openDraftTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
draftId,
});
if (tabId) {
focusTab({
serverId: normalizedServerId,
@@ -465,33 +446,36 @@ function WorkspaceScreenContent({
tabId,
});
}
return;
}
const tabId = openOrFocusTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
target:
openIntent.kind === "agent"
? { kind: "agent", agentId: openIntent.agentId }
: openIntent.kind === "terminal"
? { kind: "terminal", terminalId: openIntent.terminalId }
: { kind: "file", path: openIntent.path },
});
if (tabId) {
focusTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
tabId,
});
}
}, [
focusTab,
openDraftTab,
openIntent,
openOrFocusTab,
persistenceKey,
normalizedServerId,
normalizedWorkspaceId,
routeTabId,
]);
const normalizedRouteTabId = trimNonEmpty(routeTabId);
const effectiveRouteTabId = pendingRouteTabId ?? normalizedRouteTabId;
useEffect(() => {
if (!pendingRouteTabId || !normalizedRouteTabId) {
return;
}
if (normalizedRouteTabId === pendingRouteTabId) {
setPendingRouteTabId(null);
}
}, [normalizedRouteTabId, pendingRouteTabId]);
useEffect(() => {
setPendingRouteTabId(null);
}, [normalizedServerId, normalizedWorkspaceId]);
const tabModel = useMemo(
() =>
deriveWorkspaceTabModel({
@@ -500,9 +484,8 @@ function WorkspaceScreenContent({
uiTabs,
tabOrder,
focusedTabId,
routeTabId: effectiveRouteTabId,
}),
[effectiveRouteTabId, focusedTabId, tabOrder, terminals, uiTabs, workspaceAgents]
[focusedTabId, tabOrder, terminals, uiTabs, workspaceAgents]
);
const activeTabId = tabModel.activeTabId;
@@ -513,77 +496,6 @@ function WorkspaceScreenContent({
focusTab({ serverId: normalizedServerId, workspaceId: normalizedWorkspaceId, tabId: activeTabId });
}, [activeTabId, focusTab, normalizedServerId, normalizedWorkspaceId, persistenceKey]);
useEffect(() => {
if (!pendingRouteTabId || !activeTabId) {
return;
}
if (pendingRouteTabId === activeTabId) {
setPendingRouteTabId(null);
}
}, [activeTabId, pendingRouteTabId]);
const lastCanonicalPathRef = useRef<string | null>(null);
useEffect(() => {
const hasRouteIdentitySearchParams =
Platform.OS === "web" &&
typeof window !== "undefined" &&
(() => {
const params = new URLSearchParams(window.location.search);
return (
params.has("tabId") ||
params.has("workspaceId") ||
params.has("serverId")
);
})();
const derivedCanonicalPath = resolveWorkspaceTabCanonicalPath({
activeTabId,
pathname,
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
});
const canonicalPath =
derivedCanonicalPath ??
(hasRouteIdentitySearchParams && activeTabId
? buildHostWorkspaceTabRoute(
normalizedServerId,
normalizedWorkspaceId,
activeTabId
)
: null);
if (!canonicalPath) {
lastCanonicalPathRef.current = null;
return;
}
if (lastCanonicalPathRef.current === canonicalPath) {
return;
}
lastCanonicalPathRef.current = canonicalPath;
if (Platform.OS === "web" && typeof window !== "undefined") {
if (
window.location.pathname !== canonicalPath ||
window.location.search.length > 0 ||
window.location.hash.length > 0
) {
window.history.replaceState(window.history.state, "", canonicalPath);
}
return;
}
const href = activeTabId
? buildWorkspaceTabHref(normalizedServerId, normalizedWorkspaceId, activeTabId)
: null;
if (href) {
router.replace(href as any);
return;
}
router.replace(canonicalPath as any);
}, [
activeTabId,
normalizedServerId,
normalizedWorkspaceId,
pathname,
router,
]);
const activeTab = tabModel.activeTab;
const tabs = useMemo<WorkspaceTabDescriptor[]>(
@@ -607,23 +519,13 @@ function WorkspaceScreenContent({
if (!tabId || !normalizedServerId || !normalizedWorkspaceId) {
return;
}
setPendingRouteTabId(tabId);
focusTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
tabId,
});
const href = buildWorkspaceTabHref(
normalizedServerId,
normalizedWorkspaceId,
tabId
);
if (!href) {
return;
}
router.replace(href as any);
},
[focusTab, normalizedServerId, normalizedWorkspaceId, router]
[focusTab, normalizedServerId, normalizedWorkspaceId]
);
const emptyWorkspaceSeedRef = useRef<string | null>(null);
@@ -631,9 +533,6 @@ function WorkspaceScreenContent({
if (!persistenceKey) {
return;
}
if (effectiveRouteTabId) {
return;
}
if (tabs.length > 0) {
emptyWorkspaceSeedRef.current = null;
return;
@@ -658,7 +557,6 @@ function WorkspaceScreenContent({
normalizedWorkspaceId,
openDraftTab,
persistenceKey,
effectiveRouteTabId,
tabs.length,
]);
@@ -114,7 +114,7 @@ describe("deriveWorkspaceTabModel", () => {
]);
});
it("prefers route tab when present, then focused tab, then first tab for active selection", () => {
it("uses focused tab when present, otherwise falls back to first tab", () => {
const base = {
workspaceAgents: [makeAgent({ id: "agent-a" }), makeAgent({ id: "agent-b" })],
terminals: [],
@@ -133,42 +133,10 @@ describe("deriveWorkspaceTabModel", () => {
deriveWorkspaceTabModel({
...base,
focusedTabId: "agent_agent-b",
routeTabId: "agent_agent-a",
}).activeTabId
).toBe("agent_agent-a");
expect(
deriveWorkspaceTabModel({
...base,
focusedTabId: "agent_agent-b",
routeTabId: "agent_agent-missing",
}).activeTabId
).toBe("agent_agent-b");
});
it("switches active tab target when route tab id changes", () => {
const modelA = deriveWorkspaceTabModel({
workspaceAgents: [makeAgent({ id: "agent-a" }), makeAgent({ id: "agent-b" })],
terminals: [{ id: "term-1", name: "shell" }],
uiTabs: [],
tabOrder: ["agent_agent-a", "terminal_term-1", "agent_agent-b"],
routeTabId: "terminal_term-1",
});
const modelB = deriveWorkspaceTabModel({
workspaceAgents: [makeAgent({ id: "agent-a" }), makeAgent({ id: "agent-b" })],
terminals: [{ id: "term-1", name: "shell" }],
uiTabs: [],
tabOrder: ["agent_agent-a", "terminal_term-1", "agent_agent-b"],
routeTabId: "agent_agent-b",
});
expect(modelA.activeTab?.descriptor.tabId).toBe("terminal_term-1");
expect(modelA.activeTab?.target.kind).toBe("terminal");
expect(modelB.activeTab?.descriptor.tabId).toBe("agent_agent-b");
expect(modelB.activeTab?.target.kind).toBe("agent");
});
it("re-resolves active content for a new workspace when prior focused tab is not available", () => {
const model = deriveWorkspaceTabModel({
workspaceAgents: [makeAgent({ id: "workspace-b-agent", title: "B" })],
@@ -176,7 +144,6 @@ describe("deriveWorkspaceTabModel", () => {
uiTabs: [],
tabOrder: ["agent_workspace-b-agent"],
focusedTabId: "agent_workspace-a-agent",
routeTabId: null,
});
expect(model.activeTabId).toBe("agent_workspace-b-agent");
@@ -108,7 +108,6 @@ export function deriveWorkspaceTabModel(input: {
uiTabs: WorkspaceTab[];
tabOrder: string[];
focusedTabId?: string | null;
routeTabId?: string | null;
}): WorkspaceTabModel {
const tabsById = new Map<string, WorkspaceDerivedTab>();
@@ -209,15 +208,12 @@ export function deriveWorkspaceTabModel(input: {
.filter((tab): tab is WorkspaceDerivedTab => tab !== null);
const openTabIds = new Set(tabs.map((tab) => tab.descriptor.tabId));
const routeTabId = trimNonEmpty(input.routeTabId);
const focusedTabId = trimNonEmpty(input.focusedTabId);
const activeTabId =
routeTabId && openTabIds.has(routeTabId)
? routeTabId
: focusedTabId && openTabIds.has(focusedTabId)
? focusedTabId
: tabs[0]?.descriptor.tabId ?? null;
focusedTabId && openTabIds.has(focusedTabId)
? focusedTabId
: tabs[0]?.descriptor.tabId ?? null;
const activeTab = activeTabId
? tabs.find((tab) => tab.descriptor.tabId === activeTabId) ?? null
+48 -41
View File
@@ -1,18 +1,21 @@
import { describe, expect, it } from "vitest";
import {
buildHostAgentDetailRoute,
buildHostNewAgentRoute,
buildHostRootRoute,
buildHostWorkspaceAgentTabRoute,
buildHostWorkspaceFileTabRoute,
buildHostWorkspaceAgentRoute,
buildHostWorkspaceFileRoute,
buildHostWorkspaceRoute,
buildHostWorkspaceTerminalTabRoute,
buildHostWorkspaceRouteWithOpenIntent,
buildHostWorkspaceTerminalRoute,
decodeFilePathFromPathSegment,
decodeWorkspaceIdFromPathSegment,
encodeFilePathForPathSegment,
encodeWorkspaceIdForPathSegment,
parseHostAgentRouteFromPathname,
parseHostWorkspaceTabRouteFromPathname,
parseHostWorkspaceOpenIntentFromPathname,
parseHostWorkspaceRouteFromPathname,
parseWorkspaceOpenIntent,
} from "./host-routes";
describe("parseHostAgentRouteFromPathname", () => {
@@ -49,35 +52,10 @@ describe("workspace route parsing", () => {
});
});
it("parses workspace route for /tab targets", () => {
it("does not treat /tab routes as valid workspace routes", () => {
expect(
parseHostWorkspaceRouteFromPathname("/h/local/workspace/L3RtcC9yZXBv/tab/draft_abc123")
).toEqual({
serverId: "local",
workspaceId: "/tmp/repo",
});
});
it("parses workspace tab route", () => {
expect(
parseHostWorkspaceTabRouteFromPathname("/h/local/workspace/L3RtcC9yZXBv/tab/draft_abc123")
).toEqual({
serverId: "local",
workspaceId: "/tmp/repo",
tabId: "draft_abc123",
});
});
it("prefers path tab segment and ignores query params for workspace tab routes", () => {
expect(
parseHostWorkspaceTabRouteFromPathname(
"/h/local/workspace/L3RtcC9yZXBv/tab/draft_from_path?tabId=draft_from_query&workspaceId=wrong"
)
).toEqual({
serverId: "local",
workspaceId: "/tmp/repo",
tabId: "draft_from_path",
});
).toBeNull();
});
it("builds base64url workspace routes", () => {
@@ -92,21 +70,50 @@ describe("workspace route parsing", () => {
expect(buildHostNewAgentRoute("local")).toBe("/h/local/new-agent");
});
it("builds workspace agent tab routes", () => {
expect(buildHostWorkspaceAgentTabRoute("local", "/tmp/repo", "agent-1")).toBe(
"/h/local/workspace/L3RtcC9yZXBv/tab/agent_agent-1"
it("builds workspace routes with open intent query", () => {
expect(buildHostWorkspaceAgentRoute("local", "/tmp/repo", "agent-1")).toBe(
"/h/local/workspace/L3RtcC9yZXBv?open=agent%3Aagent-1"
);
expect(buildHostWorkspaceTerminalRoute("local", "/tmp/repo", "term-1")).toBe(
"/h/local/workspace/L3RtcC9yZXBv?open=terminal%3Aterm-1"
);
expect(buildHostWorkspaceFileRoute("local", "/tmp/repo", "src/index.ts")).toBe(
"/h/local/workspace/L3RtcC9yZXBv?open=file%3Ac3JjL2luZGV4LnRz"
);
expect(
buildHostWorkspaceRouteWithOpenIntent("local", "/tmp/repo", {
kind: "draft",
draftId: "new",
})
).toBe("/h/local/workspace/L3RtcC9yZXBv?open=draft%3Anew");
});
it("builds workspace terminal tab routes", () => {
expect(buildHostWorkspaceTerminalTabRoute("local", "/tmp/repo", "term-1")).toBe(
"/h/local/workspace/L3RtcC9yZXBv/tab/terminal_term-1"
);
it("parses workspace open intent from pathname query", () => {
expect(
parseHostWorkspaceOpenIntentFromPathname(
"/h/local/workspace/L3RtcC9yZXBv?open=agent%3Aagent-1"
)
).toEqual({
kind: "agent",
agentId: "agent-1",
});
expect(parseWorkspaceOpenIntent("terminal:term-1")).toEqual({
kind: "terminal",
terminalId: "term-1",
});
expect(parseWorkspaceOpenIntent("draft:new")).toEqual({
kind: "draft",
draftId: "new",
});
expect(parseWorkspaceOpenIntent("file:c3JjL2luZGV4LnRz")).toEqual({
kind: "file",
path: "src/index.ts",
});
});
it("builds workspace file tab routes", () => {
expect(buildHostWorkspaceFileTabRoute("local", "/tmp/repo", "src/index.ts")).toBe(
"/h/local/workspace/L3RtcC9yZXBv/tab/file_src%2Findex.ts"
it("keeps agent detail workspace routing on workspace path with open intent", () => {
expect(buildHostAgentDetailRoute("local", "agent-1", "/tmp/repo")).toBe(
"/h/local/workspace/L3RtcC9yZXBv?open=agent%3Aagent-1"
);
});
});
+134 -115
View File
@@ -11,6 +11,17 @@ function stripSearchAndHash(pathname: string): string {
return pathname.slice(0, end);
}
function extractSearch(pathname: string): string {
const queryIndex = pathname.indexOf("?");
if (queryIndex < 0) {
return "";
}
const hashIndex = pathname.indexOf("#", queryIndex);
return hashIndex >= 0
? pathname.slice(queryIndex + 1, hashIndex)
: pathname.slice(queryIndex + 1);
}
function trimNonEmpty(value: NullableString): string | null {
if (typeof value !== "string") {
return null;
@@ -88,19 +99,82 @@ 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;
export type WorkspaceOpenIntent =
| { kind: "agent"; agentId: string }
| { kind: "terminal"; terminalId: string }
| { kind: "file"; path: string }
| { kind: "draft"; draftId: string };
export function parseWorkspaceOpenIntent(
value: string | null | undefined
): WorkspaceOpenIntent | null {
const normalized = trimNonEmpty(value);
if (!normalized) {
return null;
}
const separator = normalized.indexOf(":");
if (separator <= 0 || separator >= normalized.length - 1) {
return null;
}
const kind = normalized.slice(0, separator);
const payload = trimNonEmpty(normalized.slice(separator + 1));
if (!payload) {
return null;
}
if (kind === "agent") {
return { kind: "agent", agentId: payload };
}
if (kind === "terminal") {
return { kind: "terminal", terminalId: payload };
}
if (kind === "draft") {
return { kind: "draft", draftId: payload };
}
if (kind === "file") {
const decodedPath = decodeFilePathFromPathSegment(payload);
if (!decodedPath) {
return null;
}
return { kind: "file", path: decodedPath };
}
return null;
}
function buildWorkspaceTerminalTabId(terminalId: string): string | null {
const normalized = trimNonEmpty(terminalId);
return normalized ? `terminal_${normalized}` : null;
export function buildWorkspaceOpenIntentParam(
intent: WorkspaceOpenIntent
): string | null {
if (intent.kind === "agent") {
const agentId = trimNonEmpty(intent.agentId);
return agentId ? `agent:${agentId}` : null;
}
if (intent.kind === "terminal") {
const terminalId = trimNonEmpty(intent.terminalId);
return terminalId ? `terminal:${terminalId}` : null;
}
if (intent.kind === "draft") {
const draftId = trimNonEmpty(intent.draftId);
return draftId ? `draft:${draftId}` : null;
}
const normalizedPath = trimNonEmpty(intent.path);
if (!normalizedPath) {
return null;
}
const encodedPath = encodeFilePathForPathSegment(normalizedPath);
return encodedPath ? `file:${encodedPath}` : null;
}
function buildWorkspaceFileTabId(filePath: string): string | null {
const normalized = trimNonEmpty(filePath);
return normalized ? `file_${normalized.replace(/\\/g, "/")}` : null;
export function parseHostWorkspaceOpenIntentFromPathname(
pathname: string
): WorkspaceOpenIntent | null {
const search = extractSearch(pathname);
if (!search) {
return null;
}
return parseWorkspaceOpenIntent(new URLSearchParams(search).get("open"));
}
export function encodeWorkspaceIdForPathSegment(workspaceId: string): string {
@@ -203,35 +277,17 @@ export function parseHostWorkspaceRouteFromPathname(
pathname: string
): { serverId: string; workspaceId: string } | null {
const pathOnly = stripSearchAndHash(pathname);
const prefix = "/h/";
if (!pathOnly.startsWith(prefix)) {
const match = pathOnly.match(/^\/h\/([^/]+)\/workspace\/([^/]+)\/?$/);
if (!match) {
return null;
}
const serverIdStart = prefix.length;
const serverIdEnd = pathOnly.indexOf("/", serverIdStart);
if (serverIdEnd < 0) {
return null;
}
const rawServerId = pathOnly.slice(serverIdStart, serverIdEnd);
const serverId = trimNonEmpty(decodeSegment(rawServerId));
const serverId = trimNonEmpty(decodeSegment(match[1]));
if (!serverId) {
return null;
}
const workspacePrefix = "/workspace/";
if (!pathOnly.startsWith(workspacePrefix, serverIdEnd)) {
return null;
}
const workspaceIdStart = serverIdEnd + workspacePrefix.length;
let workspaceIdEnd = pathOnly.length;
const tabIdx = pathOnly.lastIndexOf("/tab/");
if (tabIdx >= 0 && tabIdx > workspaceIdStart) {
workspaceIdEnd = tabIdx;
}
const rawWorkspaceId = pathOnly.slice(workspaceIdStart, workspaceIdEnd).replace(/\/+$/, "");
const rawWorkspaceId = match[2];
const workspaceId = decodeWorkspaceIdFromPathSegment(rawWorkspaceId);
if (!workspaceId) {
return null;
@@ -239,56 +295,6 @@ export function parseHostWorkspaceRouteFromPathname(
return { serverId, workspaceId };
}
export function parseHostWorkspaceTabRouteFromPathname(
pathname: string
): { serverId: string; workspaceId: string; tabId: string } | null {
const pathOnly = stripSearchAndHash(pathname);
const prefix = "/h/";
if (!pathOnly.startsWith(prefix)) {
return null;
}
const serverIdStart = prefix.length;
const serverIdEnd = pathOnly.indexOf("/", serverIdStart);
if (serverIdEnd < 0) {
return null;
}
const rawServerId = pathOnly.slice(serverIdStart, serverIdEnd);
const serverId = trimNonEmpty(decodeSegment(rawServerId));
if (!serverId) {
return null;
}
const workspacePrefix = "/workspace/";
if (!pathOnly.startsWith(workspacePrefix, serverIdEnd)) {
return null;
}
const workspaceIdStart = serverIdEnd + workspacePrefix.length;
const tabMarker = "/tab/";
const tabIdx = pathOnly.lastIndexOf(tabMarker);
if (tabIdx < 0 || tabIdx <= workspaceIdStart) {
return null;
}
const rawWorkspaceId = pathOnly.slice(workspaceIdStart, tabIdx).replace(/\/+$/, "");
const workspaceId = decodeWorkspaceIdFromPathSegment(rawWorkspaceId);
if (!workspaceId) {
return null;
}
const tabIdStart = tabIdx + tabMarker.length;
const tabIdEnd = pathOnly.indexOf("/", tabIdStart);
const rawTabId =
tabIdEnd < 0 ? pathOnly.slice(tabIdStart) : pathOnly.slice(tabIdStart, tabIdEnd);
const tabId = trimNonEmpty(decodeSegment(rawTabId));
if (!tabId) {
return null;
}
return { serverId, workspaceId, tabId };
}
export function buildHostWorkspaceRoute(
serverId: string,
workspaceId: string
@@ -305,53 +311,53 @@ export function buildHostWorkspaceRoute(
return `/h/${encodeSegment(normalizedServerId)}/workspace/${encodeSegment(encodedWorkspaceId)}`;
}
export function buildHostWorkspaceAgentTabRoute(
export function buildHostWorkspaceRouteWithOpenIntent(
serverId: string,
workspaceId: string,
intent: WorkspaceOpenIntent
): string {
const base = buildHostWorkspaceRoute(serverId, workspaceId);
if (base === "/") {
return "/";
}
const open = buildWorkspaceOpenIntentParam(intent);
if (!open) {
return "/";
}
return `${base}?open=${encodeURIComponent(open)}`;
}
export function buildHostWorkspaceAgentRoute(
serverId: string,
workspaceId: string,
agentId: string
): string {
const tabId = buildWorkspaceAgentTabId(agentId);
if (!tabId) {
return "/";
}
return buildHostWorkspaceTabRoute(serverId, workspaceId, tabId);
return buildHostWorkspaceRouteWithOpenIntent(serverId, workspaceId, {
kind: "agent",
agentId,
});
}
export function buildHostWorkspaceTerminalTabRoute(
export function buildHostWorkspaceTerminalRoute(
serverId: string,
workspaceId: string,
terminalId: string
): string {
const tabId = buildWorkspaceTerminalTabId(terminalId);
if (!tabId) {
return "/";
}
return buildHostWorkspaceTabRoute(serverId, workspaceId, tabId);
return buildHostWorkspaceRouteWithOpenIntent(serverId, workspaceId, {
kind: "terminal",
terminalId,
});
}
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 buildHostWorkspaceFileTabRoute(
export function buildHostWorkspaceFileRoute(
serverId: string,
workspaceId: string,
filePath: string
): string {
const tabId = buildWorkspaceFileTabId(filePath);
if (!tabId) {
return "/";
}
return buildHostWorkspaceTabRoute(serverId, workspaceId, tabId);
return buildHostWorkspaceRouteWithOpenIntent(serverId, workspaceId, {
kind: "file",
path: filePath,
});
}
export function buildHostAgentDetailRoute(
@@ -361,7 +367,7 @@ export function buildHostAgentDetailRoute(
): string {
const normalizedWorkspaceId = trimNonEmpty(workspaceId);
if (normalizedWorkspaceId) {
return buildHostWorkspaceAgentTabRoute(
return buildHostWorkspaceAgentRoute(
serverId,
normalizedWorkspaceId,
agentId
@@ -429,8 +435,21 @@ export function mapPathnameToServer(
if (suffix.startsWith("new-agent")) {
return `${base}/new-agent`;
}
if (suffix.startsWith("workspace/")) {
return `${base}/${suffix}`;
const workspaceRoute = parseHostWorkspaceRouteFromPathname(pathname);
if (workspaceRoute) {
const workspacePath = buildHostWorkspaceRoute(
normalized,
workspaceRoute.workspaceId
);
const openIntent = parseHostWorkspaceOpenIntentFromPathname(pathname);
if (!openIntent) {
return workspacePath;
}
return buildHostWorkspaceRouteWithOpenIntent(
normalized,
workspaceRoute.workspaceId,
openIntent
);
}
if (suffix.startsWith("agent/")) {
return `${base}/${suffix}`;
@@ -79,7 +79,7 @@ describe("resolveSelectedAgentForNewAgent", () => {
it("prefers the agent in the current route", () => {
expect(
resolveSelectedAgentForNewAgent({
pathname: "/h/srv-1/workspace/%2Frepo/tab/agent_agent-2",
pathname: "/h/srv-1/workspace/L3JlcG8?open=agent%3Aagent-2",
selectedAgentId: "srv-9:agent-9",
})
).toEqual({
+7 -5
View File
@@ -1,8 +1,9 @@
import type { CheckoutStatusPayload } from "@/hooks/use-checkout-status-query";
import {
parseHostWorkspaceOpenIntentFromPathname,
buildHostWorkspaceRoute,
parseHostAgentRouteFromPathname,
parseHostWorkspaceTabRouteFromPathname,
parseHostWorkspaceRouteFromPathname,
} from "@/utils/host-routes";
export function parseAgentKey(
@@ -27,11 +28,12 @@ export function resolveSelectedAgentForNewAgent(input: {
pathname: string;
selectedAgentId?: string;
}): { serverId: string; agentId: string } | null {
const workspaceTabRoute = parseHostWorkspaceTabRouteFromPathname(input.pathname);
if (workspaceTabRoute?.tabId?.startsWith("agent_")) {
const agentId = workspaceTabRoute.tabId.slice("agent_".length).trim();
const workspaceRoute = parseHostWorkspaceRouteFromPathname(input.pathname);
const openIntent = parseHostWorkspaceOpenIntentFromPathname(input.pathname);
if (workspaceRoute && openIntent?.kind === "agent") {
const agentId = openIntent.agentId.trim();
if (agentId) {
return { serverId: workspaceTabRoute.serverId, agentId };
return { serverId: workspaceRoute.serverId, agentId };
}
}
return (
@@ -34,14 +34,14 @@ describe("resolveNotificationTarget", () => {
});
describe("buildNotificationRoute", () => {
it("routes directly to workspace tab when workspace id is present", () => {
it("routes to workspace with one-shot open intent when workspace id is present", () => {
expect(
buildNotificationRoute({
serverId: "srv-1",
agentId: "agent-1",
workspaceId: "/tmp/repo",
})
).toBe("/h/srv-1/workspace/L3RtcC9yZXBv/tab/agent_agent-1");
).toBe("/h/srv-1/workspace/L3RtcC9yZXBv?open=agent%3Aagent-1");
});
it("routes directly to server-scoped agent path when both ids are present", () => {
@@ -1,7 +1,7 @@
import {
buildHostAgentDetailRoute,
buildHostRootRoute,
buildHostWorkspaceAgentTabRoute,
buildHostWorkspaceAgentRoute,
} from "@/utils/host-routes";
type NotificationData = Record<string, unknown> | null | undefined;
@@ -36,7 +36,7 @@ export function buildNotificationRoute(data: NotificationData): string {
const { serverId, agentId, workspaceId } = resolveNotificationTarget(data);
if (serverId && agentId) {
if (workspaceId) {
return buildHostWorkspaceAgentTabRoute(serverId, workspaceId, agentId);
return buildHostWorkspaceAgentRoute(serverId, workspaceId, agentId);
}
return buildHostAgentDetailRoute(serverId, agentId);
}