| ... | @@ -99,7 +99,7 @@ export function WelcomeWizard({ | ... | @@ -99,7 +99,7 @@ export function WelcomeWizard({ |
| 99 | }: { | 99 | }: { |
| 100 | /** Whether this client is authenticated to the server serving the app. */ | 100 | /** Whether this client is authenticated to the server serving the app. */ |
| 101 | readonly localAvailable: boolean; | 101 | readonly localAvailable: boolean; |
| 102 | readonly onDone: (projectRef?: ScopedProjectRef) => void; | 102 | readonly onDone: (projectRef?: ScopedProjectRef) => void | Promise<void>; |
| 103 | }) { | 103 | }) { |
| 104 | const completeOnboarding = useCompleteOnboarding(); | 104 | const completeOnboarding = useCompleteOnboarding(); |
| 105 | const [step, setStep] = useState<WizardStep>("connection"); | 105 | const [step, setStep] = useState<WizardStep>("connection"); |
| ... | @@ -141,7 +141,7 @@ export function WelcomeWizard({ | ... | @@ -141,7 +141,7 @@ export function WelcomeWizard({ |
| 141 | }; | 141 | }; |
| 142 | const stageIndex = step === "agents" ? 1 : step === "import" ? 2 : 0; | 142 | const stageIndex = step === "agents" ? 1 : step === "import" ? 2 : 0; |
| 143 | const finish = useCallback( | 143 | const finish = useCallback( |
| 144 | (projectRef?: ScopedProjectRef) => { | 144 | (projectRef?: ScopedProjectRef, importWarning?: string, importedThreadCount = 0) => { |
| 145 | if (finishingPromiseRef.current !== null) return finishingPromiseRef.current; | 145 | if (finishingPromiseRef.current !== null) return finishingPromiseRef.current; |
| 146 | if (completionErrorToastIdRef.current !== null) { | 146 | if (completionErrorToastIdRef.current !== null) { |
| 147 | toastManager.close(completionErrorToastIdRef.current); | 147 | toastManager.close(completionErrorToastIdRef.current); |
| ... | @@ -149,12 +149,25 @@ export function WelcomeWizard({ | ... | @@ -149,12 +149,25 @@ export function WelcomeWizard({ |
| 149 | } | 149 | } |
| 150 | | 150 | |
| 151 | const completion = completeOnboarding() | 151 | const completion = completeOnboarding() |
| 152 | .then(() => { | 152 | .then(async () => { |
| 153 | if (completionErrorToastIdRef.current !== null) { | 153 | if (completionErrorToastIdRef.current !== null) { |
| 154 | toastManager.close(completionErrorToastIdRef.current); | 154 | toastManager.close(completionErrorToastIdRef.current); |
| 155 | completionErrorToastIdRef.current = null; | 155 | completionErrorToastIdRef.current = null; |
| 156 | } | 156 | } |
| 157 | onDone(projectRef); | 157 | await onDone(projectRef); |
| | 158 | if (importWarning) { |
| | 159 | toastManager.add({ |
| | 160 | type: "warning", |
| | 161 | title: "Some history was not imported", |
| | 162 | description: importWarning, |
| | 163 | timeout: 0, |
| | 164 | }); |
| | 165 | } else if (importedThreadCount > 0) { |
| | 166 | toastManager.add({ |
| | 167 | type: "success", |
| | 168 | title: `Imported ${importedThreadCount} ${importedThreadCount === 1 ? "thread" : "threads"}`, |
| | 169 | }); |
| | 170 | } |
| 158 | return true; | 171 | return true; |
| 159 | }) | 172 | }) |
| 160 | .catch(() => { | 173 | .catch(() => { |
| ... | @@ -952,14 +965,19 @@ function ImportStep({ | ... | @@ -952,14 +965,19 @@ function ImportStep({ |
| 952 | readonly scans: ReturnType<typeof useProjectScans>; | 965 | readonly scans: ReturnType<typeof useProjectScans>; |
| 953 | readonly isImporting: boolean; | 966 | readonly isImporting: boolean; |
| 954 | readonly setIsImporting: (value: boolean) => void; | 967 | readonly setIsImporting: (value: boolean) => void; |
| 955 | readonly onDone: (projectRef?: ScopedProjectRef) => Promise<boolean>; | 968 | readonly onDone: ( |
| | 969 | projectRef?: ScopedProjectRef, |
| | 970 | importWarning?: string, |
| | 971 | importedThreadCount?: number, |
| | 972 | ) => Promise<boolean>; |
| 956 | }) { | 973 | }) { |
| 957 | const { environments } = useEnvironments(); | 974 | const { environments } = useEnvironments(); |
| 958 | const createProject = useAtomCommand(projectEnvironment.create, { reportFailure: false }); | 975 | const createProject = useAtomCommand(projectEnvironment.create, { reportFailure: false }); |
| 959 | const importThreads = useAtomCommand(agentSessionImport, { reportFailure: false }); | 976 | const importThreads = useAtomCommand(agentSessionImport, { reportFailure: false }); |
| 960 | const projects = useProjects(); | 977 | const projects = useProjects(); |
| 961 | const [selectedPaths, setSelectedPaths] = useState<ReadonlySet<string> | null>(null); | 978 | const [selectedPaths, setSelectedPaths] = useState<ReadonlySet<string> | null>(null); |
| 962 | const [importError, setImportError] = useState(""); | 979 | const importWarningRef = useRef(""); |
| | 980 | const importedThreadCountRef = useRef(0); |
| 963 | const [landingProject, setLandingProject] = useState<ScopedProjectRef | null>(null); | 981 | const [landingProject, setLandingProject] = useState<ScopedProjectRef | null>(null); |
| 964 | // Keep project creation attempts separate from completed history imports so both can retry. | 982 | // Keep project creation attempts separate from completed history imports so both can retry. |
| 965 | const importedProjectsRef = useRef(new Map<string, ScopedProjectRef>()); | 983 | const importedProjectsRef = useRef(new Map<string, ScopedProjectRef>()); |
| ... | @@ -988,9 +1006,11 @@ function ImportStep({ | ... | @@ -988,9 +1006,11 @@ function ImportStep({ |
| 988 | ) | 1006 | ) |
| 989 | ) { | 1007 | ) { |
| 990 | setLandingProject(null); | 1008 | setLandingProject(null); |
| 991 | void onDone(landingProject).then((completed) => { | 1009 | void onDone(landingProject, importWarningRef.current, importedThreadCountRef.current).then( |
| 992 | if (!completed) setIsImporting(false); | 1010 | (completed) => { |
| 993 | }); | 1011 | if (!completed) setIsImporting(false); |
| | 1012 | }, |
| | 1013 | ); |
| 994 | } | 1014 | } |
| 995 | }, [landingProject, onDone, projects, setIsImporting]); | 1015 | }, [landingProject, onDone, projects, setIsImporting]); |
| 996 | | 1016 | |
| ... | @@ -1020,7 +1040,7 @@ function ImportStep({ | ... | @@ -1020,7 +1040,7 @@ function ImportStep({ |
| 1020 | importedProjectsRef.current, | 1040 | importedProjectsRef.current, |
| 1021 | ); | 1041 | ); |
| 1022 | if (projectRef === undefined) { | 1042 | if (projectRef === undefined) { |
| 1023 | void onDone(); | 1043 | void onDone(undefined, importWarningRef.current, importedThreadCountRef.current); |
| 1024 | return; | 1044 | return; |
| 1025 | } | 1045 | } |
| 1026 | setIsImporting(true); | 1046 | setIsImporting(true); |
| ... | @@ -1034,7 +1054,8 @@ function ImportStep({ | ... | @@ -1034,7 +1054,8 @@ function ImportStep({ |
| 1034 | return; | 1054 | return; |
| 1035 | } | 1055 | } |
| 1036 | setIsImporting(true); | 1056 | setIsImporting(true); |
| 1037 | setImportError(""); | 1057 | importWarningRef.current = ""; |
| | 1058 | importedThreadCountRef.current = 0; |
| 1038 | lastImportSelectionRef.current = selection.map((candidate) => candidate.key); | 1059 | lastImportSelectionRef.current = selection.map((candidate) => candidate.key); |
| 1039 | const importGeneration = importGenerationRef.current; | 1060 | const importGeneration = importGenerationRef.current; |
| 1040 | const importedProjects = importedProjectsRef.current; | 1061 | const importedProjects = importedProjectsRef.current; |
| ... | @@ -1130,23 +1151,17 @@ function ImportStep({ | ... | @@ -1130,23 +1151,17 @@ function ImportStep({ |
| 1130 | if (refreshEnvironments.has(scan.environmentId)) scan.refresh(); | 1151 | if (refreshEnvironments.has(scan.environmentId)) scan.refresh(); |
| 1131 | } | 1152 | } |
| 1132 | setIsImporting(false); | 1153 | setIsImporting(false); |
| | 1154 | importedThreadCountRef.current = importedThreadCount; |
| 1133 | if (importedProjectsCount < selection.length) { | 1155 | if (importedProjectsCount < selection.length) { |
| 1134 | if (importedThreadCount > 0 && skippedThreadCount > 0) { | 1156 | if (importedThreadCount > 0 && skippedThreadCount > 0) { |
| 1135 | setImportError( | 1157 | importWarningRef.current = `Imported ${importedThreadCount} ${importedThreadCount === 1 ? "thread" : "threads"}. ${skippedThreadCount} ${skippedThreadCount === 1 ? "thread" : "threads"} could not be imported.`; |
| 1136 | `Imported ${importedThreadCount} ${importedThreadCount === 1 ? "thread" : "threads"}. ${skippedThreadCount} ${skippedThreadCount === 1 ? "thread" : "threads"} could not be imported.`, | | |
| 1137 | ); | | |
| 1138 | } else if (skippedThreadCount > 0) { | 1158 | } else if (skippedThreadCount > 0) { |
| 1139 | setImportError( | 1159 | importWarningRef.current = `${skippedThreadCount} ${skippedThreadCount === 1 ? "thread could" : "threads could"} not be imported.`; |
| 1140 | `${skippedThreadCount} ${skippedThreadCount === 1 ? "thread could" : "threads could"} not be imported.`, | | |
| 1141 | ); | | |
| 1142 | } else if (importedThreadCount > 0) { | 1160 | } else if (importedThreadCount > 0) { |
| 1143 | setImportError( | 1161 | importWarningRef.current = `Imported ${importedThreadCount} ${importedThreadCount === 1 ? "thread" : "threads"}. Some thread history could not be imported.`; |
| 1144 | `Imported ${importedThreadCount} ${importedThreadCount === 1 ? "thread" : "threads"}. Some thread history could not be imported.`, | | |
| 1145 | ); | | |
| 1146 | } else { | 1162 | } else { |
| 1147 | setImportError("Could not import thread history."); | 1163 | importWarningRef.current = "Could not import thread history."; |
| 1148 | } | 1164 | } |
| 1149 | return; | | |
| 1150 | } | 1165 | } |
| 1151 | finishAfterImport(); | 1166 | finishAfterImport(); |
| 1152 | }; | 1167 | }; |
| ... | @@ -1253,14 +1268,9 @@ function ImportStep({ | ... | @@ -1253,14 +1268,9 @@ function ImportStep({ |
| 1253 | })} | 1268 | })} |
| 1254 | </div> | 1269 | </div> |
| 1255 | </ScrollArea> | 1270 | </ScrollArea> |
| 1256 | {importError ? <p className="mt-3 text-sm text-destructive">{importError}</p> : null} | | |
| 1257 | <div className="mt-6 flex flex-wrap items-center justify-end gap-3"> | 1271 | <div className="mt-6 flex flex-wrap items-center justify-end gap-3"> |
| 1258 | <Button | 1272 | <Button variant="ghost-muted" disabled={isImporting} onClick={finishAfterImport}> |
| 1259 | variant="ghost-muted" | 1273 | Do not import projects |
| 1260 | disabled={isImporting} | | |
| 1261 | onClick={importError ? finishAfterImport : () => void onDone()} | | |
| 1262 | > | | |
| 1263 | {importError ? "Continue without the rest" : "Do not import projects"} | | |
| 1264 | </Button> | 1274 | </Button> |
| 1265 | <Button | 1275 | <Button |
| 1266 | autoFocus | 1276 | autoFocus |