diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx
index 706c84f7061..495aad30372 100644
--- a/apps/desktop/src/app/desktop-controller.tsx
+++ b/apps/desktop/src/app/desktop-controller.tsx
@@ -405,7 +405,6 @@ export function DesktopController() {
void refreshCurrentModel()
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
}}
- onOpenSettings={openSettings}
requestGateway={requestGateway}
/>
diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions.ts b/apps/desktop/src/app/session/hooks/use-prompt-actions.ts
index 410f1dcf219..1ba8c65d069 100644
--- a/apps/desktop/src/app/session/hooks/use-prompt-actions.ts
+++ b/apps/desktop/src/app/session/hooks/use-prompt-actions.ts
@@ -25,6 +25,7 @@ import {
type ComposerAttachment
} from '@/store/composer'
import { clearNotifications, notify, notifyError } from '@/store/notifications'
+import { requestDesktopOnboarding } from '@/store/onboarding'
import { $busy, $messages, setAwaitingResponse, setBusy, setMessages } from '@/store/session'
import type { ClientSessionState, ImageAttachResponse, SlashExecResponse } from '../../types'
@@ -45,6 +46,16 @@ function blobToDataUrl(blob: Blob): Promise {
})
}
+interface SetupStatus {
+ provider_configured?: boolean
+}
+
+function isProviderSetupError(error: unknown) {
+ const message = error instanceof Error ? error.message : String(error)
+
+ return /No inference provider configured|OPENROUTER_API_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY/i.test(message)
+}
+
interface PromptActionsOptions {
activeSessionId: string | null
activeSessionIdRef: MutableRefObject
@@ -216,6 +227,15 @@ export function usePromptActions({
setAwaitingResponse(true)
clearNotifications()
+ const setup = await requestGateway('setup.status').catch(() => null)
+
+ if (setup?.provider_configured === false) {
+ releaseBusy()
+ requestDesktopOnboarding('Add a provider credential before sending your first message.')
+
+ return
+ }
+
let sessionId = activeSessionId
if (!sessionId) {
@@ -257,6 +277,13 @@ export function usePromptActions({
} catch (err) {
releaseBusy()
updateSessionState(sessionId, state => ({ ...state, busy: false, awaitingResponse: false }))
+
+ if (isProviderSetupError(err)) {
+ requestDesktopOnboarding('Add a provider credential before sending your first message.')
+
+ return
+ }
+
notifyError(err, 'Prompt failed')
}
},
diff --git a/apps/desktop/src/components/desktop-onboarding-overlay.tsx b/apps/desktop/src/components/desktop-onboarding-overlay.tsx
index 893dad55031..55f05cfbf9b 100644
--- a/apps/desktop/src/components/desktop-onboarding-overlay.tsx
+++ b/apps/desktop/src/components/desktop-onboarding-overlay.tsx
@@ -1,17 +1,18 @@
+import { useStore } from '@nanostores/react'
import { useEffect, useMemo, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { getEnvVars, setEnvVar } from '@/hermes'
-import { AlertCircle, Check, ExternalLink, KeyRound, Loader2, Settings2, X } from '@/lib/icons'
+import { AlertCircle, Check, ExternalLink, KeyRound, Loader2 } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { notify, notifyError } from '@/store/notifications'
+import { $desktopOnboarding, completeDesktopOnboarding } from '@/store/onboarding'
import type { EnvVarInfo } from '@/types/hermes'
interface DesktopOnboardingOverlayProps {
enabled: boolean
onCompleted?: () => void
- onOpenSettings?: () => void
requestGateway: (method: string, params?: Record) => Promise
}
@@ -25,8 +26,6 @@ interface ProviderOption {
helper: string
}
-const DISMISS_KEY = 'desktop.onboarding.dismissed_until_reload'
-
const PREFERRED_PROVIDER_KEYS: ProviderOption[] = [
{
key: 'OPENROUTER_API_KEY',
@@ -60,22 +59,6 @@ const PREFERRED_PROVIDER_KEYS: ProviderOption[] = [
}
]
-function isDismissedForSession() {
- try {
- return window.sessionStorage.getItem(DISMISS_KEY) === '1'
- } catch {
- return false
- }
-}
-
-function dismissForSession() {
- try {
- window.sessionStorage.setItem(DISMISS_KEY, '1')
- } catch {
- // Ignore storage failures; in-memory state still dismisses the overlay.
- }
-}
-
function optionLabel(option: ProviderOption, info?: EnvVarInfo) {
return info?.description ? `${option.label} (${option.key})` : option.label
}
@@ -83,20 +66,20 @@ function optionLabel(option: ProviderOption, info?: EnvVarInfo) {
export function DesktopOnboardingOverlay({
enabled,
onCompleted,
- onOpenSettings,
requestGateway
}: DesktopOnboardingOverlayProps) {
+ const onboarding = useStore($desktopOnboarding)
const [checking, setChecking] = useState(false)
- const [dismissed, setDismissed] = useState(isDismissedForSession)
const [envVars, setEnvVars] = useState | null>(null)
const [error, setError] = useState(null)
const [providerConfigured, setProviderConfigured] = useState(true)
const [saving, setSaving] = useState(false)
const [selectedKey, setSelectedKey] = useState(PREFERRED_PROVIDER_KEYS[0].key)
const [value, setValue] = useState('')
+ const shouldCheck = enabled || onboarding.requested
useEffect(() => {
- if (!enabled || dismissed) {
+ if (!shouldCheck) {
return
}
@@ -116,6 +99,10 @@ export function DesktopOnboardingOverlay({
setProviderConfigured(Boolean(status.provider_configured))
setEnvVars(vars)
+ if (status.provider_configured) {
+ completeDesktopOnboarding()
+ }
+
const firstAvailable = PREFERRED_PROVIDER_KEYS.find(option => vars[option.key])
if (firstAvailable) {
@@ -136,7 +123,7 @@ export function DesktopOnboardingOverlay({
void checkSetup()
return () => void (cancelled = true)
- }, [dismissed, enabled, requestGateway])
+ }, [requestGateway, shouldCheck])
const providerOptions = useMemo(
() => PREFERRED_PROVIDER_KEYS.filter(option => !envVars || envVars[option.key]),
@@ -169,6 +156,7 @@ export function DesktopOnboardingOverlay({
notify({ kind: 'success', title: 'Hermes is ready', message: `${selectedKey} saved.` })
setProviderConfigured(true)
setValue('')
+ completeDesktopOnboarding()
onCompleted?.()
} catch (err) {
notifyError(err, `Failed to save ${selectedKey}`)
@@ -178,17 +166,7 @@ export function DesktopOnboardingOverlay({
}
}
- function handleDismiss() {
- dismissForSession()
- setDismissed(true)
- }
-
- function handleOpenSettings() {
- handleDismiss()
- onOpenSettings?.()
- }
-
- if (!enabled || dismissed || providerConfigured) {
+ if (!shouldCheck || providerConfigured) {
return null
}
@@ -209,9 +187,6 @@ export function DesktopOnboardingOverlay({
-
@@ -223,6 +198,13 @@ export function DesktopOnboardingOverlay({
) : null}
+ {onboarding.reason ? (
+
+
+
{onboarding.reason}
+
+ ) : null}
+
@@ -287,20 +269,11 @@ export function DesktopOnboardingOverlay({
) : null}
-
-
diff --git a/apps/desktop/src/store/onboarding.ts b/apps/desktop/src/store/onboarding.ts
new file mode 100644
index 00000000000..b291ec1f218
--- /dev/null
+++ b/apps/desktop/src/store/onboarding.ts
@@ -0,0 +1,25 @@
+import { atom } from 'nanostores'
+
+interface DesktopOnboardingState {
+ reason: null | string
+ requested: boolean
+}
+
+export const $desktopOnboarding = atom({
+ reason: null,
+ requested: false
+})
+
+export function requestDesktopOnboarding(reason = 'No inference provider is configured.') {
+ $desktopOnboarding.set({
+ reason,
+ requested: true
+ })
+}
+
+export function completeDesktopOnboarding() {
+ $desktopOnboarding.set({
+ reason: null,
+ requested: false
+ })
+}