WIP: snapshot workspace execution refactor baseline

This commit is contained in:
Mohamed Boudra
2026-04-01 10:35:06 +07:00
parent e026223c80
commit d8243bfb72
47 changed files with 2219 additions and 169 deletions
+27 -16
View File
@@ -78,6 +78,7 @@ import {
parseServerIdFromPathname,
parseHostAgentRouteFromPathname,
parseWorkspaceOpenIntent,
decodeWorkspaceIdFromPathSegment,
} from "@/utils/host-routes";
import { syncNavigationActiveWorkspace } from "@/stores/navigation-active-workspace-store";
@@ -602,8 +603,6 @@ function FaviconStatusSync() {
}
function RootStack() {
const storeReady = useStoreReady();
return (
<Stack
screenOptions={{
@@ -614,20 +613,32 @@ function RootStack() {
},
}}
>
<Stack.Protected guard={storeReady}>
<Stack.Screen name="welcome" />
<Stack.Screen name="settings" />
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]" />
<Stack.Screen
name="h/[serverId]/agent/[agentId]"
options={{ gestureEnabled: false }}
/>
<Stack.Screen name="h/[serverId]/index" />
<Stack.Screen name="h/[serverId]/sessions" />
<Stack.Screen name="h/[serverId]/open-project" />
<Stack.Screen name="h/[serverId]/settings" />
<Stack.Screen name="pair-scan" />
</Stack.Protected>
<Stack.Screen name="welcome" />
<Stack.Screen name="settings" />
<Stack.Screen
name="h/[serverId]/workspace/[workspaceId]"
getId={({ params }) => {
const serverValue = Array.isArray(params?.serverId) ? params.serverId[0] : params?.serverId;
const workspaceValue = Array.isArray(params?.workspaceId)
? params.workspaceId[0]
: params?.workspaceId;
const serverId = typeof serverValue === "string" ? serverValue.trim() : "";
const workspaceId =
typeof workspaceValue === "string"
? (decodeWorkspaceIdFromPathSegment(workspaceValue) ?? workspaceValue.trim())
: "";
return `${serverId}:${workspaceId}`;
}}
/>
<Stack.Screen
name="h/[serverId]/agent/[agentId]"
options={{ gestureEnabled: false }}
/>
<Stack.Screen name="h/[serverId]/index" />
<Stack.Screen name="h/[serverId]/sessions" />
<Stack.Screen name="h/[serverId]/open-project" />
<Stack.Screen name="h/[serverId]/settings" />
<Stack.Screen name="pair-scan" />
<Stack.Screen name="index" />
</Stack>
);
@@ -3,6 +3,7 @@ import { useLocalSearchParams, useRouter } from "expo-router";
import { useSessionStore } from "@/stores/session-store";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { buildHostRootRoute } from "@/utils/host-routes";
import { resolveHydratedWorkspaceId } from "@/utils/resolve-hydrated-workspace-id";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
export default function HostAgentReadyRoute() {
@@ -22,6 +23,21 @@ export default function HostAgentReadyRoute() {
}
return state.sessions[serverId]?.agents?.get(agentId)?.cwd ?? null;
});
const sessionWorkspaces = useSessionStore((state) =>
serverId ? state.sessions[serverId]?.workspaces : undefined,
);
const hasHydratedWorkspaces = useSessionStore((state) =>
serverId ? (state.sessions[serverId]?.hasHydratedWorkspaces ?? false) : false,
);
const resolvedWorkspaceId = useSessionStore((state) => {
if (!serverId || !agentId) {
return null;
}
return resolveHydratedWorkspaceId({
workspaces: state.sessions[serverId]?.workspaces?.values(),
path: state.sessions[serverId]?.agents?.get(agentId)?.cwd,
});
});
useEffect(() => {
if (redirectedRef.current) {
@@ -33,18 +49,17 @@ export default function HostAgentReadyRoute() {
return;
}
const normalizedCwd = agentCwd?.trim();
if (normalizedCwd) {
if (resolvedWorkspaceId) {
redirectedRef.current = true;
router.replace(
prepareWorkspaceTab({
serverId,
workspaceId: normalizedCwd,
workspaceId: resolvedWorkspaceId,
target: { kind: "agent", agentId },
}) as any,
);
}
}, [agentCwd, agentId, router, serverId]);
}, [agentId, resolvedWorkspaceId, router, serverId]);
useEffect(() => {
if (redirectedRef.current) {
@@ -53,14 +68,14 @@ export default function HostAgentReadyRoute() {
if (!serverId || !agentId) {
return;
}
if (agentCwd?.trim()) {
if (agentCwd?.trim() && !hasHydratedWorkspaces) {
return;
}
if (!client || !isConnected) {
redirectedRef.current = true;
router.replace(buildHostRootRoute(serverId) as any);
}
}, [agentCwd, agentId, client, isConnected, router, serverId]);
}, [agentCwd, agentId, client, hasHydratedWorkspaces, isConnected, router, serverId]);
useEffect(() => {
if (redirectedRef.current) {
@@ -78,12 +93,19 @@ export default function HostAgentReadyRoute() {
return;
}
const cwd = result?.agent?.cwd?.trim();
const workspaceId = resolveHydratedWorkspaceId({
workspaces: sessionWorkspaces?.values(),
path: cwd,
});
if (!workspaceId && !hasHydratedWorkspaces) {
return;
}
redirectedRef.current = true;
if (cwd) {
if (workspaceId) {
router.replace(
prepareWorkspaceTab({
serverId,
workspaceId: cwd,
workspaceId,
target: { kind: "agent", agentId },
}) as any,
);
@@ -102,7 +124,7 @@ export default function HostAgentReadyRoute() {
return () => {
cancelled = true;
};
}, [agentId, client, isConnected, router, serverId]);
}, [agentId, client, hasHydratedWorkspaces, isConnected, router, serverId, sessionWorkspaces]);
return null;
}
+23 -5
View File
@@ -3,15 +3,23 @@ import { useLocalSearchParams, usePathname, useRouter } from "expo-router";
import { useSessionStore } from "@/stores/session-store";
import { useFormPreferences } from "@/hooks/use-form-preferences";
import {
buildHostAgentDetailRoute,
buildHostOpenProjectRoute,
buildHostRootRoute,
buildHostWorkspaceOpenRoute,
buildHostWorkspaceRoute,
} from "@/utils/host-routes";
import { resolveHydratedWorkspaceId } from "@/utils/resolve-hydrated-workspace-id";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
const HOST_ROOT_REDIRECT_DELAY_MS = 300;
function getCurrentPathname(fallbackPathname: string): string {
if (typeof window === "undefined") {
return fallbackPathname;
}
return window.location.pathname || fallbackPathname;
}
export default function HostIndexRoute() {
const router = useRouter();
const pathname = usePathname();
@@ -33,11 +41,13 @@ export default function HostIndexRoute() {
return;
}
const rootRoute = buildHostRootRoute(serverId);
if (pathname !== rootRoute && pathname !== `${rootRoute}/`) {
const currentPathname = getCurrentPathname(pathname);
if (currentPathname !== rootRoute && currentPathname !== `${rootRoute}/`) {
return;
}
const timer = setTimeout(() => {
if (pathname !== rootRoute && pathname !== `${rootRoute}/`) {
const latestPathname = getCurrentPathname(pathname);
if (latestPathname !== rootRoute && latestPathname !== `${rootRoute}/`) {
return;
}
@@ -56,16 +66,24 @@ export default function HostIndexRoute() {
});
const primaryAgent = visibleAgents[0];
if (primaryAgent?.cwd?.trim()) {
const primaryAgentWorkspaceId = resolveHydratedWorkspaceId({
workspaces: sessionWorkspaces?.values(),
path: primaryAgent?.cwd,
});
if (primaryAgent && primaryAgentWorkspaceId) {
router.replace(
prepareWorkspaceTab({
serverId,
workspaceId: primaryAgent.cwd.trim(),
workspaceId: primaryAgentWorkspaceId,
target: { kind: "agent", agentId: primaryAgent.id },
}) as any,
);
return;
}
if (primaryAgent) {
router.replace(buildHostAgentDetailRoute(serverId, primaryAgent.id) as any);
return;
}
const primaryWorkspace = visibleWorkspaces[0];
if (primaryWorkspace?.id?.trim()) {
@@ -1,10 +1,10 @@
import { useEffect, useRef } from "react";
import { useGlobalSearchParams, useLocalSearchParams, useRouter } from "expo-router";
import { useGlobalSearchParams, usePathname, useRouter } from "expo-router";
import type { WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
import { WorkspaceScreen } from "@/screens/workspace/workspace-screen";
import {
buildHostWorkspaceRoute,
decodeWorkspaceIdFromPathSegment,
parseHostWorkspaceRouteFromPathname,
parseWorkspaceOpenIntent,
type WorkspaceOpenIntent,
} from "@/utils/host-routes";
@@ -37,18 +37,13 @@ function getOpenIntentTarget(openIntent: WorkspaceOpenIntent): WorkspaceTabTarge
export default function HostWorkspaceLayout() {
const router = useRouter();
const consumedIntentRef = useRef<string | null>(null);
const params = useLocalSearchParams<{
serverId?: string | string[];
workspaceId?: string | string[];
}>();
const pathname = usePathname();
const globalParams = useGlobalSearchParams<{
open?: string | string[];
}>();
const serverId = getParamValue(params.serverId);
const workspaceValue = getParamValue(params.workspaceId);
const workspaceId = workspaceValue
? (decodeWorkspaceIdFromPathSegment(workspaceValue) ?? "")
: "";
const parsedWorkspaceRoute = parseHostWorkspaceRouteFromPathname(pathname);
const serverId = parsedWorkspaceRoute?.serverId ?? "";
const workspaceId = parsedWorkspaceRoute?.workspaceId ?? "";
const openValue = getParamValue(globalParams.open);
useEffect(() => {
+9 -1
View File
@@ -14,6 +14,13 @@ import { buildHostRootRoute } from "@/utils/host-routes";
const WELCOME_ROUTE = "/welcome";
function getCurrentPathname(fallbackPathname: string): string {
if (typeof window === "undefined") {
return fallbackPathname;
}
return window.location.pathname || fallbackPathname;
}
function useAnyOnlineHostServerId(serverIds: string[]): string | null {
const runtime = getHostRuntimeStore();
@@ -51,7 +58,8 @@ export default function Index() {
if (!storeReady) {
return;
}
if (pathname !== "/" && pathname !== "") {
const currentPathname = getCurrentPathname(pathname);
if (currentPathname !== "/" && currentPathname !== "") {
return;
}
+12 -1
View File
@@ -17,6 +17,8 @@ import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
import { useSessionStore } from "@/stores/session-store";
import { Archive, SquareTerminal } from "lucide-react-native";
import { getProviderIcon } from "@/components/provider-icons";
import { buildHostAgentDetailRoute } from "@/utils/host-routes";
import { resolveHydratedWorkspaceId } from "@/utils/resolve-hydrated-workspace-id";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
interface AgentListProps {
@@ -242,12 +244,21 @@ export function AgentList({
const serverId = agent.serverId;
const agentId = agent.id;
const workspaceId = resolveHydratedWorkspaceId({
workspaces: useSessionStore.getState().sessions[serverId]?.workspaces?.values(),
path: agent.cwd,
});
onAgentSelect?.();
if (!workspaceId) {
router.navigate(buildHostAgentDetailRoute(serverId, agentId) as any);
return;
}
const route = prepareWorkspaceTab({
serverId,
workspaceId: agent.cwd,
workspaceId,
target: { kind: "agent", agentId },
pin: Boolean(agent.archivedAt),
requestReopen: agent.terminal && agent.status === "closed",
@@ -63,6 +63,7 @@ import { createMarkdownStyles } from "@/styles/markdown-styles";
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
import { getMarkdownListMarker } from "@/utils/markdown-list";
import { normalizeInlinePathTarget } from "@/utils/inline-path";
import { resolveHydratedWorkspaceId } from "@/utils/resolve-hydrated-workspace-id";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
import { useStableEvent } from "@/hooks/use-stable-event";
import {
@@ -132,10 +133,14 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
);
const workspaceRoot = agent.cwd?.trim() || "";
const workspaceId = agent.projectPlacement?.checkout?.cwd?.trim() || workspaceRoot;
const workspacePath = agent.projectPlacement?.checkout?.cwd?.trim() || workspaceRoot;
const workspaceId = resolveHydratedWorkspaceId({
workspaces: useSessionStore.getState().sessions[resolvedServerId]?.workspaces?.values(),
path: workspacePath,
});
const { requestDirectoryListing } = useFileExplorerActions({
serverId: resolvedServerId,
workspaceId,
workspaceId: workspaceId ?? undefined,
workspaceRoot,
});
const openWorkspaceFile = useStableEvent(function openWorkspaceFile(input: {
@@ -175,12 +180,14 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
return;
}
const route = prepareWorkspaceTab({
serverId: resolvedServerId,
workspaceId,
target: { kind: "file", path: normalized.file },
});
router.navigate(route as any);
if (workspaceId) {
const route = prepareWorkspaceTab({
serverId: resolvedServerId,
workspaceId,
target: { kind: "file", path: normalized.file },
});
router.navigate(route as any);
}
return;
}
@@ -40,9 +40,9 @@ export function ProjectPickerModal() {
const recommendedPaths = useMemo(() => {
if (!workspaces) return [];
return Array.from(workspaces.values()).map(
(workspace) => workspace.projectRootPath || workspace.id,
);
return Array.from(workspaces.values())
.map((workspace) => workspace.projectRootPath)
.filter((path) => path.length > 0);
}, [workspaces]);
const directorySuggestionsQuery = useQuery({
@@ -817,8 +817,8 @@ function WorkspaceRowInner({
const isMobile = Platform.OS !== "web";
const prHint = useWorkspacePrHint({
serverId: workspace.serverId,
cwd: workspace.workspaceId,
enabled: workspace.projectKind === "git",
cwd: workspace.projectRootPath ?? "",
enabled: workspace.projectKind === "git" && Boolean(workspace.projectRootPath),
});
const interaction = useLongPressDragInteraction({
drag,
@@ -985,7 +985,7 @@ function WorkspaceRowWithMenu({
const archiveStatus = useCheckoutGitActionsStore((state) =>
state.getStatus({
serverId: workspace.serverId,
cwd: workspace.workspaceId,
cwd: workspace.workspaceDirectory ?? workspace.projectRootPath ?? "",
actionId: "archive-worktree",
}),
);
@@ -1025,11 +1025,16 @@ function WorkspaceRowWithMenu({
if (!confirmed) {
return;
}
const workspaceDirectory = workspace.workspaceDirectory ?? workspace.projectRootPath;
if (!workspaceDirectory) {
toast.error("Workspace path not available");
return;
}
void archiveWorktree({
serverId: workspace.serverId,
cwd: workspace.workspaceId,
worktreePath: workspace.workspaceId,
cwd: workspaceDirectory,
worktreePath: workspaceDirectory,
})
.then(() => {
redirectAfterArchive();
@@ -1045,6 +1050,8 @@ function WorkspaceRowWithMenu({
redirectAfterArchive,
toast,
workspace.name,
workspace.projectRootPath,
workspace.workspaceDirectory,
workspace.serverId,
workspace.workspaceId,
]);
@@ -1095,9 +1102,14 @@ function WorkspaceRowWithMenu({
]);
const handleCopyPath = useCallback(() => {
void Clipboard.setStringAsync(workspace.workspaceId);
const workspaceDirectory = workspace.workspaceDirectory ?? workspace.projectRootPath;
if (!workspaceDirectory) {
toast.error("Workspace path not available");
return;
}
void Clipboard.setStringAsync(workspaceDirectory);
toast.copied("Path copied");
}, [toast, workspace.workspaceId]);
}, [toast, workspace.projectRootPath, workspace.workspaceDirectory]);
const handleCopyBranchName = useCallback(() => {
void Clipboard.setStringAsync(workspace.name);
@@ -244,6 +244,11 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
);
useEffect(() => {
const currentPathname =
typeof window === "undefined" ? null : (window.location.pathname || null);
if (currentPathname && currentPathname !== "/welcome") {
return;
}
if (!anyOnlineServerId) {
return;
}
@@ -53,7 +53,7 @@ export function WorkspaceSetupDialog() {
initialValues: projectPath ? { workingDir: projectPath } : undefined,
isVisible: pendingWorkspaceSetup !== null,
onlineServerIds: isConnected && serverId ? [serverId] : [],
lockedWorkingDir: workspace?.id ?? projectPath,
lockedWorkingDir: workspace?.projectRootPath ?? projectPath,
},
});
const composerState = chatDraft.composerState;
@@ -170,9 +170,10 @@ export function WorkspaceSetupDialog() {
}
const encodedImages = await encodeImages(images);
const workspaceDirectory = workspace.projectRootPath ?? projectPath;
const agent = await connectedClient.createAgent({
provider: composerState.selectedProvider,
cwd: workspace.id,
cwd: workspaceDirectory,
...(composerState.modeOptions.length > 0 && composerState.selectedMode !== ""
? { modeId: composerState.selectedMode }
: {}),
@@ -226,9 +227,10 @@ export function WorkspaceSetupDialog() {
throw new Error("Workspace setup composer state is required");
}
const workspaceDirectory = workspace.projectRootPath ?? projectPath;
const agent = await connectedClient.createAgent({
provider: composerState.selectedProvider,
cwd: workspace.id,
cwd: workspaceDirectory,
terminal: true,
...(terminalPrompt.trim() ? { initialPrompt: terminalPrompt.trim() } : {}),
});
@@ -272,8 +274,13 @@ export function WorkspaceSetupDialog() {
setErrorMessage(null);
const workspace = await ensureWorkspace();
const connectedClient = withConnectedClient();
const workspaceDirectory = workspace.projectRootPath ?? projectPath;
const payload = await connectedClient.createTerminal(workspace.id);
if (!workspaceDirectory) {
throw new Error("Workspace directory not found");
}
const payload = await connectedClient.createTerminal(workspaceDirectory);
if (payload.error || !payload.terminal) {
throw new Error(payload.error ?? "Failed to open terminal");
}
+16 -2
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { TextInput } from "react-native";
import { router, usePathname, type Href } from "expo-router";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { useSessionStore } from "@/stores/session-store";
import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher";
import { useHosts } from "@/runtime/host-runtime";
import { useAllAgentsList } from "@/hooks/use-all-agents-list";
@@ -11,13 +12,18 @@ import {
clearCommandCenterFocusRestoreElement,
takeCommandCenterFocusRestoreElement,
} from "@/utils/command-center-focus-restore";
import { buildHostSettingsRoute, parseServerIdFromPathname } from "@/utils/host-routes";
import {
buildHostAgentDetailRoute,
buildHostSettingsRoute,
parseServerIdFromPathname,
} from "@/utils/host-routes";
import type { ShortcutKey } from "@/utils/format-shortcut";
import { chordStringToShortcutKeys } from "@/keyboard/shortcut-string";
import { getBindingIdForAction, getDefaultKeysForAction } from "@/keyboard/keyboard-shortcuts";
import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides";
import { getShortcutOs } from "@/utils/shortcut-platform";
import { getIsDesktop } from "@/constants/layout";
import { resolveHydratedWorkspaceId } from "@/utils/resolve-hydrated-workspace-id";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
import { focusWithRetries } from "@/utils/web-focus";
@@ -215,9 +221,17 @@ export function useCommandCenter() {
// Don't restore focus back to the prior element after we navigate.
clearCommandCenterFocusRestoreElement();
setOpen(false);
const workspaceId = resolveHydratedWorkspaceId({
workspaces: useSessionStore.getState().sessions[agent.serverId]?.workspaces?.values(),
path: agent.cwd,
});
if (!workspaceId) {
router.navigate(buildHostAgentDetailRoute(agent.serverId, agent.id) as any);
return;
}
const route = prepareWorkspaceTab({
serverId: agent.serverId,
workspaceId: agent.cwd,
workspaceId,
target: { kind: "agent", agentId: agent.id },
});
router.navigate(route as any);
@@ -69,6 +69,7 @@ describe("openProjectDirectly", () => {
projectId: 1,
projectDisplayName: "project",
projectRootPath: WORKSPACE_ID,
workspaceDirectory: WORKSPACE_ID,
projectKind: "git" as const,
workspaceKind: "checkout" as const,
name: "project",
@@ -87,7 +88,12 @@ describe("openProjectDirectly", () => {
expect(result).toBe(true);
expect(useSessionStore.getState().sessions[SERVER_ID]?.hasHydratedWorkspaces).toBe(true);
expect(Array.from(useSessionStore.getState().sessions[SERVER_ID]?.workspaces.values() ?? [])).toEqual([
expect.objectContaining({ id: "1", projectId: "1", projectRootPath: WORKSPACE_ID }),
expect.objectContaining({
id: "1",
projectId: "1",
projectRootPath: WORKSPACE_ID,
workspaceDirectory: WORKSPACE_ID,
}),
]);
const workspaceKey = buildWorkspaceTabPersistenceKey({
@@ -19,7 +19,11 @@ function workspace(
Partial<
Pick<
WorkspaceDescriptor,
"projectDisplayName" | "projectRootPath" | "projectKind" | "workspaceKind"
| "projectDisplayName"
| "projectRootPath"
| "workspaceDirectory"
| "projectKind"
| "workspaceKind"
>
>,
): WorkspaceDescriptor {
@@ -28,6 +32,7 @@ function workspace(
projectId: input.projectId,
projectDisplayName: input.projectDisplayName ?? input.projectId,
projectRootPath: input.projectRootPath ?? input.id,
workspaceDirectory: input.workspaceDirectory ?? input.projectRootPath ?? input.id,
projectKind: input.projectKind ?? "git",
workspaceKind: input.workspaceKind ?? "checkout",
name: input.name,
@@ -15,6 +15,8 @@ export interface SidebarWorkspaceEntry {
workspaceKey: string;
serverId: string;
workspaceId: string;
projectRootPath?: string;
workspaceDirectory?: string;
projectKind: WorkspaceDescriptor["projectKind"];
workspaceKind: WorkspaceDescriptor["workspaceKind"];
name: string;
@@ -119,7 +121,7 @@ export function buildSidebarProjectsFromWorkspaces(input: {
projectName:
workspace.projectDisplayName || projectDisplayNameFromProjectId(workspace.projectId),
projectKind: workspace.projectKind,
iconWorkingDir: workspace.projectRootPath || workspace.id,
iconWorkingDir: workspace.projectRootPath,
statusBucket: "done",
activeCount: 0,
totalWorkspaces: 0,
@@ -131,6 +133,8 @@ export function buildSidebarProjectsFromWorkspaces(input: {
workspaceKey: `${input.serverId}:${workspace.id}`,
serverId: input.serverId,
workspaceId: workspace.id,
projectRootPath: workspace.projectRootPath,
workspaceDirectory: workspace.workspaceDirectory,
projectKind: workspace.projectKind,
workspaceKind: workspace.workspaceKind,
name: workspace.name,
@@ -254,6 +258,7 @@ function toWorkspaceDescriptor(payload: {
projectId: number;
projectDisplayName: string;
projectRootPath: string;
workspaceDirectory: string;
projectKind: WorkspaceDescriptor["projectKind"];
workspaceKind: WorkspaceDescriptor["workspaceKind"];
name: string;
+26 -8
View File
@@ -36,6 +36,11 @@ function LauncherPanel() {
const { serverId, workspaceId, target, retargetCurrentTab, isPaneFocused } = usePaneContext();
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const workspaceDirectory = useSessionStore(
(state) =>
state.sessions[serverId]?.workspaces.get(workspaceId)?.workspaceDirectory ??
state.sessions[serverId]?.workspaces.get(workspaceId)?.projectRootPath,
);
const { providers, recordUsage } = useProviderRecency();
const setAgents = useSessionStore((state) => state.setAgents);
const [pendingAction, setPendingAction] = useState<string | null>(null);
@@ -53,8 +58,8 @@ function LauncherPanel() {
const launchTerminalAgent = useCallback(
async (providerId: AgentProvider) => {
if (!client || !isConnected) {
setErrorMessage("Host is not connected");
if (!client || !isConnected || !workspaceDirectory) {
setErrorMessage(!workspaceDirectory ? "Workspace directory not found" : "Host is not connected");
return;
}
@@ -64,7 +69,7 @@ function LauncherPanel() {
try {
const agent = await client.createAgent({
provider: providerId,
cwd: workspaceId,
cwd: workspaceDirectory,
terminal: true,
});
recordUsage(providerId);
@@ -82,7 +87,7 @@ function LauncherPanel() {
setPendingAction((current) => (current === providerId ? null : current));
}
},
[client, isConnected, recordUsage, retargetCurrentTab, serverId, setAgents, workspaceId],
[client, isConnected, recordUsage, retargetCurrentTab, serverId, setAgents, workspaceDirectory],
);
const openDraftTab = useCallback(() => {
@@ -96,8 +101,8 @@ function LauncherPanel() {
}, [retargetCurrentTab]);
const openTerminalTab = useCallback(async () => {
if (!client || !isConnected) {
setErrorMessage("Host is not connected");
if (!client || !isConnected || !workspaceDirectory) {
setErrorMessage(!workspaceDirectory ? "Workspace directory not found" : "Host is not connected");
return;
}
@@ -105,7 +110,7 @@ function LauncherPanel() {
setErrorMessage(null);
try {
const payload = await client.createTerminal(workspaceId);
const payload = await client.createTerminal(workspaceDirectory);
if (payload.error || !payload.terminal) {
throw new Error(payload.error ?? "Failed to open terminal");
}
@@ -118,10 +123,20 @@ function LauncherPanel() {
} finally {
setPendingAction((current) => (current === "terminal" ? null : current));
}
}, [client, isConnected, retargetCurrentTab, workspaceId]);
}, [client, isConnected, retargetCurrentTab, workspaceDirectory]);
const actionsDisabled = pendingAction !== null;
if (!workspaceDirectory) {
return (
<View style={styles.container}>
<View style={[styles.content, styles.loadingContent]}>
<ActivityIndicator />
</View>
</View>
);
}
return (
<View style={styles.container}>
<ScrollView
@@ -339,6 +354,9 @@ const styles = StyleSheet.create((theme) => ({
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[8],
},
loadingContent: {
flex: 1,
},
contentUnfocused: {
opacity: 0.96,
},
+19 -7
View File
@@ -24,14 +24,20 @@ function useTerminalPanelDescriptor(
context: { serverId: string; workspaceId: string },
): PanelDescriptor {
const client = useSessionStore((state) => state.sessions[context.serverId]?.client ?? null);
const workspaceDirectory = useSessionStore(
(state) =>
state.sessions[context.serverId]?.workspaces.get(context.workspaceId)?.workspaceDirectory ??
state.sessions[context.serverId]?.workspaces.get(context.workspaceId)?.projectRootPath ??
null,
);
const terminalsQuery = useQuery({
queryKey: ["terminals", context.serverId, context.workspaceId] as const,
enabled: Boolean(client && context.workspaceId),
queryKey: ["terminals", context.serverId, workspaceDirectory] as const,
enabled: Boolean(client && workspaceDirectory),
queryFn: async (): Promise<ListTerminalsPayload> => {
if (!client) {
return { cwd: context.workspaceId, terminals: [], requestId: "missing-client" };
if (!client || !workspaceDirectory) {
return { cwd: workspaceDirectory ?? "", terminals: [], requestId: "missing-client" };
}
return client.listTerminals(context.workspaceId);
return client.listTerminals(workspaceDirectory);
},
staleTime: 5_000,
});
@@ -50,16 +56,22 @@ function useTerminalPanelDescriptor(
function TerminalPanel() {
const isFocused = useIsFocused();
const { serverId, workspaceId, target, isPaneFocused } = usePaneContext();
const workspaceDirectory = useSessionStore(
(state) =>
state.sessions[serverId]?.workspaces.get(workspaceId)?.workspaceDirectory ??
state.sessions[serverId]?.workspaces.get(workspaceId)?.projectRootPath ??
null,
);
invariant(target.kind === "terminal", "TerminalPanel requires terminal target");
if (!isFocused) {
if (!isFocused || !workspaceDirectory) {
return <View style={{ flex: 1 }} />;
}
return (
<TerminalPane
serverId={serverId}
cwd={workspaceId}
cwd={workspaceDirectory}
terminalId={target.terminalId}
isPaneFocused={isPaneFocused}
/>
@@ -26,6 +26,7 @@ import {
import { useAllAgentsList } from "@/hooks/use-all-agents-list";
import { useHosts } from "@/runtime/host-runtime";
import { buildBranchComboOptions, normalizeBranchOptionName } from "@/utils/branch-suggestions";
import { buildHostAgentDetailRoute } from "@/utils/host-routes";
import { shortenPath } from "@/utils/shorten-path";
import { collectAgentWorkingDirectorySuggestions } from "@/utils/agent-working-directory-suggestions";
import { buildWorkingDirectorySuggestions } from "@/utils/working-directory-suggestions";
@@ -49,6 +50,7 @@ import type {
AgentSessionConfig,
} from "@server/server/agent/agent-sdk-types";
import { AGENT_PROVIDER_DEFINITIONS } from "@server/server/agent/provider-manifest";
import { resolveHydratedWorkspaceId } from "@/utils/resolve-hydrated-workspace-id";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
import { useDesktopDragHandlers } from "@/utils/desktop-window";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
@@ -750,7 +752,7 @@ function DraftAgentScreenContent({
optimisticStreamItems,
draftAgent,
handleCreateFromInput,
} = useDraftAgentCreateFlow<Agent, { id: string; cwd: string }>({
} = useDraftAgentCreateFlow<Agent, { id: string; workspaceId: string | null }>({
draftId: draftIdRef.current,
getPendingServerId: () => selectedServerId,
validateBeforeSubmit: ({ text }) => {
@@ -908,20 +910,29 @@ function DraftAgentScreenContent({
const createdWorkingDir = typeof result.cwd === "string" ? result.cwd.trim() : "";
const configuredWorkingDir = config.cwd.trim();
const workspaceId = createdWorkingDir.length > 0 ? createdWorkingDir : configuredWorkingDir;
const workspaceId = resolveHydratedWorkspaceId({
workspaces: useSessionStore.getState().sessions[selectedServerId]?.workspaces?.values(),
path: createdWorkingDir.length > 0 ? createdWorkingDir : configuredWorkingDir,
});
return {
agentId: result.id,
result: {
id: result.id,
cwd: workspaceId,
workspaceId,
},
};
},
onCreateSuccess: ({ result }) => {
if (!result.workspaceId) {
router.replace(
buildHostAgentDetailRoute(selectedServerId as string, result.id) as any,
);
return;
}
const route = prepareWorkspaceTab({
serverId: selectedServerId as string,
workspaceId: result.cwd,
workspaceId: result.workspaceId,
target: { kind: "agent", agentId: result.id },
});
router.replace(route as any);
@@ -81,7 +81,7 @@ describe("workspace agent visibility", () => {
const result = deriveWorkspaceAgentVisibility({
sessionAgents,
workspaceId,
workspaceDirectory: workspaceId,
});
expect(result.activeAgentIds).toEqual(new Set(["visible-agent"]));
@@ -142,13 +142,33 @@ describe("workspace agent visibility", () => {
const result = deriveWorkspaceAgentVisibility({
sessionAgents,
workspaceId: "/Users/moboudra/.paseo/worktrees/1luy0po7/normal-squid",
workspaceDirectory: "/Users/moboudra/.paseo/worktrees/1luy0po7/normal-squid",
});
expect(result.activeAgentIds).toEqual(new Set(["slash-agent"]));
expect(result.knownAgentIds.has("slash-agent")).toBe(true);
});
it("matches workspace agents using the workspace directory even when the route uses a numeric workspace id", () => {
const sessionAgents = new Map<string, Agent>([
[
"terminal-agent",
makeAgent({
id: "terminal-agent",
cwd: "/tmp/workspace-lifecycle-main",
}),
],
]);
const result = deriveWorkspaceAgentVisibility({
sessionAgents,
workspaceDirectory: "/tmp/workspace-lifecycle-main",
});
expect(result.activeAgentIds).toEqual(new Set(["terminal-agent"]));
expect(result.knownAgentIds).toEqual(new Set(["terminal-agent"]));
});
describe("workspaceAgentVisibilityEqual", () => {
it("returns true for identical sets", () => {
const a = { activeAgentIds: new Set(["a", "b"]), knownAgentIds: new Set(["a", "b", "c"]) };
@@ -12,11 +12,11 @@ export interface WorkspaceAgentVisibility {
export function deriveWorkspaceAgentVisibility(input: {
sessionAgents: Map<string, Agent> | undefined;
workspaceId: string;
workspaceDirectory: string | null | undefined;
}): WorkspaceAgentVisibility {
const { sessionAgents, workspaceId } = input;
const normalizedWorkspaceId = normalizeWorkspaceId(workspaceId);
if (!sessionAgents || !workspaceId) {
const { sessionAgents, workspaceDirectory } = input;
const normalizedWorkspaceDirectory = normalizeWorkspaceId(workspaceDirectory);
if (!sessionAgents || !normalizedWorkspaceDirectory) {
return {
activeAgentIds: new Set<string>(),
knownAgentIds: new Set<string>(),
@@ -26,7 +26,7 @@ export function deriveWorkspaceAgentVisibility(input: {
const activeAgentIds = new Set<string>();
const knownAgentIds = new Set<string>();
for (const agent of sessionAgents.values()) {
if (normalizeWorkspaceId(agent.cwd) !== normalizedWorkspaceId) {
if (normalizeWorkspaceId(agent.cwd) !== normalizedWorkspaceDirectory) {
continue;
}
knownAgentIds.add(agent.id);
@@ -1,6 +1,7 @@
import { useCallback, useMemo, useRef } from "react";
import { Keyboard, Platform, ScrollView, Text, View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import invariant from "tiny-invariant";
import { Composer } from "@/components/composer";
import { FileDropZone } from "@/components/file-drop-zone";
import { AgentStreamView } from "@/components/agent-stream-view";
@@ -10,7 +11,7 @@ import { useDraftAgentCreateFlow } from "@/hooks/use-draft-agent-create-flow";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { buildWorkspaceDraftAgentConfig } from "@/screens/workspace/workspace-draft-agent-config";
import { buildDraftStoreKey } from "@/stores/draft-keys";
import type { Agent } from "@/stores/session-store";
import { type Agent, useSessionStore } from "@/stores/session-store";
import { encodeImages } from "@/utils/encode-images";
import { shouldAutoFocusWorkspaceDraftComposer } from "@/screens/workspace/workspace-draft-pane-focus";
import type { AgentCapabilityFlags } from "@server/server/agent/agent-sdk-types";
@@ -48,6 +49,11 @@ export function WorkspaceDraftAgentTab({
}: WorkspaceDraftAgentTabProps) {
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const workspaceDirectory = useSessionStore(
(state) =>
state.sessions[serverId]?.workspaces.get(workspaceId)?.workspaceDirectory ??
state.sessions[serverId]?.workspaces.get(workspaceId)?.projectRootPath,
);
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
const draftStoreKey = useMemo(
() =>
@@ -63,10 +69,10 @@ export function WorkspaceDraftAgentTab({
draftKey: draftStoreKey,
composer: {
initialServerId: serverId,
initialValues: { workingDir: workspaceId },
initialValues: { workingDir: workspaceDirectory },
isVisible: true,
onlineServerIds: isConnected ? [serverId] : [],
lockedWorkingDir: workspaceId,
lockedWorkingDir: workspaceDirectory,
},
},
);
@@ -97,6 +103,9 @@ export function WorkspaceDraftAgentTab({
if (!composerState.effectiveModelId) {
return "No model is available for the selected provider";
}
if (!workspaceDirectory) {
return "Workspace directory not found";
}
if (!client) {
return "Host is not connected";
}
@@ -110,6 +119,7 @@ export function WorkspaceDraftAgentTab({
Keyboard.dismiss();
},
buildDraftAgent: (attempt) => {
invariant(workspaceDirectory, "Workspace directory is required");
const now = attempt.timestamp;
const model = composerState.effectiveModelId || null;
const thinkingOptionId = composerState.effectiveThinkingOptionId || null;
@@ -134,20 +144,21 @@ export function WorkspaceDraftAgentTab({
persistence: null,
runtimeInfo: { provider: composerState.selectedProvider, sessionId: null, model, modeId },
title: "Agent",
cwd: workspaceId,
cwd: workspaceDirectory,
model,
thinkingOptionId,
labels: {},
};
},
createRequest: async ({ attempt, text, images }) => {
invariant(workspaceDirectory, "Workspace directory is required");
if (!client) {
throw new Error("Host is not connected");
}
const config = buildWorkspaceDraftAgentConfig({
provider: composerState.selectedProvider,
cwd: workspaceId,
cwd: workspaceDirectory,
...(composerState.modeOptions.length > 0 && composerState.selectedMode !== ""
? { modeId: composerState.selectedMode }
: {}),
@@ -1,5 +1,4 @@
import type { WorkspaceDescriptor } from "@/stores/session-store";
import { projectDisplayNameFromProjectId } from "@/utils/project-display-name";
export function resolveWorkspaceHeader(input: { workspace: WorkspaceDescriptor }): {
title: string;
@@ -7,7 +6,7 @@ export function resolveWorkspaceHeader(input: { workspace: WorkspaceDescriptor }
} {
return {
title: input.workspace.name,
subtitle: projectDisplayNameFromProjectId(input.workspace.projectId),
subtitle: input.workspace.projectDisplayName,
};
}
@@ -554,8 +554,24 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
const normalizedServerId = trimNonEmpty(decodeSegment(serverId)) ?? "";
const normalizedWorkspaceId =
const rawWorkspaceIdentifier =
normalizeWorkspaceIdentity(decodeWorkspaceIdFromPathSegment(workspaceId)) ?? "";
// Resolve the workspace ID: first try direct map key, then fall back to path-based lookup.
// This lets URLs that encode a filesystem path (e.g. from deep links or older bookmarks)
// resolve correctly even though the map is keyed by numeric DB IDs.
const normalizedWorkspaceId = useSessionStore((state) => {
if (!normalizedServerId || !rawWorkspaceIdentifier) return "";
const workspaces = state.sessions[normalizedServerId]?.workspaces;
if (!workspaces) return rawWorkspaceIdentifier;
if (workspaces.has(rawWorkspaceIdentifier)) return rawWorkspaceIdentifier;
for (const [id, ws] of workspaces.entries()) {
if (normalizeWorkspaceIdentity(ws.workspaceDirectory) === rawWorkspaceIdentifier) return id;
if (normalizeWorkspaceIdentity(ws.projectRootPath) === rawWorkspaceIdentifier) return id;
}
return rawWorkspaceIdentifier;
});
const workspaceTerminalScopeKey =
normalizedServerId && normalizedWorkspaceId
? `${normalizedServerId}:${normalizedWorkspaceId}`
@@ -567,43 +583,47 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
const queryClient = useQueryClient();
const client = useHostRuntimeClient(normalizedServerId);
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
const workspaceDescriptor = useSessionStore(
(state) => state.sessions[normalizedServerId]?.workspaces.get(normalizedWorkspaceId) ?? null,
);
const workspaceDirectory =
workspaceDescriptor?.workspaceDirectory ?? workspaceDescriptor?.projectRootPath ?? null;
const workspaceAgentVisibility = useStoreWithEqualityFn(
useSessionStore,
(state) =>
deriveWorkspaceAgentVisibility({
sessionAgents: state.sessions[normalizedServerId]?.agents,
workspaceId: normalizedWorkspaceId,
workspaceDirectory,
}),
workspaceAgentVisibilityEqual,
);
const terminalsQueryKey = useMemo(
() => ["terminals", normalizedServerId, normalizedWorkspaceId] as const,
[normalizedServerId, normalizedWorkspaceId],
() => ["terminals", normalizedServerId, workspaceDirectory] as const,
[normalizedServerId, workspaceDirectory],
);
type ListTerminalsPayload = ListTerminalsResponse["payload"];
const terminalsQuery = useQuery({
queryKey: terminalsQueryKey,
enabled:
Boolean(client && isConnected) &&
normalizedWorkspaceId.length > 0 &&
normalizedWorkspaceId.startsWith("/"),
Boolean(workspaceDirectory),
queryFn: async () => {
if (!client) {
if (!client || !workspaceDirectory) {
throw new Error("Host is not connected");
}
return await client.listTerminals(normalizedWorkspaceId);
return await client.listTerminals(workspaceDirectory);
},
staleTime: TERMINALS_QUERY_STALE_TIME,
});
const terminals = terminalsQuery.data?.terminals ?? [];
const createTerminalMutation = useMutation({
mutationFn: async (input?: { paneId?: string }) => {
if (!client) {
if (!client || !workspaceDirectory) {
throw new Error("Host is not connected");
}
return await client.createTerminal(normalizedWorkspaceId);
return await client.createTerminal(workspaceDirectory);
},
onSuccess: (payload, input) => {
const createdTerminal = payload.terminal;
@@ -613,8 +633,9 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
terminals: current?.terminals ?? [],
terminal: createdTerminal,
});
const cwd = current?.cwd ?? workspaceDirectory ?? undefined;
return {
cwd: current?.cwd ?? normalizedWorkspaceId,
...(cwd ? { cwd } : {}),
terminals: nextTerminals,
requestId: current?.requestId ?? `terminal-create-${createdTerminal.id}`,
};
@@ -657,7 +678,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
const { archiveAgent } = useArchiveAgent();
useEffect(() => {
if (!client || !isConnected || !normalizedWorkspaceId.startsWith("/")) {
if (!client || !isConnected || !workspaceDirectory) {
return;
}
@@ -665,7 +686,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
if (message.type !== "terminals_changed") {
return;
}
if (message.payload.cwd !== normalizedWorkspaceId) {
if (message.payload.cwd !== workspaceDirectory) {
return;
}
@@ -676,32 +697,27 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
}));
});
client.subscribeTerminals({ cwd: normalizedWorkspaceId });
client.subscribeTerminals({ cwd: workspaceDirectory });
return () => {
unsubscribeChanged();
client.unsubscribeTerminals({ cwd: normalizedWorkspaceId });
client.unsubscribeTerminals({ cwd: workspaceDirectory });
};
}, [client, isConnected, normalizedWorkspaceId, queryClient, terminalsQueryKey]);
}, [client, isConnected, queryClient, terminalsQueryKey, workspaceDirectory]);
const checkoutQuery = useQuery({
queryKey: checkoutStatusQueryKey(normalizedServerId, normalizedWorkspaceId),
queryKey: checkoutStatusQueryKey(normalizedServerId, workspaceDirectory ?? ""),
enabled:
Boolean(client && isConnected) &&
normalizedWorkspaceId.length > 0 &&
normalizedWorkspaceId.startsWith("/"),
Boolean(workspaceDirectory),
queryFn: async () => {
if (!client) {
if (!client || !workspaceDirectory) {
throw new Error("Host is not connected");
}
return (await client.getCheckoutStatus(normalizedWorkspaceId)) as CheckoutStatusPayload;
return (await client.getCheckoutStatus(workspaceDirectory)) as CheckoutStatusPayload;
},
staleTime: 15_000,
});
const workspaceDescriptor = useSessionStore(
(state) => state.sessions[normalizedServerId]?.workspaces.get(normalizedWorkspaceId) ?? null,
);
const hasHydratedWorkspaces = useSessionStore(
(state) => state.sessions[normalizedServerId]?.hasHydratedWorkspaces ?? false,
);
@@ -731,15 +747,15 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
const isExplorerOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen;
const activeExplorerCheckout = useMemo<ExplorerCheckoutContext | null>(() => {
if (!normalizedServerId || !normalizedWorkspaceId.startsWith("/")) {
if (!normalizedServerId || !workspaceDirectory) {
return null;
}
return {
serverId: normalizedServerId,
cwd: normalizedWorkspaceId,
cwd: workspaceDirectory,
isGit: isGitCheckout,
};
}, [isGitCheckout, normalizedServerId, normalizedWorkspaceId]);
}, [isGitCheckout, normalizedServerId, workspaceDirectory]);
useEffect(() => {
setActiveExplorerCheckout(activeExplorerCheckout);
@@ -1081,12 +1097,12 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
if (createTerminalMutation.isPending) {
return;
}
if (!normalizedWorkspaceId.startsWith("/")) {
if (!workspaceDirectory) {
return;
}
createTerminalMutation.mutate(input);
},
[createTerminalMutation, normalizedWorkspaceId],
[createTerminalMutation, workspaceDirectory],
);
const handleSelectSwitcherTab = useCallback(
@@ -1270,18 +1286,18 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
);
const handleCopyWorkspacePath = useCallback(async () => {
if (!normalizedWorkspaceId.startsWith("/")) {
if (!workspaceDirectory) {
toast.error("Workspace path not available");
return;
}
try {
await Clipboard.setStringAsync(normalizedWorkspaceId);
await Clipboard.setStringAsync(workspaceDirectory);
toast.copied("Workspace path");
} catch {
toast.error("Copy failed");
}
}, [normalizedWorkspaceId, toast]);
}, [toast, workspaceDirectory]);
const handleCopyBranchName = useCallback(async () => {
if (!currentBranchName) {
@@ -1927,7 +1943,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
<DropdownMenuItem
testID="workspace-header-copy-path"
leading={<Copy size={16} color={theme.colors.foregroundMuted} />}
disabled={!normalizedWorkspaceId.startsWith("/")}
disabled={!workspaceDirectory}
onSelect={handleCopyWorkspacePath}
>
Copy workspace path
@@ -1952,7 +1968,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
<>
<WorkspaceGitActions
serverId={normalizedServerId}
cwd={normalizedWorkspaceId}
cwd={workspaceDirectory ?? ""}
/>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger asChild>
@@ -2135,7 +2151,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
<ExplorerSidebar
serverId={normalizedServerId}
workspaceId={normalizedWorkspaceId}
workspaceRoot={normalizedWorkspaceId}
workspaceRoot={workspaceDirectory ?? ""}
isGit={isGitCheckout}
onOpenFile={handleOpenFileFromExplorer}
/>
@@ -13,6 +13,7 @@ describe("workspace source of truth consumption", () => {
projectId: "remote:github.com/getpaseo/paseo",
projectDisplayName: "getpaseo/paseo",
projectRootPath: "/repo/main",
workspaceDirectory: "/repo/main",
projectKind: "git",
workspaceKind: "checkout",
name: "feat/workspace-sot",
+2
View File
@@ -118,6 +118,7 @@ export interface WorkspaceDescriptor {
projectId: string;
projectDisplayName: string;
projectRootPath: string;
workspaceDirectory: string;
projectKind: WorkspaceDescriptorPayload["projectKind"];
workspaceKind: WorkspaceDescriptorPayload["workspaceKind"];
name: string;
@@ -135,6 +136,7 @@ export function normalizeWorkspaceDescriptor(
projectId: String(payload.projectId),
projectDisplayName: payload.projectDisplayName,
projectRootPath: payload.projectRootPath,
workspaceDirectory: payload.workspaceDirectory,
projectKind: payload.projectKind,
workspaceKind: payload.workspaceKind,
name: payload.name,
@@ -0,0 +1,26 @@
import type { WorkspaceDescriptor } from "@/stores/session-store";
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
export function resolveHydratedWorkspaceId(input: {
workspaces: Iterable<WorkspaceDescriptor> | null | undefined;
path: string | null | undefined;
}): string | null {
const normalizedPath = normalizeWorkspaceIdentity(input.path);
if (!normalizedPath) {
return null;
}
for (const workspace of input.workspaces ?? []) {
if (normalizeWorkspaceIdentity(workspace.id) === normalizedPath) {
return workspace.id;
}
if (normalizeWorkspaceIdentity(workspace.workspaceDirectory) === normalizedPath) {
return workspace.id;
}
if (normalizeWorkspaceIdentity(workspace.projectRootPath) === normalizedPath) {
return workspace.id;
}
}
return null;
}
@@ -13,6 +13,7 @@ function workspace(
projectId: input.projectId ?? "project-1",
projectDisplayName: input.projectDisplayName ?? "Project",
projectRootPath: input.projectRootPath ?? "/repo",
workspaceDirectory: input.workspaceDirectory ?? input.projectRootPath ?? "/repo",
projectKind: input.projectKind ?? "git",
workspaceKind: input.workspaceKind ?? "worktree",
name: input.name ?? input.id,