2026-03-31 03:40:19 +00:00
async function api ( path , opts = { } ) {
2026-04-16 18:20:08 +00:00
// Strip leading slash so URL resolves relative to location.href (supports subpath mounts)
const rel = path . startsWith ( '/' ) ? path . slice ( 1 ) : path ;
2026-05-01 04:47:36 +00:00
const url = new URL ( rel , document . baseURI || location . href ) ;
2026-05-19 18:41:00 +00:00
const timeoutMs = Object . prototype . hasOwnProperty . call ( opts , 'timeoutMs' ) ? opts . timeoutMs : 30000 ;
2026-05-28 03:08:38 +00:00
const timeoutToast = opts . timeoutToast !== false ;
2026-06-16 18:42:01 +00:00
const maxAttempts = Object . prototype . hasOwnProperty . call ( opts , 'retries' ) ? Math . max ( 0 , Number ( opts . retries ) || 0 ) + 1 : 3 ;
const retryTimeouts = opts . retryTimeouts === true ;
const retryStatuses = Array . isArray ( opts . retryStatuses ) ? opts . retryStatuses . map ( Number ) . filter ( Number . isFinite ) : [ ] ;
const retryDelayMs = Object . prototype . hasOwnProperty . call ( opts , 'retryDelayMs' ) ? Math . max ( 0 , Number ( opts . retryDelayMs ) || 0 ) : 350 ;
v0.50.223: model picker, idle retry, drag-drop, CSP, clipboard copy (#1127)
* fix(#604): model picker shows all configured providers
Two fixes to ensure the model picker surface every provider a user has
configured:
1. Added env var detection for XAI_API_KEY (→ x-ai) and MISTRAL_API_KEY
(→ mistralai). Previously these providers were only detectable via
hermes auth or credential pool, not via environment variables.
2. Added config.yaml providers section scanning. Users who configure
providers in config.yaml (e.g. providers.anthropic.api_key) without
setting the corresponding env var will now see those providers in the
model picker. Only providers with known model catalogs are added.
- Added 12 regression tests
* fix(#1112): allow Google Fonts in CSP style-src and font-src
Mermaid themes inject @import for fonts.googleapis.com at render time.
CSP style-src blocked these requests, causing console violations.
- Add https://fonts.googleapis.com to style-src (CSS stylesheets)
- Add https://fonts.gstatic.com to font-src (WOFF2/WOFF font files)
- Add 3 regression tests + verify existing CSP tests still pass
* fix(#1118): retry api() calls on network errors after long idle
After a long idle period, the browser's TCP keep-alive connection to the
server can become stale. The next fetch() throws a TypeError (network
failure), causing 'Failed to load session' instead of transparently
reconnecting.
- Added retry loop in api() (workspace.js): up to 3 attempts
- Only retries on TypeError (network failures), NOT on HTTP errors (4xx/5xx)
- 401 redirects still fire immediately
- Added 6 regression tests
* feat(#1116): composer placeholder reflects active profile name
When a named profile is active (not 'default'), the composer placeholder
and title bar show the profile name (capitalised) instead of the global
bot_name. Falls back to bot_name/'Hermes' for the default profile.
- boot.js: applyBotName() checks S.activeProfile before _botName
- panels.js: switchToProfile() calls applyBotName() after switch
- Added 5 regression tests
* feat(#1097): drag and drop workspace files into chat composer
Files and folders in the workspace file tree are now draggable.
Dropping them into the composer inserts @path reference at cursor
position. OS file drag-and-drop (attach files) still works.
- ui.js: _renderTreeItems sets draggable + dragstart with ws-path
- panels.js: drop handler checks for application/ws-path first,
inserts @path with smart spacing and cursor positioning
- Added 9 regression tests
* fix(#1096): copy buttons work — add clipboard-write Permissions-Policy
Copy buttons on messages and code blocks were silently failing because
the Permissions-Policy header did not include clipboard-write=(self).
Firefox blocks navigator.clipboard.writeText() without explicit permission.
- api/helpers.py: add clipboard-write=(self) to Permissions-Policy
- ui.js: _copyText now catches clipboard API errors and falls back
to execCommand('copy'). _fallbackCopy extracted as separate function
with proper focus() call and visible-but-hidden positioning (not -9999px)
- Added 8 regression tests
* chore: CHANGELOG for v0.50.223
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 22:29:02 +00:00
// Retry up to 2 times on network errors (e.g. stale keep-alive after long idle).
2026-06-16 18:42:01 +00:00
// Callers may opt into retrying timeouts / transient server statuses for idempotent GETs.
v0.50.223: model picker, idle retry, drag-drop, CSP, clipboard copy (#1127)
* fix(#604): model picker shows all configured providers
Two fixes to ensure the model picker surface every provider a user has
configured:
1. Added env var detection for XAI_API_KEY (→ x-ai) and MISTRAL_API_KEY
(→ mistralai). Previously these providers were only detectable via
hermes auth or credential pool, not via environment variables.
2. Added config.yaml providers section scanning. Users who configure
providers in config.yaml (e.g. providers.anthropic.api_key) without
setting the corresponding env var will now see those providers in the
model picker. Only providers with known model catalogs are added.
- Added 12 regression tests
* fix(#1112): allow Google Fonts in CSP style-src and font-src
Mermaid themes inject @import for fonts.googleapis.com at render time.
CSP style-src blocked these requests, causing console violations.
- Add https://fonts.googleapis.com to style-src (CSS stylesheets)
- Add https://fonts.gstatic.com to font-src (WOFF2/WOFF font files)
- Add 3 regression tests + verify existing CSP tests still pass
* fix(#1118): retry api() calls on network errors after long idle
After a long idle period, the browser's TCP keep-alive connection to the
server can become stale. The next fetch() throws a TypeError (network
failure), causing 'Failed to load session' instead of transparently
reconnecting.
- Added retry loop in api() (workspace.js): up to 3 attempts
- Only retries on TypeError (network failures), NOT on HTTP errors (4xx/5xx)
- 401 redirects still fire immediately
- Added 6 regression tests
* feat(#1116): composer placeholder reflects active profile name
When a named profile is active (not 'default'), the composer placeholder
and title bar show the profile name (capitalised) instead of the global
bot_name. Falls back to bot_name/'Hermes' for the default profile.
- boot.js: applyBotName() checks S.activeProfile before _botName
- panels.js: switchToProfile() calls applyBotName() after switch
- Added 5 regression tests
* feat(#1097): drag and drop workspace files into chat composer
Files and folders in the workspace file tree are now draggable.
Dropping them into the composer inserts @path reference at cursor
position. OS file drag-and-drop (attach files) still works.
- ui.js: _renderTreeItems sets draggable + dragstart with ws-path
- panels.js: drop handler checks for application/ws-path first,
inserts @path with smart spacing and cursor positioning
- Added 9 regression tests
* fix(#1096): copy buttons work — add clipboard-write Permissions-Policy
Copy buttons on messages and code blocks were silently failing because
the Permissions-Policy header did not include clipboard-write=(self).
Firefox blocks navigator.clipboard.writeText() without explicit permission.
- api/helpers.py: add clipboard-write=(self) to Permissions-Policy
- ui.js: _copyText now catches clipboard API errors and falls back
to execCommand('copy'). _fallbackCopy extracted as separate function
with proper focus() call and visible-but-hidden positioning (not -9999px)
- Added 8 regression tests
* chore: CHANGELOG for v0.50.223
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 22:29:02 +00:00
let lastErr ;
2026-06-16 18:42:01 +00:00
for ( let attempt = 0 ; attempt < maxAttempts ; attempt ++ ) {
2026-05-19 18:41:00 +00:00
let controller = null ;
let timeoutId = null ;
let didTimeout = false ;
let upstreamSignal = null ;
let upstreamAbort = null ;
v0.50.223: model picker, idle retry, drag-drop, CSP, clipboard copy (#1127)
* fix(#604): model picker shows all configured providers
Two fixes to ensure the model picker surface every provider a user has
configured:
1. Added env var detection for XAI_API_KEY (→ x-ai) and MISTRAL_API_KEY
(→ mistralai). Previously these providers were only detectable via
hermes auth or credential pool, not via environment variables.
2. Added config.yaml providers section scanning. Users who configure
providers in config.yaml (e.g. providers.anthropic.api_key) without
setting the corresponding env var will now see those providers in the
model picker. Only providers with known model catalogs are added.
- Added 12 regression tests
* fix(#1112): allow Google Fonts in CSP style-src and font-src
Mermaid themes inject @import for fonts.googleapis.com at render time.
CSP style-src blocked these requests, causing console violations.
- Add https://fonts.googleapis.com to style-src (CSS stylesheets)
- Add https://fonts.gstatic.com to font-src (WOFF2/WOFF font files)
- Add 3 regression tests + verify existing CSP tests still pass
* fix(#1118): retry api() calls on network errors after long idle
After a long idle period, the browser's TCP keep-alive connection to the
server can become stale. The next fetch() throws a TypeError (network
failure), causing 'Failed to load session' instead of transparently
reconnecting.
- Added retry loop in api() (workspace.js): up to 3 attempts
- Only retries on TypeError (network failures), NOT on HTTP errors (4xx/5xx)
- 401 redirects still fire immediately
- Added 6 regression tests
* feat(#1116): composer placeholder reflects active profile name
When a named profile is active (not 'default'), the composer placeholder
and title bar show the profile name (capitalised) instead of the global
bot_name. Falls back to bot_name/'Hermes' for the default profile.
- boot.js: applyBotName() checks S.activeProfile before _botName
- panels.js: switchToProfile() calls applyBotName() after switch
- Added 5 regression tests
* feat(#1097): drag and drop workspace files into chat composer
Files and folders in the workspace file tree are now draggable.
Dropping them into the composer inserts @path reference at cursor
position. OS file drag-and-drop (attach files) still works.
- ui.js: _renderTreeItems sets draggable + dragstart with ws-path
- panels.js: drop handler checks for application/ws-path first,
inserts @path with smart spacing and cursor positioning
- Added 9 regression tests
* fix(#1096): copy buttons work — add clipboard-write Permissions-Policy
Copy buttons on messages and code blocks were silently failing because
the Permissions-Policy header did not include clipboard-write=(self).
Firefox blocks navigator.clipboard.writeText() without explicit permission.
- api/helpers.py: add clipboard-write=(self) to Permissions-Policy
- ui.js: _copyText now catches clipboard API errors and falls back
to execCommand('copy'). _fallbackCopy extracted as separate function
with proper focus() call and visible-but-hidden positioning (not -9999px)
- Added 8 regression tests
* chore: CHANGELOG for v0.50.223
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 22:29:02 +00:00
try {
2026-05-19 18:41:00 +00:00
const fetchOpts = { ... opts } ;
delete fetchOpts . timeoutMs ;
2026-05-28 03:08:38 +00:00
delete fetchOpts . timeoutToast ;
2026-06-16 18:42:01 +00:00
delete fetchOpts . retries ;
delete fetchOpts . retryTimeouts ;
delete fetchOpts . retryStatuses ;
delete fetchOpts . retryDelayMs ;
2026-05-28 03:08:38 +00:00
2026-05-19 18:41:00 +00:00
const useTimeout = Number . isFinite ( Number ( timeoutMs ) ) && Number ( timeoutMs ) > 0 ;
if ( useTimeout && typeof AbortController !== 'undefined' ) {
controller = new AbortController ( ) ;
upstreamSignal = fetchOpts . signal || null ;
if ( upstreamSignal ) {
upstreamAbort = ( ) => controller . abort ( upstreamSignal . reason ) ;
if ( upstreamSignal . aborted ) upstreamAbort ( ) ;
else upstreamSignal . addEventListener ( 'abort' , upstreamAbort , { once : true } ) ;
}
fetchOpts . signal = controller . signal ;
v0.50.223: model picker, idle retry, drag-drop, CSP, clipboard copy (#1127)
* fix(#604): model picker shows all configured providers
Two fixes to ensure the model picker surface every provider a user has
configured:
1. Added env var detection for XAI_API_KEY (→ x-ai) and MISTRAL_API_KEY
(→ mistralai). Previously these providers were only detectable via
hermes auth or credential pool, not via environment variables.
2. Added config.yaml providers section scanning. Users who configure
providers in config.yaml (e.g. providers.anthropic.api_key) without
setting the corresponding env var will now see those providers in the
model picker. Only providers with known model catalogs are added.
- Added 12 regression tests
* fix(#1112): allow Google Fonts in CSP style-src and font-src
Mermaid themes inject @import for fonts.googleapis.com at render time.
CSP style-src blocked these requests, causing console violations.
- Add https://fonts.googleapis.com to style-src (CSS stylesheets)
- Add https://fonts.gstatic.com to font-src (WOFF2/WOFF font files)
- Add 3 regression tests + verify existing CSP tests still pass
* fix(#1118): retry api() calls on network errors after long idle
After a long idle period, the browser's TCP keep-alive connection to the
server can become stale. The next fetch() throws a TypeError (network
failure), causing 'Failed to load session' instead of transparently
reconnecting.
- Added retry loop in api() (workspace.js): up to 3 attempts
- Only retries on TypeError (network failures), NOT on HTTP errors (4xx/5xx)
- 401 redirects still fire immediately
- Added 6 regression tests
* feat(#1116): composer placeholder reflects active profile name
When a named profile is active (not 'default'), the composer placeholder
and title bar show the profile name (capitalised) instead of the global
bot_name. Falls back to bot_name/'Hermes' for the default profile.
- boot.js: applyBotName() checks S.activeProfile before _botName
- panels.js: switchToProfile() calls applyBotName() after switch
- Added 5 regression tests
* feat(#1097): drag and drop workspace files into chat composer
Files and folders in the workspace file tree are now draggable.
Dropping them into the composer inserts @path reference at cursor
position. OS file drag-and-drop (attach files) still works.
- ui.js: _renderTreeItems sets draggable + dragstart with ws-path
- panels.js: drop handler checks for application/ws-path first,
inserts @path with smart spacing and cursor positioning
- Added 9 regression tests
* fix(#1096): copy buttons work — add clipboard-write Permissions-Policy
Copy buttons on messages and code blocks were silently failing because
the Permissions-Policy header did not include clipboard-write=(self).
Firefox blocks navigator.clipboard.writeText() without explicit permission.
- api/helpers.py: add clipboard-write=(self) to Permissions-Policy
- ui.js: _copyText now catches clipboard API errors and falls back
to execCommand('copy'). _fallbackCopy extracted as separate function
with proper focus() call and visible-but-hidden positioning (not -9999px)
- Added 8 regression tests
* chore: CHANGELOG for v0.50.223
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 22:29:02 +00:00
}
2026-05-19 18:41:00 +00:00
const requestPromise = ( async ( ) => {
const res = await fetch ( url . href , { credentials : 'include' , headers : { 'Content-Type' : 'application/json' } , ... fetchOpts } ) ;
if ( ! res . ok ) {
// 401 means the auth session expired. Redirect to login so the user can
// re-authenticate. This is especially important for iOS PWA (standalone mode)
// and for subpath mounts like /hermes/, where /login escapes to the site root.
if ( res . status === 401 ) { window . location . href = 'login?next=' + encodeURIComponent ( window . location . pathname + window . location . search ) ; return ; }
const text = await res . text ( ) ;
// Parse JSON error body and surface the human-readable message,
// rather than showing raw JSON like {"error":"Profile 'x' does not exist."}
let message = text ;
try { const j = JSON . parse ( text ) ; message = j . error || j . message || text ; } catch ( e ) { }
// Attach the raw HTTP context so callers can branch on status (404 stale-session
// cleanup, 401 redirect, 503 retry, etc.) without re-parsing the message string.
const err = new Error ( message ) ;
err . status = res . status ;
err . statusText = res . statusText ;
err . body = text ;
throw err ;
}
const ct = res . headers . get ( 'content-type' ) || '' ;
return ct . includes ( 'application/json' ) ? await res . json ( ) : await res . text ( ) ;
} ) ( ) ;
return useTimeout ? await Promise . race ( [
requestPromise ,
new Promise ( ( _ , reject ) => {
timeoutId = setTimeout ( ( ) => {
didTimeout = true ;
if ( controller ) controller . abort ( ) ;
const err = new Error ( 'Request timed out. Please try again.' ) ;
err . name = 'TimeoutError' ;
err . timeout = true ;
reject ( err ) ;
} , Number ( timeoutMs ) ) ;
} )
] ) : await requestPromise ;
v0.50.223: model picker, idle retry, drag-drop, CSP, clipboard copy (#1127)
* fix(#604): model picker shows all configured providers
Two fixes to ensure the model picker surface every provider a user has
configured:
1. Added env var detection for XAI_API_KEY (→ x-ai) and MISTRAL_API_KEY
(→ mistralai). Previously these providers were only detectable via
hermes auth or credential pool, not via environment variables.
2. Added config.yaml providers section scanning. Users who configure
providers in config.yaml (e.g. providers.anthropic.api_key) without
setting the corresponding env var will now see those providers in the
model picker. Only providers with known model catalogs are added.
- Added 12 regression tests
* fix(#1112): allow Google Fonts in CSP style-src and font-src
Mermaid themes inject @import for fonts.googleapis.com at render time.
CSP style-src blocked these requests, causing console violations.
- Add https://fonts.googleapis.com to style-src (CSS stylesheets)
- Add https://fonts.gstatic.com to font-src (WOFF2/WOFF font files)
- Add 3 regression tests + verify existing CSP tests still pass
* fix(#1118): retry api() calls on network errors after long idle
After a long idle period, the browser's TCP keep-alive connection to the
server can become stale. The next fetch() throws a TypeError (network
failure), causing 'Failed to load session' instead of transparently
reconnecting.
- Added retry loop in api() (workspace.js): up to 3 attempts
- Only retries on TypeError (network failures), NOT on HTTP errors (4xx/5xx)
- 401 redirects still fire immediately
- Added 6 regression tests
* feat(#1116): composer placeholder reflects active profile name
When a named profile is active (not 'default'), the composer placeholder
and title bar show the profile name (capitalised) instead of the global
bot_name. Falls back to bot_name/'Hermes' for the default profile.
- boot.js: applyBotName() checks S.activeProfile before _botName
- panels.js: switchToProfile() calls applyBotName() after switch
- Added 5 regression tests
* feat(#1097): drag and drop workspace files into chat composer
Files and folders in the workspace file tree are now draggable.
Dropping them into the composer inserts @path reference at cursor
position. OS file drag-and-drop (attach files) still works.
- ui.js: _renderTreeItems sets draggable + dragstart with ws-path
- panels.js: drop handler checks for application/ws-path first,
inserts @path with smart spacing and cursor positioning
- Added 9 regression tests
* fix(#1096): copy buttons work — add clipboard-write Permissions-Policy
Copy buttons on messages and code blocks were silently failing because
the Permissions-Policy header did not include clipboard-write=(self).
Firefox blocks navigator.clipboard.writeText() without explicit permission.
- api/helpers.py: add clipboard-write=(self) to Permissions-Policy
- ui.js: _copyText now catches clipboard API errors and falls back
to execCommand('copy'). _fallbackCopy extracted as separate function
with proper focus() call and visible-but-hidden positioning (not -9999px)
- Added 8 regression tests
* chore: CHANGELOG for v0.50.223
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 22:29:02 +00:00
} catch ( e ) {
lastErr = e ;
2026-05-19 18:41:00 +00:00
const isTimeout = didTimeout || ( e && ( e . timeout === true || e . name === 'TimeoutError' ) ) ;
if ( isTimeout ) {
2026-06-16 18:42:01 +00:00
if ( retryTimeouts && attempt < 2 && attempt < maxAttempts - 1 ) {
if ( retryDelayMs ) await new Promise ( resolve => setTimeout ( resolve , retryDelayMs * Math . pow ( 2 , attempt ) ) ) ;
continue ;
}
2026-05-19 18:41:00 +00:00
const err = ( e && e . name === 'TimeoutError' ) ? e : new Error ( 'Request timed out. Please try again.' ) ;
err . name = 'TimeoutError' ;
err . timeout = true ;
2026-05-28 03:08:38 +00:00
if ( timeoutToast && typeof showToast === 'function' ) showToast ( 'Request timed out. Please try again.' , 5000 , 'error' ) ;
2026-05-19 18:41:00 +00:00
throw err ;
}
v0.50.223: model picker, idle retry, drag-drop, CSP, clipboard copy (#1127)
* fix(#604): model picker shows all configured providers
Two fixes to ensure the model picker surface every provider a user has
configured:
1. Added env var detection for XAI_API_KEY (→ x-ai) and MISTRAL_API_KEY
(→ mistralai). Previously these providers were only detectable via
hermes auth or credential pool, not via environment variables.
2. Added config.yaml providers section scanning. Users who configure
providers in config.yaml (e.g. providers.anthropic.api_key) without
setting the corresponding env var will now see those providers in the
model picker. Only providers with known model catalogs are added.
- Added 12 regression tests
* fix(#1112): allow Google Fonts in CSP style-src and font-src
Mermaid themes inject @import for fonts.googleapis.com at render time.
CSP style-src blocked these requests, causing console violations.
- Add https://fonts.googleapis.com to style-src (CSS stylesheets)
- Add https://fonts.gstatic.com to font-src (WOFF2/WOFF font files)
- Add 3 regression tests + verify existing CSP tests still pass
* fix(#1118): retry api() calls on network errors after long idle
After a long idle period, the browser's TCP keep-alive connection to the
server can become stale. The next fetch() throws a TypeError (network
failure), causing 'Failed to load session' instead of transparently
reconnecting.
- Added retry loop in api() (workspace.js): up to 3 attempts
- Only retries on TypeError (network failures), NOT on HTTP errors (4xx/5xx)
- 401 redirects still fire immediately
- Added 6 regression tests
* feat(#1116): composer placeholder reflects active profile name
When a named profile is active (not 'default'), the composer placeholder
and title bar show the profile name (capitalised) instead of the global
bot_name. Falls back to bot_name/'Hermes' for the default profile.
- boot.js: applyBotName() checks S.activeProfile before _botName
- panels.js: switchToProfile() calls applyBotName() after switch
- Added 5 regression tests
* feat(#1097): drag and drop workspace files into chat composer
Files and folders in the workspace file tree are now draggable.
Dropping them into the composer inserts @path reference at cursor
position. OS file drag-and-drop (attach files) still works.
- ui.js: _renderTreeItems sets draggable + dragstart with ws-path
- panels.js: drop handler checks for application/ws-path first,
inserts @path with smart spacing and cursor positioning
- Added 9 regression tests
* fix(#1096): copy buttons work — add clipboard-write Permissions-Policy
Copy buttons on messages and code blocks were silently failing because
the Permissions-Policy header did not include clipboard-write=(self).
Firefox blocks navigator.clipboard.writeText() without explicit permission.
- api/helpers.py: add clipboard-write=(self) to Permissions-Policy
- ui.js: _copyText now catches clipboard API errors and falls back
to execCommand('copy'). _fallbackCopy extracted as separate function
with proper focus() call and visible-but-hidden positioning (not -9999px)
- Added 8 regression tests
* chore: CHANGELOG for v0.50.223
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 22:29:02 +00:00
// Only retry on network errors (TypeError from fetch), not on HTTP errors
// that were already thrown above. Re-throw 401 redirects immediately.
if ( e . message && /401/ . test ( e . message ) ) throw e ;
2026-06-16 18:42:01 +00:00
if ( attempt < 2 && attempt < maxAttempts - 1 && ( e instanceof TypeError || retryStatuses . includes ( Number ( e . status ) ) ) ) {
if ( retryDelayMs ) await new Promise ( resolve => setTimeout ( resolve , retryDelayMs * Math . pow ( 2 , attempt ) ) ) ;
continue ;
}
v0.50.223: model picker, idle retry, drag-drop, CSP, clipboard copy (#1127)
* fix(#604): model picker shows all configured providers
Two fixes to ensure the model picker surface every provider a user has
configured:
1. Added env var detection for XAI_API_KEY (→ x-ai) and MISTRAL_API_KEY
(→ mistralai). Previously these providers were only detectable via
hermes auth or credential pool, not via environment variables.
2. Added config.yaml providers section scanning. Users who configure
providers in config.yaml (e.g. providers.anthropic.api_key) without
setting the corresponding env var will now see those providers in the
model picker. Only providers with known model catalogs are added.
- Added 12 regression tests
* fix(#1112): allow Google Fonts in CSP style-src and font-src
Mermaid themes inject @import for fonts.googleapis.com at render time.
CSP style-src blocked these requests, causing console violations.
- Add https://fonts.googleapis.com to style-src (CSS stylesheets)
- Add https://fonts.gstatic.com to font-src (WOFF2/WOFF font files)
- Add 3 regression tests + verify existing CSP tests still pass
* fix(#1118): retry api() calls on network errors after long idle
After a long idle period, the browser's TCP keep-alive connection to the
server can become stale. The next fetch() throws a TypeError (network
failure), causing 'Failed to load session' instead of transparently
reconnecting.
- Added retry loop in api() (workspace.js): up to 3 attempts
- Only retries on TypeError (network failures), NOT on HTTP errors (4xx/5xx)
- 401 redirects still fire immediately
- Added 6 regression tests
* feat(#1116): composer placeholder reflects active profile name
When a named profile is active (not 'default'), the composer placeholder
and title bar show the profile name (capitalised) instead of the global
bot_name. Falls back to bot_name/'Hermes' for the default profile.
- boot.js: applyBotName() checks S.activeProfile before _botName
- panels.js: switchToProfile() calls applyBotName() after switch
- Added 5 regression tests
* feat(#1097): drag and drop workspace files into chat composer
Files and folders in the workspace file tree are now draggable.
Dropping them into the composer inserts @path reference at cursor
position. OS file drag-and-drop (attach files) still works.
- ui.js: _renderTreeItems sets draggable + dragstart with ws-path
- panels.js: drop handler checks for application/ws-path first,
inserts @path with smart spacing and cursor positioning
- Added 9 regression tests
* fix(#1096): copy buttons work — add clipboard-write Permissions-Policy
Copy buttons on messages and code blocks were silently failing because
the Permissions-Policy header did not include clipboard-write=(self).
Firefox blocks navigator.clipboard.writeText() without explicit permission.
- api/helpers.py: add clipboard-write=(self) to Permissions-Policy
- ui.js: _copyText now catches clipboard API errors and falls back
to execCommand('copy'). _fallbackCopy extracted as separate function
with proper focus() call and visible-but-hidden positioning (not -9999px)
- Added 8 regression tests
* chore: CHANGELOG for v0.50.223
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 22:29:02 +00:00
throw e ;
2026-05-19 18:41:00 +00:00
} finally {
if ( timeoutId ) clearTimeout ( timeoutId ) ;
if ( upstreamSignal && upstreamAbort ) upstreamSignal . removeEventListener ( 'abort' , upstreamAbort ) ;
v0.50.223: model picker, idle retry, drag-drop, CSP, clipboard copy (#1127)
* fix(#604): model picker shows all configured providers
Two fixes to ensure the model picker surface every provider a user has
configured:
1. Added env var detection for XAI_API_KEY (→ x-ai) and MISTRAL_API_KEY
(→ mistralai). Previously these providers were only detectable via
hermes auth or credential pool, not via environment variables.
2. Added config.yaml providers section scanning. Users who configure
providers in config.yaml (e.g. providers.anthropic.api_key) without
setting the corresponding env var will now see those providers in the
model picker. Only providers with known model catalogs are added.
- Added 12 regression tests
* fix(#1112): allow Google Fonts in CSP style-src and font-src
Mermaid themes inject @import for fonts.googleapis.com at render time.
CSP style-src blocked these requests, causing console violations.
- Add https://fonts.googleapis.com to style-src (CSS stylesheets)
- Add https://fonts.gstatic.com to font-src (WOFF2/WOFF font files)
- Add 3 regression tests + verify existing CSP tests still pass
* fix(#1118): retry api() calls on network errors after long idle
After a long idle period, the browser's TCP keep-alive connection to the
server can become stale. The next fetch() throws a TypeError (network
failure), causing 'Failed to load session' instead of transparently
reconnecting.
- Added retry loop in api() (workspace.js): up to 3 attempts
- Only retries on TypeError (network failures), NOT on HTTP errors (4xx/5xx)
- 401 redirects still fire immediately
- Added 6 regression tests
* feat(#1116): composer placeholder reflects active profile name
When a named profile is active (not 'default'), the composer placeholder
and title bar show the profile name (capitalised) instead of the global
bot_name. Falls back to bot_name/'Hermes' for the default profile.
- boot.js: applyBotName() checks S.activeProfile before _botName
- panels.js: switchToProfile() calls applyBotName() after switch
- Added 5 regression tests
* feat(#1097): drag and drop workspace files into chat composer
Files and folders in the workspace file tree are now draggable.
Dropping them into the composer inserts @path reference at cursor
position. OS file drag-and-drop (attach files) still works.
- ui.js: _renderTreeItems sets draggable + dragstart with ws-path
- panels.js: drop handler checks for application/ws-path first,
inserts @path with smart spacing and cursor positioning
- Added 9 regression tests
* fix(#1096): copy buttons work — add clipboard-write Permissions-Policy
Copy buttons on messages and code blocks were silently failing because
the Permissions-Policy header did not include clipboard-write=(self).
Firefox blocks navigator.clipboard.writeText() without explicit permission.
- api/helpers.py: add clipboard-write=(self) to Permissions-Policy
- ui.js: _copyText now catches clipboard API errors and falls back
to execCommand('copy'). _fallbackCopy extracted as separate function
with proper focus() call and visible-but-hidden positioning (not -9999px)
- Added 8 regression tests
* chore: CHANGELOG for v0.50.223
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 22:29:02 +00:00
}
2026-04-03 19:29:24 +00:00
}
v0.50.223: model picker, idle retry, drag-drop, CSP, clipboard copy (#1127)
* fix(#604): model picker shows all configured providers
Two fixes to ensure the model picker surface every provider a user has
configured:
1. Added env var detection for XAI_API_KEY (→ x-ai) and MISTRAL_API_KEY
(→ mistralai). Previously these providers were only detectable via
hermes auth or credential pool, not via environment variables.
2. Added config.yaml providers section scanning. Users who configure
providers in config.yaml (e.g. providers.anthropic.api_key) without
setting the corresponding env var will now see those providers in the
model picker. Only providers with known model catalogs are added.
- Added 12 regression tests
* fix(#1112): allow Google Fonts in CSP style-src and font-src
Mermaid themes inject @import for fonts.googleapis.com at render time.
CSP style-src blocked these requests, causing console violations.
- Add https://fonts.googleapis.com to style-src (CSS stylesheets)
- Add https://fonts.gstatic.com to font-src (WOFF2/WOFF font files)
- Add 3 regression tests + verify existing CSP tests still pass
* fix(#1118): retry api() calls on network errors after long idle
After a long idle period, the browser's TCP keep-alive connection to the
server can become stale. The next fetch() throws a TypeError (network
failure), causing 'Failed to load session' instead of transparently
reconnecting.
- Added retry loop in api() (workspace.js): up to 3 attempts
- Only retries on TypeError (network failures), NOT on HTTP errors (4xx/5xx)
- 401 redirects still fire immediately
- Added 6 regression tests
* feat(#1116): composer placeholder reflects active profile name
When a named profile is active (not 'default'), the composer placeholder
and title bar show the profile name (capitalised) instead of the global
bot_name. Falls back to bot_name/'Hermes' for the default profile.
- boot.js: applyBotName() checks S.activeProfile before _botName
- panels.js: switchToProfile() calls applyBotName() after switch
- Added 5 regression tests
* feat(#1097): drag and drop workspace files into chat composer
Files and folders in the workspace file tree are now draggable.
Dropping them into the composer inserts @path reference at cursor
position. OS file drag-and-drop (attach files) still works.
- ui.js: _renderTreeItems sets draggable + dragstart with ws-path
- panels.js: drop handler checks for application/ws-path first,
inserts @path with smart spacing and cursor positioning
- Added 9 regression tests
* fix(#1096): copy buttons work — add clipboard-write Permissions-Policy
Copy buttons on messages and code blocks were silently failing because
the Permissions-Policy header did not include clipboard-write=(self).
Firefox blocks navigator.clipboard.writeText() without explicit permission.
- api/helpers.py: add clipboard-write=(self) to Permissions-Policy
- ui.js: _copyText now catches clipboard API errors and falls back
to execCommand('copy'). _fallbackCopy extracted as separate function
with proper focus() call and visible-but-hidden positioning (not -9999px)
- Added 8 regression tests
* chore: CHANGELOG for v0.50.223
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 22:29:02 +00:00
throw lastErr ;
2026-03-31 03:40:19 +00:00
}
2026-05-22 09:29:32 +00:00
function recordClientSSEError ( source , details = { } ) {
try {
const payload = {
event : 'sse_error' ,
source : String ( source || 'unknown' ) ,
ready _state : details . ready _state ,
session _id : details . session _id || null ,
stream _id : details . stream _id || null ,
visibility _state : ( typeof document !== 'undefined' && document . visibilityState ) || 'unknown' ,
online : ( typeof navigator !== 'undefined' && typeof navigator . onLine === 'boolean' ) ? navigator . onLine : null ,
url _path : ( typeof location !== 'undefined' && location . pathname ) || '/' ,
reason : details . reason || 'EventSource.onerror' ,
} ;
2026-05-28 03:08:38 +00:00
void api ( '/api/client-events/log' , { method : 'POST' , body : JSON . stringify ( payload ) , timeoutMs : 3000 , timeoutToast : false } ) . catch ( ( ) => { } ) ;
2026-05-22 09:29:32 +00:00
} catch ( _ ) { }
}
2026-04-04 01:59:49 +00:00
// Persist/restore expanded directory state per workspace in localStorage
function _wsExpandKey ( ) {
const ws = S . session && S . session . workspace ;
return ws ? 'hermes-webui-expanded:' + ws : null ;
}
function _saveExpandedDirs ( ) {
const key = _wsExpandKey ( ) ; if ( ! key ) return ;
try { localStorage . setItem ( key , JSON . stringify ( [ ... ( S . _expandedDirs || new Set ( ) ) ] ) ) ; } catch ( e ) { }
}
function _restoreExpandedDirs ( ) {
const key = _wsExpandKey ( ) ;
if ( ! key ) { S . _expandedDirs = new Set ( ) ; return ; }
try {
const raw = localStorage . getItem ( key ) ;
S . _expandedDirs = raw ? new Set ( JSON . parse ( raw ) ) : new Set ( ) ;
} catch ( e ) { S . _expandedDirs = new Set ( ) ; }
}
2026-05-24 18:58:37 +00:00
let _workspacePanelActiveTab = 'files' ;
let _renderSessionArtifactsTimer = null ;
function _setWorkspacePanelTabDataset ( ) {
const panel = document . querySelector ( '.rightpanel' ) ;
if ( panel ) panel . dataset . activeTab = _workspacePanelActiveTab ;
}
function scheduleRenderSessionArtifacts ( ) {
if ( _renderSessionArtifactsTimer ) clearTimeout ( _renderSessionArtifactsTimer ) ;
_renderSessionArtifactsTimer = setTimeout ( ( ) => {
_renderSessionArtifactsTimer = null ;
renderSessionArtifacts ( ) ;
} , 100 ) ;
}
if ( typeof document !== 'undefined' ) {
if ( document . readyState === 'loading' ) document . addEventListener ( 'DOMContentLoaded' , _setWorkspacePanelTabDataset , { once : true } ) ;
else _setWorkspacePanelTabDataset ( ) ;
}
function switchWorkspacePanelTab ( tab ) {
2026-06-15 00:31:13 +00:00
_workspacePanelActiveTab = tab === 'artifacts' ? 'artifacts' : tab === 'todos' ? 'todos' : 'files' ;
2026-05-24 18:58:37 +00:00
_setWorkspacePanelTabDataset ( ) ;
const filesTab = $ ( 'workspaceFilesTab' ) ;
const artifactsTab = $ ( 'workspaceArtifactsTab' ) ;
2026-06-15 00:31:13 +00:00
const todosTab = $ ( 'workspaceTodosTab' ) ;
2026-05-24 18:58:37 +00:00
if ( filesTab ) {
filesTab . classList . toggle ( 'active' , _workspacePanelActiveTab === 'files' ) ;
filesTab . setAttribute ( 'aria-selected' , _workspacePanelActiveTab === 'files' ? 'true' : 'false' ) ;
}
if ( artifactsTab ) {
artifactsTab . classList . toggle ( 'active' , _workspacePanelActiveTab === 'artifacts' ) ;
artifactsTab . setAttribute ( 'aria-selected' , _workspacePanelActiveTab === 'artifacts' ? 'true' : 'false' ) ;
}
2026-06-15 00:31:13 +00:00
if ( todosTab ) {
todosTab . classList . toggle ( 'active' , _workspacePanelActiveTab === 'todos' ) ;
todosTab . setAttribute ( 'aria-selected' , _workspacePanelActiveTab === 'todos' ? 'true' : 'false' ) ;
}
2026-05-24 18:58:37 +00:00
const artifacts = $ ( 'workspaceArtifacts' ) ;
if ( artifacts ) artifacts . hidden = _workspacePanelActiveTab !== 'artifacts' ;
2026-06-15 00:31:13 +00:00
const todosPanel = $ ( 'workspaceTodosPanel' ) ;
if ( todosPanel ) todosPanel . hidden = _workspacePanelActiveTab !== 'todos' ;
2026-05-24 18:58:37 +00:00
if ( _workspacePanelActiveTab === 'artifacts' ) renderSessionArtifacts ( ) ;
2026-06-15 00:31:13 +00:00
if ( _workspacePanelActiveTab === 'todos' ) _loadWorkspacePanelTodos ( ) ;
}
function _loadWorkspacePanelTodos ( ) {
const panel = $ ( 'workspaceTodosPanel' ) ;
if ( ! panel ) return ;
let todos = [ ] ;
try {
if ( S && Array . isArray ( S . todos ) ) {
todos = S . todos ;
} else if ( S && S . session && S . session . todo _state && Array . isArray ( S . session . todo _state . todos ) ) {
todos = S . session . todo _state . todos ;
} else if ( typeof _legacyTodosFromMessages === 'function' ) {
todos = _legacyTodosFromMessages ( ) || [ ] ;
}
} catch ( e ) { todos = [ ] ; }
if ( ! todos . length ) {
panel . innerHTML = '<div style="padding:24px 12px;text-align:center;color:var(--muted);font-size:12px">No active tasks</div>' ;
return ;
}
const statusIcon = ( s ) => {
if ( s === 'completed' ) return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--text)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>' ;
if ( s === 'in_progress' ) return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--blue)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>' ;
if ( s === 'cancelled' ) return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--muted)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>' ;
// pending
return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--muted)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/></svg>' ;
} ;
const items = todos . map ( t => {
const s = t . status || 'pending' ;
const isDone = s === 'completed' || s === 'cancelled' ;
return ` <div style="display:flex;align-items:flex-start;gap:8px;padding:6px 0;border-bottom:1px solid var(--border)"> ` +
` <span style="flex-shrink:0;margin-top:2px"> ${ statusIcon ( s ) } </span> ` +
` <span style="font-size:12px;color: ${ isDone ? 'var(--muted)' : 'var(--text)' } ;text-decoration: ${ s === 'cancelled' ? 'line-through' : 'none' } "> ${ _escHtml ( t . content || t . text || '' ) } </span> ` +
` </div> ` ;
} ) . join ( '' ) ;
panel . innerHTML = ` <div style="padding:4px 0"> ${ items } </div> ` ;
}
function _escHtml ( s ) {
return String ( s ) . replace ( /&/g , '&' ) . replace ( /</g , '<' ) . replace ( />/g , '>' ) . replace ( /"/g , '"' ) ;
2026-05-24 18:58:37 +00:00
}
const ARTIFACT _IGNORE _RE = /(^|\/)(?:\.git|\.hg|\.svn|node_modules|\.venv|venv|__pycache__|dist|build|\.next|\.cache)(?:\/|$)/ ;
// Canonical Hermes mutators plus MCP filesystem aliases that can create/edit files.
const ARTIFACT _MUTATION _TOOLS = new Set ( [ 'write_file' , 'patch' , 'edit_file' , 'create_file' , 'mcp_filesystem_write_file' , 'mcp_filesystem_edit_file' ] ) ;
function _normalizeArtifactPath ( path ) {
if ( ! path ) return '' ;
path = String ( path ) . trim ( ) . replace ( /[\`"'<>),.;:]+$/g , '' ) . replace ( /^[\`"'(<]+/g , '' ) ;
if ( ! path || path . length > 240 || path . includes ( '://' ) ) return '' ;
2026-05-31 17:03:07 +00:00
// Canonicalize workspace-relative prefixes so a file-tree open ("foo.md") and a
// tool arg recorded as "./foo.md" or "~/foo.md" compare equal for mutation
// tracking; otherwise an agent edit via a ./-prefixed path leaves the open
// preview stale (#3262 / pre-release regression-gate finding).
path = path . replace ( /^~\// , '' ) . replace ( /^(?:\.\/)+/ , '' ) ;
if ( ! path ) return '' ;
2026-05-24 18:58:37 +00:00
if ( ARTIFACT _IGNORE _RE . test ( path ) ) return '' ;
if ( ! /[./]/ . test ( path ) ) return '' ;
return path ;
}
function _artifactCandidatesFromText ( text ) {
if ( ! text || typeof text !== 'string' ) return [ ] ;
const out = [ ] ;
const seen = new Set ( ) ;
const add = ( path ) => {
path = _normalizeArtifactPath ( path ) ;
if ( ! path || seen . has ( path ) ) return ;
seen . add ( path ) ; out . push ( { path , kind : 'diff' } ) ;
} ;
// Fallback text mining is intentionally narrow: only diff/patch fences imply
// the session changed a file. Prose mentions such as "edited package.json" are
// too noisy for an Artifacts list that should track write/edit outputs.
const fenced = /```(?:diff|patch)\s*\n[\s\S]*?```/gi ;
let m ;
while ( ( m = fenced . exec ( text ) ) ) {
const block = m [ 0 ] ;
const fm = block . match ( /(?:^|\n)(?:\+\+\+|---)\s+(?:[ab]\/)?([^\n\t]+)/ ) ;
if ( fm ) add ( fm [ 1 ] . trim ( ) ) ;
}
return out ;
}
function _artifactCandidatesFromToolCall ( tc ) {
if ( ! tc ) return [ ] ;
const name = String ( tc . name || '' ) . replace ( /^functions\./ , '' ) ;
const args = tc . arguments || tc . args || tc . input || { } ;
const result = tc . result || tc . output || tc . snippet || '' ;
const out = [ ] ;
const add = ( path , source = name || 'tool' ) => {
path = _normalizeArtifactPath ( path ) ;
if ( path ) out . push ( { path , kind : source } ) ;
} ;
2026-05-25 00:46:48 +00:00
if ( ARTIFACT _MUTATION _TOOLS . has ( name ) && args && typeof args === 'object' ) {
2026-05-24 18:58:37 +00:00
for ( const key of [ 'path' , 'file_path' , 'source' , 'destination' ] ) add ( args [ key ] ) ;
if ( Array . isArray ( args . paths ) ) args . paths . forEach ( p => add ( p ) ) ;
if ( Array . isArray ( args . edits ) ) args . edits . forEach ( e => add ( e && e . path ) ) ;
}
const resultText = typeof result === 'string' ? result : ( result ? JSON . stringify ( result ) : '' ) ;
// Tool results may include unified diffs from patch-style tools; scan those
// narrowly after structured args so diff headers can still contribute paths.
for ( const a of _artifactCandidatesFromText ( resultText ) ) out . push ( a ) ;
if ( ! out . length && ARTIFACT _MUTATION _TOOLS . has ( name ) ) {
const argsText = typeof args === 'string' ? args : JSON . stringify ( args || { } ) ;
for ( const a of _artifactCandidatesFromText ( argsText ) ) out . push ( a ) ;
}
return out ;
}
2026-05-31 11:58:45 +00:00
const _turnMutatedPreviewPaths = new Set ( ) ;
function resetTurnWorkspaceMutations ( ) {
_turnMutatedPreviewPaths . clear ( ) ;
}
function noteWorkspaceMutationsFromToolCall ( tc ) {
for ( const a of _artifactCandidatesFromToolCall ( tc ) ) {
const path = _normalizeArtifactPath ( a . path ) ;
if ( path ) _turnMutatedPreviewPaths . add ( path ) ;
}
}
function noteWorkspaceMutationsFromToolCalls ( toolCalls ) {
if ( ! Array . isArray ( toolCalls ) ) return ;
for ( const tc of toolCalls ) noteWorkspaceMutationsFromToolCall ( tc ) ;
}
function _isOpenPreviewPathMutated ( ) {
if ( ! _previewCurrentPath ) return false ;
const current = _normalizeArtifactPath ( _previewCurrentPath ) ;
return ! ! ( current && _turnMutatedPreviewPaths . has ( current ) ) ;
}
async function refreshOpenPreviewIfMutated ( ) {
if ( typeof _previewDirty !== 'undefined' && _previewDirty ) return ;
if ( ! _isOpenPreviewPathMutated ( ) ) return ;
if ( ! _previewCurrentPath || ! S . session ) return ;
await openFile ( _previewCurrentPath , { bustCache : true } ) ;
}
2026-05-24 18:58:37 +00:00
function collectSessionArtifacts ( ) {
const items = [ ] ;
const seen = new Set ( ) ;
const push = ( path , source ) => {
path = _normalizeArtifactPath ( path ) ;
if ( ! path || seen . has ( path ) ) return ;
seen . add ( path ) ; items . push ( { path , source } ) ;
} ;
2026-06-01 10:27:23 +00:00
// Source 1: session-level tool call summaries (may be empty when messages
// carry their own tool metadata — see _syncToolCallsForLoadedMessages).
2026-05-24 18:58:37 +00:00
for ( const tc of ( S . toolCalls || [ ] ) ) {
for ( const a of _artifactCandidatesFromToolCall ( tc ) ) push ( a . path , a . kind || tc . name || 'tool' ) ;
}
2026-06-01 10:27:23 +00:00
// Source 2 & 3: message-level data — both text-mined diffs and structured
// tool_calls / tool_use content blocks that survive the S.toolCalls clear.
2026-05-24 18:58:37 +00:00
for ( const msg of ( S . messages || [ ] ) ) {
2026-06-01 10:27:23 +00:00
if ( ! msg ) continue ;
const text = msg . content || msg . text || msg . message || '' ;
// Text-mined diff/patch fences (existing path).
if ( typeof text === 'string' ) {
for ( const a of _artifactCandidatesFromText ( text ) ) push ( a . path , a . kind ) ;
}
// Structured tool_calls array (OpenAI format: {function:{name,arguments}}).
if ( Array . isArray ( msg . tool _calls ) ) {
for ( const tc of msg . tool _calls ) {
2026-06-01 17:27:02 +00:00
if ( ! tc || typeof tc !== 'object' ) continue ;
const fn = ( tc . function && typeof tc . function === 'object' ) ? tc . function : tc ;
2026-06-01 10:27:23 +00:00
const name = fn . name || tc . name || '' ;
let args = fn . arguments || tc . arguments || tc . args || tc . input || { } ;
if ( typeof args === 'string' ) { try { args = JSON . parse ( args ) ; } catch ( _ ) { } }
const fakeTc = { name , args , result : tc . result || tc . output || '' } ;
for ( const a of _artifactCandidatesFromToolCall ( fakeTc ) ) push ( a . path , a . kind || name || 'tool' ) ;
}
}
// Structured content array with tool_use blocks (Anthropic format).
if ( Array . isArray ( msg . content ) ) {
for ( const block of msg . content ) {
if ( ! block || block . type !== 'tool_use' ) continue ;
let inp = block . input || { } ;
if ( typeof inp === 'string' ) { try { inp = JSON . parse ( inp ) ; } catch ( _ ) { } }
const fakeTc = { name : block . name || '' , args : inp , result : block . result || '' } ;
for ( const a of _artifactCandidatesFromToolCall ( fakeTc ) ) push ( a . path , a . kind || block . name || 'tool' ) ;
}
}
2026-05-24 18:58:37 +00:00
}
return items . slice ( 0 , 50 ) ;
}
function renderSessionArtifacts ( ) {
const root = $ ( 'workspaceArtifacts' ) ;
const count = $ ( 'workspaceArtifactsCount' ) ;
if ( ! root ) return ;
const items = collectSessionArtifacts ( ) ;
if ( count ) count . textContent = String ( items . length ) ;
if ( ! S . session ) {
root . innerHTML = '<div class="workspace-artifact-empty">Open a conversation to see files changed in this session.</div>' ;
return ;
}
if ( ! items . length ) {
root . innerHTML = '<div class="workspace-artifact-empty">No artifacts detected yet. Files created or edited during this session will appear here.</div>' ;
return ;
}
2026-06-01 10:27:23 +00:00
// Strip workspace prefix for display so long absolute paths don't clutter the list.
const ws = S . session && S . session . workspace ;
const normWs = ws ? ws . replace ( /\/+$/ , '' ) + '/' : '' ;
const displayPath = ( p ) => {
if ( normWs && p . startsWith ( normWs ) ) return p . slice ( normWs . length ) ;
return p ;
} ;
root . innerHTML = items . map ( item => ` <button type="button" class="workspace-artifact-item" data-artifact-path=" ${ esc ( item . path ) } " onclick="openArtifactPath(this.dataset.artifactPath)"><div class="workspace-artifact-path"> ${ esc ( displayPath ( item . path ) ) } </div><div class="workspace-artifact-meta"> ${ esc ( item . source || 'session' ) } </div></button> ` ) . join ( '' ) ;
2026-05-24 18:58:37 +00:00
}
2026-05-25 13:48:52 +00:00
async function _workspacePathExists ( path ) {
if ( ! S . session || ! path ) return false ;
const parts = String ( path ) . split ( '/' ) . filter ( Boolean ) ;
const name = parts . pop ( ) ;
if ( ! name ) return false ;
const dir = parts . length ? parts . join ( '/' ) : '.' ;
const data = await api ( ` /api/list?session_id= ${ encodeURIComponent ( S . session . session _id ) } &path= ${ encodeURIComponent ( dir ) } ` ) ;
return ( data . entries || [ ] ) . some ( entry => entry && ( ( entry . path === path ) || entry . name === name ) ) ;
}
async function openArtifactPath ( path ) {
2026-05-24 18:58:37 +00:00
if ( ! path ) return ;
switchWorkspacePanelTab ( 'files' ) ;
2026-06-01 10:27:23 +00:00
let rel = path . replace ( /^~\// , '' ) . replace ( /^\.\/+/ , '' ) ;
// Strip workspace prefix so /api/list receives a workspace-relative path.
const ws = S . session && S . session . workspace ;
if ( ws ) {
const normWs = ws . replace ( /\/+$/ , '' ) + '/' ;
if ( rel . startsWith ( normWs ) ) rel = rel . slice ( normWs . length ) ;
else if ( rel === ws . replace ( /\/+$/ , '' ) ) rel = '.' ;
}
if ( ! rel ) rel = '.' ;
2026-05-25 13:48:52 +00:00
try {
if ( ! ( await _workspacePathExists ( rel ) ) ) {
setStatus ( t ( 'file_open_failed' ) ) ;
return ;
}
} catch ( _ ) {
setStatus ( t ( 'file_open_failed' ) ) ;
return ;
}
2026-05-24 18:58:37 +00:00
openFile ( rel ) ;
}
2026-05-31 11:56:17 +00:00
async function loadDir ( path , opts = { } ) {
const preservePreview = ! ! ( opts && opts . preservePreview ) ;
2026-06-09 20:03:15 +00:00
const refreshExpanded = ! ! ( opts && opts . refreshExpanded ) ;
2026-03-31 03:40:19 +00:00
if ( ! S . session ) return ;
2026-05-24 18:08:41 +00:00
const sessionId = S . session . session _id ;
2026-03-31 03:40:19 +00:00
try {
2026-06-09 20:03:15 +00:00
if ( ! path || path === '.' || refreshExpanded ) {
2026-04-04 01:59:49 +00:00
S . _dirCache = { } ;
2026-06-09 20:03:15 +00:00
_restoreExpandedDirs ( ) ; // restore per-workspace expanded state after root and refresh resets
2026-04-04 01:59:49 +00:00
}
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
S . currentDir = path || '.' ;
2026-05-24 18:08:41 +00:00
const data = await api ( ` /api/list?session_id= ${ encodeURIComponent ( sessionId ) } &path= ${ encodeURIComponent ( path ) } ` ) ;
if ( ! S . session || S . session . session _id !== sessionId ) return ;
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
S . entries = data . entries || [ ] ; renderBreadcrumb ( ) ; renderFileTree ( ) ;
2026-05-24 18:58:37 +00:00
// #2673 — refresh Artifacts tab when its source data (the file tree) updates.
if ( typeof renderSessionArtifacts === 'function' ) renderSessionArtifacts ( ) ;
2026-04-04 01:59:49 +00:00
// Pre-fetch contents of restored expanded dirs so they render without a second click
2026-04-27 23:27:03 +00:00
// (parallelized — avoids serial waterfall when multiple dirs are expanded)
2026-06-09 20:03:15 +00:00
if ( ! path || path === '.' || refreshExpanded ) {
2026-04-27 23:27:03 +00:00
const expanded = S . _expandedDirs || new Set ( ) ;
const pending = [ ... expanded ] . filter ( dirPath => ! S . _dirCache [ dirPath ] ) ;
if ( pending . length ) {
const results = await Promise . all ( pending . map ( dirPath =>
2026-05-24 18:08:41 +00:00
api ( ` /api/list?session_id= ${ encodeURIComponent ( sessionId ) } &path= ${ encodeURIComponent ( dirPath ) } ` )
2026-04-27 23:27:03 +00:00
. then ( dc => ( { dirPath , entries : dc . entries || [ ] } ) )
. catch ( ( ) => ( { dirPath , entries : [ ] } ) )
) ) ;
2026-05-24 18:08:41 +00:00
if ( ! S . session || S . session . session _id !== sessionId ) return ;
2026-04-27 23:27:03 +00:00
for ( const { dirPath , entries } of results ) S . _dirCache [ dirPath ] = entries ;
2026-04-04 01:59:49 +00:00
}
2026-04-27 23:27:03 +00:00
if ( expanded . size > 0 ) renderFileTree ( ) ;
2026-04-04 01:59:49 +00:00
}
2026-05-31 11:56:17 +00:00
if ( ! preservePreview && typeof clearPreview === 'function' ) {
2026-04-03 12:02:30 +00:00
if ( typeof _previewDirty !== 'undefined' && _previewDirty ) {
2026-05-07 05:14:47 +00:00
showConfirmDialog ( { title : t ( 'unsaved_confirm' ) , message : '' , confirmLabel : 'Discard' , danger : true , focusCancel : true } ) . then ( ok => { if ( ok ) clearPreview ( { keepPanelOpen : true } ) ; } ) ;
2026-04-03 12:02:30 +00:00
} else {
2026-05-07 05:14:47 +00:00
clearPreview ( { keepPanelOpen : true } ) ;
2026-04-03 12:02:30 +00:00
}
2026-05-31 11:58:45 +00:00
} else if ( preservePreview ) {
await refreshOpenPreviewIfMutated ( ) ;
2026-04-03 12:02:30 +00:00
}
feat: workspace git detection with branch/status badge
When the workspace root is a git repo, a badge in the panel header
shows the current branch name, dirty file count, and ahead/behind
status. Updates on every root directory load.
Backend:
- git_info_for_workspace() in api/workspace.py runs lightweight git
commands (rev-parse, status --porcelain, rev-list) with 3s timeout
- New GET /api/git-info endpoint returns branch, dirty count, modified,
untracked, ahead, behind
Frontend:
- _refreshGitBadge() in workspace.js fetches git info on root load
- Git badge element in panel header shows branch + status
- Badge turns gold when workspace has uncommitted changes
Inspired by PR #75 (@MartinNielsenDev).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:08:25 +00:00
// Fetch git info for workspace root (non-blocking)
if ( ! path || path === '.' ) _refreshGitBadge ( ) ;
2026-03-31 03:40:19 +00:00
} catch ( e ) { console . warn ( 'loadDir' , e ) ; }
}
2026-06-09 20:03:15 +00:00
function refreshWorkspacePanel ( ) {
if ( ! S . session ) return ;
const targetDir = S . currentDir || '.' ;
loadDir ( targetDir , { refreshExpanded : true } ) ;
}
feat: workspace git detection with branch/status badge
When the workspace root is a git repo, a badge in the panel header
shows the current branch name, dirty file count, and ahead/behind
status. Updates on every root directory load.
Backend:
- git_info_for_workspace() in api/workspace.py runs lightweight git
commands (rev-parse, status --porcelain, rev-list) with 3s timeout
- New GET /api/git-info endpoint returns branch, dirty count, modified,
untracked, ahead, behind
Frontend:
- _refreshGitBadge() in workspace.js fetches git info on root load
- Git badge element in panel header shows branch + status
- Badge turns gold when workspace has uncommitted changes
Inspired by PR #75 (@MartinNielsenDev).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:08:25 +00:00
async function _refreshGitBadge ( ) {
const badge = $ ( 'gitBadge' ) ;
if ( ! badge || ! S . session ) return ;
2026-05-24 18:08:41 +00:00
const sessionId = S . session . session _id ;
feat: workspace git detection with branch/status badge
When the workspace root is a git repo, a badge in the panel header
shows the current branch name, dirty file count, and ahead/behind
status. Updates on every root directory load.
Backend:
- git_info_for_workspace() in api/workspace.py runs lightweight git
commands (rev-parse, status --porcelain, rev-list) with 3s timeout
- New GET /api/git-info endpoint returns branch, dirty count, modified,
untracked, ahead, behind
Frontend:
- _refreshGitBadge() in workspace.js fetches git info on root load
- Git badge element in panel header shows branch + status
- Badge turns gold when workspace has uncommitted changes
Inspired by PR #75 (@MartinNielsenDev).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:08:25 +00:00
try {
2026-05-24 18:08:41 +00:00
const data = await api ( ` /api/git-info?session_id= ${ encodeURIComponent ( sessionId ) } ` ) ;
if ( ! S . session || S . session . session _id !== sessionId ) return ;
feat: workspace git detection with branch/status badge
When the workspace root is a git repo, a badge in the panel header
shows the current branch name, dirty file count, and ahead/behind
status. Updates on every root directory load.
Backend:
- git_info_for_workspace() in api/workspace.py runs lightweight git
commands (rev-parse, status --porcelain, rev-list) with 3s timeout
- New GET /api/git-info endpoint returns branch, dirty count, modified,
untracked, ahead, behind
Frontend:
- _refreshGitBadge() in workspace.js fetches git info on root load
- Git badge element in panel header shows branch + status
- Badge turns gold when workspace has uncommitted changes
Inspired by PR #75 (@MartinNielsenDev).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:08:25 +00:00
if ( data . git && data . git . is _git ) {
const g = data . git ;
let text = g . branch || 'git' ;
if ( g . dirty > 0 ) text += ` \u 00b7 ${ g . dirty } \u 2206 ` ; // middot + delta
if ( g . behind > 0 ) text += ` \u 2193 ${ g . behind } ` ;
if ( g . ahead > 0 ) text += ` \u 2191 ${ g . ahead } ` ;
badge . textContent = text ;
badge . className = 'git-badge' + ( g . dirty > 0 ? ' dirty' : '' ) ;
badge . style . display = '' ;
} else {
badge . style . display = 'none' ;
badge . textContent = '' ;
}
2026-05-24 18:08:41 +00:00
} catch ( e ) {
if ( ! S . session || S . session . session _id !== sessionId ) return ;
badge . style . display = 'none' ;
}
feat: workspace git detection with branch/status badge
When the workspace root is a git repo, a badge in the panel header
shows the current branch name, dirty file count, and ahead/behind
status. Updates on every root directory load.
Backend:
- git_info_for_workspace() in api/workspace.py runs lightweight git
commands (rev-parse, status --porcelain, rev-list) with 3s timeout
- New GET /api/git-info endpoint returns branch, dirty count, modified,
untracked, ahead, behind
Frontend:
- _refreshGitBadge() in workspace.js fetches git info on root load
- Git badge element in panel header shows branch + status
- Badge turns gold when workspace has uncommitted changes
Inspired by PR #75 (@MartinNielsenDev).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:08:25 +00:00
}
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
function navigateUp ( ) {
if ( ! S . session || S . currentDir === '.' ) return ;
const parts = S . currentDir . split ( '/' ) ;
parts . pop ( ) ;
loadDir ( parts . length ? parts . join ( '/' ) : '.' ) ;
}
2026-03-31 03:40:19 +00:00
// File extension sets for preview routing (must match server-side sets)
const IMAGE _EXTS = new Set ( [ '.png' , '.jpg' , '.jpeg' , '.gif' , '.svg' , '.webp' , '.ico' , '.bmp' ] ) ;
const MD _EXTS = new Set ( [ '.md' , '.markdown' , '.mdown' ] ) ;
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
const HTML _EXTS = new Set ( [ '.html' , '.htm' ] ) ;
2026-04-30 02:54:07 +00:00
const PDF _EXTS = new Set ( [ '.pdf' ] ) ;
const AUDIO _EXTS = new Set ( [ '.mp3' , '.wav' , '.m4a' , '.aac' , '.ogg' , '.oga' , '.opus' , '.flac' ] ) ;
const VIDEO _EXTS = new Set ( [ '.mp4' , '.mov' , '.m4v' , '.webm' , '.ogv' , '.avi' , '.mkv' ] ) ;
2026-06-03 03:58:22 +00:00
const MD _PREVIEW _RICH _RENDER _MAX _BYTES = 256 * 1024 ;
const MD _PREVIEW _RICH _RENDER _MAX _LINES = 5000 ;
2026-03-31 03:40:19 +00:00
// Binary formats that should download rather than preview
const DOWNLOAD _EXTS = new Set ( [
'.docx' , '.doc' , '.xlsx' , '.xls' , '.pptx' , '.ppt' , '.odt' , '.ods' , '.odp' ,
2026-04-30 02:54:07 +00:00
'.zip' , '.tar' , '.gz' , '.bz2' , '.7z' , '.rar' ,
2026-03-31 03:40:19 +00:00
'.exe' , '.dmg' , '.pkg' , '.deb' , '.rpm' ,
'.woff' , '.woff2' , '.ttf' , '.otf' , '.eot' ,
'.bin' , '.dat' , '.db' , '.sqlite' , '.pyc' , '.class' , '.so' , '.dylib' , '.dll' ,
] ) ;
function fileExt ( p ) { const i = p . lastIndexOf ( '.' ) ; return i >= 0 ? p . slice ( i ) . toLowerCase ( ) : '' ; }
2026-05-24 09:30:38 +00:00
function markdownPreviewByteLength ( content ) {
const text = String ( content || '' ) ;
if ( typeof Blob === 'function' ) return new Blob ( [ text ] ) . size ;
if ( typeof TextEncoder === 'function' ) return new TextEncoder ( ) . encode ( text ) . length ;
return unescape ( encodeURIComponent ( text ) ) . length ;
}
function markdownPreviewLineCount ( content ) {
const text = String ( content || '' ) ;
if ( ! text ) return 1 ;
return text . split ( '\n' ) . length ;
}
function shouldRenderMarkdownPreviewAsPlainText ( content ) {
return markdownPreviewByteLength ( content ) > MD _PREVIEW _RICH _RENDER _MAX _BYTES
|| markdownPreviewLineCount ( content ) > MD _PREVIEW _RICH _RENDER _MAX _LINES ;
}
function largeMarkdownPlainTextStatus ( content ) {
const bytes = markdownPreviewByteLength ( content ) ;
const lines = markdownPreviewLineCount ( content ) ;
const sizeLabel = bytes >= 1024 ? ` ${ Math . round ( bytes / 1024 ) } KB ` : ` ${ bytes } B ` ;
2026-06-03 03:58:22 +00:00
return ` Large markdown file ( ${ sizeLabel } , ${ lines } lines) shown as plain text. Click "Render as markdown anyway" to force rich rendering, or Edit to view raw. ` ;
}
function setLargeMarkdownForceRenderVisible ( visible ) {
const btn = $ ( 'btnRenderMarkdownAnyway' ) ;
if ( btn ) btn . style . display = visible ? 'inline-flex' : 'none' ;
}
function renderMarkdownPreviewContent ( data ) {
2026-06-17 18:43:57 +00:00
const target = data && data . el ? data . el : $ ( 'previewMd' ) ;
if ( ! data || ! data . el ) showPreview ( 'md' ) ;
target . innerHTML = renderMd ( data . content ) ;
2026-06-03 03:58:22 +00:00
requestAnimationFrame ( ( ) => { if ( typeof renderKatexBlocks === 'function' ) renderKatexBlocks ( ) ; } ) ;
}
2026-06-14 05:52:40 +00:00
function renderCodePreviewContent ( path , content ) {
showPreview ( 'code' ) ;
const codeEl = document . createElement ( 'code' ) ;
codeEl . textContent = content ;
const lang = _prismLanguageForPath ( path ) ;
if ( lang ) codeEl . className = 'language-' + lang ;
const pre = $ ( 'previewCode' ) ;
pre . textContent = '' ;
// Prism.highlightElement() propagates the language-* class onto the
// parent <pre>, so a previously-previewed code file leaves e.g.
// "language-css" on #previewCode. A subsequent plain-text file builds a
// class-less <code>, and Prism walks up to that stale ancestor class and
// mis-highlights prose. Strip any inherited language-* token from the
// <pre> before each render so highlighting never leaks across files.
pre . className = pre . className . replace ( /\blanguage-\S+/g , '' ) . replace ( /\s+/g , ' ' ) . trim ( ) ;
pre . appendChild ( codeEl ) ;
// Only invoke Prism when we actually assigned a language; otherwise the
// class-less <code> would inherit any ancestor language-* class.
if ( lang && typeof Prism !== 'undefined' && typeof Prism . highlightElement === 'function' ) {
Prism . highlightElement ( codeEl ) ;
}
}
function renderCsvPreviewContent ( path , content ) {
if ( typeof buildCsvTablePreview !== 'function' ) return false ;
const preview = buildCsvTablePreview ( path , content ) ;
if ( ! preview ) return false ;
showPreview ( 'csv' ) ;
// Preserve the raw CSV text so the Edit flow can repopulate the textarea and
// a save can re-render the table from the edited source (#4025 review, Codex).
if ( typeof content === 'string' ) {
_previewRawContent = content ;
_previewRawContentPath = path ;
}
if ( preview . html ) {
$ ( 'previewMd' ) . innerHTML = preview . html ;
return true ;
}
if ( preview . errorKey && typeof _csvPreviewErrorHtml === 'function' ) {
$ ( 'previewMd' ) . innerHTML = _csvPreviewErrorHtml ( path , preview . errorKey ) ;
return true ;
}
return false ;
}
2026-06-03 03:58:22 +00:00
function forceRenderMarkdownPreview ( ) {
// #3378 review (Codex): don't force-render from a dirty/open editor — the
// cached raw content would not reflect the unsaved edit. Require a saved,
// non-dirty state and cached content that belongs to the current file.
if ( _previewDirty || $ ( 'previewEditArea' ) . style . display !== 'none' ) return ;
if ( ! _previewRawContent || _previewRawContentPath !== _previewCurrentPath ) return ;
openFile ( _previewCurrentPath , { forceRichMarkdown : true } ) ;
setStatus ( 'Markdown rendered for this file.' ) ;
2026-05-24 09:30:38 +00:00
}
2026-03-31 03:40:19 +00:00
let _previewCurrentPath = '' ; // relative path of currently previewed file
2026-06-14 05:52:40 +00:00
let _previewCurrentMode = '' ; // 'code' | 'csv' | 'md' | 'image' | 'html' | 'pdf' | 'audio' | 'video'
2026-03-31 03:40:19 +00:00
let _previewDirty = false ; // true when edits are unsaved
function showPreview ( mode ) {
2026-06-14 05:52:40 +00:00
// mode: 'code' | 'csv' | 'image' | 'md' | 'html' | 'pdf' | 'audio' | 'video'
2026-03-31 03:40:19 +00:00
$ ( 'previewCode' ) . style . display = mode === 'code' ? '' : 'none' ;
$ ( 'previewImgWrap' ) . style . display = mode === 'image' ? '' : 'none' ;
2026-04-30 02:54:07 +00:00
const mediaWrap = $ ( 'previewMediaWrap' ) ; if ( mediaWrap ) mediaWrap . style . display = ( mode === 'audio' || mode === 'video' ) ? '' : 'none' ;
const pdfWrap = $ ( 'previewPdfWrap' ) ; if ( pdfWrap ) pdfWrap . style . display = mode === 'pdf' ? '' : 'none' ;
2026-06-14 05:52:40 +00:00
$ ( 'previewMd' ) . style . display = ( mode === 'md' || mode === 'csv' ) ? '' : 'none' ;
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
$ ( 'previewHtmlWrap' ) . style . display = mode === 'html' ? '' : 'none' ;
2026-03-31 03:40:19 +00:00
$ ( 'previewEditArea' ) . style . display = 'none' ; // start in read-only
const badge = $ ( 'previewBadge' ) ;
badge . className = 'preview-badge ' + mode ;
2026-06-14 05:52:40 +00:00
badge . textContent = mode === 'image' ? 'image' : mode === 'audio' ? 'audio' : mode === 'video' ? 'video' : mode === 'pdf' ? 'pdf' : mode === 'csv' ? 'csv' : mode === 'md' ? 'md' : mode === 'html' ? 'html' : fileExt ( $ ( 'previewPathText' ) . textContent ) || 'text' ;
2026-03-31 03:40:19 +00:00
_previewCurrentMode = mode ;
_previewDirty = false ;
updateEditBtn ( ) ;
2026-04-30 02:54:07 +00:00
// Show "Open in browser" button for iframe-backed document previews
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
const openBtn = $ ( 'btnOpenInBrowser' ) ;
2026-04-30 02:54:07 +00:00
if ( openBtn ) openBtn . style . display = ( mode === 'html' || mode === 'pdf' ) ? 'inline-flex' : 'none' ;
2026-06-03 03:58:22 +00:00
setLargeMarkdownForceRenderVisible ( false ) ;
2026-03-31 03:40:19 +00:00
}
function updateEditBtn ( ) {
const btn = $ ( 'btnEditFile' ) ;
if ( ! btn ) return ;
2026-06-14 05:52:40 +00:00
const editable = _previewCurrentMode === 'code' || _previewCurrentMode === 'md' || _previewCurrentMode === 'csv' ;
2026-03-31 03:40:19 +00:00
btn . style . display = editable ? '' : 'none' ;
const editing = $ ( 'previewEditArea' ) . style . display !== 'none' ;
2026-04-08 14:55:03 +00:00
btn . innerHTML = editing ? ` 💾 ${ t ( 'save' ) } ` : ` ✎ ${ t ( 'edit' ) } ` ;
btn . title = editing ? t ( 'save_title' ) : t ( 'edit_title' ) ;
2026-03-31 03:40:19 +00:00
btn . style . color = editing ? 'var(--blue)' : '' ;
if ( _previewDirty ) btn . innerHTML = '💾 Save*' ;
}
async function toggleEditMode ( ) {
const editing = $ ( 'previewEditArea' ) . style . display !== 'none' ;
if ( editing ) {
// Save
if ( ! S . session || ! _previewCurrentPath ) return ;
const content = $ ( 'previewEditArea' ) . value ;
try {
await api ( '/api/file/save' , { method : 'POST' , body : JSON . stringify ( {
session _id : S . session . session _id , path : _previewCurrentPath , content
} ) } ) ;
_previewDirty = false ;
2026-06-03 03:58:22 +00:00
// Update read-only views AND the cached raw content so a later
// "Render as markdown anyway" force-render reflects the just-saved text
// (not the stale pre-edit fetch). #3378 review (Codex).
_previewRawContent = content ;
_previewRawContentPath = _previewCurrentPath ;
2026-03-31 03:40:19 +00:00
if ( _previewCurrentMode === 'code' ) $ ( 'previewCode' ) . textContent = content ;
2026-06-14 05:52:40 +00:00
else if ( _previewCurrentMode === 'csv' ) renderCsvPreviewContent ( _previewCurrentPath , content ) ;
2026-06-03 03:58:22 +00:00
else renderMarkdownPreviewContent ( { content } ) ;
2026-03-31 03:40:19 +00:00
$ ( 'previewEditArea' ) . style . display = 'none' ;
if ( _previewCurrentMode === 'code' ) $ ( 'previewCode' ) . style . display = '' ;
else $ ( 'previewMd' ) . style . display = '' ;
2026-04-08 14:55:03 +00:00
showToast ( t ( 'saved' ) ) ;
} catch ( e ) { setStatus ( t ( 'save_failed' ) + e . message ) ; }
2026-03-31 03:40:19 +00:00
} else {
// Enter edit mode: populate textarea with current content
const currentText = _previewCurrentMode === 'code'
? $ ( 'previewCode' ) . textContent
: _previewRawContent || '' ;
$ ( 'previewEditArea' ) . value = currentText ;
$ ( 'previewEditArea' ) . style . display = '' ;
if ( _previewCurrentMode === 'code' ) $ ( 'previewCode' ) . style . display = 'none' ;
else $ ( 'previewMd' ) . style . display = 'none' ;
// Escape cancels the edit without saving
$ ( 'previewEditArea' ) . onkeydown = e => {
if ( e . key === 'Escape' ) { e . preventDefault ( ) ; cancelEditMode ( ) ; }
} ;
}
updateEditBtn ( ) ;
}
let _previewRawContent = '' ; // raw text for md files (to populate editor)
2026-06-03 03:58:22 +00:00
let _previewRawContentPath = '' ; // path that _previewRawContent belongs to (#3378 force-render cache guard)
2026-03-31 03:40:19 +00:00
function cancelEditMode ( ) {
// Discard changes and return to read-only view
$ ( 'previewEditArea' ) . style . display = 'none' ;
$ ( 'previewEditArea' ) . onkeydown = null ;
if ( _previewCurrentMode === 'code' ) $ ( 'previewCode' ) . style . display = '' ;
else $ ( 'previewMd' ) . style . display = '' ;
_previewDirty = false ;
updateEditBtn ( ) ;
}
2026-06-01 21:20:42 +00:00
// Map file extensions to Prism.js language identifiers.
// Prism autoloader fetches missing language components from CDN on demand.
const _PRISM _LANG _MAP = {
js : 'javascript' , mjs : 'javascript' , jsx : 'jsx' , ts : 'typescript' , tsx : 'tsx' ,
py : 'python' , pyw : 'python' , pyi : 'python' ,
rb : 'ruby' , go : 'go' , rs : 'rust' , java : 'java' , kt : 'kotlin' , kts : 'kotlin' ,
c : 'c' , h : 'c' , cpp : 'cpp' , cxx : 'cpp' , hpp : 'cpp' , cc : 'cpp' ,
cs : 'csharp' , swift : 'swift' , scala : 'scala' ,
php : 'php' , pl : 'perl' , pm : 'perl' , r : 'r' , lua : 'lua' ,
sh : 'bash' , bash : 'bash' , zsh : 'bash' , fish : 'bash' ,
ps1 : 'powershell' , psm1 : 'powershell' ,
sql : 'sql' , graphql : 'graphql' ,
json : 'json' , yaml : 'yaml' , yml : 'yaml' , toml : 'toml' , xml : 'xml' ,
html : 'markup' , htm : 'markup' , svg : 'markup' , vue : 'markup' ,
css : 'css' , scss : 'scss' , sass : 'sass' , less : 'less' ,
md : 'markdown' , markdown : 'markdown' ,
dockerfile : 'docker' , makefile : 'makefile' , cmake : 'cmake' ,
ini : 'ini' , cfg : 'ini' , conf : 'ini' , properties : 'properties' ,
diff : 'diff' , patch : 'diff' ,
txt : '' , log : '' , csv : '' , tsv : '' ,
} ;
2026-06-02 02:13:19 +00:00
const _PRISM _BASENAME _LANG _MAP = {
'dockerfile' : 'docker' , 'makefile' : 'makefile' , 'gnumakefile' : 'makefile' ,
'cmakelists.txt' : 'cmake' ,
'.gitignore' : 'ignore' , '.dockerignore' : 'ignore' ,
} ;
2026-06-01 21:20:42 +00:00
function _prismLanguageForPath ( path ) {
2026-06-02 02:13:19 +00:00
const base = String ( path || '' ) . split ( /[\\/]/ ) . pop ( ) . toLowerCase ( ) ;
if ( base . startsWith ( 'dockerfile.' ) ) return 'docker' ;
if ( _PRISM _BASENAME _LANG _MAP [ base ] !== undefined ) return _PRISM _BASENAME _LANG _MAP [ base ] ;
2026-06-01 21:20:42 +00:00
const ext = fileExt ( path ) . replace ( /^\./ , '' ) ;
return _PRISM _LANG _MAP [ ext ] !== undefined ? _PRISM _LANG _MAP [ ext ] : 'plaintext' ;
}
2026-05-31 11:58:45 +00:00
async function openFile ( path , opts = { } ) {
2026-03-31 03:40:19 +00:00
if ( ! S . session ) return ;
const ext = fileExt ( path ) ;
2026-05-31 11:58:45 +00:00
const bustCache = ! ! ( opts && opts . bustCache ) ;
2026-06-03 03:58:22 +00:00
const forceRichMarkdown = ! ! ( opts && opts . forceRichMarkdown ) ;
2026-05-31 11:58:45 +00:00
const cacheBust = bustCache ? ` &_= ${ Date . now ( ) } ` : '' ;
2026-03-31 03:40:19 +00:00
// Binary/download-only formats: trigger browser download, don't preview
if ( DOWNLOAD _EXTS . has ( ext ) ) {
downloadFile ( path ) ;
return ;
}
$ ( 'previewPathText' ) . textContent = path ;
$ ( 'previewArea' ) . classList . add ( 'visible' ) ;
$ ( 'fileTree' ) . style . display = 'none' ;
_previewCurrentPath = path ;
2026-04-12 17:51:48 +00:00
renderFileBreadcrumb ( path ) ;
2026-03-31 03:40:19 +00:00
if ( IMAGE _EXTS . has ( ext ) ) {
// Image: load via raw endpoint, show as <img>
showPreview ( 'image' ) ;
2026-05-31 11:58:45 +00:00
const url = ` api/file/raw?session_id= ${ encodeURIComponent ( S . session . session _id ) } &path= ${ encodeURIComponent ( path ) } ${ cacheBust } ` ;
2026-03-31 03:40:19 +00:00
$ ( 'previewImg' ) . alt = path ;
$ ( 'previewImg' ) . src = url ;
2026-04-08 14:55:03 +00:00
$ ( 'previewImg' ) . onerror = ( ) => setStatus ( t ( 'image_load_failed' ) ) ;
2026-04-30 02:54:07 +00:00
} else if ( AUDIO _EXTS . has ( ext ) || VIDEO _EXTS . has ( ext ) ) {
const mode = VIDEO _EXTS . has ( ext ) ? 'video' : 'audio' ;
showPreview ( mode ) ;
2026-05-31 11:58:45 +00:00
const url = ` api/file/raw?session_id= ${ encodeURIComponent ( S . session . session _id ) } &path= ${ encodeURIComponent ( path ) } &inline=1 ${ cacheBust } ` ;
2026-04-30 02:54:07 +00:00
const wrap = $ ( 'previewMediaWrap' ) ;
if ( wrap ) {
wrap . innerHTML = ( typeof _mediaPlayerHtml === 'function' )
? _mediaPlayerHtml ( mode , url , path . split ( '/' ) . pop ( ) || path )
: ` < ${ mode } src=" ${ url . replace ( /"/g , '%22' ) } " controls preload="metadata"></ ${ mode } > ` ;
if ( typeof _applyMediaPlaybackPreferences === 'function' ) _applyMediaPlaybackPreferences ( wrap ) ;
}
} else if ( PDF _EXTS . has ( ext ) ) {
showPreview ( 'pdf' ) ;
2026-05-31 11:58:45 +00:00
const url = ` api/file/raw?session_id= ${ encodeURIComponent ( S . session . session _id ) } &path= ${ encodeURIComponent ( path ) } &inline=1 ${ cacheBust } ` ;
2026-04-30 02:54:07 +00:00
const frame = $ ( 'previewPdfFrame' ) ;
if ( frame ) {
frame . src = '' ; // clear first to avoid stale content
frame . src = url ;
frame . title = ` PDF preview: ${ path . split ( '/' ) . pop ( ) || path } ` ;
}
2026-03-31 03:40:19 +00:00
} else if ( MD _EXTS . has ( ext ) ) {
// Markdown: fetch text, render with renderMd, display as formatted HTML
try {
2026-06-03 03:58:22 +00:00
// #3378 review (Codex): only reuse cached raw content when it actually
// belongs to the requested path. `path===_previewCurrentPath` is tautological
// here (_previewCurrentPath was just assigned above), so guard on the
// dedicated _previewRawContentPath instead — otherwise a force-render after a
// file switch could re-render the previous file's cached content.
const data = forceRichMarkdown && path === _previewRawContentPath && _previewRawContent
? { content : _previewRawContent }
: await api ( ` /api/file?session_id= ${ encodeURIComponent ( S . session . session _id ) } &path= ${ encodeURIComponent ( path ) } ` ) ;
2026-03-31 03:40:19 +00:00
_previewRawContent = data . content ;
2026-06-03 03:58:22 +00:00
_previewRawContentPath = path ;
if ( ! forceRichMarkdown && shouldRenderMarkdownPreviewAsPlainText ( data . content ) ) {
2026-05-24 09:30:38 +00:00
showPreview ( 'code' ) ;
$ ( 'previewCode' ) . textContent = data . content ;
2026-06-03 03:58:22 +00:00
setLargeMarkdownForceRenderVisible ( true ) ;
2026-05-24 09:30:38 +00:00
setStatus ( largeMarkdownPlainTextStatus ( data . content ) ) ;
return ;
}
2026-06-03 03:58:22 +00:00
renderMarkdownPreviewContent ( data ) ;
2026-04-08 14:55:03 +00:00
} catch ( e ) { setStatus ( t ( 'file_open_failed' ) ) ; }
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
} else if ( HTML _EXTS . has ( ext ) ) {
// HTML: render in sandboxed iframe via raw endpoint.
// SECURITY TRADEOFF: We use sandbox="allow-scripts" which lets inline JS run
// but prevents access to the parent frame (origin isolation). This is a
// deliberate choice — the user is previewing their own workspace files, so
// blocking scripts entirely would break most HTML documents. The sandbox
// still prevents the preview from navigating the parent, accessing cookies,
// or reading other origin data. If a stricter mode is needed, remove
// allow-scripts (or add sandbox="") to disable all JS execution.
showPreview ( 'html' ) ;
2026-05-31 11:58:45 +00:00
const url = ` api/file/raw?session_id= ${ encodeURIComponent ( S . session . session _id ) } &path= ${ encodeURIComponent ( path ) } &inline=1 ${ cacheBust } ` ;
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
const iframe = $ ( 'previewHtmlIframe' ) ;
if ( iframe ) {
iframe . src = '' ; // clear first to avoid stale content
iframe . src = url ;
}
2026-06-14 05:52:40 +00:00
} else if ( ext === '.csv' ) {
try {
const data = await api ( ` /api/file?session_id= ${ encodeURIComponent ( S . session . session _id ) } &path= ${ encodeURIComponent ( path ) } ` ) ;
if ( data . binary ) {
downloadFile ( path ) ;
return ;
}
if ( renderCsvPreviewContent ( path , data . content ) ) return ;
renderCodePreviewContent ( path , data . content ) ;
} catch ( e ) {
downloadFile ( path ) ;
}
2026-03-31 03:40:19 +00:00
} else {
// Plain code / text -- but fall back to download if server signals binary
try {
const data = await api ( ` /api/file?session_id= ${ encodeURIComponent ( S . session . session _id ) } &path= ${ encodeURIComponent ( path ) } ` ) ;
if ( data . binary ) {
// Server flagged this as binary content
downloadFile ( path ) ;
return ;
}
2026-06-14 05:52:40 +00:00
renderCodePreviewContent ( path , data . content ) ;
2026-03-31 03:40:19 +00:00
} catch ( e ) {
// If it's a 400/too-large error, offer download instead
downloadFile ( path ) ;
}
}
}
function downloadFile ( path ) {
if ( ! S . session ) return ;
// Trigger browser download via the raw file endpoint with content-disposition attachment
2026-04-16 18:20:08 +00:00
const url = ` api/file/raw?session_id= ${ encodeURIComponent ( S . session . session _id ) } &path= ${ encodeURIComponent ( path ) } &download=1 ` ;
2026-03-31 03:40:19 +00:00
const filename = path . split ( '/' ) . pop ( ) ;
const a = document . createElement ( 'a' ) ;
a . href = url ; a . download = filename ;
document . body . appendChild ( a ) ; a . click ( ) ;
setTimeout ( ( ) => document . body . removeChild ( a ) , 100 ) ;
2026-04-08 14:55:03 +00:00
showToast ( t ( 'downloading' , filename ) , 2000 ) ;
2026-03-31 03:40:19 +00:00
}
2026-04-12 17:51:48 +00:00
// ── Render breadcrumb for file preview mode ──────────────────────────────────
function renderFileBreadcrumb ( filePath ) {
const bar = $ ( 'breadcrumbBar' ) ;
if ( ! bar ) return ;
bar . style . display = 'flex' ;
const upBtn = $ ( 'btnUpDir' ) ;
if ( upBtn ) upBtn . style . display = '' ;
bar . innerHTML = '' ;
// Root
const root = document . createElement ( 'span' ) ;
root . className = 'breadcrumb-seg breadcrumb-link' ;
root . textContent = '~' ;
2026-05-07 05:14:47 +00:00
root . onclick = ( ) => { loadDir ( '.' ) ; } ;
2026-04-12 17:51:48 +00:00
bar . appendChild ( root ) ;
const parts = filePath . split ( '/' ) ;
let accumulated = '' ;
for ( let i = 0 ; i < parts . length ; i ++ ) {
const sep = document . createElement ( 'span' ) ;
sep . className = 'breadcrumb-sep' ;
sep . textContent = '/' ;
bar . appendChild ( sep ) ;
accumulated += ( accumulated ? '/' : '' ) + parts [ i ] ;
const seg = document . createElement ( 'span' ) ;
seg . textContent = parts [ i ] ;
if ( i < parts . length - 1 ) {
seg . className = 'breadcrumb-seg breadcrumb-link' ;
const target = accumulated ;
2026-05-07 05:14:47 +00:00
seg . onclick = ( ) => { loadDir ( target ) ; } ;
2026-04-12 17:51:48 +00:00
} else {
seg . className = 'breadcrumb-seg breadcrumb-current' ;
}
bar . appendChild ( seg ) ;
}
}
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
function openInBrowser ( ) {
if ( ! _previewCurrentPath || ! S . session ) return ;
2026-06-01 02:50:22 +00:00
const url = ` api/file/raw?session_id= ${ encodeURIComponent ( S . session . session _id ) } &path= ${ encodeURIComponent ( _previewCurrentPath ) } &inline=1 ` ;
window . open ( url , '_blank' , 'noopener' ) ;
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
}
2026-06-02 00:26:53 +00:00
// ── Workspace upload ──────────────────────────────────────────────────
function triggerWorkspaceUpload ( ) {
const input = $ ( 'workspaceFileInput' ) ;
if ( ! input ) return ;
input . value = '' ;
input . onchange = async ( ) => {
const files = input . files ;
if ( ! files || ! files . length ) return ;
for ( const file of files ) {
await uploadToWorkspace ( file , S . currentDir || '.' ) ;
}
if ( S . session ) loadDir ( S . currentDir ) ;
} ;
input . click ( ) ;
}
async function uploadToWorkspace ( file , dir ) {
if ( ! S . session ) return ;
const formData = new FormData ( ) ;
formData . append ( 'session_id' , S . session . session _id ) ;
formData . append ( 'path' , dir || '.' ) ;
formData . append ( 'file' , file , file . name ) ;
try {
showToast ( t ( 'uploading' ) || 'Uploading\u2026' , 2000 ) ;
const data = await api ( '/api/workspace/upload' , {
method : 'POST' ,
body : formData ,
headers : { } ,
timeoutMs : 120000 ,
} ) ;
if ( data && data . error ) {
showToast ( data . error , 5000 , 'error' ) ;
2026-06-02 02:20:05 +00:00
} else if ( data && ( data . extract _error || ( Array . isArray ( data . files ) && data . files . some ( function ( f ) { return f && f . extract _error ; } ) ) ) ) {
// Archive was rejected (zip-slip / zip-bomb / corrupt / too-many-members):
// the file uploaded but extraction failed. Surface it as an error instead
// of a misleading "Uploaded" success toast.
var msg = data . extract _error
|| ( data . files . find ( function ( f ) { return f && f . extract _error ; } ) || { } ) . extract _error
|| 'Archive extraction failed' ;
showToast ( msg , 5000 , 'error' ) ;
2026-06-02 00:26:53 +00:00
} else {
showToast ( t ( 'uploaded' ) || ( 'Uploaded ' + ( data . filename || file . name ) ) , 2000 ) ;
}
} catch ( e ) {
showToast ( t ( 'upload_failed' ) || ( 'Upload failed: ' + e . message ) , 5000 , 'error' ) ;
}
}
2026-06-04 01:19:09 +00:00
function _isOsFilesDrag ( e ) {
return ! ! ( e . dataTransfer && e . dataTransfer . types && e . dataTransfer . types . includes ( 'Files' ) ) ;
}
function _joinWorkspacePath ( base , rel ) {
const b = base || '.' ;
const r = ( rel || '' ) . replace ( /^\/+|\/+$/g , '' ) ;
if ( ! r ) return b ;
return b === '.' ? r : ` ${ b } / ${ r } ` ;
}
function _targetDirForRelDir ( destDir , relDir ) {
const dirPart = ( relDir || '' ) . replace ( /\/+$/ , '' ) ;
if ( ! dirPart ) return destDir || '.' ;
return _joinWorkspacePath ( destDir , dirPart ) ;
}
async function _readAllDirectoryEntries ( reader ) {
const entries = [ ] ;
while ( true ) {
const batch = await new Promise ( ( resolve , reject ) => {
reader . readEntries ( resolve , reject ) ;
} ) ;
if ( ! batch . length ) break ;
entries . push ( ... batch ) ;
}
return entries ;
}
async function _collectFilesFromEntry ( entry , relPrefix ) {
if ( entry . isFile ) {
const file = await new Promise ( ( resolve , reject ) => {
entry . file ( resolve , reject ) ;
} ) ;
return [ { file , relDir : relPrefix || '' } ] ;
}
if ( ! entry . isDirectory ) return [ ] ;
const reader = entry . createReader ( ) ;
const children = await _readAllDirectoryEntries ( reader ) ;
const dirPrefix = ` ${ relPrefix || '' } ${ entry . name } / ` ;
let out = [ ] ;
for ( const child of children ) {
out = out . concat ( await _collectFilesFromEntry ( child , dirPrefix ) ) ;
}
return out ;
}
async function _collectOsDropUploads ( dataTransfer ) {
const out = [ ] ;
const items = dataTransfer . items ? [ ... dataTransfer . items ] : [ ] ;
if ( items . length && typeof items [ 0 ] . webkitGetAsEntry === 'function' ) {
for ( const item of items ) {
if ( item . kind !== 'file' ) continue ;
const entry = item . webkitGetAsEntry ( ) ;
if ( ! entry ) continue ;
out . push ( ... await _collectFilesFromEntry ( entry , '' ) ) ;
}
if ( out . length ) return out ;
}
for ( const file of dataTransfer . files ) {
out . push ( { file , relDir : '' } ) ;
}
return out ;
}
async function uploadOsDropToWorkspace ( dataTransfer , destDir ) {
if ( ! S . session || ! dataTransfer ) return ;
const uploads = await _collectOsDropUploads ( dataTransfer ) ;
for ( const { file , relDir } of uploads ) {
await uploadToWorkspace ( file , _targetDirForRelDir ( destDir , relDir ) ) ;
}
if ( S . session ) await loadDir ( S . currentDir ) ;
}
function _clearWorkspaceOsUploadDragOver ( ) {
document . querySelectorAll ( '.file-item.drag-over-upload,.breadcrumb-seg.drag-over-upload' ) . forEach ( ( el ) => {
el . classList . remove ( 'drag-over-upload' ) ;
} ) ;
}
function _bindWorkspaceOsUploadDropTarget ( el , destDir ) {
// Use addEventListener (not on-property assignment) so these OS-upload
// handlers COMPOSE with the workspace tree-MOVE handlers bound by
// _bindWorkspaceMoveDropTarget() on the same element. A property assignment
// for the drop handler here would overwrite the move handler, and a
// workspace-file drag would fall through to the document drop (inserting
// @path into the composer) instead of moving the file. Each handler gates on
// its own drag type (_isOsFilesDrag vs _isWorkspaceTreeMoveDrag), so only the
// matching one acts.
el . addEventListener ( 'dragenter' , ( e ) => {
if ( ! _isOsFilesDrag ( e ) ) return ;
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
el . classList . add ( 'drag-over-upload' ) ;
} ) ;
el . addEventListener ( 'dragover' , ( e ) => {
if ( ! _isOsFilesDrag ( e ) ) return ;
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
e . dataTransfer . dropEffect = 'copy' ;
el . classList . add ( 'drag-over-upload' ) ;
} ) ;
el . addEventListener ( 'dragleave' , ( e ) => {
if ( el . contains ( e . relatedTarget ) ) return ;
el . classList . remove ( 'drag-over-upload' ) ;
} ) ;
el . addEventListener ( 'drop' , async ( e ) => {
if ( ! _isOsFilesDrag ( e ) ) return ;
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
el . classList . remove ( 'drag-over-upload' ) ;
await uploadOsDropToWorkspace ( e . dataTransfer , destDir ) ;
} ) ;
}
2026-06-02 00:26:53 +00:00
// Drag-and-drop files onto workspace file tree
if ( typeof document !== 'undefined' ) {
const _wsUploadInit = ( ) => {
const tree = $ ( 'fileTree' ) ;
if ( ! tree ) return ;
2026-06-03 03:58:22 +00:00
tree . addEventListener ( 'dragenter' , ( e ) => {
if ( e . dataTransfer && e . dataTransfer . types && e . dataTransfer . types . includes ( 'Files' ) ) {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
}
} ) ;
2026-06-02 00:26:53 +00:00
tree . addEventListener ( 'dragover' , ( e ) => {
if ( e . dataTransfer && e . dataTransfer . types && e . dataTransfer . types . includes ( 'Files' ) ) {
e . preventDefault ( ) ;
2026-06-03 03:58:22 +00:00
e . stopPropagation ( ) ;
2026-06-16 23:19:47 +00:00
if ( e . target . closest ( '.file-item[data-ws-type="dir"],.file-item[data-ws-is-dir="true"],.breadcrumb-seg' ) ) return ;
2026-06-02 00:26:53 +00:00
e . dataTransfer . dropEffect = 'copy' ;
tree . classList . add ( 'drag-over-upload' ) ;
}
} ) ;
2026-06-04 01:19:09 +00:00
tree . addEventListener ( 'dragleave' , ( e ) => {
if ( tree . contains ( e . relatedTarget ) ) return ;
2026-06-02 00:26:53 +00:00
tree . classList . remove ( 'drag-over-upload' ) ;
} ) ;
tree . addEventListener ( 'drop' , async ( e ) => {
tree . classList . remove ( 'drag-over-upload' ) ;
if ( ! e . dataTransfer || ! e . dataTransfer . types || ! e . dataTransfer . types . includes ( 'Files' ) ) return ;
2026-06-16 23:19:47 +00:00
if ( e . target . closest ( '.file-item[data-ws-type="dir"],.file-item[data-ws-is-dir="true"],.breadcrumb-seg' ) ) return ;
2026-06-02 00:26:53 +00:00
e . preventDefault ( ) ;
2026-06-03 03:58:22 +00:00
e . stopPropagation ( ) ;
2026-06-04 01:19:09 +00:00
await uploadOsDropToWorkspace ( e . dataTransfer , S . currentDir || '.' ) ;
2026-06-02 00:26:53 +00:00
} ) ;
} ;
if ( document . readyState === 'loading' ) {
document . addEventListener ( 'DOMContentLoaded' , _wsUploadInit , { once : true } ) ;
} else {
_wsUploadInit ( ) ;
}
}