fix(editor): stabilize image generation, layer editing and controls (#6992)

* fix(editor): stabilize image workflows and editing controls

* docs(editor): add desktop and mobile visual validation evidence

* fix(editor): synchronize quick edit model events and register imagePreferences in test coordinator

* fix(settings): enforce authoritative global image-gen policy and non-admin preference persistence

- Enforce authoritative administrator global image_gen_enabled disable in get_user_setting, do_generate_image, and image_editing_provider_available, preventing non-admin user preference overrides while preserving user-disabled opt-outs and can_generate_images privilege checks across direct, gallery, and editor paths.
- Filter non-admin preference persistence in saveImagePreferences to user preference keys only (image_model, image_edit_model, image_quality), preventing global policy toggles from leaking into user preferences.
- Return accurate 200 OK Response from saveImagePreferences after fallback persistence succeeds so settings UI displays 'Saved' instead of false 403 error, while retaining visibility of real errors.
- Load per-user preferences via readImagePreferences in initImageSettings so non-admin selections persist and display correctly after page reload.
- Clear per-user model preferences on successful admin save (POST /api/auth/settings) via PUT /api/prefs/{key} with value: null to prevent stale per-user preferences from shadowing updated global settings.
- Support clearing keys in PUT /api/prefs/{key} when value is null.
- Add comprehensive Python regression tests for global-disabled/user-enabled, global-enabled/user-disabled, and authorized generation, plus Node and browser regression tests for non-admin persistence, toggle isolation, reload, and admin clearing.

* fix(settings): invalidate shared appConfig cache near auth settings write

* fix(test): isolate test_tool_policy unit suite from external model endpoint state

* fix(test): isolate web_fetch failure regression from contextual follow-up heuristics

* fix(test): isolate test_tool_policy from cross-test approval gate state

Ensure _patch_loop_basics disables TOOL_APPROVAL_GATE_ENABLED so routing tests in test_tool_policy run in the intended opt-out posture even when prior tests populate DATA_DIR with skills or untrusted context. Remove unnecessary forced_tools and contextual web mock workarounds.

* fix(settings): isolate non-admin image toggle and surface preference cleanup errors

- Restrict image generation availability toggle to admin users in imageModels.js, disabling the switch for non-admins and reverting unauthorized changes with an explicit message.
- Separate non-admin image model preference saves from global toggle updates.
- Track preference cleanup results on admin save in imagePreferences.js, propagating cleanup failures instead of ignoring them.
- Require valid preference keys on 403 fallback so toggle-only attempts reject cleanly.
- Add /api/prefs route mock to test_image_model_settings_sync_both_directions_and_survive_reopening.
- Add regression coverage for admin cleanup failures and non-admin toggle rejection.

* fix(settings): reconcile admin toggle on partial cleanup failure and report errors

- In imagePreferences.js, distinguish pre-persistence failures from post-save cleanup failures using err.globalSaved and err.phase.

- In imagePreferences.js, notify listeners of global policy updates on partial success before propagating cleanup failure.

- In imageModels.js, retain the persisted target state on admin toggle when global persistence succeeds but cleanup fails.

- In imageModels.js, reconcile admin toggle with authoritative server state via readImagePreferences() after ambiguous or partial failures.

- In imageModels.js, report partial success accurately without suppressing preference cleanup error messages.

- Add regression coverage in test_settings_save_response_ok_js.py for cleanup HTTP 500, cleanup network rejection, successful cleanup, and failed global POST.

---------

Co-authored-by: pewdiepie-archdaemon <pewdiepie-archdaemon@users.noreply.github.com>
Co-authored-by: Alexandre Teixeira <general@alexandreteixeira.dev>
This commit is contained in:
PewDiePie
2026-11-01 02:56:46 +00:00
committed by GitHub
co-authored by pewdiepie-archdaemon Alexandre Teixeira
parent 5353241743
commit 39303f19cd
56 changed files with 3384 additions and 278 deletions
+25 -6
View File
@@ -41,6 +41,7 @@
* }} deps
*/
import { state } from './state.js';
import { imageResultGeometry } from './image-result-geometry.js';
import { beginAIOperation, decodeAIImage } from './ai-operation.js';
export function wireInpaintButtons({
@@ -184,17 +185,30 @@ export function wireInpaintButtons({
if (!state.editorOpen) return; // user closed mid-decode
try {
saveState('Inpaint result');
// OpenAI returns at one of its allowed sizes (1024²,
// 1024×1536, 1536×1024) which often differs from our
// canvas. Scale to canvas size with smoothing so the
// inpaint blends in regardless of source dims.
// Whole-image edits keep the provider's native frame; only masked
// edits need registration with the source document's coordinates.
const shortPrompt = (prompt || '').trim().replace(/\s+/g, ' ').slice(0, 40);
const layerName = shortPrompt ? `Inpaint: ${shortPrompt}` : 'Inpaint Result';
const resultLayer = createLayer(layerName, state.imgWidth, state.imgHeight);
const fullFrame = data.full_frame === true;
const geometry = imageResultGeometry(resultImg.naturalWidth, resultImg.naturalHeight, state.imgWidth, state.imgHeight);
const resultLayer = createLayer(layerName,
fullFrame ? geometry.width : state.imgWidth,
fullFrame ? geometry.height : state.imgHeight);
resultLayer.ctx.imageSmoothingEnabled = true;
resultLayer.ctx.imageSmoothingQuality = 'high';
resultLayer.ctx.drawImage(resultImg, 0, 0, state.imgWidth, state.imgHeight);
if (fullFrame) {
resultLayer.ctx.drawImage(resultImg, geometry.drawX, geometry.drawY, geometry.drawWidth, geometry.drawHeight);
state.layerOffsets.set(resultLayer.id, {
x: geometry.offsetX,
y: geometry.offsetY,
});
} else {
const scale = Math.min(state.imgWidth / resultImg.naturalWidth, state.imgHeight / resultImg.naturalHeight);
const width = resultImg.naturalWidth * scale, height = resultImg.naturalHeight * scale;
resultLayer.ctx.drawImage(resultImg, (state.imgWidth - width) / 2, (state.imgHeight - height) / 2, width, height);
}
// Snapshot the AI result + hard mask used for this run.
if (!fullFrame) {
const aiSnap = document.createElement('canvas');
aiSnap.width = state.imgWidth; aiSnap.height = state.imgHeight;
aiSnap.getContext('2d').drawImage(resultLayer.canvas, 0, 0);
@@ -205,6 +219,7 @@ export function wireInpaintButtons({
resultLayer.inpaintSource = { ai: aiSnap, mask: maskSnap, base: baseSnap, padPx };
// Apply initial alpha = hard mask (no feather, no edge shift).
applyInpaintFeather(resultLayer, 0, 0);
}
state.layers.push(resultLayer);
state.activeLayerId = resultLayer.id;
state.lastInpaintLayerId = resultLayer.id;
@@ -220,6 +235,10 @@ export function wireInpaintButtons({
}
composite();
renderLayerPanel();
if (fullFrame) {
if (uiModule) uiModule.showToast('Image edit complete', 4500);
return;
}
// Reveal post-generation Feather + Edge Stroke sliders.
// Cap Edge Stroke at ±padPx so the slider can't ask for
// more AI buffer than we generated.