feat: import session sheet redesign and metadata-gen filter
Filter persisted metadata-generation sessions out of the import list so existing on-disk entries stop polluting it for users. Rename the surface to "Import session" everywhere, swap the bespoke filter pills for SegmentedControl with provider icons, drop the trailing provider label on rows (icon is enough), tighten padding, and align the row icon with the segmented control's left edge.
This commit is contained in:
@@ -145,6 +145,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface2,
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: theme.borderRadius.lg,
|
||||||
|
gap: 2,
|
||||||
},
|
},
|
||||||
containerSm: {
|
containerSm: {
|
||||||
padding: 2,
|
padding: 2,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function ComposerImportPill({ onPress, disabled = false }: ComposerImport
|
|||||||
<Pressable
|
<Pressable
|
||||||
testID="composer-import-agent-pill"
|
testID="composer-import-agent-pill"
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Import agent"
|
accessibilityLabel="Import session"
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onHoverIn={handleHoverIn}
|
onHoverIn={handleHoverIn}
|
||||||
@@ -31,7 +31,7 @@ export function ComposerImportPill({ onPress, disabled = false }: ComposerImport
|
|||||||
>
|
>
|
||||||
<ThemedImportIcon size={14} uniProps={iconColorMapping} />
|
<ThemedImportIcon size={14} uniProps={iconColorMapping} />
|
||||||
<Text style={styles.label} numberOfLines={1}>
|
<Text style={styles.label} numberOfLines={1}>
|
||||||
Import agent
|
Import session
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -11,14 +11,17 @@ import { WorkspaceImportSheet } from "@/screens/workspace/workspace-import-sheet
|
|||||||
|
|
||||||
const { theme } = vi.hoisted(() => ({
|
const { theme } = vi.hoisted(() => ({
|
||||||
theme: {
|
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 },
|
borderWidth: { 1: 1 },
|
||||||
borderRadius: { md: 6, full: 9999 },
|
borderRadius: { md: 6, lg: 8, full: 9999 },
|
||||||
fontSize: { xs: 11, sm: 13, base: 15 },
|
fontSize: { xs: 11, sm: 13, base: 15 },
|
||||||
fontWeight: { normal: "400", medium: "500", semibold: "600" },
|
fontWeight: { normal: "400", medium: "500", semibold: "600" },
|
||||||
|
iconSize: { sm: 14, md: 16 },
|
||||||
|
opacity: { 50: 0.5 },
|
||||||
colors: {
|
colors: {
|
||||||
foreground: "#fff",
|
foreground: "#fff",
|
||||||
foregroundMuted: "#aaa",
|
foregroundMuted: "#aaa",
|
||||||
|
surface0: "#000",
|
||||||
surface1: "#111",
|
surface1: "#111",
|
||||||
surface2: "#222",
|
surface2: "#222",
|
||||||
surface3: "#333",
|
surface3: "#333",
|
||||||
@@ -58,6 +61,42 @@ vi.mock("@/components/provider-icons", () => ({
|
|||||||
getProviderIcon: () => () => null,
|
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", () => ({
|
vi.mock("@/components/adaptive-modal-sheet", () => ({
|
||||||
AdaptiveModalSheet: ({
|
AdaptiveModalSheet: ({
|
||||||
visible,
|
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("2h ago");
|
||||||
screen.getByText("Implement the importer sheet");
|
|
||||||
screen.getByText("Make the rows readable and provider opaque");
|
screen.getByText("Make the rows readable and provider opaque");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
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 { useMutation, useQueries, useQueryClient } from "@tanstack/react-query";
|
||||||
import type { DaemonClient, FetchRecentProviderSessionEntry } from "@server/client/daemon-client";
|
import type { DaemonClient, FetchRecentProviderSessionEntry } from "@server/client/daemon-client";
|
||||||
import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
|
import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
|
||||||
import { IMPORTABLE_PROVIDERS } from "@server/shared/importable-providers";
|
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 { 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 { getProviderIcon } from "@/components/provider-icons";
|
||||||
import { formatTimeAgo } from "@/utils/time";
|
import { formatTimeAgo } from "@/utils/time";
|
||||||
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
||||||
@@ -14,11 +16,7 @@ const IMPORTABLE_PROVIDER_IDS: Set<string> = new Set(IMPORTABLE_PROVIDERS);
|
|||||||
const PER_PROVIDER_LIMIT = 15;
|
const PER_PROVIDER_LIMIT = 15;
|
||||||
const IMPORT_SHEET_SNAP_POINTS = ["70%", "92%"];
|
const IMPORT_SHEET_SNAP_POINTS = ["70%", "92%"];
|
||||||
const DISABLED_ACCESSIBILITY_STATE = { disabled: true };
|
const DISABLED_ACCESSIBILITY_STATE = { disabled: true };
|
||||||
|
const ALL_FILTER_VALUE = "__all__";
|
||||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
|
||||||
const activityIndicatorColorMapping = (theme: { colors: { foregroundMuted: string } }) => ({
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
});
|
|
||||||
|
|
||||||
type RecentProviderSessionsClient = Pick<
|
type RecentProviderSessionsClient = Pick<
|
||||||
DaemonClient,
|
DaemonClient,
|
||||||
@@ -188,8 +186,9 @@ function SheetStatusMessages({
|
|||||||
showEmptyState,
|
showEmptyState,
|
||||||
allAlreadyImported,
|
allAlreadyImported,
|
||||||
}: SheetStatusMessagesProps) {
|
}: SheetStatusMessagesProps) {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
if (!isClientReady) {
|
if (!isClientReady) {
|
||||||
return <Text style={styles.statusText}>Connect to a workspace to import agents.</Text>;
|
return <Text style={styles.statusText}>Connect to a workspace to import sessions</Text>;
|
||||||
}
|
}
|
||||||
if (isSnapshotUnsupported) {
|
if (isSnapshotUnsupported) {
|
||||||
return <Text style={styles.statusText}>Update the host to import sessions.</Text>;
|
return <Text style={styles.statusText}>Update the host to import sessions.</Text>;
|
||||||
@@ -201,7 +200,7 @@ function SheetStatusMessages({
|
|||||||
) : null}
|
) : null}
|
||||||
{isLoadingSessions ? (
|
{isLoadingSessions ? (
|
||||||
<View style={styles.statusRow}>
|
<View style={styles.statusRow}>
|
||||||
<ThemedActivityIndicator size="small" uniProps={activityIndicatorColorMapping} />
|
<LoadingSpinner color={theme.colors.foregroundMuted} />
|
||||||
<Text style={styles.statusText}>Loading recent sessions...</Text>
|
<Text style={styles.statusText}>Loading recent sessions...</Text>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -227,65 +226,23 @@ function SheetStatusMessages({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProviderFilterBadgeProps {
|
function buildProviderFilterOptions(
|
||||||
testID: string;
|
providers: ReadonlyArray<string>,
|
||||||
label: string;
|
providerLabelById: ReadonlyMap<string, string>,
|
||||||
selected: boolean;
|
): SegmentedControlOption<string>[] {
|
||||||
onPress: () => void;
|
const options: SegmentedControlOption<string>[] = [
|
||||||
}
|
{ value: ALL_FILTER_VALUE, label: "All", testID: "workspace-import-filter-all" },
|
||||||
|
];
|
||||||
function ProviderFilterBadge({ testID, label, selected, onPress }: ProviderFilterBadgeProps) {
|
for (const provider of providers) {
|
||||||
const accessibilityState = useMemo(() => ({ selected }), [selected]);
|
const ProviderIcon = getProviderIcon(provider);
|
||||||
const containerStyle = useMemo(
|
options.push({
|
||||||
() => [
|
value: provider,
|
||||||
styles.filterBadge,
|
label: providerLabelById.get(provider) ?? provider,
|
||||||
selected ? styles.filterBadgeSelected : styles.filterBadgeUnselected,
|
testID: `workspace-import-filter-${provider}`,
|
||||||
],
|
icon: ({ color, size }) => <ProviderIcon color={color} size={size} />,
|
||||||
[selected],
|
});
|
||||||
);
|
}
|
||||||
const textStyle = useMemo(
|
return options;
|
||||||
() => [
|
|
||||||
styles.filterBadgeLabel,
|
|
||||||
selected ? styles.filterBadgeLabelSelected : styles.filterBadgeLabelUnselected,
|
|
||||||
],
|
|
||||||
[selected],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityState={accessibilityState}
|
|
||||||
onPress={onPress}
|
|
||||||
style={containerStyle}
|
|
||||||
testID={testID}
|
|
||||||
>
|
|
||||||
<Text style={textStyle} numberOfLines={1}>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProviderFilterBadgeItem({
|
|
||||||
provider,
|
|
||||||
label,
|
|
||||||
selected,
|
|
||||||
onSelect,
|
|
||||||
}: {
|
|
||||||
provider: string;
|
|
||||||
label: string;
|
|
||||||
selected: boolean;
|
|
||||||
onSelect: (provider: string) => void;
|
|
||||||
}) {
|
|
||||||
const handlePress = useCallback(() => onSelect(provider), [onSelect, provider]);
|
|
||||||
return (
|
|
||||||
<ProviderFilterBadge
|
|
||||||
testID={`workspace-import-filter-${provider}`}
|
|
||||||
label={label}
|
|
||||||
selected={selected}
|
|
||||||
onPress={handlePress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkspaceImportSheetRow({
|
function WorkspaceImportSheetRow({
|
||||||
@@ -311,6 +268,14 @@ function WorkspaceImportSheetRow({
|
|||||||
const handlePress = useCallback(() => {
|
const handlePress = useCallback(() => {
|
||||||
onImportSession(entry);
|
onImportSession(entry);
|
||||||
}, [entry, onImportSession]);
|
}, [entry, onImportSession]);
|
||||||
|
const pressableStyle = useCallback(
|
||||||
|
({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||||
|
styles.row,
|
||||||
|
Boolean(hovered) && styles.rowHovered,
|
||||||
|
pressed && styles.rowPressed,
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -318,25 +283,23 @@ function WorkspaceImportSheetRow({
|
|||||||
onPress={handlePress}
|
onPress={handlePress}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityState={accessibilityState}
|
accessibilityState={accessibilityState}
|
||||||
style={styles.row}
|
style={pressableStyle}
|
||||||
testID={`workspace-import-session-${entry.providerId}-${entry.providerHandleId}`}
|
testID={`workspace-import-session-${entry.providerId}-${entry.providerHandleId}`}
|
||||||
>
|
>
|
||||||
<View style={styles.rowHeader}>
|
<View style={styles.rowIconWrap}>
|
||||||
<View style={styles.rowProvider}>
|
<ProviderIcon size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
<ProviderIcon size={theme.fontSize.sm} color={theme.colors.foregroundMuted} />
|
</View>
|
||||||
<Text style={styles.providerLabel} numberOfLines={1}>
|
<View style={styles.rowContent}>
|
||||||
{entry.providerLabel}
|
<View style={styles.rowHeader}>
|
||||||
|
<Text style={styles.rowTitle} numberOfLines={1}>
|
||||||
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text style={styles.rowMeta}>{importing ? "Importing..." : lastActivity}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.lastActivity}>{lastActivity}</Text>
|
<Text style={styles.rowPreview} numberOfLines={2}>
|
||||||
|
{promptPreview}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.rowTitle} numberOfLines={1}>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.promptPreview} numberOfLines={2}>
|
|
||||||
{promptPreview}
|
|
||||||
</Text>
|
|
||||||
{importing ? <Text style={styles.importingText}>Importing...</Text> : null}
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -390,22 +353,28 @@ export function WorkspaceImportSheet({
|
|||||||
[queries],
|
[queries],
|
||||||
);
|
);
|
||||||
|
|
||||||
const badgeProviders = useMemo(() => [...(providersToFetch ?? [])].sort(), [providersToFetch]);
|
const filterProviders = useMemo(() => [...(providersToFetch ?? [])].sort(), [providersToFetch]);
|
||||||
|
|
||||||
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
const [selectedProvider, setSelectedProvider] = useState<string>(ALL_FILTER_VALUE);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible || (selectedProvider !== null && !badgeProviders.includes(selectedProvider))) {
|
if (
|
||||||
setSelectedProvider(null);
|
!visible ||
|
||||||
|
(selectedProvider !== ALL_FILTER_VALUE && !filterProviders.includes(selectedProvider))
|
||||||
|
) {
|
||||||
|
setSelectedProvider(ALL_FILTER_VALUE);
|
||||||
}
|
}
|
||||||
}, [visible, badgeProviders, selectedProvider]);
|
}, [visible, filterProviders, selectedProvider]);
|
||||||
|
|
||||||
const visibleEntries = useMemo(() => {
|
const visibleEntries = useMemo(() => {
|
||||||
if (selectedProvider === null) return aggregatedEntries;
|
if (selectedProvider === ALL_FILTER_VALUE) return aggregatedEntries;
|
||||||
return aggregatedEntries.filter((entry) => entry.providerId === selectedProvider);
|
return aggregatedEntries.filter((entry) => entry.providerId === selectedProvider);
|
||||||
}, [aggregatedEntries, selectedProvider]);
|
}, [aggregatedEntries, selectedProvider]);
|
||||||
|
|
||||||
const handleSelectAll = useCallback(() => setSelectedProvider(null), []);
|
const filterOptions = useMemo(
|
||||||
|
() => buildProviderFilterOptions(filterProviders, providerLabelById),
|
||||||
|
[filterProviders, providerLabelById],
|
||||||
|
);
|
||||||
|
|
||||||
const importMutation = useMutation({
|
const importMutation = useMutation({
|
||||||
mutationFn: async (entry: FetchRecentProviderSessionEntry) => {
|
mutationFn: async (entry: FetchRecentProviderSessionEntry) => {
|
||||||
@@ -460,39 +429,30 @@ export function WorkspaceImportSheet({
|
|||||||
allQueriesSettled &&
|
allQueriesSettled &&
|
||||||
aggregatedEntries.length === 0;
|
aggregatedEntries.length === 0;
|
||||||
const allAlreadyImported = showEmptyState && totalAlreadyImportedCount > 0;
|
const allAlreadyImported = showEmptyState && totalAlreadyImportedCount > 0;
|
||||||
const showBadges = badgeProviders.length > 1;
|
const showFilter = filterProviders.length > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdaptiveModalSheet
|
<AdaptiveModalSheet
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title="Import agent"
|
title="Import session"
|
||||||
testID="workspace-import-sheet"
|
testID="workspace-import-sheet"
|
||||||
desktopMaxWidth={560}
|
desktopMaxWidth={560}
|
||||||
snapPoints={IMPORT_SHEET_SNAP_POINTS}
|
snapPoints={IMPORT_SHEET_SNAP_POINTS}
|
||||||
>
|
>
|
||||||
{showBadges ? (
|
{showFilter ? (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.filterRow}
|
contentContainerStyle={styles.filterRow}
|
||||||
testID="workspace-import-filters"
|
|
||||||
>
|
>
|
||||||
<ProviderFilterBadge
|
<SegmentedControl
|
||||||
testID="workspace-import-filter-all"
|
testID="workspace-import-filters"
|
||||||
label="All"
|
size="sm"
|
||||||
selected={selectedProvider === null}
|
options={filterOptions}
|
||||||
onPress={handleSelectAll}
|
value={selectedProvider}
|
||||||
|
onValueChange={setSelectedProvider}
|
||||||
/>
|
/>
|
||||||
{badgeProviders.map((provider) => (
|
|
||||||
<ProviderFilterBadgeItem
|
|
||||||
key={provider}
|
|
||||||
provider={provider}
|
|
||||||
label={providerLabelById.get(provider) ?? provider}
|
|
||||||
selected={selectedProvider === provider}
|
|
||||||
onSelect={setSelectedProvider}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
) : null}
|
) : null}
|
||||||
<SheetStatusMessages
|
<SheetStatusMessages
|
||||||
@@ -524,89 +484,65 @@ export function WorkspaceImportSheet({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
|
filterRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
paddingBottom: theme.spacing[2],
|
||||||
|
},
|
||||||
list: {
|
list: {
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[1],
|
||||||
},
|
},
|
||||||
row: {
|
row: {
|
||||||
gap: theme.spacing[1],
|
flexDirection: "row",
|
||||||
borderWidth: theme.borderWidth[1],
|
alignItems: "flex-start",
|
||||||
borderColor: theme.colors.borderAccent,
|
gap: theme.spacing[2],
|
||||||
borderRadius: theme.borderRadius.md,
|
paddingVertical: theme.spacing[2],
|
||||||
|
paddingHorizontal: theme.spacing[2],
|
||||||
|
marginHorizontal: -theme.spacing[2],
|
||||||
|
borderRadius: theme.borderRadius.lg,
|
||||||
|
},
|
||||||
|
rowHovered: {
|
||||||
backgroundColor: theme.colors.surface1,
|
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: {
|
rowHeader: {
|
||||||
minWidth: 0,
|
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "baseline",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
gap: theme.spacing[2],
|
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: {
|
rowTitle: {
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
|
||||||
},
|
},
|
||||||
promptPreview: {
|
rowMeta: {
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
fontSize: theme.fontSize.sm,
|
|
||||||
lineHeight: 19,
|
|
||||||
},
|
|
||||||
importingText: {
|
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
},
|
},
|
||||||
|
rowPreview: {
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
statusRow: {
|
statusRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
|
paddingVertical: theme.spacing[2],
|
||||||
},
|
},
|
||||||
statusText: {
|
statusText: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
|
|||||||
@@ -881,7 +881,7 @@ function WorkspaceHeaderMenu({
|
|||||||
disabled={importAgentDisabled}
|
disabled={importAgentDisabled}
|
||||||
onSelect={onOpenImportSheet}
|
onSelect={onOpenImportSheet}
|
||||||
>
|
>
|
||||||
Import agent...
|
Import session
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
testID="workspace-header-copy-path"
|
testID="workspace-header-copy-path"
|
||||||
|
|||||||
@@ -179,6 +179,45 @@ test("listImportableProviderSessions filters, sorts, limits, and projects import
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("listImportableProviderSessions filters out metadata generation sessions", async () => {
|
||||||
|
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<AgentManager, "listAgents" | "listImportablePersistedAgents">,
|
||||||
|
agentStorage: {
|
||||||
|
list: async () => [],
|
||||||
|
} satisfies Pick<AgentStorage, "list">,
|
||||||
|
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 () => {
|
test("listImportableProviderSessions rejects invalid since values", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
listImportableProviderSessions({
|
listImportableProviderSessions({
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import type {
|
|||||||
|
|
||||||
type ImportAgentRequestMessage = z.infer<typeof ImportAgentRequestMessageSchema>;
|
type ImportAgentRequestMessage = z.infer<typeof ImportAgentRequestMessageSchema>;
|
||||||
|
|
||||||
|
const METADATA_GENERATION_PROMPT_PREFIX =
|
||||||
|
"Generate metadata for a coding agent based on the user prompt.";
|
||||||
|
|
||||||
export interface NormalizedImportAgentRequest {
|
export interface NormalizedImportAgentRequest {
|
||||||
provider: string;
|
provider: string;
|
||||||
providerHandleId: string;
|
providerHandleId: string;
|
||||||
@@ -91,6 +94,9 @@ export async function listImportableProviderSessions(
|
|||||||
if (sinceTimestamp !== null && descriptor.lastActivityAt.getTime() < sinceTimestamp) {
|
if (sinceTimestamp !== null && descriptor.lastActivityAt.getTime() < sinceTimestamp) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (isMetadataGenerationDescriptor(descriptor)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const providerHandleId =
|
const providerHandleId =
|
||||||
descriptor.persistence.nativeHandle ?? descriptor.persistence.sessionId;
|
descriptor.persistence.nativeHandle ?? descriptor.persistence.sessionId;
|
||||||
if (importedHandles.has(toProviderSessionHandleKey(descriptor.provider, providerHandleId))) {
|
if (importedHandles.has(toProviderSessionHandleKey(descriptor.provider, providerHandleId))) {
|
||||||
@@ -144,6 +150,14 @@ function toProviderSessionHandleKey(provider: string, providerHandleId: string):
|
|||||||
return `${provider}\0${providerHandleId}`;
|
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(
|
function collectProviderSessionHandleKeys(
|
||||||
target: Set<string>,
|
target: Set<string>,
|
||||||
provider: AgentProvider | StoredAgentRecord["provider"] | string,
|
provider: AgentProvider | StoredAgentRecord["provider"] | string,
|
||||||
|
|||||||
Reference in New Issue
Block a user