diff --git a/packages/app/src/components/command-center.tsx b/packages/app/src/components/command-center.tsx
index 59b18ddb0..0947ff176 100644
--- a/packages/app/src/components/command-center.tsx
+++ b/packages/app/src/components/command-center.tsx
@@ -8,7 +8,7 @@ import {
type PressableStateCallbackType,
} from "react-native";
import { memo, useCallback, useEffect, useMemo, useRef, type ReactNode } from "react";
-import { Plus, Settings } from "lucide-react-native";
+import { Home, Plus, Settings } from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useCommandCenter } from "@/hooks/use-command-center";
import type { AggregatedAgent } from "@/hooks/use-aggregated-agents";
@@ -106,6 +106,8 @@ function CommandCenterActionRow({
actionIcon = ;
} else if (action.icon === "settings") {
actionIcon = ;
+ } else if (action.icon === "home") {
+ actionIcon = ;
}
const titleStyle = useMemo(
() => [styles.title, { color: theme.colors.foreground }],
diff --git a/packages/app/src/screens/workspace/workspace-import-sheet.test.tsx b/packages/app/src/components/import-session-sheet.test.tsx
similarity index 81%
rename from packages/app/src/screens/workspace/workspace-import-sheet.test.tsx
rename to packages/app/src/components/import-session-sheet.test.tsx
index f87e09337..33cfa0b79 100644
--- a/packages/app/src/screens/workspace/workspace-import-sheet.test.tsx
+++ b/packages/app/src/components/import-session-sheet.test.tsx
@@ -7,7 +7,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { DaemonClient, FetchRecentProviderSessionEntry } from "@server/client/daemon-client";
import type { ProviderSnapshotEntry } from "@server/server/agent/agent-sdk-types";
import { afterEach, describe, expect, it, vi } from "vitest";
-import { WorkspaceImportSheet } from "@/screens/workspace/workspace-import-sheet";
+import { ImportSessionSheet } from "@/components/import-session-sheet";
const { theme } = vi.hoisted(() => ({
theme: {
@@ -69,12 +69,13 @@ vi.mock("lucide-react-native", () => {
};
return {
Inbox: icon("Inbox"),
+ RotateCw: icon("RotateCw"),
};
});
vi.mock("@/components/ui/loading-spinner", () => ({
LoadingSpinner: () =>
- React.createElement("span", { "data-testid": "workspace-import-loading-spinner" }),
+ React.createElement("span", { "data-testid": "import-session-loading-spinner" }),
}));
vi.mock("@/components/ui/segmented-control", () => ({
@@ -111,18 +112,19 @@ vi.mock("@/components/ui/segmented-control", () => ({
vi.mock("@/components/adaptive-modal-sheet", () => ({
AdaptiveModalSheet: ({
visible,
- title,
+ header,
children,
testID,
}: {
visible: boolean;
- title: string;
+ header?: { title: string; actions?: ReactNode };
children: ReactNode;
testID?: string;
}) =>
visible ? (
- {title}
+ {header?.title}
+ {header?.actions}
{children}
) : null,
@@ -157,6 +159,8 @@ interface RenderOptions {
visible?: boolean;
onClose?: () => void;
onImportedAgent?: (agentId: string) => void;
+ onImported?: (agent: Awaited>) => void;
+ cwd?: string | null;
snapshot?: {
entries?: ProviderSnapshotEntry[];
supportsSnapshot?: boolean;
@@ -179,15 +183,18 @@ function renderSheet(
},
});
+ const cwd = options && "cwd" in options ? (options.cwd ?? undefined) : "/repo/paseo";
+
return render(
-
,
);
@@ -265,7 +272,7 @@ function createSnapshotEntry(
};
}
-describe("WorkspaceImportSheet", () => {
+describe("ImportSessionSheet", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
@@ -431,11 +438,11 @@ describe("WorkspaceImportSheet", () => {
function TestSheet({ visible }: { visible: boolean }) {
return (
-
@@ -483,7 +490,7 @@ describe("WorkspaceImportSheet", () => {
},
);
- fireEvent.click(await screen.findByTestId("workspace-import-session-claude-provider-thread-1"));
+ fireEvent.click(await screen.findByTestId("import-session-session-claude-provider-thread-1"));
await waitFor(() => {
expect(importAgent).toHaveBeenCalledWith({
@@ -519,7 +526,7 @@ describe("WorkspaceImportSheet", () => {
},
);
- fireEvent.click(await screen.findByTestId("workspace-import-session-claude-provider-thread-1"));
+ fireEvent.click(await screen.findByTestId("import-session-session-claude-provider-thread-1"));
await screen.findByText("Could not import selected session.");
expect(importAgent).toHaveBeenCalledWith({
@@ -665,12 +672,12 @@ describe("WorkspaceImportSheet", () => {
await screen.findByText("Session claude");
await screen.findByText("Session codex");
- fireEvent.click(screen.getByTestId("workspace-import-filter-codex"));
+ fireEvent.click(screen.getByTestId("import-session-filter-codex"));
screen.getByText("Session codex");
expect(screen.queryByText("Session claude")).toBeNull();
- fireEvent.click(screen.getByTestId("workspace-import-filter-all"));
+ fireEvent.click(screen.getByTestId("import-session-filter-all"));
screen.getByText("Session claude");
screen.getByText("Session codex");
@@ -702,8 +709,8 @@ describe("WorkspaceImportSheet", () => {
await waitFor(() => {
expect(fetchRecentProviderSessions).toHaveBeenCalled();
});
- expect(screen.queryByTestId("workspace-import-filters")).toBeNull();
- expect(screen.queryByTestId("workspace-import-filter-all")).toBeNull();
+ expect(screen.queryByTestId("import-session-filters")).toBeNull();
+ expect(screen.queryByTestId("import-session-filter-all")).toBeNull();
});
it("shows a no-importable-providers message when snapshot has no enabled importable providers", async () => {
@@ -731,4 +738,119 @@ describe("WorkspaceImportSheet", () => {
await screen.findByText("No importable providers are enabled.");
expect(fetchRecentProviderSessions).not.toHaveBeenCalled();
});
+
+ it("omits cwd from fetch and renders the session cwd on each row when cwd is unset", async () => {
+ const fetchRecentProviderSessions = vi.fn(async () => ({
+ requestId: "recent-provider-sessions",
+ entries: [
+ createProviderSessionEntry({
+ providerId: "claude",
+ providerLabel: "Claude Code",
+ cwd: "/home/me/work/other-project",
+ title: "Cross-project session",
+ }),
+ ],
+ }));
+ const importAgent = vi.fn();
+
+ renderSheet(
+ { fetchRecentProviderSessions, importAgent } as Pick<
+ DaemonClient,
+ "fetchRecentProviderSessions" | "importAgent"
+ >,
+ {
+ cwd: null,
+ snapshot: { supportsSnapshot: true, entries: [createSnapshotEntry("claude")] },
+ },
+ );
+
+ await waitFor(() => {
+ expect(fetchRecentProviderSessions).toHaveBeenCalledWith({
+ providers: ["claude"],
+ limit: 15,
+ });
+ });
+ expect(fetchRecentProviderSessions).not.toHaveBeenCalledWith(
+ expect.objectContaining({ cwd: expect.anything() }),
+ );
+ await screen.findByText("/home/me/work/other-project");
+ });
+
+ it("uses the session's cwd when importing in cwd-less mode and fires onImported", async () => {
+ const fetchRecentProviderSessions = vi.fn(async () => ({
+ requestId: "recent-provider-sessions",
+ entries: [
+ createProviderSessionEntry({
+ providerId: "claude",
+ providerLabel: "Claude Code",
+ cwd: "/home/me/work/other-project",
+ }),
+ ],
+ }));
+ const importAgent = vi.fn(async () => createImportedAgentSnapshot("agent-imported"));
+ const onImported = vi.fn();
+ const onImportedAgent = vi.fn();
+ const onClose = vi.fn();
+
+ renderSheet(
+ { fetchRecentProviderSessions, importAgent } as Pick<
+ DaemonClient,
+ "fetchRecentProviderSessions" | "importAgent"
+ >,
+ {
+ cwd: null,
+ onClose,
+ onImported,
+ onImportedAgent,
+ snapshot: { supportsSnapshot: true, entries: [createSnapshotEntry("claude")] },
+ },
+ );
+
+ fireEvent.click(await screen.findByTestId("import-session-session-claude-provider-thread-1"));
+
+ await waitFor(() => {
+ expect(importAgent).toHaveBeenCalledWith({
+ providerId: "claude",
+ providerHandleId: "provider-thread-1",
+ cwd: "/home/me/work/other-project",
+ });
+ });
+ expect(onImported).toHaveBeenCalledTimes(1);
+ expect(onImported).toHaveBeenCalledWith(expect.objectContaining({ id: "agent-imported" }));
+ expect(onImportedAgent).toHaveBeenCalledWith("agent-imported");
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+
+ it("refetches sessions when the refresh button is clicked", async () => {
+ const fetchRecentProviderSessions = vi.fn(async () => ({
+ requestId: "recent-provider-sessions",
+ entries: [
+ createProviderSessionEntry({
+ providerId: "claude",
+ providerLabel: "Claude Code",
+ title: "Refreshable session",
+ }),
+ ],
+ }));
+ const importAgent = vi.fn();
+
+ renderSheet(
+ { fetchRecentProviderSessions, importAgent } as Pick<
+ DaemonClient,
+ "fetchRecentProviderSessions" | "importAgent"
+ >,
+ {
+ snapshot: { supportsSnapshot: true, entries: [createSnapshotEntry("claude")] },
+ },
+ );
+
+ await screen.findByText("Refreshable session");
+ expect(fetchRecentProviderSessions).toHaveBeenCalledTimes(1);
+
+ fireEvent.click(screen.getByTestId("import-session-refresh"));
+
+ await waitFor(() => {
+ expect(fetchRecentProviderSessions).toHaveBeenCalledTimes(2);
+ });
+ });
});
diff --git a/packages/app/src/screens/workspace/workspace-import-sheet.tsx b/packages/app/src/components/import-session-sheet.tsx
similarity index 84%
rename from packages/app/src/screens/workspace/workspace-import-sheet.tsx
rename to packages/app/src/components/import-session-sheet.tsx
index 6797aca3c..b82959199 100644
--- a/packages/app/src/screens/workspace/workspace-import-sheet.tsx
+++ b/packages/app/src/components/import-session-sheet.tsx
@@ -4,7 +4,7 @@ import { useMutation, useQueries, useQueryClient } from "@tanstack/react-query";
import type { DaemonClient, FetchRecentProviderSessionEntry } from "@server/client/daemon-client";
import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
import { IMPORTABLE_PROVIDERS } from "@server/shared/importable-providers";
-import { Inbox } from "lucide-react-native";
+import { Inbox, RotateCw } from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
@@ -14,7 +14,6 @@ import { formatTimeAgo } from "@/utils/time";
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
const IMPORTABLE_PROVIDER_IDS: Set = new Set(IMPORTABLE_PROVIDERS);
-const IMPORT_SESSION_HEADER: SheetHeader = { title: "Import session" };
const PER_PROVIDER_LIMIT = 15;
const IMPORT_SHEET_SNAP_POINTS = ["70%", "92%"];
const DISABLED_ACCESSIBILITY_STATE = { disabled: true };
@@ -25,13 +24,16 @@ type RecentProviderSessionsClient = Pick<
"fetchRecentProviderSessions" | "importAgent"
>;
-interface WorkspaceImportSheetProps {
+type ImportedAgent = Awaited>;
+
+interface ImportSessionSheetProps {
visible: boolean;
client: RecentProviderSessionsClient | null;
serverId: string | null;
- workspaceDirectory: string | null;
+ cwd?: string | null;
onClose: () => void;
- onImportedAgent: (agentId: string) => void;
+ onImportedAgent?: (agentId: string) => void;
+ onImported?: (agent: ImportedAgent) => void;
}
type RecentSessionsResponse = Awaited<
@@ -84,20 +86,20 @@ function buildSessionsQueriesConfig(args: {
sessionsQueryRoot: ReadonlyArray;
visible: boolean;
client: RecentProviderSessionsClient | null;
- workspaceDirectory: string | null;
+ cwd: string | null | undefined;
}): SessionsQueryConfig[] {
- const { providersToFetch, sessionsQueryRoot, visible, client, workspaceDirectory } = args;
+ const { providersToFetch, sessionsQueryRoot, visible, client, cwd } = args;
if (providersToFetch === null) return [];
- const enabled = visible && Boolean(client && workspaceDirectory);
+ const enabled = visible && Boolean(client);
return providersToFetch.map((provider) => ({
queryKey: [...sessionsQueryRoot, provider],
enabled,
queryFn: async () => {
- if (!client || !workspaceDirectory) {
+ if (!client) {
throw new Error("Host is not connected");
}
return await client.fetchRecentProviderSessions({
- cwd: workspaceDirectory,
+ ...(cwd ? { cwd } : {}),
providers: [provider],
limit: PER_PROVIDER_LIMIT,
});
@@ -186,7 +188,7 @@ function SheetStatusMessages({
}: SheetStatusMessagesProps) {
const { theme } = useUnistyles();
if (!isClientReady) {
- return Connect to a workspace to import sessions;
+ return Connect to a host to import sessions;
}
if (isSnapshotUnsupported) {
return Update the host to import sessions.;
@@ -253,10 +255,39 @@ function computeEmptyState(input: EmptyStateInputs): {
return { showEmptyState, emptyStateTitle: "No recent sessions to import." };
}
+function RefreshAction({ isRefreshing, onPress }: { isRefreshing: boolean; onPress: () => void }) {
+ const { theme } = useUnistyles();
+ const pressableStyle = useCallback(
+ ({ pressed }: PressableStateCallbackType) => [
+ styles.refreshButton,
+ pressed && styles.refreshButtonPressed,
+ ],
+ [],
+ );
+ return (
+
+
+ {isRefreshing ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
function SheetEmptyState({ title }: { title: string }) {
const { theme } = useUnistyles();
return (
-
+
@@ -270,29 +301,31 @@ function buildProviderFilterOptions(
providerLabelById: ReadonlyMap,
): SegmentedControlOption[] {
const options: SegmentedControlOption[] = [
- { value: ALL_FILTER_VALUE, label: "All", testID: "workspace-import-filter-all" },
+ { value: ALL_FILTER_VALUE, label: "All", testID: "import-session-filter-all" },
];
for (const provider of providers) {
const ProviderIcon = getProviderIcon(provider);
options.push({
value: provider,
label: providerLabelById.get(provider) ?? provider,
- testID: `workspace-import-filter-${provider}`,
+ testID: `import-session-filter-${provider}`,
icon: ({ color, size }) => ,
});
}
return options;
}
-function WorkspaceImportSheetRow({
+function ImportSessionSheetRow({
entry,
disabled,
importing,
+ showCwd,
onImportSession,
}: {
entry: FetchRecentProviderSessionEntry;
disabled: boolean;
importing: boolean;
+ showCwd: boolean;
onImportSession: (entry: FetchRecentProviderSessionEntry) => void;
}) {
const { theme } = useUnistyles();
@@ -323,7 +356,7 @@ function WorkspaceImportSheetRow({
accessibilityRole="button"
accessibilityState={accessibilityState}
style={pressableStyle}
- testID={`workspace-import-session-${entry.providerId}-${entry.providerHandleId}`}
+ testID={`import-session-session-${entry.providerId}-${entry.providerHandleId}`}
>
@@ -338,19 +371,25 @@ function WorkspaceImportSheetRow({
{promptPreview}
+ {showCwd && entry.cwd ? (
+
+ {entry.cwd}
+
+ ) : null}
);
}
-export function WorkspaceImportSheet({
+export function ImportSessionSheet({
visible,
client,
serverId,
- workspaceDirectory,
+ cwd,
onClose,
onImportedAgent,
-}: WorkspaceImportSheetProps) {
+ onImported,
+}: ImportSessionSheetProps) {
const queryClient = useQueryClient();
const { entries: snapshotEntries, supportsSnapshot } = useProvidersSnapshot(serverId, {
@@ -368,8 +407,8 @@ export function WorkspaceImportSheet({
);
const sessionsQueryRoot = useMemo(
- () => ["recent-provider-sessions", workspaceDirectory] as const,
- [workspaceDirectory],
+ () => ["recent-provider-sessions", cwd ?? null] as const,
+ [cwd],
);
const queriesConfig = useMemo(
@@ -379,9 +418,9 @@ export function WorkspaceImportSheet({
sessionsQueryRoot,
visible,
client,
- workspaceDirectory,
+ cwd,
}),
- [providersToFetch, sessionsQueryRoot, visible, client, workspaceDirectory],
+ [providersToFetch, sessionsQueryRoot, visible, client, cwd],
);
const queries = useQueries({ queries: queriesConfig });
@@ -417,20 +456,25 @@ export function WorkspaceImportSheet({
const importMutation = useMutation({
mutationFn: async (entry: FetchRecentProviderSessionEntry) => {
- if (!client || !workspaceDirectory) {
+ if (!client) {
throw new Error("Host is not connected");
}
+ const effectiveCwd = cwd ?? entry.cwd;
+ if (!effectiveCwd) {
+ throw new Error("Session is missing a working directory");
+ }
const agent = await client.importAgent({
providerId: entry.providerId,
providerHandleId: entry.providerHandleId,
- cwd: workspaceDirectory,
+ cwd: effectiveCwd,
});
return agent;
},
onSuccess: async (agent) => {
await queryClient.invalidateQueries({ queryKey: sessionsQueryRoot });
onClose();
- onImportedAgent(agent.id);
+ onImportedAgent?.(agent.id);
+ onImported?.(agent);
},
});
@@ -451,6 +495,20 @@ export function WorkspaceImportSheet({
[queries, providersToFetch, providerLabelById],
);
+ const isRefreshing = queries.some((query) => query.isFetching);
+
+ const handleRefresh = useCallback(() => {
+ void queryClient.invalidateQueries({ queryKey: sessionsQueryRoot });
+ }, [queryClient, sessionsQueryRoot]);
+
+ const header = useMemo(
+ () => ({
+ title: "Import session",
+ actions: ,
+ }),
+ [isRefreshing, handleRefresh],
+ );
+
const isSnapshotUnsupported = !supportsSnapshot;
const isWaitingForSnapshot = supportsSnapshot && snapshotEntries === undefined;
const hasNoImportableProviders = providersToFetch !== null && providersToFetch.length === 0;
@@ -478,8 +536,8 @@ export function WorkspaceImportSheet({
@@ -490,7 +548,7 @@ export function WorkspaceImportSheet({
contentContainerStyle={styles.filterRow}
>
) : null}
0 ? (
{visibleEntries.map((entry) => (
-
))}
@@ -580,6 +639,10 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm,
lineHeight: 20,
},
+ rowCwd: {
+ color: theme.colors.foregroundMuted,
+ fontSize: theme.fontSize.xs,
+ },
statusRow: {
flexDirection: "row",
alignItems: "center",
@@ -606,4 +669,18 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.base,
textAlign: "center",
},
+ refreshButton: {
+ padding: theme.spacing[2],
+ marginRight: theme.spacing[1],
+ borderRadius: theme.borderRadius.lg,
+ },
+ refreshButtonPressed: {
+ backgroundColor: theme.colors.surface2,
+ },
+ refreshIconSlot: {
+ width: 16,
+ height: 16,
+ alignItems: "center",
+ justifyContent: "center",
+ },
}));
diff --git a/packages/app/src/hooks/use-command-center.ts b/packages/app/src/hooks/use-command-center.ts
index 3b1a0ac9d..69706da70 100644
--- a/packages/app/src/hooks/use-command-center.ts
+++ b/packages/app/src/hooks/use-command-center.ts
@@ -10,7 +10,7 @@ import {
clearCommandCenterFocusRestoreElement,
takeCommandCenterFocusRestoreElement,
} from "@/utils/command-center-focus-restore";
-import { buildSettingsRoute } from "@/utils/host-routes";
+import { buildHostOpenProjectRoute, buildSettingsRoute } from "@/utils/host-routes";
import type { ShortcutKey } from "@/utils/format-shortcut";
import { chordStringToShortcutKeys } from "@/keyboard/shortcut-string";
import { getBindingIdForAction, getDefaultKeysForAction } from "@/keyboard/keyboard-shortcuts";
@@ -52,10 +52,10 @@ function sortAgents(left: AggregatedAgent, right: AggregatedAgent): number {
interface CommandCenterActionDefinition {
id: string;
title: string;
- icon?: "plus" | "settings";
+ icon?: "plus" | "settings" | "home";
actionId?: string;
keywords: string[];
- routeKind: "settings" | "none";
+ routeKind: "settings" | "home" | "none";
}
const COMMAND_CENTER_ACTIONS: readonly CommandCenterActionDefinition[] = [
@@ -67,6 +67,13 @@ const COMMAND_CENTER_ACTIONS: readonly CommandCenterActionDefinition[] = [
keywords: ["open", "project", "folder", "workspace", "repo"],
routeKind: "none",
},
+ {
+ id: "home",
+ title: "Home",
+ icon: "home",
+ keywords: ["home", "start", "import", "session", "pair", "device", "providers"],
+ routeKind: "home",
+ },
{
id: "settings",
title: "Settings",
@@ -89,7 +96,7 @@ export interface CommandCenterActionItem {
kind: "action";
id: string;
title: string;
- icon?: "plus" | "settings";
+ icon?: "plus" | "settings" | "home";
route?: Href;
shortcutKeys?: ShortcutKey[][];
}
@@ -155,21 +162,32 @@ export function useCommandCenter() {
return buildSettingsRoute();
}, []);
+ const homeRoute = useMemo(() => {
+ if (!routeActiveServerId) return undefined;
+ return buildHostOpenProjectRoute(routeActiveServerId) as Href;
+ }, [routeActiveServerId]);
+
const actionItems = useMemo(() => {
if (!open) {
return EMPTY_ACTION_ITEMS;
}
- return COMMAND_CENTER_ACTIONS.filter((action) =>
- matchesActionQuery(query, action),
- ).map((action) => ({
- kind: "action",
- id: action.id,
- title: action.title,
- icon: action.icon,
- route: action.routeKind === "settings" ? settingsRoute : undefined,
- shortcutKeys: resolveActionShortcutKeys(action.actionId, overrides),
- }));
- }, [open, query, settingsRoute, overrides]);
+ return COMMAND_CENTER_ACTIONS.filter((action) => {
+ if (action.routeKind === "home" && !homeRoute) return false;
+ return matchesActionQuery(query, action);
+ }).map((action) => {
+ let route: Href | undefined;
+ if (action.routeKind === "settings") route = settingsRoute;
+ else if (action.routeKind === "home") route = homeRoute;
+ return {
+ kind: "action",
+ id: action.id,
+ title: action.title,
+ icon: action.icon,
+ route,
+ shortcutKeys: resolveActionShortcutKeys(action.actionId, overrides),
+ };
+ });
+ }, [open, query, settingsRoute, homeRoute, overrides]);
const items = useMemo(() => {
if (!open) {
diff --git a/packages/app/src/screens/open-project-screen.tsx b/packages/app/src/screens/open-project-screen.tsx
index 13c4ba777..e23baffcd 100644
--- a/packages/app/src/screens/open-project-screen.tsx
+++ b/packages/app/src/screens/open-project-screen.tsx
@@ -1,14 +1,12 @@
-import { useCallback, useEffect, useState } from "react";
-import { View, Text } from "react-native";
-import { StyleSheet } from "react-native-unistyles";
-import { FolderOpen, Smartphone } from "lucide-react-native";
+import { useCallback, useEffect, useState, type ComponentType } from "react";
+import { View, Text, Pressable } from "react-native";
+import { StyleSheet, useUnistyles } from "react-native-unistyles";
+import { useRouter } from "expo-router";
+import { FolderOpen, Inbox, Plug, Smartphone } from "lucide-react-native";
import { PaseoLogo } from "@/components/icons/paseo-logo";
-import { Button } from "@/components/ui/button";
import { MenuHeader } from "@/components/headers/menu-header";
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
import { usePanelStore } from "@/stores/panel-store";
-import { useSessionStore } from "@/stores/session-store";
-import { useHasWorkspaces } from "@/stores/session-store-hooks";
import {
useIsCompactFormFactor,
HEADER_INNER_HEIGHT,
@@ -18,14 +16,21 @@ import {
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon";
import { PairDeviceModal } from "@/desktop/components/pair-device-modal";
+import { buildHostAgentDetailRoute, buildSettingsHostRoute } from "@/utils/host-routes";
+import { ImportSessionSheet } from "@/components/import-session-sheet";
+import { useHostRuntimeClient } from "@/runtime/host-runtime";
+import { useOpenProject } from "@/hooks/use-open-project";
+import type { Href } from "expo-router";
export function OpenProjectScreen({ serverId }: { serverId: string }) {
+ const router = useRouter();
const openDesktopAgentList = usePanelStore((s) => s.openDesktopAgentList);
const openProjectPicker = useOpenProjectPicker(serverId);
- const hasHydrated = useSessionStore((s) => s.sessions[serverId]?.hasHydratedWorkspaces ?? false);
- const hasProjects = useHasWorkspaces(serverId);
const isLocalDaemon = useIsLocalDaemon(serverId);
+ const client = useHostRuntimeClient(serverId);
+ const openProject = useOpenProject(serverId);
const [isPairDeviceOpen, setIsPairDeviceOpen] = useState(false);
+ const [isImportSheetOpen, setIsImportSheetOpen] = useState(false);
const isCompactLayout = useIsCompactFormFactor();
@@ -42,40 +47,62 @@ export function OpenProjectScreen({ serverId }: { serverId: string }) {
const handleOpenPairDevice = useCallback(() => setIsPairDeviceOpen(true), []);
const handleClosePairDevice = useCallback(() => setIsPairDeviceOpen(false), []);
+ const handleOpenImportSession = useCallback(() => setIsImportSheetOpen(true), []);
+ const handleCloseImportSession = useCallback(() => setIsImportSheetOpen(false), []);
+
+ const handleImported = useCallback(
+ (agent: { id: string; cwd: string }) => {
+ void (async () => {
+ await openProject(agent.cwd);
+ router.push(buildHostAgentDetailRoute(serverId, agent.id) as Href);
+ })();
+ },
+ [openProject, router, serverId],
+ );
+
+ const handleOpenProviders = useCallback(() => {
+ router.push(buildSettingsHostRoute(serverId));
+ }, [router, serverId]);
+
return (
-
+
-
- What shall we build today?
- {hasHydrated && !hasProjects ? (
-
- Add a project folder to start running agents on your codebase
-
- ) : null}
-
-
-
+ accent
+ />
+
+
{isLocalDaemon ? (
-
+ />
) : null}
@@ -84,10 +111,61 @@ export function OpenProjectScreen({ serverId }: { serverId: string }) {
onClose={handleClosePairDevice}
testID="open-project-pair-device-modal"
/>
+
);
}
+interface HomeTileProps {
+ icon: ComponentType<{ size: number; color: string }>;
+ title: string;
+ description: string;
+ onPress: () => void;
+ testID?: string;
+ accent?: boolean;
+}
+
+function HomeTile({ icon: Icon, title, description, onPress, testID, accent }: HomeTileProps) {
+ // useUnistyles is acceptable here: leaf component, off the hot path (home screen renders once).
+ const { theme } = useUnistyles();
+ const [hovered, setHovered] = useState(false);
+ const handleHoverIn = useCallback(() => setHovered(true), []);
+ const handleHoverOut = useCallback(() => setHovered(false), []);
+
+ const iconColor = accent ? theme.colors.accent : theme.colors.foregroundMuted;
+
+ const pressableStyle = useCallback(
+ ({ pressed }: { pressed: boolean }) => [
+ styles.tile,
+ hovered && styles.tileHovered,
+ pressed && styles.tilePressed,
+ ],
+ [hovered],
+ );
+
+ return (
+
+
+
+ {title}
+ {description}
+
+
+ );
+}
+
const styles = StyleSheet.create((theme) => ({
container: {
flex: 1,
@@ -97,10 +175,11 @@ const styles = StyleSheet.create((theme) => ({
content: {
position: "relative",
flex: 1,
- justifyContent: "center",
+ justifyContent: { xs: "flex-start", md: "center" },
alignItems: "center",
gap: 0,
padding: theme.spacing[6],
+ paddingTop: { xs: theme.spacing[12], md: theme.spacing[6] },
paddingBottom: {
xs: HEADER_INNER_HEIGHT_MOBILE + HEADER_TOP_PADDING_MOBILE + theme.spacing[6],
md: HEADER_INNER_HEIGHT + theme.spacing[6],
@@ -109,25 +188,43 @@ const styles = StyleSheet.create((theme) => ({
logo: {
marginBottom: theme.spacing[8],
},
- headingGroup: {
- alignItems: "center",
+ tiles: {
+ marginTop: { xs: theme.spacing[6], md: theme.spacing[12] },
+ width: "100%",
+ maxWidth: 452,
+ flexDirection: "row",
+ flexWrap: "wrap",
+ justifyContent: "flex-start",
gap: theme.spacing[3],
},
- cta: {
- marginTop: theme.spacing[12],
- flexDirection: "row",
- alignItems: "center",
- gap: theme.spacing[2],
+ tile: {
+ width: { xs: "100%", md: 220 },
+ minHeight: { xs: 0, md: 132 },
+ padding: theme.spacing[4],
+ backgroundColor: theme.colors.surface1,
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ borderRadius: theme.borderRadius.xl,
+ gap: theme.spacing[3],
},
- heading: {
+ tileHovered: {
+ backgroundColor: theme.colors.surface2,
+ borderColor: theme.colors.borderAccent,
+ },
+ tilePressed: {
+ opacity: 0.85,
+ },
+ tileText: {
+ gap: theme.spacing[1],
+ },
+ tileTitle: {
color: theme.colors.foreground,
- fontSize: theme.fontSize["2xl"],
- fontWeight: theme.fontWeight.normal,
- textAlign: "center",
- },
- subtitle: {
- color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.base,
- textAlign: "center",
+ fontWeight: theme.fontWeight.normal,
+ },
+ tileDescription: {
+ color: theme.colors.foregroundMuted,
+ fontSize: theme.fontSize.sm,
+ lineHeight: 18,
},
}));
diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx
index 33fe3b300..fa3ab1333 100644
--- a/packages/app/src/screens/workspace/workspace-screen.tsx
+++ b/packages/app/src/screens/workspace/workspace-screen.tsx
@@ -59,7 +59,7 @@ import { SourceControlPanelIcon } from "@/components/icons/source-control-panel-
import { WorkspaceGitActions } from "@/git/workspace-actions";
import { WorkspaceOpenInEditorButton } from "@/screens/workspace/workspace-open-in-editor-button";
import { WorkspaceScriptsButton } from "@/screens/workspace/workspace-scripts-button";
-import { WorkspaceImportSheet } from "@/screens/workspace/workspace-import-sheet";
+import { ImportSessionSheet } from "@/components/import-session-sheet";
import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context";
import { useToast } from "@/contexts/toast-context";
import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture";
@@ -3358,11 +3358,11 @@ function WorkspaceScreenContent({
/>
) : null}
-
diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts
index a1eca86b8..c7f41183c 100644
--- a/packages/server/src/server/session.ts
+++ b/packages/server/src/server/session.ts
@@ -3389,6 +3389,7 @@ export class Session {
paseoHome: this.paseoHome,
logger: this.sessionLogger,
});
+ await this.registerWorkspaceForImportedAgent(snapshot.cwd);
await this.forwardAgentUpdate(snapshot);
const agentPayload = await this.buildAgentPayload(snapshot);
this.emit({
@@ -7143,6 +7144,20 @@ export class Session {
}
}
+ private async registerWorkspaceForImportedAgent(cwd: string): Promise {
+ try {
+ const workspace = await this.findOrCreateWorkspaceForDirectory(cwd);
+ await this.syncWorkspaceGitObserverForWorkspace(workspace);
+ await this.describeWorkspaceRecord(workspace);
+ await this.emitWorkspaceUpdateForCwd(workspace.cwd);
+ } catch (error) {
+ this.sessionLogger.warn(
+ { err: error, cwd },
+ "Failed to register workspace for imported agent",
+ );
+ }
+ }
+
private async handleOpenProjectRequest(
request: Extract,
): Promise {
diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts
index 05a4ae0eb..902bddd07 100644
--- a/packages/server/src/server/session.workspaces.test.ts
+++ b/packages/server/src/server/session.workspaces.test.ts
@@ -66,6 +66,20 @@ interface SessionTestAccess {
agentManager: {
listAgents(): unknown[];
listImportablePersistedAgents(options?: unknown): Promise;
+ findPersistedAgent(
+ provider: string,
+ providerHandleId: string,
+ options?: unknown,
+ ): Promise;
+ resumeAgentFromPersistence(
+ handle: unknown,
+ overrides?: unknown,
+ preferredId?: string,
+ extras?: unknown,
+ ): Promise;
+ hydrateTimelineFromProvider(agentId: string): Promise;
+ getTimeline(agentId: string): readonly unknown[];
+ setTitle(agentId: string, title: string): Promise;
};
workspaceRegistry: {
list(...args: unknown[]): Promise;
@@ -2660,6 +2674,105 @@ test("open_project_request registers a workspace before any agent exists", async
expect(response?.payload.workspace?.id).toBe(REPO_CWD);
});
+test("import_agent_request registers a workspace for a never-seen cwd", async () => {
+ const emitted: SessionOutboundMessage[] = [];
+ const session = createSessionForWorkspaceTests({
+ onMessage: (message) => {
+ if (isSessionOutboundMessage(message)) emitted.push(message);
+ },
+ });
+ const projects = new Map>();
+ const workspaces = new Map>();
+ const importedCwd = path.resolve("/tmp/imported-project");
+
+ session.projectRegistry.get = async (projectId: string) => projects.get(projectId) ?? null;
+ session.projectRegistry.upsert = async (
+ record: ReturnType,
+ ) => {
+ projects.set(record.projectId, record);
+ };
+ session.workspaceRegistry.get = async (workspaceId: string) =>
+ workspaces.get(workspaceId) ?? null;
+ session.workspaceRegistry.upsert = async (
+ record: ReturnType,
+ ) => {
+ workspaces.set(record.workspaceId, record);
+ };
+ session.projectRegistry.list = async () => Array.from(projects.values());
+ session.workspaceRegistry.list = async () => Array.from(workspaces.values());
+ session.buildProjectPlacement = async (cwd: string) => ({
+ projectKey: cwd,
+ projectName: "imported",
+ checkout: {
+ cwd,
+ isGit: false,
+ currentBranch: null,
+ remoteUrl: null,
+ worktreeRoot: null,
+ isPaseoOwnedWorktree: false,
+ mainRepoRoot: null,
+ },
+ });
+
+ const managed = makeManagedAgent({
+ id: "imported-agent",
+ cwd: importedCwd,
+ lifecycle: "idle",
+ updatedAt: "2026-05-21T00:00:00.000Z",
+ });
+ session.agentManager.listAgents = () => [managed];
+ session.agentManager.findPersistedAgent = async () => null;
+ session.agentManager.resumeAgentFromPersistence = async () => managed;
+ session.agentManager.hydrateTimelineFromProvider = async () => undefined;
+ session.agentManager.getTimeline = () => [];
+ session.agentManager.setTitle = async () => undefined;
+ session.agentStorage.list = async () => [];
+ session.agentStorage.get = async () => null;
+ session.forwardAgentUpdate = async () => undefined;
+
+ session.workspaceUpdatesSubscription = {
+ subscriptionId: "sub-import",
+ filter: undefined,
+ isBootstrapping: false,
+ pendingUpdatesByWorkspaceId: new Map(),
+ lastEmittedByWorkspaceId: new Map(),
+ };
+ session.buildWorkspaceDescriptorMap = async () =>
+ new Map([
+ [
+ importedCwd,
+ {
+ id: importedCwd,
+ projectId: importedCwd,
+ projectDisplayName: "imported-project",
+ projectRootPath: importedCwd,
+ projectKind: "non_git",
+ workspaceKind: "directory",
+ name: "imported-project",
+ status: "done",
+ activityAt: null,
+ },
+ ],
+ ]);
+
+ await session.handleMessage({
+ type: "import_agent_request",
+ requestId: "req-import",
+ providerId: "codex",
+ providerHandleId: "session-xyz",
+ cwd: importedCwd,
+ });
+
+ expect(workspaces.get(importedCwd)).toBeTruthy();
+ const workspaceUpdates = filterByType(emitted, "workspace_update");
+ expect(workspaceUpdates.length).toBeGreaterThan(0);
+ expect(
+ workspaceUpdates.some(
+ (update) => update.payload.kind === "upsert" && update.payload.workspace.id === importedCwd,
+ ),
+ ).toBe(true);
+});
+
test("open_project_response returns immediately even when the GitHub fetch is slow", async () => {
const emitted: SessionOutboundMessage[] = [];
const session = createSessionForWorkspaceTests();