Skip to content

Commit d7d623e

Browse files
Fix: Preserve reference image panel state and selection on recall (#9010)
* Fix: Preserve reference image panel state and selection on recall Made-with: Cursor * chore: apply Prettier to refImagesSlice Made-with: Cursor * fix: refine ref image recall selection behavior Simplify image-name fingerprint fallback, add an explicit guard for open-panel with null selection, and document the acceptable empty-config collision tradeoff. Made-with: Cursor --------- Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev>
1 parent c550ce3 commit d7d623e

1 file changed

Lines changed: 41 additions & 4 deletions

File tree

invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ type PayloadActionWithId<T = void> = T extends void
3535
} & T
3636
>;
3737

38+
/** Fingerprint used to match the same reference image entry after recall when ids are regenerated. */
39+
/** Empty configs of the same type may collide; the worst case is selecting an equivalent empty entity. */
40+
const getRefImageRecallMatchKey = (entity: RefImageState): string => {
41+
const { config } = entity;
42+
const imageName = config.image?.original.image.image_name ?? '';
43+
const modelKey = 'model' in config && config.model ? config.model.key : '';
44+
return `${config.type}\0${modelKey}\0${imageName}`;
45+
};
46+
3847
const slice = createSlice({
3948
name: 'refImages',
4049
initialState: getInitialRefImagesState(),
@@ -54,13 +63,41 @@ const slice = createSlice({
5463
},
5564
refImagesRecalled: (state, action: PayloadAction<{ entities: RefImageState[]; replace: boolean }>) => {
5665
const { entities, replace } = action.payload;
57-
if (replace) {
58-
state.entities = entities;
66+
if (!replace) {
67+
state.entities.push(...entities);
68+
return;
69+
}
70+
const wasPanelOpen = state.isPanelOpen;
71+
const previousSelectedId = state.selectedEntityId;
72+
let previousEntity: RefImageState | null = null;
73+
if (previousSelectedId !== null) {
74+
previousEntity = state.entities.find((e) => e.id === previousSelectedId) ?? null;
75+
}
76+
state.entities = entities;
77+
if (entities.length === 0) {
78+
state.selectedEntityId = null;
5979
state.isPanelOpen = false;
80+
return;
81+
}
82+
if (!wasPanelOpen) {
6083
state.selectedEntityId = null;
61-
} else {
62-
state.entities.push(...entities);
84+
return;
85+
}
86+
const firstEntity = entities[0];
87+
assert(firstEntity);
88+
if (previousSelectedId === null) {
89+
// Open panel must have a selection; otherwise, fall back to the first entity.
90+
state.selectedEntityId = firstEntity.id;
91+
return;
92+
}
93+
if (previousSelectedId !== null && entities.some((e) => e.id === previousSelectedId)) {
94+
state.selectedEntityId = previousSelectedId;
95+
return;
6396
}
97+
const previousKey = previousEntity ? getRefImageRecallMatchKey(previousEntity) : null;
98+
const matched =
99+
previousKey !== null ? entities.find((e) => getRefImageRecallMatchKey(e) === previousKey) : undefined;
100+
state.selectedEntityId = matched?.id ?? firstEntity.id;
64101
},
65102
refImageImageChanged: (state, action: PayloadActionWithId<{ croppableImage: CroppableImageWithDims | null }>) => {
66103
const { id, croppableImage } = action.payload;

0 commit comments

Comments
 (0)