feat(desktop): add 'choose provider later' skip to first-run onboarding (#39483)

The first-run provider picker was a hard gate — the only way out was
connecting a provider. Add an 'I'll choose a provider later' link that
dismisses the overlay and persists the skip to localStorage so it never
re-nags on subsequent launches. Users connect a provider any time from
Settings -> Providers (manual onboarding already bypasses the skip gate).

- onboarding.ts: firstRunSkipped state seeded from localStorage
  (hermes-onboarding-skipped-v1) + dismissFirstRunOnboarding() action;
  completeDesktopOnboarding clears the flag once a provider connects.
- overlay: skip gate (firstRunSkipped && !manual returns null); ChooseLaterLink
  rendered in both the OAuth picker footer and the API-key fallback, first-run only.
- tests: skip persists + hidden in manual mode; full-state fixtures updated.
This commit is contained in:
Teknium
2026-06-04 19:40:54 -07:00
committed by GitHub
parent 5bcb63e400
commit 9cc47b20cb
4 changed files with 124 additions and 7 deletions
@@ -30,6 +30,7 @@ function baseState(overrides: Partial<DesktopOnboardingState> = {}): DesktopOnbo
providers: null,
reason: null,
requested: false,
firstRunSkipped: false,
manual: false,
...overrides
}
+53
View File
@@ -60,6 +60,13 @@ export interface DesktopOnboardingState {
providers: null | OAuthProvider[]
reason: null | string
requested: boolean
/** True when the user explicitly chose "I'll choose a provider later" on the
* first-run picker. Persisted to localStorage so the blocking overlay never
* re-nags on subsequent launches — the user can connect a provider any time
* from Settings → Providers (or the model picker's "Add provider"). Distinct
* from `configured`: the app still has no usable provider, so chat won't work
* until one is connected; we just stop forcing the choice up front. */
firstRunSkipped: boolean
/** True when the user explicitly opened the provider selector to add /
* switch providers from an already-configured app (e.g. via the model
* picker's "Add provider" button). Forces the overlay to show the picker
@@ -73,6 +80,7 @@ export interface OnboardingContext {
}
const CONFIGURED_CACHE_KEY = 'hermes-desktop-onboarded-v1'
const SKIP_CACHE_KEY = 'hermes-onboarding-skipped-v1'
const POLL_MS = 2000
const COPY_FLASH_MS = 1500
const DEFAULT_ONBOARDING_REASON = 'No inference provider is configured.'
@@ -105,6 +113,34 @@ function writeCachedConfigured(value: boolean) {
}
}
function readCachedSkipped(): boolean {
if (typeof window === 'undefined') {
return false
}
try {
return window.localStorage.getItem(SKIP_CACHE_KEY) === '1'
} catch {
return false
}
}
function writeCachedSkipped(value: boolean) {
if (typeof window === 'undefined') {
return
}
try {
if (value) {
window.localStorage.setItem(SKIP_CACHE_KEY, '1')
} else {
window.localStorage.removeItem(SKIP_CACHE_KEY)
}
} catch {
// localStorage unavailable — degrade silently.
}
}
const INITIAL: DesktopOnboardingState = {
configured: readCachedConfigured(),
flow: { status: 'idle' },
@@ -112,6 +148,7 @@ const INITIAL: DesktopOnboardingState = {
providers: null,
reason: null,
requested: false,
firstRunSkipped: readCachedSkipped(),
manual: false
}
@@ -398,6 +435,9 @@ export function closeManualOnboarding() {
export function completeDesktopOnboarding() {
clearPoll()
writeCachedConfigured(true)
// A real provider is now connected, so any earlier "choose later" skip is
// moot — clear it so the flag never lingers in a configured install.
writeCachedSkipped(false)
$desktopOnboarding.set({
configured: true,
flow: { status: 'idle' },
@@ -405,10 +445,23 @@ export function completeDesktopOnboarding() {
providers: null,
reason: null,
requested: false,
firstRunSkipped: false,
manual: false
})
}
// "I'll choose a provider later" on the first-run picker. Persists the skip so
// the blocking overlay never re-nags on future launches, and dismisses it now
// so the user lands in the app. Chat won't work until a provider is connected
// (from Settings → Providers or the model picker's "Add provider") — this only
// stops forcing the choice up front. Distinct from completeDesktopOnboarding,
// which marks the app actually configured.
export function dismissFirstRunOnboarding() {
clearPoll()
writeCachedSkipped(true)
patch({ firstRunSkipped: true, requested: false, manual: false, flow: { status: 'idle' } })
}
export function setOnboardingMode(mode: OnboardingMode) {
patch({ mode })
}