Fix onboarding tour footer and copy (#2799)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
a33eedfd02
commit
e0af80ddec
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue