Declutter setup guide visuals (#4614)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson
2026-06-04 22:23:28 -07:00
committed by GitHub
co-authored by Orca
parent fd161d3587
commit 843c345768
12 changed files with 278 additions and 1565 deletions
+1 -2
View File
@@ -249,8 +249,7 @@ describe('ClaudeUsageStore', () => {
expect(summary.estimatedCostUsd).toBeCloseTo(73.5)
expect(
breakdown.find((row) => row.key === 'anthropic/claude-opus-4-8-20260528')
?.estimatedCostUsd
breakdown.find((row) => row.key === 'anthropic/claude-opus-4-8-20260528')?.estimatedCostUsd
).toBeCloseTo(36.75)
expect(
breakdown.find((row) => row.key === 'claude-opus-4.8-20260528')?.estimatedCostUsd
@@ -1,201 +0,0 @@
import { useEffect, useState } from 'react'
import type { JSX, ReactNode } from 'react'
import { FolderGit2 } from 'lucide-react'
import { cn } from '@/lib/utils'
import { ClaudeIcon } from '../status-bar/icons'
import { CodexInlineIcon, WorkingSpinner } from './feature-tour-preview-glyphs'
type RepoFocus = 'personal' | 'work' | 'both'
type RepoVisual = {
id: Exclude<RepoFocus, 'both'>
name: string
worktree: string
prompt: string
action: string
agent: 'Claude Code' | 'Codex'
agentIcon: ReactNode
}
const REPOS: readonly RepoVisual[] = [
{
id: 'personal',
name: 'recipe-box',
worktree: 'weekend polish',
prompt: 'refine recipe search',
action: 'Editing search filters',
agent: 'Claude Code',
agentIcon: <ClaudeIcon size={12} />
},
{
id: 'work',
name: 'billing-app',
worktree: 'checkout fix',
prompt: 'fix checkout bug',
action: 'Updating checkout tests',
agent: 'Codex',
agentIcon: <CodexInlineIcon />
}
]
const FOCUS_SEQUENCE: readonly RepoFocus[] = ['personal', 'both', 'work', 'both', 'both']
export function AddReposAnimatedVisual(props: { reducedMotion: boolean }): JSX.Element {
const focus = useRepoFocus(props.reducedMotion)
return (
<div className="grid min-h-[282px] gap-3 rounded-xl border border-border bg-card p-3 text-foreground shadow-xs md:grid-cols-2">
{REPOS.map((repo) => (
<RepoProjectCard
key={repo.id}
repo={repo}
active={focus === repo.id || focus === 'both'}
reducedMotion={props.reducedMotion}
/>
))}
</div>
)
}
function RepoProjectCard(props: {
repo: RepoVisual
active: boolean
reducedMotion: boolean
}): JSX.Element {
return (
<section
className={cn(
'flex min-w-0 flex-col gap-2 rounded-lg border border-sidebar-border bg-sidebar p-2 text-sidebar-foreground transition-[border-color,box-shadow] duration-500',
props.active ? 'shadow-xs' : null
)}
>
<ProjectSidebarRow name={props.repo.name} />
<ProjectWorktreeRow
repo={props.repo}
active={props.active}
reducedMotion={props.reducedMotion}
/>
<div className="min-w-0 overflow-hidden rounded-lg border border-border bg-background text-foreground">
<VisualTitlebar title={`${props.repo.worktree} - ${props.repo.agent}`} />
<div className="space-y-1.5 p-2.5 font-mono text-[11px]">
<TerminalLine>
<Prompt>&gt;</Prompt>
{props.repo.prompt}
</TerminalLine>
<TerminalLine muted>
{props.repo.agentIcon}
{props.repo.agent} session ready
</TerminalLine>
<TerminalLine muted>
<WorkingSpinner size="xs" reducedMotion={props.reducedMotion} />
{props.repo.action}
</TerminalLine>
</div>
</div>
</section>
)
}
function ProjectSidebarRow(props: { name: string }): JSX.Element {
return (
<div
aria-hidden
className="relative flex h-8 min-w-0 items-center gap-1.5 rounded-md px-1.5 text-sidebar-foreground"
>
<span className="flex size-4 shrink-0 items-center justify-center text-muted-foreground">
<FolderGit2 className="size-3.5" />
</span>
<span className="min-w-0 flex-1 truncate text-[13px] font-semibold leading-none">
{props.name}
</span>
<span className="min-w-[3.75rem] rounded-full bg-sidebar-accent px-1.5 py-0.5 text-center text-[9px] font-medium leading-none text-muted-foreground">
1 worktree
</span>
</div>
)
}
function ProjectWorktreeRow(props: {
repo: RepoVisual
active: boolean
reducedMotion: boolean
}): JSX.Element {
return (
<div
className={cn(
'rounded-md border border-sidebar-border px-2.5 py-2 transition-colors',
props.active ? 'bg-sidebar-accent' : 'bg-sidebar'
)}
>
<div className="flex items-center gap-2">
<span
className={cn(
'size-1.5 shrink-0 rounded-full',
props.active ? 'bg-emerald-500' : 'bg-muted-foreground/35'
)}
/>
<span className="truncate text-xs font-medium text-sidebar-foreground">
{props.repo.worktree}
</span>
</div>
<div className="mt-2 grid grid-cols-[8px_14px_minmax(0,1fr)] items-center gap-1.5">
<WorkingSpinner size="xs" reducedMotion={props.reducedMotion} />
<span className="flex size-3.5 items-center justify-center text-sidebar-foreground/65">
{props.repo.agentIcon}
</span>
<span className="truncate font-mono text-[11px] text-sidebar-foreground/65">
{props.repo.prompt}
</span>
</div>
</div>
)
}
function VisualTitlebar(props: { title: string }): JSX.Element {
return (
<div className="flex h-6 items-center gap-1.5 border-b border-border bg-muted/40 px-2">
<span className="size-2 rounded-full bg-foreground/15" />
<span className="size-2 rounded-full bg-foreground/15" />
<span className="size-2 rounded-full bg-foreground/15" />
<span className="ml-1 truncate font-mono text-[11px] text-muted-foreground">
{props.title}
</span>
</div>
)
}
function TerminalLine(props: { children: ReactNode; muted?: boolean }): JSX.Element {
return (
<div
className={cn(
'flex min-w-0 items-center gap-1.5 truncate leading-[1.45]',
props.muted ? 'text-muted-foreground' : 'text-foreground'
)}
>
{props.children}
</div>
)
}
function Prompt(props: { children: ReactNode }): JSX.Element {
return <span className="shrink-0 text-primary">{props.children}</span>
}
function useRepoFocus(reducedMotion: boolean): RepoFocus {
const [idx, setIdx] = useState(() => (reducedMotion ? FOCUS_SEQUENCE.indexOf('both') : 0))
useEffect(() => {
if (reducedMotion) {
setIdx(FOCUS_SEQUENCE.indexOf('both'))
return
}
const id = window.setInterval(() => {
setIdx((current) => (current + 1) % FOCUS_SEQUENCE.length)
}, 1700)
return () => window.clearInterval(id)
}, [reducedMotion])
return FOCUS_SEQUENCE[idx] ?? 'both'
}
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'
import { Check, Globe2, Loader2, MonitorCog, Terminal, Workflow } from 'lucide-react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
@@ -19,17 +19,12 @@ import {
useAgentCapabilitySetupStatus,
type AgentCapabilityInstallStatus
} from './agent-capability-setup-status'
import { AgentsOrchestrationVisual } from './AgentsOrchestrationVisual'
import { BrowserAnimatedVisual } from './BrowserAnimatedVisual'
import { ComputerUseAnimatedVisual } from './ComputerUseAnimatedVisual'
export function AgentCapabilitiesSetupAction(props: {
reducedMotion: boolean
onOrchestrationSkillInstalledChange: (installed: boolean) => void
onBrowserUseSkillInstalledChange: (installed: boolean) => void
}): React.JSX.Element {
const { reducedMotion, onBrowserUseSkillInstalledChange, onOrchestrationSkillInstalledChange } =
props
const { onBrowserUseSkillInstalledChange, onOrchestrationSkillInstalledChange } = props
const capabilitySetupStatus = useAgentCapabilitySetupStatus()
const { readiness } = capabilitySetupStatus
const featureSetupDefaultsAppliedRef = useRef(false)
@@ -112,7 +107,6 @@ export function AgentCapabilitiesSetupAction(props: {
onStartFeatureSetup={() => void handleStartFeatureSetup()}
installStatus={capabilitySetupStatus.installStatus}
/>
<AgentCapabilityAnimationCarousel reducedMotion={reducedMotion} />
</div>
)
}
@@ -120,31 +114,30 @@ export function AgentCapabilitiesSetupAction(props: {
type AgentCapabilitySetupRow = {
id: OnboardingFeatureSetupId
title: string
description: string
icon: ReactNode
}
type AgentCapabilitySlide = {
id: string
title: string
icon: ReactNode
widthClassName: string
node: ReactNode
}
const AGENT_CAPABILITY_SETUP_ROWS: readonly AgentCapabilitySetupRow[] = [
{
id: 'orchestration',
title: 'Agent Orchestration',
description:
'Let agents coordinate through Orca to keep large, multi-step tasks moving to completion.',
icon: <Workflow className="size-4" />
},
{
id: 'browserUse',
title: 'Agent Browser Use',
description:
"Give agents direct access to Orca's browser so they can test pages, capture screenshots, and act on what they see.",
icon: <Globe2 className="size-4" />
},
{
id: 'computerUse',
title: 'Computer Use',
description:
'Let agents control the desktop, moving the cursor, clicking, and typing in any app.',
icon: <MonitorCog className="size-4" />
}
]
@@ -247,6 +240,9 @@ function AgentCapabilitySetupChecklist(props: {
</span>
</span>
<span className="mt-3 text-sm font-medium text-foreground">{row.title}</span>
<span className="mt-1 text-xs leading-snug text-muted-foreground">
{row.description}
</span>
<AgentCapabilityStatusNote status={installStatus} />
</button>
)
@@ -290,99 +286,3 @@ function AgentCapabilityStatusNote(props: {
</span>
)
}
const AGENT_CAPABILITY_SLIDE_COUNT = 3
function AgentCapabilityAnimationCarousel(props: { reducedMotion: boolean }): React.JSX.Element {
const [activeIndex, setActiveIndex] = useState(() => (props.reducedMotion ? 1 : 0))
const advanceSlide = useCallback(() => {
if (props.reducedMotion) {
return
}
setActiveIndex((current) => (current + 1) % AGENT_CAPABILITY_SLIDE_COUNT)
}, [props.reducedMotion])
const slides = useMemo<readonly AgentCapabilitySlide[]>(
() => [
{
id: 'browser-use',
title: 'Browser use',
icon: <Globe2 className="size-3.5" />,
widthClassName: 'w-[460px]',
node: (
<BrowserAnimatedVisual
reducedMotion={props.reducedMotion}
onCycleComplete={advanceSlide}
/>
)
},
{
id: 'computer-use',
title: 'Computer Use',
icon: <MonitorCog className="size-3.5" />,
widthClassName: 'w-[520px]',
node: (
<ComputerUseAnimatedVisual
reducedMotion={props.reducedMotion}
onCycleComplete={advanceSlide}
/>
)
},
{
id: 'orchestration',
title: 'Agent orchestration',
icon: <Workflow className="size-3.5" />,
widthClassName: 'w-[520px]',
node: (
<AgentsOrchestrationVisual
reducedMotion={props.reducedMotion}
activeStepId="orchestration"
widthPx={400}
heightPx={300}
onCycleComplete={advanceSlide}
/>
)
}
],
[advanceSlide, props.reducedMotion]
)
useEffect(() => {
if (props.reducedMotion) {
setActiveIndex(1)
}
}, [props.reducedMotion])
const activeSlide = slides[activeIndex] ?? slides[0]
return (
<div className="rounded-xl border border-border bg-muted/20 p-3">
<div className="mb-3 flex items-center justify-between gap-3">
<div className="text-xs font-semibold leading-none text-muted-foreground">Preview</div>
<div className="flex items-center gap-1">
{slides.map((slide, index) => (
<button
key={slide.id}
type="button"
aria-label={`Show ${slide.title}`}
aria-pressed={index === activeIndex}
className={cn(
'inline-flex h-7 items-center gap-1.5 rounded-md border px-2 text-[11px] font-medium transition-colors',
index === activeIndex
? 'border-border bg-card text-foreground shadow-xs'
: 'border-transparent text-muted-foreground hover:bg-muted/50 hover:text-foreground'
)}
onClick={() => setActiveIndex(index)}
>
{slide.icon}
<span className="max-w-28 truncate">{slide.title}</span>
</button>
))}
</div>
</div>
<div className="mx-auto flex min-h-[300px] max-w-[560px] items-start justify-center overflow-visible">
<div className={cn('max-w-full', activeSlide.widthClassName)}>{activeSlide.node}</div>
</div>
</div>
)
}
@@ -7,16 +7,19 @@ import type {
import { getFeatureWallSetupStepsForSection } from '../../../../shared/feature-wall-setup-steps'
import { cn } from '@/lib/utils'
import type { FeatureWallSetupProgress } from './feature-wall-setup-progress'
import { usePrefersReducedMotion } from './feature-wall-modal-helpers'
import { TasksAnimatedVisual } from './TasksAnimatedVisual'
import { AgentCapabilitiesSetupAction } from './AgentCapabilitiesSetupAction'
import {
AddReposAction,
SetupScriptAction,
SplitTerminalShortcutHint,
TwoAgentsAction,
WorkspacesAction
} from './FeatureWallSetupWorkflowActions'
import { SetupStepPreview } from './SetupStepPreview'
import {
SetupMultipleReposVisual,
SetupTwoAgentsVisual,
SetupWorkspacesVisual
} from './FeatureWallSetupStepVisuals'
import { Button } from '@/components/ui/button'
import { GitHubRow, LinearRow } from '../onboarding/IntegrationsStep'
import { AgentStep } from '../onboarding/AgentStep'
@@ -36,21 +39,26 @@ function SetupStepRow(props: {
step: FeatureWallSetupStep
done: boolean
active: boolean
ordinal: number
onSelect: () => void
}): React.JSX.Element {
const { step, done, active, onSelect } = props
const { step, done, active, ordinal, onSelect } = props
return (
<button
type="button"
onClick={onSelect}
aria-current={active ? 'step' : undefined}
className={cn(
'flex w-full items-center gap-3 rounded-md border px-3 py-2.5 text-left transition-colors',
'relative flex w-full items-center gap-3 rounded-md border px-3 py-2.5 text-left transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
active
? 'border-border bg-accent text-accent-foreground'
: 'border-border bg-card hover:bg-accent'
: 'border-border bg-background hover:bg-accent'
)}
>
{active ? (
<span className="absolute bottom-2 left-0 top-2 w-0.5 rounded-full bg-foreground" />
) : null}
<span
className={cn(
'flex size-5 shrink-0 items-center justify-center rounded-full border',
@@ -59,7 +67,7 @@ function SetupStepRow(props: {
: 'border-border text-muted-foreground'
)}
>
{done ? <Check className="size-3" /> : null}
{done ? <Check className="size-3" /> : <span className="text-xs">{ordinal}</span>}
</span>
<span className="min-w-0">
<span className="block text-[15px] font-medium leading-tight text-foreground">
@@ -73,6 +81,7 @@ function SetupStepRow(props: {
function SetupSection(props: {
title: string
steps: readonly FeatureWallSetupStep[]
startOrdinal: number
activeStepId: FeatureWallSetupStepId | null
progress: FeatureWallSetupProgress
onSelectStep: (id: FeatureWallSetupStepId) => void
@@ -89,12 +98,13 @@ function SetupSection(props: {
</span>
</div>
<div className="space-y-1.5">
{props.steps.map((step) => (
{props.steps.map((step, index) => (
<SetupStepRow
key={step.id}
step={step}
done={props.progress.stepDone[step.id]}
active={props.activeStepId === step.id}
ordinal={props.startOrdinal + index}
onSelect={() => props.onSelectStep(step.id)}
/>
))}
@@ -105,7 +115,6 @@ function SetupSection(props: {
function SelectedStepAction(props: FeatureWallSetupChecklistProps): React.JSX.Element | null {
const { activeStep } = props
const reducedMotion = usePrefersReducedMotion()
if (!activeStep) {
return null
}
@@ -114,31 +123,43 @@ function SelectedStepAction(props: FeatureWallSetupChecklistProps): React.JSX.El
return <DefaultAgentAction />
}
if (activeStep.id === 'add-two-repos') {
return <AddReposAction reducedMotion={reducedMotion} />
return <AddReposAction />
}
if (activeStep.id === 'notifications') {
return <NotificationAction />
}
if (activeStep.id === 'split-terminal') {
return <TwoAgentsAction reducedMotion={reducedMotion} done={activeDone} />
return <TwoAgentsAction done={activeDone} />
}
if (activeStep.id === 'two-worktrees') {
return <WorkspacesAction reducedMotion={reducedMotion} done={activeDone} />
return <WorkspacesAction done={activeDone} />
}
if (activeStep.id === 'task-sources') {
return <TaskSourcesAction reducedMotion={reducedMotion} />
return <TaskSourcesAction />
}
if (activeStep.id === 'agent-capabilities') {
return (
<AgentCapabilitiesSetupAction
reducedMotion={reducedMotion}
onOrchestrationSkillInstalledChange={props.onOrchestrationSkillInstalledChange}
onBrowserUseSkillInstalledChange={props.onBrowserUseSkillInstalledChange}
/>
)
}
if (activeStep.id === 'setup-script') {
return <SetupScriptAction reducedMotion={reducedMotion} />
return <SetupScriptAction />
}
return null
}
function SelectedStepVisual(props: { stepId: FeatureWallSetupStepId }): React.JSX.Element | null {
if (props.stepId === 'split-terminal') {
return <SetupTwoAgentsVisual />
}
if (props.stepId === 'two-worktrees') {
return <SetupWorkspacesVisual />
}
if (props.stepId === 'add-two-repos') {
return <SetupMultipleReposVisual />
}
return null
}
@@ -166,7 +187,7 @@ function DefaultAgentAction(): React.JSX.Element {
}, [refreshDetectedAgents])
return (
<div className="rounded-xl border border-border bg-muted/20 p-4">
<div className="max-w-3xl">
<AgentStep
selectedAgent={selectedAgent}
onSelect={handleSelectAgent}
@@ -181,13 +202,13 @@ function NotificationAction(): React.JSX.Element {
const settings = useAppStore((s) => s.settings)
const updateSettings = useAppStore((s) => s.updateSettings)
return (
<div className="rounded-xl border border-border bg-muted/20 p-4">
<div className="max-w-3xl">
<NotificationStep settings={settings} updateSettings={updateSettings} />
</div>
)
}
function TaskSourcesAction(props: { reducedMotion: boolean }): React.JSX.Element {
function TaskSourcesAction(): React.JSX.Element {
const closeModal = useAppStore((s) => s.closeModal)
const openTaskPage = useAppStore((s) => s.openTaskPage)
return (
@@ -196,21 +217,20 @@ function TaskSourcesAction(props: { reducedMotion: boolean }): React.JSX.Element
<GitHubRow compact />
<LinearRow compact />
</div>
<Button
type="button"
size="sm"
className="w-fit gap-2"
onClick={() => {
closeModal()
openTaskPage()
}}
>
<ArrowUpRight className="size-3.5" />
See tasks
</Button>
<SetupStepPreview className="mx-auto h-[220px] w-full max-w-[480px]">
<TasksAnimatedVisual reducedMotion={props.reducedMotion} />
</SetupStepPreview>
<div className="flex items-center">
<Button
type="button"
size="sm"
className="w-fit gap-2"
onClick={() => {
closeModal()
openTaskPage()
}}
>
<ArrowUpRight className="size-3.5" />
See tasks
</Button>
</div>
</div>
)
}
@@ -220,6 +240,10 @@ export function FeatureWallSetupChecklist(
): React.JSX.Element {
const { activeStep, progress, onSelectStep } = props
const activeDone = activeStep ? progress.stepDone[activeStep.id] : false
const useWideStepCopyLayout =
activeStep?.id === 'split-terminal' ||
activeStep?.id === 'two-worktrees' ||
activeStep?.id === 'add-two-repos'
const parallelWorkSteps = getFeatureWallSetupStepsForSection('parallel-work')
const setupSteps = getFeatureWallSetupStepsForSection('setup')
@@ -227,32 +251,31 @@ export function FeatureWallSetupChecklist(
<div className="grid h-full min-h-0 grid-rows-[auto_minmax(0,1fr)] gap-5 lg:grid-cols-[minmax(200px,300px)_minmax(0,1fr)] lg:grid-rows-[minmax(0,1fr)]">
<div className="scrollbar-sleek min-h-0 space-y-5 overflow-y-auto pr-1">
<SetupSection
title="Setup"
steps={setupSteps}
title="Milestones"
steps={parallelWorkSteps}
startOrdinal={1}
activeStepId={activeStep?.id ?? null}
progress={progress}
onSelectStep={onSelectStep}
/>
<SetupSection
title="Milestones"
steps={parallelWorkSteps}
title="Setup"
steps={setupSteps}
startOrdinal={parallelWorkSteps.length + 1}
activeStepId={activeStep?.id ?? null}
progress={progress}
onSelectStep={onSelectStep}
/>
</div>
<section className="scrollbar-sleek min-h-0 overflow-y-auto rounded-xl border border-border bg-card p-5">
<section className="scrollbar-sleek min-h-0 overflow-y-auto border-t border-border pt-5 lg:border-l lg:border-t-0 lg:pl-7 lg:pt-0">
{activeStep ? (
<div className="flex h-full flex-col gap-4">
<div className="flex h-full flex-col gap-5">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 space-y-1">
<div className="text-2xl font-semibold leading-tight tracking-tight text-foreground">
<div className="min-w-0">
<div className="text-2xl font-semibold leading-tight text-foreground">
{activeStep.name}
</div>
<p className="max-w-[58ch] text-base leading-normal text-muted-foreground">
{activeStep.description}
</p>
</div>
<span
className={cn(
@@ -265,7 +288,32 @@ export function FeatureWallSetupChecklist(
{activeDone ? 'Done' : 'Not done yet'}
</span>
</div>
<SelectedStepAction {...props} />
<div
className={cn(
'grid max-w-3xl items-start sm:grid-cols-[minmax(0,48ch)_auto]',
useWideStepCopyLayout ? 'gap-8 sm:gap-10' : 'gap-5'
)}
>
<div className="min-w-0">
<p
className={cn(
'text-base leading-normal text-muted-foreground',
useWideStepCopyLayout ? 'pr-4 sm:pr-6' : null
)}
>
{activeStep.description}
</p>
{activeStep.id === 'split-terminal' ? (
<div className="mt-3">
<SplitTerminalShortcutHint />
</div>
) : null}
</div>
<SelectedStepVisual stepId={activeStep.id} />
</div>
<div className="min-w-0">
<SelectedStepAction {...props} />
</div>
</div>
) : null}
</section>
@@ -1,576 +1,111 @@
/* eslint-disable max-lines -- Why: these setup visuals are self-contained storyboards; splitting the phase markup from timing and measured cursor logic would make the animation harder to verify. */
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import type { JSX, MutableRefObject, ReactNode, RefObject } from 'react'
import { FolderGit2, Plus } from 'lucide-react'
import type { JSX } from 'react'
import { Check, FolderGit2 } from 'lucide-react'
import { cn } from '@/lib/utils'
import { ClaudeIcon } from '../status-bar/icons'
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
import { CodexInlineIcon, CursorIcon, WorkingSpinner } from './feature-tour-preview-glyphs'
import { WorkbenchAnimatedVisual } from './WorkbenchAnimatedVisual'
import { FeatureWallClickRing } from './FeatureWallClickRing'
type WorktreePhase =
| 'claude-active'
| 'plus-hover'
| 'plus-click'
| 'modal'
| 'modal-name-start'
| 'modal-name-complete'
| 'modal-hover'
| 'create-click'
| 'modal-closing'
| 'codex-starting'
| 'codex-typing-start'
| 'codex-typing-mid'
| 'codex-submitted'
| 'both-working'
// Why: these static marks replace storyboarded animations for setup steps whose
// meaning reads instantly as a single mark — quieter than a looping demo.
// Each compresses its step to one recognizable idea drawn from the old animation.
const WORKTREE_PHASES: readonly WorktreePhase[] = [
'claude-active',
'claude-active',
'plus-hover',
'plus-hover',
'plus-click',
'modal',
'modal-name-start',
'modal-name-complete',
// Why: allow the cursor to slide onto and highlight the "Create" button before click.
'modal-hover',
'modal-hover',
'create-click',
// Why: the new worktree should not appear until the create dialog has cleared.
'modal-closing',
'codex-starting',
'codex-typing-start',
'codex-typing-mid',
'codex-submitted',
'both-working',
'both-working',
'both-working'
]
const CODEX_WORKTREE_NAME = 'checkout fix'
const CODEX_WORKTREE_PROMPT = 'fix checkout timeout'
const WORKTREE_NAME_TYPE_PER_CHAR_MS = 70
const WORKTREE_PROMPT_TYPE_PER_CHAR_MS = 55
export function SetupTwoAgentsVisual(props: { reducedMotion: boolean }): JSX.Element {
// A green ✓ + skeleton line: the agent-action checklist beat shared by terminal marks.
function CheckLine(props: { className?: string }): JSX.Element {
return (
<WorkbenchAnimatedVisual reducedMotion={props.reducedMotion} variant="two-agents-checklist" />
<span className={cn('flex items-center gap-1.5', props.className)}>
<Check className="size-3 shrink-0 text-emerald-500" strokeWidth={3} />
<span className="h-[5px] flex-1 rounded-full bg-foreground/10" />
</span>
)
}
export function SetupWorkspacesVisual(props: { reducedMotion: boolean }): JSX.Element {
const { reducedMotion } = props
const rootRef = useRef<HTMLDivElement | null>(null)
const plusRef = useRef<HTMLSpanElement | null>(null)
const createButtonRef = useRef<HTMLDivElement | null>(null)
const modalNameTimeoutsRef = useRef<number[]>([])
const codexPromptTimeoutsRef = useRef<number[]>([])
const newWorktreeShortcut = useShortcutLabel('workspace.create')
const phase = useLoopedPhase(WORKTREE_PHASES, 900, reducedMotion, 'both-working')
const [modalNameValue, setModalNameValue] = useState(() =>
reducedMotion ? CODEX_WORKTREE_NAME : ''
)
const [codexPromptText, setCodexPromptText] = useState(() =>
reducedMotion ? CODEX_WORKTREE_PROMPT : ''
)
const plusHovered = phase === 'plus-hover'
const plusClicked = phase === 'plus-click'
const modalVisible =
phase === 'modal' ||
phase === 'modal-name-start' ||
phase === 'modal-name-complete' ||
phase === 'modal-hover' ||
phase === 'create-click'
const createHovered = phase === 'modal-hover' || phase === 'create-click'
const createClicked = phase === 'create-click'
const codexStarting = phase === 'codex-starting'
const codexTyping = phase === 'codex-typing-start' || phase === 'codex-typing-mid'
const codexSubmitted = phase === 'codex-submitted'
const codexWorking = phase === 'both-working'
const codexWorktreeVisible = codexStarting || codexTyping || codexSubmitted || codexWorking
const modalNameTyping = phase === 'modal-name-start'
const cursorTarget =
phase === 'claude-active'
? 'start'
: phase === 'plus-hover' ||
phase === 'plus-click' ||
phase === 'modal' ||
phase === 'modal-name-start' ||
phase === 'modal-name-complete'
? 'plus'
: phase === 'modal-hover' || phase === 'create-click'
? 'create'
: 'hidden'
const cursor = useMeasuredCursor(rootRef, plusRef, createButtonRef, cursorTarget, reducedMotion)
useEffect(() => {
return () => {
clearTypedTimeouts(modalNameTimeoutsRef)
clearTypedTimeouts(codexPromptTimeoutsRef)
}
}, [])
useEffect(() => {
if (reducedMotion) {
clearTypedTimeouts(modalNameTimeoutsRef)
setModalNameValue(CODEX_WORKTREE_NAME)
return
}
if (phase === 'modal') {
clearTypedTimeouts(modalNameTimeoutsRef)
setModalNameValue('')
return
}
if (phase === 'modal-name-start') {
scheduleTypedText(
modalNameTimeoutsRef,
CODEX_WORKTREE_NAME,
WORKTREE_NAME_TYPE_PER_CHAR_MS,
setModalNameValue
)
return
}
if (modalVisible || codexWorktreeVisible) {
clearTypedTimeouts(modalNameTimeoutsRef)
setModalNameValue(CODEX_WORKTREE_NAME)
return
}
clearTypedTimeouts(modalNameTimeoutsRef)
setModalNameValue('')
}, [codexWorktreeVisible, modalVisible, phase, reducedMotion])
useEffect(() => {
if (reducedMotion) {
clearTypedTimeouts(codexPromptTimeoutsRef)
setCodexPromptText(CODEX_WORKTREE_PROMPT)
return
}
if (phase === 'codex-typing-start') {
scheduleTypedText(
codexPromptTimeoutsRef,
CODEX_WORKTREE_PROMPT,
WORKTREE_PROMPT_TYPE_PER_CHAR_MS,
setCodexPromptText
)
return
}
// Why: the prompt is long enough to keep typing across the second typing beat.
if (phase === 'codex-typing-mid') {
return
}
if (codexSubmitted || codexWorking) {
clearTypedTimeouts(codexPromptTimeoutsRef)
setCodexPromptText(CODEX_WORKTREE_PROMPT)
return
}
clearTypedTimeouts(codexPromptTimeoutsRef)
setCodexPromptText('')
}, [codexSubmitted, codexWorking, phase, reducedMotion])
// Mac-style terminal traffic-light dots — the signature of an Orca terminal pane.
function TerminalDots(): JSX.Element {
return (
<div
ref={rootRef}
className="relative grid min-h-[320px] gap-3 overflow-hidden rounded-xl border border-border bg-card p-3 text-foreground shadow-xs md:grid-cols-[220px_minmax(0,1fr)]"
>
<div className="flex min-w-0 flex-col gap-1.5 rounded-lg border border-sidebar-border bg-sidebar p-2 text-sidebar-foreground">
<ProjectSidebarRow
plusRef={plusRef}
plusActive={plusHovered || plusClicked}
plusHovered={plusHovered}
shortcut={newWorktreeShortcut}
worktreeCount={codexWorktreeVisible ? 2 : 1}
/>
<div className="relative flex min-w-0 flex-col gap-1.5">
<WorkspaceListCard
title="release notes"
active
prompt="draft release notes"
icon={<ClaudeIcon size={12} />}
state="working"
reducedMotion={reducedMotion}
/>
<WorkspaceListCard
title="checkout fix"
active={codexWorktreeVisible}
prompt="fix checkout timeout"
icon={<CodexInlineIcon />}
state={
codexSubmitted || codexWorking
? 'working'
: codexWorktreeVisible
? 'starting'
: 'idle'
}
reducedMotion={reducedMotion}
className={cn(
'transition-[opacity,transform]',
codexWorktreeVisible
? 'translate-y-0 opacity-100 duration-500'
: '-translate-y-1 opacity-0 duration-0'
)}
/>
</div>
</div>
<span className="flex gap-[3px]">
<span className="size-[5px] rounded-full bg-foreground/15" />
<span className="size-[5px] rounded-full bg-foreground/15" />
<span className="size-[5px] rounded-full bg-foreground/15" />
</span>
)
}
<div className="relative grid min-w-0 gap-3 md:grid-rows-[88px_minmax(0,1fr)]">
<div className="min-w-0 overflow-hidden rounded-lg border border-border bg-background">
<VisualTitlebar title="release notes - Claude Code" compact />
<div className="space-y-1.5 p-2.5 font-mono text-[11px]">
<TerminalLine>
<Prompt>&gt;</Prompt> draft release notes
</TerminalLine>
<TerminalLine muted>
<WorkingSpinner size="xs" reducedMotion={reducedMotion} />
Edit release-notes.md
</TerminalLine>
</div>
// Split a terminal: one terminal split into two panes, each running an agent.
export function SetupTwoAgentsVisual(): JSX.Element {
return (
<div aria-hidden className="relative h-28 w-[156px] shrink-0">
<div className="absolute inset-y-1 inset-x-0 flex flex-col overflow-hidden rounded-[10px] border-[1.5px] border-border bg-muted shadow-[0_6px_16px_rgba(0,0,0,0.12)]">
<div className="flex items-center border-b border-border px-2 py-1.5">
<TerminalDots />
</div>
<div
className={cn(
'min-w-0 overflow-hidden rounded-lg border border-border bg-background transition-[opacity,transform]',
codexWorktreeVisible
? 'translate-y-0 opacity-100 duration-500'
: 'translate-y-2 opacity-0 duration-0'
)}
>
<VisualTitlebar title="checkout fix - Codex" compact />
<div className="space-y-1.5 p-2.5 font-mono text-[11px]">
{codexStarting ? (
<TerminalLine muted>
<CodexInlineIcon />
Codex session ready
</TerminalLine>
) : (
<TerminalLine>
<Prompt>&gt;</Prompt>
{codexPromptText}
{codexTyping ? (
<span className="ml-px inline-block h-[10px] w-[5px] -translate-y-px animate-pulse bg-foreground align-[-1px]" />
) : null}
</TerminalLine>
)}
<TerminalLine muted>
<CodexInlineIcon />
{codexWorking
? 'Read checkout.test.ts'
: codexSubmitted
? 'Reading checkout.test.ts'
: 'Waiting'}
</TerminalLine>
{codexWorking ? (
<TerminalLine muted>
<WorkingSpinner size="xs" reducedMotion={reducedMotion} />
Edit src/checkout.ts
</TerminalLine>
) : null}
</div>
</div>
<NewWorkspaceModal
visible={modalVisible}
createHovered={createHovered}
createClicked={createClicked}
createButtonRef={createButtonRef}
nameValue={modalNameValue}
nameTyping={modalNameTyping}
/>
</div>
<div
aria-hidden
className={cn(
'pointer-events-none absolute left-0 top-0 z-50 size-5 drop-shadow-sm transition-[opacity,transform] duration-700 ease-[cubic-bezier(.45,.05,.2,1)] [&_svg]:size-5',
cursor.visible ? 'opacity-100' : 'opacity-0'
)}
style={{ transform: `translate(${cursor.x}px, ${cursor.y}px)` }}
>
<div className="relative">
<CursorIcon />
{plusClicked || createClicked ? <FeatureWallClickRing /> : null}
<div className="flex flex-1">
<SplitTerminalPane />
<SplitTerminalPane className="border-l-[1.5px] border-border" />
</div>
</div>
</div>
)
}
function ProjectSidebarRow(props: {
plusRef: RefObject<HTMLSpanElement | null>
plusActive: boolean
plusHovered: boolean
shortcut: string
worktreeCount: number
}): JSX.Element {
function SplitTerminalPane(props: { className?: string }): JSX.Element {
return (
<div
aria-hidden
className="relative flex h-8 min-w-0 items-center gap-1.5 rounded-md px-1.5 text-sidebar-foreground"
>
<span className="flex size-4 shrink-0 items-center justify-center text-muted-foreground">
<FolderGit2 className="size-3.5" />
<div className={cn('flex flex-1 flex-col gap-1.5 p-2', props.className)}>
<span className="flex items-center gap-1.5">
<span className="font-mono text-[11px] leading-none text-muted-foreground">{'>'}</span>
<span className="h-[5px] w-3/5 rounded-full bg-foreground/10" />
</span>
<span className="min-w-0 flex-1 truncate text-[13px] font-semibold leading-none">orca</span>
<span className="min-w-[3.75rem] rounded-full bg-sidebar-accent px-1.5 py-0.5 text-center text-[9px] font-medium leading-none text-muted-foreground">
{props.worktreeCount} worktree{props.worktreeCount === 1 ? '' : 's'}
</span>
<span
ref={props.plusRef}
className={cn(
'relative flex size-5 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors',
props.plusActive ? 'bg-sidebar-accent text-sidebar-accent-foreground' : null
)}
>
<Plus className="size-3.5" />
</span>
<div
className={cn(
'pointer-events-none absolute left-[calc(100%+0.375rem)] top-0 z-10 flex h-7 w-max items-center gap-2 whitespace-nowrap rounded-md border border-border bg-popover px-2.5 text-[11px] font-medium text-popover-foreground shadow-xs transition-[opacity,transform] duration-200',
props.plusHovered ? 'translate-y-0 opacity-100' : '-translate-y-1 opacity-0'
)}
>
<span className="whitespace-nowrap">New worktree</span>
<kbd className="rounded border border-border bg-background px-1.5 py-0.5 font-mono text-[10px] font-medium text-muted-foreground">
{props.shortcut}
</kbd>
</div>
<CheckLine className="w-4/5" />
</div>
)
}
function NewWorkspaceModal(props: {
visible: boolean
createHovered: boolean
createClicked: boolean
createButtonRef: RefObject<HTMLDivElement | null>
nameValue: string
nameTyping: boolean
}): JSX.Element {
// Why: a small, static mark of two parallel worktrees — quieter than an animated
// storyboard, which read as cluttered for a step whose meaning is just "two isolated spaces."
export function SetupWorkspacesVisual(): JSX.Element {
return (
<div aria-hidden className="relative h-28 w-[156px] shrink-0">
<WorktreeGlyphPanel className="right-0 top-0 bg-muted/60" />
<WorktreeGlyphPanel className="bottom-0 left-0 bg-muted shadow-[0_6px_16px_rgba(0,0,0,0.12)]" />
</div>
)
}
function WorktreeGlyphPanel(props: { className?: string }): JSX.Element {
return (
<div
className={cn(
'pointer-events-none absolute inset-0 flex items-center justify-center rounded-lg bg-background/55 transition-opacity duration-300',
props.visible ? 'opacity-100' : 'opacity-0'
)}
aria-hidden
>
<div
className={cn(
'relative w-[min(250px,88%)] rounded-lg border border-border bg-popover p-3 text-popover-foreground shadow-xs transition-[opacity,transform] duration-300',
props.visible
? 'translate-y-0 scale-100 opacity-100'
: 'translate-y-2 scale-[0.98] opacity-0'
)}
>
<div className="text-sm font-semibold leading-none text-foreground">Create Worktree</div>
<div className="mt-3 space-y-2">
<ModalField label="Project" value="orca" />
<ModalField label="Name" value={props.nameValue} typing={props.nameTyping} />
<ModalField label="Agent" value="Codex" icon={<CodexInlineIcon />} />
</div>
<div
ref={props.createButtonRef}
aria-hidden
className={cn(
'relative mt-3 flex h-8 w-full items-center justify-center rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground transition-all duration-200',
props.createHovered ? 'opacity-90' : null,
props.createClicked ? 'scale-[0.98]' : null
)}
>
Create worktree
</div>
</div>
</div>
)
}
function clearTypedTimeouts(ref: MutableRefObject<number[]>): void {
for (const timeoutId of ref.current) {
window.clearTimeout(timeoutId)
}
ref.current = []
}
function scheduleTypedText(
ref: MutableRefObject<number[]>,
text: string,
perCharMs: number,
setValue: (value: string) => void
): void {
clearTypedTimeouts(ref)
setValue('')
for (let i = 1; i <= text.length; i += 1) {
ref.current.push(window.setTimeout(() => setValue(text.slice(0, i)), i * perCharMs))
}
}
function useMeasuredCursor(
rootRef: RefObject<HTMLDivElement | null>,
plusRef: RefObject<HTMLElement | null>,
createButtonRef: RefObject<HTMLElement | null>,
target: 'hidden' | 'start' | 'plus' | 'create',
reducedMotion: boolean
): { x: number; y: number; visible: boolean } {
const [pos, setPos] = useState({ x: 0, y: 0, visible: false })
// Why: measuring targets keeps the cursor aligned as the setup card resizes.
useLayoutEffect(() => {
if (reducedMotion || target === 'hidden') {
setPos((current) => ({ ...current, visible: false }))
return
}
const root = rootRef.current
if (target === 'start') {
setPos({ x: 34, y: 218, visible: true })
return
}
const targetNode = target === 'plus' ? plusRef.current : createButtonRef.current
if (!root || !targetNode) {
return
}
const rootRect = root.getBoundingClientRect()
const targetRect = targetNode.getBoundingClientRect()
setPos({
x: targetRect.left - rootRect.left + targetRect.width * 0.58,
y: targetRect.top - rootRect.top + targetRect.height * 0.58,
visible: true
})
}, [createButtonRef, plusRef, reducedMotion, rootRef, target])
return pos
}
function ModalField(props: {
label: string
value: string
icon?: ReactNode
typing?: boolean
}): JSX.Element {
return (
<div>
<div className="text-[11px] font-medium text-muted-foreground">{props.label}</div>
<div className="mt-1 flex h-7 items-center gap-1.5 rounded-md border border-border bg-background px-2 text-xs font-medium text-foreground">
{props.icon ? (
<span className="flex size-3.5 shrink-0 items-center">{props.icon}</span>
) : null}
<span className="truncate">{props.value}</span>
{props.typing ? (
<span className="ml-px inline-block h-[10px] w-[5px] animate-pulse bg-foreground" />
) : null}
</div>
</div>
)
}
function useLoopedPhase<T extends string>(
phases: readonly T[],
intervalMs: number,
reducedMotion: boolean,
reducedMotionPhase: T
): T {
const [idx, setIdx] = useState(() => {
const reducedIdx = phases.indexOf(reducedMotionPhase)
return reducedMotion && reducedIdx >= 0 ? reducedIdx : 0
})
useEffect(() => {
if (reducedMotion) {
const reducedIdx = phases.indexOf(reducedMotionPhase)
setIdx(Math.max(reducedIdx, 0))
return
}
setIdx(0)
const id = window.setInterval(() => {
setIdx((current) => (current + 1) % phases.length)
}, intervalMs)
return () => window.clearInterval(id)
}, [intervalMs, phases, reducedMotion, reducedMotionPhase])
return phases[idx] ?? phases[0]
}
function VisualTitlebar(props: { title: string; compact?: boolean }): JSX.Element {
return (
<div
className={cn(
'flex items-center gap-1.5 border-b border-border bg-muted/40',
props.compact ? 'h-6 px-2' : 'h-8 px-3'
)}
>
<span className="size-2 rounded-full bg-foreground/15" />
<span className="size-2 rounded-full bg-foreground/15" />
<span className="size-2 rounded-full bg-foreground/15" />
<span className="ml-1 truncate font-mono text-[11px] text-muted-foreground">
{props.title}
</span>
</div>
)
}
function WorkspaceListCard(props: {
title: string
active: boolean
prompt: string
icon: ReactNode
state: 'idle' | 'starting' | 'working'
reducedMotion: boolean
className?: string
}): JSX.Element {
return (
<div
className={cn(
'rounded-md border border-sidebar-border px-2.5 py-2 transition-colors',
props.active ? 'bg-sidebar-accent' : 'bg-sidebar',
'absolute flex h-[70px] w-[108px] items-start gap-2 rounded-[10px] border border-border p-3',
props.className
)}
>
<div className="flex items-center gap-2">
<span
className={cn(
'size-1.5 shrink-0 rounded-full',
props.active ? 'bg-emerald-500' : 'bg-muted-foreground/35'
)}
/>
<span className="truncate text-xs font-medium text-sidebar-foreground">{props.title}</span>
</div>
<div className="mt-2 grid grid-cols-[8px_14px_minmax(0,1fr)] items-center gap-1.5">
{props.state === 'working' || props.state === 'starting' ? (
<WorkingSpinner size="xs" reducedMotion={props.reducedMotion} />
) : (
<span className="size-1.5 rounded-full bg-muted-foreground/35" />
)}
<span className="flex size-3.5 items-center justify-center text-sidebar-foreground/65">
{props.icon}
</span>
<span className="truncate font-mono text-[11px] text-sidebar-foreground/65">
{props.state === 'idle' ? 'ready' : props.prompt}
</span>
</div>
<span className="mt-0.5 size-2 shrink-0 rounded-full bg-emerald-500 ring-[3px] ring-emerald-500/10" />
<span className="flex min-w-0 flex-1 flex-col gap-1.5">
<span className="h-[5px] w-4/5 rounded-full bg-foreground/10" />
<span className="h-[5px] w-1/2 rounded-full bg-foreground/10" />
</span>
</div>
)
}
function TerminalLine(props: { children: ReactNode; muted?: boolean }): JSX.Element {
// Start work in multiple repos: two project cards, each a folder + name and a
// live worktree row (emerald dot) — your repos, each running their own work.
export function SetupMultipleReposVisual(): JSX.Element {
return (
<div
className={cn(
'flex min-w-0 items-center gap-1.5 truncate leading-[1.45]',
props.muted ? 'text-muted-foreground' : 'text-foreground'
)}
>
{props.children}
<div aria-hidden className="flex w-[156px] shrink-0 flex-col gap-2.5">
<RepoCard nameWidth="w-[62%]" worktreeWidth="w-[78%]" />
<RepoCard nameWidth="w-[70%]" worktreeWidth="w-[66%]" />
</div>
)
}
function Prompt(props: { children: ReactNode }): JSX.Element {
return <span className="shrink-0 text-primary">{props.children}</span>
function RepoCard(props: { nameWidth: string; worktreeWidth: string }): JSX.Element {
return (
<div className="flex flex-col gap-2 rounded-[10px] border-[1.5px] border-emerald-500/35 bg-muted p-2.5 shadow-[0_6px_16px_rgba(0,0,0,0.12)]">
<span className="flex items-center gap-1.5">
<FolderGit2 className="size-[15px] shrink-0 text-muted-foreground" />
<span className={cn('h-[5px] rounded-full bg-foreground/10', props.nameWidth)} />
</span>
<span className="flex items-center gap-1.5">
<span className="size-2 shrink-0 rounded-full bg-emerald-500 ring-[3px] ring-emerald-500/10" />
<span className={cn('h-[5px] rounded-full bg-foreground/10', props.worktreeWidth)} />
</span>
</div>
)
}
@@ -37,15 +37,8 @@ vi.mock('@/runtime/web-runtime-session', () => ({
vi.mock('./FeatureWallSetupStepVisuals', () => ({
SetupTwoAgentsVisual: () => <div />,
SetupWorkspacesVisual: () => <div />
}))
vi.mock('./AddReposAnimatedVisual', () => ({
AddReposAnimatedVisual: () => <div />
}))
vi.mock('./SetupScriptAnimatedVisual', () => ({
SetupScriptAnimatedVisual: () => <div />
SetupWorkspacesVisual: () => <div />,
SetupMultipleReposVisual: () => <div />
}))
function makeRepo(id: string, overrides: Partial<Repo> = {}): Repo {
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { activateAndRevealWorktree } from '@/lib/worktree-activation'
import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface'
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
import { useAppStore } from '@/store'
import { useAllWorktrees } from '@/store/selectors'
import { getDefaultRepoHookSettings } from '../../../../shared/constants'
@@ -16,10 +17,6 @@ import type {
Worktree
} from '../../../../shared/types'
import { getRepositoryLocalCommandsSectionId } from '../settings/repository-settings-targets'
import { AddReposAnimatedVisual } from './AddReposAnimatedVisual'
import { SetupTwoAgentsVisual, SetupWorkspacesVisual } from './FeatureWallSetupStepVisuals'
import { SetupScriptAnimatedVisual } from './SetupScriptAnimatedVisual'
import { SetupStepPreview } from './SetupStepPreview'
import {
requestContextualTourWhenReady,
type RequestContextualTourWhenReadyArgs
@@ -43,25 +40,17 @@ export function getSetupGuideGitRepo(
return activeRepo ?? repos.find((entry) => isGitRepoKind(entry)) ?? null
}
export function AddReposAction(props: { reducedMotion: boolean }): React.JSX.Element {
export function AddReposAction(): React.JSX.Element {
const openModal = useAppStore((s) => s.openModal)
return (
<div className="space-y-4">
<Button type="button" size="sm" className="w-fit gap-2" onClick={() => openModal('add-repo')}>
<Plus className="size-3.5" />
Add project
</Button>
<SetupStepPreview>
<AddReposAnimatedVisual reducedMotion={props.reducedMotion} />
</SetupStepPreview>
</div>
<Button type="button" size="sm" className="w-fit gap-2" onClick={() => openModal('add-repo')}>
<Plus className="size-3.5" />
Add project
</Button>
)
}
export function TwoAgentsAction(props: {
reducedMotion: boolean
done: boolean
}): React.JSX.Element {
export function TwoAgentsAction(props: { done: boolean }): React.JSX.Element | null {
const targetWorktree = useSetupTargetWorktree()
const openModal = useAppStore((s) => s.openModal)
const closeModal = useAppStore((s) => s.closeModal)
@@ -84,73 +73,80 @@ export function TwoAgentsAction(props: {
})
}, [closeModal, openModal, targetWorktree])
if (props.done || paneTarget) {
return null
}
return (
<div className="space-y-4">
{!props.done && !paneTarget ? (
<div>
<div className="flex flex-wrap gap-2">
<Button type="button" size="sm" className="w-fit gap-2" onClick={handlePrimaryAction}>
<ArrowUpRight className="size-3.5" />
Try it out
</Button>
</div>
</div>
) : null}
<SetupStepPreview>
<SetupTwoAgentsVisual reducedMotion={props.reducedMotion} />
</SetupStepPreview>
<Button type="button" size="sm" className="w-fit gap-2" onClick={handlePrimaryAction}>
<ArrowUpRight className="size-3.5" />
Try it out
</Button>
)
}
const SETUP_HINT_KBD_CLASS =
'rounded border border-border bg-card px-1.5 py-0.5 font-mono text-[11.5px] text-foreground'
// Platform-aware split/close shortcuts for the active terminal pane. The
// labels resolve to ⌘D / Ctrl+Shift+D etc. based on the user's OS and overrides.
export function SplitTerminalShortcutHint(): React.JSX.Element {
const splitRight = useShortcutLabel('terminal.splitRight')
const splitDown = useShortcutLabel('terminal.splitDown')
const closePane = useShortcutLabel('terminal.closePane')
return (
<div className="space-y-1.5 text-[13px] leading-relaxed text-muted-foreground">
<p>
Split right with <kbd className={SETUP_HINT_KBD_CLASS}>{splitRight}</kbd> or down with{' '}
<kbd className={SETUP_HINT_KBD_CLASS}>{splitDown}</kbd>, or right-click a pane and choose a
split. Close the active pane with <kbd className={SETUP_HINT_KBD_CLASS}>{closePane}</kbd>.
</p>
</div>
)
}
export function WorkspacesAction(props: {
reducedMotion: boolean
done: boolean
}): React.JSX.Element {
export function WorkspacesAction(props: { done: boolean }): React.JSX.Element | null {
const openModal = useAppStore((s) => s.openModal)
const activeRepoId = useAppStore((s) => s.activeRepoId)
const repos = useAppStore((s) => s.repos)
const repo = getSetupGuideGitRepo(repos, activeRepoId)
if (props.done) {
return null
}
return (
<div className="space-y-4">
{!props.done ? (
<Button
type="button"
size="sm"
className="w-fit gap-2"
onClick={() => {
cancelPendingSetupGuideTourRequest()
if (!repo) {
promptForSetupGuideProject(openModal)
return
}
const tourRequestId = createSetupGuideTourRequestId()
openModal('new-workspace-composer', {
initialRepoId: repo.id,
telemetrySource: 'unknown',
contextualTourSource: 'setup_guide_parallel_work',
setupGuideTourRequestId: tourRequestId
})
requestSetupGuideTourWhenReady({
id: 'workspace-creation',
source: 'setup_guide_parallel_work',
wasFeaturePreviouslyInteracted: false,
shouldContinue: () => isSetupGuideWorkspaceComposerRequestCurrent(tourRequestId)
})
}}
>
<ArrowUpRight className="size-3.5" />
Try it out
</Button>
) : null}
<SetupStepPreview>
<SetupWorkspacesVisual reducedMotion={props.reducedMotion} />
</SetupStepPreview>
</div>
<Button
type="button"
size="sm"
className="w-fit gap-2"
onClick={() => {
cancelPendingSetupGuideTourRequest()
if (!repo) {
promptForSetupGuideProject(openModal)
return
}
const tourRequestId = createSetupGuideTourRequestId()
openModal('new-workspace-composer', {
initialRepoId: repo.id,
telemetrySource: 'unknown',
contextualTourSource: 'setup_guide_parallel_work',
setupGuideTourRequestId: tourRequestId
})
requestSetupGuideTourWhenReady({
id: 'workspace-creation',
source: 'setup_guide_parallel_work',
wasFeaturePreviouslyInteracted: false,
shouldContinue: () => isSetupGuideWorkspaceComposerRequestCurrent(tourRequestId)
})
}}
>
<ArrowUpRight className="size-3.5" />
Try it out
</Button>
)
}
export function SetupScriptAction(props: { reducedMotion: boolean }): React.JSX.Element {
export function SetupScriptAction(): React.JSX.Element {
const repos = useAppStore((s) => s.repos)
const activeRepoId = useAppStore((s) => s.activeRepoId)
const closeModal = useAppStore((s) => s.closeModal)
@@ -212,47 +208,42 @@ export function SetupScriptAction(props: { reducedMotion: boolean }): React.JSX.
return (
<div className="space-y-4">
<div className="rounded-lg border border-border bg-muted/20 p-3">
<div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_auto]">
<Input
value={setupScript}
disabled={!canConfigure}
onChange={(event) => setSetupScript(event.target.value)}
placeholder="pnpm install"
aria-label="Setup script"
className="font-mono text-sm"
/>
<Button
type="button"
size="sm"
className="gap-2"
disabled={!canConfigure || setupScript.trim().length === 0}
onClick={() => void handleSaveSetupScript()}
>
<Save className="size-3.5" />
Save
</Button>
</div>
<div className="grid max-w-2xl gap-2 sm:grid-cols-[minmax(0,1fr)_auto]">
<Input
value={setupScript}
disabled={!canConfigure}
onChange={(event) => setSetupScript(event.target.value)}
placeholder="pnpm install"
aria-label="Setup script"
className="font-mono text-sm"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="mt-2 w-fit gap-2 px-0 text-muted-foreground hover:bg-transparent hover:text-foreground"
disabled={!canConfigure}
onClick={openLocalCommandSettings}
className="gap-2"
disabled={!canConfigure || setupScript.trim().length === 0}
onClick={() => void handleSaveSetupScript()}
>
<Settings className="size-3.5" />
View in settings
<Save className="size-3.5" />
Save
</Button>
</div>
<Button
type="button"
variant="ghost"
size="sm"
className="w-fit gap-2 px-0 text-muted-foreground hover:bg-transparent hover:text-foreground"
disabled={!canConfigure}
onClick={openLocalCommandSettings}
>
<Settings className="size-3.5" />
View in settings
</Button>
{!canConfigure ? (
<p className="text-xs text-muted-foreground">
Add a git project first, then configure the setup script for that repository.
</p>
) : null}
<SetupStepPreview>
<SetupScriptAnimatedVisual reducedMotion={props.reducedMotion} />
</SetupStepPreview>
</div>
)
}
@@ -1,408 +0,0 @@
/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: this visual is a timed storyboard; phase and typed-name state intentionally advance from animation effects and the reduced-motion gate. */
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import type { JSX, ReactNode, RefObject } from 'react'
import { FolderGit2, Plus, TerminalSquare } from 'lucide-react'
import { cn } from '@/lib/utils'
import { ClaudeIcon } from '../status-bar/icons'
import { CodexInlineIcon, CursorIcon, WorkingSpinner } from './feature-tour-preview-glyphs'
import { FeatureWallClickRing } from './FeatureWallClickRing'
import { SetupScriptNewWorkspaceModal } from './SetupScriptNewWorkspaceModal'
import { SetupScriptWorkspaceListCard } from './SetupScriptWorkspaceListCard'
const PHASES = [
{ name: 'create-init', duration: 1200 },
{ name: 'plus-hover', duration: 600 },
{ name: 'plus-click', duration: 400 },
{ name: 'modal-visible', duration: 600 },
{ name: 'modal-typing', duration: 1500 },
{ name: 'modal-complete', duration: 600 },
{ name: 'create-click', duration: 400 },
{ name: 'modal-closing', duration: 400 },
{ name: 'worktree-opening', duration: 1000 },
{ name: 'pane-splitting', duration: 1000 },
{ name: 'setup-running', duration: 2500 },
{ name: 'setup-complete', duration: 800 },
{ name: 'agent-running', duration: 3000 },
{ name: 'dwell', duration: 2000 }
] as const
export function SetupScriptAnimatedVisual(props: { reducedMotion: boolean }): JSX.Element {
const { reducedMotion } = props
const rootRef = useRef<HTMLDivElement | null>(null)
const plusRef = useRef<HTMLSpanElement | null>(null)
const createButtonRef = useRef<HTMLDivElement | null>(null)
const [phaseIdx, setPhaseIdx] = useState(0)
const [modalNameValue, setModalNameValue] = useState('')
// Why: variable-length timeout loop to perfectly sync animation story beats.
useEffect(() => {
if (reducedMotion) {
setPhaseIdx(PHASES.findIndex((p) => p.name === 'agent-running'))
return
}
let timer: number
const tick = (idx: number) => {
const currentPhase = PHASES[idx]
timer = window.setTimeout(() => {
const nextIdx = (idx + 1) % PHASES.length
setPhaseIdx(nextIdx)
tick(nextIdx)
}, currentPhase.duration)
}
tick(0)
return () => {
window.clearTimeout(timer)
}
}, [reducedMotion])
const phase = PHASES[phaseIdx]?.name ?? 'agent-running'
// Why: type out the worktree name character-by-character to simulate user input.
useEffect(() => {
if (reducedMotion) {
setModalNameValue('checkout fix')
return
}
if (
phase === 'create-init' ||
phase === 'plus-hover' ||
phase === 'plus-click' ||
phase === 'modal-visible'
) {
setModalNameValue('')
return
}
if (phase === 'modal-typing') {
const text = 'checkout fix'
let current = ''
let idx = 0
const timer = window.setInterval(() => {
if (idx < text.length) {
current += text[idx]
setModalNameValue(current)
idx++
} else {
window.clearInterval(timer)
}
}, 75)
return () => window.clearInterval(timer)
}
setModalNameValue('checkout fix')
return undefined
}, [phase, reducedMotion])
const isWorkspaceActive = !(
phase === 'create-init' ||
phase === 'plus-hover' ||
phase === 'plus-click' ||
phase === 'modal-visible' ||
phase === 'modal-typing' ||
phase === 'modal-complete' ||
phase === 'create-click' ||
phase === 'modal-closing'
)
const modalVisible =
phase === 'modal-visible' ||
phase === 'modal-typing' ||
phase === 'modal-complete' ||
phase === 'create-click'
const plusHovered = phase === 'plus-hover'
const plusClicked = phase === 'plus-click'
const createHovered = phase === 'modal-complete'
const createClicked = phase === 'create-click'
const cursorTarget =
phase === 'plus-hover' || phase === 'plus-click'
? 'plus'
: phase === 'modal-complete' || phase === 'create-click'
? 'create'
: phase === 'create-init'
? 'start'
: 'hidden'
const cursor = useMeasuredCursor(rootRef, plusRef, createButtonRef, cursorTarget, reducedMotion)
return (
<div
ref={rootRef}
className="relative grid min-h-[240px] gap-3 rounded-xl border border-border bg-card p-3 text-foreground shadow-xs md:grid-cols-[180px_minmax(0,1fr)] overflow-hidden"
>
{/* Sidebar */}
<div className="flex min-w-0 flex-col rounded-lg border border-sidebar-border bg-sidebar p-2 text-sidebar-foreground h-full">
<div className="flex h-8 items-center justify-between gap-2 px-1.5">
<div className="flex items-center gap-2 min-w-0">
<FolderGit2 className="size-3.5 text-muted-foreground" />
<span className="truncate text-[13px] font-semibold">orca</span>
</div>
<span
ref={plusRef}
className={cn(
'relative flex size-5 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-all duration-300 z-10',
plusHovered ? 'bg-sidebar-accent text-sidebar-accent-foreground' : null,
plusClicked
? 'scale-90 bg-sidebar-accent text-sidebar-accent-foreground animate-pulse'
: null
)}
>
<Plus className="size-3.5" />
{plusClicked && <FeatureWallClickRing />}
</span>
</div>
<div className="mt-1 flex flex-col gap-1.5 min-w-0 flex-1">
<SetupScriptWorkspaceListCard
title="release notes"
active={!isWorkspaceActive}
prompt="draft release notes"
icon={<ClaudeIcon size={12} />}
state={!isWorkspaceActive ? 'working' : 'idle'}
reducedMotion={reducedMotion}
/>
<SetupScriptWorkspaceListCard
title="checkout fix"
active={isWorkspaceActive}
prompt="fix checkout timeout"
icon={<CodexInlineIcon />}
state={
phase === 'worktree-opening' || phase === 'pane-splitting'
? 'starting'
: phase === 'setup-running'
? 'setup'
: isWorkspaceActive
? 'working'
: 'idle'
}
reducedMotion={reducedMotion}
className={cn(
'transition-all duration-500 ease-in-out',
isWorkspaceActive
? 'opacity-100 translate-y-0 scale-100'
: 'opacity-0 -translate-y-1 scale-95 h-0 overflow-hidden border-none p-0 m-0'
)}
/>
</div>
</div>
{/* Terminal Pane / Screen */}
<div className="relative min-w-0 overflow-hidden rounded-lg border border-border bg-background h-full flex-1">
{/* Inactive Terminal (Visible when no active worktree) */}
<div
className={cn(
'absolute inset-0 flex flex-col bg-background transition-opacity duration-300 z-10',
isWorkspaceActive ? 'opacity-0 pointer-events-none' : 'opacity-100'
)}
>
<div className="flex h-7 items-center gap-1.5 border-b border-border bg-muted/40 px-2.5">
<TerminalSquare className="size-3.5 text-muted-foreground/60" />
<span className="truncate text-[11px] font-medium text-muted-foreground/60">
Terminal
</span>
</div>
<div className="flex-1 p-3 font-mono text-[11px] text-muted-foreground/30">
<div className="flex items-center gap-1.5">
<span>$</span>
<span className="h-[10px] w-[5px] bg-muted-foreground/30 animate-pulse" />
</div>
</div>
</div>
{/* Active Split Terminal Pane (Visible when worktree opens) */}
<div
className={cn(
'absolute inset-0 flex h-full w-full gap-2 p-2 bg-background transition-opacity duration-300 z-20',
isWorkspaceActive ? 'opacity-100' : 'opacity-0 pointer-events-none'
)}
>
{/* Agent Pane (Left) */}
<div
className={cn(
'flex flex-col rounded-md border border-border bg-card/40 overflow-hidden h-full transition-all duration-700 ease-in-out min-w-0',
phase === 'worktree-opening' ? 'w-full' : 'w-1/2'
)}
>
<div className="flex h-6 items-center gap-1.5 border-b border-border bg-muted/40 px-2 shrink-0">
<CodexInlineIcon />
<span className="truncate text-[10px] font-semibold text-muted-foreground">
Agent: Codex
</span>
</div>
<div className="flex-1 p-2 font-mono text-[10px] space-y-1.5 overflow-hidden">
<TerminalLine>
<Prompt>$</Prompt> orca agent start
</TerminalLine>
{phase === 'worktree-opening' ||
phase === 'pane-splitting' ||
phase === 'setup-running' ? (
<TerminalLine muted>
<WorkingSpinner size="xs" reducedMotion={reducedMotion} />
Waiting for setup script...
</TerminalLine>
) : phase === 'setup-complete' ? (
<TerminalLine muted>
<span>Initializing agent...</span>
</TerminalLine>
) : (
<>
<TerminalLine muted>
<span className="text-emerald-500 font-semibold">✓ Setup complete</span>
</TerminalLine>
<TerminalLine className="text-foreground">
<WorkingSpinner size="xs" reducedMotion={reducedMotion} />
<span>Running: checkout timeout</span>
</TerminalLine>
<TerminalLine muted className="pl-3 text-[9px] text-muted-foreground/75">
<span>Reading checkout.test.ts</span>
</TerminalLine>
<TerminalLine muted className="pl-3 text-[9px] text-muted-foreground/75">
<span>Modifying checkout.ts</span>
</TerminalLine>
</>
)}
</div>
</div>
{/* Setup Script Pane (Right - Splits and slides/fades in) */}
<div
className={cn(
'flex flex-col rounded-md border border-border bg-card/40 overflow-hidden h-full transition-all duration-700 ease-in-out min-w-0',
phase === 'worktree-opening' ? 'w-0 opacity-0 border-none p-0' : 'w-1/2 opacity-100'
)}
>
<div className="flex h-6 items-center gap-1.5 border-b border-border bg-muted/40 px-2 shrink-0">
<TerminalSquare className="size-3 text-muted-foreground" />
<span className="truncate text-[10px] font-semibold text-muted-foreground">
Setup Script
</span>
</div>
<div className="flex-1 p-2 font-mono text-[10px] space-y-1.5 overflow-hidden">
<TerminalLine>
<Prompt>$</Prompt> pnpm install
</TerminalLine>
{phase === 'pane-splitting' ? (
<TerminalLine muted>
<span className="h-2.5 w-1 bg-muted-foreground/40 animate-pulse" />
</TerminalLine>
) : phase === 'setup-running' ? (
<>
<TerminalLine muted>
<WorkingSpinner size="xs" reducedMotion={reducedMotion} />
<span>Installing dependencies</span>
</TerminalLine>
<TerminalLine muted className="text-[9px] text-muted-foreground/75 pl-3">
<span>pnpm-lock.yaml found</span>
</TerminalLine>
<TerminalLine muted className="text-[9px] text-muted-foreground/75 pl-3">
<span>Resolving package tree...</span>
</TerminalLine>
</>
) : (
<>
<TerminalLine muted className="text-emerald-500">
<span className="size-1.5 rounded-full bg-emerald-500 shrink-0" />
<span className="font-semibold text-emerald-500">Dependencies ready</span>
</TerminalLine>
<TerminalLine muted className="text-[9px] text-muted-foreground/75 pl-3">
<span>Done in 1.4s</span>
</TerminalLine>
</>
)}
</div>
</div>
</div>
{/* Create Worktree Modal Overlay */}
<SetupScriptNewWorkspaceModal
visible={modalVisible}
nameValue={modalNameValue}
nameTyping={phase === 'modal-typing'}
createHovered={createHovered}
createClicked={createClicked}
createButtonRef={createButtonRef}
/>
</div>
{/* Floating Animated Cursor */}
<div
aria-hidden
className={cn(
'pointer-events-none absolute left-0 top-0 z-50 size-5 drop-shadow-sm transition-[opacity,transform] duration-700 ease-[cubic-bezier(.45,.05,.2,1)] [&_svg]:size-5',
cursor.visible ? 'opacity-100' : 'opacity-0'
)}
style={{ transform: `translate(${cursor.x}px, ${cursor.y}px)` }}
>
<div className="relative">
<CursorIcon />
{createClicked && <FeatureWallClickRing />}
</div>
</div>
</div>
)
}
// Why: measuring targets keeps the cursor aligned as the setup card resizes.
function useMeasuredCursor(
rootRef: RefObject<HTMLDivElement | null>,
plusRef: RefObject<HTMLElement | null>,
createButtonRef: RefObject<HTMLDivElement | null>,
target: 'hidden' | 'start' | 'plus' | 'create',
reducedMotion: boolean
): { x: number; y: number; visible: boolean } {
const [pos, setPos] = useState({ x: 0, y: 0, visible: false })
useLayoutEffect(() => {
if (reducedMotion || target === 'hidden') {
setPos((current) => ({ ...current, visible: false }))
return
}
const root = rootRef.current
if (target === 'start') {
setPos({ x: 30, y: 150, visible: true })
return
}
const targetNode = target === 'plus' ? plusRef.current : createButtonRef.current
if (!root || !targetNode) {
return
}
const rootRect = root.getBoundingClientRect()
const targetRect = targetNode.getBoundingClientRect()
setPos({
x: targetRect.left - rootRect.left + targetRect.width * 0.58,
y: targetRect.top - rootRect.top + targetRect.height * 0.58,
visible: true
})
}, [createButtonRef, plusRef, reducedMotion, rootRef, target])
return pos
}
function TerminalLine(props: {
children: ReactNode
muted?: boolean
className?: string
}): JSX.Element {
return (
<div
className={cn(
'flex min-w-0 items-center gap-1.5 truncate leading-[1.45]',
props.muted ? 'text-muted-foreground' : 'text-foreground',
props.className
)}
>
{props.children}
</div>
)
}
function Prompt(props: { children: ReactNode }): JSX.Element {
return <span className="shrink-0 text-primary">{props.children}</span>
}
@@ -1,72 +0,0 @@
import type { JSX, RefObject } from 'react'
import { cn } from '@/lib/utils'
type SetupScriptNewWorkspaceModalProps = {
visible: boolean
nameValue: string
nameTyping: boolean
createHovered: boolean
createClicked: boolean
createButtonRef: RefObject<HTMLDivElement | null>
}
export function SetupScriptNewWorkspaceModal({
visible,
nameValue,
nameTyping,
createHovered,
createClicked,
createButtonRef
}: SetupScriptNewWorkspaceModalProps): JSX.Element {
return (
<div
className={cn(
'pointer-events-none absolute inset-0 flex items-center justify-center rounded-lg bg-background/55 backdrop-blur-[1px] transition-opacity duration-300 z-40',
visible ? 'opacity-100' : 'opacity-0'
)}
aria-hidden
>
<div
className={cn(
'relative w-[min(240px,92%)] rounded-lg border border-border bg-popover p-3 text-popover-foreground shadow-lg transition-[opacity,transform] duration-300',
visible ? 'translate-y-0 scale-100 opacity-100' : 'translate-y-2 scale-[0.98] opacity-0'
)}
>
<div className="text-xs font-semibold leading-none text-foreground">Create Worktree</div>
<div className="mt-2.5 space-y-1.5">
<SetupScriptModalField label="Project" value="orca" />
<SetupScriptModalField label="Name" value={nameValue} typing={nameTyping} />
<SetupScriptModalField label="Agent" value="Codex" />
</div>
<div
ref={createButtonRef}
className={cn(
'mt-3 flex h-7 w-full items-center justify-center rounded-md bg-primary px-3 text-[11px] font-medium text-primary-foreground transition-all duration-200',
createHovered ? 'opacity-90' : null,
createClicked ? 'scale-[0.98]' : null
)}
>
Create worktree
</div>
</div>
</div>
)
}
function SetupScriptModalField(props: {
label: string
value: string
typing?: boolean
}): JSX.Element {
return (
<div className="flex items-center justify-between gap-2 rounded-md border border-border bg-muted/30 px-2 py-1 text-[11px]">
<span className="text-muted-foreground">{props.label}</span>
<div className="flex items-center font-mono font-medium text-foreground min-w-0">
<span className="truncate">{props.value}</span>
{props.typing ? (
<span className="ml-px inline-block h-2.5 w-[5px] animate-pulse bg-foreground" />
) : null}
</div>
</div>
)
}
@@ -1,54 +0,0 @@
import type { JSX, ReactNode } from 'react'
import { cn } from '@/lib/utils'
import { WorkingSpinner } from './feature-tour-preview-glyphs'
type SetupScriptWorkspaceListCardProps = {
title: string
active: boolean
prompt?: string
icon?: ReactNode
state: 'idle' | 'starting' | 'setup' | 'working'
reducedMotion: boolean
className?: string
}
export function SetupScriptWorkspaceListCard(
props: SetupScriptWorkspaceListCardProps
): JSX.Element {
return (
<div
className={cn(
'rounded-md border border-sidebar-border px-2 py-1.5 transition-colors duration-300',
props.active ? 'bg-sidebar-accent' : 'bg-sidebar',
props.className
)}
>
<div className="flex items-center gap-1.5">
<span
className={cn(
'size-1.5 shrink-0 rounded-full transition-colors duration-300',
props.active ? 'bg-emerald-500' : 'bg-muted-foreground/35'
)}
/>
<span className="truncate text-xs font-semibold text-sidebar-foreground">
{props.title}
</span>
</div>
{props.prompt ? (
<div className="mt-1.5 grid grid-cols-[8px_14px_minmax(0,1fr)] items-center gap-1.5">
{props.state === 'working' || props.state === 'starting' || props.state === 'setup' ? (
<WorkingSpinner size="xs" reducedMotion={props.reducedMotion} />
) : (
<span className="size-1.5 rounded-full bg-muted-foreground/35" />
)}
<span className="flex size-3.5 items-center justify-center text-sidebar-foreground/65">
{props.icon}
</span>
<span className="truncate font-mono text-[10px] text-sidebar-foreground/65">
{props.state === 'setup' ? 'running setup' : props.prompt}
</span>
</div>
) : null}
</div>
)
}
@@ -1,18 +0,0 @@
import type { ReactNode } from 'react'
import { cn } from '@/lib/utils'
type SetupStepPreviewProps = {
children: ReactNode
className?: string
}
export function SetupStepPreview(props: SetupStepPreviewProps): React.JSX.Element {
return (
<div className={cn('relative', props.className)}>
<div className="pointer-events-none absolute left-3 top-3 z-20 rounded-md bg-card/85 px-1.5 py-0.5 text-xs font-semibold leading-none text-muted-foreground shadow-xs backdrop-blur-sm">
Preview
</div>
<div className="h-full [&>*]:h-full [&>*]:pt-8">{props.children}</div>
</div>
)
}
+4 -4
View File
@@ -27,14 +27,15 @@ export const FEATURE_WALL_SETUP_STEPS: readonly FeatureWallSetupStep[] = [
id: 'split-terminal',
name: 'Split a terminal',
subtitle: 'Split a terminal',
description: 'Using this, you can run 2 agents side-by-side.'
description:
'Split a terminal to run two things at once: two agents, an agent next to a dev server, or logs beside a shell.'
},
{
id: 'two-worktrees',
name: 'Multi-task',
subtitle: 'Multi-task',
description:
'Have 2 worktrees at once. Each one is isolated (even in the same project). Perfect for working on 2 features at once.'
'Work in 2 different worktrees at once. Each one is isolated (even in the same project). Perfect for working on 2 features at once.'
},
{
id: 'notifications',
@@ -72,8 +73,7 @@ export const FEATURE_WALL_SETUP_STEPS: readonly FeatureWallSetupStep[] = [
id: 'agent-capabilities',
name: 'Enable Orca CLI',
subtitle: 'Enable Orca CLI',
description:
'Let agents use the browser, computer, and orchestration tools when a task needs it.'
description: 'Let agents use the browser, computer, and orchestration tools.'
}
] as const