refactor: move workspace tab navigation logic to prepareWorkspaceTab

This commit is contained in:
Mohamed Boudra
2026-03-21 15:21:02 +07:00
parent 78fc1fe862
commit 48c3308c31
17 changed files with 184 additions and 390 deletions

View File

@@ -2,10 +2,7 @@ import { expect, type Page } from "@playwright/test";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { randomUUID } from "node:crypto";
import {
buildHostWorkspaceAgentRoute,
buildHostWorkspaceRoute,
} from "../../src/utils/host-routes";
import { buildHostWorkspaceRoute } from "../../src/utils/host-routes";
const NEAR_BOTTOM_THRESHOLD_PX = 72;
@@ -162,7 +159,7 @@ export async function seedBottomAnchorAgent(input: {
id: created.id,
title,
expectedTailText,
url: buildHostWorkspaceAgentRoute(getServerId(), input.cwd, created.id),
url: `${buildHostWorkspaceRoute(getServerId(), input.cwd)}?open=${encodeURIComponent(`agent:${created.id}`)}`,
workspaceUrl: buildHostWorkspaceRoute(getServerId(), input.cwd),
};
}

View File

@@ -4,8 +4,8 @@ import { useSessionStore } from "@/stores/session-store";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import {
buildHostRootRoute,
buildHostWorkspaceAgentRoute,
} from "@/utils/host-routes";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
export default function HostAgentReadyRoute() {
const router = useRouter();
@@ -39,7 +39,11 @@ export default function HostAgentReadyRoute() {
if (normalizedCwd) {
redirectedRef.current = true;
router.replace(
buildHostWorkspaceAgentRoute(serverId, normalizedCwd, agentId) as any
prepareWorkspaceTab({
serverId,
workspaceId: normalizedCwd,
target: { kind: "agent", agentId },
}) as any
);
}
}, [agentCwd, agentId, router, serverId]);
@@ -78,7 +82,13 @@ export default function HostAgentReadyRoute() {
const cwd = result?.agent?.cwd?.trim();
redirectedRef.current = true;
if (cwd) {
router.replace(buildHostWorkspaceAgentRoute(serverId, cwd, agentId) as any);
router.replace(
prepareWorkspaceTab({
serverId,
workspaceId: cwd,
target: { kind: "agent", agentId },
}) as any
);
return;
}
router.replace(buildHostRootRoute(serverId) as any);

View File

@@ -5,9 +5,9 @@ import { useFormPreferences } from "@/hooks/use-form-preferences";
import {
buildHostOpenProjectRoute,
buildHostRootRoute,
buildHostWorkspaceAgentRoute,
buildHostWorkspaceRoute,
} from "@/utils/host-routes";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
const HOST_ROOT_REDIRECT_DELAY_MS = 300;
@@ -59,11 +59,11 @@ export default function HostIndexRoute() {
const primaryAgent = visibleAgents[0];
if (primaryAgent?.cwd?.trim()) {
router.replace(
buildHostWorkspaceAgentRoute(
prepareWorkspaceTab({
serverId,
primaryAgent.cwd.trim(),
primaryAgent.id
) as any
workspaceId: primaryAgent.cwd.trim(),
target: { kind: "agent", agentId: primaryAgent.id },
}) as any
);
return;
}

View File

@@ -1,11 +1,42 @@
import { useGlobalSearchParams, useLocalSearchParams } from 'expo-router'
import { useEffect, useRef } from 'react'
import { useGlobalSearchParams, useLocalSearchParams, useRouter } from 'expo-router'
import type { WorkspaceTabTarget } from '@/stores/workspace-tabs-store'
import { WorkspaceScreen } from '@/screens/workspace/workspace-screen'
import {
buildHostWorkspaceRoute,
decodeWorkspaceIdFromPathSegment,
parseWorkspaceOpenIntent,
type WorkspaceOpenIntent,
} from '@/utils/host-routes'
import { prepareWorkspaceTab } from '@/utils/workspace-navigation'
function getParamValue(value: string | string[] | undefined): string {
if (typeof value === 'string') {
return value.trim()
}
if (Array.isArray(value)) {
const firstValue = value[0]
return typeof firstValue === 'string' ? firstValue.trim() : ''
}
return ''
}
function getOpenIntentTarget(openIntent: WorkspaceOpenIntent): WorkspaceTabTarget {
if (openIntent.kind === 'agent') {
return { kind: 'agent', agentId: openIntent.agentId }
}
if (openIntent.kind === 'terminal') {
return { kind: 'terminal', terminalId: openIntent.terminalId }
}
if (openIntent.kind === 'file') {
return { kind: 'file', path: openIntent.path }
}
return { kind: 'draft', draftId: openIntent.draftId }
}
export default function HostWorkspaceLayout() {
const router = useRouter()
const consumedIntentRef = useRef<string | null>(null)
const params = useLocalSearchParams<{
serverId?: string | string[]
workspaceId?: string | string[]
@@ -13,21 +44,44 @@ export default function HostWorkspaceLayout() {
const globalParams = useGlobalSearchParams<{
open?: string | string[]
}>()
const serverValue = Array.isArray(params.serverId) ? params.serverId[0] : params.serverId
const workspaceValue = Array.isArray(params.workspaceId)
? params.workspaceId[0]
: params.workspaceId
const serverId = serverValue?.trim() ?? ''
const serverId = getParamValue(params.serverId)
const workspaceValue = getParamValue(params.workspaceId)
const workspaceId = workspaceValue ? (decodeWorkspaceIdFromPathSegment(workspaceValue) ?? '') : ''
const openValue = Array.isArray(globalParams.open) ? globalParams.open[0] : globalParams.open
const openIntent = parseWorkspaceOpenIntent(openValue)
const openValue = getParamValue(globalParams.open)
useEffect(() => {
if (!openValue) {
return
}
const consumptionKey = `${serverId}:${workspaceId}:${openValue}`
if (consumedIntentRef.current === consumptionKey) {
return
}
consumedIntentRef.current = consumptionKey
const openIntent = parseWorkspaceOpenIntent(openValue)
const route = openIntent
? prepareWorkspaceTab({
serverId,
workspaceId,
target: getOpenIntentTarget(openIntent),
pin: openIntent.kind === 'agent',
})
: buildHostWorkspaceRoute(serverId, workspaceId)
router.replace(route as any)
}, [openValue, router, serverId, workspaceId])
if (openValue) {
return null
}
return (
<WorkspaceScreen
key={`${serverId}:${workspaceId}`}
serverId={serverId}
workspaceId={workspaceId}
openIntent={openIntent}
/>
)
}

View File

@@ -9,14 +9,14 @@ import {
} from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useCallback, useMemo, useState, type ReactElement } from 'react'
import { router, type Href } from 'expo-router'
import { router } from 'expo-router'
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
import { formatTimeAgo } from '@/utils/time'
import { shortenPath } from '@/utils/shorten-path'
import { type AggregatedAgent } from '@/hooks/use-aggregated-agents'
import { useSessionStore } from '@/stores/session-store'
import { AgentStatusDot } from '@/components/agent-status-dot'
import { buildHostWorkspaceAgentRoute } from '@/utils/host-routes'
import { prepareWorkspaceTab } from '@/utils/workspace-navigation'
interface AgentListProps {
agents: AggregatedAgent[]
@@ -272,8 +272,12 @@ export function AgentList({
onAgentSelect?.()
const route: Href = buildHostWorkspaceAgentRoute(serverId, agent.cwd, agentId) as Href
router.navigate(route)
const route = prepareWorkspaceTab({
serverId,
workspaceId: agent.cwd,
target: { kind: 'agent', agentId },
})
router.navigate(route as any)
},
[isActionSheetVisible, onAgentSelect]
)

View File

@@ -67,8 +67,8 @@ import {
import { createMarkdownStyles } from "@/styles/markdown-styles";
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
import { getMarkdownListMarker } from "@/utils/markdown-list";
import { buildHostWorkspaceFileRoute } from "@/utils/host-routes";
import { normalizeInlinePathTarget } from "@/utils/inline-path";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
import {
getWorkingIndicatorDotStrength,
WORKING_INDICATOR_CYCLE_MS,
@@ -171,12 +171,12 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
return;
}
const route = buildHostWorkspaceFileRoute(
resolvedServerId,
const route = prepareWorkspaceTab({
serverId: resolvedServerId,
workspaceId,
normalized.file
);
router.replace(route as any);
target: { kind: "file", path: normalized.file },
});
router.navigate(route as any);
return;
}

View File

@@ -33,9 +33,9 @@ import { getHostRuntimeStore, isHostRuntimeConnected } from '@/runtime/host-runt
import { getIsDesktop } from '@/constants/layout'
import { projectIconQueryKey } from '@/hooks/use-project-icon-query'
import {
buildHostWorkspaceRouteWithOpenIntent,
parseHostWorkspaceRouteFromPathname,
} from '@/utils/host-routes'
import { prepareWorkspaceTab } from '@/utils/workspace-navigation'
import {
type SidebarProjectEntry,
type SidebarWorkspaceEntry,
@@ -1649,10 +1649,11 @@ export function SidebarWorkspaceList({
}, 3000)
)
onWorkspacePress?.()
router.replace(
buildHostWorkspaceRouteWithOpenIntent(serverId, workspace.id, {
kind: 'draft',
draftId: 'new',
router.navigate(
prepareWorkspaceTab({
serverId,
workspaceId: workspace.id,
target: { kind: 'draft', draftId: 'new' },
}) as any
)
} catch (error) {

View File

@@ -12,11 +12,11 @@ import {
takeCommandCenterFocusRestoreElement,
} from "@/utils/command-center-focus-restore";
import {
buildHostWorkspaceAgentRoute,
buildHostSettingsRoute,
parseServerIdFromPathname,
} from "@/utils/host-routes";
import type { ShortcutKey } from "@/utils/format-shortcut";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
import { focusWithRetries } from "@/utils/web-focus";
const EMPTY_AGENTS: AggregatedAgent[] = [];
@@ -199,12 +199,12 @@ export function useCommandCenter() {
// Don't restore focus back to the prior element after we navigate.
clearCommandCenterFocusRestoreElement();
setOpen(false);
const route: Href = buildHostWorkspaceAgentRoute(
agent.serverId,
agent.cwd,
agent.id
) as Href;
router.navigate(route);
const route = prepareWorkspaceTab({
serverId: agent.serverId,
workspaceId: agent.cwd,
target: { kind: "agent", agentId: agent.id },
});
router.navigate(route as any);
},
[setOpen]
);

View File

@@ -6,7 +6,7 @@ import {
normalizeWorkspaceDescriptor,
useSessionStore,
} from "@/stores/session-store";
import { buildHostWorkspaceRouteWithOpenIntent } from "@/utils/host-routes";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
export function useOpenProject(
serverId: string | null
@@ -37,11 +37,11 @@ export function useOpenProject(
]);
setHasHydratedWorkspaces(normalizedServerId, true);
router.replace(
buildHostWorkspaceRouteWithOpenIntent(
normalizedServerId,
payload.workspace.id,
{ kind: "draft", draftId: "new" }
) as any
prepareWorkspaceTab({
serverId: normalizedServerId,
workspaceId: payload.workspace.id,
target: { kind: "draft", draftId: "new" },
}) as any
);
return true;
} catch (error) {

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore
import { createNameId } from 'mnemonic-id'
import type { ImageAttachment } from '@/components/message-input'
import { View, Text, Pressable, ScrollView, Keyboard, Platform } from 'react-native'
import { useLocalSearchParams, useRouter, type Href } from 'expo-router'
import { useLocalSearchParams, useRouter } from 'expo-router'
import { useIsFocused } from '@react-navigation/native'
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
@@ -46,7 +46,7 @@ import type {
AgentSessionConfig,
} from '@server/server/agent/agent-sdk-types'
import { AGENT_PROVIDER_DEFINITIONS } from '@server/server/agent/provider-manifest'
import { buildHostWorkspaceAgentRoute } from '@/utils/host-routes'
import { prepareWorkspaceTab } from '@/utils/workspace-navigation'
import { useDesktopDragHandlers } from '@/utils/desktop-window'
import { useKeyboardShiftStyle } from '@/hooks/use-keyboard-shift-style'
import { normalizeAgentSnapshot } from '@/utils/agent-snapshots'
@@ -928,12 +928,12 @@ function DraftAgentScreenContent({
}
},
onCreateSuccess: ({ result }) => {
const route: Href = buildHostWorkspaceAgentRoute(
selectedServerId as string,
result.cwd,
result.id
) as Href
router.replace(route)
const route = prepareWorkspaceTab({
serverId: selectedServerId as string,
workspaceId: result.cwd,
target: { kind: 'agent', agentId: result.id },
})
router.replace(route as any)
},
})
useEffect(() => {

View File

@@ -1,7 +1,6 @@
import { describe, expect, it } from "vitest";
import type { Agent } from "@/stores/session-store";
import {
canOpenAgentTabFromRoute,
deriveWorkspaceAgentVisibility,
shouldPruneWorkspaceAgentTab,
workspaceAgentVisibilityEqual,
@@ -89,18 +88,6 @@ describe("workspace agent visibility", () => {
expect(result.knownAgentIds.has("other-workspace-agent")).toBe(false);
});
it("allows explicit route open for archived agent once agents are hydrated", () => {
const knownAgentIds = new Set(["archived-agent"]);
expect(
canOpenAgentTabFromRoute({
agentId: "archived-agent",
agentsHydrated: true,
knownAgentIds,
})
).toBe(true);
});
it("does not prune archived agent tabs when knownAgentIds contains the agent", () => {
const knownAgentIds = new Set(["archived-agent"]);

View File

@@ -57,20 +57,6 @@ function setsEqual(a: Set<string>, b: Set<string>): boolean {
return true;
}
export function canOpenAgentTabFromRoute(input: {
agentId: string;
agentsHydrated: boolean;
knownAgentIds: Set<string>;
}): boolean {
if (!input.agentId.trim()) {
return false;
}
if (!input.agentsHydrated) {
return true;
}
return input.knownAgentIds.has(input.agentId);
}
export function shouldPruneWorkspaceAgentTab(input: {
agentId: string;
agentsHydrated: boolean;

View File

@@ -64,8 +64,6 @@ import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import {
buildWorkspaceOpenIntentParam,
type WorkspaceOpenIntent,
decodeWorkspaceIdFromPathSegment,
} from "@/utils/host-routes";
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
@@ -103,7 +101,6 @@ import {
} from "@/screens/workspace/workspace-agent-visibility";
import {
deriveWorkspacePaneState,
type WorkspaceDerivedTab,
} from "@/screens/workspace/workspace-pane-state";
import {
buildWorkspacePaneContentModel,
@@ -125,7 +122,6 @@ const EMPTY_PINNED_AGENT_IDS = new Set<string>();
type WorkspaceScreenProps = {
serverId: string;
workspaceId: string;
openIntent?: WorkspaceOpenIntent | null;
};
function trimNonEmpty(value: string | null | undefined): string | null {
@@ -144,49 +140,6 @@ function decodeSegment(value: string): string {
}
}
function buildOpenIntentKey(input: {
serverId: string;
workspaceId: string;
openIntent?: WorkspaceOpenIntent | null;
}): string | null {
if (!input.openIntent) {
return null;
}
const openParam = buildWorkspaceOpenIntentParam(input.openIntent);
if (!openParam) {
return null;
}
return `${input.serverId}:${input.workspaceId}:${openParam}`;
}
function openIntentMatchesActiveTab(input: {
openIntent?: WorkspaceOpenIntent | null;
activeTab: WorkspaceDerivedTab | null;
}): boolean {
const { openIntent, activeTab } = input;
if (!openIntent || !activeTab) {
return false;
}
const target = activeTab.descriptor.target;
if (openIntent.kind !== target.kind) {
return false;
}
if (openIntent.kind === "agent" && target.kind === "agent") {
return target.agentId === openIntent.agentId;
}
if (openIntent.kind === "terminal" && target.kind === "terminal") {
return target.terminalId === openIntent.terminalId;
}
if (openIntent.kind === "file" && target.kind === "file") {
return target.path === openIntent.path;
}
if (openIntent.kind === "draft" && target.kind === "draft") {
return openIntent.draftId === "new" || target.draftId === openIntent.draftId;
}
return false;
}
function getFallbackTabOptionLabel(tab: WorkspaceTabDescriptor): string {
if (tab.target.kind === "draft") {
return "New Agent";
@@ -507,7 +460,6 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
export function WorkspaceScreen({
serverId,
workspaceId,
openIntent,
}: WorkspaceScreenProps) {
const isFocused = useIsFocused();
@@ -520,7 +472,6 @@ export function WorkspaceScreen({
<WorkspaceScreenContent
serverId={serverId}
workspaceId={workspaceId}
openIntent={openIntent}
/>
</ExplorerSidebarAnimationProvider>
);
@@ -529,7 +480,6 @@ export function WorkspaceScreen({
function WorkspaceScreenContent({
serverId,
workspaceId,
openIntent,
}: WorkspaceScreenProps) {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
@@ -811,7 +761,6 @@ function WorkspaceScreenContent({
const openWorkspaceTab = useWorkspaceLayoutStore((state) => state.openTab);
const focusWorkspaceTab = useWorkspaceLayoutStore((state) => state.focusTab);
const closeWorkspaceTab = useWorkspaceLayoutStore((state) => state.closeTab);
const pinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.pinAgent);
const unpinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.unpinAgent);
const retargetWorkspaceTab = useWorkspaceLayoutStore((state) => state.retargetTab);
const splitWorkspacePane = useWorkspaceLayoutStore((state) => state.splitPane);
@@ -826,33 +775,7 @@ function WorkspaceScreenContent({
: EMPTY_PINNED_AGENT_IDS
);
const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId);
const consumedOpenIntentsRef = useRef(new Set<string>());
const pendingCloseTabIdsRef = useRef(new Set<string>());
const [resolvedOpenIntentKey, setResolvedOpenIntentKey] = useState<string | null>(null);
const currentOpenIntentKey = useMemo(
() =>
buildOpenIntentKey({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
openIntent,
}),
[normalizedServerId, normalizedWorkspaceId, openIntent]
);
const currentOpenIntentPinnedAgentId = useMemo(
function getCurrentOpenIntentPinnedAgentId() {
if (!openIntent || openIntent.kind !== "agent" || !hasHydratedAgents) {
return null;
}
if (!workspaceAgentVisibility.knownAgentIds.has(openIntent.agentId)) {
return null;
}
if (workspaceAgentVisibility.activeAgentIds.has(openIntent.agentId)) {
return null;
}
return openIntent.agentId;
},
[hasHydratedAgents, openIntent, workspaceAgentVisibility]
);
const closeWorkspaceTabWithCleanup = useCallback(
function closeWorkspaceTabWithCleanup(input: {
tabId: string;
@@ -949,93 +872,6 @@ function WorkspaceScreenContent({
[ensureWorkspaceTab, focusWorkspaceTab, openWorkspaceTab, persistenceKey]
);
useEffect(() => {
if (!currentOpenIntentKey) {
if (resolvedOpenIntentKey !== null) {
setResolvedOpenIntentKey(null);
}
return;
}
if (resolvedOpenIntentKey === currentOpenIntentKey) {
return;
}
}, [currentOpenIntentKey, resolvedOpenIntentKey]);
useEffect(() => {
if (!openIntent || !persistenceKey) {
return;
}
if (!currentOpenIntentKey) {
return;
}
if (currentOpenIntentPinnedAgentId) {
pinWorkspaceAgent(persistenceKey, currentOpenIntentPinnedAgentId);
}
const intentKey = currentOpenIntentKey;
if (consumedOpenIntentsRef.current.has(intentKey)) {
if (resolvedOpenIntentKey !== intentKey) {
setResolvedOpenIntentKey(intentKey);
}
return;
}
consumedOpenIntentsRef.current.add(intentKey);
if (openIntent.kind === "draft") {
const draftId = openIntent.draftId.trim();
const tabId = openWorkspaceDraftTab({
...(draftId === "new" ? {} : { draftId }),
focus: true,
});
return;
}
const target: WorkspaceTabTarget =
openIntent.kind === "agent"
? { kind: "agent", agentId: openIntent.agentId }
: openIntent.kind === "terminal"
? { kind: "terminal", terminalId: openIntent.terminalId }
: { kind: "file", path: openIntent.path };
const tabId = openWorkspaceTab(persistenceKey, target);
if (tabId) {
focusWorkspaceTab(persistenceKey, tabId);
}
}, [
currentOpenIntentPinnedAgentId,
currentOpenIntentKey,
focusWorkspaceTab,
openIntent,
openWorkspaceDraftTab,
openWorkspaceTab,
pinWorkspaceAgent,
persistenceKey,
]);
const unresolvedOpenIntent = currentOpenIntentKey && resolvedOpenIntentKey !== currentOpenIntentKey
? openIntent
: null;
const resolvedPaneTabState = useMemo(
() =>
deriveWorkspacePaneState({
layout: workspaceLayout,
tabs: uiTabs,
preferredTarget:
unresolvedOpenIntent?.kind === "agent"
? { kind: "agent", agentId: unresolvedOpenIntent.agentId }
: unresolvedOpenIntent?.kind === "terminal"
? { kind: "terminal", terminalId: unresolvedOpenIntent.terminalId }
: unresolvedOpenIntent?.kind === "draft"
? { kind: "draft", draftId: unresolvedOpenIntent.draftId }
: unresolvedOpenIntent?.kind === "file"
? { kind: "file", path: unresolvedOpenIntent.path }
: null,
}),
[uiTabs, unresolvedOpenIntent, workspaceLayout]
);
useEffect(() => {
if (!normalizedServerId || !normalizedWorkspaceId || !persistenceKey) {
return;
@@ -1077,12 +913,10 @@ function WorkspaceScreenContent({
canPruneAgentTabs &&
tab.target.kind === "agent" &&
!pinnedAgentIds.has(tab.target.agentId) &&
tab.target.agentId !== currentOpenIntentPinnedAgentId &&
shouldPruneWorkspaceAgentTab({
agentId: tab.target.agentId,
agentsHydrated: hasHydratedAgents,
knownAgentIds: workspaceAgentVisibility.knownAgentIds,
activeAgentIds: workspaceAgentVisibility.activeAgentIds,
})
) {
closeWorkspaceTabWithCleanup({ tabId: tab.tabId, target: tab.target });
@@ -1097,7 +931,6 @@ function WorkspaceScreenContent({
}
}, [
closeWorkspaceTabWithCleanup,
currentOpenIntentPinnedAgentId,
ensureWorkspaceTab,
hasHydratedAgents,
pendingByDraftId,
@@ -1109,25 +942,8 @@ function WorkspaceScreenContent({
workspaceAgentVisibility,
]);
const activeTabId = resolvedPaneTabState.activeTabId;
const activeTab = resolvedPaneTabState.activeTab;
useEffect(() => {
if (!openIntent || !currentOpenIntentKey) {
return;
}
if (resolvedOpenIntentKey === currentOpenIntentKey) {
return;
}
if (
openIntentMatchesActiveTab({
openIntent,
activeTab,
})
) {
setResolvedOpenIntentKey(currentOpenIntentKey);
}
}, [activeTab, currentOpenIntentKey, openIntent, resolvedOpenIntentKey]);
const activeTabId = focusedPaneTabState.activeTabId;
const activeTab = focusedPaneTabState.activeTab;
useEffect(() => {
if (!activeTabId || !persistenceKey) {
@@ -1137,8 +953,8 @@ function WorkspaceScreenContent({
}, [activeTabId, focusWorkspaceTab, persistenceKey]);
const tabs = useMemo<WorkspaceTabDescriptor[]>(
() => resolvedPaneTabState.tabs.map((tab) => tab.descriptor),
[resolvedPaneTabState.tabs]
() => focusedPaneTabState.tabs.map((tab) => tab.descriptor),
[focusedPaneTabState.tabs]
);
const navigateToTabId = useCallback(
@@ -1156,10 +972,6 @@ function WorkspaceScreenContent({
if (!persistenceKey) {
return;
}
if (openIntent) {
emptyWorkspaceSeedRef.current = null;
return;
}
if (workspaceAgentVisibility.activeAgentIds.size > 0 || terminals.length > 0) {
emptyWorkspaceSeedRef.current = null;
return;
@@ -1177,7 +989,6 @@ function WorkspaceScreenContent({
}, [
normalizedServerId,
normalizedWorkspaceId,
openIntent,
openWorkspaceDraftTab,
persistenceKey,
terminals.length,

View File

@@ -2,11 +2,7 @@ import { describe, expect, it } from "vitest";
import {
buildHostAgentDetailRoute,
buildHostRootRoute,
buildHostWorkspaceAgentRoute,
buildHostWorkspaceFileRoute,
buildHostWorkspaceRoute,
buildHostWorkspaceRouteWithOpenIntent,
buildHostWorkspaceTerminalRoute,
decodeFilePathFromPathSegment,
decodeWorkspaceIdFromPathSegment,
encodeFilePathForPathSegment,
@@ -65,24 +61,6 @@ describe("workspace route parsing", () => {
expect(buildHostRootRoute("local")).toBe("/h/local");
});
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("parses workspace open intent from pathname query", () => {
expect(
parseHostWorkspaceOpenIntentFromPathname(
@@ -106,7 +84,7 @@ describe("workspace route parsing", () => {
});
});
it("keeps agent detail workspace routing on workspace path with open intent", () => {
it("uses the plain workspace route when workspace context is provided", () => {
expect(buildHostAgentDetailRoute("local", "agent-1", "/tmp/repo")).toBe(
"/h/local/workspace/L3RtcC9yZXBv?open=agent%3Aagent-1"
);

View File

@@ -144,29 +144,6 @@ export function parseWorkspaceOpenIntent(
return 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;
}
export function parseHostWorkspaceOpenIntentFromPathname(
pathname: string
): WorkspaceOpenIntent | null {
@@ -311,55 +288,6 @@ export function buildHostWorkspaceRoute(
return `/h/${encodeSegment(normalizedServerId)}/workspace/${encodeSegment(encodedWorkspaceId)}`;
}
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 {
return buildHostWorkspaceRouteWithOpenIntent(serverId, workspaceId, {
kind: "agent",
agentId,
});
}
export function buildHostWorkspaceTerminalRoute(
serverId: string,
workspaceId: string,
terminalId: string
): string {
return buildHostWorkspaceRouteWithOpenIntent(serverId, workspaceId, {
kind: "terminal",
terminalId,
});
}
export function buildHostWorkspaceFileRoute(
serverId: string,
workspaceId: string,
filePath: string
): string {
return buildHostWorkspaceRouteWithOpenIntent(serverId, workspaceId, {
kind: "file",
path: filePath,
});
}
export function buildHostAgentDetailRoute(
serverId: string,
agentId: string,
@@ -367,11 +295,15 @@ export function buildHostAgentDetailRoute(
): string {
const normalizedWorkspaceId = trimNonEmpty(workspaceId);
if (normalizedWorkspaceId) {
return buildHostWorkspaceAgentRoute(
serverId,
normalizedWorkspaceId,
agentId
);
const normalizedAgentId = trimNonEmpty(agentId);
if (!normalizedAgentId) {
return "/";
}
const base = buildHostWorkspaceRoute(serverId, normalizedWorkspaceId);
if (base === "/") {
return "/";
}
return `${base}?open=${encodeURIComponent(`agent:${normalizedAgentId}`)}`;
}
const normalizedServerId = trimNonEmpty(serverId);
const normalizedAgentId = trimNonEmpty(agentId);
@@ -437,19 +369,10 @@ export function mapPathnameToServer(
}
const workspaceRoute = parseHostWorkspaceRouteFromPathname(pathname);
if (workspaceRoute) {
const workspacePath = buildHostWorkspaceRoute(
return 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}`;

View File

@@ -1,7 +1,7 @@
import {
buildHostAgentDetailRoute,
buildHostRootRoute,
buildHostWorkspaceAgentRoute,
buildHostWorkspaceRoute,
} from "@/utils/host-routes";
type NotificationData = Record<string, unknown> | null | undefined;
@@ -36,7 +36,8 @@ export function buildNotificationRoute(data: NotificationData): string {
const { serverId, agentId, workspaceId } = resolveNotificationTarget(data);
if (serverId && agentId) {
if (workspaceId) {
return buildHostWorkspaceAgentRoute(serverId, workspaceId, agentId);
const base = buildHostWorkspaceRoute(serverId, workspaceId);
return `${base}?open=${encodeURIComponent(`agent:${agentId}`)}`;
}
return buildHostAgentDetailRoute(serverId, agentId);
}

View File

@@ -0,0 +1,42 @@
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
import { generateDraftId } from "@/stores/draft-keys";
import {
buildWorkspaceTabPersistenceKey,
type WorkspaceTabTarget,
} from "@/stores/workspace-tabs-store";
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
interface PrepareWorkspaceTabInput {
serverId: string;
workspaceId: string;
target: WorkspaceTabTarget;
pin?: boolean;
}
function getPreparedTarget(target: WorkspaceTabTarget): WorkspaceTabTarget {
if (target.kind !== "draft" || target.draftId.trim() !== "new") {
return target;
}
return { kind: "draft", draftId: generateDraftId() };
}
export function prepareWorkspaceTab(input: PrepareWorkspaceTabInput): string {
const target = getPreparedTarget(input.target);
const key =
buildWorkspaceTabPersistenceKey({
serverId: input.serverId,
workspaceId: input.workspaceId,
}) ?? "";
const tabId = useWorkspaceLayoutStore.getState().openTab(key, target);
if (tabId) {
useWorkspaceLayoutStore.getState().focusTab(key, tabId);
}
if (input.pin && target.kind === "agent") {
useWorkspaceLayoutStore.getState().pinAgent(key, target.agentId);
}
return buildHostWorkspaceRoute(input.serverId, input.workspaceId);
}