fix(onboarding): start the default agent in the first folder added after skipping onboarding (#32701)

This commit is contained in:
Kelvin Amoaba
2026-10-23 21:44:40 +00:00
committed by GitHub
parent aeb039938f
commit ffd784286e
6 changed files with 22 additions and 55 deletions
@@ -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'
}
})
})
})
@@ -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
})
@@ -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)
}
@@ -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<void> {
): Promise<OnboardingState | null> {
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<OnboardingState['checklist']> = {}
@@ -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
}
@@ -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
})
@@ -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<OnboardingState['checklist']> }) => {
onboarding = { ...onboarding, checklist: { ...onboarding.checklist, ...checklist } }
return onboarding
}
)
const store = createTestStore()
store.setState({
settings: {