2026-04-16 18:55:18 +00:00
|
|
|
"""
|
|
|
|
|
Bug batch fixes — April 2026.
|
|
|
|
|
|
|
|
|
|
Covers:
|
|
|
|
|
- #594: .app-dialog and .file-rename-input have light theme overrides in style.css
|
|
|
|
|
- #576: workspace panel localStorage restore is gated on session.workspace presence (boot.js)
|
|
|
|
|
- #585: get_available_models() calls reload_config() before reading config cache
|
|
|
|
|
- #567: docker-compose.yml comment mentions macOS UID mismatch
|
|
|
|
|
- #590: _transcribeBlob already calls setComposerStatus('Transcribing…') — confirmed present
|
|
|
|
|
"""
|
|
|
|
|
import pathlib
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
REPO_ROOT = pathlib.Path(__file__).parent.parent
|
|
|
|
|
STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
|
|
|
|
BOOT_JS = (REPO_ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
|
|
|
|
COMPOSE = (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── #594: light theme dialog overrides ───────────────────────────────────────
|
|
|
|
|
|
2026-04-18 06:37:09 +00:00
|
|
|
def test_594_app_dialog_has_light_mode_override():
|
|
|
|
|
"""style.css must have a light mode rule targeting .app-dialog background."""
|
|
|
|
|
assert ':root:not(.dark) .app-dialog{' in STYLE_CSS, (
|
|
|
|
|
"Missing light mode override for .app-dialog — dialogs appear dark on light theme"
|
2026-04-16 18:55:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-04-18 06:37:09 +00:00
|
|
|
def test_594_app_dialog_input_has_light_mode_override():
|
|
|
|
|
"""style.css must have a light mode rule for .app-dialog-input."""
|
|
|
|
|
assert ":root:not(.dark) .app-dialog-input{" in STYLE_CSS, (
|
|
|
|
|
"Missing light mode override for .app-dialog-input"
|
2026-04-16 18:55:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-04-18 06:37:09 +00:00
|
|
|
def test_594_app_dialog_btn_has_light_mode_override():
|
|
|
|
|
"""style.css must have a light mode rule for .app-dialog-btn."""
|
|
|
|
|
assert ":root:not(.dark) .app-dialog-btn{" in STYLE_CSS, (
|
|
|
|
|
"Missing light mode override for .app-dialog-btn"
|
2026-04-16 18:55:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-04-18 06:37:09 +00:00
|
|
|
def test_594_app_dialog_close_has_light_mode_override():
|
|
|
|
|
"""style.css must have a light mode rule for .app-dialog-close."""
|
|
|
|
|
assert ":root:not(.dark) .app-dialog-close{" in STYLE_CSS, (
|
|
|
|
|
"Missing light mode override for .app-dialog-close"
|
2026-04-16 18:55:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-04-18 06:37:09 +00:00
|
|
|
def test_594_file_rename_input_has_light_mode_override():
|
|
|
|
|
"""style.css must have a light mode rule for .file-rename-input."""
|
|
|
|
|
assert ":root:not(.dark) .file-rename-input{" in STYLE_CSS, (
|
|
|
|
|
"Missing light mode override for .file-rename-input"
|
2026-04-16 18:55:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
fix(ui): dark-mode user bubbles use subtle tint + thinking card collapsible — v0.50.111 (#759)
## Summary
Rebased on behalf of @aronprins from fork branch `codex/dark-user-bubbles`. Two asset-only commits (PR screenshot add/remove) were dropped; the two code commits are applied cleanly on top of current master (v0.50.110).
### What changed
**Dark-mode user bubbles** (`static/style.css`):
- `:root.dark` now overrides `--user-bubble-bg`/`--user-bubble-border` to `var(--accent-bg-strong)` (a 15% opacity tint) — keeps the bubble visually subdued in dark skins instead of a glaring bright accent fill
- Removes 6 per-skin `--user-bubble-text` hacks (ares, mono, slate, poseidon, sisyphus, charizard); text falls back to `var(--text)` which is already correct in dark mode
- Adds `--user-bubble-placeholder` token; edit-area box-shadow now uses `--focus-ring` instead of hardcoded `rgba(255,255,255,.15)`
**Thinking card collapsibility** (`static/ui.js` + `static/style.css`):
- `_thinkingMarkup()` now includes `onclick` toggle and chevron affordance, matching the compression reference card pattern
- `.thinking-card-header` gets `display:flex; gap:8px` for proper icon/label/chevron alignment
**Tests**: 2 new in `test_bugbatch_apr2026.py` (dark bubble token contract + no-per-skin-hack assertion), 2 updated in `test_ui_card_animation.py` (flex header layout + onclick pattern).
1520 passed. QA 20/20. Browser verified: dark mode bubble uses subtle tint, thinking card toggles correctly.
(credit: @aronprins)
2026-04-20 08:12:45 +00:00
|
|
|
# ── dark-mode user bubble semantics ──────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def test_dark_user_bubbles_use_dark_tinted_surface():
|
|
|
|
|
"""Dark mode should keep user bubbles dark, with skin only tinting the bubble."""
|
|
|
|
|
assert "--user-bubble-bg: var(--accent-bg-strong);" in STYLE_CSS, (
|
|
|
|
|
"Dark mode user bubbles should use the dark accent tint, not the full bright accent fill"
|
|
|
|
|
)
|
|
|
|
|
assert "--user-bubble-border: var(--accent-bg-strong);" in STYLE_CSS, (
|
|
|
|
|
"Dark mode user bubble borders should match the quieter thinking-card border intensity"
|
|
|
|
|
)
|
|
|
|
|
assert "--user-bubble-text: var(--text);" in STYLE_CSS, (
|
|
|
|
|
"Dark mode user bubble text should inherit the theme text color"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_dark_user_bubbles_do_not_need_per_skin_text_hacks():
|
|
|
|
|
"""Dark-mode user bubble contrast should not rely on per-skin text overrides."""
|
|
|
|
|
assert re.search(r':root\.dark\[data-skin="[^"]+"\]\s*\{\s*--user-bubble-text:', STYLE_CSS) is None, (
|
|
|
|
|
"Dark-mode user bubble contrast should come from shared theme tokens, not per-skin text hacks"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-04-23 01:19:21 +00:00
|
|
|
def test_user_bubbles_define_selection_tokens_for_both_modes():
|
|
|
|
|
"""User bubbles need dedicated selection colors so selected text remains readable."""
|
|
|
|
|
assert "--user-selection-bg: rgba(0,0,0,.22);" in STYLE_CSS, (
|
|
|
|
|
"Light-mode user bubbles should define a darker selection fill for contrast"
|
|
|
|
|
)
|
|
|
|
|
assert "--user-selection-bg: rgba(255,255,255,.18);" in STYLE_CSS, (
|
|
|
|
|
"Dark-mode user bubbles should define a lighter selection fill for contrast"
|
|
|
|
|
)
|
|
|
|
|
assert "--user-selection-text: #fff;" in STYLE_CSS, (
|
|
|
|
|
"Light-mode user bubble selection should preserve readable text color"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_user_bubble_selection_is_scoped_to_user_message_body():
|
|
|
|
|
"""Selection override must apply only to user bubbles, including nested markdown nodes."""
|
|
|
|
|
assert '.msg-row[data-role="user"] .msg-body::selection,' in STYLE_CSS, (
|
|
|
|
|
"Missing selection override on the user message bubble"
|
|
|
|
|
)
|
|
|
|
|
assert '.msg-row[data-role="user"] .msg-body *::selection {' in STYLE_CSS, (
|
|
|
|
|
"Nested elements inside user messages must inherit the same selection colors"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-04-16 18:55:18 +00:00
|
|
|
# ── #576: workspace panel snap fix ───────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def test_576_panel_restore_gated_on_workspace():
|
|
|
|
|
"""boot.js: localStorage panel restore must be gated on session.workspace."""
|
|
|
|
|
# The guard must appear: session.workspace check before _workspacePanelMode='browse'
|
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042)
* feat: add manual 'Check for Updates' button in System settings (#785)
Add a 'Check now' button next to the version badge in the System
settings section, allowing users to manually trigger an update check
at any time without waiting for the automatic periodic check.
Changes:
- index.html: add button with spinner and status text inline with version badge
- panels.js: add checkUpdatesNow() calling /api/updates/check?force=1
with immediate feedback (checking... / up to date / X updates available)
- style.css: style the button block and spinner
- i18n.js: add 5 new keys (settings_check_now, settings_checking,
settings_up_to_date, settings_updates_available, settings_updates_disabled)
in all 6 locales (en, ru, es, de, zh, zh-Hant)
* fix: sanitize error message in checkUpdatesNow to avoid exposing paths
Review feedback: strip filesystem paths from error messages and cap
length to prevent internal details leaking into the UI.
* fix: fully sanitize error in update check — never expose raw e.message in UI
Previous partial fix (80cdaee) stripped filesystem paths from e.message but
still displayed the JS exception message to users. Per reviewer feedback and
project convention (NEVER expose raw e.message in UI), replace with:
- A generic user-facing i18n key (settings_update_check_failed) as default
- Fallback to API response body error if available (structured, not raw)
- Full error logged via console.warn for debugging
- Button disable-during-check already confirmed working (try/finally pattern)
- settings_update_check_failed key added in all 6 locales
* fix(#785): align HTML selectors with CSS and add regression tests
- Wrap update button in div#checkUpdatesBlock so CSS selectors apply
- Change button class from sm-btn to btn-tiny (matching stylesheet)
- Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny)
- Move spinner sizing to CSS class .spinner-xs
- Add 4 static tests in test_update_banner_fixes.py:
checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales
* feat: 'Keep workspace panel open' toggle in Appearance settings (#999)
* feat: categorize providers in setup wizard (#603)
- Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok),
Ollama, LM Studio to the onboarding quick-setup catalog
- Group providers into 3 categories: Easy start, Open/self-hosted,
Specialized — rendered as <optgroup> in the provider dropdown
- Generic base_url save logic (requires_base_url + default_base_url)
instead of hardcoded provider checks
- i18n keys for category labels in en, ru, es, zh, zh-Hant
* ci: re-run tests
* fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644
The test helper _available_models_with_cfg patches cfg in-memory but
get_available_models() calls reload_config() when the config file's
mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0
and _cfg_mtime starts at 0.0, triggering a reload that overwrites the
test's mock with on-disk content.
Fix: freeze _cfg_mtime to the current config file mtime inside the
helper, so reload_config() is not triggered during the test.
* fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests
- gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview
- x-ai: grok-4.20 → grok-3
- deepseek: deepseek-chat-v3-0324 → deepseek-chat
- Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for
gemini, deepseek, mistral, and x-ai through apply_onboarding_setup
* test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai
* fix(onboarding): correct stale model defaults for specialized providers
Three issues in the new specialized provider catalog (#1027 hold reason):
1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog
has the 3.1 family. Updated to `gemini-3.1-pro-preview`.
2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`.
Updated.
3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")`
which returns []. The catalog in api/config.py is keyed under "google"
(even though the agent's alias map normalizes google -> gemini).
Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces
the actual 5-model list. Also forward-compatible lookup for x-ai
(xai or x-ai key).
Without these fixes, users picking gemini or x-ai in the wizard would
see no model dropdown and the default_model written to config.yaml
would 404 on first chat.
deepseek default_model bumped from `deepseek-chat` to
`deepseek-chat-v3-0324` to match the test fixture's expectation and
the agent catalog's pinned version.
Added two regression tests:
- test_gemini_model_list_is_populated: pins the catalog-key correctness
- test_specialized_default_models_match_catalog: pins the version
prefixes (3.x for gemini, 4.x for grok)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: inline HTML preview in workspace panel (#779)
Render .html/.htm files as live previews in a sandboxed iframe instead
of showing raw source code. Adds an 'Open in browser' button to open
the file in a new tab.
Changes:
- workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing
in openFile(), and openInBrowser() function
- index.html: add sandboxed iframe element and 'Open in browser' button
in preview toolbar (visible only for HTML files)
- i18n.js: add 'open_in_browser' key in all 6 locales
The iframe uses sandbox='allow-scripts' for security. Download button
remains available alongside the new preview.
* docs: document sandbox security tradeoff for HTML preview
Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work.
Added code comment explaining the deliberate sandbox=allow-scripts choice:
scripts are needed for most HTML documents but the iframe is still origin-
isolated and cannot access parent cookies/data.
* fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading
routes.py: add inline_preview param — bypasses Content-Disposition:attachment for
text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe.
workspace.js: add &inline=1 to the iframe src URL.
test: add 5 static regression tests for the inline HTML preview.
* fix(security): CSP sandbox header for inline HTML preview
The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only
applies when HTML is loaded INSIDE that iframe. A user tricked into
opening /api/file/raw?path=evil.html&inline=1 directly in a top-level
tab (e.g. via a chat link) would render the HTML in the WebUI's origin
without any sandbox, giving the page full access to cookies and
localStorage.
Server-side Content-Security-Policy: sandbox allow-scripts mirrors the
iframe sandbox exactly: scripts run, but the document is treated as a
unique opaque origin (no allow-same-origin) and cannot read WebUI
cookies, localStorage, or postMessage to the parent regardless of how
the URL is accessed.
Added test_inline_html_response_sets_csp_sandbox to pin the header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43)
* docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209
The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the
stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in
without a corresponding CHANGELOG entry. Without this fix, the queue
feature ships silently and the bundled Cloudflare CSP relaxation in
api/helpers.py is also undocumented.
Adds two entries:
- Added: queue flyout (#1040) under v0.50.209
- Changed: CSP allowlist for Cloudflare Access deployments
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
|
|
|
# Panel pref key takes priority over runtime key (toolbar close must not clear preference)
|
|
|
|
|
assert "S.session&&S.session.workspace&&panelPref" in BOOT_JS, (
|
2026-04-16 18:55:18 +00:00
|
|
|
"Workspace panel localStorage restore must be gated on S.session.workspace "
|
|
|
|
|
"to prevent snap-open-then-closed on sessions without a workspace (#576)"
|
|
|
|
|
)
|
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042)
* feat: add manual 'Check for Updates' button in System settings (#785)
Add a 'Check now' button next to the version badge in the System
settings section, allowing users to manually trigger an update check
at any time without waiting for the automatic periodic check.
Changes:
- index.html: add button with spinner and status text inline with version badge
- panels.js: add checkUpdatesNow() calling /api/updates/check?force=1
with immediate feedback (checking... / up to date / X updates available)
- style.css: style the button block and spinner
- i18n.js: add 5 new keys (settings_check_now, settings_checking,
settings_up_to_date, settings_updates_available, settings_updates_disabled)
in all 6 locales (en, ru, es, de, zh, zh-Hant)
* fix: sanitize error message in checkUpdatesNow to avoid exposing paths
Review feedback: strip filesystem paths from error messages and cap
length to prevent internal details leaking into the UI.
* fix: fully sanitize error in update check — never expose raw e.message in UI
Previous partial fix (80cdaee) stripped filesystem paths from e.message but
still displayed the JS exception message to users. Per reviewer feedback and
project convention (NEVER expose raw e.message in UI), replace with:
- A generic user-facing i18n key (settings_update_check_failed) as default
- Fallback to API response body error if available (structured, not raw)
- Full error logged via console.warn for debugging
- Button disable-during-check already confirmed working (try/finally pattern)
- settings_update_check_failed key added in all 6 locales
* fix(#785): align HTML selectors with CSS and add regression tests
- Wrap update button in div#checkUpdatesBlock so CSS selectors apply
- Change button class from sm-btn to btn-tiny (matching stylesheet)
- Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny)
- Move spinner sizing to CSS class .spinner-xs
- Add 4 static tests in test_update_banner_fixes.py:
checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales
* feat: 'Keep workspace panel open' toggle in Appearance settings (#999)
* feat: categorize providers in setup wizard (#603)
- Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok),
Ollama, LM Studio to the onboarding quick-setup catalog
- Group providers into 3 categories: Easy start, Open/self-hosted,
Specialized — rendered as <optgroup> in the provider dropdown
- Generic base_url save logic (requires_base_url + default_base_url)
instead of hardcoded provider checks
- i18n keys for category labels in en, ru, es, zh, zh-Hant
* ci: re-run tests
* fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644
The test helper _available_models_with_cfg patches cfg in-memory but
get_available_models() calls reload_config() when the config file's
mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0
and _cfg_mtime starts at 0.0, triggering a reload that overwrites the
test's mock with on-disk content.
Fix: freeze _cfg_mtime to the current config file mtime inside the
helper, so reload_config() is not triggered during the test.
* fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests
- gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview
- x-ai: grok-4.20 → grok-3
- deepseek: deepseek-chat-v3-0324 → deepseek-chat
- Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for
gemini, deepseek, mistral, and x-ai through apply_onboarding_setup
* test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai
* fix(onboarding): correct stale model defaults for specialized providers
Three issues in the new specialized provider catalog (#1027 hold reason):
1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog
has the 3.1 family. Updated to `gemini-3.1-pro-preview`.
2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`.
Updated.
3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")`
which returns []. The catalog in api/config.py is keyed under "google"
(even though the agent's alias map normalizes google -> gemini).
Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces
the actual 5-model list. Also forward-compatible lookup for x-ai
(xai or x-ai key).
Without these fixes, users picking gemini or x-ai in the wizard would
see no model dropdown and the default_model written to config.yaml
would 404 on first chat.
deepseek default_model bumped from `deepseek-chat` to
`deepseek-chat-v3-0324` to match the test fixture's expectation and
the agent catalog's pinned version.
Added two regression tests:
- test_gemini_model_list_is_populated: pins the catalog-key correctness
- test_specialized_default_models_match_catalog: pins the version
prefixes (3.x for gemini, 4.x for grok)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: inline HTML preview in workspace panel (#779)
Render .html/.htm files as live previews in a sandboxed iframe instead
of showing raw source code. Adds an 'Open in browser' button to open
the file in a new tab.
Changes:
- workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing
in openFile(), and openInBrowser() function
- index.html: add sandboxed iframe element and 'Open in browser' button
in preview toolbar (visible only for HTML files)
- i18n.js: add 'open_in_browser' key in all 6 locales
The iframe uses sandbox='allow-scripts' for security. Download button
remains available alongside the new preview.
* docs: document sandbox security tradeoff for HTML preview
Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work.
Added code comment explaining the deliberate sandbox=allow-scripts choice:
scripts are needed for most HTML documents but the iframe is still origin-
isolated and cannot access parent cookies/data.
* fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading
routes.py: add inline_preview param — bypasses Content-Disposition:attachment for
text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe.
workspace.js: add &inline=1 to the iframe src URL.
test: add 5 static regression tests for the inline HTML preview.
* fix(security): CSP sandbox header for inline HTML preview
The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only
applies when HTML is loaded INSIDE that iframe. A user tricked into
opening /api/file/raw?path=evil.html&inline=1 directly in a top-level
tab (e.g. via a chat link) would render the HTML in the WebUI's origin
without any sandbox, giving the page full access to cookies and
localStorage.
Server-side Content-Security-Policy: sandbox allow-scripts mirrors the
iframe sandbox exactly: scripts run, but the document is treated as a
unique opaque origin (no allow-same-origin) and cannot read WebUI
cookies, localStorage, or postMessage to the parent regardless of how
the URL is accessed.
Added test_inline_html_response_sets_csp_sandbox to pin the header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43)
* docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209
The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the
stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in
without a corresponding CHANGELOG entry. Without this fix, the queue
feature ships silently and the bundled Cloudflare CSP relaxation in
api/helpers.py is also undocumented.
Adds two entries:
- Added: queue flyout (#1040) under v0.50.209
- Changed: CSP allowlist for Cloudflare Access deployments
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
|
|
|
assert "'hermes-webui-workspace-panel-pref'" in BOOT_JS, (
|
|
|
|
|
"Panel restore must check the preference key so toolbar close does not clear it"
|
|
|
|
|
)
|
2026-04-16 18:55:18 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_576_restore_happens_after_load_session():
|
|
|
|
|
"""boot.js: loadSession() must come before the panel restore guard."""
|
2026-06-27 20:51:46 +00:00
|
|
|
load_pos = BOOT_JS.find("await loadSession(saved, {preserveActiveInput:true})")
|
v0.50.209: check-for-updates, workspace toggle, HTML preview, provider categories, queue flyout docs (#1042)
* feat: add manual 'Check for Updates' button in System settings (#785)
Add a 'Check now' button next to the version badge in the System
settings section, allowing users to manually trigger an update check
at any time without waiting for the automatic periodic check.
Changes:
- index.html: add button with spinner and status text inline with version badge
- panels.js: add checkUpdatesNow() calling /api/updates/check?force=1
with immediate feedback (checking... / up to date / X updates available)
- style.css: style the button block and spinner
- i18n.js: add 5 new keys (settings_check_now, settings_checking,
settings_up_to_date, settings_updates_available, settings_updates_disabled)
in all 6 locales (en, ru, es, de, zh, zh-Hant)
* fix: sanitize error message in checkUpdatesNow to avoid exposing paths
Review feedback: strip filesystem paths from error messages and cap
length to prevent internal details leaking into the UI.
* fix: fully sanitize error in update check — never expose raw e.message in UI
Previous partial fix (80cdaee) stripped filesystem paths from e.message but
still displayed the JS exception message to users. Per reviewer feedback and
project convention (NEVER expose raw e.message in UI), replace with:
- A generic user-facing i18n key (settings_update_check_failed) as default
- Fallback to API response body error if available (structured, not raw)
- Full error logged via console.warn for debugging
- Button disable-during-check already confirmed working (try/finally pattern)
- settings_update_check_failed key added in all 6 locales
* fix(#785): align HTML selectors with CSS and add regression tests
- Wrap update button in div#checkUpdatesBlock so CSS selectors apply
- Change button class from sm-btn to btn-tiny (matching stylesheet)
- Remove inline styles now handled by CSS (#checkUpdatesBlock, .btn-tiny)
- Move spinner sizing to CSS class .spinner-xs
- Add 4 static tests in test_update_banner_fixes.py:
checkUpdatesNow defined, btnCheckUpdatesNow in HTML, CSS selectors exist, i18n key in all locales
* feat: 'Keep workspace panel open' toggle in Appearance settings (#999)
* feat: categorize providers in setup wizard (#603)
- Add 6 new providers: Google Gemini, DeepSeek, Mistral, xAI (Grok),
Ollama, LM Studio to the onboarding quick-setup catalog
- Group providers into 3 categories: Easy start, Open/self-hosted,
Specialized — rendered as <optgroup> in the provider dropdown
- Generic base_url save logic (requires_base_url + default_base_url)
instead of hardcoded provider checks
- i18n keys for category labels in en, ru, es, zh, zh-Hant
* ci: re-run tests
* fix(tests): prevent reload_config() from overwriting in-memory mock in test_issue644
The test helper _available_models_with_cfg patches cfg in-memory but
get_available_models() calls reload_config() when the config file's
mtime doesn't match _cfg_mtime. On CI, config.yaml exists so mtime > 0
and _cfg_mtime starts at 0.0, triggering a reload that overwrites the
test's mock with on-disk content.
Fix: freeze _cfg_mtime to the current config file mtime inside the
helper, so reload_config() is not triggered during the test.
* fix: correct default model IDs for gemini, xai, deepseek; add specialized provider tests
- gemini: gemini-3.1-pro-preview → gemini-2.5-pro-preview
- x-ai: grok-4.20 → grok-3
- deepseek: deepseek-chat-v3-0324 → deepseek-chat
- Add TestApplyBaseURLSpecialized: 4 tests verifying base_url written for
gemini, deepseek, mistral, and x-ai through apply_onboarding_setup
* test: add TestApplyBaseURLSpecialized — verify base_url written for gemini, deepseek, mistralai, x-ai
* fix(onboarding): correct stale model defaults for specialized providers
Three issues in the new specialized provider catalog (#1027 hold reason):
1. gemini default_model was `gemini-2.5-pro-preview` — agent's catalog
has the 3.1 family. Updated to `gemini-3.1-pro-preview`.
2. x-ai default_model was `grok-3` — agent's catalog has `grok-4.20`.
Updated.
3. gemini `models` list was sourcing from `_PROVIDER_MODELS.get("gemini")`
which returns []. The catalog in api/config.py is keyed under "google"
(even though the agent's alias map normalizes google -> gemini).
Switched to `_PROVIDER_MODELS.get("google")` so the wizard surfaces
the actual 5-model list. Also forward-compatible lookup for x-ai
(xai or x-ai key).
Without these fixes, users picking gemini or x-ai in the wizard would
see no model dropdown and the default_model written to config.yaml
would 404 on first chat.
deepseek default_model bumped from `deepseek-chat` to
`deepseek-chat-v3-0324` to match the test fixture's expectation and
the agent catalog's pinned version.
Added two regression tests:
- test_gemini_model_list_is_populated: pins the catalog-key correctness
- test_specialized_default_models_match_catalog: pins the version
prefixes (3.x for gemini, 4.x for grok)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: inline HTML preview in workspace panel (#779)
Render .html/.htm files as live previews in a sandboxed iframe instead
of showing raw source code. Adds an 'Open in browser' button to open
the file in a new tab.
Changes:
- workspace.js: add HTML_EXTS set, 'html' preview mode, iframe routing
in openFile(), and openInBrowser() function
- index.html: add sandboxed iframe element and 'Open in browser' button
in preview toolbar (visible only for HTML files)
- i18n.js: add 'open_in_browser' key in all 6 locales
The iframe uses sandbox='allow-scripts' for security. Download button
remains available alongside the new preview.
* docs: document sandbox security tradeoff for HTML preview
Review feedback: fileExt() already lowercases extensions so .HTML/.HTM work.
Added code comment explaining the deliberate sandbox=allow-scripts choice:
scripts are needed for most HTML documents but the iframe is still origin-
isolated and cannot access parent cookies/data.
* fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading
routes.py: add inline_preview param — bypasses Content-Disposition:attachment for
text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe.
workspace.js: add &inline=1 to the iframe src URL.
test: add 5 static regression tests for the inline HTML preview.
* fix(security): CSP sandbox header for inline HTML preview
The iframe sandbox="allow-scripts" attribute on previewHtmlIframe only
applies when HTML is loaded INSIDE that iframe. A user tricked into
opening /api/file/raw?path=evil.html&inline=1 directly in a top-level
tab (e.g. via a chat link) would render the HTML in the WebUI's origin
without any sandbox, giving the page full access to cookies and
localStorage.
Server-side Content-Security-Policy: sandbox allow-scripts mirrors the
iframe sandbox exactly: scripts run, but the document is treated as a
unique opaque origin (no allow-same-origin) and cannot read WebUI
cookies, localStorage, or postMessage to the parent regardless of how
the URL is accessed.
Added test_inline_html_response_sets_csp_sandbox to pin the header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: v0.50.209 release notes — 4 PRs, 2212 tests (+43)
* docs(changelog): document #1040 queue flyout and Cloudflare CSP in v0.50.209
The stage commit ed2bd18 listed v0.50.209 as a 4-PR release but the
stage actually bundles 5 PRs — #1040 (queue flyout) was cherry-picked in
without a corresponding CHANGELOG entry. Without this fix, the queue
feature ships silently and the bundled Cloudflare CSP relaxation in
api/helpers.py is also undocumented.
Adds two entries:
- Added: queue flyout (#1040) under v0.50.209
- Changed: CSP allowlist for Cloudflare Access deployments
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:33:41 +00:00
|
|
|
restore_pos = BOOT_JS.find("panelPref")
|
2026-04-16 18:55:18 +00:00
|
|
|
assert load_pos != -1, "loadSession call not found in boot.js"
|
|
|
|
|
assert restore_pos != -1, "workspace panel restore guard not found"
|
|
|
|
|
assert load_pos < restore_pos, (
|
|
|
|
|
"loadSession() must run before the panel restore guard "
|
|
|
|
|
"so S.session.workspace is known at restore time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── #585: get_available_models reloads config ─────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def test_585_get_available_models_calls_reload_config():
|
|
|
|
|
"""api/config.py: get_available_models() must do a mtime-based reload check."""
|
|
|
|
|
config_src = (REPO_ROOT / "api" / "config.py").read_text(encoding="utf-8")
|
2026-06-09 05:36:18 +00:00
|
|
|
fn_start = config_src.find("def get_available_models(")
|
2026-04-16 18:55:18 +00:00
|
|
|
assert fn_start != -1, "get_available_models not found"
|
|
|
|
|
fn_body_end = config_src.find('"""', config_src.find('"""', fn_start + 30) + 3) + 3
|
|
|
|
|
# Must check mtime before reading config
|
|
|
|
|
mtime_pos = config_src.find("_current_mtime", fn_body_end)
|
|
|
|
|
active_prov_pos = config_src.find("active_provider = None", fn_body_end)
|
|
|
|
|
assert mtime_pos != -1, (
|
|
|
|
|
"get_available_models() must check config file mtime before reading cache (#585)"
|
|
|
|
|
)
|
|
|
|
|
assert mtime_pos < active_prov_pos, (
|
|
|
|
|
"mtime check must come before active_provider = None in get_available_models()"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── #567: docker-compose UID note ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def test_567_compose_mentions_macos_uid():
|
|
|
|
|
"""docker-compose.yml must mention macOS UID / id -u to help macOS users."""
|
|
|
|
|
assert "macOS" in COMPOSE or "macos" in COMPOSE.lower(), (
|
|
|
|
|
"docker-compose.yml should mention macOS UID issue (#567)"
|
|
|
|
|
)
|
|
|
|
|
assert "id -u" in COMPOSE, (
|
|
|
|
|
"docker-compose.yml should tell users to run 'id -u' to find their UID (#567)"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── #590: transcription spinner already present ───────────────────────────────
|
|
|
|
|
|
|
|
|
|
def test_590_transcribing_status_shown_before_fetch():
|
|
|
|
|
"""boot.js: setComposerStatus('Transcribing…') must fire before the fetch call."""
|
|
|
|
|
transcribe_fn_start = BOOT_JS.find("async function _transcribeBlob(")
|
|
|
|
|
assert transcribe_fn_start != -1, "_transcribeBlob not found in boot.js"
|
|
|
|
|
fn_body = BOOT_JS[transcribe_fn_start:transcribe_fn_start + 600]
|
|
|
|
|
status_pos = fn_body.find("setComposerStatus('Transcribing")
|
|
|
|
|
fetch_pos = fn_body.find("await fetch(")
|
|
|
|
|
assert status_pos != -1, (
|
|
|
|
|
"setComposerStatus('Transcribing…') must be called before the fetch in _transcribeBlob"
|
|
|
|
|
)
|
|
|
|
|
assert fetch_pos != -1, "await fetch not found in _transcribeBlob"
|
|
|
|
|
assert status_pos < fetch_pos, (
|
|
|
|
|
"setComposerStatus('Transcribing…') must appear before 'await fetch' "
|
|
|
|
|
"so the UI shows a spinner immediately on stop (#590)"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_590_recording_stops_before_transcribe():
|
2026-07-01 23:24:38 +00:00
|
|
|
"""boot.js: _setRecording(false) must fire in onstop before the pinned send path."""
|
Release exp-v0.52.49: dictation preserves composer text (#5895) (#5978)
* fix(dictation): preserve composer text when server STT commits
Dictation via the server STT path (MediaRecorder → POST /api/transcribe)
replaced any text already in the composer with the recognized transcript.
Browser SpeechRecognition worked correctly; only the async path was broken.
Root cause: the MediaRecorder.onstop handler calls _setRecording(false)
before awaiting _transcribeBlob's fetch. _setRecording(false) clears the
module-level _prefix. By the time the fetch resolves and _commitTranscript
runs, _prefix is '' — so the existing append logic collapses to
'just the transcript' and overwrites the textarea.
Browser SR dodged this because sr.onend builds the merged string into a
local variable before invoking _setRecording(false). Server STT is async
and could not use that pattern without help.
Fix:
- _commitTranscript accepts an optional prefixOverride parameter.
- _transcribeBlob snapshots _prefix into a local before the await.
- Pass the snapshot through to _commitTranscript.
Add a Settings → Voice → 'Append dictation to composer' toggle (default
ON) so users can opt into replace mode if they prefer the old buggy
behavior. The bug fix changes the default to append, matching what the
browser SR path always did.
i18n: new keys dictation_append_label / dictation_append_desc added to
all 14 locale blocks in static/i18n.js (English fallback where no native
translation exists).
Files: static/boot.js (+43/-7), static/index.html (+7), static/i18n.js (+30).
* fix(dictation): snapshot _prefix in onstop, not inside _transcribeBlob
First attempt at the append-mode fix took the snapshot inside
_transcribeBlob via 'const prefixSnapshot = _prefix'. By the time
_transcribeBlob runs (awaited from recorder.onstop), the sync block
above has already called _setRecording(false), which clears _prefix
inside _setRecording's !on branch. So the snapshot was always '' and
the append path fell through to the replace branch.
Move the snapshot into recorder.onstop, before _setRecording(false)
runs. Pass it as a parameter into _transcribeBlob so the call site
doesn't need to re-read the (now-cleared) module-level _prefix.
No behavior change for the browser-SR path — sr.onend still builds
the merged string into a local variable before invoking
_setRecording(false), and was already correct.
* fix(dictation): trim comments to fit test_590_recording_stops_before_transcribe
test_bugbatch_apr2026.py::test_590_recording_stops_before_transcribe slices
700 chars from 'recorder.onstop' and asserts that _setRecording(false)
comes before _sendRawAudio(/_transcribeBlob( inside that window. With
the verbose 6-line comment block from the previous fix, the actual
call sites landed at offsets 834/890 — past the 700-char window — so
the test's .find() returned -1 and the assertion failed mechanically.
Also: the verbose comment in _transcribeBlob contained the substring
'recorder.onstop', which made the test's first .find() land in the
comment instead of the actual handler, returning a wrong onstop_start.
Trim both comment blocks to single lines that fit within the test's
window and don't contain the literal 'recorder.onstop' substring.
Functional behavior unchanged from the previous fix; this is purely
a comment-shape adjustment to satisfy the brittle test.
* fix(tests): anchor test_590 on handler definition + widen window per review
The reviewer's fix-spec for test_590_recording_stops_before_transcribe
was more thorough than my single-shot comment trim:
(1) Reword the _transcribeBlob comment so it doesn't contain the
literal 'recorder.onstop' substring.
(2) Make the test robust to the handler growing: anchor on
'recorder.onstop=async' instead of 'recorder.onstop', and
widen the fixed 700-char window to 1200.
Applied both. With these changes the test passes regardless of how
much comment text gets added in or around the handler in the future.
Also restored the verbose explanatory comments in both locations
(the previous a7f84027 had trimmed them to fit the brittle 700-char
window — that workaround is no longer needed).
Backward-compat check: the new test logic also passes against
origin/master (verified locally), so this change doesn't regress
the existing green-master invariant.
* fix(dictation): preserve user's mid-transcription edits in async server STT
Greptile flagged this as a 4/5 concern on the append-mode branch:
the async server-STT path appended the dictated text to the stale
snapshot (`_prefix` captured at recording start), which clobbered
any keystrokes the user made during the ~1s transcription round-trip.
Before this fix, with append mode ON:
User typed before mic: 'Hello, '
User typed during STT: 'Hello, from K' (typed while waiting)
Dictated text: 'world'
Result: 'Hello, world' (lost 'from K')
After this fix:
User typed before mic: 'Hello, '
User typed during STT: 'Hello, from K'
Dictated text: 'world'
Result: 'Hello, from K world' (live state preserved)
The fix prefers live `ta.value` as the append base when the textarea
is non-empty, falling back to the `prefixOverride` snapshot only
when the textarea is genuinely empty (the original empty-composer path
where nothing to race with).
Adds a regression test in test_raw_audio_upload.py that asserts the
new `ta.value ||` fallback structure on `_commitTranscript`.
* fix(dictation): don't resurrect cleared text on async transcription resolve
Greptile P1 (re-reviewed at 20:07 UTC): the previous race fix
(`ta.value || (prefixOverride ?? _prefix)`) treats an empty
`ta.value` as falsy and falls back to the recording-start snapshot.
If the user explicitly clears the textarea while server transcription
is in flight, the cleared text reappears with the dictated text
appended — the user's intent is violated.
Fix: when prefixOverride IS passed (server-STT path), trust live
ta.value UNCONDITIONALLY — even when empty. The fallback to _prefix
only fires when no prefixOverride is given AND ta.value is empty,
which is purely defensive (no current caller hits this branch — the
browser-SR path commits inline in sr.onend without calling this
function).
Concrete repro:
User typed before mic: 'Hello, '
User clears textarea during STT
Dictated text: 'world'
OLD (0239bb4e) result: 'Hello, world' (cleared text resurrected)
NEW result: 'world' (user intent respected)
Greptile's suggested one-liner:
const base = prefixOverride !== undefined ? ta.value : (ta.value || _prefix);
Applied with expanded inline comment documenting the three concerns
this function balances (race, clear-during-STT, defensive fallback).
* Release exp-v0.52.49: preserve composer text on server STT commit (#5895)
---------
Co-authored-by: Konstantin M <kopamedve@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-12 03:47:54 +00:00
|
|
|
# Anchor on the handler definition (not just the substring "recorder.onstop",
|
|
|
|
|
# which can collide with comments referencing the handler) and widen the
|
|
|
|
|
# window so future comment additions don't push the send path out of range.
|
|
|
|
|
onstop_start = BOOT_JS.find("recorder.onstop=async")
|
|
|
|
|
assert onstop_start != -1, "recorder.onstop=async handler not found"
|
|
|
|
|
onstop_body = BOOT_JS[onstop_start:onstop_start + 1200]
|
2026-04-16 18:55:18 +00:00
|
|
|
rec_pos = onstop_body.find("_setRecording(false)")
|
|
|
|
|
blob_pos = onstop_body.find("_transcribeBlob(")
|
2026-07-01 23:24:38 +00:00
|
|
|
raw_pos = onstop_body.find("_sendRawAudio(")
|
|
|
|
|
send_pos = raw_pos if raw_pos != -1 and (blob_pos == -1 or raw_pos < blob_pos) else blob_pos
|
|
|
|
|
assert rec_pos != -1 and send_pos != -1
|
|
|
|
|
assert rec_pos < send_pos, (
|
|
|
|
|
"_setRecording(false) must come before the pinned send path so the mic icon clears immediately"
|
2026-04-16 18:55:18 +00:00
|
|
|
)
|