diff --git a/src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts b/src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts index 954d0e3fe30..54edf17210d 100644 --- a/src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts +++ b/src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' import { getDefaultOnboardingState } from '../../../../shared/onboarding-defaults' import { - buildDismissedOnboardingFolderAgentStartup, buildOnboardingFolderAgentStartup, shouldSeedFolderAgentAfterDismissedOnboarding } from '@/lib/onboarding-folder-agent-startup' @@ -111,34 +110,4 @@ describe('buildOnboardingFolderAgentStartup', () => { ) ).toBe(false) }) - - it('builds the skipped-onboarding folder startup from the persisted default agent', () => { - expect( - buildDismissedOnboardingFolderAgentStartup( - { - ...getDefaultSettings('/tmp/orca-workspaces'), - defaultTuiAgent: 'codex', - agentCmdOverrides: { codex: 'echo onboarding-folder-agent' } - }, - { ...getDefaultOnboardingState(), outcome: 'dismissed' }, - false, - HOST - ) - ).toEqual({ - command: "echo onboarding-folder-agent '--dangerously-bypass-approvals-and-sandbox'", - env: {}, - launchAgent: 'codex', - launchConfig: { - agentCommand: "echo onboarding-folder-agent '--dangerously-bypass-approvals-and-sandbox'", - agentArgs: '--dangerously-bypass-approvals-and-sandbox', - agentEnv: {} - }, - sessionOptions: undefined, - telemetry: { - agent_kind: 'codex', - launch_source: 'onboarding', - request_kind: 'new' - } - }) - }) }) diff --git a/src/renderer/src/components/sidebar/NonGitFolderDialog.tsx b/src/renderer/src/components/sidebar/NonGitFolderDialog.tsx index 0801c7b4fa6..7e13f3adea4 100644 --- a/src/renderer/src/components/sidebar/NonGitFolderDialog.tsx +++ b/src/renderer/src/components/sidebar/NonGitFolderDialog.tsx @@ -95,7 +95,7 @@ const NonGitFolderDialog = React.memo(function NonGitFolderDialog() { }) const state = useAppStore.getState() const hadProjectBeforeAdd = stateBeforeAdd.repos.length > 0 - await markOnboardingProjectAdded('addedFolder') + const onboardingBeforeAdd = await markOnboardingProjectAdded('addedFolder') await state.fetchWorktrees(repo.id, ownerOptions) // Why: mirror the local non-git folder flow — without this the // dialog closes and the UI shows no visible change, making the @@ -105,13 +105,12 @@ const NonGitFolderDialog = React.memo(function NonGitFolderDialog() { (worktree) => worktree.hostId === ownerOptions.executionHostId ) if (folderWorktree) { - const onboarding = await window.api.onboarding.get().catch(() => null) // Why: SSH users can hit this dialog from Add Project after // dismissing onboarding, bypassing the local addNonGitFolder path. const launch = resolveDismissedOnboardingFolderAgentLaunch({ worktreeId: folderWorktree.id, store: useAppStore.getState(), - onboarding, + onboarding: onboardingBeforeAdd, hasExistingProject: hadProjectBeforeAdd, executionHostId: ownerOptions.executionHostId ?? connectionId }) diff --git a/src/renderer/src/lib/onboarding-folder-agent-startup.ts b/src/renderer/src/lib/onboarding-folder-agent-startup.ts index 3366f130761..d35c1350ccd 100644 --- a/src/renderer/src/lib/onboarding-folder-agent-startup.ts +++ b/src/renderer/src/lib/onboarding-folder-agent-startup.ts @@ -84,15 +84,3 @@ export function shouldSeedFolderAgentAfterDismissedOnboarding( !onboarding.checklist.addedFolder ) } - -export function buildDismissedOnboardingFolderAgentStartup( - settings: GlobalSettings | null, - onboarding: OnboardingState | null, - hasExistingProject: boolean, - host: OnboardingStartupHost -): OnboardingFolderAgentStartup | undefined { - if (!shouldSeedFolderAgentAfterDismissedOnboarding(onboarding, hasExistingProject)) { - return undefined - } - return buildOnboardingFolderAgentStartup(settings, host) -} diff --git a/src/renderer/src/lib/onboarding-project-checklist.ts b/src/renderer/src/lib/onboarding-project-checklist.ts index 742cd164776..33eb52de034 100644 --- a/src/renderer/src/lib/onboarding-project-checklist.ts +++ b/src/renderer/src/lib/onboarding-project-checklist.ts @@ -3,15 +3,16 @@ import type { OnboardingState } from '../../../shared/onboarding-state-types' export type OnboardingProjectChecklistItem = 'addedRepo' | 'addedFolder' +/** Returns the onboarding state read before marking, so callers can tell a first add from a later one. */ export async function markOnboardingProjectAdded( item: OnboardingProjectChecklistItem -): Promise { +): Promise { if (typeof window === 'undefined' || !window.api?.onboarding) { - return + return null } const onboarding = await window.api.onboarding.get().catch(() => null) if (!onboarding || onboarding.checklist[item]) { - return + return onboarding } const checklist: Partial = {} @@ -20,11 +21,12 @@ export async function markOnboardingProjectAdded( await window.api.onboarding.update({ checklist }) } catch (err) { console.warn('[onboarding] Failed to update project checklist item:', err) - return + return onboarding } track('activation_checklist_item_completed', { item, time_since_completed_ms: 0 }) + return onboarding } diff --git a/src/renderer/src/store/repos/repo-add-actions.ts b/src/renderer/src/store/repos/repo-add-actions.ts index 1213b524298..dc5f4e8319b 100644 --- a/src/renderer/src/store/repos/repo-add-actions.ts +++ b/src/renderer/src/store/repos/repo-add-actions.ts @@ -173,7 +173,7 @@ export function createRepoAddActions( if (!repo) { return null } - await markOnboardingProjectAdded('addedFolder') + const onboardingBeforeAdd = await markOnboardingProjectAdded('addedFolder') // Why: focus the new folder so the add is visible; lazy-import worktree-activation to avoid a circular module load (it imports the store root). const executionHostId = options?.runtimeEnvironmentId === undefined @@ -186,7 +186,6 @@ export function createRepoAddActions( (worktree) => executionHostId === undefined || worktree.hostId === executionHostId ) if (folderWorktree) { - const onboarding = await window.api.onboarding.get().catch(() => null) // Why: lazy-import to avoid a circular module load (the launch graph imports the store root). const { resolveDismissedOnboardingFolderAgentLaunch, @@ -196,7 +195,7 @@ export function createRepoAddActions( const launch = resolveDismissedOnboardingFolderAgentLaunch({ worktreeId: folderWorktree.id, store: get(), - onboarding, + onboarding: onboardingBeforeAdd, hasExistingProject: hadProjectBeforeAdd, executionHostId: executionHostId ?? LOCAL_EXECUTION_HOST_ID }) diff --git a/src/renderer/src/store/slices/repos-onboarding-folder-startup.test.ts b/src/renderer/src/store/slices/repos-onboarding-folder-startup.test.ts index 3a3af4f5287..9fce9b24728 100644 --- a/src/renderer/src/store/slices/repos-onboarding-folder-startup.test.ts +++ b/src/renderer/src/store/slices/repos-onboarding-folder-startup.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' import { getDefaultOnboardingState } from '../../../../shared/onboarding-defaults' +import type { OnboardingState } from '../../../../shared/onboarding-state-types' import { useAppStore } from '@/store' import { resolveDismissedOnboardingFolderAgentLaunch, @@ -30,17 +31,19 @@ afterEach(() => vi.restoreAllMocks()) const reposAdd = vi.fn() const worktreesList = vi.fn() const onboardingGet = vi.fn() +const onboardingUpdate = vi.fn() beforeEach(() => { reposAdd.mockReset() worktreesList.mockReset() onboardingGet.mockReset() + onboardingUpdate.mockReset() worktreeActivation.activateAndRevealWorktree.mockReset() vi.stubGlobal('window', { api: { repos: { add: reposAdd }, worktrees: { list: worktreesList }, - onboarding: { get: onboardingGet } + onboarding: { get: onboardingGet, update: onboardingUpdate } } }) }) @@ -57,7 +60,14 @@ describe('repo slice skipped-onboarding folder startup', () => { worktreesList.mockImplementation(({ repoId }: { repoId: string }) => [ makeWorktree({ id: `${repoId}::/folder`, repoId }) ]) - onboardingGet.mockResolvedValue({ ...getDefaultOnboardingState(), outcome: 'dismissed' }) + let onboarding: OnboardingState = { ...getDefaultOnboardingState(), outcome: 'dismissed' } + onboardingGet.mockImplementation(async () => onboarding) + onboardingUpdate.mockImplementation( + async ({ checklist }: { checklist: Partial }) => { + onboarding = { ...onboarding, checklist: { ...onboarding.checklist, ...checklist } } + return onboarding + } + ) const store = createTestStore() store.setState({ settings: {