Refine onboarding GitHub task setup (#3984)

This commit is contained in:
Jinjing
2026-05-30 21:53:51 -07:00
committed by GitHub
parent 369a06eb03
commit df1bc8038d
5 changed files with 270 additions and 187 deletions
@@ -60,7 +60,6 @@ export function FeatureWallBody(props: {
const isAgentsUsage = isAgents && agentsActiveStep?.id === 'usage'
const isAgentsStatuses = isAgents && agentsActiveStep?.id === 'statuses'
const isAgentsOrchestration = isAgents && agentsActiveStep?.id === 'orchestration'
const isOnboardingAgentsStep = source === 'onboarding' && isAgents
const isWorkbenchEditor = isWorkbench && workbenchActiveStep?.id === 'editor'
const isWorkbenchBrowser = isWorkbench && workbenchActiveStep?.id === 'browser'
const isReviewPrView = isReview && reviewActiveStep?.id === 'pr-view'
@@ -69,13 +68,10 @@ export function FeatureWallBody(props: {
const isOnboardingUsage = isAgentsUsage && source === 'onboarding'
const isOnboardingStatuses = isAgentsStatuses && source === 'onboarding'
const isOnboardingWorkbenchBrowser = isWorkbenchBrowser && source === 'onboarding'
const isReviewSettingBesideVisual = isReviewPrView || isReviewShip
const isOnboardingOrchestrationBesideVisual = isAgentsOrchestration && source === 'onboarding'
const orchestrationVisualWidthPx = isOnboardingOrchestrationBesideVisual ? 440 : 520
const orchestrationVisualHeightPx = 392
const isCompactBesideVisual =
source === 'onboarding' &&
(isAgentsUsage || isReviewSettingBesideVisual || isAgentsOrchestration)
const isReviewSettingStep = isReviewPrView || isReviewShip
const isOnboardingOrchestration = isAgentsOrchestration && source === 'onboarding'
const orchestrationVisualWidthPx = isOnboardingOrchestration ? 440 : 520
const orchestrationVisualHeightPx = isOnboardingOrchestration ? 240 : 392
const animatedVisualWidth = isWorkspaces
? 'w-[440px]'
: isWorkbenchEditor
@@ -95,33 +91,34 @@ export function FeatureWallBody(props: {
: isAgentsStatuses
? 'w-[420px]'
: isAgentsOrchestration
? isOnboardingOrchestrationBesideVisual
? isOnboardingOrchestration
? 'w-[440px]'
: 'w-[520px]'
: 'w-[520px]'
const settingWidth = isAgentsUsage
? isOnboardingUsage
? 'max-w-[400px]'
: 'max-w-[440px]'
: isAgentsStatuses
? isOnboardingStatuses
? 'max-w-[360px]'
: 'max-w-[520px]'
: isAgentsOrchestration
? isOnboardingOrchestrationBesideVisual
const settingWidth = isTasks
? 'max-w-[760px]'
: isAgentsUsage
? isOnboardingUsage
? 'max-w-[400px]'
: 'max-w-[440px]'
: isAgentsStatuses
? isOnboardingStatuses
? 'max-w-[360px]'
: 'max-w-[400px]'
: isReviewSettingBesideVisual
? isCompactBesideVisual
? 'max-w-[320px]'
: 'max-w-[420px]'
: isWorkbenchBrowser
? isOnboardingWorkbenchBrowser
? 'max-w-[340px]'
: 'max-w-[400px]'
: 'max-w-[480px]'
: 'max-w-[520px]'
: isAgentsOrchestration
? isOnboardingOrchestration
? 'max-w-[360px]'
: 'max-w-[400px]'
: isReviewSettingStep
? 'max-w-[420px]'
: isWorkbenchBrowser
? isOnboardingWorkbenchBrowser
? 'max-w-[340px]'
: 'max-w-[400px]'
: 'max-w-[480px]'
const setupTerminalHeightPx = source === 'onboarding' ? 140 : 240
const settingContent = isTasks ? (
<div className="grid grid-cols-1 gap-3">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<LinearRow compact />
<GitHubRow compact />
</div>
@@ -130,37 +127,50 @@ export function FeatureWallBody(props: {
) : isAgentsUsage ? (
<UsageAccountsCard onAccountStateChange={onUsageAccountStateChange} />
) : isAgentsOrchestration ? (
<OrchestrationSetupCard compact terminalHeightPx={240} skill={orchestrationSkill} />
<OrchestrationSetupCard
compact
terminalHeightPx={setupTerminalHeightPx}
skill={orchestrationSkill}
/>
) : isWorkbenchBrowser ? (
<BrowserUseSkillSetupCard compact terminalHeightPx={240} skill={browserUseSkill} />
<BrowserUseSkillSetupCard
compact
terminalHeightPx={setupTerminalHeightPx}
skill={browserUseSkill}
/>
) : isReviewPrView ? (
<GitHubRow compact />
) : isReviewShip ? (
<AiCommitPrSettingsCard />
) : null
const settingBesideVisual =
(isOnboardingAgentsStep && Boolean(settingContent)) ||
isAgentsUsage ||
isAgentsOrchestration ||
isReviewSettingBesideVisual
const shouldLabelOnboardingTourZones = isOnboardingAgentsStep && settingBesideVisual
const shouldLabelOnboardingTourZones =
source === 'onboarding' && hasAnimatedVisual && Boolean(settingContent)
const shouldStickSetupToBottom = shouldLabelOnboardingTourZones
// Why: several visuals expand/collapse internally; setup controls should sit
// after a stable stage so they do not jump with the animation loop.
const visualStageHeight = isWorkbenchEditor
? 'h-[390px]'
: isWorkbenchBrowser
? 'h-[270px]'
: isWorkbench
? 'h-[340px]'
: isReview
? 'h-[416px]'
: isAgentsOrchestration
? 'h-[392px]'
: isAgentsStatuses
? 'h-[250px]'
: isAgentsUsage
? 'h-[392px]'
: 'h-[330px]'
const visualStageHeight = isTasks
? 'h-[288px]'
: isWorkbenchEditor
? 'h-[390px]'
: isWorkbenchBrowser
? 'h-[270px]'
: isWorkbench
? 'h-[340px]'
: isReview
? 'h-[416px]'
: isAgentsOrchestration
? isOnboardingOrchestration
? 'h-[240px]'
: 'h-[392px]'
: isAgentsStatuses
? isOnboardingStatuses
? 'h-[200px]'
: 'h-[250px]'
: isAgentsUsage
? isOnboardingUsage
? 'h-[320px]'
: 'h-[392px]'
: 'h-[330px]'
const animatedVisual = isWorkspaces ? (
<WorkspacesAnimatedVisual reducedMotion={prefersReducedMotion} />
) : isTasks ? (
@@ -187,22 +197,26 @@ export function FeatureWallBody(props: {
reducedMotion={prefersReducedMotion}
activeStepId={agentsActiveStep.id}
widthPx={isAgentsUsage ? (isOnboardingUsage ? 360 : 400) : isAgentsStatuses ? 420 : undefined}
heightPx={isAgentsStatuses ? 250 : undefined}
heightPx={
isAgentsUsage
? isOnboardingUsage
? 320
: undefined
: isAgentsStatuses
? isOnboardingStatuses
? 200
: 250
: undefined
}
/>
) : null
const animatedVisualNode = (
<div
className={cn(
'flex w-full items-start justify-center',
visualStageHeight,
settingBesideVisual ? 'self-center' : null
)}
>
<div className={cn('flex w-full items-start justify-center', visualStageHeight)}>
<div
className={cn(
'max-w-full',
animatedVisualWidth,
isAgentsOrchestration && !isOnboardingOrchestrationBesideVisual ? 'translate-x-6' : null
isAgentsOrchestration && !isOnboardingOrchestration ? 'translate-x-6' : null
)}
>
{animatedVisual}
@@ -210,30 +224,15 @@ export function FeatureWallBody(props: {
</div>
)
const previewVisualNode = shouldLabelOnboardingTourZones ? (
<TourZone label="Preview" className="items-center">
<TourZone label="Demo" className="items-center">
{animatedVisualNode}
</TourZone>
) : (
animatedVisualNode
)
if (isTasks) {
return (
<div className="flex h-full min-h-0 flex-col px-8 pb-6 pt-1">
<div className="@container flex min-h-0 flex-1 items-start">
<div className="grid w-full items-start gap-5 @[820px]:grid-cols-[minmax(420px,560px)_minmax(280px,320px)]">
<div className="flex h-[260px] w-full items-start justify-center">
<div className="w-[520px] max-w-full">{animatedVisual}</div>
</div>
<div className="grid w-full gap-3">{settingContent}</div>
</div>
</div>
</div>
)
}
return (
<div className="flex flex-col gap-5 px-8 pb-9 pt-1">
<div className="flex min-h-full flex-col gap-4 px-8 pb-0 pt-1">
<div
className={cn(
'grid grid-cols-1 items-start gap-7',
@@ -251,82 +250,7 @@ export function FeatureWallBody(props: {
) : null}
{hasAnimatedVisual ? (
settingContent && settingBesideVisual ? (
<div className="@container w-full">
<div
className={cn(
'grid w-full items-start',
isOnboardingOrchestrationBesideVisual ||
isOnboardingStatuses ||
isOnboardingWorkbenchBrowser ||
(isCompactBesideVisual && isReviewSettingBesideVisual)
? 'gap-4'
: 'gap-5',
'justify-center',
isAgentsUsage
? isOnboardingUsage
? '@[780px]:grid-cols-[minmax(360px,400px)_minmax(340px,360px)] @[780px]:items-center'
: '@[860px]:grid-cols-[minmax(400px,440px)_minmax(360px,400px)] @[860px]:items-center'
: isOnboardingStatuses
? '@[760px]:grid-cols-[minmax(320px,360px)_minmax(400px,420px)] @[760px]:items-center'
: isWorkbenchBrowser
? isOnboardingWorkbenchBrowser
? '@[800px]:grid-cols-[minmax(320px,340px)_minmax(440px,460px)] @[800px]:items-center'
: '@[880px]:grid-cols-[minmax(360px,400px)_minmax(440px,480px)] @[880px]:items-center'
: isOnboardingOrchestrationBesideVisual
? '@[800px]:grid-cols-[minmax(340px,360px)_minmax(420px,440px)] @[800px]:items-center'
: isReviewSettingBesideVisual
? isCompactBesideVisual
? '@[800px]:grid-cols-[minmax(300px,320px)_minmax(460px,480px)] @[800px]:items-center'
: cn(
'@[840px]:grid-cols-[minmax(380px,420px)_minmax(440px,480px)]',
isReviewShip ? '@[840px]:items-start' : '@[840px]:items-center'
)
: isCompactBesideVisual
? '@[700px]:grid-cols-[minmax(300px,340px)_minmax(320px,340px)] @[700px]:items-center'
: isAgentsOrchestration
? '@[860px]:grid-cols-[minmax(340px,380px)_auto] @[860px]:items-center'
: '@[760px]:grid-cols-[auto_minmax(320px,420px)] @[760px]:items-center'
)}
>
<TourZone
label={shouldLabelOnboardingTourZones ? 'Setup' : null}
className={cn(
'w-full',
isReviewShip ? 'translate-y-0.5 self-start' : 'self-center',
isAgentsUsage
? isOnboardingUsage
? 'max-w-[400px]'
: 'max-w-[440px]'
: isAgentsStatuses
? isOnboardingStatuses
? 'max-w-[360px]'
: 'max-w-[520px]'
: isWorkbenchBrowser
? isOnboardingWorkbenchBrowser
? 'max-w-[340px]'
: 'max-w-[400px]'
: isOnboardingOrchestrationBesideVisual
? 'max-w-[360px]'
: isCompactBesideVisual
? isReviewSettingBesideVisual
? 'max-w-[320px]'
: 'max-w-[340px]'
: isAgentsOrchestration
? 'max-w-[400px]'
: isReviewSettingBesideVisual
? 'max-w-[420px]'
: 'max-w-[420px]'
)}
>
{settingContent}
</TourZone>
{previewVisualNode}
</div>
</div>
) : (
animatedVisualNode
)
previewVisualNode
) : (
<aside className="flex flex-col gap-5">
{selected.relatedTileIds.length > 0 ? (
@@ -335,8 +259,25 @@ export function FeatureWallBody(props: {
</aside>
)}
</div>
{settingContent && !settingBesideVisual ? (
<div className={cn('mx-auto w-full', settingWidth)}>{settingContent}</div>
{settingContent && shouldStickSetupToBottom ? (
<div className="sticky bottom-0 z-10 -mx-8 mt-auto border-t border-border bg-card/95 px-8 py-3 backdrop-blur supports-[backdrop-filter]:bg-card/85">
<TourZone
label="Setup"
className={cn(
'scrollbar-sleek mx-auto max-h-[220px] w-full overflow-y-auto',
settingWidth
)}
>
{settingContent}
</TourZone>
</div>
) : settingContent ? (
<TourZone
label={shouldLabelOnboardingTourZones ? 'Setup' : null}
className={cn('mx-auto w-full', settingWidth)}
>
{settingContent}
</TourZone>
) : null}
</div>
)
@@ -167,20 +167,18 @@ export function LinearRow(props: { compact?: boolean } = {}): React.JSX.Element
}
const CAPABILITIES = [
'Start a workspace from any issue, PR, or Linear ticket, prefilled with its title and context',
'Browse your assigned tasks in the Tasks view without leaving Orca',
'See issue state, PR review status, and CI checks on every worktree',
'Start a workspace from any GitHub issue or pull request, prefilled with its title and context',
'Browse GitHub issues and pull requests in the Tasks view without leaving Orca',
'See issue state, review status, and CI checks on every worktree',
'Read, comment on, and merge pull requests without leaving Orca'
] as const
export function IntegrationsStep(): React.JSX.Element {
const refreshPreflightStatus = useAppStore((s) => s.refreshPreflightStatus)
const checkLinearConnection = useAppStore((s) => s.checkLinearConnection)
useEffect(() => {
void refreshPreflightStatus()
void checkLinearConnection()
}, [checkLinearConnection, refreshPreflightStatus])
}, [refreshPreflightStatus])
return (
<div className="space-y-6">
@@ -195,15 +193,14 @@ export function IntegrationsStep(): React.JSX.Element {
<div className="space-y-3">
<GitHubRow />
<LinearRow />
<div className="mt-4 flex items-center justify-between rounded-xl border border-border bg-muted/10 px-5 py-3.5">
<div className="flex items-center gap-3">
<span className="text-[14px] font-medium text-foreground/70">Jira</span>
<span className="text-[13px] text-muted-foreground">
Issues, sprints, and assignees.
<div className="mt-4 rounded-xl border border-border bg-muted/10 px-5 py-4">
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
<span className="text-[14px] font-medium text-foreground/70">More task sources</span>
<span className="text-[13px] leading-relaxed text-muted-foreground">
Linear, GitLab, Bitbucket, Azure DevOps, Gitea, and Jira live in Settings &gt;
Integrations.
</span>
</div>
<IntegrationStatusPill tone="neutral">Coming soon</IntegrationStatusPill>
</div>
</div>
</div>
@@ -64,6 +64,60 @@ describe('OnboardingFlow', () => {
expect(html).not.toContain('>Skip</button>')
})
it('skips GitHub task setup when the GitHub CLI is already detected', () => {
useAppStore.setState({
preflightStatus: {
git: { installed: true },
gh: { installed: true, authenticated: false }
},
preflightStatusChecked: true
})
const html = renderToStaticMarkup(
<OnboardingFlow
onboarding={{
...getDefaultOnboardingState(),
lastCompletedStep: 4
}}
onOnboardingChange={vi.fn()}
/>
)
expect(html).toContain('Explore Orca')
expect(html).not.toContain('Set up GitHub tasks')
expect(html).not.toContain('Connect your task sources')
})
it('shows only GitHub on the task setup page when the GitHub CLI is missing', () => {
useAppStore.setState({
preflightStatus: {
git: { installed: true },
gh: { installed: false, authenticated: false }
},
preflightStatusChecked: true
})
const html = renderToStaticMarkup(
<OnboardingFlow
onboarding={{
...getDefaultOnboardingState(),
lastCompletedStep: 4
}}
onOnboardingChange={vi.fn()}
/>
)
expect(html).toContain('Set up GitHub tasks')
expect(html).toContain('Install the GitHub CLI to:')
expect(html).toContain('GitHub')
expect(html).not.toContain(
'<h3 class="text-[15px] font-semibold leading-tight text-foreground">Linear</h3>'
)
expect(html).toContain(
'Linear, GitLab, Bitbucket, Azure DevOps, Gitea, and Jira live in Settings'
)
})
it('renders onboarding inside a centered modal shell', () => {
const html = renderToStaticMarkup(
<OnboardingFlow onboarding={getDefaultOnboardingState()} onOnboardingChange={vi.fn()} />
@@ -11,7 +11,7 @@ import { AgentFeatureSetupStep } from './AgentFeatureSetupStep'
import { IntegrationsStep } from './IntegrationsStep'
import { RepoStep } from './RepoStep'
import { OnboardingTourStep } from './OnboardingTourStep'
import { STEPS, useOnboardingFlow } from './use-onboarding-flow'
import { useOnboardingFlow } from './use-onboarding-flow'
import { OnboardingSkipConfirmationDialog } from './OnboardingSkipConfirmationDialog'
import { OnboardingFooter } from './OnboardingFooter'
import { shouldRequestOnboardingSkipConfirmation } from './onboarding-dismiss-target'
@@ -36,8 +36,8 @@ const stepCopy = {
subtitle: 'Turn on advanced Orca capabilities for agents.'
},
integrations: {
title: 'Connect your task sources',
subtitle: 'Connect GitHub or Linear to:'
title: 'Set up GitHub tasks',
subtitle: 'Install the GitHub CLI to:'
},
tour: {
title: 'Explore Orca',
@@ -214,9 +214,9 @@ export default function OnboardingFlow({
) : (
<div className="mt-10 flex items-center gap-2 transition-[margin-top] duration-[760ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none">
<TooltipProvider delayDuration={0} skipDelayDuration={0}>
{STEPS.map((step, idx) => {
const isActive = idx === stepIndex
const isDone = idx < stepIndex
{flow.visibleSteps.map(({ step, index: realStepIndex }, visibleIdx) => {
const isActive = realStepIndex === stepIndex
const isDone = realStepIndex < stepIndex
return (
<Tooltip key={step.id}>
<TooltipTrigger asChild>
@@ -232,9 +232,9 @@ export default function OnboardingFlow({
? 'w-6 bg-muted-foreground/70 hover:bg-foreground/80'
: 'w-6 bg-muted-foreground/25 hover:bg-muted-foreground/45'
)}
aria-label={`Go to onboarding step ${step.stepNumber}: ${stepCopy[step.id].title}`}
aria-label={`Go to onboarding step ${visibleIdx + 1}: ${stepCopy[step.id].title}`}
aria-current={isActive ? 'step' : undefined}
onClick={() => flow.jumpToStep(idx)}
onClick={() => flow.jumpToStep(realStepIndex)}
/>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={8} style={{ zIndex: 110 }}>
@@ -245,7 +245,7 @@ export default function OnboardingFlow({
})}
</TooltipProvider>
<span className="ml-3 text-xs font-medium text-muted-foreground">
{stepIndex + 1} of {STEPS.length}
{flow.visibleStepIndex + 1} of {flow.visibleSteps.length}
</span>
</div>
)}
@@ -57,6 +57,33 @@ type TaskSourcesGithubStatus = TaskSourcesSnapshotProps['github_status']
type TaskSourcesLinearStatus = TaskSourcesSnapshotProps['linear_status']
type TaskSourcesExitAction = TaskSourcesSnapshotProps['exit_action']
function shouldSkipIntegrationsStep(
status: ReturnType<typeof useAppStore.getState>['preflightStatus']
): boolean {
return status?.gh.installed === true
}
function isSkippedStepIndex(index: number, skipIntegrations: boolean): boolean {
return skipIntegrations && STEPS[index]?.id === 'integrations'
}
function resolveStepIndex(
index: number,
skipIntegrations: boolean,
direction: 'forward' | 'backward'
): number {
const lastIndex = STEPS.length - 1
let nextIndex = Math.min(Math.max(index, 0), lastIndex)
while (isSkippedStepIndex(nextIndex, skipIntegrations)) {
const candidate = nextIndex + (direction === 'forward' ? 1 : -1)
if (candidate < 0 || candidate > lastIndex) {
return direction === 'forward' ? lastIndex : 0
}
nextIndex = candidate
}
return nextIndex
}
function defaultProjectGroupNameForPath(path: string): string {
return (
path
@@ -112,14 +139,24 @@ export function useOnboardingFlow(
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
const openSettingsTarget = useAppStore((s) => s.openSettingsTarget)
const preflightStatus = useAppStore((s) => s.preflightStatus)
const preflightStatusChecked = useAppStore((s) => s.preflightStatusChecked)
const preflightStatusLoading = useAppStore((s) => s.preflightStatusLoading)
const refreshPreflightStatus = useAppStore((s) => s.refreshPreflightStatus)
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)
// Why: renderToStaticMarkup uses Zustand's initial server snapshot. The
// synchronous read keeps tests and the first client render aligned.
const effectivePreflightStatus = preflightStatus ?? useAppStore.getState().preflightStatus
const initialStep = Math.min(Math.max(onboarding.lastCompletedStep, 0), STEPS.length - 1)
const skipIntegrations = shouldSkipIntegrationsStep(effectivePreflightStatus)
const initialStep = resolveStepIndex(
Math.min(Math.max(onboarding.lastCompletedStep, 0), STEPS.length - 1),
skipIntegrations,
'forward'
)
const [stepIndex, setStepIndex] = useState(initialStep)
const [selectedAgent, setSelectedAgent] = useState<TuiAgent | null>(
settings?.defaultTuiAgent && settings.defaultTuiAgent !== 'blank'
@@ -233,6 +270,17 @@ export function useOnboardingFlow(
const detectedSet = useMemo(() => new Set(detectedAgentIds ?? []), [detectedAgentIds])
const currentStep = STEPS[stepIndex]
const visibleSteps = useMemo(
() =>
STEPS.map((step, index) => ({ step, index })).filter(
({ index }) => !isSkippedStepIndex(index, skipIntegrations)
),
[skipIntegrations]
)
const visibleStepIndex = Math.max(
0,
visibleSteps.findIndex(({ index }) => index === stepIndex)
)
const hasExistingProject = repos.length > 0
// Why: pin start time once so onboarding_completed reports a real funnel duration.
@@ -273,6 +321,43 @@ export function useOnboardingFlow(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
void refreshPreflightStatus()
}, [refreshPreflightStatus])
const getNextStepIndex = useCallback(
(idx: number): number => resolveStepIndex(idx + 1, skipIntegrations, 'forward'),
[skipIntegrations]
)
const getPreviousStepIndex = useCallback(
(idx: number): number => resolveStepIndex(idx - 1, skipIntegrations, 'backward'),
[skipIntegrations]
)
useEffect(() => {
if (currentStep.id !== 'integrations' || !preflightStatusChecked || !skipIntegrations) {
return
}
const nextIndex = getNextStepIndex(stepIndex)
setStepIndex(nextIndex)
// Why: users with gh already on PATH don't need this setup page, but
// persistence must still resume them at the tour instead of bouncing back.
void persistStep(currentStep.stepNumber).then(onOnboardingChange, (err) => {
toast.error('Could not save progress', {
description: err instanceof Error ? err.message : String(err)
})
})
}, [
currentStep.id,
currentStep.stepNumber,
getNextStepIndex,
onOnboardingChange,
preflightStatusChecked,
skipIntegrations,
stepIndex
])
// Why: ref guard prevents StrictMode's double-invoke from emitting
// `onboarding_started` twice on mount.
const startedTrackedRef = useRef(false)
@@ -520,7 +605,7 @@ export function useOnboardingFlow(
return
}
if (currentStep.id === 'agentSetup' && featureSetupTerminalCommand) {
setStepIndex((idx) => Math.min(idx + 1, STEPS.length - 1))
setStepIndex(getNextStepIndex)
return
}
nextInFlightRef.current = true
@@ -535,7 +620,7 @@ export function useOnboardingFlow(
}
if (result.ok) {
trackCurrentStepCompleted(advancedVia)
setStepIndex((idx) => Math.min(idx + 1, STEPS.length - 1))
setStepIndex(getNextStepIndex)
}
} finally {
nextInFlightRef.current = false
@@ -546,6 +631,7 @@ export function useOnboardingFlow(
currentStep.id,
featureSetupSelection,
featureSetupTerminalCommand,
getNextStepIndex,
persistCurrentStep,
trackCurrentStepCompleted
]
@@ -1138,7 +1224,7 @@ export function useOnboardingFlow(
})
setFeatureSetupTerminalCommand(null)
setFeatureSetupTerminalSelection(null)
setStepIndex((idx) => Math.min(idx + 1, STEPS.length - 1))
setStepIndex(getNextStepIndex)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
setError(message)
@@ -1150,6 +1236,7 @@ export function useOnboardingFlow(
currentStep.id,
currentStep.stepNumber,
currentStep.valueKind,
getNextStepIndex,
onOnboardingChange
])
@@ -1190,8 +1277,8 @@ export function useOnboardingFlow(
return
}
setTourStarted(false)
setStepIndex((idx) => Math.max(idx - 1, 0))
}, [nestedScan, trackNestedBackAndClear])
setStepIndex(getPreviousStepIndex)
}, [getPreviousStepIndex, nestedScan, trackNestedBackAndClear])
// Why: returns the user to the "Take the tour" intro without leaving the
// tour step. Don't emit the tour outcome here — re-entry must still let
@@ -1207,15 +1294,19 @@ export function useOnboardingFlow(
trackNestedBackAndClear()
}
setTourStarted(false)
setStepIndex(Math.min(Math.max(idx, 0), STEPS.length - 1))
setStepIndex(
resolveStepIndex(idx, skipIntegrations, idx < stepIndex ? 'backward' : 'forward')
)
},
[nestedScan, stepIndex, trackNestedBackAndClear]
[nestedScan, skipIntegrations, stepIndex, trackNestedBackAndClear]
)
return {
settings,
updateSettings,
stepIndex,
visibleSteps,
visibleStepIndex,
currentStep,
selectedAgent,
setSelectedAgent: setSelectedAgentInteractive,