diff --git a/apps/desktop/src/app/hooks/use-route-overlay-active.ts b/apps/desktop/src/app/hooks/use-route-overlay-active.ts
new file mode 100644
index 00000000000..261f9de15d5
--- /dev/null
+++ b/apps/desktop/src/app/hooks/use-route-overlay-active.ts
@@ -0,0 +1,19 @@
+import { useLocation } from 'react-router-dom'
+
+import { appViewForPath, isOverlayView } from '@/app/routes'
+
+/**
+ * True while a full-screen route overlay (settings, agents, command-center, …)
+ * is showing.
+ *
+ * A portaled Radix modal sits above the app shell, so it would cover such a
+ * route. Any modal that sends the user to one (e.g. "set up image generation" →
+ * `/settings`) can `if (useRouteOverlayActive()) return null` to *yield* the
+ * screen — its open state lives in a store, so it stays open — and reappear,
+ * re-running its mount effects (a free refresh), when the route overlay closes.
+ */
+export function useRouteOverlayActive(): boolean {
+ const { pathname } = useLocation()
+
+ return isOverlayView(appViewForPath(pathname))
+}
diff --git a/apps/desktop/src/app/pet-generate/pet-generate-overlay.tsx b/apps/desktop/src/app/pet-generate/pet-generate-overlay.tsx
index 2ba12a22bc0..954dac23bfd 100644
--- a/apps/desktop/src/app/pet-generate/pet-generate-overlay.tsx
+++ b/apps/desktop/src/app/pet-generate/pet-generate-overlay.tsx
@@ -10,8 +10,11 @@
import { useStore } from '@nanostores/react'
import { useEffect, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { SETTINGS_ROUTE } from '@/app/routes'
import { useGatewayRequest } from '@/app/gateway/hooks/use-gateway-request'
+import { useRouteOverlayActive } from '@/app/hooks/use-route-overlay-active'
import { PetEggHatch } from '@/components/pet/pet-egg-hatch'
import { PetStarShower } from '@/components/pet/pet-star-shower'
import { PetSprite } from '@/components/pet/pet-sprite'
@@ -22,12 +25,14 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
import { GenerateButton } from '@/components/ui/generate-button'
import { Input } from '@/components/ui/input'
import { useI18n } from '@/i18n'
+import { ExternalLink } from '@/lib/external-link'
import { triggerHaptic } from '@/lib/haptics'
-import { Egg, Loader2, PawPrint, RefreshCw } from '@/lib/icons'
+import { Egg, Loader2, PawPrint, RefreshCw, Settings2 } from '@/lib/icons'
import { selectableCardClass } from '@/lib/selectable-card'
import { cn } from '@/lib/utils'
import { type PetInfo } from '@/store/pet'
import {
+ $petGenAvailable,
$petGenDrafts,
$petGenerateOpen,
$petGenError,
@@ -38,6 +43,7 @@ import {
adoptHatched,
cancelGenerate,
cancelHatch,
+ checkPetGenAvailable,
cleanPetName,
cleanupPetGen,
closePetGenerate,
@@ -87,6 +93,13 @@ export function PetGenerateOverlay() {
const status = useStore($petGenStatus)
const { requestGateway } = useGatewayRequest()
+ // Yield the screen to a full-screen route overlay (e.g. /settings while the
+ // user adds an image-gen key) without tearing down — the store keeps us open,
+ // and we reappear + re-check on return.
+ if (useRouteOverlayActive()) {
+ return null
+ }
+
const handleOpenChange = (next: boolean) => {
if (!next) {
// Deletes a hatched-but-unadopted preview pet so it doesn't linger, then
@@ -116,9 +129,14 @@ function PetGenerateContent() {
const { t } = useI18n()
const copy = t.commandCenter.generatePet
const { requestGateway } = useGatewayRequest()
+ const navigate = useNavigate()
const status = useStore($petGenStatus)
const error = useStore($petGenError)
+ const available = useStore($petGenAvailable)
+ // `null` = not yet probed → stay optimistic (show the prompt); only the
+ // confirmed-no-backend case swaps in the setup card.
+ const unavailable = available === false
const drafts = useStore($petGenDrafts)
const selected = useStore($petGenSelected)
const preview = useStore($petGenPreview)
@@ -126,6 +144,13 @@ function PetGenerateContent() {
const [prompt, setPrompt] = useState('')
+ // Probe backend availability on open — and again whenever the content
+ // remounts (e.g. after returning from the providers settings), so adding a
+ // key flips the setup card to the prompt with no manual refresh.
+ useEffect(() => {
+ void checkPetGenAvailable(requestGateway)
+ }, [requestGateway])
+
const busy = status === 'generating' || status === 'hatching'
const hasDrafts = drafts.length > 0
const generating = status === 'generating'
@@ -176,14 +201,23 @@ function PetGenerateContent() {
// The header title tracks the phase instead of sticking on "Generate a pet".
const headerTitle =
status === 'hatching' ? copy.spawning : status === 'preview' || status === 'adopting' ? copy.hatched : copy.title
- // Prompt input only belongs on the describe/draft screens.
- const showPrompt = status !== 'hatching' && status !== 'preview' && status !== 'adopting'
+ // Send the user to set up a key without closing — the overlay yields to the
+ // settings route (useRouteOverlayActive) and reappears + re-checks on return.
+ const setupImageGen = () => navigate(`${SETTINGS_ROUTE}?tab=providers`)
+
+ // Prompt input only belongs on the describe/draft screens (and never when
+ // there's no backend to generate with).
+ const showPrompt = !unavailable && status !== 'hatching' && status !== 'preview' && status !== 'adopting'
return (
<>
-
Add an image backend to generate
++ Hatching a custom pet needs a provider that can ground on a reference image. +
+
+ Grab a key from
+