Fix onboarding tour footer and copy (#2799)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson
2026-05-25 18:56:43 -07:00
committed by GitHub
co-authored by Orca
parent a33eedfd02
commit e0af80ddec
14 changed files with 284 additions and 234 deletions
@@ -14,13 +14,15 @@ describe('AgentFeatureSetupStep', () => {
onFeatureSetupChange={vi.fn()}
featureSetupCommand={null}
featureSetupCommandSelection={null}
setupBusyLabel={null}
onStartFeatureSetup={vi.fn()}
/>
)
expect(html).toContain('Set up agent features')
expect(html).toContain('Agent Browser Use')
expect(html).toContain('Computer Use')
expect(html).toContain('Agent Orchestration')
expect(html).toContain('Enable capabilities')
expect(html).toContain('role="checkbox"')
})
})
@@ -1,23 +1,49 @@
import { Loader2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { FeatureSetupChecklist } from './FeatureSetupChecklist'
import { FeatureSetupInlineTerminal } from './FeatureSetupInlineTerminal'
import type { OnboardingFeatureSetupSelection } from './onboarding-feature-setup'
import {
hasSelectedOnboardingFeatureSetup,
type OnboardingFeatureSetupSelection
} from './onboarding-feature-setup'
type AgentFeatureSetupStepProps = {
featureSetup: OnboardingFeatureSetupSelection
onFeatureSetupChange: (value: OnboardingFeatureSetupSelection) => void
featureSetupCommand: string | null
featureSetupCommandSelection: OnboardingFeatureSetupSelection | null
setupBusyLabel: string | null
onStartFeatureSetup: () => void
}
export function AgentFeatureSetupStep({
featureSetup,
onFeatureSetupChange,
featureSetupCommand,
featureSetupCommandSelection
featureSetupCommandSelection,
setupBusyLabel,
onStartFeatureSetup
}: AgentFeatureSetupStepProps): React.JSX.Element {
const hasSelectedFeatures = hasSelectedOnboardingFeatureSetup(featureSetup)
const showSetupAction = !featureSetupCommand
return (
<>
<FeatureSetupChecklist value={featureSetup} onChange={onFeatureSetupChange} />
{showSetupAction ? (
<div className="mt-4 flex items-center">
<Button
type="button"
variant="default"
className="shrink-0"
disabled={!hasSelectedFeatures || Boolean(setupBusyLabel)}
onClick={onStartFeatureSetup}
>
{setupBusyLabel ? <Loader2 className="size-4 animate-spin" /> : null}
{setupBusyLabel ?? 'Enable capabilities'}
</Button>
</div>
) : null}
{featureSetupCommand ? (
<FeatureSetupInlineTerminal
command={featureSetupCommand}
@@ -1,9 +1,5 @@
import { useEffect, useState, type ReactNode } from 'react'
import type { ReactNode } from 'react'
import { Check, Globe2, MonitorCog, Workflow } from 'lucide-react'
import {
AGENT_SKILL_CLI_PREREQUISITE_NOTICE,
isOrcaCliAvailableOnPath
} from '@/lib/agent-skill-cli-prerequisite'
import { cn } from '@/lib/utils'
import type {
OnboardingFeatureSetupId,
@@ -51,46 +47,8 @@ export function FeatureSetupChecklist({
value,
onChange
}: FeatureSetupChecklistProps): React.JSX.Element {
const [showCliNotice, setShowCliNotice] = useState(true)
useEffect(() => {
let canceled = false
const refreshCliNotice = async (): Promise<void> => {
try {
const status = await window.api.cli.getInstallStatus()
if (!canceled) {
setShowCliNotice(!isOrcaCliAvailableOnPath(status))
}
} catch {
if (!canceled) {
setShowCliNotice(true)
}
}
}
void refreshCliNotice()
window.addEventListener('focus', refreshCliNotice)
return () => {
canceled = true
window.removeEventListener('focus', refreshCliNotice)
}
}, [])
return (
<section className="mt-6 space-y-3">
<div className="space-y-1">
<h2 className="text-sm font-semibold text-foreground">Set up agent features</h2>
<p className="text-[13px] leading-relaxed text-muted-foreground">
Pick the capabilities you want ready after onboarding. Selected features run setup on the
next click and show a terminal here with the skill command ready for review.
</p>
{showCliNotice ? (
<p className="text-[12px] leading-relaxed text-muted-foreground">
{AGENT_SKILL_CLI_PREREQUISITE_NOTICE}
</p>
) : null}
</div>
<section className="mt-6">
<div className="grid gap-3 md:grid-cols-3">
{FEATURE_SETUP_ROWS.map((row) => {
const selected = value[row.id]
@@ -58,6 +58,9 @@ export function FeatureSetupInlineTerminal({
title="Skill setup"
ariaLabel="Skill setup command"
description="Press Enter to run the command and confirm npx if asked. You can also set this up later in Settings."
terminalHeightPx={180}
terminalTopMarginPx={16}
autoScrollIntoView={false}
onOpened={trackTerminalOpened}
onInteracted={trackTerminalInteraction}
/>
@@ -30,11 +30,12 @@ describe('OnboardingFlow', () => {
/>
)
expect(html).toContain('Interested in Orca&#x27;s advanced features?')
expect(html).toContain('Take a short tour before getting started.')
expect(html).toContain('Learn how Orca can help you')
expect(html).toContain('Hand off a feature to an orchestrator agent.')
expect(html).toContain('Grab an element from your running app and send it to an agent.')
expect(html).toContain('Explore Orca')
expect(html).toContain('Preview the core workflow.')
expect(html).toContain('Run agents in isolated worktrees.')
expect(html).toContain('Orchestrate agents to finish larger tasks.')
expect(html).toContain('Start tasks from GitHub or Linear.')
expect(html).toContain('Send webpage elements to agents from the Orca browser.')
expect(html).not.toContain('Write and preview Markdown.')
expect(html).toContain('items-start')
expect(html).toContain('text-left')
@@ -42,4 +43,23 @@ describe('OnboardingFlow', () => {
expect(html).toContain('Skip to project setup')
expect(html).not.toContain('Skip the tour')
})
it('keeps agent setup actions out of the footer', () => {
const html = renderToStaticMarkup(
<OnboardingFlow
onboarding={{
...getDefaultOnboardingState(),
lastCompletedStep: 3
}}
onOnboardingChange={vi.fn()}
/>
)
expect(html).toContain('Set up Orca for agents')
expect(html).toContain('Turn on advanced Orca capabilities for agents.')
expect(html).toContain('Enable capabilities')
expect(html).toContain('Continue')
expect(html).toContain('Skip to project setup')
expect(html).not.toContain('>Skip</button>')
})
})
@@ -31,15 +31,15 @@ const stepCopy = {
},
agentSetup: {
title: 'Set up Orca for agents',
subtitle: 'Choose the capabilities Orca should enable on this computer.'
subtitle: 'Turn on advanced Orca capabilities for agents.'
},
integrations: {
title: 'Connect your task sources',
subtitle: 'Connect GitHub or Linear to:'
},
tour: {
title: "Interested in Orca's advanced features?",
subtitle: 'Take a short tour before getting started.'
title: 'Explore Orca',
subtitle: ''
},
repo: {
title: 'Point Orca at some code',
@@ -72,18 +72,15 @@ export default function OnboardingFlow({
const continueShortcutModifierLabel = getScreenSubmitModifierLabel()
const { currentStep, stepIndex, busyLabel } = flow
const copy = stepCopy[currentStep.id]
const shouldShowSetupAction =
currentStep.id === 'agentSetup' &&
flow.hasSelectedFeatureSetup &&
!flow.featureSetupTerminalCommand
const primaryActionLabel = busyLabel ?? (shouldShowSetupAction ? 'Set up' : 'Continue')
const isTourStep = currentStep.id === 'tour'
const tourStarted = flow.tourStarted
const isInlineTourRunning = isTourStep && tourStarted
const shouldShowFooter = !isInlineTourRunning
const shouldShowSkipToProjectSetup = currentStep.id !== 'repo'
const shouldShowStepHeading = !isInlineTourRunning
const footerPrimaryLabel = primaryActionLabel
const shouldShowFooterBusy = Boolean(busyLabel) && currentStep.id !== 'agentSetup'
const footerPrimaryLabel =
currentStep.id === 'agentSetup' ? 'Continue' : (busyLabel ?? 'Continue')
const {
next: flowNext,
openFolder: flowOpenFolder,
@@ -138,10 +135,7 @@ export default function OnboardingFlow({
return (
<div
className={cn(
'fixed inset-0 z-[100] bg-background text-foreground',
isInlineTourRunning ? 'overflow-hidden' : 'scrollbar-sleek overflow-auto'
)}
className={cn('fixed inset-0 z-[100] bg-background text-foreground', 'overflow-hidden')}
data-onboarding-overlay
>
<div
@@ -159,10 +153,8 @@ export default function OnboardingFlow({
<div
className={cn(
'relative mx-auto flex w-full flex-col px-8 pb-10 pt-16 transition-[max-width] duration-[760ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none',
isInlineTourRunning
? 'h-screen min-h-0 max-w-[1180px] overflow-hidden'
: 'min-h-screen max-w-[820px]'
'relative mx-auto flex h-screen min-h-0 w-full flex-col px-8 pb-10 pt-16 transition-[max-width] duration-[760ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none',
isInlineTourRunning ? 'max-w-[1180px] overflow-hidden' : 'max-w-[820px] overflow-hidden'
)}
>
<div className="flex items-center gap-2.5 text-sm font-semibold tracking-tight">
@@ -233,7 +225,12 @@ export default function OnboardingFlow({
{copy.title}
</h1>
{copy.subtitle ? (
<p className="mt-3 max-w-[58ch] text-[15px] leading-relaxed text-muted-foreground">
<p
className={cn(
'mt-3 text-[15px] leading-relaxed text-muted-foreground',
currentStep.id === 'agentSetup' ? 'max-w-none' : 'max-w-[58ch]'
)}
>
{copy.subtitle}
</p>
) : null}
@@ -242,8 +239,15 @@ export default function OnboardingFlow({
<div
className={cn(
'flex-1 transition-[margin-top] duration-[760ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none',
isInlineTourRunning ? 'mt-7 min-h-0' : 'mt-10'
'min-h-0 flex-1 transition-[margin-top] duration-[760ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none',
// Why: long setup output should scroll inside the step so the footer
// actions stay anchored across every onboarding page.
isInlineTourRunning
? 'mt-7 overflow-hidden'
: cn(
'scrollbar-sleek overflow-y-auto pr-1',
currentStep.id === 'agentSetup' ? 'mt-4' : 'mt-10'
)
)}
>
{currentStep.id === 'agent' && (
@@ -271,6 +275,8 @@ export default function OnboardingFlow({
onFeatureSetupChange={flow.setFeatureSetupSelection}
featureSetupCommand={flow.featureSetupTerminalCommand}
featureSetupCommandSelection={flow.featureSetupTerminalSelection}
setupBusyLabel={currentStep.id === 'agentSetup' ? busyLabel : null}
onStartFeatureSetup={() => void flow.startFeatureSetup()}
/>
)}
{currentStep.id === 'integrations' && <IntegrationsStep />}
@@ -305,7 +311,7 @@ export default function OnboardingFlow({
</div>
{shouldShowFooter && (
<footer className="mt-10 flex items-center justify-between border-t border-border pt-5">
<footer className="mt-6 flex flex-none items-center justify-between border-t border-border pt-5">
{shouldShowSkipToProjectSetup ? (
<button
className="rounded-md px-3 py-2 text-sm text-muted-foreground hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:text-muted-foreground"
@@ -328,19 +334,10 @@ export default function OnboardingFlow({
Back
</button>
)}
{shouldShowSetupAction && (
<button
className="rounded-md px-3 py-2 text-sm text-muted-foreground hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:text-muted-foreground"
disabled={Boolean(busyLabel)}
onClick={() => void flow.skipAgentSetup()}
>
Skip
</button>
)}
{(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)}
aria-busy={shouldShowFooterBusy}
disabled={Boolean(busyLabel)}
onClick={() => {
if (isTourStep) {
@@ -354,7 +351,7 @@ export default function OnboardingFlow({
void flow.next()
}}
>
{busyLabel ? <Loader2 className="size-4 animate-spin" /> : null}
{shouldShowFooterBusy ? <Loader2 className="size-4 animate-spin" /> : null}
{footerPrimaryLabel}
<span className="ml-1 inline-flex items-center gap-0.5 rounded border border-primary-foreground/20 px-1.5 py-0.5 text-[10px] font-medium leading-none text-current/80">
<span>{continueShortcutModifierLabel}</span>
@@ -8,10 +8,10 @@ import { FeatureWallTourSurface } from '../feature-wall/FeatureWallTourSurface'
import { usePrefersReducedMotion } from '../feature-wall/feature-wall-modal-helpers'
const TOUR_LEARNING_POINTS: readonly string[] = [
'Work on several branches at once.',
'Hand off a feature to an orchestrator agent.',
'Start work straight from a GitHub or Linear ticket.',
'Grab an element from your running app and send it to an agent.'
'Run agents in isolated worktrees.',
'Orchestrate agents to finish larger tasks.',
'Start tasks from GitHub or Linear.',
'Send webpage elements to agents from the Orca browser.'
]
type OnboardingTourStepProps = {
@@ -83,7 +83,7 @@ export function OnboardingTourStep({
compactRail
detachedFooter
onTourDepthSummaryChange={onTourDepthSummaryChange}
className="h-full max-h-[790px] min-h-0"
className="h-full min-h-0"
panelClassName="rounded-xl border border-border bg-card"
leadingFooterContent={
<button
@@ -103,7 +103,7 @@ export function OnboardingTourStep({
<div className="flex h-full min-h-[430px] flex-col">
<div className="grid w-full grid-cols-1 items-start gap-10 md:grid-cols-[1fr_minmax(0,340px)]">
<div className="flex flex-col gap-4">
<p className="text-sm font-medium text-foreground">Learn how Orca can help you…</p>
<p className="text-sm font-medium text-foreground">Preview the core workflow.</p>
<ul className="flex flex-col gap-2.5">
{TOUR_LEARNING_POINTS.map((point) => (
<li key={point} className="flex items-start gap-3">
@@ -114,24 +114,26 @@ export function OnboardingTourStep({
</li>
))}
</ul>
<div className="mt-2 flex items-center gap-3">
</div>
<div className="flex w-full flex-col gap-3">
<FeatureTourPreview className="w-full" />
<div className="rounded-lg border border-border bg-muted/40 p-3">
<Button
variant="default"
onClick={handleStartTour}
disabled={Boolean(busyLabel)}
className="gap-2"
className="w-full gap-2"
>
Take the tour
<ArrowRight className="size-4" />
</Button>
<span className="text-xs text-muted-foreground">~ 60 seconds</span>
<div className="mt-2 text-center text-xs text-muted-foreground">~ 60 seconds</div>
</div>
</div>
<FeatureTourPreview className="w-full" />
</div>
<p className="mt-auto max-w-[560px] text-left text-xs leading-relaxed text-muted-foreground">
This tour can be seen anytime under Help &gt; Explore Orca.
Available later under Help &gt; Explore Orca.
</p>
</div>
)
@@ -128,6 +128,10 @@ export type PersistCurrentStepResult = {
featureSetupResult?: OnboardingFeatureSetupResult
}
type PersistCurrentStepOptions = {
runFeatureSetup?: boolean
}
export function usePersistCurrentStep({
currentStepId,
selectedAgent,
@@ -139,97 +143,104 @@ export function usePersistCurrentStep({
onOnboardingChange,
setError
}: PersistCurrentStepDeps) {
return useCallback(async (): Promise<PersistCurrentStepResult> => {
if (!settings) {
return { ok: false }
}
try {
if (currentStepId === 'agent') {
const defaultTuiAgent = selectedAgentOrBlank(selectedAgent)
await updateSettings({ defaultTuiAgent })
const choseAgent = defaultTuiAgent !== 'blank'
const wasAlreadyChosen = onboardingChecklist.choseAgent
onOnboardingChange(
await persistStep(1, {
checklist: { ...onboardingChecklist, choseAgent }
})
)
if (choseAgent && !wasAlreadyChosen) {
track('activation_checklist_item_completed', {
item: 'choseAgent',
time_since_completed_ms: 0
})
}
return { ok: true }
return useCallback(
async (options: PersistCurrentStepOptions = {}): Promise<PersistCurrentStepResult> => {
if (!settings) {
return { ok: false }
}
if (currentStepId === 'theme') {
await updateSettings({ theme })
onOnboardingChange(await persistStep(2))
return { ok: true }
}
if (currentStepId === 'notifications') {
await updateSettings({
notifications: {
...settings.notifications,
enabled: true,
agentTaskComplete: true,
terminalBell: true
}
})
onOnboardingChange(await persistStep(3))
return { ok: true }
}
if (currentStepId === 'agentSetup') {
const setupResult = await runOnboardingFeatureSetup(featureSetupSelection)
const featureSetupResult: OnboardingFeatureSetupResult = setupResult
track('onboarding_feature_setup_run', {
...onboardingFeatureSetupRunTelemetry(featureSetupSelection, setupResult)
})
if (hasSelectedOnboardingFeatureSetup(featureSetupSelection)) {
const firstWarning = setupResult.warnings[0]
if (firstWarning) {
toast.warning('Some feature setup needs attention', {
description: firstWarning.message
try {
if (currentStepId === 'agent') {
const defaultTuiAgent = selectedAgentOrBlank(selectedAgent)
await updateSettings({ defaultTuiAgent })
const choseAgent = defaultTuiAgent !== 'blank'
const wasAlreadyChosen = onboardingChecklist.choseAgent
onOnboardingChange(
await persistStep(1, {
checklist: { ...onboardingChecklist, choseAgent }
})
)
if (choseAgent && !wasAlreadyChosen) {
track('activation_checklist_item_completed', {
item: 'choseAgent',
time_since_completed_ms: 0
})
}
if (setupResult.skillCommandsCopied) {
toast.success('Feature setup ready', {
description: 'Skill command copied and inserted below for review.'
})
}
if (setupResult.computerUsePermissionsOpened) {
toast.message('Opened Computer Use permissions')
}
return { ok: true }
}
onOnboardingChange(await persistStep(4))
return { ok: true, featureSetupResult }
if (currentStepId === 'theme') {
await updateSettings({ theme })
onOnboardingChange(await persistStep(2))
return { ok: true }
}
if (currentStepId === 'notifications') {
await updateSettings({
notifications: {
...settings.notifications,
enabled: true,
agentTaskComplete: true,
terminalBell: true
}
})
onOnboardingChange(await persistStep(3))
return { ok: true }
}
if (currentStepId === 'agentSetup') {
if (!options.runFeatureSetup) {
onOnboardingChange(await persistStep(4))
return { ok: true }
}
const setupResult = await runOnboardingFeatureSetup(featureSetupSelection)
const featureSetupResult: OnboardingFeatureSetupResult = setupResult
track('onboarding_feature_setup_run', {
...onboardingFeatureSetupRunTelemetry(featureSetupSelection, setupResult)
})
if (hasSelectedOnboardingFeatureSetup(featureSetupSelection)) {
const firstWarning = setupResult.warnings[0]
if (firstWarning) {
toast.warning('Some feature setup needs attention', {
description: firstWarning.message
})
}
if (setupResult.skillCommandsCopied) {
toast.success('Feature setup ready', {
description: 'Skill command copied and inserted below for review.'
})
}
if (setupResult.computerUsePermissionsOpened) {
toast.message('Opened Computer Use permissions')
}
}
onOnboardingChange(await persistStep(4))
return { ok: true, featureSetupResult }
}
if (currentStepId === 'integrations') {
// Why: GitHub and Linear connections persist through their own
// store slices when the user actually wires them up. The step itself
// is a no-op for settings/onboarding state beyond marking it
// completed.
onOnboardingChange(await persistStep(5))
return { ok: true }
}
if (currentStepId === 'tour') {
onOnboardingChange(await persistStep(6))
return { ok: true }
}
return { ok: false }
} catch (err) {
setError(err instanceof Error ? err.message : String(err))
return { ok: false }
}
if (currentStepId === 'integrations') {
// Why: GitHub and Linear connections persist through their own
// store slices when the user actually wires them up. The step itself
// is a no-op for settings/onboarding state beyond marking it
// completed.
onOnboardingChange(await persistStep(5))
return { ok: true }
}
if (currentStepId === 'tour') {
onOnboardingChange(await persistStep(6))
return { ok: true }
}
return { ok: false }
} catch (err) {
setError(err instanceof Error ? err.message : String(err))
return { ok: false }
}
}, [
currentStepId,
featureSetupSelection,
onboardingChecklist,
onOnboardingChange,
selectedAgent,
settings,
theme,
updateSettings,
setError
])
},
[
currentStepId,
featureSetupSelection,
onboardingChecklist,
onOnboardingChange,
selectedAgent,
settings,
theme,
updateSettings,
setError
]
)
}
@@ -466,6 +466,36 @@ export function useOnboardingFlow(
// step. A ref flips synchronously so re-entries bail immediately.
const nextInFlightRef = useRef(false)
const featureSetupStepCompletedTrackedRef = useRef(false)
const trackCurrentStepCompleted = useCallback(
(advancedVia: 'button' | 'keyboard'): void => {
if (currentStep.id === 'agentSetup') {
if (featureSetupStepCompletedTrackedRef.current) {
return
}
// Why: feature setup can keep the user on this already-persisted
// step to review a terminal command; later checklist edits must
// not double-count the same step completion.
featureSetupStepCompletedTrackedRef.current = true
}
const durationMs = consumeStepDurationMs()
track('onboarding_step_completed', {
step: currentStep.stepNumber,
value_kind: currentStep.valueKind,
duration_ms: durationMs,
advanced_via: advancedVia
})
if (currentStep.id === 'integrations') {
trackTaskSourcesSnapshot('continue', durationMs, advancedVia)
}
},
[
consumeStepDurationMs,
currentStep.id,
currentStep.stepNumber,
currentStep.valueKind,
trackTaskSourcesSnapshot
]
)
const next = useCallback(
async (advancedVia: 'button' | 'keyboard' = 'button') => {
if (nextInFlightRef.current || busyLabel || currentStep.id === 'repo') {
@@ -476,64 +506,69 @@ export function useOnboardingFlow(
return
}
nextInFlightRef.current = true
if (currentStep.id === 'agentSetup' && hasSelectedFeatureSetup) {
setBusyLabel('Setting up features…')
}
try {
const trackCurrentStepCompleted = (): void => {
if (currentStep.id === 'agentSetup') {
if (featureSetupStepCompletedTrackedRef.current) {
return
}
// Why: feature setup can keep the user on this already-persisted
// step to review a terminal command; later checklist edits must
// not double-count the same step completion.
featureSetupStepCompletedTrackedRef.current = true
}
const durationMs = consumeStepDurationMs()
track('onboarding_step_completed', {
step: currentStep.stepNumber,
value_kind: currentStep.valueKind,
duration_ms: durationMs,
advanced_via: advancedVia
})
if (currentStep.id === 'integrations') {
trackTaskSourcesSnapshot('continue', durationMs, advancedVia)
}
}
const result = await persistCurrentStep()
const nextCommand = result.featureSetupResult?.skillInstallCommand ?? null
if (currentStep.id === 'agentSetup' && nextCommand) {
trackCurrentStepCompleted()
trackCurrentStepCompleted(advancedVia)
setFeatureSetupTerminalSelection(featureSetupSelection)
setFeatureSetupTerminalCommand(nextCommand)
return
}
if (result.ok) {
trackCurrentStepCompleted()
trackCurrentStepCompleted(advancedVia)
setStepIndex((idx) => Math.min(idx + 1, STEPS.length - 1))
}
} finally {
if (currentStep.id === 'agentSetup') {
setBusyLabel(null)
}
nextInFlightRef.current = false
}
},
[
busyLabel,
consumeStepDurationMs,
currentStep.id,
currentStep.stepNumber,
currentStep.valueKind,
featureSetupSelection,
featureSetupTerminalCommand,
hasSelectedFeatureSetup,
persistCurrentStep,
trackTaskSourcesSnapshot
trackCurrentStepCompleted
]
)
const startFeatureSetup = useCallback(async () => {
if (
nextInFlightRef.current ||
busyLabel ||
currentStep.id !== 'agentSetup' ||
featureSetupTerminalCommand ||
!hasSelectedFeatureSetup
) {
return
}
nextInFlightRef.current = true
setBusyLabel('Setting up features…')
try {
const result = await persistCurrentStep({ runFeatureSetup: true })
const nextCommand = result.featureSetupResult?.skillInstallCommand ?? null
if (result.ok) {
trackCurrentStepCompleted('button')
}
if (nextCommand) {
setFeatureSetupTerminalSelection(featureSetupSelection)
setFeatureSetupTerminalCommand(nextCommand)
}
} finally {
setBusyLabel(null)
nextInFlightRef.current = false
}
}, [
busyLabel,
currentStep.id,
featureSetupSelection,
featureSetupTerminalCommand,
hasSelectedFeatureSetup,
persistCurrentStep,
trackCurrentStepCompleted
])
const openFolder = useCallback(
async (kind: 'git' | 'folder' = 'git') => {
// Why: re-entry guard — rapid Cmd+Enter must not launch duplicate pickers.
@@ -945,6 +980,7 @@ export function useOnboardingFlow(
detectedSet,
isDetectingAgents,
next,
startFeatureSetup,
skipAgentSetup,
skipToRepo,
startTour,
@@ -14,7 +14,8 @@ describe('terminal agent quick command presets', () => {
gemini: "gemini --prompt-interactive 'your prompt here'",
antigravity: "agy --prompt-interactive 'your prompt here'",
cursor: "cursor-agent 'your prompt here'",
droid: "droid 'your prompt here'"
droid: "droid 'your prompt here'",
omp: "omp 'your prompt here'"
}
const promptStartingCommands = Object.keys(TUI_AGENT_CONFIG)