Files
paseo/packages/app/src/onboarding/internal/steps/projects-step.tsx
T
Mohamed Boudra 1284ff355f feat(onboarding): suggest projects from agent history
The setup's Projects step lists the repos the user already ran agents in,
with busy recent ones preselected, so a new user adds their projects in one
click instead of browsing for folders. With rows checked, the footer's main
button adds them and only moves on once every one was added; a checked row
always means "this will be added".

The daemon resolves each session folder to its project root (main checkout
for worktrees, repo root for subfolders) because only it can read the git
state; the app asks once per provider so a slow or broken provider never
holds up the others and can be retried on its own. Hosts without the
projectSuggestions feature show only the Add project button.

Onboarding e2e hosts read an empty Claude history with other providers
off, so suggestions never depend on the machine running the tests.
2026-10-31 10:37:51 +01:00

90 lines
3.1 KiB
TypeScript

import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Text, View } from "react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { Check, FolderPlus } from "lucide-react-native";
import { Button } from "@/components/ui/button";
import { useOpenAddProject } from "@/hooks/use-open-add-project";
import { ProjectSuggestionList, type ProjectSuggestionsController } from "@/project-suggestions";
import { useProjectRootPaths } from "@/stores/session-store-hooks";
import { settingsStyles } from "@/styles/settings";
import type { Theme } from "@/styles/theme";
const ThemedCheck = withUnistyles(Check);
const successMapping = (theme: Theme) => ({ color: theme.colors.palette.green[400] });
function basename(path: string): string {
const segments = path.split(/[\\/]/).filter((segment) => segment.length > 0);
return segments.at(-1) ?? path;
}
/** Adding the checked suggestions is the setup's primary action; see the modal footer. */
export function ProjectsStep({
serverId,
suggestions,
}: {
serverId: string;
suggestions: ProjectSuggestionsController;
}) {
const { t } = useTranslation();
const openAddProject = useOpenAddProject();
const projectRootPaths = useProjectRootPaths(serverId);
const handleAddProject = useCallback(() => openAddProject(serverId), [openAddProject, serverId]);
return (
<View style={styles.container}>
<ProjectSuggestionList suggestions={suggestions} />
{projectRootPaths.length > 0 ? (
<View style={settingsStyles.card}>
{projectRootPaths.map((rootPath, index) => (
<View
key={rootPath}
style={[settingsStyles.row, index > 0 && settingsStyles.rowBorder]}
>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle} numberOfLines={1}>
{basename(rootPath)}
</Text>
<Text style={settingsStyles.rowHint} numberOfLines={1}>
{rootPath}
</Text>
</View>
<View style={styles.addedBadge}>
<ThemedCheck size={14} uniProps={successMapping} />
<Text style={styles.addedLabel}>{t("onboarding.setup.steps.projects.added")}</Text>
</View>
</View>
))}
</View>
) : null}
<View style={styles.actions}>
<Button variant="outline" leftIcon={FolderPlus} onPress={handleAddProject}>
{suggestions.list.status === "ready" && suggestions.list.rows.length > 0
? t("onboarding.setup.steps.projects.addAnotherProject")
: t("onboarding.setup.steps.projects.addProject")}
</Button>
</View>
</View>
);
}
const styles = StyleSheet.create((theme) => ({
container: {
gap: theme.spacing[4],
},
actions: {
flexDirection: "row",
flexWrap: "wrap",
gap: theme.spacing[2],
},
addedBadge: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
},
addedLabel: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
},
}));