Merge remote-tracking branch 'origin/dev' into fix/editor-followups-20261030

Reconcile branch with current dev (bebf53be).
Resolve conflicts in static/sw.js (bump cache version and preserve panel precache)
and src/ai_interaction.py (preserve upstream quality handling with per-user settings).

Remediate PR #6992 findings:
- A (CI & Playwright): Pin playwright==1.62.0 in requirements-dev and CI workflow; guard browser test imports against pytest runner collection failure.
- B (Per-User Image Model Settings): Support per-user image_model and image_edit_model in editor_image_models() and ai_interaction; enable non-admin preference persistence via /api/prefs fallback.
- C (CodeQL Regex Alert 1212): Replace polynomial-backtracking regex in routes/editor_agent_routes.py with linear token-based intent matcher.
- D (Undo/Redo rembg layer): Clear stale rembgLiveLayer and rembgLiveSnap references in _restoreState() to prevent edge tuner from corrupting detached layers.
This commit is contained in:
Alexandre Teixeira
2026-10-31 12:14:12 +00:00
104 changed files with 5097 additions and 457 deletions
+30 -2
View File
@@ -5329,6 +5329,8 @@ function _restoreState(snap) {
// inside _buildEditor, so we touch the DOM here).
const sizeLabel = document.getElementById('ge-canvas-size');
if (sizeLabel) sizeLabel.textContent = `${state.imgWidth}×${state.imgHeight} px`;
state.rembgLiveLayer = null;
state.rembgLiveSnap = null;
}
function undo() {
@@ -5787,7 +5789,20 @@ function _updateBrushCursor(e) {
state.cursorEl.style.height = diameterY + 'px';
}
state.cursorEl.classList.toggle('ge-brush-cursor--pixel', pixelBrush);
state.cursorEl.style.transform = `translate3d(${clientX - diameterX / 2}px, ${clientY - diameterY / 2}px, 0)`;
// Updating an inline style on every move invalidates document-wide :has()
// selectors. A paused animation keeps the same transform in the rendering
// pipeline without mutating the cursor's style attribute on every frame.
const cursorTransform = `translate3d(${clientX - diameterX / 2}px, ${clientY - diameterY / 2}px, 0)`;
if (!state.cursorEl._positionAnimation || state.cursorEl._positionAnimation.playState === 'idle') {
state.cursorEl._positionAnimation = state.cursorEl.animate(
[{ transform: cursorTransform }, { transform: cursorTransform }],
{ duration: 1, fill: 'both' },
);
state.cursorEl._positionAnimation.pause();
state.cursorEl._positionAnimation.currentTime = 0;
} else {
state.cursorEl._positionAnimation.effect.setKeyframes([{ transform: cursorTransform }, { transform: cursorTransform }]);
}
if (state.cursorEl.style.display !== 'block') {
state.cursorEl.style.display = 'block';
}
@@ -14125,7 +14140,20 @@ export function closeEditor(options = {}) {
const h = state.editorDocClickHandlers.pop();
try { document.removeEventListener('click', h); } catch {}
}
if (state.cursorEl) { state.cursorEl.remove(); state.cursorEl = null; }
if (_cursorRaf) { cancelAnimationFrame(_cursorRaf); _cursorRaf = null; }
_pendingCursorEvent = null;
_lastCursorRect = null;
state._lastCursorDiameterX = null;
state._lastCursorDiameterY = null;
state._lastCursorBorderColor = null;
if (state.cursorEl) {
if (state.cursorEl._positionAnimation) {
try { state.cursorEl._positionAnimation.cancel(); } catch {}
state.cursorEl._positionAnimation = null;
}
state.cursorEl.remove();
state.cursorEl = null;
}
try { document.getElementById('ge-cursor-layer')?.remove(); } catch {}
// Tear down all floating popups + the dock so closing the editor
// doesn't leave stale chips/panels behind on top of the gallery.