diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css
index 97719fcb2..fc93e49c0 100644
--- a/src/renderer/src/assets/main.css
+++ b/src/renderer/src/assets/main.css
@@ -1808,12 +1808,18 @@ html.onboarding-tour-start-transition::view-transition-new(root) {
/* Frame 2 — Orchestration: child cards flash as bubbles land along the
dashed paths fanning down from the coordinator. */
+/* Why: bubbles use percent-based left/top so they always travel from the
+ root card's right edge (34%) to the child card's left edge (64%),
+ regardless of preview width. The previous offset-path used hardcoded
+ pixels and drifted off the dashed lines once the preview resized. */
.feature-tour-orch-bubble {
position: absolute;
- top: 0;
- left: 0;
+ left: 34%;
+ top: 50%;
width: 12px;
height: 12px;
+ margin-left: -6px;
+ margin-top: -6px;
border-radius: 3px;
background: var(--primary);
color: var(--primary-foreground);
@@ -1821,58 +1827,124 @@ html.onboarding-tour-start-transition::view-transition-new(root) {
align-items: center;
justify-content: center;
opacity: 0;
- offset-rotate: 0deg;
z-index: 4;
- animation: feature-tour-orch-travel var(--feature-tour-cycle) cubic-bezier(0.4, 0, 0.2, 1)
+}
+.feature-tour-orch-bubble.top {
+ animation: feature-tour-orch-travel-top var(--feature-tour-cycle) cubic-bezier(0.4, 0, 0.2, 1)
infinite;
-}
-.feature-tour-orch-bubble.left {
- offset-path: path('M 168 36 C 132 56, 102 66, 72 86');
- offset-anchor: center;
animation-delay: 4s;
}
-.feature-tour-orch-bubble.right {
- offset-path: path('M 168 36 C 204 56, 234 66, 264 86');
- offset-anchor: center;
- animation-delay: 4s;
+.feature-tour-orch-bubble.mid {
+ animation: feature-tour-orch-travel-mid var(--feature-tour-cycle) cubic-bezier(0.4, 0, 0.2, 1)
+ infinite;
+ animation-delay: calc(4s + 0.2s);
}
-.feature-tour-orch-bubble.left.b2 {
- animation-delay: calc(4s + 0.55s);
+.feature-tour-orch-bubble.bot {
+ animation: feature-tour-orch-travel-bot var(--feature-tour-cycle) cubic-bezier(0.4, 0, 0.2, 1)
+ infinite;
+ animation-delay: calc(4s + 0.4s);
}
-.feature-tour-orch-bubble.right.b2 {
- animation-delay: calc(4s + 0.75s);
-}
-@keyframes feature-tour-orch-travel {
+@keyframes feature-tour-orch-travel-top {
0% {
- offset-distance: 0%;
+ left: 34%;
+ top: 50%;
opacity: 0;
}
2% {
- offset-distance: 0%;
+ opacity: 1;
+ }
+ 3.5% {
+ left: 49%;
+ top: 50%;
opacity: 1;
}
6.5% {
- offset-distance: 100%;
+ left: 64%;
+ top: 18%;
opacity: 1;
}
7.5% {
- offset-distance: 100%;
+ left: 64%;
+ top: 18%;
opacity: 0;
}
25%,
100% {
- offset-distance: 100%;
+ left: 64%;
+ top: 18%;
+ opacity: 0;
+ }
+}
+@keyframes feature-tour-orch-travel-mid {
+ 0% {
+ left: 34%;
+ top: 50%;
+ opacity: 0;
+ }
+ 2% {
+ opacity: 1;
+ }
+ 6.5% {
+ left: 64%;
+ top: 50%;
+ opacity: 1;
+ }
+ 7.5% {
+ left: 64%;
+ top: 50%;
+ opacity: 0;
+ }
+ 25%,
+ 100% {
+ left: 64%;
+ top: 50%;
+ opacity: 0;
+ }
+}
+@keyframes feature-tour-orch-travel-bot {
+ 0% {
+ left: 34%;
+ top: 50%;
+ opacity: 0;
+ }
+ 2% {
+ opacity: 1;
+ }
+ 3.5% {
+ left: 49%;
+ top: 50%;
+ opacity: 1;
+ }
+ 6.5% {
+ left: 64%;
+ top: 82%;
+ opacity: 1;
+ }
+ 7.5% {
+ left: 64%;
+ top: 82%;
+ opacity: 0;
+ }
+ 25%,
+ 100% {
+ left: 64%;
+ top: 82%;
opacity: 0;
}
}
.feature-tour-orch-child {
animation: feature-tour-orch-child-flash var(--feature-tour-cycle) linear infinite;
+}
+.feature-tour-orch-child.top {
animation-delay: 4s;
}
-.feature-tour-orch-child.right {
+.feature-tour-orch-child.mid {
animation-delay: calc(4s + 0.2s);
}
+.feature-tour-orch-child.bot {
+ animation-delay: calc(4s + 0.4s);
+}
@keyframes feature-tour-orch-child-flash {
0%,
6% {
@@ -1897,14 +1969,18 @@ html.onboarding-tour-start-transition::view-transition-new(root) {
/* Frame 3 — Tasks: cursor lands on the second row, the "Open" pill morphs
to a dark "Start workspace →" pill, then a workspace card slides in. */
.feature-tour-tasks-cursor {
+ /* Why: anchored from the row's right edge so the cursor sits just past the
+ pill's right edge (12px row padding + ~6px past pill end) instead of
+ covering the label text. The arrow tip (top-left of the SVG) lands on the
+ dark pill's trailing edge with the body of the cursor extending right. */
position: absolute;
- top: 0;
- left: 0;
+ top: 50%;
+ right: 14px;
width: 14px;
height: 14px;
pointer-events: none;
z-index: 8;
- transform: translate(20px, 8px);
+ transform: translate(120px, -30%);
opacity: 0;
animation: feature-tour-tasks-cursor var(--feature-tour-cycle) cubic-bezier(0.45, 0.05, 0.2, 1)
infinite;
@@ -1917,22 +1993,30 @@ html.onboarding-tour-start-transition::view-transition-new(root) {
}
@keyframes feature-tour-tasks-cursor {
0% {
- transform: translate(20px, 8px);
+ transform: translate(160px, -10%);
opacity: 0;
}
2% {
opacity: 1;
}
5% {
- transform: translate(120px, 60px);
+ transform: translate(60px, -30%);
opacity: 1;
}
8% {
- transform: translate(238px, 60px);
+ transform: translate(0px, -30%);
+ opacity: 1;
+ }
+ 9% {
+ transform: translate(0px, -10%);
+ opacity: 1;
+ }
+ 10.5% {
+ transform: translate(0px, -30%);
opacity: 1;
}
23.5% {
- transform: translate(238px, 60px);
+ transform: translate(0px, -30%);
opacity: 1;
}
24.5% {
@@ -1940,7 +2024,7 @@ html.onboarding-tour-start-transition::view-transition-new(root) {
}
25%,
100% {
- transform: translate(238px, 60px);
+ transform: translate(0px, -30%);
opacity: 0;
}
}
@@ -1977,25 +2061,29 @@ html.onboarding-tour-start-transition::view-transition-new(root) {
@keyframes feature-tour-tasks-pill-morph {
0%,
6% {
- width: 36px;
+ width: 28px;
background: rgb(34 197 94 / 0.14);
border-color: rgb(34 197 94 / 0.3);
+ box-shadow: none;
}
8% {
- width: 110px;
+ width: 78px;
background: var(--primary);
border-color: var(--primary);
+ box-shadow: 0 1px 2px rgb(0 0 0 / 0.18);
}
23.5% {
- width: 110px;
+ width: 78px;
background: var(--primary);
border-color: var(--primary);
+ box-shadow: 0 1px 2px rgb(0 0 0 / 0.18);
}
25%,
100% {
- width: 36px;
+ width: 28px;
background: rgb(34 197 94 / 0.14);
border-color: rgb(34 197 94 / 0.3);
+ box-shadow: none;
}
}
@@ -2027,11 +2115,15 @@ html.onboarding-tour-start-transition::view-transition-new(root) {
/* Click ring fires when the cursor "clicks" the morphed pill on the tasks
row. Positioned to overlay the right-side pill of the second task. */
.feature-tour-tasks-click-ring {
+ /* Why: ring is centered on the cursor tip (right=28 from row's right edge)
+ so the click feedback lands where the cursor is, on the arrow icon — not
+ centered on the pill, which would visually disconnect from the cursor. */
position: absolute;
+ top: 50%;
right: 14px;
- top: 50px;
- width: 22px;
- height: 22px;
+ width: 28px;
+ height: 28px;
+ margin-top: -14px;
border-radius: 9999px;
border: 1.5px solid color-mix(in srgb, var(--foreground) 55%, transparent);
opacity: 0;
diff --git a/src/renderer/src/components/feature-wall/FeatureTourPreview.tsx b/src/renderer/src/components/feature-wall/FeatureTourPreview.tsx
index a0406b443..6297877cc 100644
--- a/src/renderer/src/components/feature-wall/FeatureTourPreview.tsx
+++ b/src/renderer/src/components/feature-wall/FeatureTourPreview.tsx
@@ -1,24 +1,13 @@
import type { JSX } from 'react'
import { cn } from '@/lib/utils'
import { ClaudeIcon, OpenCodeGoIcon } from '../status-bar/icons'
-
-function CodexInlineIcon(): JSX.Element {
- return (
-
-
-
- )
-}
+import {
+ CodexInlineIcon,
+ CursorIcon,
+ MailGlyph,
+ WorkingSpinner
+} from './feature-tour-preview-glyphs'
+import { FeatureTourWorkspaceCard } from './FeatureTourWorkspaceCard'
type FrameId = 1 | 2 | 3 | 4
@@ -54,214 +43,277 @@ export const FEATURE_TOUR_PREVIEW_COPY: readonly FeatureTourPreviewFrameCopy[] =
}
]
-function WorkingSpinner({ size = 'sm' }: { size?: 'sm' | 'xs' }): JSX.Element {
- // Why: matches AgentStateDot's working indicator so the preview teaches
- // the same state language users see in the real app.
- const ring = size === 'xs' ? 'size-1.5 border' : 'size-2 border-2'
- return (
-
- )
-}
-
-function CursorIcon(): JSX.Element {
- return (
-
-
-
- )
-}
-
-function MailGlyph(): JSX.Element {
- return (
-
-
-
-
- )
-}
-
function WorkspaceFrame(): JSX.Element {
return (
-
-
+
+
Isolated workspaces
-
-
-
-
- fix login race condition
-
-
-
-
-
-
-
-
- speed up CI pipeline
-
-
-
+ {/* Why: 3 cards in a row tells the "ship several at once" story by
+ composition; the wide preview aspect (~4.9:1) makes a vertical stack
+ read as wasted space. The grid auto-sizes (no flex-1) so the cards
+ don't stretch to the container's bottom edge — keeps headroom under
+ the dot indicators. */}
+
+
+
+
)
}
+type OrchChildAgent = 'claude' | 'codex' | 'opencode-go'
+
+const ORCH_CHILDREN: readonly {
+ key: 'top' | 'mid' | 'bot'
+ position: string
+ label: string
+ agent: OrchChildAgent
+}[] = [
+ // Why: card vertical centers anchor to 18% / 50% / 82% — the same Y
+ // endpoints the dashed SVG paths terminate at — so the connectors land on
+ // each card's center regardless of card height.
+ { key: 'top', position: 'top-[18%] -translate-y-1/2', label: 'PR 1/3', agent: 'claude' },
+ { key: 'mid', position: 'top-1/2 -translate-y-1/2', label: 'PR 2/3', agent: 'codex' },
+ { key: 'bot', position: 'top-[82%] -translate-y-1/2', label: 'PR 3/3', agent: 'opencode-go' }
+]
+
function OrchestrationFrame(): JSX.Element {
+ // Why: a horizontal fan (root → 3 children L→R) reads naturally as
+ // "fans out and ships parallel PRs" at the wide aspect; the previous
+ // top-down tree wasted the horizontal space. SVG paths are sized to a
+ // 600×130 viewBox and stretched non-uniformly, so the dashed lines flex
+ // with the container while the absolutely-positioned cards stay aligned
+ // to viewport-relative anchors (root left, children right column).
return (
-
-
+
+
Agent orchestration
+ {/* Why: viewBox is percent-units (100×100, preserveAspectRatio="none")
+ so endpoints anchor to the same percentage anchors as the cards
+ and the bubbles — root right edge at 34%, child left edge at 64%,
+ child Y centers at 18%/50%/82%. Explicit width/height attrs are
+ required because an SVG with a 1:1 viewBox and only inset-0
+ otherwise picks its intrinsic 1:1 aspect for height. */}
+ {/* Why: vectorEffect is NOT inheritable in SVG, so the
+ non-scaling-stroke attribute must live on each path. Hoisting
+ it onto let preserveAspectRatio="none" stretch the dashes
+ into trapezoids on the diagonal connectors. */}
+
-
-
redesign auth flow
-
-
+ {/* Why: parent matches WorkspaceCard composition — spinner + title row,
+ then a Claude agent row underneath — so the user reads it as "a
+ workspace running Claude as the orchestrator," consistent with how
+ workspaces look elsewhere in the app. */}
+
+
+
+
+ redesign auth flow
+
+
+
+
+
+
+ orchestrating 3 agents
+
+
-
- 2 children
-
+ {/* Why: children mirror the parent's WorkspaceCard composition so the
+ fan reads as "coordinator workspace dispatches to 3 child
+ workspaces, each running its own agent." */}
+ {ORCH_CHILDREN.map(({ key, position, label, agent }) => (
+
+
+
+
+ {label}
+
+
+
+
+ {agent === 'claude' ? (
+
+ ) : agent === 'codex' ? (
+
+ ) : (
+
+ )}
+
+
+
+ ))}
-
- PR 1/2
-
-
-
- PR 2/2
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {ORCH_CHILDREN.map(({ key }) => (
+
+
+
+ ))}
)
}
function TasksFrame(): JSX.Element {
+ // Why: a left→right pipeline reads as "pick from backlog → workspace
+ // appears" in one glance. The wide aspect lets the backlog and the
+ // resulting workspace card sit side-by-side instead of stacked, which
+ // makes the cause/effect visible in the composition itself.
return (
-
-
- GitHub & Linear tasks
+
+
+ GitHub & Linear tasks
-
-
- #1799
-
-
- Bulk archive in source control
-
-
+
+
+
+
+ GH #1799
+
+ {/* Why: surrounding rows show only the issue number + a skeleton
+ so the user's eye is drawn to the row that has real text — the
+ one the cursor clicks on. */}
+
+
+
+
+ GH #1842
+
+
+ Worktree picker truncates
+
+
+
+ Start
+
+
+
+
+
+
+ {/* Why: cursor + ring live inside the row so they anchor to the
+ pill's right-edge ml-auto, instead of using fixed pixel offsets
+ that drift when the preview is resized. */}
+
+
+
+
+
+
+
+ LIN-329
+
+
+
+
+
+
+
+
+
+ fix/worktree-picker-truncates
+
+
+
+
+
+
+
+
+
+
+
Linked to GH #1842
+
-
-
- #1842
-
-
- Worktree picker truncates
-
-
-
- Start workspace →
-
-
-
-
-
-
- fix/worktree-picker-truncates
-
-
-
-
-
-
-
-
-
)
}
function TerminalFrame(): JSX.Element {
return (
-
-
+
+
Splittable terminal
-
+
-
+
orca · zsh
-
+
$
@@ -302,7 +354,7 @@ function TerminalFrame(): JSX.Element {
className="feature-tour-terminal-output flex min-w-0 items-center gap-1.5"
data-line="1"
>
-
+
session started
+
+ {status === 'working' ? (
+
+ ) : (
+
+ )}
+
+ {title}
+
+
+
+ {agents.map((agent, idx) => (
+
+ {agent.state === 'working' ? (
+
+ ) : (
+
+ )}
+ {agent.kind === 'claude' ? (
+
+ ) : agent.kind === 'codex' ? (
+
+ ) : (
+
+ )}
+
+
+ ))}
+
+
+ )
+}
diff --git a/src/renderer/src/components/feature-wall/feature-tour-preview-glyphs.tsx b/src/renderer/src/components/feature-wall/feature-tour-preview-glyphs.tsx
new file mode 100644
index 000000000..9acf5a321
--- /dev/null
+++ b/src/renderer/src/components/feature-wall/feature-tour-preview-glyphs.tsx
@@ -0,0 +1,57 @@
+import type { JSX } from 'react'
+import { cn } from '@/lib/utils'
+
+export function CodexInlineIcon(): JSX.Element {
+ return (
+
+
+
+ )
+}
+
+export function WorkingSpinner({ size = 'sm' }: { size?: 'sm' | 'xs' }): JSX.Element {
+ // Why: matches AgentStateDot's working indicator so the preview teaches
+ // the same state language users see in the real app.
+ const ring = size === 'xs' ? 'size-1.5 border' : 'size-2 border-2'
+ return (
+
+ )
+}
+
+export function CursorIcon(): JSX.Element {
+ return (
+
+
+
+ )
+}
+
+export function MailGlyph(): JSX.Element {
+ return (
+
+
+
+
+ )
+}
diff --git a/src/renderer/src/components/onboarding/OnboardingFlow.test.tsx b/src/renderer/src/components/onboarding/OnboardingFlow.test.tsx
index df74d4760..106524714 100644
--- a/src/renderer/src/components/onboarding/OnboardingFlow.test.tsx
+++ b/src/renderer/src/components/onboarding/OnboardingFlow.test.tsx
@@ -32,14 +32,14 @@ describe('OnboardingFlow', () => {
)
expect(html).toContain('Explore Orca')
- expect(html).toContain('Preview the core workflow.')
- expect(html).toContain('Run agents in isolated worktrees.')
- expect(html).toContain('Orchestrate agents to finish larger tasks.')
- expect(html).toContain('Start tasks from GitHub or Linear.')
- expect(html).toContain('Send webpage elements to agents from the Orca browser.')
- expect(html).not.toContain('Write and preview Markdown.')
- expect(html).toContain('items-start')
- expect(html).toContain('text-left')
+ expect(html).toContain('Take a 60-second tour of Orca's advanced features.')
+ expect(html).toContain('Take the tour')
+ // Why: the prior intro carried a redundant lead, a four-item checklist, and
+ // a help-menu footnote. The tour animation already conveys all of that, so
+ // the body is now just the preview + a single CTA — guard against drift.
+ expect(html).not.toContain('Preview the core workflow.')
+ expect(html).not.toContain('Run agents in isolated worktrees.')
+ expect(html).not.toContain('Available later under Help')
expect(html).toContain('Continue')
expect(html).toContain('Skip to project setup')
expect(html).not.toContain('Skip the tour')
diff --git a/src/renderer/src/components/onboarding/OnboardingFlow.tsx b/src/renderer/src/components/onboarding/OnboardingFlow.tsx
index b6a87412f..eefacda5f 100644
--- a/src/renderer/src/components/onboarding/OnboardingFlow.tsx
+++ b/src/renderer/src/components/onboarding/OnboardingFlow.tsx
@@ -40,7 +40,7 @@ const stepCopy = {
},
tour: {
title: 'Explore Orca',
- subtitle: ''
+ subtitle: "Take a 60-second tour of Orca's advanced features."
},
repo: {
title: 'Point Orca at some code',
diff --git a/src/renderer/src/components/onboarding/OnboardingTourStep.tsx b/src/renderer/src/components/onboarding/OnboardingTourStep.tsx
index af99cd7b2..b5a809390 100644
--- a/src/renderer/src/components/onboarding/OnboardingTourStep.tsx
+++ b/src/renderer/src/components/onboarding/OnboardingTourStep.tsx
@@ -1,19 +1,12 @@
import type { JSX } from 'react'
import { flushSync } from 'react-dom'
-import { ArrowRight, Check } from 'lucide-react'
+import { ArrowRight } from 'lucide-react'
import { Button } from '@/components/ui/button'
import type { FeatureWallTourDepthSummary } from '../../../../shared/feature-wall-tour-depth'
import { FeatureTourPreview } from '../feature-wall/FeatureTourPreview'
import { FeatureWallTourSurface } from '../feature-wall/FeatureWallTourSurface'
import { usePrefersReducedMotion } from '../feature-wall/feature-wall-modal-helpers'
-const TOUR_LEARNING_POINTS: readonly string[] = [
- 'Run agents in isolated worktrees.',
- 'Orchestrate agents to finish larger tasks.',
- 'Start tasks from GitHub or Linear.',
- 'Send webpage elements to agents from the Orca browser.'
-]
-
type OnboardingTourStepProps = {
tourStarted: boolean
busyLabel: string | null
@@ -100,41 +93,12 @@ export function OnboardingTourStep({
}
return (
-
-
-
-
Preview the core workflow.
-
- {TOUR_LEARNING_POINTS.map((point) => (
-
-
-
-
- {point}
-
- ))}
-
-
-
-
-
-
- Take the tour
-
-
-
~ 60 seconds
-
-
-
-
-
- Available later under Help > Explore Orca.
-
+
)
}