From 5196575d40caa367395c5535da1c99caecef072c Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 25 Jun 2026 01:09:19 -0500 Subject: [PATCH 1/2] feat(pets): remix a draft into a fresh round Add a hover/focus "Remix" action on each completed draft card in the generation grid. It re-runs generation with the chosen draft fed back in as the reference image, keeping the same prompt and staying on step 2 so the user can explore variations without starting over. Because regenerating is slow and replaces the current drafts, the first remix shows a one-time confirmation; the acknowledgement is persisted so subsequent remixes fire immediately. --- .../pet-generate/components/draft-grid.tsx | 90 +++++++++++++------ .../app/pet-generate/pet-generate-content.tsx | 50 ++++++++++- apps/desktop/src/i18n/en.ts | 4 + apps/desktop/src/i18n/ja.ts | 3 + apps/desktop/src/i18n/types.ts | 3 + apps/desktop/src/i18n/zh-hant.ts | 3 + apps/desktop/src/i18n/zh.ts | 3 + apps/desktop/src/store/pet-generate.ts | 12 ++- 8 files changed, 139 insertions(+), 29 deletions(-) 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..f4079ed8941 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,58 @@ export function DraftGrid({ drafts, generating, hasDrafts, onCancel, onHatch, on const isSelected = draft != null && selected === draft.index return ( - + + {/* Branch off this look — reuses the draft as the next reference so + the user can explore variations without leaving the grid. Hidden + until hover/focus to keep the grid clean. */} + {draft != null && !generating && ( + + + )} - disabled={draft == null} - key={draft ? `draft-${draft.index}` : `slot-${i}`} - onClick={() => draft != null && onSelect(draft.index)} - type="button" - > - {draft != null ? ( - // Hatches into place as each draft streams back. - - ) : ( - // Incubating: a creme egg bouncing on its contact shadow. -
- - -
- )} - + ) })} 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..4f43be0443e 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,29 @@ export function PetGenerateContent() { void generateDrafts(requestGateway, { prompt: example }) } + // Branch a fresh draft round off an existing look by feeding it back in as the + // reference image. Keeps the user on step 2 with the same prompt, just grounded + // on the chosen draft. + const runRemix = (draft: { dataUri: string }) => { + void generateDrafts(requestGateway, { prompt: prompt.trim(), referenceImage: draft.dataUri }) + } + + // "Remix" affordance: regenerating is slow and replaces the current drafts, so + // warn once (then remember the acknowledgement) before kicking off the round. + 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 +312,28 @@ export function PetGenerateContent() { hasDrafts={hasDrafts} onCancel={discardDrafts} onHatch={hatch} + onRemix={remixDraft} onSelect={index => $petGenSelected.set(index)} selected={selected} /> )} + + setRemixPending(null)} + onConfirm={() => { + const draft = remixPending + markRemixConfirmed() + + if (draft) { + runRemix(draft) + } + }} + 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 { From 6b3ea2cea6889d24a67fbd973868c8cca2d8a615 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 25 Jun 2026 01:10:56 -0500 Subject: [PATCH 2/2] refactor(pets): tighten remix comments and confirm handler --- .../src/app/pet-generate/components/draft-grid.tsx | 4 +--- .../src/app/pet-generate/pet-generate-content.tsx | 13 +++++-------- 2 files changed, 6 insertions(+), 11 deletions(-) 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 f4079ed8941..d8e98a415e6 100644 --- a/apps/desktop/src/app/pet-generate/components/draft-grid.tsx +++ b/apps/desktop/src/app/pet-generate/components/draft-grid.tsx @@ -82,9 +82,7 @@ export function DraftGrid({ )} - {/* Branch off this look — reuses the draft as the next reference so - the user can explore variations without leaving the grid. Hidden - until hover/focus to keep the grid clean. */} + {/* Remix: branch a new round off this look. Revealed on hover/focus. */} {draft != null && !generating && (