mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 16:02:37 +00:00
feat: onboarding flow for new users (#1596)
* wip * WIP: Changes before auto-review fixes Co-authored-by: Orca <help@stably.ai> * WIP: Changes before auto-review fixes Co-authored-by: Orca <help@stably.ai> * WIP: Changes before auto-review fixes Co-authored-by: Orca <help@stably.ai> * fix: address auto-review findings (iteration 1) Co-authored-by: Orca <help@stably.ai> * fix: address auto-review findings (iteration 2) Co-authored-by: Orca <help@stably.ai> * fix: archive review context and improve agent detection on wizard mount Co-authored-by: Orca <help@stably.ai> * fix: address CI lint failures and split use-onboarding-flow.ts Co-authored-by: Orca <help@stably.ai> * fix: mock ./onboarding in register-core-handlers test Co-authored-by: Orca <help@stably.ai> * fix: also toggle light class on documentElement so onboarding e2e theme wait resolves The onboarding e2e calls waitForFunction(() => classList.contains('dark') || classList.contains('light')) before snapshotting the starting theme. applyDocumentTheme only toggled 'dark', so on a host that resolves system to light the wait timed out (CI Linux headless). Toggle 'light' as the inverse class so consumers can observe the resolved theme symmetrically; Tailwind keys only on 'dark' so styling is unchanged. Co-authored-by: Orca <help@stably.ai> * fix: add braces to Landing menu close-on-outside-click handler oxlint config requires braces for all if statements. Co-authored-by: Orca <help@stably.ai> * chore: trigger CI Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
+30
-1
@@ -1,6 +1,8 @@
|
||||
import type {
|
||||
GlobalSettings,
|
||||
NotificationSettings,
|
||||
OnboardingChecklistState,
|
||||
OnboardingState,
|
||||
PersistedState,
|
||||
PersistedUIState,
|
||||
RepoHookSettings,
|
||||
@@ -13,6 +15,10 @@ import { DEFAULT_TERMINAL_FONT_WEIGHT } from './terminal-fonts'
|
||||
export const SCHEMA_VERSION = 1
|
||||
export const DEFAULT_APP_FONT_FAMILY = 'Geist'
|
||||
|
||||
// Why: the onboarding wizard's last step index. Centralized so backfill,
|
||||
// clamps, and UI step references all agree on the same upper bound.
|
||||
export const ONBOARDING_FINAL_STEP = 4
|
||||
|
||||
export const ORCA_BROWSER_PARTITION = 'persist:orca-browser'
|
||||
// Why: blank browser tabs must start from an inert guest URL that does not
|
||||
// navigate the privileged main window to about:blank. Renderer and main both
|
||||
@@ -111,6 +117,28 @@ export function getDefaultNotificationSettings(): NotificationSettings {
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultOnboardingState(): OnboardingState {
|
||||
return {
|
||||
closedAt: null,
|
||||
outcome: null,
|
||||
lastCompletedStep: -1,
|
||||
checklist: {
|
||||
addedRepo: false,
|
||||
choseAgent: false,
|
||||
ranFirstAgent: false,
|
||||
ranSecondAgentOnSameTask: false,
|
||||
triedCmdJ: false,
|
||||
shapedSidebar: false,
|
||||
reviewedDiff: false,
|
||||
openedPr: false,
|
||||
addedFolder: false,
|
||||
openedFile: false,
|
||||
ranAgentOnFile: false,
|
||||
dismissed: false
|
||||
} satisfies OnboardingChecklistState
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultSettings(homedir: string): GlobalSettings {
|
||||
return {
|
||||
workspaceDir: `${homedir}/orca/workspaces`,
|
||||
@@ -240,7 +268,8 @@ export function getDefaultPersistedState(homedir: string): PersistedState {
|
||||
ui: getDefaultUIState(),
|
||||
githubCache: { pr: {}, issue: {} },
|
||||
workspaceSession: getDefaultWorkspaceSession(),
|
||||
sshTargets: []
|
||||
sshTargets: [],
|
||||
onboarding: getDefaultOnboardingState()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
|
||||
import { z } from 'zod'
|
||||
|
||||
import type { GlobalSettings } from './types'
|
||||
import { ONBOARDING_FINAL_STEP } from './constants'
|
||||
import type { GlobalSettings, OnboardingChecklistState } from './types'
|
||||
|
||||
// ── Shared property enums ───────────────────────────────────────────────
|
||||
|
||||
@@ -108,6 +109,7 @@ export const workspaceSourceSchema = z.enum([
|
||||
'sidebar',
|
||||
'shortcut',
|
||||
'drag_drop',
|
||||
'onboarding',
|
||||
'unknown'
|
||||
])
|
||||
export type WorkspaceSource = z.infer<typeof workspaceSourceSchema>
|
||||
@@ -120,6 +122,7 @@ export const launchSourceSchema = z.enum([
|
||||
'new_workspace_composer',
|
||||
'workspace_jump_palette',
|
||||
'shortcut',
|
||||
'onboarding',
|
||||
'diff_notes_send',
|
||||
'unknown'
|
||||
])
|
||||
@@ -246,6 +249,86 @@ const workspaceCreateFailedSchema = z
|
||||
})
|
||||
.strict()
|
||||
|
||||
// ── Onboarding ──────────────────────────────────────────────────────────
|
||||
//
|
||||
// Closed enums only — no raw paths, repo names, clone URLs, or error
|
||||
// strings. The funnel exists to measure activation, not to debug specific
|
||||
// user repos.
|
||||
// Why: bound is derived from ONBOARDING_FINAL_STEP so adding a wizard step
|
||||
// only requires bumping the constant. Zod can't build a literal-union from a
|
||||
// numeric constant without runtime gymnastics, so we use a clamped int range.
|
||||
const onboardingStepSchema = z.number().int().min(1).max(ONBOARDING_FINAL_STEP)
|
||||
const onboardingPathSchema = z.enum(['open_folder', 'clone_url'])
|
||||
const onboardingFailureReasonSchema = z.enum([
|
||||
'invalid_path',
|
||||
'clone_failed',
|
||||
'cancelled',
|
||||
'unknown'
|
||||
])
|
||||
const onboardingValueKindSchema = z.enum(['agent', 'theme', 'notifications', 'repo'])
|
||||
// `dismissed` from `OnboardingChecklistState` is intentionally excluded —
|
||||
// it is a UI panel-visibility flag, not an activation event, so it never
|
||||
// fires `activation_checklist_item_completed`. Keep this list in sync with
|
||||
// the activation keys of `OnboardingChecklistState` in shared/types.ts.
|
||||
const onboardingChecklistItemSchema = z.enum([
|
||||
'addedRepo',
|
||||
'addedFolder',
|
||||
'choseAgent',
|
||||
'ranFirstAgent',
|
||||
'ranSecondAgentOnSameTask',
|
||||
'triedCmdJ',
|
||||
'shapedSidebar',
|
||||
'reviewedDiff',
|
||||
'openedPr',
|
||||
'openedFile',
|
||||
'ranAgentOnFile'
|
||||
])
|
||||
|
||||
// Why: compile-time guard that the enum above stays in lockstep with the
|
||||
// activation keys of OnboardingChecklistState (everything except the UI-only
|
||||
// `dismissed` flag). Adding/removing a checklist key without updating this
|
||||
// schema breaks the build here rather than silently dropping telemetry.
|
||||
type _OnboardingChecklistItemSync =
|
||||
z.infer<typeof onboardingChecklistItemSchema> extends Exclude<
|
||||
keyof OnboardingChecklistState,
|
||||
'dismissed'
|
||||
>
|
||||
? Exclude<keyof OnboardingChecklistState, 'dismissed'> extends z.infer<
|
||||
typeof onboardingChecklistItemSchema
|
||||
>
|
||||
? true
|
||||
: never
|
||||
: never
|
||||
const _onboardingChecklistItemSyncCheck: _OnboardingChecklistItemSync = true
|
||||
void _onboardingChecklistItemSyncCheck
|
||||
|
||||
const onboardingStartedSchema = z
|
||||
.object({ resumed_from_step: onboardingStepSchema.optional() })
|
||||
.strict()
|
||||
const onboardingStepViewedSchema = z.object({ step: onboardingStepSchema }).strict()
|
||||
const onboardingStepCompletedSchema = z
|
||||
.object({ step: onboardingStepSchema, value_kind: onboardingValueKindSchema })
|
||||
.strict()
|
||||
const onboardingStepSkippedSchema = z.object({ step: onboardingStepSchema }).strict()
|
||||
const onboardingStep4PathClickedSchema = z.object({ path: onboardingPathSchema }).strict()
|
||||
const onboardingStep4PathFailedSchema = z
|
||||
.object({ path: onboardingPathSchema, reason: onboardingFailureReasonSchema })
|
||||
.strict()
|
||||
const onboardingCompletedSchema = z
|
||||
.object({
|
||||
path: onboardingPathSchema,
|
||||
is_git_repo: z.boolean(),
|
||||
total_duration_ms: z.number().int().nonnegative()
|
||||
})
|
||||
.strict()
|
||||
const onboardingDismissedSchema = z.object({ last_step: onboardingStepSchema }).strict()
|
||||
const activationChecklistItemCompletedSchema = z
|
||||
.object({
|
||||
item: onboardingChecklistItemSchema,
|
||||
time_since_completed_ms: z.number().int().nonnegative()
|
||||
})
|
||||
.strict()
|
||||
|
||||
// ── Event registry: the one record the validator consumes ───────────────
|
||||
//
|
||||
// The validator does `eventSchemas[name].safeParse(props)`. `EventMap` is
|
||||
@@ -273,7 +356,17 @@ export const eventSchemas = {
|
||||
settings_changed: settingsChangedSchema,
|
||||
|
||||
telemetry_opted_in: telemetryOptedInSchema,
|
||||
telemetry_opted_out: telemetryOptedOutSchema
|
||||
telemetry_opted_out: telemetryOptedOutSchema,
|
||||
|
||||
onboarding_started: onboardingStartedSchema,
|
||||
onboarding_step_viewed: onboardingStepViewedSchema,
|
||||
onboarding_step_completed: onboardingStepCompletedSchema,
|
||||
onboarding_step_skipped: onboardingStepSkippedSchema,
|
||||
onboarding_step4_path_clicked: onboardingStep4PathClickedSchema,
|
||||
onboarding_step4_path_failed: onboardingStep4PathFailedSchema,
|
||||
onboarding_completed: onboardingCompletedSchema,
|
||||
onboarding_dismissed: onboardingDismissedSchema,
|
||||
activation_checklist_item_completed: activationChecklistItemCompletedSchema
|
||||
} as const
|
||||
|
||||
export type EventMap = { [N in keyof typeof eventSchemas]: z.infer<(typeof eventSchemas)[N]> }
|
||||
|
||||
@@ -1316,6 +1316,41 @@ export type NotificationSoundPathResult =
|
||||
| { ok: true; path: string }
|
||||
| { ok: false; reason: 'missing-path' | 'invalid-path' | 'unsupported-type' }
|
||||
|
||||
export type OnboardingOutcome = 'completed' | 'dismissed'
|
||||
|
||||
export type OnboardingChecklistState = {
|
||||
addedRepo: boolean
|
||||
choseAgent: boolean
|
||||
ranFirstAgent: boolean
|
||||
ranSecondAgentOnSameTask: boolean
|
||||
triedCmdJ: boolean
|
||||
shapedSidebar: boolean
|
||||
reviewedDiff: boolean
|
||||
openedPr: boolean
|
||||
addedFolder: boolean
|
||||
openedFile: boolean
|
||||
ranAgentOnFile: boolean
|
||||
// Why: UI state flag (panel visibility), not an activation event. The
|
||||
// telemetry checklist enum in telemetry-events.ts intentionally omits this.
|
||||
dismissed: boolean
|
||||
}
|
||||
|
||||
export type OnboardingState = {
|
||||
closedAt: number | null
|
||||
outcome: OnboardingOutcome | null
|
||||
// Sentinel `-1` = not started; `1..4` = highest wizard step the user
|
||||
// finished. Kept as `number` (not a literal union) because callers clamp
|
||||
// via `Math.max`/`Math.min` against arbitrary numerics.
|
||||
lastCompletedStep: number
|
||||
checklist: OnboardingChecklistState
|
||||
}
|
||||
|
||||
export type NotificationPermissionStatusResult = {
|
||||
supported: boolean
|
||||
platform: NodeJS.Platform
|
||||
requested: boolean
|
||||
}
|
||||
|
||||
export type WorktreeCardProperty =
|
||||
| 'status'
|
||||
| 'unread'
|
||||
@@ -1534,6 +1569,7 @@ export type PersistedState = {
|
||||
}
|
||||
workspaceSession: WorkspaceSessionState
|
||||
sshTargets: SshTarget[]
|
||||
onboarding: OnboardingState
|
||||
}
|
||||
|
||||
// ─── Filesystem ─────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user