mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
Fix onboarding tour footer and copy (#2799)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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'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 > Explore Orca.
|
||||
Available later under Help > 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,
|
||||
|
||||
+2
-1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user