Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/reactCompiledFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ export const REACT_COMPILED_FILES = [
"apps/web/src/components/desktop/SnapShotCoordinator.tsx",
"apps/web/src/components/BranchToolbar.tsx",
"apps/web/src/components/DiffPanel.tsx",
"apps/web/src/components/onboarding/WelcomeWizard.tsx",
] as const;
159 changes: 78 additions & 81 deletions apps/web/src/components/onboarding/WelcomeWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
MonitorIcon,
TerminalIcon,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";

import { TYPOGRAPHY_ADVANCED_STORAGE_KEY } from "../../appearanceFonts";
import { useLocalStorage } from "../../hooks/useLocalStorage";
Expand Down Expand Up @@ -120,7 +120,9 @@ export function WelcomeWizard({
const [step, setStep] = useState<WizardStep>(resumeEnvironmentId ? "agents" : "connection");
const { environments } = useEnvironments();
const [selection, setSelection] = useState<ReadonlySet<EnvironmentId> | null>(null);
const autoSelectedComputers = useRef(new Set<EnvironmentId>());
// One registry for the wizard's lifetime, shared with the connect list, so a computer is
// auto-selected once even after the user unchecks it.
const [autoSelectedComputers] = useState(() => new Set<EnvironmentId>());
const [setupIds, setSetupIds] = useState<readonly EnvironmentId[]>(
resumeEnvironmentId ? [resumeEnvironmentId] : [],
);
Expand All @@ -130,22 +132,23 @@ export function WelcomeWizard({
const primaryEnvironment = usePrimaryEnvironment();
useEffect(() => {
const newComputers = environments.filter(
(environment) => !autoSelectedComputers.current.has(environment.environmentId),
(environment) => !autoSelectedComputers.has(environment.environmentId),
);
if (newComputers.length === 0) return;
for (const environment of newComputers) {
autoSelectedComputers.current.add(environment.environmentId);
autoSelectedComputers.add(environment.environmentId);
}
const enabledComputers = newComputers.filter((environment) => environment.entry.enabled);
if (enabledComputers.length === 0) return;
// oxlint-disable-next-line react/set-state-in-effect -- The registry marks each computer once, and only after commit; render must not mutate it.
setSelection(
(current) =>
new Set([
...(current ?? []),
...enabledComputers.map((environment) => environment.environmentId),
]),
);
}, [environments]);
}, [autoSelectedComputers, environments]);
const selectedIds =
selection ?? new Set(primaryEnvironment ? [primaryEnvironment.environmentId] : []);
const scans = useProjectScans(step === "import" ? setupIds : NO_ENVIRONMENTS);
Expand All @@ -159,59 +162,60 @@ export function WelcomeWizard({
setStep("agents");
};
const stageIndex = step === "agents" ? 1 : step === "import" ? 2 : 0;
const finish = useCallback(
(projectRef?: ScopedProjectRef, importWarning?: string, importedThreadCount = 0) => {
if (finishingPromiseRef.current !== null) return finishingPromiseRef.current;
if (completionErrorToastIdRef.current !== null) {
toastManager.close(completionErrorToastIdRef.current);
completionErrorToastIdRef.current = null;
}
const finish = (
projectRef?: ScopedProjectRef,
importWarning?: string,
importedThreadCount = 0,
) => {
if (finishingPromiseRef.current !== null) return finishingPromiseRef.current;
if (completionErrorToastIdRef.current !== null) {
toastManager.close(completionErrorToastIdRef.current);
completionErrorToastIdRef.current = null;
}

const completion = completeOnboarding()
.then(async () => {
if (completionErrorToastIdRef.current !== null) {
toastManager.close(completionErrorToastIdRef.current);
completionErrorToastIdRef.current = null;
}
await onDone(projectRef);
if (importWarning) {
toastManager.add({
type: "warning",
title: "Some history was not imported",
description: importWarning,
timeout: 0,
});
} else if (importedThreadCount > 0) {
toastManager.add({
type: "success",
title: `Imported ${importedThreadCount} ${importedThreadCount === 1 ? "thread" : "threads"}`,
});
}
return true;
})
.catch(() => {
const errorToast = {
type: "error",
title: "Could not finish setup",
description: "Your settings could not be saved. Try again.",
} as const;
if (completionErrorToastIdRef.current === null) {
completionErrorToastIdRef.current = toastManager.add(errorToast);
} else {
toastManager.update(completionErrorToastIdRef.current, errorToast);
}
return false;
})
.finally(() => {
if (finishingPromiseRef.current === completion) {
finishingPromiseRef.current = null;
}
});
finishingPromiseRef.current = completion;
return completion;
},
[completeOnboarding, onDone],
);
const completion = completeOnboarding()
.then(async () => {
if (completionErrorToastIdRef.current !== null) {
toastManager.close(completionErrorToastIdRef.current);
completionErrorToastIdRef.current = null;
}
await onDone(projectRef);
if (importWarning) {
toastManager.add({
type: "warning",
title: "Some history was not imported",
description: importWarning,
timeout: 0,
});
} else if (importedThreadCount > 0) {
toastManager.add({
type: "success",
title: `Imported ${importedThreadCount} ${importedThreadCount === 1 ? "thread" : "threads"}`,
});
}
return true;
})
.catch(() => {
const errorToast = {
type: "error",
title: "Could not finish setup",
description: "Your settings could not be saved. Try again.",
} as const;
if (completionErrorToastIdRef.current === null) {
completionErrorToastIdRef.current = toastManager.add(errorToast);
} else {
toastManager.update(completionErrorToastIdRef.current, errorToast);
}
return false;
})
.finally(() => {
if (finishingPromiseRef.current === completion) {
finishingPromiseRef.current = null;
}
});
finishingPromiseRef.current = completion;
return completion;
};

return (
<Dialog open disablePointerDismissal onOpenChange={(_, event) => event.cancel()}>
Expand Down Expand Up @@ -248,7 +252,7 @@ export function WelcomeWizard({
<ConnectionStep
expandPairingInitially={!localAvailable && !hasCloudPublicConfig()}
selectedIds={selectedIds}
autoSelectedComputers={autoSelectedComputers.current}
autoSelectedComputers={autoSelectedComputers}
onSelectionChange={setSelection}
onToggleEnvironment={(environmentId, checked) =>
setSelection((current) => {
Expand Down Expand Up @@ -461,7 +465,7 @@ function ConnectAccountOption({
const { openAuthPrompt } = useT3ConnectAuthPrompt();
const [expanded, setExpanded] = useState(true);
const [discoveryReady, setDiscoveryReady] = useState(false);
const onDiscoveryReady = useCallback(() => setDiscoveryReady(true), []);
const onDiscoveryReady = () => setDiscoveryReady(true);

return (
<div className="rounded-lg border border-border bg-background">
Expand Down Expand Up @@ -709,7 +713,7 @@ function ConnectedAgentsStep({
void refreshProviders({ environmentId, input: {} });
}, [environmentId, refreshProviders]);

const byDriver = useMemo(() => selectOnboardingProvidersByDriver(providers), [providers]);
const byDriver = selectOnboardingProvidersByDriver(providers);

const primaryAgents = PRIMARY_AGENT_DRIVERS.flatMap((driver) => {
const instances =
Expand Down Expand Up @@ -1013,10 +1017,7 @@ function AgentInstallTerminal({
const setupGenerationRef = useRef(0);
const activeSetupGenerationRef = useRef<number | null>(null);
const [terminalId] = useState(() => `onboarding-${driver}-${randomUUID()}`);
const threadRef = useMemo(
() => scopeThreadRef(environmentId, AGENT_ONBOARDING_THREAD_ID),
[environmentId],
);
const threadRef = scopeThreadRef(environmentId, AGENT_ONBOARDING_THREAD_ID);
const [setupAttempt, setSetupAttempt] = useState(0);
const [setupState, setSetupState] = useState<
"preparing" | "ready" | "openFailed" | "writeFailed"
Expand All @@ -1030,6 +1031,7 @@ function AgentInstallTerminal({
const generation = setupGenerationRef.current + 1;
setupGenerationRef.current = generation;
activeSetupGenerationRef.current = generation;
// oxlint-disable-next-line react/set-state-in-effect -- A retry or a new command reopens the terminal, which shows as preparing until the open settles.
setSetupState("preparing");

setupQueueRef.current = setupQueueRef.current.then(async () => {
Expand Down Expand Up @@ -1087,6 +1089,7 @@ function AgentInstallTerminal({
environmentId,
openTerminal,
providerInstanceId,
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- Retry bumps setupAttempt to open the terminal again.
setupAttempt,
terminalId,
writeTerminal,
Expand Down Expand Up @@ -1208,6 +1211,7 @@ function ImportStep({
project.environmentId === landingProject.environmentId,
)
) {
// oxlint-disable-next-line react/set-state-in-effect -- The landing project is known only once the projects subscription delivers it.
setLandingProject(null);
void onDone(landingProject, importWarningRef.current, importedThreadCountRef.current).then(
(completed) => {
Expand All @@ -1217,23 +1221,16 @@ function ImportStep({
}
}, [landingProject, onDone, projects, setIsImporting]);

const { available: candidates, recent } = useMemo(
() =>
partitionOnboardingProjects(
scans.flatMap((scan) =>
(scan.data?.candidates ?? []).map((candidate) => ({
...candidate,
environmentId: scan.environmentId,
key: onboardingProjectKey(scan.environmentId, candidate.path),
})),
),
),
[scans],
);
const selectedKeys = useMemo(
() => selectedPaths ?? new Set(recent.map((candidate) => candidate.key)),
[selectedPaths, recent],
const { available: candidates, recent } = partitionOnboardingProjects(
scans.flatMap((scan) =>
(scan.data?.candidates ?? []).map((candidate) => ({
...candidate,
environmentId: scan.environmentId,
key: onboardingProjectKey(scan.environmentId, candidate.path),
})),
),
);
const selectedKeys = selectedPaths ?? new Set(recent.map((candidate) => candidate.key));
const selected = candidates.filter((candidate) => selectedKeys.has(candidate.key));

const canImport = selected.every((candidate) =>
Expand Down Expand Up @@ -1531,7 +1528,7 @@ function ImportCandidateList({
readonly selectedKeys: ReadonlySet<string>;
readonly onSelectionChange: (next: ReadonlySet<string>) => void;
}) {
const { repositories, other } = useMemo(() => groupOnboardingProjects(candidates), [candidates]);
const { repositories, other } = groupOnboardingProjects(candidates);
const setKeys = (keys: ReadonlyArray<string>, checked: boolean) => {
const next = new Set(selectedKeys);
for (const key of keys) {
Expand Down
Loading