mirror of
https://github.com/stablyai/orca.git
synced 2026-10-14 08:02:56 +00:00
Remove workspace creation contextual tour
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,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[]
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user