From 305392d03154a6af46c1c800139ce41257d0761b Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 20 Aug 2026 13:13:42 +0200 Subject: [PATCH 1/9] feat: offer tracking events as onboarding option --- .../e2e/__snapshots__/happy-path.e2e.ts.snap | 7 ++- .../e2e/__snapshots__/select-goal.e2e.ts.snap | 7 ++- __tests__/e2e/select-goal.e2e.ts | 39 +++++++++---- .../ui/screens/SelectGoalScreen.test.tsx | 55 ++++++++++++++----- src/integrations/shared.ts | 1 + src/lib/onboarding-prompt/event-tracking.ts | 24 ++++++++ src/lib/onboarding-prompt/index.ts | 7 ++- src/lib/onboarding-prompt/report-templates.ts | 35 +++++++++++- .../steps/instrument-events.md | 36 ++++++++++++ src/lib/session.ts | 2 +- .../components/OnboardingLeftPanel.tsx | 7 ++- .../onboard-project/useOnboardingProcess.ts | 36 ++++++++---- src/ui/tui/screens/select-goal/actions.ts | 15 ++--- .../select-goal/components/LeftPanel.tsx | 8 ++- .../screens/select-goal/useGoalSelection.ts | 11 ---- .../select-goal/useInitialGoalSelection.ts | 3 +- src/ui/tui/theme/icons.ts | 1 + 17 files changed, 231 insertions(+), 63 deletions(-) create mode 100644 src/lib/onboarding-prompt/event-tracking.ts create mode 100644 src/lib/onboarding-prompt/steps/instrument-events.md diff --git a/__tests__/e2e/__snapshots__/happy-path.e2e.ts.snap b/__tests__/e2e/__snapshots__/happy-path.e2e.ts.snap index 04957d3..fc2589e 100644 --- a/__tests__/e2e/__snapshots__/happy-path.e2e.ts.snap +++ b/__tests__/e2e/__snapshots__/happy-path.e2e.ts.snap @@ -120,15 +120,18 @@ exports[`happy-path flow > navigates Welcome → SystemCheck → Authenticate Choose the Confidence features to integrate with your project. ● Check system ● Sign in to Confidence - ◆ Session Recordings (β) require the feature to be enabled on your ● Set up your agent - Confidence account. ● Connect tools + ✱ Event Tracking works best with a managed warehouse or an ● Set up your agent + existing warehouse setup. ● Connect tools ▶ Onboard project + ✱ Session Recordings (β) require the feature to be enabled on your + Confidence account. ────────────────────────────────────────────────────────────────────────────────────────────────── Select features to set up: ❯ Feature Flags + Event Tracking Session Recordings (β) YOLO! Set up everything Skip setup diff --git a/__tests__/e2e/__snapshots__/select-goal.e2e.ts.snap b/__tests__/e2e/__snapshots__/select-goal.e2e.ts.snap index 677d0ad..1c5576d 100644 --- a/__tests__/e2e/__snapshots__/select-goal.e2e.ts.snap +++ b/__tests__/e2e/__snapshots__/select-goal.e2e.ts.snap @@ -7,15 +7,18 @@ exports[`SelectGoal screen > shows goal options for a browser framework > select Choose the Confidence features to integrate with your project. ● Check system ● Sign in to Confidence - ◆ Session Recordings (β) require the feature to be enabled on your ● Set up your agent - Confidence account. ● Connect tools + ✱ Event Tracking works best with a managed warehouse or an ● Set up your agent + existing warehouse setup. ● Connect tools ▶ Onboard project + ✱ Session Recordings (β) require the feature to be enabled on your + Confidence account. ────────────────────────────────────────────────────────────────────────────────────────────────── Select features to set up: ❯ Feature Flags + Event Tracking Session Recordings (β) YOLO! Set up everything Skip setup diff --git a/__tests__/e2e/select-goal.e2e.ts b/__tests__/e2e/select-goal.e2e.ts index 017994e..9b034ce 100644 --- a/__tests__/e2e/select-goal.e2e.ts +++ b/__tests__/e2e/select-goal.e2e.ts @@ -12,6 +12,7 @@ describe('SelectGoal screen', () => { await session.waitForText('Feature Flags'); await session.waitForText('Session Recordings'); + await session.waitForText('Event Tracking'); await session.waitForText('YOLO'); await session.waitForText('Skip setup'); expect(session.snapshot()).toMatchSnapshot('select-goal'); @@ -35,8 +36,8 @@ describe('SelectGoal screen', () => { await navigateToGoalSelection(session); session.checkpoint(); - // Session Recordings — 2nd option - await session.press('ArrowDown'); + // Session Recordings — 3rd option + await session.pressRepeat('ArrowDown', 2); await session.press('Enter'); await session.waitForText('Start onboarding?'); @@ -44,19 +45,35 @@ describe('SelectGoal screen', () => { expect(session.snapshot()).toContain('set up session recordings'); }); + it('shows event tracking steps after selecting Event Tracking', async () => { + using session = createSession(); + + await navigateToGoalSelection(session); + session.checkpoint(); + + // Event Tracking — 2nd option + await session.press('ArrowDown'); + await session.press('Enter'); + + await session.waitForText('Start onboarding?'); + expect(session.snapshot()).toContain('add the Confidence SDK'); + expect(session.snapshot()).toContain('instrument event tracking'); + }); + it('shows combined steps after selecting YOLO', async () => { using session = createSession(); await navigateToGoalSelection(session); session.checkpoint(); - // YOLO — 3rd option - await session.pressRepeat('ArrowDown', 2); + // YOLO — 4th option + await session.pressRepeat('ArrowDown', 3); await session.press('Enter'); await session.waitForText('Start onboarding?'); expect(session.snapshot()).toContain('set up feature flags'); expect(session.snapshot()).toContain('set up session recordings'); + expect(session.snapshot()).toContain('instrument event tracking'); }); it('advances to Done when skip is selected', async () => { @@ -64,23 +81,23 @@ describe('SelectGoal screen', () => { await navigateToGoalSelection(session); - // Skip setup — 4th option - await session.pressRepeat('ArrowDown', 3); + // Skip setup — 5th option + await session.pressRepeat('ArrowDown', 4); await session.press('Enter'); await session.waitForText('Onboarding skipped'); }); - it('auto-advances for non-browser project', async () => { + it('shows goal selection for non-browser project without recording option', async () => { using session = createSession({ project: 'statsig-node' }); await navigateToConnectTools(session); session.checkpoint(); await session.press('Enter'); - // Non-browser project auto-advances past goal selection to onboarding - await session.waitForText('Start onboarding?'); - expect(session.snapshot()).toContain('add the Confidence SDK'); - expect(session.snapshot()).toContain('create your first feature flag'); + // Non-browser project now shows goal selection (without Session Recordings) + await session.waitForText('Feature Flags'); + await session.waitForText('Event Tracking'); + await session.waitForText('Skip setup'); }); }); diff --git a/__tests__/ui/screens/SelectGoalScreen.test.tsx b/__tests__/ui/screens/SelectGoalScreen.test.tsx index f8c2066..5c7107e 100644 --- a/__tests__/ui/screens/SelectGoalScreen.test.tsx +++ b/__tests__/ui/screens/SelectGoalScreen.test.tsx @@ -24,6 +24,7 @@ describe('SelectGoalScreen', () => { expect(frame).toContain('Which features would you like to set up?'); expect(frame).toContain('Feature Flags'); expect(frame).toContain('Session Recording'); + expect(frame).toContain('Event Tracking'); expect(frame).toContain('YOLO'); expect(frame).toContain('Skip setup'); }); @@ -44,7 +45,22 @@ describe('SelectGoalScreen', () => { }); }); - it('auto-skips to OnboardProject for unknown framework', async () => { + it('shows warehouse note for event tracking', async () => { + using project = createProjectDir(); + + using sut = renderApp({ + screen: ScreenId.SelectGoal, + dir: project.path, + framework: 'react', + }); + + await waitFor(() => { + expect(sut.lastFrame()).toContain('managed warehouse'); + expect(sut.lastFrame()).toContain('warehouse setup'); + }); + }); + + it('shows goal selection for non-browser project without recording option', async () => { using project = createProjectDir('empty'); using sut = renderApp({ @@ -53,8 +69,11 @@ describe('SelectGoalScreen', () => { }); await waitFor(() => { - expect(sut.lastFrame()).toContain('Ready to start?'); - expect(sut.lastFrame()).toContain('Start onboarding?'); + const frame = sut.lastFrame()!; + expect(frame).toContain('Feature Flags'); + expect(frame).toContain('Event Tracking'); + expect(frame).toContain('Skip setup'); + expect(frame).not.toContain('Session Recording'); }); }); @@ -92,41 +111,51 @@ describe('SelectGoalScreen', () => { expect(sut.lastFrame()).toContain('Skip setup'); }); - await act(() => sut.stdin.write(ARROW_DOWN.repeat(3) + ENTER)); + // Skip is the 5th option for browser projects (flags, events, recordings, yolo, skip) + await act(() => sut.stdin.write(ARROW_DOWN.repeat(4) + ENTER)); await waitFor(() => { expect(sut.lastFrame()).toContain('Onboarding skipped'); }); }); - it('auto-skips for non-browser project with competitors', async () => { - using project = createProjectDir('statsig-node'); + it('shows event tracking steps after selecting Event Tracking', async () => { + using project = createProjectDir(); using sut = renderApp({ screen: ScreenId.SelectGoal, dir: project.path, - framework: 'node', - plugins: ['claude'], + framework: 'react', }); + await waitFor(() => { + expect(sut.lastFrame()).toContain('Event Tracking'); + }); + + // Event Tracking is the 2nd option for browser projects + await act(() => sut.stdin.write(ARROW_DOWN + ENTER)); + await waitFor(() => { expect(sut.lastFrame()).toContain('Ready to start?'); - expect(sut.lastFrame()).toContain('Start onboarding?'); + expect(sut.lastFrame()).toContain('instrument event tracking'); }); }); - it('skips directly to OnboardProject for non-browser project without competitors', async () => { - using project = createProjectDir('empty'); + it('shows goal selection for non-browser project with competitors', async () => { + using project = createProjectDir('statsig-node'); using sut = renderApp({ screen: ScreenId.SelectGoal, dir: project.path, framework: 'node', + plugins: ['claude'], }); await waitFor(() => { - expect(sut.lastFrame()).toContain('Ready to start?'); - expect(sut.lastFrame()).toContain('Start onboarding?'); + const frame = sut.lastFrame()!; + expect(frame).toContain('Feature Flags'); + expect(frame).toContain('Event Tracking'); + expect(frame).not.toContain('Session Recording'); }); }); }); diff --git a/src/integrations/shared.ts b/src/integrations/shared.ts index a7c80ad..3df292f 100644 --- a/src/integrations/shared.ts +++ b/src/integrations/shared.ts @@ -5,6 +5,7 @@ import { SKILLS_BASE_URL } from '@lib/constants.js'; export const PLUGIN_SKILLS = [ 'analyze-project', + 'instrument-events', 'onboard-confidence', 'onboard-confidence-dry-run', 'setup-warehouse', diff --git a/src/lib/onboarding-prompt/event-tracking.ts b/src/lib/onboarding-prompt/event-tracking.ts new file mode 100644 index 0000000..5d12ffe --- /dev/null +++ b/src/lib/onboarding-prompt/event-tracking.ts @@ -0,0 +1,24 @@ +import type { ChosenIde } from '../session.js'; +import { loadStep } from './steps/load.js'; + +const SKILLS_DIR: Record = { + claude: '.claude/skills', + cursor: '.cursor/skills', + codex: '.agents/skills', +}; + +export function instrumentEvents( + framework: string, + step: number, + isEmptyProject: boolean, + ide: ChosenIde, +): string { + return loadStep('instrument-events.md', { + STEP: step, + FRAMEWORK: framework, + SKILLS_DIR: SKILLS_DIR[ide], + DOMAIN_CONTEXT: isEmptyProject + ? "The project was just scaffolded — treat the sample app's features as the domain." + : "Study the project's codebase to understand its domain, UI flows, and business logic.", + }); +} diff --git a/src/lib/onboarding-prompt/index.ts b/src/lib/onboarding-prompt/index.ts index 553d929..42e7675 100644 --- a/src/lib/onboarding-prompt/index.ts +++ b/src/lib/onboarding-prompt/index.ts @@ -4,6 +4,7 @@ import { preflight, scaffold } from './preflight.js'; import { determineSDK, resolveClient } from './sdk.js'; import { integrateSDK, integrateViaSkill } from './integrate.js'; import { determineRecordingSDK, integrateRecording } from './session-recording.js'; +import { instrumentEvents } from './event-tracking.js'; import { generateReport, summary, rules } from './report.js'; import { buildToolVars } from './tool-vars.js'; @@ -31,6 +32,7 @@ export function buildOnboardingPrompt({ const withFlags = ['feature-flags', 'all'].includes(goal); const withRecordings = ['session-recordings', 'all'].includes(goal); + const withEventTracking = ['event-tracking', 'all'].includes(goal); const viaSkill = withFlags && hasPlugins; const sections = [ @@ -51,6 +53,8 @@ export function buildOnboardingPrompt({ addIf(withRecordings, () => determineRecordingSDK(framework, steps.next(), tools)), addIf(withRecordings, () => integrateRecording(steps.next(), isEmptyProject)), + addIf(withEventTracking, () => instrumentEvents(framework, steps.next(), isEmptyProject, ide)), + generateReport({ step: steps.next(), isEmptyProject, @@ -78,7 +82,8 @@ class StepCounter { const GOAL_PREAMBLE: Record = { 'feature-flags': 'the Confidence SDK', 'session-recordings': 'Confidence Session Recording', - all: 'the Confidence SDK and Session Recording', + 'event-tracking': 'Confidence Event Tracking', + all: 'the Confidence SDK, Session Recording, and Event Tracking', }; function preamble( diff --git a/src/lib/onboarding-prompt/report-templates.ts b/src/lib/onboarding-prompt/report-templates.ts index 3432dc9..b9b473d 100644 --- a/src/lib/onboarding-prompt/report-templates.ts +++ b/src/lib/onboarding-prompt/report-templates.ts @@ -10,6 +10,9 @@ export function buildReportTemplate(goal: OnboardingGoal): ReportTemplate { case 'session-recordings': return { start: RECORDING_TEMPLATE_START, end: COMMON_TEMPLATE_END }; + case 'event-tracking': + return { start: EVENT_TRACKING_TEMPLATE_START, end: COMMON_TEMPLATE_END }; + case 'all': return { start: ALL_TEMPLATE_START, end: COMMON_TEMPLATE_END }; @@ -69,6 +72,32 @@ const RECORDING_TEMPLATE_START = `\ - \`\``; +const EVENT_TRACKING_TEMPLATE_START = `\ +\`\`\`markdown +# Confidence Quickstart Report + +## What was set up + +| | | +|---|---| +| Integration | Event Tracking | +| Client | | +| Event Definitions | created | +| Fact Tables | auto-created | + +## What changed in your codebase + +**New/modified files:** + +- \`<.env file>\` — added \`CONFIDENCE_CLIENT_SECRET\` +- \`\` — added SDK initialization +- \`\` — added event tracking calls + + +**New dependencies:** + +- \`\``; + const ALL_TEMPLATE_START = `\ \`\`\`markdown # Confidence Quickstart Report @@ -82,14 +111,17 @@ const ALL_TEMPLATE_START = `\ | Variants | | | Default | (100% allocation) | | Session Recording | Enabled | +| Event Definitions | created | +| Fact Tables | auto-created | ## What changed in your codebase **New/modified files:** - \`<.env file>\` — added \`CONFIDENCE_CLIENT_SECRET\` -- \`\` — added SDK initialization and session recording provider +- \`\` — added SDK initialization, session recording provider, and event tracking - \`\` — added flag evaluation +- \`\` — added event tracking calls **New dependencies:** @@ -117,6 +149,7 @@ const COMMON_TEMPLATE_END = `\ - [SDK reference]() - Set up a data warehouse → \`/setup-warehouse\` - Migrate flags from another provider → \`/migrate-\` (e.g. \`/migrate-statsig\`, \`/migrate-eppo\`) +- Preview and create metrics → \`/confidence:explore-metric\` - Invite your team → \`/onboard-confidence invite-user\` - Run an A/B experiment → \`/onboard-confidence learn\` diff --git a/src/lib/onboarding-prompt/steps/instrument-events.md b/src/lib/onboarding-prompt/steps/instrument-events.md new file mode 100644 index 0000000..0ba67b8 --- /dev/null +++ b/src/lib/onboarding-prompt/steps/instrument-events.md @@ -0,0 +1,36 @@ +## {{STEP}}. Instrument Event Tracking + +Print "STATUS: Analyzing project for event tracking..." + +The detected framework is **{{FRAMEWORK}}**. + +Read `{{SKILLS_DIR}}/instrument-events/SKILL.md` as a **methodology reference** — use it for what to scan, how to identify trackable events, how to create event definitions with entity references, and how to add `track()` calls. Ignore its output formatting entirely: no step tracker, no EDUCATE blocks, no AskUserQuestion calls. + +Execute the skill's workflow automatically, without pausing for user input: + +- **Discovery** (skill steps 1–2): scan the project, detect existing instrumentation, cross-reference with registered event definitions. +- **Proposal** (skill step 3): identify the top 3–5 most valuable events paired with metrics. Pick automatically based on domain analysis — do not ask the user. +- **Implementation** (skill steps 4–6): create event definitions with entity references, add SDK `track()` calls, verify the pipeline. + +{{DOMAIN_CONTEXT}} + +**Example STATUS lines for this step:** + +- "STATUS: Scanning for existing event tracking..." +- "STATUS: Checking registered event definitions..." +- "STATUS: Identifying trackable events..." +- "STATUS: Selected events to instrument" +- "STATUS: Creating event definitions..." +- "STATUS: Created event: " +- "STATUS: Adding track() calls..." +- "STATUS: Modified with track() call" +- "STATUS: Verifying event pipeline..." + +**Event-tracking guardrails:** + +- Every event definition MUST have at least one string field with `semanticType.entityReference` pointing to an entity (e.g. `entities/visitor`). Without this, no fact table is auto-created and the metric pipeline is broken. Call `mcp__confidence-flags__listEntities` to find available entities. +- Never create duplicate event definitions — cross-reference existing definitions first. +- Use the Confidence vanilla SDK `confidence.track()` for event tracking — not the OpenFeature `client.track()`. Adapt to the app's existing analytics layer if one is present. +- If the warehouse is not configured, note this in the status output. Event definitions and code can still be created — data will flow once a warehouse is connected. +- Event definition IDs: 4–63 chars, lowercase letters, digits, and hyphens only. +- Schema field names: use snake_case for multi-word fields. diff --git a/src/lib/session.ts b/src/lib/session.ts index 4c045d4..fe037fb 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -3,7 +3,7 @@ import type { DetectedProvider } from '@providers/types.js'; export type ChosenIde = 'claude' | 'cursor' | 'codex'; -export type OnboardingGoal = 'feature-flags' | 'session-recordings' | 'all'; +export type OnboardingGoal = 'feature-flags' | 'session-recordings' | 'event-tracking' | 'all'; export type FrameworkSource = 'detected' | 'selected'; diff --git a/src/ui/tui/screens/onboard-project/components/OnboardingLeftPanel.tsx b/src/ui/tui/screens/onboard-project/components/OnboardingLeftPanel.tsx index 8800daa..21862f5 100644 --- a/src/ui/tui/screens/onboard-project/components/OnboardingLeftPanel.tsx +++ b/src/ui/tui/screens/onboard-project/components/OnboardingLeftPanel.tsx @@ -90,7 +90,12 @@ export function OnboardingLeftPanel({ const GOAL_STEPS: Record = { 'feature-flags': ['create your first feature flag'], 'session-recordings': ['set up session recordings to capture user sessions'], - all: ['set up feature flags', 'set up session recordings to capture user sessions'], + 'event-tracking': ['instrument event tracking to measure user behavior'], + all: [ + 'set up feature flags', + 'set up session recordings to capture user sessions', + 'instrument event tracking to measure user behavior', + ], }; function onboardingSteps(goal: OnboardingGoal): string[] { diff --git a/src/ui/tui/screens/onboard-project/useOnboardingProcess.ts b/src/ui/tui/screens/onboard-project/useOnboardingProcess.ts index db6ef8f..3be5a45 100644 --- a/src/ui/tui/screens/onboard-project/useOnboardingProcess.ts +++ b/src/ui/tui/screens/onboard-project/useOnboardingProcess.ts @@ -235,15 +235,22 @@ function buildDryRunSteps(fw: string, isEmpty: boolean, goal: OnboardingGoal): s 'Adding session recording provider...', 'Configuring privacy settings...', ]; + const eventTrackingSteps = [ + 'Scanning for existing event tracking...', + 'Identifying trackable events...', + 'Creating event definitions...', + 'Adding track() calls...', + 'Verifying event pipeline...', + ]; - const goalSteps = - goal === 'feature-flags' - ? flagSteps - : goal === 'session-recordings' - ? recordingSteps - : [...flagSteps, ...recordingSteps]; + const goalSteps: Record = { + 'feature-flags': flagSteps, + 'session-recordings': recordingSteps, + 'event-tracking': eventTrackingSteps, + all: [...flagSteps, ...recordingSteps, ...eventTrackingSteps], + }; - return [...detect, ...goalSteps, 'Generating CONFIDENCE_QUICKSTART.md report...']; + return [...detect, ...goalSteps[goal], 'Generating CONFIDENCE_QUICKSTART.md report...']; } function dryRunCodeChanges(goal: OnboardingGoal): string[] { @@ -256,8 +263,17 @@ function dryRunCodeChanges(goal: OnboardingGoal): string[] { 'Added @spotify-confidence/session-recording dependency', 'Added session recording provider', ]; + const eventTrackingChanges = [ + 'Created event definitions with entity references', + 'Added confidence.track() calls', + ]; + + const goalChanges: Record = { + 'feature-flags': flagChanges, + 'session-recordings': recordingChanges, + 'event-tracking': eventTrackingChanges, + all: [...flagChanges, ...recordingChanges, ...eventTrackingChanges], + }; - if (goal === 'feature-flags') return flagChanges; - if (goal === 'session-recordings') return recordingChanges; - return [...flagChanges, ...recordingChanges]; + return goalChanges[goal]; } diff --git a/src/ui/tui/screens/select-goal/actions.ts b/src/ui/tui/screens/select-goal/actions.ts index faa9a7f..b16d959 100644 --- a/src/ui/tui/screens/select-goal/actions.ts +++ b/src/ui/tui/screens/select-goal/actions.ts @@ -3,21 +3,22 @@ import type { PromptOption } from '../../components/PromptPanel.js'; export type GoalValue = OnboardingGoal | 'skip'; -const GOAL_OPTIONS: PromptOption[] = [ +const BASE_GOAL_OPTIONS: PromptOption[] = [ { label: 'Feature Flags', value: 'feature-flags' }, - { label: 'Session Recordings (β)', value: 'session-recordings' }, + { label: 'Event Tracking', value: 'event-tracking' }, { label: 'YOLO! Set up everything', value: 'all' }, { label: 'Skip setup', value: 'skip' }, ]; -const FLAGS_ONLY_OPTIONS: PromptOption[] = GOAL_OPTIONS.filter((o) => - (['feature-flags', 'skip'] as GoalValue[]).includes(o.value), -); +const WITH_RECORDINGS: PromptOption[] = BASE_GOAL_OPTIONS.toSpliced(2, 0, { + label: 'Session Recordings (β)', + value: 'session-recordings', +} as PromptOption); export function goalOptionsFor(recordingAvailable: boolean): PromptOption[] { - return recordingAvailable ? GOAL_OPTIONS : FLAGS_ONLY_OPTIONS; + return recordingAvailable ? WITH_RECORDINGS : BASE_GOAL_OPTIONS; } export function goalLabel(goal: OnboardingGoal): string { - return GOAL_OPTIONS.find((o) => o.value === goal)?.label ?? goal; + return BASE_GOAL_OPTIONS.find((o) => o.value === goal)?.label ?? goal; } diff --git a/src/ui/tui/screens/select-goal/components/LeftPanel.tsx b/src/ui/tui/screens/select-goal/components/LeftPanel.tsx index c59f5d8..e37874a 100644 --- a/src/ui/tui/screens/select-goal/components/LeftPanel.tsx +++ b/src/ui/tui/screens/select-goal/components/LeftPanel.tsx @@ -22,10 +22,16 @@ export function LeftPanel({ goalSelection }: GoalSelectionProps) { Choose the Confidence features to integrate with your project. + + + {Icons.star} Event Tracking works best with a managed warehouse or an existing + warehouse setup. + + {goalSelection.recordingAvailable && ( - {Icons.diamond} Session Recordings (β) require the feature to be enabled on your + {Icons.star} Session Recordings (β) require the feature to be enabled on your Confidence account. diff --git a/src/ui/tui/screens/select-goal/useGoalSelection.ts b/src/ui/tui/screens/select-goal/useGoalSelection.ts index 087de08..01259a8 100644 --- a/src/ui/tui/screens/select-goal/useGoalSelection.ts +++ b/src/ui/tui/screens/select-goal/useGoalSelection.ts @@ -1,7 +1,6 @@ import { ScreenId } from '@lib/session.js'; import { useNavigation } from '../../hooks/useNavigation.js'; import { useLogger } from '../../hooks/useLog.js'; -import { useAutoAdvance } from '../../hooks/useAutoAdvance.js'; import { store } from '../../store.js'; import { track } from '@lib/telemetry.js'; import { skipped } from '../../lib/log-messages.js'; @@ -21,16 +20,6 @@ export function useGoalSelection(): GoalSelection { const log = useLogger(ScreenId.SelectGoal); const initial = useInitialGoalSelection(); - useAutoAdvance({ - screen: ScreenId.SelectGoal, - when: initial.phase === 'done', - delay: 0, - onAdvance: () => { - store.setOnboardingGoal('feature-flags'); - log(goalChosen('feature-flags (auto — framework)')); - }, - }); - function selectGoal(value: GoalValue) { if (value === 'skip') { track(te.goalSkipped()); diff --git a/src/ui/tui/screens/select-goal/useInitialGoalSelection.ts b/src/ui/tui/screens/select-goal/useInitialGoalSelection.ts index 1a4ca79..1800797 100644 --- a/src/ui/tui/screens/select-goal/useInitialGoalSelection.ts +++ b/src/ui/tui/screens/select-goal/useInitialGoalSelection.ts @@ -18,6 +18,5 @@ export function useInitialGoalSelection(): InitialGoalSelection { function resolveInitialGoalSelection(session: WizardSession): InitialGoalSelection { const supportsRecordings = !!session.framework && BROWSER_PLATFORMS.has(session.framework); - const phase = supportsRecordings ? 'select-goal' : 'done'; - return { phase, supportsRecordings }; + return { phase: 'select-goal', supportsRecordings }; } diff --git a/src/ui/tui/theme/icons.ts b/src/ui/tui/theme/icons.ts index 3e031d5..da5b376 100644 --- a/src/ui/tui/theme/icons.ts +++ b/src/ui/tui/theme/icons.ts @@ -10,6 +10,7 @@ export const Icons = { square: '■', squareSmall: '▪', triangleRight: '▶', + star: '✱', dash: '─', plus: '+', verticalLine: '│', From 0833c6cecd5a89e6d493bc5fd3ad98185097735c Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 20 Aug 2026 13:13:58 +0200 Subject: [PATCH 2/9] chore: clean up downloaded skills --- src/integrations/shared.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/integrations/shared.ts b/src/integrations/shared.ts index 3df292f..138a974 100644 --- a/src/integrations/shared.ts +++ b/src/integrations/shared.ts @@ -7,7 +7,6 @@ export const PLUGIN_SKILLS = [ 'analyze-project', 'instrument-events', 'onboard-confidence', - 'onboard-confidence-dry-run', 'setup-warehouse', 'setup-warehouse-bigquery', 'setup-warehouse-databricks', From 32b4e90654dbce7898947c7d4860b3a58976ec00 Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 20 Aug 2026 13:44:12 +0200 Subject: [PATCH 3/9] feat(PromptPanel): support multi-select mode --- src/ui/tui/components/PromptPanel.tsx | 31 ++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/ui/tui/components/PromptPanel.tsx b/src/ui/tui/components/PromptPanel.tsx index a9c1dcf..a4eee09 100644 --- a/src/ui/tui/components/PromptPanel.tsx +++ b/src/ui/tui/components/PromptPanel.tsx @@ -1,5 +1,5 @@ import { Box, Text, useInput } from 'ink'; -import { Select, TextInput } from '@inkjs/ui'; +import { MultiSelect, Select, TextInput } from '@inkjs/ui'; import { APP_VERSION } from '@lib/meta.js'; import { Colors, HAlign } from '../styles.js'; import { useIsNarrow } from '../hooks/useIsNarrow.js'; @@ -27,13 +27,23 @@ type PromptPanelInputProps = PromptPanelBase & { onSubmit: (value: string) => void; }; +type PromptPanelMultiSelectProps = PromptPanelBase & { + mode: 'multi-select'; + status: string; + options: PromptOption[]; + onSubmit: (values: T[]) => void; +}; + type PromptPanelInfoProps = PromptPanelBase & { mode: 'info'; status: string; }; type PromptPanelProps = - PromptPanelSelectProps | PromptPanelInputProps | PromptPanelInfoProps; + | PromptPanelSelectProps + | PromptPanelMultiSelectProps + | PromptPanelInputProps + | PromptPanelInfoProps; const MAX_VISIBLE_OPTIONS = 8; @@ -70,6 +80,13 @@ export function PromptPanel(props: PromptPanelProps )} + {props.mode === 'multi-select' && ( + void} + visibleOptionCount={Math.min(props.options.length, MAX_VISIBLE_OPTIONS)} + /> + )} {props.mode === 'input' && ( )} @@ -77,7 +94,7 @@ export function PromptPanel(props: PromptPanelProps - {props.mode === 'select' && props.options.length > 1 && ( + {(props.mode === 'select' || props.mode === 'multi-select') && props.options.length > 1 && ( ↑↓ @@ -85,6 +102,14 @@ export function PromptPanel(props: PromptPanelPropsnavigate )} + {props.mode === 'multi-select' && ( + + + space + + toggle + + )} {props.mode !== 'info' && ( From aca726571c1f89806f3768c012aae3699070f08d Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 20 Aug 2026 14:20:21 +0200 Subject: [PATCH 4/9] feat: improve onboarding goal selection UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allows multi-selection; treats empty value as “skip.” --- .../e2e/__snapshots__/happy-path.e2e.ts.snap | 10 +- .../e2e/__snapshots__/select-goal.e2e.ts.snap | 10 +- __tests__/e2e/chat-launch.e2e.ts | 3 +- __tests__/e2e/happy-path.e2e.ts | 3 +- __tests__/e2e/select-goal.e2e.ts | 33 ++-- __tests__/e2e/skip-plugins.e2e.ts | 6 +- __tests__/e2e/skip-tools.e2e.ts | 3 +- __tests__/e2e/stale-mcp-auth.e2e.ts | 3 +- __tests__/e2e/testing-framework/navigation.ts | 10 +- __tests__/ui/screens/OnboardingFlow.test.tsx | 11 +- .../ui/screens/SelectGoalScreen.test.tsx | 20 +-- __tests__/ui/testing-framework/index.ts | 2 + __tests__/ui/testing-framework/ink/render.tsx | 4 +- src/lib/onboarding-prompt/index.ts | 27 ++-- src/lib/onboarding-prompt/report-templates.ts | 149 ++++++------------ src/lib/onboarding-prompt/report.ts | 6 +- src/lib/session.ts | 6 +- .../onboard-project/OnboardProjectScreen.tsx | 6 +- .../components/OnboardingLeftPanel.tsx | 15 +- .../onboard-project/useOnboardingProcess.ts | 70 ++++---- src/ui/tui/screens/select-goal/actions.ts | 18 +-- .../select-goal/components/BottomPrompt.tsx | 6 +- .../select-goal/components/LeftPanel.tsx | 4 +- .../tui/screens/select-goal/log-messages.ts | 4 +- .../screens/select-goal/telemetry-events.ts | 4 +- .../screens/select-goal/useGoalSelection.ts | 20 +-- src/ui/tui/store.ts | 4 +- 27 files changed, 198 insertions(+), 259 deletions(-) diff --git a/__tests__/e2e/__snapshots__/happy-path.e2e.ts.snap b/__tests__/e2e/__snapshots__/happy-path.e2e.ts.snap index fc2589e..a3446ef 100644 --- a/__tests__/e2e/__snapshots__/happy-path.e2e.ts.snap +++ b/__tests__/e2e/__snapshots__/happy-path.e2e.ts.snap @@ -116,9 +116,9 @@ exports[`happy-path flow > navigates Welcome → SystemCheck → Authenticate exports[`happy-path flow > navigates Welcome → SystemCheck → Authenticate → InstallPlugins → ConnectTools → SelectGoal → OnboardProject → Done > select-goal 1`] = ` " Confidence by Spotify https://confidence.spotify.com/ - Which features would you like to set up? Todo (4/5) + Select the features you'd like to set up Todo (4/5) - Choose the Confidence features to integrate with your project. ● Check system + Toggle Confidence features to integrate with your project. ● Check system ● Sign in to Confidence ✱ Event Tracking works best with a managed warehouse or an ● Set up your agent existing warehouse setup. ● Connect tools @@ -128,15 +128,13 @@ exports[`happy-path flow > navigates Welcome → SystemCheck → Authenticate ────────────────────────────────────────────────────────────────────────────────────────────────── - Select features to set up: + Toggle features to set up (select none to skip): ❯ Feature Flags Event Tracking Session Recordings (β) - YOLO! Set up everything - Skip setup - ↑↓ navigate enter confirm vX.Y.Z" + ↑↓ navigate space toggle enter confirm vX.Y.Z" `; exports[`happy-path flow > navigates Welcome → SystemCheck → Authenticate → InstallPlugins → ConnectTools → SelectGoal → OnboardProject → Done > system-check 1`] = ` diff --git a/__tests__/e2e/__snapshots__/select-goal.e2e.ts.snap b/__tests__/e2e/__snapshots__/select-goal.e2e.ts.snap index 1c5576d..8f06107 100644 --- a/__tests__/e2e/__snapshots__/select-goal.e2e.ts.snap +++ b/__tests__/e2e/__snapshots__/select-goal.e2e.ts.snap @@ -3,9 +3,9 @@ exports[`SelectGoal screen > shows goal options for a browser framework > select-goal 1`] = ` " Confidence by Spotify https://confidence.spotify.com/ - Which features would you like to set up? Todo (4/5) + Select the features you'd like to set up Todo (4/5) - Choose the Confidence features to integrate with your project. ● Check system + Toggle Confidence features to integrate with your project. ● Check system ● Sign in to Confidence ✱ Event Tracking works best with a managed warehouse or an ● Set up your agent existing warehouse setup. ● Connect tools @@ -15,13 +15,11 @@ exports[`SelectGoal screen > shows goal options for a browser framework > select ────────────────────────────────────────────────────────────────────────────────────────────────── - Select features to set up: + Toggle features to set up (select none to skip): ❯ Feature Flags Event Tracking Session Recordings (β) - YOLO! Set up everything - Skip setup - ↑↓ navigate enter confirm vX.Y.Z" + ↑↓ navigate space toggle enter confirm vX.Y.Z" `; diff --git a/__tests__/e2e/chat-launch.e2e.ts b/__tests__/e2e/chat-launch.e2e.ts index b5859d5..37b8fc8 100644 --- a/__tests__/e2e/chat-launch.e2e.ts +++ b/__tests__/e2e/chat-launch.e2e.ts @@ -82,7 +82,8 @@ describe('when the user starts chat after onboarding', () => { await session.waitForText('Skipped'); // SelectGoal - await session.waitForText('Which features would you like to set up?'); + await session.waitForText("Select the features you'd like to set up"); + await session.press('Space'); await session.press('Enter'); // OnboardProject diff --git a/__tests__/e2e/happy-path.e2e.ts b/__tests__/e2e/happy-path.e2e.ts index b0b587f..f881d07 100644 --- a/__tests__/e2e/happy-path.e2e.ts +++ b/__tests__/e2e/happy-path.e2e.ts @@ -43,9 +43,10 @@ describe('happy-path flow', () => { await session.waitForText('Connected successfully'); // SelectGoal - await session.waitForText('Which features would you like to set up?'); + await session.waitForText("Select the features you'd like to set up"); expect(session.snapshot()).toMatchSnapshot('select-goal'); session.checkpoint(); + await session.press('Space'); await session.press('Enter'); // OnboardProject diff --git a/__tests__/e2e/select-goal.e2e.ts b/__tests__/e2e/select-goal.e2e.ts index 9b034ce..698aa3b 100644 --- a/__tests__/e2e/select-goal.e2e.ts +++ b/__tests__/e2e/select-goal.e2e.ts @@ -13,8 +13,8 @@ describe('SelectGoal screen', () => { await session.waitForText('Feature Flags'); await session.waitForText('Session Recordings'); await session.waitForText('Event Tracking'); - await session.waitForText('YOLO'); - await session.waitForText('Skip setup'); + await session.waitForText('space'); + await session.waitForText('toggle'); expect(session.snapshot()).toMatchSnapshot('select-goal'); }); @@ -23,6 +23,7 @@ describe('SelectGoal screen', () => { await navigateToGoalSelection(session); session.checkpoint(); + await session.press('Space'); await session.press('Enter'); await session.waitForText('Start onboarding?'); @@ -38,6 +39,7 @@ describe('SelectGoal screen', () => { // Session Recordings — 3rd option await session.pressRepeat('ArrowDown', 2); + await session.press('Space'); await session.press('Enter'); await session.waitForText('Start onboarding?'); @@ -45,44 +47,46 @@ describe('SelectGoal screen', () => { expect(session.snapshot()).toContain('set up session recordings'); }); - it('shows event tracking steps after selecting Event Tracking', async () => { + it('shows combined steps when multiple goals are selected', async () => { using session = createSession(); await navigateToGoalSelection(session); session.checkpoint(); - // Event Tracking — 2nd option + // Toggle Feature Flags (1st), Event Tracking (2nd), Session Recordings (3rd) + await session.press('Space'); await session.press('ArrowDown'); + await session.press('Space'); + await session.pressRepeat('ArrowDown', 1); + await session.press('Space'); await session.press('Enter'); await session.waitForText('Start onboarding?'); - expect(session.snapshot()).toContain('add the Confidence SDK'); + expect(session.snapshot()).toContain('create your first feature flag'); expect(session.snapshot()).toContain('instrument event tracking'); + expect(session.snapshot()).toContain('set up session recordings'); }); - it('shows combined steps after selecting YOLO', async () => { + it('shows event tracking steps after selecting Event Tracking', async () => { using session = createSession(); await navigateToGoalSelection(session); session.checkpoint(); - // YOLO — 4th option - await session.pressRepeat('ArrowDown', 3); + // Event Tracking — 2nd option + await session.press('ArrowDown'); + await session.press('Space'); await session.press('Enter'); await session.waitForText('Start onboarding?'); - expect(session.snapshot()).toContain('set up feature flags'); - expect(session.snapshot()).toContain('set up session recordings'); + expect(session.snapshot()).toContain('add the Confidence SDK'); expect(session.snapshot()).toContain('instrument event tracking'); }); - it('advances to Done when skip is selected', async () => { + it('advances to Done when submitting with nothing selected', async () => { using session = createSession(); await navigateToGoalSelection(session); - - // Skip setup — 5th option - await session.pressRepeat('ArrowDown', 4); await session.press('Enter'); await session.waitForText('Onboarding skipped'); @@ -98,6 +102,5 @@ describe('SelectGoal screen', () => { // Non-browser project now shows goal selection (without Session Recordings) await session.waitForText('Feature Flags'); await session.waitForText('Event Tracking'); - await session.waitForText('Skip setup'); }); }); diff --git a/__tests__/e2e/skip-plugins.e2e.ts b/__tests__/e2e/skip-plugins.e2e.ts index 58cb32c..3579f98 100644 --- a/__tests__/e2e/skip-plugins.e2e.ts +++ b/__tests__/e2e/skip-plugins.e2e.ts @@ -16,7 +16,8 @@ describe('when the user skips installing AI plugin', () => { await session.waitForText('Connected successfully'); // SelectGoal - await session.waitForText('Which features would you like to set up?'); + await session.waitForText("Select the features you'd like to set up"); + await session.press('Space'); await session.press('Enter'); // OnboardProject @@ -50,7 +51,8 @@ describe('when the user skips installing AI plugin', () => { await session.waitForText('Connected successfully'); // SelectGoal - await session.waitForText('Which features would you like to set up?'); + await session.waitForText("Select the features you'd like to set up"); + await session.press('Space'); await session.press('Enter'); await session.waitForText('Start onboarding?'); diff --git a/__tests__/e2e/skip-tools.e2e.ts b/__tests__/e2e/skip-tools.e2e.ts index 445c9e3..d463a11 100644 --- a/__tests__/e2e/skip-tools.e2e.ts +++ b/__tests__/e2e/skip-tools.e2e.ts @@ -14,7 +14,8 @@ describe('when the user skips connecting tools', () => { await session.waitForText('Skipped'); // SelectGoal - await session.waitForText('Which features would you like to set up?'); + await session.waitForText("Select the features you'd like to set up"); + await session.press('Space'); await session.press('Enter'); // OnboardProject diff --git a/__tests__/e2e/stale-mcp-auth.e2e.ts b/__tests__/e2e/stale-mcp-auth.e2e.ts index 9c9dae7..bd0b37d 100644 --- a/__tests__/e2e/stale-mcp-auth.e2e.ts +++ b/__tests__/e2e/stale-mcp-auth.e2e.ts @@ -81,7 +81,8 @@ describe('when MCP config has expired auth tokens', () => { await session.waitForText('Skipped'); // SelectGoal - await session.waitForText('Which features would you like to set up?'); + await session.waitForText("Select the features you'd like to set up"); + await session.press('Space'); await session.press('Enter'); // OnboardProject diff --git a/__tests__/e2e/testing-framework/navigation.ts b/__tests__/e2e/testing-framework/navigation.ts index 153adab..13a7358 100644 --- a/__tests__/e2e/testing-framework/navigation.ts +++ b/__tests__/e2e/testing-framework/navigation.ts @@ -70,7 +70,7 @@ export async function navigateToGoalSelection(session: TerminalSession): Promise await navigateToConnectTools(session); session.checkpoint(); await session.press('Enter'); - await session.waitForText('Which features would you like to set up?'); + await session.waitForText("Select the features you'd like to set up"); } /** @@ -82,6 +82,7 @@ export async function navigateToGoalSelection(session: TerminalSession): Promise export async function navigateToOnboarding(session: TerminalSession): Promise { await navigateToGoalSelection(session); session.checkpoint(); + await session.press('Space'); await session.press('Enter'); await session.waitForText('Start onboarding?'); } @@ -114,17 +115,18 @@ export async function selectIdeAndOnboard( const matched = await session.waitForText([ 'Start onboarding?', 'Connect Confidence tools?', - 'Which features would you like to set up?', + "Select the features you'd like to set up", ]); if (matched === 'Connect Confidence tools?') { await session.press('Enter'); await session.waitForText('Connected successfully'); - await session.waitForText('Which features would you like to set up?'); + await session.waitForText("Select the features you'd like to set up"); } if (matched !== 'Start onboarding?') { - // SelectGoal — pick Feature Flags (default) + // SelectGoal — toggle Feature Flags and submit + await session.press('Space'); await session.press('Enter'); await session.waitForText('Start onboarding?'); } diff --git a/__tests__/ui/screens/OnboardingFlow.test.tsx b/__tests__/ui/screens/OnboardingFlow.test.tsx index 24895c8..7050a55 100644 --- a/__tests__/ui/screens/OnboardingFlow.test.tsx +++ b/__tests__/ui/screens/OnboardingFlow.test.tsx @@ -234,7 +234,7 @@ describe('Onboarding flow', () => { using sut = renderScreen(, { screen: ScreenId.OnboardProject, dir: project.path, - goal: 'feature-flags', + goals: ['feature-flags'], }); await waitFor(() => { @@ -251,7 +251,7 @@ describe('Onboarding flow', () => { using sut = renderScreen(, { screen: ScreenId.OnboardProject, dir: project.path, - goal: 'session-recordings', + goals: ['session-recordings'], }); await waitFor(() => { @@ -262,20 +262,21 @@ describe('Onboarding flow', () => { }); }); - it('shows combined steps when goal is all', async () => { + it('shows combined steps when multiple goals are selected', async () => { using project = createProjectDir(); using sut = renderScreen(, { screen: ScreenId.OnboardProject, dir: project.path, - goal: 'all', + goals: ['feature-flags', 'session-recordings', 'event-tracking'], }); await waitFor(() => { const frame = sut.lastFrame()!; expect(frame).toContain('add the Confidence SDK'); - expect(frame).toContain('set up feature flags'); + expect(frame).toContain('create your first feature flag'); expect(frame).toContain('set up session recordings'); + expect(frame).toContain('instrument event tracking'); }); }); }); diff --git a/__tests__/ui/screens/SelectGoalScreen.test.tsx b/__tests__/ui/screens/SelectGoalScreen.test.tsx index 5c7107e..535e9cc 100644 --- a/__tests__/ui/screens/SelectGoalScreen.test.tsx +++ b/__tests__/ui/screens/SelectGoalScreen.test.tsx @@ -4,6 +4,7 @@ import { createProjectDir, ENTER, ARROW_DOWN, + SPACE, waitFor, } from '../testing-framework/index.js'; import { ScreenId } from '@lib/session.js'; @@ -21,12 +22,12 @@ describe('SelectGoalScreen', () => { await waitFor(() => { const frame = sut.lastFrame()!; - expect(frame).toContain('Which features would you like to set up?'); + expect(frame).toContain('Select the features'); expect(frame).toContain('Feature Flags'); expect(frame).toContain('Session Recording'); expect(frame).toContain('Event Tracking'); - expect(frame).toContain('YOLO'); - expect(frame).toContain('Skip setup'); + expect(frame).toContain('space'); + expect(frame).toContain('toggle'); }); }); @@ -72,7 +73,6 @@ describe('SelectGoalScreen', () => { const frame = sut.lastFrame()!; expect(frame).toContain('Feature Flags'); expect(frame).toContain('Event Tracking'); - expect(frame).toContain('Skip setup'); expect(frame).not.toContain('Session Recording'); }); }); @@ -90,6 +90,7 @@ describe('SelectGoalScreen', () => { expect(sut.lastFrame()).toContain('Feature Flags'); }); + await act(() => sut.stdin.write(SPACE)); await act(() => sut.stdin.write(ENTER)); await waitFor(() => { @@ -98,7 +99,7 @@ describe('SelectGoalScreen', () => { }); }); - it('advances to Done when skip is selected', async () => { + it('advances to Done when submitting with nothing selected', async () => { using project = createProjectDir(); using sut = renderApp({ @@ -108,11 +109,10 @@ describe('SelectGoalScreen', () => { }); await waitFor(() => { - expect(sut.lastFrame()).toContain('Skip setup'); + expect(sut.lastFrame()).toContain('Feature Flags'); }); - // Skip is the 5th option for browser projects (flags, events, recordings, yolo, skip) - await act(() => sut.stdin.write(ARROW_DOWN.repeat(4) + ENTER)); + await act(() => sut.stdin.write(ENTER)); await waitFor(() => { expect(sut.lastFrame()).toContain('Onboarding skipped'); @@ -132,8 +132,8 @@ describe('SelectGoalScreen', () => { expect(sut.lastFrame()).toContain('Event Tracking'); }); - // Event Tracking is the 2nd option for browser projects - await act(() => sut.stdin.write(ARROW_DOWN + ENTER)); + await act(() => sut.stdin.write(ARROW_DOWN + SPACE)); + await act(() => sut.stdin.write(ENTER)); await waitFor(() => { expect(sut.lastFrame()).toContain('Ready to start?'); diff --git a/__tests__/ui/testing-framework/index.ts b/__tests__/ui/testing-framework/index.ts index b32bd1d..2cf0cce 100644 --- a/__tests__/ui/testing-framework/index.ts +++ b/__tests__/ui/testing-framework/index.ts @@ -21,3 +21,5 @@ export const ARROW_UP = KEY_MAP.ArrowUp; export const ENTER = KEY_MAP.Enter; /** @see {@link KEY_MAP.Escape} */ export const ESCAPE = KEY_MAP.Escape; +/** @see {@link KEY_MAP.Space} */ +export const SPACE = KEY_MAP.Space; diff --git a/__tests__/ui/testing-framework/ink/render.tsx b/__tests__/ui/testing-framework/ink/render.tsx index 6426684..603e251 100644 --- a/__tests__/ui/testing-framework/ink/render.tsx +++ b/__tests__/ui/testing-framework/ink/render.tsx @@ -25,7 +25,7 @@ type RenderOptions = StoreOptions & { framework?: string; - goal?: OnboardingGoal; + goals?: OnboardingGoal[]; }; export function renderScreen(element: ReactElement, opts?: RenderOptions) { @@ -61,5 +61,5 @@ function setupStore(opts: RenderOptions = {}): void { if (opts.plugins) store.setInstalledPlugins(opts.plugins); if (opts.framework) store.setFramework(opts.framework); if (opts.authState) store.setAuthState(opts.authState); - if (opts.goal) store.setOnboardingGoal(opts.goal); + if (opts.goals) store.setOnboardingGoals(opts.goals); } diff --git a/src/lib/onboarding-prompt/index.ts b/src/lib/onboarding-prompt/index.ts index 42e7675..a942eda 100644 --- a/src/lib/onboarding-prompt/index.ts +++ b/src/lib/onboarding-prompt/index.ts @@ -13,7 +13,7 @@ type PromptOptions = { projectDir: string; ide?: ChosenIde; isEmptyProject?: boolean; - goal?: OnboardingGoal; + goals?: OnboardingGoal[]; hasPlugins?: boolean; hasProviders?: boolean; }; @@ -23,20 +23,20 @@ export function buildOnboardingPrompt({ projectDir, ide = 'claude', isEmptyProject = false, - goal = 'feature-flags', + goals = ['feature-flags'], hasPlugins = false, hasProviders = false, }: PromptOptions): string { const steps = new StepCounter(isEmptyProject ? 2 : 1); const tools = buildToolVars(ide); - const withFlags = ['feature-flags', 'all'].includes(goal); - const withRecordings = ['session-recordings', 'all'].includes(goal); - const withEventTracking = ['event-tracking', 'all'].includes(goal); + const withFlags = goals.includes('feature-flags'); + const withRecordings = goals.includes('session-recordings'); + const withEventTracking = goals.includes('event-tracking'); const viaSkill = withFlags && hasPlugins; const sections = [ - preamble(framework, projectDir, isEmptyProject, goal), + preamble(framework, projectDir, isEmptyProject, goals), preflight(tools), addIf(isEmptyProject, () => scaffold(framework, steps.next())), @@ -58,7 +58,7 @@ export function buildOnboardingPrompt({ generateReport({ step: steps.next(), isEmptyProject, - goal, + goals, hasPlugins, hasProviders, }), @@ -79,21 +79,26 @@ class StepCounter { } } -const GOAL_PREAMBLE: Record = { +const GOAL_LABELS: Record = { 'feature-flags': 'the Confidence SDK', 'session-recordings': 'Confidence Session Recording', 'event-tracking': 'Confidence Event Tracking', - all: 'the Confidence SDK, Session Recording, and Event Tracking', }; +function goalPreamble(goals: OnboardingGoal[]): string { + const labels = goals.map((g) => GOAL_LABELS[g]); + if (labels.length <= 2) return labels.join(' and '); + return labels.slice(0, -1).join(', ') + ', and ' + labels.at(-1); +} + function preamble( framework: string, projectDir: string, isEmptyProject: boolean, - goal: OnboardingGoal, + goals: OnboardingGoal[], ): string { const action = isEmptyProject ? 'Generate a sample app and integrate' : 'Integrate'; return `\ -${action} ${GOAL_PREAMBLE[goal]} into a ${framework} project at ${projectDir}. +${action} ${goalPreamble(goals)} into a ${framework} project at ${projectDir}. Follow these steps in order, printing a short status line before each one.`; } diff --git a/src/lib/onboarding-prompt/report-templates.ts b/src/lib/onboarding-prompt/report-templates.ts index b9b473d..e1753ff 100644 --- a/src/lib/onboarding-prompt/report-templates.ts +++ b/src/lib/onboarding-prompt/report-templates.ts @@ -2,103 +2,51 @@ import type { OnboardingGoal } from '../session.js'; export type ReportTemplate = { start: string; end: string }; -export function buildReportTemplate(goal: OnboardingGoal): ReportTemplate { - switch (goal) { - case 'feature-flags': - return { start: FLAGS_TEMPLATE_START, end: COMMON_TEMPLATE_END }; - - case 'session-recordings': - return { start: RECORDING_TEMPLATE_START, end: COMMON_TEMPLATE_END }; - - case 'event-tracking': - return { start: EVENT_TRACKING_TEMPLATE_START, end: COMMON_TEMPLATE_END }; - - case 'all': - return { start: ALL_TEMPLATE_START, end: COMMON_TEMPLATE_END }; - - default: { - const _exhaustive: never = goal satisfies never; - throw new Error(`Unhandled goal: ${_exhaustive}`); - } - } +export function buildReportTemplate(goals: OnboardingGoal[]): ReportTemplate { + return { start: buildTemplateStart(goals), end: TEMPLATE_END }; } -const FLAGS_TEMPLATE_START = `\ -\`\`\`markdown -# Confidence Quickstart Report - -## What was created in Confidence - -| | | -|---|---| -| Client | | -| Flag | | -| Variants | | -| Default | (100% allocation) | - -## What changed in your codebase - -**New/modified files:** - -- \`<.env file>\` — added \`CONFIDENCE_CLIENT_SECRET\` -- \`\` — added SDK initialization -- \`\` — added flag evaluation - - -**New dependencies:** - -- \`\``; - -const RECORDING_TEMPLATE_START = `\ -\`\`\`markdown -# Confidence Quickstart Report - -## What was set up - -| | | -|---|---| -| Integration | Session Recording | -| Client | | - -## What changed in your codebase - -**New/modified files:** - -- \`<.env file>\` — added \`CONFIDENCE_CLIENT_SECRET\` -- \`\` — added session recording provider - - -**New dependencies:** - -- \`\``; - -const EVENT_TRACKING_TEMPLATE_START = `\ -\`\`\`markdown -# Confidence Quickstart Report - -## What was set up - -| | | -|---|---| -| Integration | Event Tracking | -| Client | | -| Event Definitions | created | -| Fact Tables | auto-created | - -## What changed in your codebase - -**New/modified files:** +function buildTemplateStart(goals: OnboardingGoal[]): string { + const tableRows: string[] = []; + const fileEntries: string[] = []; + const deps: string[] = []; + + if (goals.includes('feature-flags')) { + tableRows.push( + '| Client | |', + '| Flag | |', + '| Variants | |', + '| Default | (100% allocation) |', + ); + fileEntries.push( + '- `<.env file>` — added `CONFIDENCE_CLIENT_SECRET`', + '- `` — added SDK initialization', + '- `` — added flag evaluation', + ); + deps.push('- ``'); + } -- \`<.env file>\` — added \`CONFIDENCE_CLIENT_SECRET\` -- \`\` — added SDK initialization -- \`\` — added event tracking calls - + if (goals.includes('session-recordings')) { + tableRows.push('| Session Recording | Enabled |'); + fileEntries.push('- `` — added session recording provider'); + deps.push('- ``'); + } -**New dependencies:** + if (goals.includes('event-tracking')) { + tableRows.push( + '| Event Definitions | created |', + '| Fact Tables | auto-created |', + ); + fileEntries.push('- `` — added event tracking calls'); + deps.push('- ``'); + } -- \`\``; + if (!goals.includes('feature-flags')) { + fileEntries.unshift('- `<.env file>` — added `CONFIDENCE_CLIENT_SECRET`'); + fileEntries.push('- `` — added SDK initialization'); + } -const ALL_TEMPLATE_START = `\ + return `\ \`\`\`markdown # Confidence Quickstart Report @@ -106,30 +54,21 @@ const ALL_TEMPLATE_START = `\ | | | |---|---| -| Client | | -| Flag | | -| Variants | | -| Default | (100% allocation) | -| Session Recording | Enabled | -| Event Definitions | created | -| Fact Tables | auto-created | +${tableRows.join('\n')} ## What changed in your codebase **New/modified files:** -- \`<.env file>\` — added \`CONFIDENCE_CLIENT_SECRET\` -- \`\` — added SDK initialization, session recording provider, and event tracking -- \`\` — added flag evaluation -- \`\` — added event tracking calls +${fileEntries.join('\n')} **New dependencies:** -- \`\` -- \`\``; +${deps.join('\n')}`; +} -const COMMON_TEMPLATE_END = `\ +const TEMPLATE_END = `\ ## How to use it - Manage your setup at https://app.confidence.spotify.com diff --git a/src/lib/onboarding-prompt/report.ts b/src/lib/onboarding-prompt/report.ts index cf95c47..e1fe9fb 100644 --- a/src/lib/onboarding-prompt/report.ts +++ b/src/lib/onboarding-prompt/report.ts @@ -21,17 +21,17 @@ function buildSkillsNote(hasProviders: boolean): string { export function generateReport({ step, isEmptyProject, - goal = 'feature-flags', + goals = ['feature-flags'], hasPlugins = false, hasProviders = false, }: { step: number; isEmptyProject: boolean; - goal?: OnboardingGoal; + goals?: OnboardingGoal[]; hasPlugins?: boolean; hasProviders?: boolean; }): string { - const template = buildReportTemplate(goal); + const template = buildReportTemplate(goals); return loadStep('generate-report.md', { STEP: step, diff --git a/src/lib/session.ts b/src/lib/session.ts index fe037fb..dbc52d9 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -3,7 +3,7 @@ import type { DetectedProvider } from '@providers/types.js'; export type ChosenIde = 'claude' | 'cursor' | 'codex'; -export type OnboardingGoal = 'feature-flags' | 'session-recordings' | 'event-tracking' | 'all'; +export type OnboardingGoal = 'feature-flags' | 'session-recordings' | 'event-tracking'; export type FrameworkSource = 'detected' | 'selected'; @@ -30,7 +30,7 @@ export type WizardSession = { connectedMcps: string[]; isEmptyProject: boolean; detectedProviders: DetectedProvider[]; - onboardingGoal: OnboardingGoal | null; + onboardingGoals: OnboardingGoal[]; onboardingStatus: string; reportFile: string | null; codeChanges: string[]; @@ -86,7 +86,7 @@ export function createSession(opts?: { connectedMcps: [], isEmptyProject: false, detectedProviders: [], - onboardingGoal: null, + onboardingGoals: [], onboardingStatus: '', reportFile: null, codeChanges: [], diff --git a/src/ui/tui/screens/onboard-project/OnboardProjectScreen.tsx b/src/ui/tui/screens/onboard-project/OnboardProjectScreen.tsx index afdd122..e8b99ac 100644 --- a/src/ui/tui/screens/onboard-project/OnboardProjectScreen.tsx +++ b/src/ui/tui/screens/onboard-project/OnboardProjectScreen.tsx @@ -4,7 +4,7 @@ import { buildWizardTasks } from '../../lib/wizard-tasks.js'; import { useTipRotation } from '../../hooks/useTipRotation.js'; import { useTerminalSize } from '../../hooks/useTerminalSize.js'; import { tipsFitInViewport } from '../../lib/layout-budget.js'; -import { ScreenId, type OnboardingGoal } from '@lib/session.js'; +import { ScreenId } from '@lib/session.js'; import { useAutoAdvance } from '../../hooks/useAutoAdvance.js'; import { useLogger } from '../../hooks/useLog.js'; import { useNavigation } from '../../hooks/useNavigation.js'; @@ -20,8 +20,6 @@ import { MAX_VISIBLE_STATUS, } from './components/index.js'; -const DEFAULT_GOAL: OnboardingGoal = 'feature-flags'; - export function OnboardProjectScreen() { const session = useSession(); const navigate = useNavigation(ScreenId.OnboardProject); @@ -82,7 +80,7 @@ export function OnboardProjectScreen() { phase={onboarding.phase} statusLines={onboarding.statusLines} error={onboarding.error} - goal={session.onboardingGoal ?? DEFAULT_GOAL} + goals={session.onboardingGoals} showTips={showTips} tip={tip} /> diff --git a/src/ui/tui/screens/onboard-project/components/OnboardingLeftPanel.tsx b/src/ui/tui/screens/onboard-project/components/OnboardingLeftPanel.tsx index 21862f5..181f1ba 100644 --- a/src/ui/tui/screens/onboard-project/components/OnboardingLeftPanel.tsx +++ b/src/ui/tui/screens/onboard-project/components/OnboardingLeftPanel.tsx @@ -16,7 +16,7 @@ type OnboardingLeftPanelProps = { phase: OnboardingPhase; statusLines: StatusLine[]; error: string | null; - goal: OnboardingGoal; + goals: OnboardingGoal[]; showTips: boolean; tip: Tip; }; @@ -25,7 +25,7 @@ export function OnboardingLeftPanel({ phase, statusLines, error, - goal, + goals, showTips, tip, }: OnboardingLeftPanelProps) { @@ -41,7 +41,7 @@ export function OnboardingLeftPanel({ <> The wizard will: - {onboardingSteps(goal).map((step) => ( + {onboardingSteps(goals).map((step) => ( {Icons.check} {step} @@ -91,13 +91,8 @@ const GOAL_STEPS: Record = { 'feature-flags': ['create your first feature flag'], 'session-recordings': ['set up session recordings to capture user sessions'], 'event-tracking': ['instrument event tracking to measure user behavior'], - all: [ - 'set up feature flags', - 'set up session recordings to capture user sessions', - 'instrument event tracking to measure user behavior', - ], }; -function onboardingSteps(goal: OnboardingGoal): string[] { - return ['add the Confidence SDK', ...GOAL_STEPS[goal]]; +function onboardingSteps(goals: OnboardingGoal[]): string[] { + return ['add the Confidence SDK', ...goals.flatMap((g) => GOAL_STEPS[g])]; } diff --git a/src/ui/tui/screens/onboard-project/useOnboardingProcess.ts b/src/ui/tui/screens/onboard-project/useOnboardingProcess.ts index 3be5a45..d94457d 100644 --- a/src/ui/tui/screens/onboard-project/useOnboardingProcess.ts +++ b/src/ui/tui/screens/onboard-project/useOnboardingProcess.ts @@ -53,7 +53,7 @@ export function useOnboardingProcess(): OnboardingProcess { }, []); const markComplete = useCallback( - function markComplete(goal: OnboardingGoal, lines?: string[]) { + function markComplete(goals: OnboardingGoal[], lines?: string[]) { addStatus('', 'blank'); addStatus('Project onboarding complete!', 'success'); store.setReportFile('CONFIDENCE_QUICKSTART.md'); @@ -65,7 +65,7 @@ export function useOnboardingProcess(): OnboardingProcess { line.includes('Created') || line.includes('Modified') || line.includes('Added'), ) .map(normalizeStatusLine) - : dryRunCodeChanges(goal), + : dryRunCodeChanges(goals), ); setPhase('done'); }, @@ -76,7 +76,7 @@ export function useOnboardingProcess(): OnboardingProcess { function start(fwName: string | null) { const s = $session.get(); const isEmpty = s.isEmptyProject; - const goal = s.onboardingGoal ?? 'feature-flags'; + const goals = s.onboardingGoals; const fw = fwName ?? 'React'; setPhase('onboarding'); addStatus(isEmpty ? 'Scaffolding sample app...' : 'Setting up Confidence...'); @@ -86,7 +86,7 @@ export function useOnboardingProcess(): OnboardingProcess { function startDryRun() { log(onboardingDryRun(fw)); - const steps = buildDryRunSteps(fw, isEmpty, goal); + const steps = buildDryRunSteps(fw, isEmpty, goals); let i = 0; const interval = setInterval(() => { @@ -96,7 +96,7 @@ export function useOnboardingProcess(): OnboardingProcess { i++; } else { clearInterval(interval); - markComplete(goal); + markComplete(goals); } }, 3000); } @@ -116,7 +116,7 @@ export function useOnboardingProcess(): OnboardingProcess { projectDir: s.projectDir, ide, isEmptyProject: isEmpty, - goal, + goals, hasPlugins: s.installedPlugins.length > 0, hasProviders: s.detectedProviders.length > 0, }); @@ -139,7 +139,7 @@ export function useOnboardingProcess(): OnboardingProcess { onComplete(lines) { childRef.current = null; log(onboardingExitSuccess(lines.length)); - markComplete(goal, lines); + markComplete(goals, lines); }, onError(message) { childRef.current = null; @@ -222,58 +222,52 @@ export function useOnboardingProcess(): OnboardingProcess { }; } -function buildDryRunSteps(fw: string, isEmpty: boolean, goal: OnboardingGoal): string[] { - const detect = isEmpty ? ['Scaffolding sample app...'] : [`Detected framework: ${fw}`]; - const flagSteps = [ +const DRY_RUN_STEPS: Record = { + 'feature-flags': [ 'Determining appropriate SDK...', 'Installing @spotify-confidence/sdk...', 'Generating Confidence configuration...', 'Creating feature flag example...', - ]; - const recordingSteps = [ + ], + 'session-recordings': [ 'Installing session recording SDK...', 'Adding session recording provider...', 'Configuring privacy settings...', - ]; - const eventTrackingSteps = [ + ], + 'event-tracking': [ 'Scanning for existing event tracking...', 'Identifying trackable events...', 'Creating event definitions...', 'Adding track() calls...', 'Verifying event pipeline...', - ]; - - const goalSteps: Record = { - 'feature-flags': flagSteps, - 'session-recordings': recordingSteps, - 'event-tracking': eventTrackingSteps, - all: [...flagSteps, ...recordingSteps, ...eventTrackingSteps], - }; + ], +}; - return [...detect, ...goalSteps[goal], 'Generating CONFIDENCE_QUICKSTART.md report...']; +function buildDryRunSteps(fw: string, isEmpty: boolean, goals: OnboardingGoal[]): string[] { + const detect = isEmpty ? ['Scaffolding sample app...'] : [`Detected framework: ${fw}`]; + return [ + ...detect, + ...goals.flatMap((g) => DRY_RUN_STEPS[g]), + 'Generating CONFIDENCE_QUICKSTART.md report...', + ]; } -function dryRunCodeChanges(goal: OnboardingGoal): string[] { - const flagChanges = [ +const DRY_RUN_CODE_CHANGES: Record = { + 'feature-flags': [ 'Added @spotify-confidence/sdk dependency', 'Created confidence.config.ts', 'Added feature flag example', - ]; - const recordingChanges = [ + ], + 'session-recordings': [ 'Added @spotify-confidence/session-recording dependency', 'Added session recording provider', - ]; - const eventTrackingChanges = [ + ], + 'event-tracking': [ 'Created event definitions with entity references', 'Added confidence.track() calls', - ]; - - const goalChanges: Record = { - 'feature-flags': flagChanges, - 'session-recordings': recordingChanges, - 'event-tracking': eventTrackingChanges, - all: [...flagChanges, ...recordingChanges, ...eventTrackingChanges], - }; + ], +}; - return goalChanges[goal]; +function dryRunCodeChanges(goals: OnboardingGoal[]): string[] { + return goals.flatMap((g) => DRY_RUN_CODE_CHANGES[g]); } diff --git a/src/ui/tui/screens/select-goal/actions.ts b/src/ui/tui/screens/select-goal/actions.ts index b16d959..1030700 100644 --- a/src/ui/tui/screens/select-goal/actions.ts +++ b/src/ui/tui/screens/select-goal/actions.ts @@ -1,24 +1,22 @@ import type { OnboardingGoal } from '@lib/session.js'; import type { PromptOption } from '../../components/PromptPanel.js'; -export type GoalValue = OnboardingGoal | 'skip'; - -const BASE_GOAL_OPTIONS: PromptOption[] = [ +const BASE_GOALS: PromptOption[] = [ { label: 'Feature Flags', value: 'feature-flags' }, { label: 'Event Tracking', value: 'event-tracking' }, - { label: 'YOLO! Set up everything', value: 'all' }, - { label: 'Skip setup', value: 'skip' }, ]; -const WITH_RECORDINGS: PromptOption[] = BASE_GOAL_OPTIONS.toSpliced(2, 0, { +const RECORDINGS_GOAL: PromptOption = { label: 'Session Recordings (β)', value: 'session-recordings', -} as PromptOption); +}; + +const GOAL_OPTIONS = [...BASE_GOALS, RECORDINGS_GOAL]; -export function goalOptionsFor(recordingAvailable: boolean): PromptOption[] { - return recordingAvailable ? WITH_RECORDINGS : BASE_GOAL_OPTIONS; +export function goalOptionsFor(recordingAvailable: boolean): PromptOption[] { + return recordingAvailable ? GOAL_OPTIONS : BASE_GOALS; } export function goalLabel(goal: OnboardingGoal): string { - return BASE_GOAL_OPTIONS.find((o) => o.value === goal)?.label ?? goal; + return GOAL_OPTIONS.find((o) => o.value === goal)?.label ?? goal; } diff --git a/src/ui/tui/screens/select-goal/components/BottomPrompt.tsx b/src/ui/tui/screens/select-goal/components/BottomPrompt.tsx index 013ec08..e2463b1 100644 --- a/src/ui/tui/screens/select-goal/components/BottomPrompt.tsx +++ b/src/ui/tui/screens/select-goal/components/BottomPrompt.tsx @@ -11,10 +11,10 @@ export function BottomPrompt({ goalSelection }: GoalSelectionProps) { case 'select-goal': return ( ); case 'done': diff --git a/src/ui/tui/screens/select-goal/components/LeftPanel.tsx b/src/ui/tui/screens/select-goal/components/LeftPanel.tsx index e37874a..e61d4f5 100644 --- a/src/ui/tui/screens/select-goal/components/LeftPanel.tsx +++ b/src/ui/tui/screens/select-goal/components/LeftPanel.tsx @@ -12,7 +12,7 @@ export function LeftPanel({ goalSelection }: GoalSelectionProps) { {goalSelection.phase === 'select-goal' - ? 'Which features would you like to set up?' + ? "Select the features you'd like to set up" : 'Set up your project'} @@ -20,7 +20,7 @@ export function LeftPanel({ goalSelection }: GoalSelectionProps) { {goalSelection.phase === 'select-goal' && ( - Choose the Confidence features to integrate with your project. + Toggle Confidence features to integrate with your project. diff --git a/src/ui/tui/screens/select-goal/log-messages.ts b/src/ui/tui/screens/select-goal/log-messages.ts index 07b7443..c20d185 100644 --- a/src/ui/tui/screens/select-goal/log-messages.ts +++ b/src/ui/tui/screens/select-goal/log-messages.ts @@ -1,5 +1,5 @@ import type { LogMessage } from '../../lib/log-messages.js'; -export function goalChosen(goal: string): LogMessage { - return { input: 'Select goal', output: goal }; +export function goalsChosen(goals: string): LogMessage { + return { input: 'Select goals', output: goals }; } diff --git a/src/ui/tui/screens/select-goal/telemetry-events.ts b/src/ui/tui/screens/select-goal/telemetry-events.ts index fd8a3a9..b8fb1ae 100644 --- a/src/ui/tui/screens/select-goal/telemetry-events.ts +++ b/src/ui/tui/screens/select-goal/telemetry-events.ts @@ -1,7 +1,7 @@ import type { TelemetryEvent } from '@lib/telemetry.js'; -export function goalSelected(goal: string): TelemetryEvent { - return { step: 'select-goal.select', action: goal }; +export function goalsSelected(goals: string[]): TelemetryEvent { + return { step: 'select-goal.select', action: goals.join(',') }; } export function goalSkipped(): TelemetryEvent { diff --git a/src/ui/tui/screens/select-goal/useGoalSelection.ts b/src/ui/tui/screens/select-goal/useGoalSelection.ts index 01259a8..a81cd9a 100644 --- a/src/ui/tui/screens/select-goal/useGoalSelection.ts +++ b/src/ui/tui/screens/select-goal/useGoalSelection.ts @@ -1,3 +1,4 @@ +import type { OnboardingGoal } from '@lib/session.js'; import { ScreenId } from '@lib/session.js'; import { useNavigation } from '../../hooks/useNavigation.js'; import { useLogger } from '../../hooks/useLog.js'; @@ -5,14 +6,14 @@ import { store } from '../../store.js'; import { track } from '@lib/telemetry.js'; import { skipped } from '../../lib/log-messages.js'; import { useInitialGoalSelection, type Phase } from './useInitialGoalSelection.js'; -import { goalChosen } from './log-messages.js'; +import { goalsChosen } from './log-messages.js'; import * as te from './telemetry-events.js'; -import { goalLabel, type GoalValue } from './actions.js'; +import { goalLabel } from './actions.js'; export type GoalSelection = { phase: Phase; recordingAvailable: boolean; - selectGoal: (value: GoalValue) => void; + submitGoals: (values: OnboardingGoal[]) => void; }; export function useGoalSelection(): GoalSelection { @@ -20,24 +21,23 @@ export function useGoalSelection(): GoalSelection { const log = useLogger(ScreenId.SelectGoal); const initial = useInitialGoalSelection(); - function selectGoal(value: GoalValue) { - if (value === 'skip') { + function submitGoals(values: OnboardingGoal[]) { + if (values.length === 0) { track(te.goalSkipped()); log(skipped()); navigate.to('skip'); return; } - const goal = value; - store.setOnboardingGoal(goal); - track(te.goalSelected(goal)); - log(goalChosen(goalLabel(goal))); + store.setOnboardingGoals(values); + track(te.goalsSelected(values)); + log(goalsChosen(values.map(goalLabel).join(', '))); navigate.to('next'); } return { phase: initial.phase, recordingAvailable: initial.supportsRecordings, - selectGoal, + submitGoals, }; } diff --git a/src/ui/tui/store.ts b/src/ui/tui/store.ts index 3cceaed..614ddbb 100644 --- a/src/ui/tui/store.ts +++ b/src/ui/tui/store.ts @@ -99,9 +99,9 @@ export const store = { detectedProviders: providers, }), - setOnboardingGoal: (goal: OnboardingGoal): void => + setOnboardingGoals: (goals: OnboardingGoal[]): void => updateSession({ - onboardingGoal: goal, + onboardingGoals: goals, }), setOnboardingStatus: (status: string): void => From 9157fe7a44ec596be327d8123a5d42d16eef1ca9 Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 20 Aug 2026 14:30:48 +0200 Subject: [PATCH 5/9] refactor: reduce duplication --- src/lib/onboarding-prompt/event-tracking.ts | 9 ++------- src/lib/onboarding-prompt/integrate.ts | 9 ++------- src/lib/onboarding-prompt/tool-vars.ts | 10 ++++++++++ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/lib/onboarding-prompt/event-tracking.ts b/src/lib/onboarding-prompt/event-tracking.ts index 5d12ffe..a97e2b8 100644 --- a/src/lib/onboarding-prompt/event-tracking.ts +++ b/src/lib/onboarding-prompt/event-tracking.ts @@ -1,11 +1,6 @@ import type { ChosenIde } from '../session.js'; import { loadStep } from './steps/load.js'; - -const SKILLS_DIR: Record = { - claude: '.claude/skills', - cursor: '.cursor/skills', - codex: '.agents/skills', -}; +import { skillsDir } from './tool-vars.js'; export function instrumentEvents( framework: string, @@ -16,7 +11,7 @@ export function instrumentEvents( return loadStep('instrument-events.md', { STEP: step, FRAMEWORK: framework, - SKILLS_DIR: SKILLS_DIR[ide], + SKILLS_DIR: skillsDir(ide), DOMAIN_CONTEXT: isEmptyProject ? "The project was just scaffolded — treat the sample app's features as the domain." : "Study the project's codebase to understand its domain, UI flows, and business logic.", diff --git a/src/lib/onboarding-prompt/integrate.ts b/src/lib/onboarding-prompt/integrate.ts index 9509a53..e6952f3 100644 --- a/src/lib/onboarding-prompt/integrate.ts +++ b/src/lib/onboarding-prompt/integrate.ts @@ -1,12 +1,7 @@ import type { ChosenIde } from '../session.js'; import { CONFIDENCE_DOCS_URL } from '../constants.js'; import { loadStep } from './steps/load.js'; - -const SKILLS_DIR: Record = { - claude: '.claude/skills', - cursor: '.cursor/skills', - codex: '.agents/skills', -}; +import { skillsDir } from './tool-vars.js'; export function integrateViaSkill( framework: string, @@ -19,7 +14,7 @@ export function integrateViaSkill( return loadStep('integrate-via-skill.md', { STEP: step, FRAMEWORK: framework, - SKILLS_DIR: SKILLS_DIR[ide], + SKILLS_DIR: skillsDir(ide), DOMAIN_CONTEXT: isEmptyProject ? "The project was just scaffolded — treat the sample app's features as the domain." diff --git a/src/lib/onboarding-prompt/tool-vars.ts b/src/lib/onboarding-prompt/tool-vars.ts index 111fbff..4e9ba7b 100644 --- a/src/lib/onboarding-prompt/tool-vars.ts +++ b/src/lib/onboarding-prompt/tool-vars.ts @@ -8,6 +8,16 @@ const TOOL_FORMATTERS: Record = { cursor: (server, tool) => `mcp__${server}__${tool}`, }; +const SKILLS_DIRS: Record = { + claude: '.claude/skills', + cursor: '.cursor/skills', + codex: '.agents/skills', +}; + +export function skillsDir(ide: ChosenIde): string { + return SKILLS_DIRS[ide]; +} + export function buildToolVars(ide: ChosenIde): Record { const fmt = TOOL_FORMATTERS[ide]; const flags = (tool: string) => fmt('confidence-flags', tool); From a7901bb25669d82073f5728020724d36d05bf70d Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 20 Aug 2026 16:15:45 +0200 Subject: [PATCH 6/9] chore: remove dead phasing --- .../select-goal/components/BottomPrompt.tsx | 25 +++++--------- .../select-goal/components/LeftPanel.tsx | 34 ++++++++----------- .../screens/select-goal/useGoalSelection.ts | 12 +++---- .../select-goal/useInitialGoalSelection.ts | 22 ------------ 4 files changed, 29 insertions(+), 64 deletions(-) delete mode 100644 src/ui/tui/screens/select-goal/useInitialGoalSelection.ts diff --git a/src/ui/tui/screens/select-goal/components/BottomPrompt.tsx b/src/ui/tui/screens/select-goal/components/BottomPrompt.tsx index e2463b1..a124832 100644 --- a/src/ui/tui/screens/select-goal/components/BottomPrompt.tsx +++ b/src/ui/tui/screens/select-goal/components/BottomPrompt.tsx @@ -7,21 +7,12 @@ type GoalSelectionProps = { }; export function BottomPrompt({ goalSelection }: GoalSelectionProps) { - switch (goalSelection.phase) { - case 'select-goal': - return ( - - ); - case 'done': - return null; - default: { - const _exhaustive: never = goalSelection.phase satisfies never; - throw new Error(`Unhandled phase: ${_exhaustive}`); - } - } + return ( + + ); } diff --git a/src/ui/tui/screens/select-goal/components/LeftPanel.tsx b/src/ui/tui/screens/select-goal/components/LeftPanel.tsx index e61d4f5..c3c4858 100644 --- a/src/ui/tui/screens/select-goal/components/LeftPanel.tsx +++ b/src/ui/tui/screens/select-goal/components/LeftPanel.tsx @@ -11,33 +11,29 @@ export function LeftPanel({ goalSelection }: GoalSelectionProps) { - {goalSelection.phase === 'select-goal' - ? "Select the features you'd like to set up" - : 'Set up your project'} + Select the features you'd like to set up - {goalSelection.phase === 'select-goal' && ( - - - Toggle Confidence features to integrate with your project. + + + Toggle Confidence features to integrate with your project. + + + + {Icons.star} Event Tracking works best with a managed warehouse or an existing warehouse + setup. + + {goalSelection.recordingAvailable && ( - {Icons.star} Event Tracking works best with a managed warehouse or an existing - warehouse setup. + {Icons.star} Session Recordings (β) require the feature to be enabled on your + Confidence account. - {goalSelection.recordingAvailable && ( - - - {Icons.star} Session Recordings (β) require the feature to be enabled on your - Confidence account. - - - )} - - )} + )} + ); } diff --git a/src/ui/tui/screens/select-goal/useGoalSelection.ts b/src/ui/tui/screens/select-goal/useGoalSelection.ts index a81cd9a..01ab128 100644 --- a/src/ui/tui/screens/select-goal/useGoalSelection.ts +++ b/src/ui/tui/screens/select-goal/useGoalSelection.ts @@ -1,25 +1,26 @@ import type { OnboardingGoal } from '@lib/session.js'; import { ScreenId } from '@lib/session.js'; +import { BROWSER_PLATFORMS } from '@lib/sdk-options.js'; import { useNavigation } from '../../hooks/useNavigation.js'; import { useLogger } from '../../hooks/useLog.js'; -import { store } from '../../store.js'; +import { store, useSession } from '../../store.js'; import { track } from '@lib/telemetry.js'; import { skipped } from '../../lib/log-messages.js'; -import { useInitialGoalSelection, type Phase } from './useInitialGoalSelection.js'; import { goalsChosen } from './log-messages.js'; import * as te from './telemetry-events.js'; import { goalLabel } from './actions.js'; export type GoalSelection = { - phase: Phase; recordingAvailable: boolean; submitGoals: (values: OnboardingGoal[]) => void; }; export function useGoalSelection(): GoalSelection { + const session = useSession(); const navigate = useNavigation(ScreenId.SelectGoal); const log = useLogger(ScreenId.SelectGoal); - const initial = useInitialGoalSelection(); + + const recordingAvailable = !!session.framework && BROWSER_PLATFORMS.has(session.framework); function submitGoals(values: OnboardingGoal[]) { if (values.length === 0) { @@ -36,8 +37,7 @@ export function useGoalSelection(): GoalSelection { } return { - phase: initial.phase, - recordingAvailable: initial.supportsRecordings, + recordingAvailable, submitGoals, }; } diff --git a/src/ui/tui/screens/select-goal/useInitialGoalSelection.ts b/src/ui/tui/screens/select-goal/useInitialGoalSelection.ts deleted file mode 100644 index 1800797..0000000 --- a/src/ui/tui/screens/select-goal/useInitialGoalSelection.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useState } from 'react'; -import { useSession } from '../../store.js'; -import type { WizardSession } from '@lib/session.js'; -import { BROWSER_PLATFORMS } from '@lib/sdk-options.js'; - -export type Phase = 'select-goal' | 'done'; - -type InitialGoalSelection = { - phase: Phase; - supportsRecordings: boolean; -}; - -export function useInitialGoalSelection(): InitialGoalSelection { - const session = useSession(); - const [resolved] = useState(() => resolveInitialGoalSelection(session)); - return resolved; -} - -function resolveInitialGoalSelection(session: WizardSession): InitialGoalSelection { - const supportsRecordings = !!session.framework && BROWSER_PLATFORMS.has(session.framework); - return { phase: 'select-goal', supportsRecordings }; -} From 747aa50abe3d5dd2d7a0e9626a5b2eace697785e Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 20 Aug 2026 16:16:20 +0200 Subject: [PATCH 7/9] chore: clarify prompt template builder --- __tests__/lib/report-templates.test.ts | 111 ++++++++++++++++++ src/lib/onboarding-prompt/report-templates.ts | 42 +++---- 2 files changed, 129 insertions(+), 24 deletions(-) create mode 100644 __tests__/lib/report-templates.test.ts diff --git a/__tests__/lib/report-templates.test.ts b/__tests__/lib/report-templates.test.ts new file mode 100644 index 0000000..b07ece5 --- /dev/null +++ b/__tests__/lib/report-templates.test.ts @@ -0,0 +1,111 @@ +import { buildReportTemplate } from '@lib/onboarding-prompt/report-templates.js'; +import type { OnboardingGoal } from '@lib/session.js'; + +function fileEntries(goals: OnboardingGoal[]): string[] { + const { start } = buildReportTemplate(goals); + const section = start.split('**New/modified files:**')[1]?.split('**New dependencies:**')[0]; + return section + ? section + .split('\n') + .map((l) => l.trim()) + .filter((l) => l.startsWith('- ')) + : []; +} + +function depEntries(goals: OnboardingGoal[]): string[] { + const { start } = buildReportTemplate(goals); + const section = start.split('**New dependencies:**')[1]; + return section + ? section + .split('\n') + .map((l) => l.trim()) + .filter((l) => l.startsWith('- ')) + : []; +} + +describe('when only feature-flags is selected', () => { + const goals: OnboardingGoal[] = ['feature-flags']; + + it('includes .env, entry point with SDK initialization, and flag evaluation', () => { + const sut = fileEntries(goals); + + expect(sut).toEqual([ + '- `<.env file>` — added `CONFIDENCE_CLIENT_SECRET`', + '- `` — added SDK initialization', + '- `` — added flag evaluation', + ]); + }); + + it('lists the feature flags SDK dependency', () => { + const sut = depEntries(goals); + expect(sut).toEqual(['- ``']); + }); +}); + +describe('when only session-recordings is selected', () => { + const goals: OnboardingGoal[] = ['session-recordings']; + + it('includes separate entry point lines for SDK init and session recording', () => { + const sut = fileEntries(goals); + + expect(sut).toEqual([ + '- `<.env file>` — added `CONFIDENCE_CLIENT_SECRET`', + '- `` — added SDK initialization', + '- `` — added session recording provider', + ]); + }); + + it('lists the session recording SDK dependency', () => { + const sut = depEntries(goals); + expect(sut).toEqual(['- ``']); + }); +}); + +describe('when only event-tracking is selected', () => { + const goals: OnboardingGoal[] = ['event-tracking']; + + it('includes .env, entry point, and track() calls without duplicates', () => { + const sut = fileEntries(goals); + + expect(sut).toEqual([ + '- `<.env file>` — added `CONFIDENCE_CLIENT_SECRET`', + '- `` — added SDK initialization', + '- `` — added event tracking calls', + ]); + }); + + it('lists a specific event tracking SDK dependency', () => { + const sut = depEntries(goals); + expect(sut).toEqual(['- ``']); + }); +}); + +describe('when all goals are selected', () => { + const goals: OnboardingGoal[] = ['feature-flags', 'session-recordings', 'event-tracking']; + + it('produces one .env line and no duplicate entry point lines', () => { + const sut = fileEntries(goals); + const envLines = sut.filter((l) => l.includes('.env file')); + const entryPointLines = sut.filter((l) => l.includes('entry point file')); + + expect(envLines).toHaveLength(1); + expect(entryPointLines).toHaveLength(2); + expect(entryPointLines[0]).toContain('SDK initialization'); + expect(entryPointLines[1]).toContain('session recording provider'); + }); + + it('lists separate entry point lines for SDK init and session recording', () => { + const sut = fileEntries(goals); + const entryPointLines = sut.filter((l) => l.includes('entry point file')); + + expect(entryPointLines).toEqual([ + '- `` — added SDK initialization', + '- `` — added session recording provider', + ]); + }); + + it('lists all three SDK dependencies', () => { + const sut = depEntries(goals); + expect(sut).toHaveLength(3); + }); +}); diff --git a/src/lib/onboarding-prompt/report-templates.ts b/src/lib/onboarding-prompt/report-templates.ts index e1753ff..4fc5ac6 100644 --- a/src/lib/onboarding-prompt/report-templates.ts +++ b/src/lib/onboarding-prompt/report-templates.ts @@ -7,43 +7,37 @@ export function buildReportTemplate(goals: OnboardingGoal[]): ReportTemplate { } function buildTemplateStart(goals: OnboardingGoal[]): string { - const tableRows: string[] = []; - const fileEntries: string[] = []; - const deps: string[] = []; + const tableRowEntries: string[] = []; + const dependencyEntries: string[] = []; + const fileChangeEntries = [ + '- `<.env file>` — added `CONFIDENCE_CLIENT_SECRET`', + '- `` — added SDK initialization', + ]; if (goals.includes('feature-flags')) { - tableRows.push( + tableRowEntries.push( '| Client | |', '| Flag | |', '| Variants | |', '| Default | (100% allocation) |', ); - fileEntries.push( - '- `<.env file>` — added `CONFIDENCE_CLIENT_SECRET`', - '- `` — added SDK initialization', - '- `` — added flag evaluation', - ); - deps.push('- ``'); + fileChangeEntries.push('- `` — added flag evaluation'); + dependencyEntries.push('- ``'); } if (goals.includes('session-recordings')) { - tableRows.push('| Session Recording | Enabled |'); - fileEntries.push('- `` — added session recording provider'); - deps.push('- ``'); + tableRowEntries.push('| Session Recording | Enabled |'); + fileChangeEntries.push('- `` — added session recording provider'); + dependencyEntries.push('- ``'); } if (goals.includes('event-tracking')) { - tableRows.push( + tableRowEntries.push( '| Event Definitions | created |', '| Fact Tables | auto-created |', ); - fileEntries.push('- `` — added event tracking calls'); - deps.push('- ``'); - } - - if (!goals.includes('feature-flags')) { - fileEntries.unshift('- `<.env file>` — added `CONFIDENCE_CLIENT_SECRET`'); - fileEntries.push('- `` — added SDK initialization'); + fileChangeEntries.push('- `` — added event tracking calls'); + dependencyEntries.push('- ``'); } return `\ @@ -54,18 +48,18 @@ function buildTemplateStart(goals: OnboardingGoal[]): string { | | | |---|---| -${tableRows.join('\n')} +${tableRowEntries.join('\n')} ## What changed in your codebase **New/modified files:** -${fileEntries.join('\n')} +${fileChangeEntries.join('\n')} **New dependencies:** -${deps.join('\n')}`; +${dependencyEntries.join('\n')}`; } const TEMPLATE_END = `\ From 7d5e76b0724026fbba44ee316ebc892754047fe6 Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 20 Aug 2026 16:20:16 +0200 Subject: [PATCH 8/9] chore: guard against empty goals array --- src/lib/onboarding-prompt/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/onboarding-prompt/index.ts b/src/lib/onboarding-prompt/index.ts index a942eda..3622e58 100644 --- a/src/lib/onboarding-prompt/index.ts +++ b/src/lib/onboarding-prompt/index.ts @@ -87,6 +87,8 @@ const GOAL_LABELS: Record = { function goalPreamble(goals: OnboardingGoal[]): string { const labels = goals.map((g) => GOAL_LABELS[g]); + + if (labels.length === 0) return ''; if (labels.length <= 2) return labels.join(' and '); return labels.slice(0, -1).join(', ') + ', and ' + labels.at(-1); } From 56e5b50819ea3aca2671bb40551965692829afff Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 20 Aug 2026 16:25:31 +0200 Subject: [PATCH 9/9] chore: prettier --- src/ui/tui/screens/select-goal/components/LeftPanel.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ui/tui/screens/select-goal/components/LeftPanel.tsx b/src/ui/tui/screens/select-goal/components/LeftPanel.tsx index c3c4858..3280d33 100644 --- a/src/ui/tui/screens/select-goal/components/LeftPanel.tsx +++ b/src/ui/tui/screens/select-goal/components/LeftPanel.tsx @@ -16,9 +16,7 @@ export function LeftPanel({ goalSelection }: GoalSelectionProps) { - - Toggle Confidence features to integrate with your project. - + Toggle Confidence features to integrate with your project. {Icons.star} Event Tracking works best with a managed warehouse or an existing warehouse