WIP: snapshot workspace execution refactor baseline
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user