diff --git a/apps/desktop/src/app/pet-generate/components/draft-grid.tsx b/apps/desktop/src/app/pet-generate/components/draft-grid.tsx
index abef61f027f..d8e98a415e6 100644
--- a/apps/desktop/src/app/pet-generate/components/draft-grid.tsx
+++ b/apps/desktop/src/app/pet-generate/components/draft-grid.tsx
@@ -1,5 +1,7 @@
import { PixelEggSprite } from '@/components/pet/pixel-egg-sprite'
import { Button } from '@/components/ui/button'
+import { Codicon } from '@/components/ui/codicon'
+import { Tip } from '@/components/ui/tooltip'
import { useI18n } from '@/i18n'
import { PawPrint } from '@/lib/icons'
import { selectableCardClass } from '@/lib/selectable-card'
@@ -13,11 +15,21 @@ interface DraftGridProps {
hasDrafts: boolean
onCancel: () => void
onHatch: () => void
+ onRemix: (draft: { index: number; dataUri: string }) => void
onSelect: (index: number) => void
selected: number | null
}
-export function DraftGrid({ drafts, generating, hasDrafts, onCancel, onHatch, onSelect, selected }: DraftGridProps) {
+export function DraftGrid({
+ drafts,
+ generating,
+ hasDrafts,
+ onCancel,
+ onHatch,
+ onRemix,
+ onSelect,
+ selected
+}: DraftGridProps) {
const { t } = useI18n()
const copy = t.commandCenter.generatePet
@@ -43,32 +55,56 @@ export function DraftGrid({ drafts, generating, hasDrafts, onCancel, onHatch, on
const isSelected = draft != null && selected === draft.index
return (
-
+
)
})}
diff --git a/apps/desktop/src/app/pet-generate/pet-generate-content.tsx b/apps/desktop/src/app/pet-generate/pet-generate-content.tsx
index 87d34897cc7..0f183678b1d 100644
--- a/apps/desktop/src/app/pet-generate/pet-generate-content.tsx
+++ b/apps/desktop/src/app/pet-generate/pet-generate-content.tsx
@@ -1,10 +1,11 @@
import { useStore } from '@nanostores/react'
-import { useEffect, useRef } from 'react'
+import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useGatewayRequest } from '@/app/gateway/hooks/use-gateway-request'
import { SETTINGS_ROUTE } from '@/app/routes'
import { Alert, AlertDescription } from '@/components/ui/alert'
+import { ConfirmDialog } from '@/components/ui/confirm-dialog'
import { DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { GenerateButton } from '@/components/ui/generate-button'
import { Input } from '@/components/ui/input'
@@ -20,6 +21,7 @@ import {
$petGenPreview,
$petGenRefImage,
$petGenRefName,
+ $petGenRemixConfirmed,
$petGenSelected,
$petGenStage,
$petGenStatus,
@@ -31,7 +33,8 @@ import {
discardDrafts,
discardHatched,
generateDrafts,
- hatchSelected
+ hatchSelected,
+ markRemixConfirmed
} from '@/store/pet-generate'
import { DraftGrid } from './components/draft-grid'
@@ -68,6 +71,9 @@ export function PetGenerateContent() {
const refName = useStore($petGenRefName)
const fileRef = useRef(null)
+ // The draft awaiting the one-time "remix regenerates" confirmation.
+ const [remixPending, setRemixPending] = useState<{ dataUri: string } | null>(null)
+
// 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.
@@ -139,6 +145,27 @@ export function PetGenerateContent() {
void generateDrafts(requestGateway, { prompt: example })
}
+ // A remix re-runs generation grounded on an existing draft — same prompt, stay
+ // on step 2 — so the user explores variations without starting over.
+ const runRemix = (draft: { dataUri: string }) => {
+ void generateDrafts(requestGateway, { prompt: prompt.trim(), referenceImage: draft.dataUri })
+ }
+
+ // Slow, and it replaces the current drafts — so confirm once, then remember it.
+ const remixDraft = (draft: { dataUri: string }) => {
+ if (busy) {
+ return
+ }
+
+ if ($petGenRemixConfirmed.get()) {
+ runRemix(draft)
+
+ return
+ }
+
+ setRemixPending(draft)
+ }
+
// Hatch the selected draft. The user can pick one before the rest stream in —
// if so, abort the remaining generations first (keeping the drafts we have).
// The prompt is grounding text, not a label; the user names it on reveal.
@@ -283,11 +310,27 @@ export function PetGenerateContent() {
hasDrafts={hasDrafts}
onCancel={discardDrafts}
onHatch={hatch}
+ onRemix={remixDraft}
onSelect={index => $petGenSelected.set(index)}
selected={selected}
/>
)}
+
+ setRemixPending(null)}
+ onConfirm={() => {
+ markRemixConfirmed()
+
+ if (remixPending) {
+ runRemix(remixPending)
+ }
+ }}
+ open={remixPending !== null}
+ title={copy.remixConfirmTitle}
+ />
>
)
}
diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts
index a607ed93efb..851792f8163 100644
--- a/apps/desktop/src/i18n/en.ts
+++ b/apps/desktop/src/i18n/en.ts
@@ -799,6 +799,10 @@ export const en: Translations = {
staleBackend: 'Update Hermes to generate pets.',
backgroundHint: 'You can close this — Hermes will notify you when it’s done.',
slowProviderHint: 'This can take several minutes',
+ remix: 'Remix',
+ remixConfirmTitle: 'Remix this look?',
+ remixConfirmBody:
+ 'This generates a fresh set of drafts using this one as the starting point. It can take several minutes.',
genericError: 'Generation failed — try again or pick a suggestion.',
referenceImageTooLarge: 'Reference image is too large. Use one under 16 MB.',
referenceImageInvalid: 'Could not read that reference image. Try a PNG, JPG, WebP, or GIF.',
diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts
index 2f5344e3d67..c58cc6be1f0 100644
--- a/apps/desktop/src/i18n/ja.ts
+++ b/apps/desktop/src/i18n/ja.ts
@@ -917,6 +917,9 @@ export const ja = defineLocale({
staleBackend: 'ペットを生成するには Hermes を更新してください。',
backgroundHint: 'このウィンドウは閉じても大丈夫です。完了したら Hermes が通知します。',
slowProviderHint: '数分かかることがあります',
+ remix: 'リミックス',
+ remixConfirmTitle: 'この見た目でリミックスしますか?',
+ remixConfirmBody: 'これを起点に新しい候補を生成します。数分かかることがあります。',
genericError: '生成に失敗しました。もう一度試すか、候補を選んでください。',
referenceImageTooLarge: '参照画像が大きすぎます。16 MB 未満の画像を使ってください。',
referenceImageInvalid: '参照画像を読み込めませんでした。PNG/JPG/WebP/GIF を試してください。',
diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts
index 0dcc51bca84..4986030231c 100644
--- a/apps/desktop/src/i18n/types.ts
+++ b/apps/desktop/src/i18n/types.ts
@@ -703,6 +703,9 @@ export interface Translations {
staleBackend: string
backgroundHint: string
slowProviderHint: string
+ remix: string
+ remixConfirmTitle: string
+ remixConfirmBody: string
genericError: string
referenceImageTooLarge: string
referenceImageInvalid: string
diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts
index 439aa0bf5c2..7bd0473625b 100644
--- a/apps/desktop/src/i18n/zh-hant.ts
+++ b/apps/desktop/src/i18n/zh-hant.ts
@@ -889,6 +889,9 @@ export const zhHant = defineLocale({
staleBackend: '請更新 Hermes 以生成寵物。',
backgroundHint: '你可以關閉此視窗——完成後 Hermes 會通知你。',
slowProviderHint: '這可能需要幾分鐘',
+ remix: '混合生成',
+ remixConfirmTitle: '以此造型混合生成?',
+ remixConfirmBody: '將以此造型為起點生成一組新草圖,可能需要幾分鐘。',
genericError: '生成失敗——請重試或選一個建議。',
referenceImageTooLarge: '參考圖片過大。請使用小於 16 MB 的圖片。',
referenceImageInvalid: '無法讀取該參考圖片。請嘗試 PNG、JPG、WebP 或 GIF。',
diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts
index 619f9817b02..502d5954323 100644
--- a/apps/desktop/src/i18n/zh.ts
+++ b/apps/desktop/src/i18n/zh.ts
@@ -987,6 +987,9 @@ export const zh: Translations = {
staleBackend: '请更新 Hermes 以生成宠物。',
backgroundHint: '你可以关闭此窗口——完成后 Hermes 会通知你。',
slowProviderHint: '这可能需要几分钟',
+ remix: '混合生成',
+ remixConfirmTitle: '以此造型混合生成?',
+ remixConfirmBody: '将以此造型为起点生成一组新草图,可能需要几分钟。',
genericError: '生成失败——请重试或选择一个建议。',
referenceImageTooLarge: '参考图过大。请使用小于 16 MB 的图片。',
referenceImageInvalid: '无法读取该参考图。请尝试 PNG、JPG、WebP 或 GIF。',
diff --git a/apps/desktop/src/store/pet-generate.ts b/apps/desktop/src/store/pet-generate.ts
index 5713b04a8a7..9659115fb9a 100644
--- a/apps/desktop/src/store/pet-generate.ts
+++ b/apps/desktop/src/store/pet-generate.ts
@@ -1,6 +1,6 @@
import { atom } from 'nanostores'
-import { persistString, storedString } from '@/lib/storage'
+import { persistBoolean, persistString, storedBoolean, storedString } from '@/lib/storage'
import { $gateway } from '@/store/gateway'
import { dispatchNativeNotification } from '@/store/native-notifications'
import { notify } from '@/store/notifications'
@@ -118,6 +118,7 @@ export interface PetGenProvider {
}
const PROVIDER_KEY = 'hermes.desktop.petgen.provider'
+const REMIX_CONFIRMED_KEY = 'hermes.desktop.petgen.remixConfirmed'
/** Reference-capable providers available to pick (from `pet.generate.status`). */
export const $petGenProviders = atom([])
@@ -130,6 +131,15 @@ export function setPetGenProvider(name: string): void {
persistString(PROVIDER_KEY, name || null)
}
+/** Whether the user has acknowledged the one-time "remix regenerates" notice. */
+export const $petGenRemixConfirmed = atom(storedBoolean(REMIX_CONFIRMED_KEY, false))
+
+/** Remember that the remix notice has been shown so we don't ask again. */
+export function markRemixConfirmed(): void {
+ $petGenRemixConfirmed.set(true)
+ persistBoolean(REMIX_CONFIRMED_KEY, true)
+}
+
/** Probe whether generation is possible (a reference-capable backend exists). */
export async function checkPetGenAvailable(request: GatewayRequest): Promise {
try {