From 843c345768f16da05be3ac044f529e47ebdb2074 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 4 Jun 2026 22:23:28 -0700 Subject: [PATCH] Declutter setup guide visuals (#4614) Co-authored-by: Orca --- src/main/claude-usage/store.test.ts | 3 +- .../feature-wall/AddReposAnimatedVisual.tsx | 201 ------ .../AgentCapabilitiesSetupAction.tsx | 124 +--- .../FeatureWallSetupChecklist.tsx | 138 ++-- .../FeatureWallSetupStepVisuals.tsx | 611 +++--------------- .../FeatureWallSetupWorkflowActions.test.tsx | 11 +- .../FeatureWallSetupWorkflowActions.tsx | 195 +++--- .../SetupScriptAnimatedVisual.tsx | 408 ------------ .../SetupScriptNewWorkspaceModal.tsx | 72 --- .../SetupScriptWorkspaceListCard.tsx | 54 -- .../feature-wall/SetupStepPreview.tsx | 18 - src/shared/feature-wall-setup-steps.ts | 8 +- 12 files changed, 278 insertions(+), 1565 deletions(-) delete mode 100644 src/renderer/src/components/feature-wall/AddReposAnimatedVisual.tsx delete mode 100644 src/renderer/src/components/feature-wall/SetupScriptAnimatedVisual.tsx delete mode 100644 src/renderer/src/components/feature-wall/SetupScriptNewWorkspaceModal.tsx delete mode 100644 src/renderer/src/components/feature-wall/SetupScriptWorkspaceListCard.tsx delete mode 100644 src/renderer/src/components/feature-wall/SetupStepPreview.tsx diff --git a/src/main/claude-usage/store.test.ts b/src/main/claude-usage/store.test.ts index 2f4c89d4b38..b8342ba0846 100644 --- a/src/main/claude-usage/store.test.ts +++ b/src/main/claude-usage/store.test.ts @@ -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 diff --git a/src/renderer/src/components/feature-wall/AddReposAnimatedVisual.tsx b/src/renderer/src/components/feature-wall/AddReposAnimatedVisual.tsx deleted file mode 100644 index 06f6350c52a..00000000000 --- a/src/renderer/src/components/feature-wall/AddReposAnimatedVisual.tsx +++ /dev/null @@ -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 - 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: - }, - { - id: 'work', - name: 'billing-app', - worktree: 'checkout fix', - prompt: 'fix checkout bug', - action: 'Updating checkout tests', - agent: 'Codex', - agentIcon: - } -] - -const FOCUS_SEQUENCE: readonly RepoFocus[] = ['personal', 'both', 'work', 'both', 'both'] - -export function AddReposAnimatedVisual(props: { reducedMotion: boolean }): JSX.Element { - const focus = useRepoFocus(props.reducedMotion) - - return ( -
- {REPOS.map((repo) => ( - - ))} -
- ) -} - -function RepoProjectCard(props: { - repo: RepoVisual - active: boolean - reducedMotion: boolean -}): JSX.Element { - return ( -
- - - - -
- -
- - > - {props.repo.prompt} - - - {props.repo.agentIcon} - {props.repo.agent} session ready - - - - {props.repo.action} - -
-
-
- ) -} - -function ProjectSidebarRow(props: { name: string }): JSX.Element { - return ( -
- - - - - {props.name} - - - 1 worktree - -
- ) -} - -function ProjectWorktreeRow(props: { - repo: RepoVisual - active: boolean - reducedMotion: boolean -}): JSX.Element { - return ( -
-
- - - {props.repo.worktree} - -
-
- - - {props.repo.agentIcon} - - - {props.repo.prompt} - -
-
- ) -} - -function VisualTitlebar(props: { title: string }): JSX.Element { - return ( -
- - - - - {props.title} - -
- ) -} - -function TerminalLine(props: { children: ReactNode; muted?: boolean }): JSX.Element { - return ( -
- {props.children} -
- ) -} - -function Prompt(props: { children: ReactNode }): JSX.Element { - return {props.children} -} - -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' -} diff --git a/src/renderer/src/components/feature-wall/AgentCapabilitiesSetupAction.tsx b/src/renderer/src/components/feature-wall/AgentCapabilitiesSetupAction.tsx index d50517a3690..904ed538ee3 100644 --- a/src/renderer/src/components/feature-wall/AgentCapabilitiesSetupAction.tsx +++ b/src/renderer/src/components/feature-wall/AgentCapabilitiesSetupAction.tsx @@ -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} /> - ) } @@ -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: }, { 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: }, { id: 'computerUse', title: 'Computer Use', + description: + 'Let agents control the desktop, moving the cursor, clicking, and typing in any app.', icon: } ] @@ -247,6 +240,9 @@ function AgentCapabilitySetupChecklist(props: { {row.title} + + {row.description} + ) @@ -290,99 +286,3 @@ function AgentCapabilityStatusNote(props: { ) } - -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( - () => [ - { - id: 'browser-use', - title: 'Browser use', - icon: , - widthClassName: 'w-[460px]', - node: ( - - ) - }, - { - id: 'computer-use', - title: 'Computer Use', - icon: , - widthClassName: 'w-[520px]', - node: ( - - ) - }, - { - id: 'orchestration', - title: 'Agent orchestration', - icon: , - widthClassName: 'w-[520px]', - node: ( - - ) - } - ], - [advanceSlide, props.reducedMotion] - ) - - useEffect(() => { - if (props.reducedMotion) { - setActiveIndex(1) - } - }, [props.reducedMotion]) - - const activeSlide = slides[activeIndex] ?? slides[0] - - return ( -
-
-
Preview
-
- {slides.map((slide, index) => ( - - ))} -
-
-
-
{activeSlide.node}
-
-
- ) -} diff --git a/src/renderer/src/components/feature-wall/FeatureWallSetupChecklist.tsx b/src/renderer/src/components/feature-wall/FeatureWallSetupChecklist.tsx index e7615f28cd0..b09ac46ea80 100644 --- a/src/renderer/src/components/feature-wall/FeatureWallSetupChecklist.tsx +++ b/src/renderer/src/components/feature-wall/FeatureWallSetupChecklist.tsx @@ -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 ( - - - +
+ +
) } @@ -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(
-
+
{activeStep ? ( -
+
-
-
+
+
{activeStep.name}
-

- {activeStep.description} -

- +
+
+

+ {activeStep.description} +

+ {activeStep.id === 'split-terminal' ? ( +
+ +
+ ) : null} +
+ +
+
+ +
) : null}
diff --git a/src/renderer/src/components/feature-wall/FeatureWallSetupStepVisuals.tsx b/src/renderer/src/components/feature-wall/FeatureWallSetupStepVisuals.tsx index 6fa9f5b4904..3be340bebd7 100644 --- a/src/renderer/src/components/feature-wall/FeatureWallSetupStepVisuals.tsx +++ b/src/renderer/src/components/feature-wall/FeatureWallSetupStepVisuals.tsx @@ -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 ( - + + + + ) } -export function SetupWorkspacesVisual(props: { reducedMotion: boolean }): JSX.Element { - const { reducedMotion } = props - const rootRef = useRef(null) - const plusRef = useRef(null) - const createButtonRef = useRef(null) - const modalNameTimeoutsRef = useRef([]) - const codexPromptTimeoutsRef = useRef([]) - 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 ( -
-
- -
- } - state="working" - reducedMotion={reducedMotion} - /> - } - 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' - )} - /> -
-
+ + + + + + ) +} -
-
- -
- - > draft release notes - - - - Edit release-notes.md - -
+// Split a terminal: one terminal split into two panes, each running an agent. +export function SetupTwoAgentsVisual(): JSX.Element { + return ( +
+
+
+
- -
- -
- {codexStarting ? ( - - - Codex session ready - - ) : ( - - > - {codexPromptText} - {codexTyping ? ( - - ) : null} - - )} - - - {codexWorking - ? 'Read checkout.test.ts' - : codexSubmitted - ? 'Reading checkout.test.ts' - : 'Waiting'} - - {codexWorking ? ( - - - Edit src/checkout.ts - - ) : null} -
-
- - -
-
-
- - {plusClicked || createClicked ? : null} +
+ +
) } -function ProjectSidebarRow(props: { - plusRef: RefObject - plusActive: boolean - plusHovered: boolean - shortcut: string - worktreeCount: number -}): JSX.Element { +function SplitTerminalPane(props: { className?: string }): JSX.Element { return ( -
- - +
+ + {'>'} + - orca - - {props.worktreeCount} worktree{props.worktreeCount === 1 ? '' : 's'} - - - - -
- New worktree - - {props.shortcut} - -
+
) } -function NewWorkspaceModal(props: { - visible: boolean - createHovered: boolean - createClicked: boolean - createButtonRef: RefObject - 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 ( +
+ + +
+ ) +} + +function WorktreeGlyphPanel(props: { className?: string }): JSX.Element { return (
-
-
Create Worktree
-
- - - } /> -
-
- Create worktree -
-
-
- ) -} - -function clearTypedTimeouts(ref: MutableRefObject): void { - for (const timeoutId of ref.current) { - window.clearTimeout(timeoutId) - } - ref.current = [] -} - -function scheduleTypedText( - ref: MutableRefObject, - 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, - plusRef: RefObject, - createButtonRef: RefObject, - 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 ( -
-
{props.label}
-
- {props.icon ? ( - {props.icon} - ) : null} - {props.value} - {props.typing ? ( - - ) : null} -
-
- ) -} - -function useLoopedPhase( - 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 ( -
- - - - - {props.title} - -
- ) -} - -function WorkspaceListCard(props: { - title: string - active: boolean - prompt: string - icon: ReactNode - state: 'idle' | 'starting' | 'working' - reducedMotion: boolean - className?: string -}): JSX.Element { - return ( -
-
- - {props.title} -
-
- {props.state === 'working' || props.state === 'starting' ? ( - - ) : ( - - )} - - {props.icon} - - - {props.state === 'idle' ? 'ready' : props.prompt} - -
+ + + + +
) } -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 ( -
- {props.children} +
+ +
) } -function Prompt(props: { children: ReactNode }): JSX.Element { - return {props.children} +function RepoCard(props: { nameWidth: string; worktreeWidth: string }): JSX.Element { + return ( +
+ + + + + + + + +
+ ) } diff --git a/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.test.tsx b/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.test.tsx index 72fcca255aa..d11dfc98f26 100644 --- a/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.test.tsx +++ b/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.test.tsx @@ -37,15 +37,8 @@ vi.mock('@/runtime/web-runtime-session', () => ({ vi.mock('./FeatureWallSetupStepVisuals', () => ({ SetupTwoAgentsVisual: () =>
, - SetupWorkspacesVisual: () =>
-})) - -vi.mock('./AddReposAnimatedVisual', () => ({ - AddReposAnimatedVisual: () =>
-})) - -vi.mock('./SetupScriptAnimatedVisual', () => ({ - SetupScriptAnimatedVisual: () =>
+ SetupWorkspacesVisual: () =>
, + SetupMultipleReposVisual: () =>
})) function makeRepo(id: string, overrides: Partial = {}): Repo { diff --git a/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.tsx b/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.tsx index 36a792d2626..1ad25e15d78 100644 --- a/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.tsx +++ b/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.tsx @@ -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 ( -
- - - - -
+ ) } -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 ( -
- {!props.done && !paneTarget ? ( -
-
- -
-
- ) : null} - - - + + ) +} + +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 ( +
+

+ Split right with {splitRight} or down with{' '} + {splitDown}, or right-click a pane and choose a + split. Close the active pane with {closePane}. +

) } -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 ( -
- {!props.done ? ( - - ) : null} - - - -
+ ) } -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 (
-
-
- setSetupScript(event.target.value)} - placeholder="pnpm install" - aria-label="Setup script" - className="font-mono text-sm" - /> - -
+
+ setSetupScript(event.target.value)} + placeholder="pnpm install" + aria-label="Setup script" + className="font-mono text-sm" + />
+ {!canConfigure ? (

Add a git project first, then configure the setup script for that repository.

) : null} - - -
) } diff --git a/src/renderer/src/components/feature-wall/SetupScriptAnimatedVisual.tsx b/src/renderer/src/components/feature-wall/SetupScriptAnimatedVisual.tsx deleted file mode 100644 index 19da3614a29..00000000000 --- a/src/renderer/src/components/feature-wall/SetupScriptAnimatedVisual.tsx +++ /dev/null @@ -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(null) - const plusRef = useRef(null) - const createButtonRef = useRef(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 ( -
- {/* Sidebar */} -
-
-
- - orca -
- - - {plusClicked && } - -
- -
- } - state={!isWorkspaceActive ? 'working' : 'idle'} - reducedMotion={reducedMotion} - /> - - } - 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' - )} - /> -
-
- - {/* Terminal Pane / Screen */} -
- {/* Inactive Terminal (Visible when no active worktree) */} -
-
- - - Terminal - -
-
-
- $ - -
-
-
- - {/* Active Split Terminal Pane (Visible when worktree opens) */} -
- {/* Agent Pane (Left) */} -
-
- - - Agent: Codex - -
-
- - $ orca agent start - - {phase === 'worktree-opening' || - phase === 'pane-splitting' || - phase === 'setup-running' ? ( - - - Waiting for setup script... - - ) : phase === 'setup-complete' ? ( - - Initializing agent... - - ) : ( - <> - - ✓ Setup complete - - - - Running: checkout timeout - - - Reading checkout.test.ts - - - Modifying checkout.ts - - - )} -
-
- - {/* Setup Script Pane (Right - Splits and slides/fades in) */} -
-
- - - Setup Script - -
-
- - $ pnpm install - - {phase === 'pane-splitting' ? ( - - - - ) : phase === 'setup-running' ? ( - <> - - - Installing dependencies - - - pnpm-lock.yaml found - - - Resolving package tree... - - - ) : ( - <> - - - Dependencies ready - - - Done in 1.4s - - - )} -
-
-
- - {/* Create Worktree Modal Overlay */} - -
- - {/* Floating Animated Cursor */} -
-
- - {createClicked && } -
-
-
- ) -} - -// Why: measuring targets keeps the cursor aligned as the setup card resizes. -function useMeasuredCursor( - rootRef: RefObject, - plusRef: RefObject, - createButtonRef: RefObject, - 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 ( -
- {props.children} -
- ) -} - -function Prompt(props: { children: ReactNode }): JSX.Element { - return {props.children} -} diff --git a/src/renderer/src/components/feature-wall/SetupScriptNewWorkspaceModal.tsx b/src/renderer/src/components/feature-wall/SetupScriptNewWorkspaceModal.tsx deleted file mode 100644 index 935b4986f97..00000000000 --- a/src/renderer/src/components/feature-wall/SetupScriptNewWorkspaceModal.tsx +++ /dev/null @@ -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 -} - -export function SetupScriptNewWorkspaceModal({ - visible, - nameValue, - nameTyping, - createHovered, - createClicked, - createButtonRef -}: SetupScriptNewWorkspaceModalProps): JSX.Element { - return ( -
-
-
Create Worktree
-
- - - -
-
- Create worktree -
-
-
- ) -} - -function SetupScriptModalField(props: { - label: string - value: string - typing?: boolean -}): JSX.Element { - return ( -
- {props.label} -
- {props.value} - {props.typing ? ( - - ) : null} -
-
- ) -} diff --git a/src/renderer/src/components/feature-wall/SetupScriptWorkspaceListCard.tsx b/src/renderer/src/components/feature-wall/SetupScriptWorkspaceListCard.tsx deleted file mode 100644 index 450bc30bfd9..00000000000 --- a/src/renderer/src/components/feature-wall/SetupScriptWorkspaceListCard.tsx +++ /dev/null @@ -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 ( -
-
- - - {props.title} - -
- {props.prompt ? ( -
- {props.state === 'working' || props.state === 'starting' || props.state === 'setup' ? ( - - ) : ( - - )} - - {props.icon} - - - {props.state === 'setup' ? 'running setup' : props.prompt} - -
- ) : null} -
- ) -} diff --git a/src/renderer/src/components/feature-wall/SetupStepPreview.tsx b/src/renderer/src/components/feature-wall/SetupStepPreview.tsx deleted file mode 100644 index ef5ba385526..00000000000 --- a/src/renderer/src/components/feature-wall/SetupStepPreview.tsx +++ /dev/null @@ -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 ( -
-
- Preview -
-
{props.children}
-
- ) -} diff --git a/src/shared/feature-wall-setup-steps.ts b/src/shared/feature-wall-setup-steps.ts index 8c147fd4cb0..aa0d7365575 100644 --- a/src/shared/feature-wall-setup-steps.ts +++ b/src/shared/feature-wall-setup-steps.ts @@ -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