Remove workspace creation contextual tour

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
brennanb2025
2026-05-28 15:55:05 -07:00
co-authored by Orca
parent 888c36e31d
commit d06a623a31
14 changed files with 23 additions and 104 deletions
@@ -32,7 +32,7 @@ Emitted once when a contextual tour first renders a measured target.
Payload:
- `tour_id`: `workspace-board`, `browser`, `tasks`, `automations`, or `workspace-creation`
- `tour_id`: `workspace-board`, `browser`, `tasks`, or `automations`
- `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
@@ -122,7 +122,6 @@ Question: Do users later use the feature taught by the tour?
Build funnels from tour events to real product events:
- `workspace-creation`: `contextual_tour_outcome` -> `workspace_created`
- `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.
- `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.
@@ -307,7 +307,7 @@ export default function NewWorkspaceComposerCard({
)}
>
<div className="min-w-0 space-y-4 pt-3">
<div className="space-y-1" data-contextual-tour-target="workspace-creation-source">
<div className="space-y-1">
<div className="flex items-center justify-between gap-2">
<label className="text-xs font-medium text-muted-foreground">Project</label>
<Tooltip>
@@ -452,7 +452,6 @@ export default function NewWorkspaceComposerCard({
size="sm"
onClick={onToggleAdvanced}
className="-ml-2 text-xs"
data-contextual-tour-target="workspace-creation-setup"
>
Advanced
<ChevronDown
@@ -649,7 +648,6 @@ export default function NewWorkspaceComposerCard({
disabled={createDisabled}
size="sm"
className="text-xs"
data-contextual-tour-target="workspace-creation-action"
>
{creating ? <LoaderCircle className="size-4 animate-spin" /> : null}
{primaryActionLabel}
@@ -8,7 +8,6 @@ import { AGENT_CATALOG } from '@/lib/agent-catalog'
import type { LinkedWorkItemSummary } from '@/lib/new-workspace'
import { shouldAllowComposerEnterSubmitTarget } from '@/lib/new-workspace-enter-guard'
import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut'
import { useContextualTour } from '@/components/contextual-tours/use-contextual-tour'
import type {
TuiAgent,
WorkspaceCreateTelemetrySource,
@@ -99,7 +98,6 @@ function QuickTabBody({
onClose: () => void
active: boolean
}): React.JSX.Element {
useContextualTour('workspace-creation', active, 'workspace_creation_modal')
const settings = useAppStore((s) => s.settings)
const { cardProps, composerRef, nameInputRef, submitQuick, createDisabled } = useComposerState({
initialName: modalData.prefilledName ?? '',
@@ -14,15 +14,12 @@ import {
import { getContextualTourPanelCssPosition } from './contextual-tour-panel-position'
describe('contextual tour gate', () => {
it('allows workspace creation only over its own creation modals', () => {
const workspaceCreation = getContextualTour('workspace-creation')
it('blocks tours while another modal is active', () => {
const tasks = getContextualTour('tasks')
expect(isContextualTourAllowedForModal(workspaceCreation, 'new-workspace-composer')).toBe(true)
expect(isContextualTourAllowedForModal(workspaceCreation, 'add-repo')).toBe(true)
expect(isContextualTourAllowedForModal(workspaceCreation, 'quick-open')).toBe(false)
expect(isContextualTourAllowedForModal(tasks, 'none')).toBe(true)
expect(isContextualTourAllowedForModal(tasks, 'new-workspace-composer')).toBe(false)
expect(isContextualTourAllowedForModal(tasks, 'add-repo')).toBe(false)
})
it('does not start when the required first target is missing', () => {
@@ -7,8 +7,7 @@ const TOUR_SOURCES = {
'workspace-board': 'workspace_board_visible',
browser: 'browser_visible',
tasks: 'tasks_open',
automations: 'automations_open',
'workspace-creation': 'workspace_creation_visible'
automations: 'automations_open'
} satisfies Record<ContextualTourId, string>
export function useContextualTour(
@@ -320,10 +320,7 @@ export function CreateStep({
(= content size), so a long path inside the Location row would blow out
the dialog width even with flex + truncate on the row itself. min-w-0
here caps the grid track at the dialog's max-width. */}
<div
className="space-y-3.5 pt-1 min-w-0"
data-contextual-tour-target="workspace-creation-source"
>
<div className="space-y-3.5 pt-1 min-w-0">
{/* Kind toggle. Real radiogroup so screen readers announce it as a choice. */}
<div
ref={radioGroupRef}
@@ -429,13 +426,7 @@ export function CreateStep({
</p>
)}
<Button
onClick={onCreate}
disabled={!canSubmit}
size="lg"
className="w-full"
data-contextual-tour-target="workspace-creation-action"
>
<Button onClick={onCreate} disabled={!canSubmit} size="lg" className="w-full">
{isCreating ? 'Creating…' : 'Create project'}
</Button>
</div>
@@ -43,7 +43,6 @@ import {
effectiveExternalWorktreeVisibility,
isLegacyRepoForExternalWorktreeVisibility
} from '../../../../shared/worktree-ownership'
import { useContextualTour } from '@/components/contextual-tours/use-contextual-tour'
function defaultProjectGroupNameForPath(path: string): string {
return (
@@ -200,11 +199,6 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
const isOpen = activeModal === 'add-repo'
const projectId = addedRepo?.id ?? ''
const isRuntimeEnvironmentActive = Boolean(settings?.activeRuntimeEnvironmentId?.trim())
useContextualTour(
'workspace-creation',
isOpen && step === 'setup',
'workspace_creation_add_project'
)
const worktrees = useMemo(() => {
return worktreesByRepo[projectId] ?? []
@@ -782,7 +776,7 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
</DialogDescription>
</DialogHeader>
<div className="space-y-3 pt-2" data-contextual-tour-target="workspace-creation-source">
<div className="space-y-3 pt-2">
<div className="space-y-1">
<label
htmlFor="server-project-path"
@@ -806,7 +800,6 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
onClick={() => void handleAddServerPath('git')}
disabled={!serverPath.trim() || isAddingServerPath}
className="h-10"
data-contextual-tour-target="workspace-creation-action"
>
Add Git Project
</Button>
@@ -854,16 +847,12 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
</DialogDescription>
</DialogHeader>
<div
className="grid grid-cols-3 gap-3 pt-2"
data-contextual-tour-target="workspace-creation-source"
>
<div className="grid grid-cols-3 gap-3 pt-2">
<Button
onClick={handleBrowse}
disabled={isAdding}
variant="outline"
className="h-auto py-5 px-2 flex flex-col items-center gap-2 text-center border-border/80"
data-contextual-tour-target="workspace-creation-action"
>
<FolderOpen className="size-6 text-muted-foreground" />
<div>
@@ -173,7 +173,7 @@ export function ProjectAddedContent({
return (
<>
<DialogHeader data-contextual-tour-target="workspace-creation-source">
<DialogHeader>
<DialogTitle>Repo added</DialogTitle>
<DialogDescription>
{repoName
@@ -182,7 +182,7 @@ export function ProjectAddedContent({
</DialogDescription>
</DialogHeader>
<div className="space-y-3 pt-1" data-contextual-tour-target="workspace-creation-setup">
<div className="space-y-3 pt-1">
{choices.length > 1 ? (
<div
ref={radioGroupRef}
@@ -252,12 +252,7 @@ export function ProjectAddedContent({
<Settings className="size-3" />
Configure repo
</button>
<Button
type="button"
size="sm"
onClick={handlePrimaryAction}
data-contextual-tour-target="workspace-creation-action"
>
<Button type="button" size="sm" onClick={handlePrimaryAction}>
{selectedChoice === 'primary' ? (
<>
<GitBranch className="size-4" />
@@ -254,7 +254,7 @@ export function RemoteStep({
</DialogDescription>
</DialogHeader>
<div className="space-y-3 pt-1" data-contextual-tour-target="workspace-creation-source">
<div className="space-y-3 pt-1">
<div className="space-y-1">
<label className="text-[11px] font-medium text-muted-foreground">SSH target</label>
{sshTargets.length === 0 ? (
@@ -321,7 +321,6 @@ export function RemoteStep({
onClick={onAdd}
disabled={!selectedTargetId || !remotePath.trim() || isAddingRemote}
className="w-full"
data-contextual-tour-target="workspace-creation-action"
>
{isAddingRemote ? 'Adding...' : 'Add remote project'}
</Button>
@@ -373,7 +372,7 @@ export function CloneStep({
<DialogDescription>Enter the Git URL and choose where to clone it.</DialogDescription>
</DialogHeader>
<div className="space-y-3 pt-1" data-contextual-tour-target="workspace-creation-source">
<div className="space-y-3 pt-1">
<div className="space-y-1">
<label className="text-[11px] font-medium text-muted-foreground">Git URL</label>
<Input
@@ -417,7 +416,6 @@ export function CloneStep({
onClick={onClone}
disabled={!cloneUrl.trim() || !cloneDestination.trim() || isCloning}
className="w-full"
data-contextual-tour-target="workspace-creation-action"
>
{isCloning ? 'Cloning...' : 'Clone'}
</Button>
+2 -8
View File
@@ -1130,20 +1130,14 @@ describe('createUISlice contextual tours', () => {
expect(store.getState().contextualTourShownThisSession).toBe(false)
})
it('allows only workspace creation over its creation modals', () => {
it('blocks contextual tours while creation modals are open', () => {
const store = createUIStore()
stubContextualTourTargets([
'[data-contextual-tour-target="workspace-creation-source"]',
'[data-contextual-tour-target="tasks-source-filters"]'
])
stubContextualTourTargets(['[data-contextual-tour-target="tasks-source-filters"]'])
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', () => {
+4 -11
View File
@@ -8,13 +8,7 @@ import {
describe('contextual tour definitions', () => {
it('defines the required tours with three concise steps each', () => {
const expectedIds: ContextualTourId[] = [
'workspace-board',
'browser',
'tasks',
'automations',
'workspace-creation'
]
const expectedIds: ContextualTourId[] = ['workspace-board', 'browser', 'tasks', 'automations']
expect(CONTEXTUAL_TOURS.map((tour) => tour.id)).toEqual(expectedIds)
for (const tour of CONTEXTUAL_TOURS) {
@@ -29,13 +23,12 @@ describe('contextual tour definitions', () => {
}
})
it('keeps workspace creation as the only modal-allowed tour', () => {
it('keeps contextual tours out of modal-only flows', () => {
const modalTours = (CONTEXTUAL_TOURS as readonly ContextualTour[]).filter(
(tour) => tour.allowedActiveModals?.length
)
expect(modalTours.map((tour) => tour.id)).toEqual(['workspace-creation'])
expect(modalTours[0]?.allowedActiveModals).toEqual(['new-workspace-composer', 'add-repo'])
expect(modalTours).toEqual([])
})
it('normalizes persisted ids by removing unknowns and duplicates', () => {
@@ -48,6 +41,6 @@ describe('contextual tour definitions', () => {
null,
'workspace-creation'
])
).toEqual(['tasks', 'browser', 'workspace-creation'])
).toEqual(['tasks', 'browser'])
})
})
+1 -28
View File
@@ -1,9 +1,4 @@
export type ContextualTourId =
| 'workspace-board'
| 'browser'
| 'tasks'
| 'automations'
| 'workspace-creation'
export type ContextualTourId = 'workspace-board' | 'browser' | 'tasks' | 'automations'
export type ContextualTourStep = {
title: string
@@ -105,28 +100,6 @@ export const CONTEXTUAL_TOURS = [
targetSelector: '[data-contextual-tour-target="automations-runs"]'
}
]
},
{
id: 'workspace-creation',
allowedActiveModals: ['new-workspace-composer', 'add-repo'],
steps: [
{
title: 'Add the code Orca should work on',
body: 'Pick a local folder, clone a repo URL, or connect to a remote machine. Orca needs a project before it can create workspaces.',
targetSelector: '[data-contextual-tour-target="workspace-creation-source"]',
requiredForStart: true
},
{
title: 'Review advanced options',
body: 'Use Advanced or setup-step options for scripts, existing worktrees, notes, or workspace details.',
targetSelector: '[data-contextual-tour-target="workspace-creation-setup"]'
},
{
title: 'Create when the inputs are ready',
body: 'Start the workspace after the project, name, agent, and setup choices look right.',
targetSelector: '[data-contextual-tour-target="workspace-creation-action"]'
}
]
}
] as const satisfies readonly ContextualTour[]
+1 -5
View File
@@ -4,8 +4,7 @@ export const FEATURE_EDUCATION_CONTEXTUAL_TOUR_IDS = [
'workspace-board',
'browser',
'tasks',
'automations',
'workspace-creation'
'automations'
] as const satisfies readonly ContextualTourId[]
export const FEATURE_EDUCATION_SOURCES = [
@@ -13,9 +12,6 @@ export const FEATURE_EDUCATION_SOURCES = [
'browser_visible',
'tasks_open',
'automations_open',
'workspace_creation_visible',
'workspace_creation_modal',
'workspace_creation_add_project',
'unknown'
] as const
@@ -6,8 +6,7 @@ const SEEN_FIRST_RUN_CONTEXTUAL_TOUR_IDS = [
'workspace-board',
'browser',
'tasks',
'automations',
'workspace-creation'
'automations'
] as const
const SEEN_FIRST_RUN_FEATURE_INTERACTION_TIMESTAMP = Date.parse('2026-01-01T00:00:00.000Z')