diff --git a/docs/reference/feature-education-retention-analytics.md b/docs/reference/feature-education-retention-analytics.md
index 1478ab46fb6..29204bb5016 100644
--- a/docs/reference/feature-education-retention-analytics.md
+++ b/docs/reference/feature-education-retention-analytics.md
@@ -32,7 +32,7 @@ Emitted once when a contextual tour first renders a measured target.
Payload:
-- `tour_id`: `workspace-board`, `browser`, `tasks`, or `automations`
+- `tour_id`: `workspace-board`, `browser`, `tasks`, `automations`, or `workspace-creation`
- `source`: bounded source enum from `src/shared/feature-education-telemetry.ts`
- `was_feature_previously_interacted`: boolean from local education state at the moment the tour is shown
@@ -123,6 +123,7 @@ Question: Do users later use the feature taught by the tour?
Build funnels from tour events to real product events:
- `tasks`: `contextual_tour_outcome` -> workspace creation from task-linked flows where existing telemetry exposes the source; if the current event is insufficient, add a targeted task-started-workspace event.
+- `workspace-creation`: `contextual_tour_outcome` -> `workspace_created`
- `browser`: `contextual_tour_outcome` -> browser grab/annotation events. If these do not exist, add targeted events for the concrete actions, not a broad feature-interaction event.
- `automations`: `contextual_tour_outcome` -> automation creation/run events. If these do not exist, add targeted events for create/run.
- `workspace-board`: `contextual_tour_outcome` -> board-specific actions such as card moved/status changed. If these do not exist, add targeted events for those actions.
diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.tsx
index d0403ba91df..caae7ab1bab 100644
--- a/src/renderer/src/components/NewWorkspaceComposerCard.tsx
+++ b/src/renderer/src/components/NewWorkspaceComposerCard.tsx
@@ -20,6 +20,7 @@ import { useAppStore } from '@/store'
import { cn } from '@/lib/utils'
import { WORKSPACE_FILE_PATH_MIME } from '@/lib/workspace-file-drag'
import { getScreenSubmitModifierLabel } from '@/lib/screen-submit-shortcut'
+import { useContextualTour } from '@/components/contextual-tours/use-contextual-tour'
import type {
GitHubWorkItem,
GitLabWorkItem,
@@ -248,6 +249,7 @@ export default function NewWorkspaceComposerCard({
}: NewWorkspaceComposerCardProps): React.JSX.Element {
const { isFileDragOver, dragHandlers } = useComposerFileDragOver()
const openModal = useAppStore((s) => s.openModal)
+ const activeModal = useAppStore((s) => s.activeModal)
const defaultTuiAgent = useAppStore((s) => s.settings?.defaultTuiAgent ?? null)
const updateSettings = useAppStore((s) => s.updateSettings)
const submitShortcutModifierLabel = getScreenSubmitModifierLabel()
@@ -288,6 +290,13 @@ export default function NewWorkspaceComposerCard({
const handleAddRepo = React.useCallback((): void => {
openModal('add-repo')
}, [openModal])
+ useContextualTour(
+ 'workspace-creation',
+ eligibleRepos.length > 0 && Boolean(repoId),
+ activeModal === 'new-workspace-composer'
+ ? 'workspace_creation_modal'
+ : 'workspace_creation_visible'
+ )
return (
-
+
Project
@@ -374,7 +383,7 @@ export default function NewWorkspaceComposerCard({
) : null}
-
+
{selectedRepoIsGit ? "Name or 'Create From'" : 'Workspace name'}{' '}
[Optional]
@@ -408,7 +417,7 @@ export default function NewWorkspaceComposerCard({
/>
-
+
Agent
diff --git a/src/renderer/src/components/contextual-tours/ContextualTourOverlay.test.tsx b/src/renderer/src/components/contextual-tours/ContextualTourOverlay.test.tsx
index 526dbbbed89..5539d1cca23 100644
--- a/src/renderer/src/components/contextual-tours/ContextualTourOverlay.test.tsx
+++ b/src/renderer/src/components/contextual-tours/ContextualTourOverlay.test.tsx
@@ -126,6 +126,20 @@ describe('ContextualTourOverlaySurface', () => {
expect(markup).toContain('Done')
})
+ it('renders configured step controls inside the tour panel', () => {
+ const markup = renderToStaticMarkup(
+ renderSurface({
+ control: { kind: 'auto-rename-branch-from-work' },
+ title: 'Name it now or let Orca do it',
+ body: 'Type a name if you know it.'
+ })
+ )
+
+ expect(markup).toContain('Auto-name from first message')
+ expect(markup).toContain('role="switch"')
+ expect(markup).toContain('Auto-name workspace from first agent message')
+ })
+
it('hides the Back button on the first visible step', () => {
const markup = renderToStaticMarkup(renderSurface({ isFirstStep: true }))
expect(markup).not.toContain('>Back<')
diff --git a/src/renderer/src/components/contextual-tours/ContextualTourOverlay.tsx b/src/renderer/src/components/contextual-tours/ContextualTourOverlay.tsx
index b174fc2942b..f4c8fec030c 100644
--- a/src/renderer/src/components/contextual-tours/ContextualTourOverlay.tsx
+++ b/src/renderer/src/components/contextual-tours/ContextualTourOverlay.tsx
@@ -182,6 +182,7 @@ export function ContextualTourOverlay(): JSX.Element | null {
progress,
title: activeStep.title,
body: getContextualTourStepCopy(activeStep),
+ control: activeStep.control,
isLastStep: progress.current === progress.total,
isFirstStep: progress.current === 1,
panelHost: getContextualTourPanelHost(target.element)
diff --git a/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx b/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx
index eeb6eace749..f81b0bcd57a 100644
--- a/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx
+++ b/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx
@@ -4,7 +4,10 @@ import { ArrowLeft, ArrowRight } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { useAppStore } from '@/store'
-import type { ContextualTourId } from '../../../../shared/contextual-tours'
+import type {
+ ContextualTourId,
+ ContextualTourStepControl
+} from '../../../../shared/contextual-tours'
import type { ContextualTourPanelPlacement } from './contextual-tour-gate'
const FOCUSABLE_SELECTOR =
@@ -17,6 +20,7 @@ export type ActiveTourRenderState = {
progress: { current: number; total: number }
title: string
body: string
+ control?: ContextualTourStepControl
isLastStep: boolean
isFirstStep: boolean
panelHost: HTMLElement | null
@@ -101,6 +105,7 @@ export function ContextualTourOverlaySurface({
{renderState.title}
{renderState.body}
+ {renderState.control ? : null}
+ default:
+ return null
+ }
+}
+
+function AutoRenameBranchFromWorkControl(): JSX.Element {
+ const enabled = useAppStore((s) => s.settings?.autoRenameBranchFromWork === true)
+ const updateSettings = useAppStore((s) => s.updateSettings)
+
+ return (
+
+
+
+
Auto-name from first message
+
+ Works when you leave the generated name unchanged.
+
+
+
{
+ void updateSettings({ autoRenameBranchFromWork: !enabled })
+ }}
+ className={cn(
+ 'relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors',
+ enabled ? 'bg-foreground' : 'bg-muted-foreground/30'
+ )}
+ >
+
+
+
+
+ )
+}
+
function ContextualTourProgressDots({
current,
total
diff --git a/src/renderer/src/components/contextual-tours/contextual-tour-gate.test.ts b/src/renderer/src/components/contextual-tours/contextual-tour-gate.test.ts
index 46a325a4955..222784fc23b 100644
--- a/src/renderer/src/components/contextual-tours/contextual-tour-gate.test.ts
+++ b/src/renderer/src/components/contextual-tours/contextual-tour-gate.test.ts
@@ -14,9 +14,12 @@ import {
import { getContextualTourPanelCssPosition } from './contextual-tour-panel-position'
describe('contextual tour gate', () => {
- it('blocks tours while another modal is active', () => {
+ it('allows only workspace creation over its workspace composer modal', () => {
+ const workspaceCreation = getContextualTour('workspace-creation')
const tasks = getContextualTour('tasks')
+ expect(isContextualTourAllowedForModal(workspaceCreation, 'new-workspace-composer')).toBe(true)
+ expect(isContextualTourAllowedForModal(workspaceCreation, 'add-repo')).toBe(false)
expect(isContextualTourAllowedForModal(tasks, 'none')).toBe(true)
expect(isContextualTourAllowedForModal(tasks, 'new-workspace-composer')).toBe(false)
expect(isContextualTourAllowedForModal(tasks, 'add-repo')).toBe(false)
diff --git a/src/renderer/src/components/contextual-tours/use-contextual-tour.ts b/src/renderer/src/components/contextual-tours/use-contextual-tour.ts
index d18282042ff..7603505bf54 100644
--- a/src/renderer/src/components/contextual-tours/use-contextual-tour.ts
+++ b/src/renderer/src/components/contextual-tours/use-contextual-tour.ts
@@ -7,7 +7,8 @@ const TOUR_SOURCES = {
'workspace-board': 'workspace_board_visible',
browser: 'browser_visible',
tasks: 'tasks_open',
- automations: 'automations_open'
+ automations: 'automations_open',
+ 'workspace-creation': 'workspace_creation_visible'
} satisfies Record
export function useContextualTour(
diff --git a/src/renderer/src/store/slices/ui.test.ts b/src/renderer/src/store/slices/ui.test.ts
index dded20b6237..f37443e4b5c 100644
--- a/src/renderer/src/store/slices/ui.test.ts
+++ b/src/renderer/src/store/slices/ui.test.ts
@@ -1130,14 +1130,20 @@ describe('createUISlice contextual tours', () => {
expect(store.getState().contextualTourShownThisSession).toBe(false)
})
- it('blocks contextual tours while creation modals are open', () => {
+ it('allows only workspace creation over its workspace composer modal', () => {
const store = createUIStore()
- stubContextualTourTargets(['[data-contextual-tour-target="tasks-source-filters"]'])
+ stubContextualTourTargets([
+ '[data-contextual-tour-target="tasks-source-filters"]',
+ '[data-contextual-tour-target="workspace-creation-project"]'
+ ])
store.getState().hydratePersistedUI(makeAutoTourEligibleUI())
store.getState().openModal('new-workspace-composer')
store.getState().requestContextualTour('tasks', 'tasks_open')
expect(store.getState().activeContextualTourId).toBeNull()
+
+ store.getState().requestContextualTour('workspace-creation', 'workspace_creation_modal')
+ expect(store.getState().activeContextualTourId).toBe('workspace-creation')
})
it('advances across visible steps and leaves completion to the overlay', () => {
diff --git a/src/shared/contextual-tours.test.ts b/src/shared/contextual-tours.test.ts
index 53f49d3457f..4d54d32c13b 100644
--- a/src/shared/contextual-tours.test.ts
+++ b/src/shared/contextual-tours.test.ts
@@ -8,7 +8,13 @@ import {
describe('contextual tour definitions', () => {
it('defines the required tours with three concise steps each', () => {
- const expectedIds: ContextualTourId[] = ['workspace-board', 'browser', 'tasks', 'automations']
+ const expectedIds: ContextualTourId[] = [
+ 'workspace-board',
+ 'browser',
+ 'tasks',
+ 'automations',
+ 'workspace-creation'
+ ]
expect(CONTEXTUAL_TOURS.map((tour) => tour.id)).toEqual(expectedIds)
for (const tour of CONTEXTUAL_TOURS) {
@@ -23,12 +29,13 @@ describe('contextual tour definitions', () => {
}
})
- it('keeps contextual tours out of modal-only flows', () => {
+ it('allows only workspace creation over its workspace composer modal', () => {
const modalTours = (CONTEXTUAL_TOURS as readonly ContextualTour[]).filter(
(tour) => tour.allowedActiveModals?.length
)
- expect(modalTours).toEqual([])
+ expect(modalTours.map((tour) => tour.id)).toEqual(['workspace-creation'])
+ expect(modalTours[0]?.allowedActiveModals).toEqual(['new-workspace-composer'])
})
it('normalizes persisted ids by removing unknowns and duplicates', () => {
@@ -41,6 +48,6 @@ describe('contextual tour definitions', () => {
null,
'workspace-creation'
])
- ).toEqual(['tasks', 'browser'])
+ ).toEqual(['tasks', 'browser', 'workspace-creation'])
})
})
diff --git a/src/shared/contextual-tours.ts b/src/shared/contextual-tours.ts
index 1907f8e3e04..568509cbc38 100644
--- a/src/shared/contextual-tours.ts
+++ b/src/shared/contextual-tours.ts
@@ -1,4 +1,13 @@
-export type ContextualTourId = 'workspace-board' | 'browser' | 'tasks' | 'automations'
+export type ContextualTourId =
+ | 'workspace-board'
+ | 'browser'
+ | 'tasks'
+ | 'automations'
+ | 'workspace-creation'
+
+export type ContextualTourStepControl = {
+ kind: 'auto-rename-branch-from-work'
+}
export type ContextualTourStep = {
title: string
@@ -6,6 +15,7 @@ export type ContextualTourStep = {
targetSelector: string
requiredForStart?: boolean
fallbackCopy?: string
+ control?: ContextualTourStepControl
}
export type ContextualTour = {
@@ -100,6 +110,29 @@ export const CONTEXTUAL_TOURS = [
targetSelector: '[data-contextual-tour-target="automations-runs"]'
}
]
+ },
+ {
+ id: 'workspace-creation',
+ allowedActiveModals: ['new-workspace-composer'],
+ steps: [
+ {
+ title: 'Pick the code to work in',
+ body: 'Choose the project for this task so the workspace opens in the right codebase.',
+ targetSelector: '[data-contextual-tour-target="workspace-creation-project"]',
+ requiredForStart: true
+ },
+ {
+ title: 'Name it now or let Orca do it',
+ body: 'Type a name if you know it. Or leave the field blank and let Orca rename the workspace after the first agent message.',
+ targetSelector: '[data-contextual-tour-target="workspace-creation-name"]',
+ control: { kind: 'auto-rename-branch-from-work' }
+ },
+ {
+ title: 'Choose who starts the work',
+ body: 'Pick the agent you want, then create the workspace. Orca opens a separate place for this task.',
+ targetSelector: '[data-contextual-tour-target="workspace-creation-agent"]'
+ }
+ ]
}
] as const satisfies readonly ContextualTour[]
diff --git a/src/shared/feature-education-telemetry.ts b/src/shared/feature-education-telemetry.ts
index d12713e70c6..b4fd7b8e7dd 100644
--- a/src/shared/feature-education-telemetry.ts
+++ b/src/shared/feature-education-telemetry.ts
@@ -4,7 +4,8 @@ export const FEATURE_EDUCATION_CONTEXTUAL_TOUR_IDS = [
'workspace-board',
'browser',
'tasks',
- 'automations'
+ 'automations',
+ 'workspace-creation'
] as const satisfies readonly ContextualTourId[]
export const FEATURE_EDUCATION_SOURCES = [
@@ -12,6 +13,8 @@ export const FEATURE_EDUCATION_SOURCES = [
'browser_visible',
'tasks_open',
'automations_open',
+ 'workspace_creation_visible',
+ 'workspace_creation_modal',
'unknown'
] as const
diff --git a/tests/e2e/helpers/e2e-completed-onboarding-profile.ts b/tests/e2e/helpers/e2e-completed-onboarding-profile.ts
index 7224a88155d..40260f6c67d 100644
--- a/tests/e2e/helpers/e2e-completed-onboarding-profile.ts
+++ b/tests/e2e/helpers/e2e-completed-onboarding-profile.ts
@@ -6,7 +6,8 @@ const SEEN_FIRST_RUN_CONTEXTUAL_TOUR_IDS = [
'workspace-board',
'browser',
'tasks',
- 'automations'
+ 'automations',
+ 'workspace-creation'
] as const
const SEEN_FIRST_RUN_FEATURE_INTERACTION_TIMESTAMP = Date.parse('2026-01-01T00:00:00.000Z')