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
parent a33eedfd02
commit e0af80ddec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 284 additions and 234 deletions

View File

@ -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"')
})
})

View File

@ -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}

View File

@ -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]

View File

@ -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}
/>

View File

@ -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>')
})
})

View File

@ -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>

View File

@ -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>
)

View File

@ -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
]
)
}

View File

@ -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,

View File

@ -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)

View File

@ -21,15 +21,13 @@ export const AGENTS_STEPS: readonly AgentsStep[] = [
id: 'statuses',
name: 'Visibility',
subtitle: 'Agent Visibility',
description:
'Track every running agent across your workspaces, including who is working, waiting, or needs you.'
description: 'Know which agents are working, waiting, live, or blocked.'
},
{
id: 'orchestration',
name: 'Orchestration',
subtitle: 'Orchestration',
description:
'Enable agents to manage and coordinate Orca workspaces so larger tasks can move to completion.'
description: 'Enable agents to manage and coordinate Orca workspaces to execute larger tasks.'
},
{
id: 'usage',

View File

@ -27,7 +27,7 @@ export const FEATURE_WALL_WORKFLOWS: readonly FeatureWallWorkflow[] = [
id: 'workspaces',
title: 'Workspaces',
meta: 'Isolated work · Context kept together',
lede: 'Workspaces let you quickly split tasks into isolated environments, so multiple agents can work side by side without interfering with each other. Each workspace has its own branch, terminal, and agent state, keeping parallel work organized.',
lede: 'Orca splits each task into an isolated workspace so agents can run in parallel.',
primaryTileId: 'tile-01',
relatedTileIds: ['tile-10'],
docsUrl: 'https://www.onorca.dev/docs/model/worktrees'
@ -36,7 +36,7 @@ export const FEATURE_WALL_WORKFLOWS: readonly FeatureWallWorkflow[] = [
id: 'tasks',
title: 'Tasks',
meta: 'GitHub · Linear',
lede: "See your GitHub and Linear tasks in one place, and start a workspace for each when you're ready to build.",
lede: 'Start work directly from GitHub or Linear.',
primaryTileId: 'tile-03',
relatedTileIds: [],
docsUrl: 'https://www.onorca.dev/docs/review/linear'

View File

@ -16,21 +16,19 @@ export const REVIEW_STEPS: readonly ReviewStep[] = [
id: 'notes',
name: 'Notes',
subtitle: 'Notes & diffs',
description:
'Review diffs, leave notes on exact changed lines, and send focused feedback back to an agent.'
description: 'Send focused review notes to an agent.'
},
{
id: 'pr-view',
name: 'PR checks',
subtitle: 'PR checks & comments',
description: 'See PR details, CI, comments, conflicts, and merge readiness from the Checks tab.'
description: 'See PR status in the Checks tab.'
},
{
id: 'ship',
name: 'Ship with AI',
subtitle: 'Ship with AI',
description:
"Use Orca's built-in AI flow to draft commit messages and pull requests from the diff, with everything editable before submission."
description: 'Let AI prepare commit and PR drafts for you.'
}
] as const

View File

@ -19,22 +19,20 @@ export const WORKBENCH_STEPS: readonly WorkbenchStep[] = [
id: 'terminal',
name: 'Terminal',
subtitle: 'Terminal',
description:
'Bring your terminal setup into Orca, then split panes to keep servers, tests, logs, and agents running side by side.'
description: 'Keep your agents, tests, and dev logs visible at once.'
},
{
id: 'editor',
name: 'Editor',
subtitle: 'Editor',
description:
'A rich Notion-style markdown editor with slash commands, inline blocks, and autosave.'
description: 'Use our Notion-style markdown editor to write notes without leaving Orca.'
},
{
id: 'browser',
name: 'Browser',
subtitle: 'Browser',
description:
"Run your app in Orca's browser, send selected UI elements to agents, and let agents navigate, click, and verify pages."
"Run your app in Orca's browser, send selected UI elements to agents, and let your agents interact with your webpage."
}
] as const