diff --git a/packages/app/src/components/ui/segmented-control.tsx b/packages/app/src/components/ui/segmented-control.tsx
index 78426e69b..4e0495d35 100644
--- a/packages/app/src/components/ui/segmented-control.tsx
+++ b/packages/app/src/components/ui/segmented-control.tsx
@@ -145,6 +145,7 @@ const styles = StyleSheet.create((theme) => ({
maxWidth: "100%",
backgroundColor: theme.colors.surface2,
borderRadius: theme.borderRadius.lg,
+ gap: 2,
},
containerSm: {
padding: 2,
diff --git a/packages/app/src/screens/workspace/composer-import-pill.tsx b/packages/app/src/screens/workspace/composer-import-pill.tsx
index 25267a834..98b452dc5 100644
--- a/packages/app/src/screens/workspace/composer-import-pill.tsx
+++ b/packages/app/src/screens/workspace/composer-import-pill.tsx
@@ -22,7 +22,7 @@ export function ComposerImportPill({ onPress, disabled = false }: ComposerImport
- Import agent
+ Import session
diff --git a/packages/app/src/screens/workspace/workspace-import-sheet.test.tsx b/packages/app/src/screens/workspace/workspace-import-sheet.test.tsx
index 9ce5dc958..0f82febd1 100644
--- a/packages/app/src/screens/workspace/workspace-import-sheet.test.tsx
+++ b/packages/app/src/screens/workspace/workspace-import-sheet.test.tsx
@@ -11,14 +11,17 @@ import { WorkspaceImportSheet } from "@/screens/workspace/workspace-import-sheet
const { theme } = vi.hoisted(() => ({
theme: {
- spacing: { 1: 4, 1.5: 6, 2: 8, 3: 12, 4: 16 },
+ spacing: { 1: 4, 1.5: 6, 2: 8, 3: 12, 4: 16, 6: 24 },
borderWidth: { 1: 1 },
- borderRadius: { md: 6, full: 9999 },
+ borderRadius: { md: 6, lg: 8, full: 9999 },
fontSize: { xs: 11, sm: 13, base: 15 },
fontWeight: { normal: "400", medium: "500", semibold: "600" },
+ iconSize: { sm: 14, md: 16 },
+ opacity: { 50: 0.5 },
colors: {
foreground: "#fff",
foregroundMuted: "#aaa",
+ surface0: "#000",
surface1: "#111",
surface2: "#222",
surface3: "#333",
@@ -58,6 +61,42 @@ vi.mock("@/components/provider-icons", () => ({
getProviderIcon: () => () => null,
}));
+vi.mock("@/components/ui/loading-spinner", () => ({
+ LoadingSpinner: () =>
+ React.createElement("span", { "data-testid": "workspace-import-loading-spinner" }),
+}));
+
+vi.mock("@/components/ui/segmented-control", () => ({
+ SegmentedControl: ({
+ options,
+ value,
+ onValueChange,
+ testID,
+ }: {
+ options: ReadonlyArray<{ value: string; label: string; testID?: string }>;
+ value: string;
+ onValueChange: (value: string) => void;
+ testID?: string;
+ }) =>
+ React.createElement(
+ "div",
+ { "data-testid": testID },
+ options.map((option) =>
+ React.createElement(
+ "button",
+ {
+ key: option.value,
+ type: "button",
+ "data-testid": option.testID,
+ "data-selected": value === option.value,
+ onClick: () => onValueChange(option.value),
+ },
+ option.label,
+ ),
+ ),
+ ),
+}));
+
vi.mock("@/components/adaptive-modal-sheet", () => ({
AdaptiveModalSheet: ({
visible,
@@ -349,9 +388,8 @@ describe("WorkspaceImportSheet", () => {
});
});
- await screen.findByText("Claude Code");
+ await screen.findByText("Implement the importer sheet");
screen.getByText("2h ago");
- screen.getByText("Implement the importer sheet");
screen.getByText("Make the rows readable and provider opaque");
});
diff --git a/packages/app/src/screens/workspace/workspace-import-sheet.tsx b/packages/app/src/screens/workspace/workspace-import-sheet.tsx
index 9073254e0..accef3a93 100644
--- a/packages/app/src/screens/workspace/workspace-import-sheet.tsx
+++ b/packages/app/src/screens/workspace/workspace-import-sheet.tsx
@@ -1,11 +1,13 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
-import { ActivityIndicator, Pressable, ScrollView, Text, View } from "react-native";
+import { Pressable, type PressableStateCallbackType, ScrollView, Text, View } from "react-native";
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 { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles";
+import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
+import { LoadingSpinner } from "@/components/ui/loading-spinner";
+import { SegmentedControl, type SegmentedControlOption } from "@/components/ui/segmented-control";
import { getProviderIcon } from "@/components/provider-icons";
import { formatTimeAgo } from "@/utils/time";
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
@@ -14,11 +16,7 @@ const IMPORTABLE_PROVIDER_IDS: Set = new Set(IMPORTABLE_PROVIDERS);
const PER_PROVIDER_LIMIT = 15;
const IMPORT_SHEET_SNAP_POINTS = ["70%", "92%"];
const DISABLED_ACCESSIBILITY_STATE = { disabled: true };
-
-const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
-const activityIndicatorColorMapping = (theme: { colors: { foregroundMuted: string } }) => ({
- color: theme.colors.foregroundMuted,
-});
+const ALL_FILTER_VALUE = "__all__";
type RecentProviderSessionsClient = Pick<
DaemonClient,
@@ -188,8 +186,9 @@ function SheetStatusMessages({
showEmptyState,
allAlreadyImported,
}: SheetStatusMessagesProps) {
+ const { theme } = useUnistyles();
if (!isClientReady) {
- return Connect to a workspace to import agents.;
+ return Connect to a workspace to import sessions;
}
if (isSnapshotUnsupported) {
return Update the host to import sessions.;
@@ -201,7 +200,7 @@ function SheetStatusMessages({
) : null}
{isLoadingSessions ? (
-
+
Loading recent sessions...
) : null}
@@ -227,65 +226,23 @@ function SheetStatusMessages({
);
}
-interface ProviderFilterBadgeProps {
- testID: string;
- label: string;
- selected: boolean;
- onPress: () => void;
-}
-
-function ProviderFilterBadge({ testID, label, selected, onPress }: ProviderFilterBadgeProps) {
- const accessibilityState = useMemo(() => ({ selected }), [selected]);
- const containerStyle = useMemo(
- () => [
- styles.filterBadge,
- selected ? styles.filterBadgeSelected : styles.filterBadgeUnselected,
- ],
- [selected],
- );
- const textStyle = useMemo(
- () => [
- styles.filterBadgeLabel,
- selected ? styles.filterBadgeLabelSelected : styles.filterBadgeLabelUnselected,
- ],
- [selected],
- );
-
- return (
-
-
- {label}
-
-
- );
-}
-
-function ProviderFilterBadgeItem({
- provider,
- label,
- selected,
- onSelect,
-}: {
- provider: string;
- label: string;
- selected: boolean;
- onSelect: (provider: string) => void;
-}) {
- const handlePress = useCallback(() => onSelect(provider), [onSelect, provider]);
- return (
-
- );
+function buildProviderFilterOptions(
+ providers: ReadonlyArray,
+ providerLabelById: ReadonlyMap,
+): SegmentedControlOption[] {
+ const options: SegmentedControlOption[] = [
+ { value: ALL_FILTER_VALUE, label: "All", testID: "workspace-import-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}`,
+ icon: ({ color, size }) => ,
+ });
+ }
+ return options;
}
function WorkspaceImportSheetRow({
@@ -311,6 +268,14 @@ function WorkspaceImportSheetRow({
const handlePress = useCallback(() => {
onImportSession(entry);
}, [entry, onImportSession]);
+ const pressableStyle = useCallback(
+ ({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [
+ styles.row,
+ Boolean(hovered) && styles.rowHovered,
+ pressed && styles.rowPressed,
+ ],
+ [],
+ );
return (
-
-
-
-
- {entry.providerLabel}
+
+
+
+
+
+
+ {title}
+ {importing ? "Importing..." : lastActivity}
- {lastActivity}
+
+ {promptPreview}
+
-
- {title}
-
-
- {promptPreview}
-
- {importing ? Importing... : null}
);
}
@@ -390,22 +353,28 @@ export function WorkspaceImportSheet({
[queries],
);
- const badgeProviders = useMemo(() => [...(providersToFetch ?? [])].sort(), [providersToFetch]);
+ const filterProviders = useMemo(() => [...(providersToFetch ?? [])].sort(), [providersToFetch]);
- const [selectedProvider, setSelectedProvider] = useState(null);
+ const [selectedProvider, setSelectedProvider] = useState(ALL_FILTER_VALUE);
useEffect(() => {
- if (!visible || (selectedProvider !== null && !badgeProviders.includes(selectedProvider))) {
- setSelectedProvider(null);
+ if (
+ !visible ||
+ (selectedProvider !== ALL_FILTER_VALUE && !filterProviders.includes(selectedProvider))
+ ) {
+ setSelectedProvider(ALL_FILTER_VALUE);
}
- }, [visible, badgeProviders, selectedProvider]);
+ }, [visible, filterProviders, selectedProvider]);
const visibleEntries = useMemo(() => {
- if (selectedProvider === null) return aggregatedEntries;
+ if (selectedProvider === ALL_FILTER_VALUE) return aggregatedEntries;
return aggregatedEntries.filter((entry) => entry.providerId === selectedProvider);
}, [aggregatedEntries, selectedProvider]);
- const handleSelectAll = useCallback(() => setSelectedProvider(null), []);
+ const filterOptions = useMemo(
+ () => buildProviderFilterOptions(filterProviders, providerLabelById),
+ [filterProviders, providerLabelById],
+ );
const importMutation = useMutation({
mutationFn: async (entry: FetchRecentProviderSessionEntry) => {
@@ -460,39 +429,30 @@ export function WorkspaceImportSheet({
allQueriesSettled &&
aggregatedEntries.length === 0;
const allAlreadyImported = showEmptyState && totalAlreadyImportedCount > 0;
- const showBadges = badgeProviders.length > 1;
+ const showFilter = filterProviders.length > 1;
return (
- {showBadges ? (
+ {showFilter ? (
-
- {badgeProviders.map((provider) => (
-
- ))}
) : null}
({
+ filterRow: {
+ flexDirection: "row",
+ paddingBottom: theme.spacing[2],
+ },
list: {
- gap: theme.spacing[2],
+ gap: theme.spacing[1],
},
row: {
- gap: theme.spacing[1],
- borderWidth: theme.borderWidth[1],
- borderColor: theme.colors.borderAccent,
- borderRadius: theme.borderRadius.md,
+ flexDirection: "row",
+ alignItems: "flex-start",
+ gap: theme.spacing[2],
+ paddingVertical: theme.spacing[2],
+ paddingHorizontal: theme.spacing[2],
+ marginHorizontal: -theme.spacing[2],
+ borderRadius: theme.borderRadius.lg,
+ },
+ rowHovered: {
backgroundColor: theme.colors.surface1,
- padding: theme.spacing[3],
+ },
+ rowPressed: {
+ backgroundColor: theme.colors.surface2,
+ },
+ rowIconWrap: {
+ width: theme.iconSize.md,
+ paddingTop: 2,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ rowContent: {
+ flex: 1,
+ minWidth: 0,
+ gap: theme.spacing[1],
},
rowHeader: {
- minWidth: 0,
flexDirection: "row",
- alignItems: "center",
+ alignItems: "baseline",
justifyContent: "space-between",
gap: theme.spacing[2],
},
- rowProvider: {
- flex: 1,
- minWidth: 0,
- flexDirection: "row",
- alignItems: "center",
- gap: theme.spacing[1],
- },
- providerLabel: {
- flex: 1,
- minWidth: 0,
- color: theme.colors.foregroundMuted,
- fontSize: theme.fontSize.xs,
- fontWeight: theme.fontWeight.medium,
- },
- filterRow: {
- flexDirection: "row",
- gap: theme.spacing[2],
- paddingBottom: theme.spacing[2],
- },
- filterBadge: {
- paddingHorizontal: theme.spacing[3],
- paddingVertical: theme.spacing[1],
- borderRadius: theme.borderRadius.full,
- borderWidth: theme.borderWidth[1],
- },
- filterBadgeSelected: {
- backgroundColor: theme.colors.surface3,
- borderColor: theme.colors.borderAccent,
- },
- filterBadgeUnselected: {
- backgroundColor: "transparent",
- borderColor: theme.colors.border,
- },
- filterBadgeLabel: {
- fontSize: theme.fontSize.xs,
- fontWeight: theme.fontWeight.medium,
- },
- filterBadgeLabelSelected: {
- color: theme.colors.foreground,
- },
- filterBadgeLabelUnselected: {
- color: theme.colors.foregroundMuted,
- },
- lastActivity: {
- color: theme.colors.foregroundMuted,
- fontSize: theme.fontSize.xs,
- },
rowTitle: {
+ flex: 1,
+ minWidth: 0,
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
- fontWeight: theme.fontWeight.semibold,
},
- promptPreview: {
- color: theme.colors.foregroundMuted,
- fontSize: theme.fontSize.sm,
- lineHeight: 19,
- },
- importingText: {
+ rowMeta: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
},
+ rowPreview: {
+ color: theme.colors.foregroundMuted,
+ fontSize: theme.fontSize.sm,
+ lineHeight: 20,
+ },
statusRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
+ paddingVertical: theme.spacing[2],
},
statusText: {
color: theme.colors.foregroundMuted,
diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx
index 2725c430c..272d5f395 100644
--- a/packages/app/src/screens/workspace/workspace-screen.tsx
+++ b/packages/app/src/screens/workspace/workspace-screen.tsx
@@ -881,7 +881,7 @@ function WorkspaceHeaderMenu({
disabled={importAgentDisabled}
onSelect={onOpenImportSheet}
>
- Import agent...
+ Import session
{
+ const cwd = "/tmp/project";
+ const descriptors = [
+ makeDescriptor({
+ sessionId: "metadata-session",
+ nativeHandle: "metadata-handle",
+ cwd,
+ title: "Generate metadata for a coding agent based on the user prom...",
+ lastActivityAt: "2026-04-30T12:05:00.000Z",
+ firstPrompt:
+ "Generate metadata for a coding agent based on the user prompt.\nTitle: short descriptive label (<= 40 chars).",
+ }),
+ makeDescriptor({
+ sessionId: "real-session",
+ nativeHandle: "real-handle",
+ cwd,
+ title: "Real session",
+ lastActivityAt: "2026-04-30T12:00:00.000Z",
+ firstPrompt: "hey hey",
+ }),
+ ];
+
+ const result = await listImportableProviderSessions({
+ request: makeRequest({ cwd, providers: ["codex"] }),
+ agentManager: {
+ listAgents: () => [],
+ listImportablePersistedAgents: async () => descriptors,
+ } satisfies Pick,
+ agentStorage: {
+ list: async () => [],
+ } satisfies Pick,
+ providerRegistry: { codex: { label: "Codex" } },
+ });
+
+ expect(result.entries).toHaveLength(1);
+ expect(result.entries[0].providerHandleId).toBe("real-handle");
+ expect(result.filteredAlreadyImportedCount).toBe(0);
+});
+
test("listImportableProviderSessions rejects invalid since values", async () => {
await expect(
listImportableProviderSessions({
diff --git a/packages/server/src/server/agent/import-sessions.ts b/packages/server/src/server/agent/import-sessions.ts
index 118b0b55d..3317f6daf 100644
--- a/packages/server/src/server/agent/import-sessions.ts
+++ b/packages/server/src/server/agent/import-sessions.ts
@@ -16,6 +16,9 @@ import type {
type ImportAgentRequestMessage = z.infer;
+const METADATA_GENERATION_PROMPT_PREFIX =
+ "Generate metadata for a coding agent based on the user prompt.";
+
export interface NormalizedImportAgentRequest {
provider: string;
providerHandleId: string;
@@ -91,6 +94,9 @@ export async function listImportableProviderSessions(
if (sinceTimestamp !== null && descriptor.lastActivityAt.getTime() < sinceTimestamp) {
continue;
}
+ if (isMetadataGenerationDescriptor(descriptor)) {
+ continue;
+ }
const providerHandleId =
descriptor.persistence.nativeHandle ?? descriptor.persistence.sessionId;
if (importedHandles.has(toProviderSessionHandleKey(descriptor.provider, providerHandleId))) {
@@ -144,6 +150,14 @@ function toProviderSessionHandleKey(provider: string, providerHandleId: string):
return `${provider}\0${providerHandleId}`;
}
+function isMetadataGenerationDescriptor(descriptor: PersistedAgentDescriptor): boolean {
+ for (const item of descriptor.timeline) {
+ if (item.type !== "user_message") continue;
+ return item.text.trimStart().startsWith(METADATA_GENERATION_PROMPT_PREFIX);
+ }
+ return false;
+}
+
function collectProviderSessionHandleKeys(
target: Set,
provider: AgentProvider | StoredAgentRecord["provider"] | string,