From 1236b6dd2ffb83356f0f7b6bd2563e7149a1c44e Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Sat, 11 Jul 2026 11:23:25 +0200 Subject: [PATCH] feat: add commands for clearing candidates and reusing candidate settings, update UI controls --- commands/generation.test.ts | 34 +++++++++++++++++++ commands/generation.ts | 29 ++++++++++++++++ commands/ids.ts | 1 + commands/payloads.ts | 2 ++ .../GenerateActionControls.tsx | 22 ++++++++++-- 5 files changed, 85 insertions(+), 3 deletions(-) diff --git a/commands/generation.test.ts b/commands/generation.test.ts index 22baf9c..029d5e2 100644 --- a/commands/generation.test.ts +++ b/commands/generation.test.ts @@ -5,6 +5,8 @@ import { generationAddCandidateCommand, generationApplyCandidateAsLayerCommand, generationRemoveCandidateCommand, + generationClearCandidatesCommand, + generationReuseCandidateSettingsCommand, generationReplaceCandidatePixelsCommand, generationSetCompareModeCommand, generationFailJobCommand, @@ -38,6 +40,38 @@ describe("generation commands", () => { expect(next.editor.generation.compareMode).toBe("split"); }); + test("clears the candidate session and resets comparison", () => { + const withCandidate = generationAddCandidateCommand.execute( + { state: createInitialAppState("Test") }, + { candidate: generationCandidate("candidate-1") }, + ); + const comparing = generationSetCompareModeCommand.execute({ state: withCandidate }, { mode: "split" }); + + const next = generationClearCandidatesCommand.execute({ state: comparing }, undefined); + + expect(next.editor.generation.candidates).toEqual([]); + expect(next.editor.generation.selectedCandidateId).toBeUndefined(); + expect(next.editor.generation.compareMode).toBe("result"); + }); + + test("reuses a candidate's full settings with its resolved seed", () => { + const candidate = generationCandidate("candidate-1"); + candidate.settings = { + ...candidate.settings, + prompt: "Stored prompt", + model: "stored-model.safetensors", + seed: -1, + inpaint: { ...candidate.settings.inpaint, maskBlur: 7 }, + }; + candidate.seed = 987654; + const state = generationAddCandidateCommand.execute({ state: createInitialAppState("Test") }, { candidate }); + + const next = generationReuseCandidateSettingsCommand.execute({ state }, { candidateId: candidate.id }); + + expect(next.editor.tools.generate).toEqual({ ...candidate.settings, seed: 987654 }); + expect(next.editor.generation).toBe(state.editor.generation); + }); + test("applies candidates as top-level layers", () => { const withRemainingCandidate = generationAddCandidateCommand.execute( { state: documentWithSourceLayer() }, diff --git a/commands/generation.ts b/commands/generation.ts index 80a677d..47f3db9 100644 --- a/commands/generation.ts +++ b/commands/generation.ts @@ -24,6 +24,10 @@ export type GenerationRemoveCandidatePayload = { candidateId: GenerationCandidateId; }; +export type GenerationReuseCandidateSettingsPayload = { + candidateId: GenerationCandidateId; +}; + export type GenerationApplyCandidateAsLayerPayload = { candidateId: GenerationCandidateId; assetId: AssetId; @@ -140,6 +144,30 @@ export const generationClearCandidatesCommand: Command = { }, }; +export const generationReuseCandidateSettingsCommand: Command = { + id: commandIds.generationReuseCandidateSettings, + name: "Reuse generation candidate settings", + execute({ state }, payload) { + const candidate = state.editor.generation.candidates.find((item) => item.id === payload.candidateId); + if (!candidate) return state; + return { + ...state, + editor: { + ...state.editor, + tools: { + ...state.editor.tools, + generate: { + ...candidate.settings, + seed: candidate.seed, + outpaint: { ...candidate.settings.outpaint }, + inpaint: { ...candidate.settings.inpaint }, + }, + }, + }, + }; + }, +}; + export const generationApplyCandidateAsLayerCommand: Command = { id: commandIds.generationApplyCandidateAsLayer, name: "Apply generation candidate as layer", @@ -284,6 +312,7 @@ export const generationCommands = [ generationSetCompareModeCommand, generationRemoveCandidateCommand, generationClearCandidatesCommand, + generationReuseCandidateSettingsCommand, generationApplyCandidateAsLayerCommand, generationReplaceCandidatePixelsCommand, generationStartJobCommand, diff --git a/commands/ids.ts b/commands/ids.ts index 8da84ea..5e1f8ba 100644 --- a/commands/ids.ts +++ b/commands/ids.ts @@ -42,6 +42,7 @@ export const commandIds = { generationSetCompareMode: "generation.setCompareMode", generationRemoveCandidate: "generation.removeCandidate", generationClearCandidates: "generation.clearCandidates", + generationReuseCandidateSettings: "generation.reuseCandidateSettings", generationApplyCandidateAsLayer: "generation.applyCandidateAsLayer", generationReplaceCandidatePixels: "generation.replaceCandidatePixels", generationStartJob: "generation.startJob", diff --git a/commands/payloads.ts b/commands/payloads.ts index c1e913d..0a8e692 100644 --- a/commands/payloads.ts +++ b/commands/payloads.ts @@ -27,6 +27,7 @@ import type { GenerationAddCandidatePayload, GenerationApplyCandidateAsLayerPayload, GenerationRemoveCandidatePayload, + GenerationReuseCandidateSettingsPayload, GenerationReplaceCandidatePixelsPayload, GenerationSelectCandidatePayload, GenerationSetCompareModePayload, @@ -99,6 +100,7 @@ export type CommandPayloads = { [commandIds.generationSetCompareMode]: GenerationSetCompareModePayload; [commandIds.generationRemoveCandidate]: GenerationRemoveCandidatePayload; [commandIds.generationClearCandidates]: void; + [commandIds.generationReuseCandidateSettings]: GenerationReuseCandidateSettingsPayload; [commandIds.generationApplyCandidateAsLayer]: GenerationApplyCandidateAsLayerPayload; [commandIds.generationReplaceCandidatePixels]: GenerationReplaceCandidatePixelsPayload; [commandIds.generationStartJob]: GenerationStartJobPayload; diff --git a/view/bottom-controls/GenerateActionControls.tsx b/view/bottom-controls/GenerateActionControls.tsx index 9acaf81..24263a3 100644 --- a/view/bottom-controls/GenerateActionControls.tsx +++ b/view/bottom-controls/GenerateActionControls.tsx @@ -53,7 +53,6 @@ export function GenerateActionControls({ settings, generation, dispatch, workflo } function CandidatePicker({ generation, dispatch }: { generation: GenerationState; dispatch: AppStore["dispatch"] }) { - if (generation.candidates.length < 2) return null; return (
{generation.candidates.map((candidate) => { @@ -70,6 +69,15 @@ function CandidatePicker({ generation, dispatch }: { generation: GenerationState ); })} +
); } @@ -97,7 +105,9 @@ function CandidateControls({ return (
- Seed {candidate.seed} + + Seed {candidate.seed} + rerun("Regenerate", candidate.settings)} /> rerun("Reuse seed", { ...candidate.settings, seed: candidate.seed })} /> rerun("New seed", { ...candidate.settings, seed: -1 })} /> + dispatch(commandIds.generationReuseCandidateSettings, { candidateId: candidate.id })} + /> workflow.applyCandidateAsLayer(candidate.id)} /> dispatch(commandIds.generationRemoveCandidate, { candidateId: candidate.id })} /> - {settings.seed !== candidate.seed ? null : Current settings reuse this seed} + {settings.seed !== candidate.seed ? null : Current settings use this candidate's resolved seed}
); }