From 8cfdb8e2e5451a7c7cc3c4f29d6f86918d764cb9 Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Mon, 26 Oct 2026 16:49:01 +0000 Subject: [PATCH 1/4] Render saved action labels as text instead of HTML --- static/js/editor/actions-panel.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/static/js/editor/actions-panel.js b/static/js/editor/actions-panel.js index a41d86817..5cfc61182 100644 --- a/static/js/editor/actions-panel.js +++ b/static/js/editor/actions-panel.js @@ -32,10 +32,10 @@ export const ACTION_COMMANDS = Object.freeze([ const STORAGE_KEY = 'ge-actions'; -function el(tag, className, html) { +function el(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; - if (html != null) node.innerHTML = html; + if (text != null) node.textContent = text; return node; } @@ -100,7 +100,11 @@ export function createActionsPanel({ showToast = () => {}, getLayers = null, sel } steps.forEach((step, index) => { const command = commandById(step); - const row = el('div', 'ge-actions-step', `${index + 1}${command ? command[1] : step}`); + const row = el('div', 'ge-actions-step'); + row.append( + el('span', 'ge-actions-step-index', String(index + 1)), + el('span', 'ge-actions-step-label', command ? command[1] : step), + ); const remove = el('button', 'ge-action-remove', '✕'); remove.type = 'button'; remove.addEventListener('click', () => { steps.splice(index, 1); renderList(); }); From 9ab440c4cbe6d0c97980050a92f5fdbfaf9931a4 Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Mon, 26 Oct 2026 16:49:01 +0000 Subject: [PATCH 2/4] Keep Gallery image-model picker tests with the editor client --- tests/test_gallery_image_model_picker.py | 39 ++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/test_gallery_image_model_picker.py diff --git a/tests/test_gallery_image_model_picker.py b/tests/test_gallery_image_model_picker.py new file mode 100644 index 000000000..160355210 --- /dev/null +++ b/tests/test_gallery_image_model_picker.py @@ -0,0 +1,39 @@ +"""Source checks for the gallery Settings-tab image-model picker. + +The Quick Edit model picker lives in the editor; HARNESS mirrors it into the +gallery Settings tab, fed by GET /api/editor/image-models and sharing the +same localStorage key (`ge-ai-command-model`) so the two stay in sync. +""" +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +GALLERY_JS = (ROOT / "static/js/gallery.js").read_text(encoding="utf-8") + + +def test_settings_tab_has_image_model_picker(): + assert 'id="gallery-image-model"' in GALLERY_JS + assert "/api/editor/image-models" in GALLERY_JS + assert "ge-ai-command-model" in GALLERY_JS + + +def test_image_model_picker_lives_in_the_settings_container(): + settings_idx = GALLERY_JS.index('id="gallery-settings-container"') + picker_idx = GALLERY_JS.index('id="gallery-image-model"') + assert picker_idx > settings_idx + + +def test_picker_persists_with_the_shared_localstorage_key(): + assert "localStorage.setItem(_GE_MODEL_KEY" in GALLERY_JS + assert "localStorage.getItem(_GE_MODEL_KEY)" in GALLERY_JS + + +def test_cloud_image_models_are_listed_alongside_the_local_ones(): + """Gateway endpoints (OpenRouter) register as llm but carry image models. + + The picker must surface those ids too — otherwise the only selectable + edit model is the local Qwen server. + """ + routes = (ROOT / "routes/editor_agent_routes.py").read_text(encoding="utf-8") + assert "from src.image_model_ids import looks_like_image_generation_model" in routes + assert 'image_endpoint = getattr(ep, "model_type", None) == "image"' in routes + assert "if not image_endpoint and not looks_like_image_generation_model(name):" in routes From 106b1e7c86fc2f4b0b997cd2d966a2c21fe6a482 Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Mon, 26 Oct 2026 16:50:09 +0000 Subject: [PATCH 3/4] Verify saved action names and steps cannot inject HTML --- tests/test_editor_actions_panel_security.py | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_editor_actions_panel_security.py diff --git a/tests/test_editor_actions_panel_security.py b/tests/test_editor_actions_panel_security.py new file mode 100644 index 000000000..269941964 --- /dev/null +++ b/tests/test_editor_actions_panel_security.py @@ -0,0 +1,50 @@ +import json +from pathlib import Path +import subprocess + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_action_names_and_unknown_steps_are_literal_text(): + script = r''' + const fs = require('fs'); + const { chromium } = require('playwright'); + (async () => { + const browser = await chromium.launch({ headless: true }); + try { + const page = await browser.newPage(); + await page.route('http://actions.test/**', route => route.fulfill({ + status: 200, contentType: 'text/html', body: '', + })); + await page.goto('http://actions.test/'); + const source = fs.readFileSync(process.argv[1], 'utf8'); + const moduleUrl = 'data:text/javascript;base64,' + Buffer.from(source).toString('base64'); + const result = await page.evaluate(async url => { + const attack = ''; + localStorage.setItem('ge-actions', JSON.stringify({ [attack]: ['flip-h'] })); + const { createActionsPanel } = await import(url); + const panel = createActionsPanel(); + document.body.append(panel.element); + panel.setSteps([attack]); + return { + images: panel.element.querySelectorAll('img').length, + saved: panel.element.querySelector('.ge-actions-saved-row button').textContent, + step: panel.element.querySelector('.ge-actions-step-label').textContent, + executed: Boolean(window.__actionsXss), + attack, + }; + }, moduleUrl); + console.log(JSON.stringify(result)); + } finally { await browser.close(); } + })().catch(error => { console.error(error); process.exit(1); }); + ''' + result = subprocess.run( + ['node', '-e', script, str(ROOT / 'static/js/editor/actions-panel.js')], + cwd=ROOT, capture_output=True, text=True, timeout=30, + ) + assert result.returncode == 0, result.stderr + observed = json.loads(result.stdout) + assert observed['images'] == 0 + assert observed['saved'] == observed['step'] == observed['attack'] + assert not observed['executed'] From 0b8176b11ed2ea18fbbb2c43cbdabd655e712bfd Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Mon, 26 Oct 2026 16:57:01 +0000 Subject: [PATCH 4/4] Keep streamed-chat module identity consistently cache-busted --- static/index.html | 2 +- static/js/chat.js | 2 +- static/sw.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/static/index.html b/static/index.html index 72a586a20..901b42510 100644 --- a/static/index.html +++ b/static/index.html @@ -2686,7 +2686,7 @@ - + diff --git a/static/js/chat.js b/static/js/chat.js index 5cab3b003..09c7ed274 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -9,7 +9,7 @@ import Storage from './storage.js'; import uiModule from './ui.js?v=20260916largetoolscroll1'; import sessionModule from './sessions.js'; import chatRenderer, { renderToolIcon } from './chatRenderer.js?v=20260914metricssummary1'; -import chatStream from './chatStream.js'; +import chatStream from './chatStream.js?v=20261026devsync1'; import { addAITTSButton } from './tts-ai.js'; import markdownModule from './markdown.js'; import spinnerModule from './spinner.js'; diff --git a/static/sw.js b/static/sw.js index bca88b00d..282e599f9 100644 --- a/static/sw.js +++ b/static/sw.js @@ -84,7 +84,7 @@ const PRECACHE = [ '/static/js/gallery.js?v=20260910promptcopy1', '/static/js/chatRenderer.js?v=20260914metricssummary1', '/static/js/codeRunner.js?v=20260831richtexttools91', - '/static/js/chatStream.js', + '/static/js/chatStream.js?v=20261026devsync1', '/static/js/chat.js?v=20260917toolttft1', '/static/js/cookbook.js', '/static/js/search-chat.js',