Add new-user parallel work activation tour

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
brennanb2025
2026-05-31 11:47:56 -07:00
co-authored by Orca
parent 096a63b4a0
commit 347179ea99
34 changed files with 1381 additions and 133 deletions
+46 -4
View File
@@ -20,12 +20,15 @@ describe('contextual tour definitions', () => {
expect(CONTEXTUAL_TOURS.map((tour) => tour.id)).toEqual(expectedIds)
for (const tour of CONTEXTUAL_TOURS) {
expect(tour.steps[0]?.requiredForStart).toBe(true)
if (tour.steps.length === 1) {
expect(tour.steps[0]?.advanceOnFeatureInteraction).toBeTruthy()
const stepCount = (tour.steps as readonly unknown[]).length
if (stepCount === 1) {
expect(
(tour.steps[0] as ContextualTour['steps'][number]).advanceOnFeatureInteraction
).toBeTruthy()
} else {
expect(tour.steps.length).toBeGreaterThanOrEqual(2)
expect(stepCount).toBeGreaterThanOrEqual(2)
}
expect(tour.steps.length).toBeLessThanOrEqual(3)
expect(stepCount).toBeLessThanOrEqual(tour.id === 'workspace-agent-sessions' ? 5 : 3)
for (const step of tour.steps) {
expect(step.title.length).toBeGreaterThan(0)
expect(step.body.length).toBeGreaterThan(0)
@@ -35,6 +38,45 @@ describe('contextual tour definitions', () => {
}
})
it('defines the workspace agent sessions value tour with split and navigation actions', () => {
const tour = CONTEXTUAL_TOURS.find((entry) => entry.id === 'workspace-agent-sessions') as
| ContextualTour
| undefined
expect(tour?.steps.map((step) => step.title)).toEqual([
'Work side by side',
'Split the terminal',
'Keep separate tasks isolated',
'Start from real work',
'Orchestrate capable agents'
])
expect(tour?.steps[0]).toMatchObject({
requiredForStart: true,
primaryAction: { kind: 'next', label: 'Show me' }
})
expect(tour?.steps[1]).toMatchObject({
primaryAction: { kind: 'split-terminal-pane', label: 'Split terminal' },
advanceOnFeatureInteraction: 'terminal-pane-split'
})
expect(tour?.steps[1]?.targetSelector).toContain('terminal-pane-split-target')
expect(tour?.steps[1]?.targetSelector).not.toContain('terminal-split-control')
expect(tour?.steps[1]?.secondaryAction).toBeUndefined()
expect(tour?.steps[2]).toMatchObject({
primaryAction: { kind: 'show-worktrees', label: 'Show worktrees' },
secondaryAction: { kind: 'next', label: 'Skip' }
})
expect(tour?.steps[2]?.targetSelector).toContain('workspace-list')
expect(tour?.steps[3]).toMatchObject({
primaryAction: { kind: 'open-tasks', label: 'Show tasks' },
secondaryAction: { kind: 'next', label: 'Skip' }
})
expect(tour?.steps[3]?.targetSelector).toContain('sidebar-tasks')
expect(tour?.steps[4]).toMatchObject({
primaryAction: { kind: 'open-getting-started', label: 'Open Getting started' },
secondaryAction: { kind: 'complete', label: 'Done' }
})
})
it('allows only workspace creation over its workspace composer modal', () => {
const modalTours = (CONTEXTUAL_TOURS as readonly ContextualTour[]).filter(
(tour) => tour.allowedActiveModals?.length
+53 -2
View File
@@ -12,6 +12,19 @@ export type ContextualTourStepControl = {
kind: 'auto-rename-branch-from-work'
}
export type ContextualTourStepActionKind =
| 'next'
| 'complete'
| 'split-terminal-pane'
| 'show-worktrees'
| 'open-tasks'
| 'open-getting-started'
export type ContextualTourStepAction = {
kind: ContextualTourStepActionKind
label: string
}
export type ContextualTourStepPlacement = 'top' | 'right' | 'bottom' | 'left'
export type ContextualTourStep = {
@@ -22,6 +35,8 @@ export type ContextualTourStep = {
fallbackCopy?: string
preferredPlacement?: ContextualTourStepPlacement
control?: ContextualTourStepControl
primaryAction?: ContextualTourStepAction
secondaryAction?: ContextualTourStepAction
advanceOnFeatureInteraction?: FeatureInteractionId
}
@@ -57,12 +72,48 @@ export const CONTEXTUAL_TOURS = [
id: 'workspace-agent-sessions',
steps: [
{
title: 'Split panes for agents',
body: 'Use the right-click menu or {terminal.splitRight} to split the terminal and keep multiple agent sessions visible at once.',
title: 'Work side by side',
body: 'Orca is built for running more than one thread of work at once.',
targetSelector: '[data-contextual-tour-target="workspace-agent-terminal-tip"]',
requiredForStart: true,
preferredPlacement: 'bottom',
primaryAction: { kind: 'next', label: 'Show me' }
},
{
title: 'Split the terminal',
body: 'Split right to make room for another agent, a server, or a follow-up.',
targetSelector:
'[data-contextual-tour-target="terminal-pane-split-target"], [data-contextual-tour-target="workspace-agent-terminal-tip"]',
preferredPlacement: 'bottom',
primaryAction: { kind: 'split-terminal-pane', label: 'Split terminal' },
advanceOnFeatureInteraction: 'terminal-pane-split'
},
{
title: 'Keep separate tasks isolated',
body: 'Use separate worktrees when you want different tasks or agents isolated from each other.',
targetSelector:
'[data-contextual-tour-target="workspace-list"], [data-contextual-tour-target="workspace-create-control"], [data-contextual-tour-target="sidebar-workspaces"]',
preferredPlacement: 'right',
primaryAction: { kind: 'show-worktrees', label: 'Show worktrees' },
secondaryAction: { kind: 'next', label: 'Skip' }
},
{
title: 'Start from real work',
body: 'Start from tasks, issues, reviews, or merge requests so agents begin with the right context.',
targetSelector:
'[data-contextual-tour-target="sidebar-tasks"], [data-contextual-tour-target="tasks-source-filters"], [data-contextual-tour-target="sidebar-navigation"]',
preferredPlacement: 'right',
primaryAction: { kind: 'open-tasks', label: 'Show tasks' },
secondaryAction: { kind: 'next', label: 'Skip' }
},
{
title: 'Orchestrate capable agents',
body: 'Use browser context, setup scripts, notifications, orchestration, and action skills so agents can coordinate beyond the terminal.',
targetSelector:
'[data-contextual-tour-target="browser-toolbar"], [data-contextual-tour-target="setup-guide-entry"], [data-contextual-tour-target="sidebar-navigation"]',
preferredPlacement: 'right',
primaryAction: { kind: 'open-getting-started', label: 'Open Getting started' },
secondaryAction: { kind: 'complete', label: 'Done' }
}
]
},
@@ -15,6 +15,9 @@ describe('feature education telemetry constants', () => {
expect(normalizeFeatureEducationSource('workspace_agent_sessions_visible')).toBe(
'workspace_agent_sessions_visible'
)
expect(normalizeFeatureEducationSource('setup_guide_parallel_work')).toBe(
'setup_guide_parallel_work'
)
expect(normalizeFeatureEducationSource('https://example.com/private')).toBe('unknown')
expect(normalizeFeatureEducationSource(null)).toBe('unknown')
})
@@ -17,6 +17,7 @@ export const FEATURE_EDUCATION_SOURCES = [
'automations_open',
'workspace_creation_visible',
'workspace_creation_modal',
'setup_guide_parallel_work',
'unknown'
] as const
+56 -18
View File
@@ -15,19 +15,14 @@ export type FeatureWallSetupStep = {
readonly description: string
}
export const FEATURE_WALL_SETUP_PARALLEL_WORK_STEP_IDS = [
'two-agents',
'three-workspaces'
] as const satisfies readonly FeatureWallSetupStepId[]
export type FeatureWallSetupSectionId = 'parallel-work' | 'setup'
export const FEATURE_WALL_SETUP_STEPS: readonly FeatureWallSetupStep[] = [
{
id: 'default-agent',
name: 'Pick a default agent',
subtitle: 'Pick a default agent',
description: 'Start new work with the agent you trust most, without choosing every time.'
},
{
id: 'notifications',
name: 'Configure notifications',
subtitle: 'Configure notifications',
description: 'Know when agents need attention, finish work, or get blocked.'
},
{
id: 'two-agents',
name: 'Start 2 agents in one worktree',
@@ -40,18 +35,24 @@ export const FEATURE_WALL_SETUP_STEPS: readonly FeatureWallSetupStep[] = [
subtitle: 'Create 2 worktrees',
description: 'Keep separate tasks in separate worktrees so agents can work independently.'
},
{
id: 'notifications',
name: 'Configure notifications',
subtitle: 'Configure notifications',
description: 'Know when agents need attention, finish work, or get blocked.'
},
{
id: 'default-agent',
name: 'Pick a default agent',
subtitle: 'Pick a default agent',
description: 'Start new work with the agent you trust most, without choosing every time.'
},
{
id: 'task-sources',
name: 'Enable task sources',
subtitle: 'Enable task sources',
description: 'Start work directly from your tasks and keep PR status in view.'
},
{
id: 'agent-capabilities',
name: 'Install CLI & Skills',
subtitle: 'Install CLI & Skills',
description: 'Enable the tools that let agents work more independently.'
},
{
id: 'setup-script',
name: 'Configure a setup script',
@@ -64,6 +65,12 @@ export const FEATURE_WALL_SETUP_STEPS: readonly FeatureWallSetupStep[] = [
name: 'Add 2 projects',
subtitle: 'Add 2 projects',
description: 'Bring your key repos into Orca and run agent work across them in parallel.'
},
{
id: 'agent-capabilities',
name: 'Install CLI & Skills',
subtitle: 'Install CLI & Skills',
description: 'Unlock browser, orchestration, and computer-use actions for agents.'
}
] as const
@@ -72,3 +79,34 @@ export const FEATURE_WALL_SETUP_STEP_IDS = FEATURE_WALL_SETUP_STEPS.map((step) =
export function getFeatureWallSetupSteps(): readonly FeatureWallSetupStep[] {
return FEATURE_WALL_SETUP_STEPS
}
export function getFeatureWallSetupSectionId(
stepId: FeatureWallSetupStepId
): FeatureWallSetupSectionId {
return FEATURE_WALL_SETUP_PARALLEL_WORK_STEP_IDS.includes(
stepId as (typeof FEATURE_WALL_SETUP_PARALLEL_WORK_STEP_IDS)[number]
)
? 'parallel-work'
: 'setup'
}
export function getFeatureWallSetupStepsForSection(
sectionId: FeatureWallSetupSectionId
): readonly FeatureWallSetupStep[] {
return FEATURE_WALL_SETUP_STEPS.filter(
(step) => getFeatureWallSetupSectionId(step.id) === sectionId
)
}
export function getFirstIncompleteFeatureWallSetupStepId(
stepDone: Partial<Record<FeatureWallSetupStepId, boolean>>
): FeatureWallSetupStepId {
const parallelStep = getFeatureWallSetupStepsForSection('parallel-work').find(
(step) => !stepDone[step.id]
)
if (parallelStep) {
return parallelStep.id
}
const setupStep = getFeatureWallSetupStepsForSection('setup').find((step) => !stepDone[step.id])
return setupStep?.id ?? FEATURE_WALL_SETUP_STEPS[0].id
}
@@ -24,6 +24,24 @@ describe('feature education telemetry event schemas', () => {
expect(parsed.success).toBe(true)
})
it('accepts setup-guide contextual tour sources for shown and outcome telemetry', () => {
const shown = eventSchemas.contextual_tour_shown.safeParse({
tour_id: 'workspace-agent-sessions',
source: 'setup_guide_parallel_work',
was_feature_previously_interacted: false
})
const outcome = eventSchemas.contextual_tour_outcome.safeParse({
tour_id: 'workspace-agent-sessions',
source: 'setup_guide_parallel_work',
outcome: 'completed',
steps_seen: 2,
total_steps: 5
})
expect(shown.success).toBe(true)
expect(outcome.success).toBe(true)
})
it('rejects contextual tour outcome payloads with impossible progress', () => {
const parsed = eventSchemas.contextual_tour_outcome.safeParse({
tour_id: 'tasks',