Fix onboarding rerun project continuation (#2735)

This commit is contained in:
Neil 2026-05-23 21:44:46 -07:00 committed by GitHub
parent f0627cccc6
commit 83c90db261
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 96 additions and 4 deletions

View File

@ -84,7 +84,12 @@ export default function OnboardingFlow({
const shouldShowSkipToProjectSetup = currentStep.id !== 'repo' && currentStep.id !== 'tour'
const shouldShowStepHeading = !isTourStep
const footerPrimaryLabel = isTourStep ? 'Skip the tour' : primaryActionLabel
const { next: flowNext, openFolder: flowOpenFolder, skipTourToRepo: flowSkipTourToRepo } = flow
const {
next: flowNext,
openFolder: flowOpenFolder,
continueWithExistingProject: flowContinueWithExistingProject,
skipTourToRepo: flowSkipTourToRepo
} = flow
// Why: depend on stable callbacks + step id only so the listener doesn't
// re-bind on every render of the parent (flow object identity changes).
useEffect(() => {
@ -110,14 +115,26 @@ export default function OnboardingFlow({
return
}
if (currentStep.id === 'repo') {
void flowOpenFolder()
if (flow.hasExistingProject) {
void flowContinueWithExistingProject('keyboard')
} else {
void flowOpenFolder()
}
} else {
void flowNext('keyboard')
}
}
window.addEventListener('keydown', onKeyDown, { capture: true })
return () => window.removeEventListener('keydown', onKeyDown, { capture: true })
}, [currentStep.id, flowNext, flowOpenFolder, flowSkipTourToRepo, tourStarted])
}, [
currentStep.id,
flow.hasExistingProject,
flowContinueWithExistingProject,
flowNext,
flowOpenFolder,
flowSkipTourToRepo,
tourStarted
])
return (
<div
@ -319,7 +336,7 @@ export default function OnboardingFlow({
Skip
</button>
)}
{currentStep.id !== 'repo' && (
{(currentStep.id !== 'repo' || flow.hasExistingProject) && (
<button
className="inline-flex items-center justify-center gap-2 rounded-md bg-primary px-5 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-60"
aria-busy={Boolean(busyLabel)}
@ -329,6 +346,10 @@ export default function OnboardingFlow({
void flow.skipTourToRepo()
return
}
if (currentStep.id === 'repo') {
void flow.continueWithExistingProject()
return
}
void flow.next()
}}
>

View File

@ -0,0 +1,36 @@
import { renderToStaticMarkup } from 'react-dom/server'
import type { ComponentProps } from 'react'
import { describe, expect, it, vi } from 'vitest'
import { RepoStep } from './RepoStep'
function renderRepoStep(overrides: Partial<ComponentProps<typeof RepoStep>> = {}): string {
return renderToStaticMarkup(
<RepoStep
cloneUrl=""
onCloneUrlChange={vi.fn()}
onOpenFolder={vi.fn()}
onOpenServerFolder={vi.fn()}
onClone={vi.fn()}
onOpenSshSettings={vi.fn()}
serverPath=""
onServerPathChange={vi.fn()}
cloneDestination=""
onCloneDestinationChange={vi.fn()}
workspaceDir="/workspace"
runtimeActive={false}
busyLabel={null}
error={null}
{...overrides}
/>
)
}
describe('RepoStep', () => {
it('renders the add project options without existing-project chrome', () => {
const html = renderRepoStep()
expect(html).not.toContain('Project already added')
expect(html).toContain('Open a folder')
expect(html).toContain('Clone a repo')
})
})

View File

@ -88,6 +88,9 @@ export function useOnboardingFlow(
const preflightStatusLoading = useAppStore((s) => s.preflightStatusLoading)
const linearStatus = useAppStore((s) => s.linearStatus)
const linearStatusChecked = useAppStore((s) => s.linearStatusChecked)
// Why: App hydrates repos before mounting onboarding. Reading the store
// synchronously lets the final step render its already-added state without a flash.
const repos = useAppStore((s) => s.repos)
const initialStep = Math.min(Math.max(onboarding.lastCompletedStep, 0), STEPS.length - 1)
const [stepIndex, setStepIndex] = useState(initialStep)
@ -193,6 +196,7 @@ export function useOnboardingFlow(
const detectedSet = useMemo(() => new Set(detectedAgentIds ?? []), [detectedAgentIds])
const currentStep = STEPS[stepIndex]
const hasExistingProject = repos.length > 0
// Why: refs let `setSelectedAgentInteractive` (a stable useCallback) read
// the freshest detection snapshot at click time without re-rebinding the
@ -634,6 +638,35 @@ export function useOnboardingFlow(
}
}, [busyLabel, cloneDestination, cloneUrl, completeRepo, settings])
const continueWithExistingProject = useCallback(
async (advancedVia: 'button' | 'keyboard' = 'button') => {
if (busyLabel !== null || currentStep.id !== 'repo' || repos.length === 0) {
return
}
setError(null)
setBusyLabel('Finishing...')
try {
const checklist = repos.some((repo) => isGitRepoKind(repo))
? { addedRepo: true }
: { addedFolder: true }
const closed = await closeWith('completed', checklist, ONBOARDING_FINAL_STEP)
if (!closed) {
return
}
emitPendingTourOutcome()
track('onboarding_step_completed', {
step: ONBOARDING_FINAL_STEP,
value_kind: 'repo',
duration_ms: consumeStepDurationMs(),
advanced_via: advancedVia
})
} finally {
setBusyLabel(null)
}
},
[busyLabel, closeWith, consumeStepDurationMs, currentStep.id, emitPendingTourOutcome, repos]
)
const skipToRepo = useCallback(async () => {
if (busyLabel) {
return
@ -882,6 +915,7 @@ export function useOnboardingFlow(
hasSelectedFeatureSetup,
cloneUrl,
setCloneUrl,
hasExistingProject,
serverPath,
setServerPath,
cloneDestination,
@ -901,6 +935,7 @@ export function useOnboardingFlow(
back,
jumpToStep,
openFolder,
continueWithExistingProject,
openSshSettings,
clone
}