mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 00:02:37 +00:00
Declutter setup guide visuals (#4614)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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>></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>></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>></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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user