diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx
index 294943631..a3c0961bc 100644
--- a/packages/app/src/app/_layout.tsx
+++ b/packages/app/src/app/_layout.tsx
@@ -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);
diff --git a/packages/app/src/app/h/[serverId]/agent/[agentId].tsx b/packages/app/src/app/h/[serverId]/agent/[agentId].tsx
index 7413bae15..4b7fb7401 100644
--- a/packages/app/src/app/h/[serverId]/agent/[agentId].tsx
+++ b/packages/app/src/app/h/[serverId]/agent/[agentId].tsx
@@ -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);
diff --git a/packages/app/src/app/h/[serverId]/index.tsx b/packages/app/src/app/h/[serverId]/index.tsx
index 72f64412b..10dd25192 100644
--- a/packages/app/src/app/h/[serverId]/index.tsx
+++ b/packages/app/src/app/h/[serverId]/index.tsx
@@ -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
diff --git a/packages/app/src/app/h/[serverId]/workspace/[workspaceId]/_layout.tsx b/packages/app/src/app/h/[serverId]/workspace/[workspaceId]/_layout.tsx
index b21bad146..c8adcbf3b 100644
--- a/packages/app/src/app/h/[serverId]/workspace/[workspaceId]/_layout.tsx
+++ b/packages/app/src/app/h/[serverId]/workspace/[workspaceId]/_layout.tsx
@@ -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 (
);
}
diff --git a/packages/app/src/app/h/[serverId]/workspace/[workspaceId]/tab/[tabId].tsx b/packages/app/src/app/h/[serverId]/workspace/[workspaceId]/tab/[tabId].tsx
deleted file mode 100644
index 85de1f2e1..000000000
--- a/packages/app/src/app/h/[serverId]/workspace/[workspaceId]/tab/[tabId].tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function HostWorkspaceTabRoute() {
- return null;
-}
diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx
index a8e779d75..ce8219b91 100644
--- a/packages/app/src/components/agent-list.tsx
+++ b/packages/app/src/components/agent-list.tsx
@@ -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
diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx
index 58bc9b34f..df7908374 100644
--- a/packages/app/src/components/agent-stream-view.tsx
+++ b/packages/app/src/components/agent-stream-view.tsx
@@ -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 {
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);
diff --git a/packages/app/src/keyboard/keyboard-shortcut-routing.ts b/packages/app/src/keyboard/keyboard-shortcut-routing.ts
index 7cf2ad691..f18943fe2 100644
--- a/packages/app/src/keyboard/keyboard-shortcut-routing.ts
+++ b/packages/app/src/keyboard/keyboard-shortcut-routing.ts
@@ -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;
}
}
diff --git a/packages/app/src/screens/agent/draft-agent-screen.tsx b/packages/app/src/screens/agent/draft-agent-screen.tsx
index b59e43fe6..2fd9c64a4 100644
--- a/packages/app/src/screens/agent/draft-agent-screen.tsx
+++ b/packages/app/src/screens/agent/draft-agent-screen.tsx
@@ -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
diff --git a/packages/app/src/screens/workspace/workspace-route-canonicalization.test.ts b/packages/app/src/screens/workspace/workspace-route-canonicalization.test.ts
deleted file mode 100644
index 1d5b0f237..000000000
--- a/packages/app/src/screens/workspace/workspace-route-canonicalization.test.ts
+++ /dev/null
@@ -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/ 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();
- });
-});
diff --git a/packages/app/src/screens/workspace/workspace-route-canonicalization.ts b/packages/app/src/screens/workspace/workspace-route-canonicalization.ts
deleted file mode 100644
index 9d12975d9..000000000
--- a/packages/app/src/screens/workspace/workspace-route-canonicalization.ts
+++ /dev/null
@@ -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;
-}
diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx
index 93aa782b1..b1aa8c5e9 100644
--- a/packages/app/src/screens/workspace/workspace-screen.tsx
+++ b/packages/app/src/screens/workspace/workspace-screen.tsx
@@ -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 (
);
@@ -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(null);
+ const consumedOpenIntentsRef = useRef(new Set());
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(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(
@@ -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(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,
]);
diff --git a/packages/app/src/screens/workspace/workspace-tab-model.test.ts b/packages/app/src/screens/workspace/workspace-tab-model.test.ts
index 01d2abb69..01224ee8c 100644
--- a/packages/app/src/screens/workspace/workspace-tab-model.test.ts
+++ b/packages/app/src/screens/workspace/workspace-tab-model.test.ts
@@ -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");
diff --git a/packages/app/src/screens/workspace/workspace-tab-model.ts b/packages/app/src/screens/workspace/workspace-tab-model.ts
index 6e29ff861..c3f9a7edf 100644
--- a/packages/app/src/screens/workspace/workspace-tab-model.ts
+++ b/packages/app/src/screens/workspace/workspace-tab-model.ts
@@ -108,7 +108,6 @@ export function deriveWorkspaceTabModel(input: {
uiTabs: WorkspaceTab[];
tabOrder: string[];
focusedTabId?: string | null;
- routeTabId?: string | null;
}): WorkspaceTabModel {
const tabsById = new Map();
@@ -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
diff --git a/packages/app/src/utils/host-routes.test.ts b/packages/app/src/utils/host-routes.test.ts
index 6e75b1527..fad43fd36 100644
--- a/packages/app/src/utils/host-routes.test.ts
+++ b/packages/app/src/utils/host-routes.test.ts
@@ -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"
);
});
});
diff --git a/packages/app/src/utils/host-routes.ts b/packages/app/src/utils/host-routes.ts
index 2d91e0be3..40d09d296 100644
--- a/packages/app/src/utils/host-routes.ts
+++ b/packages/app/src/utils/host-routes.ts
@@ -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}`;
diff --git a/packages/app/src/utils/new-agent-routing.test.ts b/packages/app/src/utils/new-agent-routing.test.ts
index 90e90520c..2372b63e8 100644
--- a/packages/app/src/utils/new-agent-routing.test.ts
+++ b/packages/app/src/utils/new-agent-routing.test.ts
@@ -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({
diff --git a/packages/app/src/utils/new-agent-routing.ts b/packages/app/src/utils/new-agent-routing.ts
index 6383a068b..931daf001 100644
--- a/packages/app/src/utils/new-agent-routing.ts
+++ b/packages/app/src/utils/new-agent-routing.ts
@@ -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 (
diff --git a/packages/app/src/utils/notification-routing.test.ts b/packages/app/src/utils/notification-routing.test.ts
index ee85ab628..033bee811 100644
--- a/packages/app/src/utils/notification-routing.test.ts
+++ b/packages/app/src/utils/notification-routing.test.ts
@@ -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", () => {
diff --git a/packages/app/src/utils/notification-routing.ts b/packages/app/src/utils/notification-routing.ts
index 11befbfb9..f8dd1244b 100644
--- a/packages/app/src/utils/notification-routing.ts
+++ b/packages/app/src/utils/notification-routing.ts
@@ -1,7 +1,7 @@
import {
buildHostAgentDetailRoute,
buildHostRootRoute,
- buildHostWorkspaceAgentTabRoute,
+ buildHostWorkspaceAgentRoute,
} from "@/utils/host-routes";
type NotificationData = Record | 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);
}