HERMES-GIELIE/static/ui.js

6142 lines
279 KiB
JavaScript
Raw Normal View History

const S={session:null,messages:[],entries:[],busy:false,pendingFiles:[],toolCalls:[],activeStreamId:null,currentDir:'.',activeProfile:'default'};
const INFLIGHT={}; // keyed by session_id while request in-flight
const SESSION_QUEUES={}; // keyed by session_id for queued follow-up turns
// Tracks which session's queue to drain in setBusy(false).
// Set to activeSid just before setBusy(false) in done/error handlers so the
// queue drains the session that *finished*, not the one currently viewed.
// Single-shot: setBusy() reads and clears this on every call. Concurrent
// back-to-back stream completions would overwrite it, but HTTPServer is
// single-threaded so only one done event fires at a time in practice.
let _queueDrainSid=null;
const $=id=>document.getElementById(id);
// Redirect to login when the server responds with 401 (auth session expired).
// Handles iOS PWA standalone mode and keeps subpath mounts like /hermes/ from
// escaping to the personal site root /login.
function _redirectIfUnauth(res){if(res&&res.status===401){window.location.href='login?next='+encodeURIComponent(window.location.pathname+window.location.search);return true;}return false;}
function _getSessionQueue(sid, create=false){
if(!sid) return [];
if(!SESSION_QUEUES[sid]&&create) SESSION_QUEUES[sid]=[];
return SESSION_QUEUES[sid]||[];
}
function queueSessionMessage(sid, payload){
if(!sid||!payload) return 0;
const q=_getSessionQueue(sid,true);
// Stamp created_at so the restore path can detect stale entries (agent already responded)
const entry={...payload, _queued_at: Date.now()};
q.push(entry);
// Persist to sessionStorage so the queue survives page refresh
try{ sessionStorage.setItem('hermes-queue-'+sid, JSON.stringify(q)); }catch(_){}
return q.length;
}
function shiftQueuedSessionMessage(sid){
const q=_getSessionQueue(sid,false);
if(!q.length) return null;
const next=q.shift();
if(!q.length){
delete SESSION_QUEUES[sid];
try{ sessionStorage.removeItem('hermes-queue-'+sid); }catch(_){}
} else {
try{ sessionStorage.setItem('hermes-queue-'+sid, JSON.stringify(q)); }catch(_){}
}
return next;
}
function getQueuedSessionCount(sid){
return _getSessionQueue(sid,false).length;
}
function _compressionSessionLock(){
return window._compressionLockSid||null;
}
function _setCompressionSessionLock(sid){
window._compressionLockSid=sid||null;
}
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
2026-05-05 15:36:17 +00:00
function _matchBacktickFenceLine(line){
const m=String(line||'').match(/^[ ]{0,3}(`{3,})([^`]*)$/);
if(!m) return null;
return {fence:m[1],len:m[1].length,info:(m[2]||'').trim()};
}
function _isBacktickFenceClose(line,minLen){
const m=String(line||'').match(/^[ ]{0,3}(`{3,})[ \t]*$/);
return !!(m&&m[1].length>=minLen);
}
/**
* Render fenced code blocks inside user messages.
* Extracts ```…``` fences, replaces them with placeholders,
* escapes remaining text as plain HTML, then restores code blocks
* with the same <pre><code> pipeline used by renderMd().
* All non-fenced text stays escaped (no bold/italic/link interpretation).
*/
2026-05-04 23:58:54 +00:00
function _renderUserFencedBlocks(text){
const stash=[];
let s=String(text||'');
// Extract fenced code blocks → stash, replace with null-token placeholder
2026-05-05 15:36:17 +00:00
// CommonMark §4.5 line-anchored fence: the closing run must use at least
// as many backticks as the opener, so inner triple-backtick fences remain content.
s=s.replace(/(^|\n)[ ]{0,3}(`{3,})([^\n`]*)\n(?:([\s\S]*?)\n)?[ ]{0,3}\2`*[ \t]*(?=\n|$)/g,(_,lead,_fence,info,code)=>{
const langInfo=(info||'').trim();
const langMatch=langInfo.match(/^(\w[\w+-]*)$/);
let lang=langMatch?(langMatch[1]||'').trim().toLowerCase():'';
fix(renderer): line-anchor fence regex to prevent mid-line ``` corruption (#1438) The markdown fence regex /```([\s\S]*?)```/g had no line anchoring. A literal triple backtick inside code block content (e.g. a regex with ``` in a lookbehind, or a script that documents fences) terminated the outer fence at the wrong place. The leaked tail then went through bold/italic/inline-code passes, eating `*` characters as italic markers and emitting literal </strong> tags into the rendered output. CommonMark §4.5 requires that an opening code fence be the first non-whitespace content of a line (up to 3 spaces of indent allowed) and that the closing fence also start a line. This patch updates 3 sites + the Python mirror to use that invariant: static/ui.js:1559 renderMd() fenced-block stash (assistant messages) static/ui.js:66 _renderUserFencedBlocks() (user messages) static/ui.js:2599 _stripForTTS() (TTS speech pre-strip) tests/test_sprint16.py Python mirror Pattern: (^|\n)[ ]{0,3}```(?:([\s\S]*?)\n)?[ ]{0,3}```(?=\n|$) The non-capturing (?:...\n)? group keeps empty fences (```\n```) working; without it, a body+\n is required and the closing fence on the very next line no longer matches. The lead group (^|\n) is prefixed back to the stash token so paragraphs above don't bleed into the <pre> block. 20 regression tests in tests/test_issue1438_fence_anchoring.py cover: - Cygnus's exact repro from Discord (May 1 2026) - Inline ``` mid-paragraph (must not open fence) - Partial/streaming fence with no close (must not eat content) - Empty fences with and without language tag - 3-space indented fences (allowed) vs 4-space (not a fence) - Multiple adjacent blocks - Bold/italic/inline-code surviving after a fence - Source-level guards on all 3 patched sites + lead-prefix invariant Empirical browser verification (live JS, on bug repro): Before fix: </code></pre>[^\n]<em>|%%[ \t]</em>... ← truncated, italic leak After fix: <pre><code>...```[^\n]*|%%...</code></pre> ← intact, regex preserved Tests: 3678 passed (+20 from new test file, was 3658), 0 failures. Reported-By: Cygnus (Discord) Relayed-By: @AvidFuturist Closes #1438
2026-05-02 02:30:20 +00:00
code=code||'';
// Remove one trailing newline if present (the fence consumes its own)
if(code.endsWith('\n')) code=code.slice(0,-1);
const h=lang?`<div class="pre-header">${esc(lang)}</div>`:'';
const langAttr=lang?` class="language-${esc(lang)}"`:'';
if(lang==='diff'||lang==='patch'){
const colored=esc(code).split('\n').map(line=>{
if(line.startsWith('@@')) return `<span class="diff-line diff-hunk">${line}</span>`;
if(line.startsWith('+')) return `<span class="diff-line diff-plus">${line}</span>`;
if(line.startsWith('-')) return `<span class="diff-line diff-minus">${line}</span>`;
return `<span class="diff-line">${line}</span>`;
}).join('\n');
stash.push(`${h}<pre class="diff-block"><code${langAttr}>${colored}</code></pre>`);
} else {
stash.push(`${h}<pre><code${langAttr}>${esc(code)}</code></pre>`);
}
fix(renderer): line-anchor fence regex to prevent mid-line ``` corruption (#1438) The markdown fence regex /```([\s\S]*?)```/g had no line anchoring. A literal triple backtick inside code block content (e.g. a regex with ``` in a lookbehind, or a script that documents fences) terminated the outer fence at the wrong place. The leaked tail then went through bold/italic/inline-code passes, eating `*` characters as italic markers and emitting literal </strong> tags into the rendered output. CommonMark §4.5 requires that an opening code fence be the first non-whitespace content of a line (up to 3 spaces of indent allowed) and that the closing fence also start a line. This patch updates 3 sites + the Python mirror to use that invariant: static/ui.js:1559 renderMd() fenced-block stash (assistant messages) static/ui.js:66 _renderUserFencedBlocks() (user messages) static/ui.js:2599 _stripForTTS() (TTS speech pre-strip) tests/test_sprint16.py Python mirror Pattern: (^|\n)[ ]{0,3}```(?:([\s\S]*?)\n)?[ ]{0,3}```(?=\n|$) The non-capturing (?:...\n)? group keeps empty fences (```\n```) working; without it, a body+\n is required and the closing fence on the very next line no longer matches. The lead group (^|\n) is prefixed back to the stash token so paragraphs above don't bleed into the <pre> block. 20 regression tests in tests/test_issue1438_fence_anchoring.py cover: - Cygnus's exact repro from Discord (May 1 2026) - Inline ``` mid-paragraph (must not open fence) - Partial/streaming fence with no close (must not eat content) - Empty fences with and without language tag - 3-space indented fences (allowed) vs 4-space (not a fence) - Multiple adjacent blocks - Bold/italic/inline-code surviving after a fence - Source-level guards on all 3 patched sites + lead-prefix invariant Empirical browser verification (live JS, on bug repro): Before fix: </code></pre>[^\n]<em>|%%[ \t]</em>... ← truncated, italic leak After fix: <pre><code>...```[^\n]*|%%...</code></pre> ← intact, regex preserved Tests: 3678 passed (+20 from new test file, was 3658), 0 failures. Reported-By: Cygnus (Discord) Relayed-By: @AvidFuturist Closes #1438
2026-05-02 02:30:20 +00:00
return lead+'\x00UF'+(stash.length-1)+'\x00';
});
// Escape remaining plain text and convert newlines to <br>
s=esc(s).replace(/\n/g,'<br>');
// Restore stashed code blocks
s=s.replace(/\x00UF(\d+)\x00/g,(_,i)=>stash[+i]);
return s;
}
2026-05-04 23:58:54 +00:00
function _statusCardHtml(card){
card=card||{};
const rows=Array.isArray(card.rows)?card.rows:[];
const sessionId=String(card.sessionId||'');
const shortSessionId=sessionId.length>22?`${sessionId.slice(0,10)}…${sessionId.slice(-8)}`:sessionId;
const copyIcon=(typeof li==='function')?li('copy',13):'Copy';
const copyBtn=sessionId
? `<button class="status-card-session-copy" type="button" data-copy-status-session="${esc(card.sessionId||'')}" title="${esc(t('copy'))}" onclick="copyStatusSessionId(this);event.stopPropagation()"><span>${esc(shortSessionId)}</span>${copyIcon}</button>`
: '';
const rowHtml=rows.map(row=>`
<div class="status-card-row">
<span class="status-card-label">${esc(row.label||'')}</span>
<span class="status-card-value">${esc(row.value||'')}</span>
</div>`).join('');
return `<div class="status-card" data-status-card="1">
<div class="status-card-head">
<div class="status-card-title-wrap">
<div class="status-card-title">${esc(card.title||t('status_heading'))}</div>
<div class="status-card-subtitle">${esc(card.subtitle||'')}</div>
</div>
${copyBtn}
</div>
<div class="status-card-grid">${rowHtml}</div>
</div>`;
}
const MESSAGE_RENDER_WINDOW_DEFAULT=50;
let _messageRenderWindowSid=null;
let _messageRenderWindowSize=MESSAGE_RENDER_WINDOW_DEFAULT;
function _resetMessageRenderWindow(sid){
_messageRenderWindowSid=sid||null;
_messageRenderWindowSize=MESSAGE_RENDER_WINDOW_DEFAULT;
}
function _currentMessageRenderWindowSize(){
return Math.max(
MESSAGE_RENDER_WINDOW_DEFAULT,
Number(_messageRenderWindowSize)||MESSAGE_RENDER_WINDOW_DEFAULT
);
}
function _messageRenderableMessageCount(){
let count=0;
for(const m of (S.messages||[])){
if(!m||!m.role||m.role==='tool') continue;
if(_isContextCompactionMessage(m)||_isPreservedCompressionTaskListMessage(m)) continue;
const hasTc=Array.isArray(m.tool_calls)&&m.tool_calls.length>0;
const hasTu=Array.isArray(m.content)&&m.content.some(p=>p&&p.type==='tool_use');
if(msgContent(m)||m.attachments?.length||(m.role==='assistant'&&(hasTc||hasTu||_messageHasReasoningPayload(m)))) count++;
}
return count;
}
function _messageHiddenBeforeCount(){
return Math.max(0,_messageRenderableMessageCount()-_currentMessageRenderWindowSize());
}
function _wireMessageWindowLoadEarlierButton(){
const indicator=$('loadOlderIndicator');
if(!indicator) return;
indicator.onclick=()=>{
if(_messageHiddenBeforeCount()>0) _showEarlierRenderedMessages();
else if(typeof _loadOlderMessages==='function') _loadOlderMessages();
};
}
function _showEarlierRenderedMessages(){
const container=$('messages');
const prevScrollH=container?container.scrollHeight:0;
const prevScrollTop=container?container.scrollTop:0;
_messageRenderWindowSize=_currentMessageRenderWindowSize()+MESSAGE_RENDER_WINDOW_DEFAULT;
renderMessages();
if(container){
const newScrollH=container.scrollHeight;
container.scrollTop=prevScrollTop+(newScrollH-prevScrollH);
}
_scrollPinned=false;
}
2026-05-05 00:21:52 +00:00
const DASHBOARD_STATUS_TTL_MS=60000;
let _dashboardStatusCache=null;
let _dashboardStatusFetchedAt=0;
function _dashboardIsBrowserLoopback(){
const host=(window.location.hostname||'').replace(/^\[|\]$/g,'').toLowerCase();
return host==='127.0.0.1'||host==='localhost'||host==='::1';
}
function _dashboardBrowserUrl(status){
if(!status||!status.running||!status.port) return '';
let source;
try{source=new URL(status.url||('http://127.0.0.1:'+status.port));}
catch(_){source=new URL('http://127.0.0.1:'+status.port);}
const browserHost=window.location.hostname||source.hostname;
const displayHost=browserHost.includes(':')&&!browserHost.startsWith('[')?'['+browserHost+']':browserHost;
return source.protocol+'//'+displayHost+':'+status.port;
}
function _applyDashboardStatus(status){
const running=!!(status&&status.running);
const url=running?_dashboardBrowserUrl(status):'';
const warning=running&&!_dashboardIsBrowserLoopback()?t('dashboard_loopback_warning'):'';
document.querySelectorAll('[data-dashboard-link]').forEach(btn=>{
btn.classList.toggle('dashboard-link-visible',running);
btn.style.display=running?'':'none';
btn.dataset.dashboardUrl=url;
btn.title=warning||t('tab_dashboard');
btn.setAttribute('aria-label',warning||t('tab_dashboard'));
});
}
async function refreshDashboardStatus(force=false){
const now=Date.now();
if(!force&&_dashboardStatusCache&&(now-_dashboardStatusFetchedAt)<DASHBOARD_STATUS_TTL_MS){
_applyDashboardStatus(_dashboardStatusCache);
return _dashboardStatusCache;
}
try{
const status=await api('/api/dashboard/status');
_dashboardStatusCache=status||{running:false};
}catch(_){
_dashboardStatusCache={running:false};
}
_dashboardStatusFetchedAt=Date.now();
_applyDashboardStatus(_dashboardStatusCache);
return _dashboardStatusCache;
}
async function loadDashboardSettings(){
const modeEl=$('settingsDashboardMode');
const urlEl=$('settingsDashboardUrl');
if(!modeEl&&!urlEl) return;
try{
const cfg=await api('/api/dashboard/config');
if(modeEl) modeEl.value=cfg.enabled||'auto';
if(urlEl) urlEl.value=cfg.url||'';
}catch(_){/* leave defaults visible */}
}
async function saveDashboardSettings(){
const modeEl=$('settingsDashboardMode');
const urlEl=$('settingsDashboardUrl');
const statusEl=$('settingsDashboardStatus');
const payload={enabled:(modeEl&&modeEl.value)||'auto',url:(urlEl&&urlEl.value||'').trim()};
try{
const saved=await api('/api/dashboard/config',{method:'POST',body:JSON.stringify(payload)});
if(modeEl) modeEl.value=saved.enabled||'auto';
if(urlEl) urlEl.value=saved.url||'';
if(statusEl) statusEl.textContent='Dashboard link settings saved.';
await refreshDashboardStatus(true);
}catch(err){
if(statusEl) statusEl.textContent='Dashboard link settings failed to save.';
else if(typeof showToast==='function') showToast('Dashboard link settings failed to save.');
}
}
function openHermesDashboard(event){
if(event){event.preventDefault();event.stopPropagation();}
const btn=event&&event.currentTarget?event.currentTarget:document.querySelector('[data-dashboard-link]');
const url=(btn&&btn.dataset&&btn.dataset.dashboardUrl)||_dashboardBrowserUrl(_dashboardStatusCache);
if(!url) return false;
window.open(url,'_blank','noopener,noreferrer');
return false;
}
function _initDashboardLinkProbe(){
loadDashboardSettings();
refreshDashboardStatus(true);
setInterval(refreshDashboardStatus,DASHBOARD_STATUS_TTL_MS);
}
if(document.readyState==='complete'){
_initDashboardLinkProbe();
}else{
document.addEventListener('DOMContentLoaded',_initDashboardLinkProbe,{once:true});
}
/* ── Image lightbox — click any .msg-media-img to enlarge ─────────────────── */
function _openImgLightbox(src, alt) {
const lb = document.createElement('div');
lb.className = 'img-lightbox';
lb.setAttribute('role', 'dialog');
lb.setAttribute('aria-label', alt || 'Image');
const img = document.createElement('img');
img.src = src;
img.alt = alt || '';
img.onclick = e => e.stopPropagation();
const cls = document.createElement('button');
cls.className = 'img-lightbox-close';
cls.setAttribute('aria-label', 'Close');
cls.textContent = '×';
cls.onclick = () => _closeImgLightbox(lb);
lb.appendChild(img);
lb.appendChild(cls);
lb.onclick = () => _closeImgLightbox(lb);
document.body.appendChild(lb);
// Close on Escape
lb._escHandler = e => { if(e.key==='Escape') _closeImgLightbox(lb); };
document.addEventListener('keydown', lb._escHandler);
}
function _closeImgLightbox(lb) {
if(!lb || !lb.parentNode) return;
document.removeEventListener('keydown', lb._escHandler);
lb.style.animation = 'lb-in .12s ease reverse';
setTimeout(() => lb.parentNode && lb.parentNode.removeChild(lb), 120);
}
document.addEventListener('click', e => {
const img = e.target && e.target.closest ? e.target.closest('.msg-media-img') : null;
if(!img) return;
_openImgLightbox(img.src, img.alt);
});
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117) * fix(#1096): copy buttons fall back to execCommand on HTTP contexts - Add _copyText() helper: tries navigator.clipboard first, falls back to document.execCommand('copy') with hidden textarea when not in secure context - Update copyMsg() and addCopyButtons() to use helper instead of direct navigator.clipboard.writeText() - Code block copy button now has .catch() handler (was silently failing) - Error messages use t('copy_failed') for i18n instead of hardcoded string - Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant) - Add 10 regression tests * fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge - User message attachments with image extensions now render as <img> via api/media endpoint, with click-to-fullscreen support - Non-image attachments still show paperclip + filename badge - Extracts filename from full path for display - Add 5 regression tests * fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix) * fix: improve mobile touch responsiveness for session list items iPad Safari has known issues with the click/dblclick pattern on touch: - :hover-triggered padding-right layout shift causes the first tap click to target the wrong element (actions button that just appeared) - No touch-action:manipulation means iOS still delays taps for double-tap zoom detection - The old onclick+ondblclick pattern is designed for mouse, not touch Changes: - CSS: Remove :hover from padding-right rule to prevent layout shift - CSS: Add touch-action:manipulation and -webkit-tap-highlight-color to .session-item for immediate tap response - JS: Replace onclick/ondblclick with onpointerup + manual 350ms double-tap detection — works consistently on mouse and touch * fix(#1106): iterate custom_providers[].models dict keys for dropdown population - After reading singular 'model' field, also iterate 'models' dict keys - Deduplicate: model field value not repeated if also in models dict - Skip non-string keys gracefully - Works for both named and unnamed custom_providers entries - Add 7 regression tests * fix(#1105): allow custom_providers hostnames through SSRF check - Build trusted hostname set from custom_providers[].base_url in config.yaml - These are user-explicitly configured endpoints — not SSRF risks - Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active - Unknown private IPs still blocked - Add 7 tests (5 source analysis + 2 functional with mocked socket) * fix(tests): update hover padding assertions for #1110 touch fix (absorb) * fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb) * fix: filter right/middle-click from pointerup handler (absorb) * docs: v0.50.221 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
const _IMAGE_EXTS=/\.(png|jpg|jpeg|gif|webp|bmp|ico|avif)$/i;
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const _PDF_EXTS=/\.pdf$/i;
const _HTML_EXTS=/\.(html?|htm)$/i;
const _ARCHIVE_EXTS=/\.(zip|tar|tar\.gz|tgz|tar\.bz2|tbz2|tar\.xz|txz)$/i;
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const _SVG_EXTS=/\.svg$/i;
const _AUDIO_EXTS=/\.(mp3|ogg|wav|m4a|aac|flac|wma|opus|webm|oga)$/i;
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const _VIDEO_EXTS=/\.(mp4|webm|mkv|mov|avi|ogv|m4v)$/i;
const _CSV_EXTS=/\.csv$/i;
const _EXCALIDRAW_EXTS=/\.excalidraw$/i;
// ── Media playback speed controls ─────────────────────────────────────────
const MEDIA_PLAYBACK_RATES=[0.5,0.75,1,1.25,1.5,2];
const MEDIA_PLAYBACK_STORAGE_KEY='hermes-media-playback-rate';
function _getStoredMediaPlaybackRate(){
try{
const raw=localStorage.getItem(MEDIA_PLAYBACK_STORAGE_KEY);
const rate=Number(raw);
return MEDIA_PLAYBACK_RATES.includes(rate)?rate:1;
}catch(_){return 1;}
}
function _setStoredMediaPlaybackRate(rate){
if(!MEDIA_PLAYBACK_RATES.includes(rate)) return;
try{localStorage.setItem(MEDIA_PLAYBACK_STORAGE_KEY,String(rate));}catch(_){}
}
function _syncMediaSpeedButtons(editor, rate){
if(!editor) return;
editor.querySelectorAll('.media-speed-btn').forEach(b=>{
const active=Number(b.dataset.rate)===rate;
b.classList.toggle('active',active);
b.setAttribute('aria-pressed',active?'true':'false');
});
}
function _applyMediaPlaybackRate(media, rate=_getStoredMediaPlaybackRate()){
if(!media) return;
media.playbackRate=rate;
_syncMediaSpeedButtons(media.closest('.msg-media-editor,.preview-media-wrap'),rate);
}
function _mediaKindForName(name=''){
const clean=String(name||'').split('?')[0].toLowerCase();
if(_AUDIO_EXTS.test(clean)) return 'audio';
if(_VIDEO_EXTS.test(clean)) return 'video';
if(_IMAGE_EXTS.test(clean)) return 'image';
return '';
}
function _mediaSpeedControlsHtml(kind, label){
const safeLabel=esc(label||kind||'media');
const current=_getStoredMediaPlaybackRate();
return `<div class="media-speed-controls" role="group" aria-label="Playback speed for ${safeLabel}">${MEDIA_PLAYBACK_RATES.map(rate=>`<button type="button" class="media-speed-btn${rate===current?' active':''}" data-rate="${rate}" aria-pressed="${rate===current?'true':'false'}">${rate}×</button>`).join('')}</div>`;
}
function _mediaPlayerHtml(kind, src, name, extra=''){
const safeName=esc(name||'media');
const safeSrc=esc(src);
const tag=kind==='video'
? `<video class="msg-media-player msg-media-video" src="${safeSrc}" controls preload="metadata" playsinline title="${safeName}"></video>`
: `<audio class="msg-media-player msg-media-audio" src="${safeSrc}" controls preload="metadata" title="${safeName}"></audio>`;
return `<div class="msg-media-editor msg-media-editor--${kind}" data-media-kind="${kind}">${tag}<div class="msg-media-meta"><span class="msg-media-name">${safeName}</span>${extra}</div>${_mediaSpeedControlsHtml(kind,safeName)}</div>`;
}
function _renderAttachmentHtml(fname, url){
const kind=_mediaKindForName(fname);
if(kind==='image') return `<img class="msg-media-img" src="${esc(url)}" alt="${esc(fname)}" loading="lazy">`;
if(kind==='audio'||kind==='video') return _mediaPlayerHtml(kind,url,fname);
return `<div class="msg-file-badge">${li('paperclip',12)} ${esc(fname)}</div>`;
}
document.addEventListener('click', e => {
const btn=e.target&&e.target.closest?e.target.closest('.media-speed-btn'):null;
if(!btn) return;
const editor=btn.closest('.msg-media-editor,.preview-media-wrap');
if(!editor) return;
const media=editor.querySelector('audio,video');
if(!media) return;
const rate=Number(btn.dataset.rate)||1;
_setStoredMediaPlaybackRate(rate);
_applyMediaPlaybackRate(media,rate);
});
document.addEventListener("loadedmetadata", e=>{
if(e.target&&e.target.matches&&e.target.matches('.msg-media-player,audio,video')){
_applyMediaPlaybackRate(e.target);
}
},true);
function _initMediaPlaybackObserver(){
if(!document.body||window._mediaPlaybackObserver) return;
window._mediaPlaybackObserver=new MutationObserver(records=>{
for(const rec of records){
for(const node of rec.addedNodes||[]){
if(!node||node.nodeType!==1) continue;
const media=[];
if(node.matches&&node.matches('audio,video')) media.push(node);
if(node.querySelectorAll) media.push(...node.querySelectorAll('audio,video'));
media.forEach(m=>_applyMediaPlaybackRate(m));
}
}
});
window._mediaPlaybackObserver.observe(document.body,{childList:true,subtree:true});
document.querySelectorAll('audio,video').forEach(m=>_applyMediaPlaybackRate(m));
}
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',_initMediaPlaybackObserver);
else _initMediaPlaybackObserver();
setTimeout(_initMediaPlaybackObserver,0);
2026-03-31 07:02:47 +00:00
// Dynamic model labels -- populated by populateModelDropdown(), fallback to static map
let _dynamicModelLabels={};
window._configuredModelBadges=window._configuredModelBadges||{};
const MODEL_STATE_KEY='hermes-webui-model-state';
2026-03-31 07:02:47 +00:00
fix: model picker correctly updates on profile switch without flicker or raw injection Root cause: three interacting bugs caused the model picker to show the wrong model or flicker after a profile switch. Bug 1 — syncTopbar() fought switchToProfile(). After switchToProfile() set the picker to the profile's model, syncTopbar() was called (via renderSessionList -> loadSession, then explicitly at the end) and overwrote it with S.session.model -- the old session's model. Fix: added S._pendingProfileModel flag. switchToProfile() sets it; syncTopbar() checks it first, applies the override, then clears it. S.session.model is also updated to the resolved value so subsequent syncTopbar() calls are consistent. Bug 2 — Raw option injected at top of list for mismatched model IDs. Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent format: hyphens, no namespace prefix) but the dropdown has 'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix). The old code did sel.value = id, found no match, then injected a new <option> at the top of the list -- creating a lowercase duplicate that didn't match any real provider group entry. Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots, lowercase) and finds the best matching existing option. No new options are ever injected for profile switching. Bug 3 — populateModelDropdown() injected raw option on cold load. Same issue: if default_model from /api/models didn't exactly match a dropdown value, an extra option was added. Fixed by using _applyModelToDropdown() which only selects existing options. New helpers in ui.js: _findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value _applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
// ── Smart model resolver ────────────────────────────────────────────────────
// Finds the best matching option value in a <select> for a given model ID.
// Handles mismatches like 'claude-sonnet-4-6' vs 'anthropic/claude-sonnet-4.6'.
// When a preferred provider is supplied, duplicate normalized IDs prefer that
// provider's option so Settings/profile rehydration doesn't snap back to the
// first colliding entry.
function _getOptionProviderId(opt){
if(!opt) return '';
const group=opt.parentElement;
if(group && group.tagName==='OPTGROUP' && group.dataset && group.dataset.provider){
return group.dataset.provider;
}
const value=String(opt.value||'');
if(value.startsWith('@') && value.includes(':')) return value.slice(1,value.lastIndexOf(':'));
return '';
}
function _providerFromModelValue(modelId){
const value=String(modelId||'').trim();
if(value.startsWith('@')&&value.includes(':')) return value.slice(1,value.lastIndexOf(':'));
return '';
}
function _modelStateForSelect(sel, modelId){
const value=String(modelId||'').trim();
if(!value) return {model:'',model_provider:null};
const explicitProvider=_providerFromModelValue(value);
if(explicitProvider) return {model:value,model_provider:explicitProvider};
const opt=sel&&sel.selectedOptions&&sel.selectedOptions[0];
const provider=String(_getOptionProviderId(opt)||'').trim();
return {model:value,model_provider:(provider&&provider!=='default')?provider:null};
}
function _providerQualifiedModelValueForSelect(sel, modelId){
return _modelStateForSelect(sel,modelId).model;
}
function _readPersistedModelState(){
try{
const raw=localStorage.getItem(MODEL_STATE_KEY);
if(raw){
const parsed=JSON.parse(raw);
if(parsed&&parsed.model){
return {
model:String(parsed.model||''),
model_provider:parsed.model_provider?String(parsed.model_provider):(_providerFromModelValue(parsed.model)||null),
};
}
}
}catch(_){}
const legacy=localStorage.getItem('hermes-webui-model');
if(!legacy) return null;
return {model:legacy,model_provider:_providerFromModelValue(legacy)||null};
}
function _writePersistedModelState(model, modelProvider){
const value=String(model||'').trim();
const provider=modelProvider?String(modelProvider).trim():(_providerFromModelValue(value)||null);
if(!value){
localStorage.removeItem('hermes-webui-model');
localStorage.removeItem(MODEL_STATE_KEY);
return;
}
localStorage.setItem('hermes-webui-model', value);
try{
localStorage.setItem(MODEL_STATE_KEY, JSON.stringify({model:value,model_provider:provider||null}));
}catch(_){}
}
function _clearPersistedModelState(){
localStorage.removeItem('hermes-webui-model');
localStorage.removeItem(MODEL_STATE_KEY);
}
function _findModelInDropdown(modelId, sel, preferredProviderId){
fix: model picker correctly updates on profile switch without flicker or raw injection Root cause: three interacting bugs caused the model picker to show the wrong model or flicker after a profile switch. Bug 1 — syncTopbar() fought switchToProfile(). After switchToProfile() set the picker to the profile's model, syncTopbar() was called (via renderSessionList -> loadSession, then explicitly at the end) and overwrote it with S.session.model -- the old session's model. Fix: added S._pendingProfileModel flag. switchToProfile() sets it; syncTopbar() checks it first, applies the override, then clears it. S.session.model is also updated to the resolved value so subsequent syncTopbar() calls are consistent. Bug 2 — Raw option injected at top of list for mismatched model IDs. Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent format: hyphens, no namespace prefix) but the dropdown has 'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix). The old code did sel.value = id, found no match, then injected a new <option> at the top of the list -- creating a lowercase duplicate that didn't match any real provider group entry. Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots, lowercase) and finds the best matching existing option. No new options are ever injected for profile switching. Bug 3 — populateModelDropdown() injected raw option on cold load. Same issue: if default_model from /api/models didn't exactly match a dropdown value, an extra option was added. Fixed by using _applyModelToDropdown() which only selects existing options. New helpers in ui.js: _findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value _applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
if(!modelId||!sel) return null;
const options=Array.from(sel.options);
const opts=options.map(o=>o.value);
// 1. Normalize: lowercase, strip namespace prefix, replace hyphens→dots.
// Also strip @provider: prefix from deduplicated model IDs (#1228, #1313).
const norm=s=>s.toLowerCase().replace(/^[^/]+\//,'').replace(/^@([^:]+:)+/,'').replace(/-/g,'.');
fix: model picker correctly updates on profile switch without flicker or raw injection Root cause: three interacting bugs caused the model picker to show the wrong model or flicker after a profile switch. Bug 1 — syncTopbar() fought switchToProfile(). After switchToProfile() set the picker to the profile's model, syncTopbar() was called (via renderSessionList -> loadSession, then explicitly at the end) and overwrote it with S.session.model -- the old session's model. Fix: added S._pendingProfileModel flag. switchToProfile() sets it; syncTopbar() checks it first, applies the override, then clears it. S.session.model is also updated to the resolved value so subsequent syncTopbar() calls are consistent. Bug 2 — Raw option injected at top of list for mismatched model IDs. Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent format: hyphens, no namespace prefix) but the dropdown has 'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix). The old code did sel.value = id, found no match, then injected a new <option> at the top of the list -- creating a lowercase duplicate that didn't match any real provider group entry. Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots, lowercase) and finds the best matching existing option. No new options are ever injected for profile switching. Bug 3 — populateModelDropdown() injected raw option on cold load. Same issue: if default_model from /api/models didn't exactly match a dropdown value, an extra option was added. Fixed by using _applyModelToDropdown() which only selects existing options. New helpers in ui.js: _findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value _applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
const target=norm(modelId);
let explicitProvider='';
const rawModel=String(modelId||'');
if(rawModel.startsWith('@')&&rawModel.includes(':')){
explicitProvider=rawModel.slice(1,rawModel.lastIndexOf(':'));
}
const preferred=String(preferredProviderId||explicitProvider||'').toLowerCase();
if(preferred){
const providerMatch=options.find(o=>norm(o.value)===target && _getOptionProviderId(o).toLowerCase()===preferred);
if(providerMatch) return providerMatch.value;
}
// 2. Exact match
if(opts.includes(modelId)) return modelId;
fix: model picker correctly updates on profile switch without flicker or raw injection Root cause: three interacting bugs caused the model picker to show the wrong model or flicker after a profile switch. Bug 1 — syncTopbar() fought switchToProfile(). After switchToProfile() set the picker to the profile's model, syncTopbar() was called (via renderSessionList -> loadSession, then explicitly at the end) and overwrote it with S.session.model -- the old session's model. Fix: added S._pendingProfileModel flag. switchToProfile() sets it; syncTopbar() checks it first, applies the override, then clears it. S.session.model is also updated to the resolved value so subsequent syncTopbar() calls are consistent. Bug 2 — Raw option injected at top of list for mismatched model IDs. Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent format: hyphens, no namespace prefix) but the dropdown has 'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix). The old code did sel.value = id, found no match, then injected a new <option> at the top of the list -- creating a lowercase duplicate that didn't match any real provider group entry. Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots, lowercase) and finds the best matching existing option. No new options are ever injected for profile switching. Bug 3 — populateModelDropdown() injected raw option on cold load. Same issue: if default_model from /api/models didn't exactly match a dropdown value, an extra option was added. Fixed by using _applyModelToDropdown() which only selects existing options. New helpers in ui.js: _findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value _applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
const exact=opts.find(o=>norm(o)===target);
if(exact) return exact;
// 3. Prefix/substring: require the candidate to start with the FULL normalized target
// (not a truncated base). This avoids false matches like gpt.5.5 → gpt.5.4.mini (#1188).
// Only fall back to the shorter base form if target itself is very short (a bare root
// like "gpt" or "claude") where stripping would be a no-op anyway.
fix: model picker correctly updates on profile switch without flicker or raw injection Root cause: three interacting bugs caused the model picker to show the wrong model or flicker after a profile switch. Bug 1 — syncTopbar() fought switchToProfile(). After switchToProfile() set the picker to the profile's model, syncTopbar() was called (via renderSessionList -> loadSession, then explicitly at the end) and overwrote it with S.session.model -- the old session's model. Fix: added S._pendingProfileModel flag. switchToProfile() sets it; syncTopbar() checks it first, applies the override, then clears it. S.session.model is also updated to the resolved value so subsequent syncTopbar() calls are consistent. Bug 2 — Raw option injected at top of list for mismatched model IDs. Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent format: hyphens, no namespace prefix) but the dropdown has 'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix). The old code did sel.value = id, found no match, then injected a new <option> at the top of the list -- creating a lowercase duplicate that didn't match any real provider group entry. Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots, lowercase) and finds the best matching existing option. No new options are ever injected for profile switching. Bug 3 — populateModelDropdown() injected raw option on cold load. Same issue: if default_model from /api/models didn't exactly match a dropdown value, an extra option was added. Fixed by using _applyModelToDropdown() which only selects existing options. New helpers in ui.js: _findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value _applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
const base=target.replace(/\.\d+$/,''); // strip trailing version number
const useBase=base.length<=4||base===target; // bare root — stripping changed nothing meaningful
const prefixTarget=useBase?base:target;
const partial=opts.find(o=>norm(o).startsWith(prefixTarget));
fix: model picker correctly updates on profile switch without flicker or raw injection Root cause: three interacting bugs caused the model picker to show the wrong model or flicker after a profile switch. Bug 1 — syncTopbar() fought switchToProfile(). After switchToProfile() set the picker to the profile's model, syncTopbar() was called (via renderSessionList -> loadSession, then explicitly at the end) and overwrote it with S.session.model -- the old session's model. Fix: added S._pendingProfileModel flag. switchToProfile() sets it; syncTopbar() checks it first, applies the override, then clears it. S.session.model is also updated to the resolved value so subsequent syncTopbar() calls are consistent. Bug 2 — Raw option injected at top of list for mismatched model IDs. Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent format: hyphens, no namespace prefix) but the dropdown has 'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix). The old code did sel.value = id, found no match, then injected a new <option> at the top of the list -- creating a lowercase duplicate that didn't match any real provider group entry. Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots, lowercase) and finds the best matching existing option. No new options are ever injected for profile switching. Bug 3 — populateModelDropdown() injected raw option on cold load. Same issue: if default_model from /api/models didn't exactly match a dropdown value, an extra option was added. Fixed by using _applyModelToDropdown() which only selects existing options. New helpers in ui.js: _findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value _applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
return partial||null;
}
// Set the model picker to the best match for modelId.
// Returns the resolved value that was actually set, or null if nothing matched.
function _applyModelToDropdown(modelId, sel, preferredProviderId){
fix: model picker correctly updates on profile switch without flicker or raw injection Root cause: three interacting bugs caused the model picker to show the wrong model or flicker after a profile switch. Bug 1 — syncTopbar() fought switchToProfile(). After switchToProfile() set the picker to the profile's model, syncTopbar() was called (via renderSessionList -> loadSession, then explicitly at the end) and overwrote it with S.session.model -- the old session's model. Fix: added S._pendingProfileModel flag. switchToProfile() sets it; syncTopbar() checks it first, applies the override, then clears it. S.session.model is also updated to the resolved value so subsequent syncTopbar() calls are consistent. Bug 2 — Raw option injected at top of list for mismatched model IDs. Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent format: hyphens, no namespace prefix) but the dropdown has 'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix). The old code did sel.value = id, found no match, then injected a new <option> at the top of the list -- creating a lowercase duplicate that didn't match any real provider group entry. Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots, lowercase) and finds the best matching existing option. No new options are ever injected for profile switching. Bug 3 — populateModelDropdown() injected raw option on cold load. Same issue: if default_model from /api/models didn't exactly match a dropdown value, an extra option was added. Fixed by using _applyModelToDropdown() which only selects existing options. New helpers in ui.js: _findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value _applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
if(!modelId||!sel) return null;
const resolved=_findModelInDropdown(modelId,sel,preferredProviderId);
fix: model picker correctly updates on profile switch without flicker or raw injection Root cause: three interacting bugs caused the model picker to show the wrong model or flicker after a profile switch. Bug 1 — syncTopbar() fought switchToProfile(). After switchToProfile() set the picker to the profile's model, syncTopbar() was called (via renderSessionList -> loadSession, then explicitly at the end) and overwrote it with S.session.model -- the old session's model. Fix: added S._pendingProfileModel flag. switchToProfile() sets it; syncTopbar() checks it first, applies the override, then clears it. S.session.model is also updated to the resolved value so subsequent syncTopbar() calls are consistent. Bug 2 — Raw option injected at top of list for mismatched model IDs. Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent format: hyphens, no namespace prefix) but the dropdown has 'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix). The old code did sel.value = id, found no match, then injected a new <option> at the top of the list -- creating a lowercase duplicate that didn't match any real provider group entry. Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots, lowercase) and finds the best matching existing option. No new options are ever injected for profile switching. Bug 3 — populateModelDropdown() injected raw option on cold load. Same issue: if default_model from /api/models didn't exactly match a dropdown value, an extra option was added. Fixed by using _applyModelToDropdown() which only selects existing options. New helpers in ui.js: _findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value _applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
if(resolved){
sel.value=resolved;
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(sel.id==='modelSelect' && typeof syncModelChip==='function') syncModelChip();
fix: model picker correctly updates on profile switch without flicker or raw injection Root cause: three interacting bugs caused the model picker to show the wrong model or flicker after a profile switch. Bug 1 — syncTopbar() fought switchToProfile(). After switchToProfile() set the picker to the profile's model, syncTopbar() was called (via renderSessionList -> loadSession, then explicitly at the end) and overwrote it with S.session.model -- the old session's model. Fix: added S._pendingProfileModel flag. switchToProfile() sets it; syncTopbar() checks it first, applies the override, then clears it. S.session.model is also updated to the resolved value so subsequent syncTopbar() calls are consistent. Bug 2 — Raw option injected at top of list for mismatched model IDs. Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent format: hyphens, no namespace prefix) but the dropdown has 'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix). The old code did sel.value = id, found no match, then injected a new <option> at the top of the list -- creating a lowercase duplicate that didn't match any real provider group entry. Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots, lowercase) and finds the best matching existing option. No new options are ever injected for profile switching. Bug 3 — populateModelDropdown() injected raw option on cold load. Same issue: if default_model from /api/models didn't exactly match a dropdown value, an extra option was added. Fixed by using _applyModelToDropdown() which only selects existing options. New helpers in ui.js: _findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value _applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
return resolved;
}
return null;
}
2026-03-31 07:02:47 +00:00
async function populateModelDropdown(){
const sel=$('modelSelect');
if(!sel) return;
try{
const _modelsRes=await fetch(new URL('api/models',document.baseURI||location.href).href,{credentials:'include'});
v0.50.210: gpt-5.5, cron titles, agent cache, bfcache fix, onboarding fix, mermaid CSP, PWA auth (#1056) * feat(models): add gpt-5.5 to openai, openai-codex, copilot catalogs Adds GPT-5.5 and GPT-5.5 Mini entries to the static _PROVIDER_MODELS catalog so they appear in the model picker for the openai, openai-codex, and copilot providers. Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent * fix(models): add gpt-5.5-mini to copilot provider catalog * fix(renderer): suppress Mermaid Google Fonts CSP violation via fontFamily inherit (#1044) Mermaid's built-in 'dark' and 'default' themes inject an @import for fonts.googleapis.com/Manrope into every generated SVG. The CSP style-src only allows cdn.jsdelivr.net, so this request is blocked on every diagram render, filling the console with CSP errors. Fix: pass fontFamily:'inherit' (and fontSize:'14px') in the themeVariables block of mermaid.initialize() in renderMermaidBlocks(). This suppresses Mermaid's external font import and uses the page's existing font stack. Avoids adding fonts.googleapis.com to the CSP — no new external dependency, no font FOUT, consistent with the rest of the UI typography. 3 regression tests added in tests/test_1044_mermaid_csp_font.py. 2215/2215 tests passing. * fix(onboarding): non-standard provider/path cluster (#1029) * fix(bfcache): restore full layout on tab/session restore — rail, topbar, panels (#1045) The pageshow handler added for #822 only cleared the session search filter and re-rendered the session list. This left the rest of the layout chrome (topbar, rail icons, workspace panel, resize handles, gateway SSE) in the stale bfcache DOM state, causing a broken layout (oversized search icon, uninitialized rail) that required a hard refresh to fix. Fix: extend the pageshow handler to re-run the full set of layout sync calls that the boot IIFE runs on a fresh page load: syncTopbar() — restores model chip, title, topbar state syncWorkspacePanelState() — restores workspace panel open/closed _initResizePanels() — reattaches panel resize drag listeners startGatewaySSE() — reconnects the gateway SSE watcher (bfcache-persisted connections are dead) All four calls are typeof-guarded for safe degradation if a helper is not yet defined. The existing #822 fixes (sessionSearch clear + renderSessionListFromCache) are preserved unchanged. loadSession() is intentionally NOT re-called — it would cause message flicker; the sync calls above are sufficient to restore visual state. 7 regression tests added in tests/test_1045_bfcache_layout_restore.py. 2219/2219 tests passing. * fix(bfcache): also close open dropdowns on bfcache restore (#1045) Additional symptom noted in issue #1045: bfcache freezes the DOM including any open dropdown/popover state. The thinking-level selector (and other composer dropdowns) left open when navigating away would appear open without user interaction on tab restore. Extend the pageshow handler to call all four named close functions before the layout sync: closeModelDropdown() — composer model selector closeReasoningDropdown() — thinking/reasoning effort selector closeWsDropdown() — workspace chip dropdown closeProfileDropdown() — profile switcher dropdown All calls are typeof-guarded, matching the style of the layout sync calls already in the handler. 2 new tests (9 total in test_1045_bfcache_layout_restore.py): - pageshow closes all four named dropdowns - dropdown closes appear before layout sync calls (clean state first) 2221/2221 tests passing. * fix(bfcache): remove _initResizePanels() — bfcache preserves listeners * fix(bfcache): remove _initResizePanels from pageshow — bfcache preserves listeners; update test * fix(sessions): use cron job name as session title when available (#1032) * fix(test): add id column to messages table in cron title test fixture * fix(merge): inject cron title lookup into read_importable loop, remove stale sqlite3 block * fix(pwa): redirect to /login client-side on 401 — fixes iOS PWA auth expiry trap (#1038) When an auth session expires, the server returns a 302→/login for page requests. In a normal browser this works fine, but in an iOS PWA running in standalone mode the redirect navigates out of the PWA shell into Safari, leaving the app permanently stuck on 'Authentication required' with no recovery path. Fix: intercept 401 responses client-side before surfacing any error. - workspace.js api(): check res.status===401 first; call window.location.href='/login' and return immediately (no throw) - ui.js: add _redirectIfUnauth() helper; wire into all direct fetch() calls that bypass api() — api/models, api/models/live, api/upload All fetch paths that could receive a 401 now redirect cleanly within the PWA frame rather than opening Safari. 6 regression tests added in tests/test_1038_pwa_auth_redirect.py. 2175/2175 tests passing. * fix(pwa): preserve current URL in ?next= param on 401 redirect * fix(test): update 401-redirect assertion to accept ?next= URL format * feat(pwa): add _safeNextPath() to login.js so ?next= param is honored after re-login Addresses reviewer suggestion: the ?next= URL set on 401 redirect was ignored by the login success handler (always redirected to ./). _safeNextPath() validates and returns the ?next= param with open-redirect guards: rejects non-path-absolute inputs, // protocol-relative URLs, backslash variants, and control characters. 4 new regression tests added. * Implement session agent cache for AIAgent reuse Added session agent cache to reuse AIAgent across messages. * Implement agent caching for session management * Implement session agent eviction on session deletion Added session agent eviction to prevent turn count leakage in recycled sessions. * docs: v0.50.210 release notes — 7 PRs, 2239 tests (+27) * docs(changelog): drop stale [Unreleased] entries duplicated by v0.50.210 Three entries in the [Unreleased] section are duplicates of items now listed under v0.50.210: - Mermaid CSP font fix (#1044) → v0.50.210 / Mermaid Google Fonts CSP - bfcache layout restore (#1045) → v0.50.210 / bfcache layout and dropdown restore - iOS PWA auth redirect (#1038) → v0.50.210 / Login redirects back to original URL The original drafts landed in [Unreleased] when individual PRs (#1047, #1048, #1043) were approved; the v0.50.210 release-notes commit then added the same items under the version section without removing the [Unreleased] copies. Drop the duplicates so users reading the CHANGELOG don't see the same fix listed twice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent Co-authored-by: Pix (Hermes) <aliceisjustplaying@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: qxxaa <mrhanoi@outlook.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:47:44 +00:00
if(_redirectIfUnauth(_modelsRes)) return;
const data=await _modelsRes.json();
2026-03-31 07:02:47 +00:00
if(!data.groups||!data.groups.length) return; // keep HTML defaults
fix: warn on provider/model mismatch, surface auth errors (#266) * fix: warn on provider/model mismatch, surface auth errors (#266) Fixes #266 — WebUI silently ignores provider/model selection mismatch. The problem: selecting an OpenRouter (or Anthropic/OpenAI) model while Hermes is configured for a different provider (e.g. local Ollama) sends the request to the wrong endpoint, which returns a 401 Unauthorized error with no UI indication of why. Three-layer fix: 1. api/streaming.py — detect 401/auth errors explicitly Added is_auth_error detection covering '401', 'AuthenticationError', 'authentication', 'unauthorized', 'invalid api key', and the specific Ollama error string 'no cookie auth credentials'. Auth errors emit apperror with type='auth_mismatch' and a hint pointing to 'hermes model'. 2. static/ui.js — expose active_provider and warn on selection - populateModelDropdown() stores data.active_provider from /api/models as window._activeProvider (the field was already in the response but the frontend never used it) - New _checkProviderMismatch(modelId) helper: compares the selected model's slash-prefix (e.g. 'openai/' from 'openai/gpt-4o') against the active provider. Skips the check for 'openrouter' and 'custom' to avoid false positives on configs that legitimately route any model. 3. static/boot.js — warn on model dropdown change modelSelect.onchange calls _checkProviderMismatch() and shows a toast when the selected model looks incompatible with the configured provider. 4. static/messages.js — distinct UI label for auth errors apperror handler now distinguishes type='auth_mismatch' and shows 'Provider mismatch' as the error label instead of 'Error'. 5. static/i18n.js — provider_mismatch_warning and provider_mismatch_label keys added to all 5 locales (en, es, de, zh-Hans, zh-Hant). Tests: 21 new tests in tests/test_provider_mismatch.py covering all five change areas. 679/679 total pass (658 baseline + 21 new). * fix: t() call args spread + use i18n label for auth mismatch 1. ui.js: _checkProviderMismatch passed [modelId, ap] as a single array arg to t(). Since t(key, ...args) spreads, the function received the array as m and undefined as p. Fixed to pass as separate args: t('provider_mismatch_warning', modelId, ap). 2. messages.js: 'Provider mismatch' label was hardcoded instead of using t('provider_mismatch_label'). Now uses the i18n key with fallback for when t() isn't available. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 04:25:18 +00:00
// Store active provider globally so the send path can warn on mismatch
window._activeProvider=data.active_provider||null;
fix(settings): show live models in default model picker and apply to new chats (#872) (#900) * fix(settings): show live models in default model picker and apply to new chats (#872) Two related bugs: 1. Settings > Preferences > Default Model dropdown only showed static models from /api/models — live-fetched models (e.g. @nous:anthropic/claude-opus-4.7) were missing. Now calls _fetchLiveModels() on the settings picker too. 2. New chats ignored the saved default model preference — they always used the chat-header dropdown value (which reflects the previous session's model). Now newSession() uses the saved default_model and syncs the dropdown. Extracted _addLiveModelsToSelect() from _fetchLiveModels() so cached live models can be applied to any <select> element (chat-header or settings picker). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(tests): update live-model prefix tests for _addLiveModelsToSelect extraction The tests searched for og.dataset.provider, _isPortalFetch, and openrouter exclusion patterns inside _fetchLiveModels(). These were extracted into _addLiveModelsToSelect() as part of the #872 fix. Updated regex targets to check _addLiveModelsToSelect first, falling back to _fetchLiveModels. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: add multi-tab note on window._defaultModel Clarifies that window._defaultModel is per-page-load and not synced across browser tabs, following maintainer feedback on #889. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: CHANGELOG for v0.50.170 * chore: trigger PR refresh after rebase --------- Co-authored-by: fr33m1nd <bergeouss@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-23 16:58:15 +00:00
// Store default model so newSession() can apply it (#872).
// Per-page-load — not synced across browser tabs.
window._defaultModel=data.default_model||null;
window._configuredModelBadges=data.configured_model_badges||{};
2026-03-31 07:02:47 +00:00
// Clear existing options
sel.innerHTML='';
_dynamicModelLabels={};
for(const g of data.groups){
const og=document.createElement('optgroup');
og.label=g.provider;
if(g.provider_id) og.dataset.provider=g.provider_id;
2026-03-31 07:02:47 +00:00
for(const m of g.models){
const opt=document.createElement('option');
opt.value=m.id;
opt.textContent=m.label;
og.appendChild(opt);
_dynamicModelLabels[m.id]=m.label;
}
fix(picker): Nous Portal featured-set cap + endpoint symmetry (closes #1567) Two related dropdown bugs in one PR — same root shape (model-picker endpoints disagreeing about which Nous Portal models exist) plus the preemptive UX guard against the picker becoming unusable on large-tier Nous accounts. #1567 — Endpoint disagreement ============================= Reporter (Deor, Discord, May 03 2026) saw Settings → Providers card showing "Nous Portal — 396 models · OAuth" while the in-conversation picker dropdown listed only the four hardcoded curated entries. Two structural causes: 1. ``api/providers.py:get_providers`` iterates ALL OAuth providers regardless of authentication state and unconditionally live-fetches the catalog. 2. ``api/config.py:_build_available_models_uncached`` only iterates providers in ``detected_providers``, gated on ``hermes_cli.models.list_available_providers().authenticated``. That flag can disagree with ``get_auth_status(<id>).logged_in`` on some hermes_cli versions. When the disagreement happens for Nous, the picker silently falls through to the curated 4-entry static list while the providers card keeps showing the live catalog — exactly the asymmetry users report. Plus: the Nous live-fetch branch in `_build_available_models_uncached` fell back to the same curated 4-entry list when `provider_model_ids` returned an empty list (transient failure / OAuth refresh in flight), which doubles down on the disagreement instead of healing it. UX cap (the design concern Nathan flagged on triage) ==================================================== Even with the disagreement fixed, dumping a 397-model catalog into a flat dropdown is unusable. We trim the visible picker to a curated ~15-entry featured set when the catalog exceeds 25 models, and surface the rest under a new ``extra_models`` field so: - ``/model`` slash autocomplete (commands.js) covers the full catalog - ``_dynamicModelLabels`` (ui.js) hydrates from both lists, so a model selected from outside the featured slice still gets a proper label - The optgroup label gets ``" (15 of 397)"`` appended so the user understands the dropdown is intentionally trimmed, not broken - The providers card surfaces ``models_total`` separately so the header still reads "397 models · OAuth" - A small "+N more" disclosure pill appears at the end of the rendered pill list (only fires for non-OAuth providers — OAuth cards never render pills) with a tooltip pointing at the slash command Featured selection rules ------------------------ Deterministic; same algorithm runs in both `/api/models` and `/api/models/live` so background enrichment doesn't undo the trim: 1. Always include the user's currently-selected model (sticky — no orphan IDs in the dropdown after a refresh) 2. Always include every entry from the curated static ``_PROVIDER_MODELS["nous"]`` list whose id maps onto a live id 3. Top up to 15 by walking ``_NOUS_VENDOR_PRIORITY`` round-robin (one model per vendor each pass) so no vendor monopolises the slots Changes by file =============== api/config.py - New `_format_nous_label` neighbour: `_NOUS_FEATURED_THRESHOLD = 25`, `_NOUS_FEATURED_TARGET = 15`, `_NOUS_VENDOR_PRIORITY` tuple, `_build_nous_featured_set()` helper (~80 LOC) - `_build_available_models_uncached` Nous branch: - Apply featured-set cap with sticky-selection signal - Return `extra_models` alongside `models` for the catalog tail - Decorate optgroup label with truncation count - Drop stale-4 fallback when authenticated but live-fetch empty (omit the group entirely; truth lives in the providers card and the next cache rebuild will heal it) - Keep stale-4 fallback when hermes_cli is unavailable (test envs, package mismatches) — that's a different failure mode - Detection symmetry: explicit `get_auth_status("nous").logged_in` check after the existing `list_available_providers()` loop, so the picker matches the providers card on hermes_cli versions where the two signals disagree api/providers.py:get_providers - Apply same featured-set cap so card body doesn't render 397 pills - Add `models_total` field reporting full catalog size (used by frontend for the "N models · OAuth" header text) api/routes.py:_handle_live_models - Apply same featured-set cap for `/api/models/live` so background enrichment via `_fetchLiveModels()` doesn't undo the dropdown trim - Use sticky-selection from `cfg["model"]["model"]` matching the main endpoint's logic static/ui.js:populateModelDropdown - Hydrate `_dynamicModelLabels` from `g.extra_models` so a selection outside the visible dropdown still renders with its proper label static/commands.js:_loadSlashModelSubArgs - Iterate `group.extra_models` so `/model` autocomplete covers the full catalog (not just the trimmed featured slice) static/panels.js:_buildProviderCard - Header count uses `p.models_total` (full catalog size) instead of `p.models.length` (trimmed slice) - Render trailing "+N more" disclosure pill when `models.length < models_total` with a tooltip pointing at the slash command static/style.css - New `.provider-card-model-tag-more` rule (italic, dashed border, cursor:help, no select) — visually distinct from real model pills Tests ===== `tests/test_issue1567_nous_picker_capacity_and_symmetry.py` (20 tests): - TestBuildNousFeaturedSet (8): unit tests on the helper — small-catalog no-op, large-catalog cap to target, disjoint+complete invariants, priority-vendor round-robin guarantee, sticky selection with and without `@nous:` prefix, curated-flagship preservation, empty-catalog handling, determinism - TestApiModelsLargeCatalog (2): /api/models cap behavior end-to-end on a synthetic 397-model catalog vs a 20-model catalog - TestNousDetectionSymmetry (2): picker includes Nous when `get_auth_status` agrees but `list_available_providers` disagrees; picker omits Nous when both disagree - TestNousLiveFetchEmpty (2): authenticated + empty-fetch omits group; hermes_cli unavailable still falls back to static-4 - TestProvidersCardPickerSymmetry (1): both endpoints agree on exactly the same featured-set IDs + total catalog count - TestFrontendExtrasContract (4): static-source assertions pinning the JS contract for `extra_models`, `models_total`, and the "+N more" disclosure Verified live on port 8789 (30-model catalog): - /api/models Nous group: provider="Nous Portal (15 of 30)", 15 models, 15 extra_models - /api/models/live?provider=nous: 15 entries (matches main path) - /api/providers Nous card: models_total=30, models=15 - Browser dropdown after backfill: 15 options, 30 entries in _dynamicModelLabels - Sticky selection: Claude Opus 4.7 (the active model) in the featured slice as expected 4073 pytest passed (was 4053 → 4073, +20 from this PR). 3 CI test runs (3.11/3.12/3.13) green. QA harness 11/11 passed. Reporter: Deor (Discord #report-bugs, May 03 2026 14:15 PT) Relayed by: AvidFuturist
2026-05-03 21:44:22 +00:00
// Hydrate the label map from extra_models too (the catalog tail that
// doesn't render as <option> entries when the picker is capped — see
// _build_nous_featured_set in api/config.py for the rationale). This
// keeps a model selected from the slash-command autocomplete or a
// persisted-localStorage value renderable with its proper label
// instead of falling back to the bare ID. #1567.
if(Array.isArray(g.extra_models)){
for(const m of g.extra_models){
if(m && m.id) _dynamicModelLabels[m.id]=m.label||m.id;
}
}
2026-03-31 07:02:47 +00:00
sel.appendChild(og);
}
// Set default model from server if no localStorage preference
if(data.default_model && !(typeof _readPersistedModelState==='function'&&_readPersistedModelState()) && !localStorage.getItem('hermes-webui-model')){
_applyModelToDropdown(data.default_model, sel, data.active_provider||null);
2026-03-31 07:02:47 +00:00
}
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(typeof syncModelChip==='function') syncModelChip();
fix: silent agent errors, stale model list, live model fetching (#377) * fix: silent errors, stale models, live model fetching (#373, #374, #375) - api/streaming.py: detect empty agent response (_assistant_added check), emit apperror(type='no_response' or 'auth_mismatch') instead of silent done - api/streaming.py: add _token_sent flag so guard works for streaming agents - static/messages.js: done handler belt-and-suspenders guard for zero replies - static/messages.js: apperror handler labels 'no_response' type distinctly - api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and _PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini) - api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models from provider API with B310 scheme check + SSRF guard - static/ui.js: _fetchLiveModels() background fetch after static list loads, appends new models to dropdown, caches per session, skips unsupported providers Other: - tests/test_issues_373_374_375.py: 25 new structural tests - tests/test_regressions.py: extend done-handler window 1500->2500 chars - CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922) * fix: SSRF hostname bypass + auth detection operator precedence 1. routes.py: SSRF guard used substring matching (any(k in hostname)) which allows bypass via hostnames like evil-ollama.attacker.com. Changed to exact hostname matching against a fixed set of known local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1). 2. streaming.py: _is_auth detection had a Python operator precedence bug on the ternary expression. The line: 'AuthenticationError' in type(...).__name__ if _last_err else False parsed as the ternary absorbing the rest of the or-chain when _last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: fix v0.50.20 CHANGELOG version number and test count (949 tests) --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
// Kick off a background live-model fetch for the active provider.
// This runs after the static list is already shown (no blocking flicker).
if(data.active_provider) _fetchLiveModels(data.active_provider, sel);
2026-03-31 07:02:47 +00:00
}catch(e){
// API unavailable -- keep the hardcoded HTML options as fallback
console.warn('Failed to load models from server:',e.message);
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(typeof syncModelChip==='function') syncModelChip();
2026-03-31 07:02:47 +00:00
}
}
fix: silent agent errors, stale model list, live model fetching (#377) * fix: silent errors, stale models, live model fetching (#373, #374, #375) - api/streaming.py: detect empty agent response (_assistant_added check), emit apperror(type='no_response' or 'auth_mismatch') instead of silent done - api/streaming.py: add _token_sent flag so guard works for streaming agents - static/messages.js: done handler belt-and-suspenders guard for zero replies - static/messages.js: apperror handler labels 'no_response' type distinctly - api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and _PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini) - api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models from provider API with B310 scheme check + SSRF guard - static/ui.js: _fetchLiveModels() background fetch after static list loads, appends new models to dropdown, caches per session, skips unsupported providers Other: - tests/test_issues_373_374_375.py: 25 new structural tests - tests/test_regressions.py: extend done-handler window 1500->2500 chars - CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922) * fix: SSRF hostname bypass + auth detection operator precedence 1. routes.py: SSRF guard used substring matching (any(k in hostname)) which allows bypass via hostnames like evil-ollama.attacker.com. Changed to exact hostname matching against a fixed set of known local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1). 2. streaming.py: _is_auth detection had a Python operator precedence bug on the ternary expression. The line: 'AuthenticationError' in type(...).__name__ if _last_err else False parsed as the ternary absorbing the rest of the or-chain when _last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: fix v0.50.20 CHANGELOG version number and test count (949 tests) --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
// Cache so we don't re-fetch on every page load
const _liveModelCache={};
// Tracks providers for which a live-model fetch is in flight.
// Used by syncTopbar() to defer model corrections until the fetch completes,
// preventing premature fallback to the first static model (#1169).
const _liveModelFetchPending=new Set();
fix: silent agent errors, stale model list, live model fetching (#377) * fix: silent errors, stale models, live model fetching (#373, #374, #375) - api/streaming.py: detect empty agent response (_assistant_added check), emit apperror(type='no_response' or 'auth_mismatch') instead of silent done - api/streaming.py: add _token_sent flag so guard works for streaming agents - static/messages.js: done handler belt-and-suspenders guard for zero replies - static/messages.js: apperror handler labels 'no_response' type distinctly - api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and _PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini) - api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models from provider API with B310 scheme check + SSRF guard - static/ui.js: _fetchLiveModels() background fetch after static list loads, appends new models to dropdown, caches per session, skips unsupported providers Other: - tests/test_issues_373_374_375.py: 25 new structural tests - tests/test_regressions.py: extend done-handler window 1500->2500 chars - CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922) * fix: SSRF hostname bypass + auth detection operator precedence 1. routes.py: SSRF guard used substring matching (any(k in hostname)) which allows bypass via hostnames like evil-ollama.attacker.com. Changed to exact hostname matching against a fixed set of known local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1). 2. streaming.py: _is_auth detection had a Python operator precedence bug on the ternary expression. The line: 'AuthenticationError' in type(...).__name__ if _last_err else False parsed as the ternary absorbing the rest of the or-chain when _last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: fix v0.50.20 CHANGELOG version number and test count (949 tests) --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
fix(settings): show live models in default model picker and apply to new chats (#872) (#900) * fix(settings): show live models in default model picker and apply to new chats (#872) Two related bugs: 1. Settings > Preferences > Default Model dropdown only showed static models from /api/models — live-fetched models (e.g. @nous:anthropic/claude-opus-4.7) were missing. Now calls _fetchLiveModels() on the settings picker too. 2. New chats ignored the saved default model preference — they always used the chat-header dropdown value (which reflects the previous session's model). Now newSession() uses the saved default_model and syncs the dropdown. Extracted _addLiveModelsToSelect() from _fetchLiveModels() so cached live models can be applied to any <select> element (chat-header or settings picker). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(tests): update live-model prefix tests for _addLiveModelsToSelect extraction The tests searched for og.dataset.provider, _isPortalFetch, and openrouter exclusion patterns inside _fetchLiveModels(). These were extracted into _addLiveModelsToSelect() as part of the #872 fix. Updated regex targets to check _addLiveModelsToSelect first, falling back to _fetchLiveModels. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: add multi-tab note on window._defaultModel Clarifies that window._defaultModel is per-page-load and not synced across browser tabs, following maintainer feedback on #889. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: CHANGELOG for v0.50.170 * chore: trigger PR refresh after rebase --------- Co-authored-by: fr33m1nd <bergeouss@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-23 16:58:15 +00:00
function _addLiveModelsToSelect(provider, models, sel){
if(!provider||!models||!models.length||!sel) return 0;
const currentVal=sel.value;
let providerGroup=null;
for(const og of sel.querySelectorAll('optgroup')){
if(og.dataset.provider&&og.dataset.provider===provider){
providerGroup=og; break;
}
if(og.label&&og.label.toLowerCase().includes(provider.toLowerCase())){
providerGroup=og; break;
}
}
if(!providerGroup){
providerGroup=document.createElement('optgroup');
providerGroup.label=provider.charAt(0).toUpperCase()+provider.slice(1)+' (live)';
sel.appendChild(providerGroup);
}
const existingIds=new Set([...sel.options].map(o=>o.value));
// Normalized dedup: strip @provider: prefix and unify separators so
// 'minimax/minimax-m2.7' matches '@nous:minimax/minimax-m2.7' (#907).
// Strip ONLY the first colon — Ollama tag IDs are multi-colon
// (e.g. '@ollama-cloud:qwen3-vl:235b-instruct') and split(':',2) would
// truncate the tag suffix in JS (the limit arg discards extras, unlike Python).
const _normId=id=>{
let s=String(id||'');
if(s.startsWith('@')&&s.includes(':')) s=s.substring(s.indexOf(':')+1); // strip only @provider:
s=s.split('/').pop(); // strip namespace prefix
return s.replace(/-/g,'.').toLowerCase();
};
const existingNorm=new Set([...sel.options].map(o=>_normId(o.value)));
fix(settings): show live models in default model picker and apply to new chats (#872) (#900) * fix(settings): show live models in default model picker and apply to new chats (#872) Two related bugs: 1. Settings > Preferences > Default Model dropdown only showed static models from /api/models — live-fetched models (e.g. @nous:anthropic/claude-opus-4.7) were missing. Now calls _fetchLiveModels() on the settings picker too. 2. New chats ignored the saved default model preference — they always used the chat-header dropdown value (which reflects the previous session's model). Now newSession() uses the saved default_model and syncs the dropdown. Extracted _addLiveModelsToSelect() from _fetchLiveModels() so cached live models can be applied to any <select> element (chat-header or settings picker). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(tests): update live-model prefix tests for _addLiveModelsToSelect extraction The tests searched for og.dataset.provider, _isPortalFetch, and openrouter exclusion patterns inside _fetchLiveModels(). These were extracted into _addLiveModelsToSelect() as part of the #872 fix. Updated regex targets to check _addLiveModelsToSelect first, falling back to _fetchLiveModels. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: add multi-tab note on window._defaultModel Clarifies that window._defaultModel is per-page-load and not synced across browser tabs, following maintainer feedback on #889. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: CHANGELOG for v0.50.170 * chore: trigger PR refresh after rebase --------- Co-authored-by: fr33m1nd <bergeouss@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-23 16:58:15 +00:00
let added=0;
const _ap=(window._activeProvider||'').toLowerCase();
const _isPortalFetch=_ap && _ap!=='openrouter' && _ap!=='custom' && _ap!=='openai-codex' && provider===_ap;
fix(settings): show live models in default model picker and apply to new chats (#872) (#900) * fix(settings): show live models in default model picker and apply to new chats (#872) Two related bugs: 1. Settings > Preferences > Default Model dropdown only showed static models from /api/models — live-fetched models (e.g. @nous:anthropic/claude-opus-4.7) were missing. Now calls _fetchLiveModels() on the settings picker too. 2. New chats ignored the saved default model preference — they always used the chat-header dropdown value (which reflects the previous session's model). Now newSession() uses the saved default_model and syncs the dropdown. Extracted _addLiveModelsToSelect() from _fetchLiveModels() so cached live models can be applied to any <select> element (chat-header or settings picker). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(tests): update live-model prefix tests for _addLiveModelsToSelect extraction The tests searched for og.dataset.provider, _isPortalFetch, and openrouter exclusion patterns inside _fetchLiveModels(). These were extracted into _addLiveModelsToSelect() as part of the #872 fix. Updated regex targets to check _addLiveModelsToSelect first, falling back to _fetchLiveModels. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: add multi-tab note on window._defaultModel Clarifies that window._defaultModel is per-page-load and not synced across browser tabs, following maintainer feedback on #889. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: CHANGELOG for v0.50.170 * chore: trigger PR refresh after rebase --------- Co-authored-by: fr33m1nd <bergeouss@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-23 16:58:15 +00:00
for(const m of models){
let mid=m.id;
if(_isPortalFetch && !mid.startsWith('@')){
mid=`@${provider}:${mid}`;
}
if(existingIds.has(mid)) continue;
if(existingNorm.has(_normId(mid))) continue; // dedup cross-prefix duplicates (#907)
fix(settings): show live models in default model picker and apply to new chats (#872) (#900) * fix(settings): show live models in default model picker and apply to new chats (#872) Two related bugs: 1. Settings > Preferences > Default Model dropdown only showed static models from /api/models — live-fetched models (e.g. @nous:anthropic/claude-opus-4.7) were missing. Now calls _fetchLiveModels() on the settings picker too. 2. New chats ignored the saved default model preference — they always used the chat-header dropdown value (which reflects the previous session's model). Now newSession() uses the saved default_model and syncs the dropdown. Extracted _addLiveModelsToSelect() from _fetchLiveModels() so cached live models can be applied to any <select> element (chat-header or settings picker). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(tests): update live-model prefix tests for _addLiveModelsToSelect extraction The tests searched for og.dataset.provider, _isPortalFetch, and openrouter exclusion patterns inside _fetchLiveModels(). These were extracted into _addLiveModelsToSelect() as part of the #872 fix. Updated regex targets to check _addLiveModelsToSelect first, falling back to _fetchLiveModels. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: add multi-tab note on window._defaultModel Clarifies that window._defaultModel is per-page-load and not synced across browser tabs, following maintainer feedback on #889. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: CHANGELOG for v0.50.170 * chore: trigger PR refresh after rebase --------- Co-authored-by: fr33m1nd <bergeouss@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-23 16:58:15 +00:00
const opt=document.createElement('option');
opt.value=mid;
opt.textContent=m.label||m.id;
opt.title='Live model — fetched from provider';
providerGroup.appendChild(opt);
_dynamicModelLabels[mid]=m.label||m.id;
added++;
}
const currentProvider=(S.session&&S.session.model_provider)||null;
if(added>0 && currentVal) _applyModelToDropdown(currentVal, sel, currentProvider);
// After live models are added, re-apply the session's model in case it was
// absent from the static list and syncTopbar() fired before the live fetch
// completed (#1169). This ensures the session model wins over any premature
// fallback that may have set sel.value to the first available option.
if(S.session && S.session.model && sel.id==='modelSelect'){
const reapplied=_applyModelToDropdown(S.session.model, sel, S.session.model_provider||null);
if(reapplied && typeof syncModelChip==='function') syncModelChip();
}
fix(settings): show live models in default model picker and apply to new chats (#872) (#900) * fix(settings): show live models in default model picker and apply to new chats (#872) Two related bugs: 1. Settings > Preferences > Default Model dropdown only showed static models from /api/models — live-fetched models (e.g. @nous:anthropic/claude-opus-4.7) were missing. Now calls _fetchLiveModels() on the settings picker too. 2. New chats ignored the saved default model preference — they always used the chat-header dropdown value (which reflects the previous session's model). Now newSession() uses the saved default_model and syncs the dropdown. Extracted _addLiveModelsToSelect() from _fetchLiveModels() so cached live models can be applied to any <select> element (chat-header or settings picker). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(tests): update live-model prefix tests for _addLiveModelsToSelect extraction The tests searched for og.dataset.provider, _isPortalFetch, and openrouter exclusion patterns inside _fetchLiveModels(). These were extracted into _addLiveModelsToSelect() as part of the #872 fix. Updated regex targets to check _addLiveModelsToSelect first, falling back to _fetchLiveModels. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: add multi-tab note on window._defaultModel Clarifies that window._defaultModel is per-page-load and not synced across browser tabs, following maintainer feedback on #889. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: CHANGELOG for v0.50.170 * chore: trigger PR refresh after rebase --------- Co-authored-by: fr33m1nd <bergeouss@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-23 16:58:15 +00:00
return added;
}
fix: silent agent errors, stale model list, live model fetching (#377) * fix: silent errors, stale models, live model fetching (#373, #374, #375) - api/streaming.py: detect empty agent response (_assistant_added check), emit apperror(type='no_response' or 'auth_mismatch') instead of silent done - api/streaming.py: add _token_sent flag so guard works for streaming agents - static/messages.js: done handler belt-and-suspenders guard for zero replies - static/messages.js: apperror handler labels 'no_response' type distinctly - api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and _PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini) - api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models from provider API with B310 scheme check + SSRF guard - static/ui.js: _fetchLiveModels() background fetch after static list loads, appends new models to dropdown, caches per session, skips unsupported providers Other: - tests/test_issues_373_374_375.py: 25 new structural tests - tests/test_regressions.py: extend done-handler window 1500->2500 chars - CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922) * fix: SSRF hostname bypass + auth detection operator precedence 1. routes.py: SSRF guard used substring matching (any(k in hostname)) which allows bypass via hostnames like evil-ollama.attacker.com. Changed to exact hostname matching against a fixed set of known local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1). 2. streaming.py: _is_auth detection had a Python operator precedence bug on the ternary expression. The line: 'AuthenticationError' in type(...).__name__ if _last_err else False parsed as the ternary absorbing the rest of the or-chain when _last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: fix v0.50.20 CHANGELOG version number and test count (949 tests) --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
async function _fetchLiveModels(provider, sel){
if(!provider||!sel) return;
fix(settings): show live models in default model picker and apply to new chats (#872) (#900) * fix(settings): show live models in default model picker and apply to new chats (#872) Two related bugs: 1. Settings > Preferences > Default Model dropdown only showed static models from /api/models — live-fetched models (e.g. @nous:anthropic/claude-opus-4.7) were missing. Now calls _fetchLiveModels() on the settings picker too. 2. New chats ignored the saved default model preference — they always used the chat-header dropdown value (which reflects the previous session's model). Now newSession() uses the saved default_model and syncs the dropdown. Extracted _addLiveModelsToSelect() from _fetchLiveModels() so cached live models can be applied to any <select> element (chat-header or settings picker). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(tests): update live-model prefix tests for _addLiveModelsToSelect extraction The tests searched for og.dataset.provider, _isPortalFetch, and openrouter exclusion patterns inside _fetchLiveModels(). These were extracted into _addLiveModelsToSelect() as part of the #872 fix. Updated regex targets to check _addLiveModelsToSelect first, falling back to _fetchLiveModels. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: add multi-tab note on window._defaultModel Clarifies that window._defaultModel is per-page-load and not synced across browser tabs, following maintainer feedback on #889. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: CHANGELOG for v0.50.170 * chore: trigger PR refresh after rebase --------- Co-authored-by: fr33m1nd <bergeouss@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-23 16:58:15 +00:00
// Already fetched — apply cached models to this select element (#872)
if(_liveModelCache[provider]){
const added=_addLiveModelsToSelect(provider,_liveModelCache[provider],sel);
if(added>0 && typeof syncModelChip==='function') syncModelChip();
return;
}
_liveModelFetchPending.add(provider);
fix: silent agent errors, stale model list, live model fetching (#377) * fix: silent errors, stale models, live model fetching (#373, #374, #375) - api/streaming.py: detect empty agent response (_assistant_added check), emit apperror(type='no_response' or 'auth_mismatch') instead of silent done - api/streaming.py: add _token_sent flag so guard works for streaming agents - static/messages.js: done handler belt-and-suspenders guard for zero replies - static/messages.js: apperror handler labels 'no_response' type distinctly - api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and _PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini) - api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models from provider API with B310 scheme check + SSRF guard - static/ui.js: _fetchLiveModels() background fetch after static list loads, appends new models to dropdown, caches per session, skips unsupported providers Other: - tests/test_issues_373_374_375.py: 25 new structural tests - tests/test_regressions.py: extend done-handler window 1500->2500 chars - CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922) * fix: SSRF hostname bypass + auth detection operator precedence 1. routes.py: SSRF guard used substring matching (any(k in hostname)) which allows bypass via hostnames like evil-ollama.attacker.com. Changed to exact hostname matching against a fixed set of known local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1). 2. streaming.py: _is_auth detection had a Python operator precedence bug on the ternary expression. The line: 'AuthenticationError' in type(...).__name__ if _last_err else False parsed as the ternary absorbing the rest of the or-chain when _last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: fix v0.50.20 CHANGELOG version number and test count (949 tests) --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
try{
const url=new URL('api/models/live',document.baseURI||location.href);
fix: silent agent errors, stale model list, live model fetching (#377) * fix: silent errors, stale models, live model fetching (#373, #374, #375) - api/streaming.py: detect empty agent response (_assistant_added check), emit apperror(type='no_response' or 'auth_mismatch') instead of silent done - api/streaming.py: add _token_sent flag so guard works for streaming agents - static/messages.js: done handler belt-and-suspenders guard for zero replies - static/messages.js: apperror handler labels 'no_response' type distinctly - api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and _PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini) - api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models from provider API with B310 scheme check + SSRF guard - static/ui.js: _fetchLiveModels() background fetch after static list loads, appends new models to dropdown, caches per session, skips unsupported providers Other: - tests/test_issues_373_374_375.py: 25 new structural tests - tests/test_regressions.py: extend done-handler window 1500->2500 chars - CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922) * fix: SSRF hostname bypass + auth detection operator precedence 1. routes.py: SSRF guard used substring matching (any(k in hostname)) which allows bypass via hostnames like evil-ollama.attacker.com. Changed to exact hostname matching against a fixed set of known local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1). 2. streaming.py: _is_auth detection had a Python operator precedence bug on the ternary expression. The line: 'AuthenticationError' in type(...).__name__ if _last_err else False parsed as the ternary absorbing the rest of the or-chain when _last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: fix v0.50.20 CHANGELOG version number and test count (949 tests) --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
url.searchParams.set('provider',provider);
v0.50.210: gpt-5.5, cron titles, agent cache, bfcache fix, onboarding fix, mermaid CSP, PWA auth (#1056) * feat(models): add gpt-5.5 to openai, openai-codex, copilot catalogs Adds GPT-5.5 and GPT-5.5 Mini entries to the static _PROVIDER_MODELS catalog so they appear in the model picker for the openai, openai-codex, and copilot providers. Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent * fix(models): add gpt-5.5-mini to copilot provider catalog * fix(renderer): suppress Mermaid Google Fonts CSP violation via fontFamily inherit (#1044) Mermaid's built-in 'dark' and 'default' themes inject an @import for fonts.googleapis.com/Manrope into every generated SVG. The CSP style-src only allows cdn.jsdelivr.net, so this request is blocked on every diagram render, filling the console with CSP errors. Fix: pass fontFamily:'inherit' (and fontSize:'14px') in the themeVariables block of mermaid.initialize() in renderMermaidBlocks(). This suppresses Mermaid's external font import and uses the page's existing font stack. Avoids adding fonts.googleapis.com to the CSP — no new external dependency, no font FOUT, consistent with the rest of the UI typography. 3 regression tests added in tests/test_1044_mermaid_csp_font.py. 2215/2215 tests passing. * fix(onboarding): non-standard provider/path cluster (#1029) * fix(bfcache): restore full layout on tab/session restore — rail, topbar, panels (#1045) The pageshow handler added for #822 only cleared the session search filter and re-rendered the session list. This left the rest of the layout chrome (topbar, rail icons, workspace panel, resize handles, gateway SSE) in the stale bfcache DOM state, causing a broken layout (oversized search icon, uninitialized rail) that required a hard refresh to fix. Fix: extend the pageshow handler to re-run the full set of layout sync calls that the boot IIFE runs on a fresh page load: syncTopbar() — restores model chip, title, topbar state syncWorkspacePanelState() — restores workspace panel open/closed _initResizePanels() — reattaches panel resize drag listeners startGatewaySSE() — reconnects the gateway SSE watcher (bfcache-persisted connections are dead) All four calls are typeof-guarded for safe degradation if a helper is not yet defined. The existing #822 fixes (sessionSearch clear + renderSessionListFromCache) are preserved unchanged. loadSession() is intentionally NOT re-called — it would cause message flicker; the sync calls above are sufficient to restore visual state. 7 regression tests added in tests/test_1045_bfcache_layout_restore.py. 2219/2219 tests passing. * fix(bfcache): also close open dropdowns on bfcache restore (#1045) Additional symptom noted in issue #1045: bfcache freezes the DOM including any open dropdown/popover state. The thinking-level selector (and other composer dropdowns) left open when navigating away would appear open without user interaction on tab restore. Extend the pageshow handler to call all four named close functions before the layout sync: closeModelDropdown() — composer model selector closeReasoningDropdown() — thinking/reasoning effort selector closeWsDropdown() — workspace chip dropdown closeProfileDropdown() — profile switcher dropdown All calls are typeof-guarded, matching the style of the layout sync calls already in the handler. 2 new tests (9 total in test_1045_bfcache_layout_restore.py): - pageshow closes all four named dropdowns - dropdown closes appear before layout sync calls (clean state first) 2221/2221 tests passing. * fix(bfcache): remove _initResizePanels() — bfcache preserves listeners * fix(bfcache): remove _initResizePanels from pageshow — bfcache preserves listeners; update test * fix(sessions): use cron job name as session title when available (#1032) * fix(test): add id column to messages table in cron title test fixture * fix(merge): inject cron title lookup into read_importable loop, remove stale sqlite3 block * fix(pwa): redirect to /login client-side on 401 — fixes iOS PWA auth expiry trap (#1038) When an auth session expires, the server returns a 302→/login for page requests. In a normal browser this works fine, but in an iOS PWA running in standalone mode the redirect navigates out of the PWA shell into Safari, leaving the app permanently stuck on 'Authentication required' with no recovery path. Fix: intercept 401 responses client-side before surfacing any error. - workspace.js api(): check res.status===401 first; call window.location.href='/login' and return immediately (no throw) - ui.js: add _redirectIfUnauth() helper; wire into all direct fetch() calls that bypass api() — api/models, api/models/live, api/upload All fetch paths that could receive a 401 now redirect cleanly within the PWA frame rather than opening Safari. 6 regression tests added in tests/test_1038_pwa_auth_redirect.py. 2175/2175 tests passing. * fix(pwa): preserve current URL in ?next= param on 401 redirect * fix(test): update 401-redirect assertion to accept ?next= URL format * feat(pwa): add _safeNextPath() to login.js so ?next= param is honored after re-login Addresses reviewer suggestion: the ?next= URL set on 401 redirect was ignored by the login success handler (always redirected to ./). _safeNextPath() validates and returns the ?next= param with open-redirect guards: rejects non-path-absolute inputs, // protocol-relative URLs, backslash variants, and control characters. 4 new regression tests added. * Implement session agent cache for AIAgent reuse Added session agent cache to reuse AIAgent across messages. * Implement agent caching for session management * Implement session agent eviction on session deletion Added session agent eviction to prevent turn count leakage in recycled sessions. * docs: v0.50.210 release notes — 7 PRs, 2239 tests (+27) * docs(changelog): drop stale [Unreleased] entries duplicated by v0.50.210 Three entries in the [Unreleased] section are duplicates of items now listed under v0.50.210: - Mermaid CSP font fix (#1044) → v0.50.210 / Mermaid Google Fonts CSP - bfcache layout restore (#1045) → v0.50.210 / bfcache layout and dropdown restore - iOS PWA auth redirect (#1038) → v0.50.210 / Login redirects back to original URL The original drafts landed in [Unreleased] when individual PRs (#1047, #1048, #1043) were approved; the v0.50.210 release-notes commit then added the same items under the version section without removing the [Unreleased] copies. Drop the duplicates so users reading the CHANGELOG don't see the same fix listed twice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent Co-authored-by: Pix (Hermes) <aliceisjustplaying@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: qxxaa <mrhanoi@outlook.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:47:44 +00:00
const _liveRes=await fetch(url.href,{credentials:'include'});
if(_redirectIfUnauth(_liveRes)) return;
const data=await _liveRes.json();
fix: silent agent errors, stale model list, live model fetching (#377) * fix: silent errors, stale models, live model fetching (#373, #374, #375) - api/streaming.py: detect empty agent response (_assistant_added check), emit apperror(type='no_response' or 'auth_mismatch') instead of silent done - api/streaming.py: add _token_sent flag so guard works for streaming agents - static/messages.js: done handler belt-and-suspenders guard for zero replies - static/messages.js: apperror handler labels 'no_response' type distinctly - api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and _PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini) - api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models from provider API with B310 scheme check + SSRF guard - static/ui.js: _fetchLiveModels() background fetch after static list loads, appends new models to dropdown, caches per session, skips unsupported providers Other: - tests/test_issues_373_374_375.py: 25 new structural tests - tests/test_regressions.py: extend done-handler window 1500->2500 chars - CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922) * fix: SSRF hostname bypass + auth detection operator precedence 1. routes.py: SSRF guard used substring matching (any(k in hostname)) which allows bypass via hostnames like evil-ollama.attacker.com. Changed to exact hostname matching against a fixed set of known local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1). 2. streaming.py: _is_auth detection had a Python operator precedence bug on the ternary expression. The line: 'AuthenticationError' in type(...).__name__ if _last_err else False parsed as the ternary absorbing the rest of the or-chain when _last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: fix v0.50.20 CHANGELOG version number and test count (949 tests) --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
if(!data.models||!data.models.length) return;
_liveModelCache[provider]=data.models;
fix(settings): show live models in default model picker and apply to new chats (#872) (#900) * fix(settings): show live models in default model picker and apply to new chats (#872) Two related bugs: 1. Settings > Preferences > Default Model dropdown only showed static models from /api/models — live-fetched models (e.g. @nous:anthropic/claude-opus-4.7) were missing. Now calls _fetchLiveModels() on the settings picker too. 2. New chats ignored the saved default model preference — they always used the chat-header dropdown value (which reflects the previous session's model). Now newSession() uses the saved default_model and syncs the dropdown. Extracted _addLiveModelsToSelect() from _fetchLiveModels() so cached live models can be applied to any <select> element (chat-header or settings picker). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(tests): update live-model prefix tests for _addLiveModelsToSelect extraction The tests searched for og.dataset.provider, _isPortalFetch, and openrouter exclusion patterns inside _fetchLiveModels(). These were extracted into _addLiveModelsToSelect() as part of the #872 fix. Updated regex targets to check _addLiveModelsToSelect first, falling back to _fetchLiveModels. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: add multi-tab note on window._defaultModel Clarifies that window._defaultModel is per-page-load and not synced across browser tabs, following maintainer feedback on #889. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: CHANGELOG for v0.50.170 * chore: trigger PR refresh after rebase --------- Co-authored-by: fr33m1nd <bergeouss@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-23 16:58:15 +00:00
const added=_addLiveModelsToSelect(provider,data.models,sel);
fix: silent agent errors, stale model list, live model fetching (#377) * fix: silent errors, stale models, live model fetching (#373, #374, #375) - api/streaming.py: detect empty agent response (_assistant_added check), emit apperror(type='no_response' or 'auth_mismatch') instead of silent done - api/streaming.py: add _token_sent flag so guard works for streaming agents - static/messages.js: done handler belt-and-suspenders guard for zero replies - static/messages.js: apperror handler labels 'no_response' type distinctly - api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and _PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini) - api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models from provider API with B310 scheme check + SSRF guard - static/ui.js: _fetchLiveModels() background fetch after static list loads, appends new models to dropdown, caches per session, skips unsupported providers Other: - tests/test_issues_373_374_375.py: 25 new structural tests - tests/test_regressions.py: extend done-handler window 1500->2500 chars - CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922) * fix: SSRF hostname bypass + auth detection operator precedence 1. routes.py: SSRF guard used substring matching (any(k in hostname)) which allows bypass via hostnames like evil-ollama.attacker.com. Changed to exact hostname matching against a fixed set of known local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1). 2. streaming.py: _is_auth detection had a Python operator precedence bug on the ternary expression. The line: 'AuthenticationError' in type(...).__name__ if _last_err else False parsed as the ternary absorbing the rest of the or-chain when _last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: fix v0.50.20 CHANGELOG version number and test count (949 tests) --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
if(added>0){
if(typeof syncModelChip==='function') syncModelChip();
console.log('[hermes] Live models loaded for',provider+':',added,'new models added');
}
}catch(e){
console.debug('[hermes] Live model fetch failed for',provider,e.message);
}finally{
_liveModelFetchPending.delete(provider);
fix: silent agent errors, stale model list, live model fetching (#377) * fix: silent errors, stale models, live model fetching (#373, #374, #375) - api/streaming.py: detect empty agent response (_assistant_added check), emit apperror(type='no_response' or 'auth_mismatch') instead of silent done - api/streaming.py: add _token_sent flag so guard works for streaming agents - static/messages.js: done handler belt-and-suspenders guard for zero replies - static/messages.js: apperror handler labels 'no_response' type distinctly - api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and _PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini) - api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models from provider API with B310 scheme check + SSRF guard - static/ui.js: _fetchLiveModels() background fetch after static list loads, appends new models to dropdown, caches per session, skips unsupported providers Other: - tests/test_issues_373_374_375.py: 25 new structural tests - tests/test_regressions.py: extend done-handler window 1500->2500 chars - CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922) * fix: SSRF hostname bypass + auth detection operator precedence 1. routes.py: SSRF guard used substring matching (any(k in hostname)) which allows bypass via hostnames like evil-ollama.attacker.com. Changed to exact hostname matching against a fixed set of known local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1). 2. streaming.py: _is_auth detection had a Python operator precedence bug on the ternary expression. The line: 'AuthenticationError' in type(...).__name__ if _last_err else False parsed as the ternary absorbing the rest of the or-chain when _last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: fix v0.50.20 CHANGELOG version number and test count (949 tests) --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:52:35 +00:00
}
}
fix: warn on provider/model mismatch, surface auth errors (#266) * fix: warn on provider/model mismatch, surface auth errors (#266) Fixes #266 — WebUI silently ignores provider/model selection mismatch. The problem: selecting an OpenRouter (or Anthropic/OpenAI) model while Hermes is configured for a different provider (e.g. local Ollama) sends the request to the wrong endpoint, which returns a 401 Unauthorized error with no UI indication of why. Three-layer fix: 1. api/streaming.py — detect 401/auth errors explicitly Added is_auth_error detection covering '401', 'AuthenticationError', 'authentication', 'unauthorized', 'invalid api key', and the specific Ollama error string 'no cookie auth credentials'. Auth errors emit apperror with type='auth_mismatch' and a hint pointing to 'hermes model'. 2. static/ui.js — expose active_provider and warn on selection - populateModelDropdown() stores data.active_provider from /api/models as window._activeProvider (the field was already in the response but the frontend never used it) - New _checkProviderMismatch(modelId) helper: compares the selected model's slash-prefix (e.g. 'openai/' from 'openai/gpt-4o') against the active provider. Skips the check for 'openrouter' and 'custom' to avoid false positives on configs that legitimately route any model. 3. static/boot.js — warn on model dropdown change modelSelect.onchange calls _checkProviderMismatch() and shows a toast when the selected model looks incompatible with the configured provider. 4. static/messages.js — distinct UI label for auth errors apperror handler now distinguishes type='auth_mismatch' and shows 'Provider mismatch' as the error label instead of 'Error'. 5. static/i18n.js — provider_mismatch_warning and provider_mismatch_label keys added to all 5 locales (en, es, de, zh-Hans, zh-Hant). Tests: 21 new tests in tests/test_provider_mismatch.py covering all five change areas. 679/679 total pass (658 baseline + 21 new). * fix: t() call args spread + use i18n label for auth mismatch 1. ui.js: _checkProviderMismatch passed [modelId, ap] as a single array arg to t(). Since t(key, ...args) spreads, the function received the array as m and undefined as p. Fixed to pass as separate args: t('provider_mismatch_warning', modelId, ap). 2. messages.js: 'Provider mismatch' label was hardcoded instead of using t('provider_mismatch_label'). Now uses the i18n key with fallback for when t() isn't available. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 04:25:18 +00:00
/**
* Check if the given model ID belongs to a different provider than the one
* currently configured in Hermes. Returns a warning string if mismatched,
* or null if the selection looks compatible.
*
* Provider detection is intentionally loose — we compare the model's slash
* prefix (e.g. "openai/" from "openai/gpt-4o") against the active provider
* name. Custom/local endpoints report active_provider='custom' or the
* base_url hostname and we skip the check to avoid false positives.
*/
function _checkProviderMismatch(modelId){
const ap=(window._activeProvider||'').toLowerCase();
if(!ap||ap==='custom'||ap==='openrouter') return null; // can't reliably check
// @provider: prefixed IDs came from that provider's live model list — no mismatch possible
if(modelId.startsWith('@')) return null;
fix: warn on provider/model mismatch, surface auth errors (#266) * fix: warn on provider/model mismatch, surface auth errors (#266) Fixes #266 — WebUI silently ignores provider/model selection mismatch. The problem: selecting an OpenRouter (or Anthropic/OpenAI) model while Hermes is configured for a different provider (e.g. local Ollama) sends the request to the wrong endpoint, which returns a 401 Unauthorized error with no UI indication of why. Three-layer fix: 1. api/streaming.py — detect 401/auth errors explicitly Added is_auth_error detection covering '401', 'AuthenticationError', 'authentication', 'unauthorized', 'invalid api key', and the specific Ollama error string 'no cookie auth credentials'. Auth errors emit apperror with type='auth_mismatch' and a hint pointing to 'hermes model'. 2. static/ui.js — expose active_provider and warn on selection - populateModelDropdown() stores data.active_provider from /api/models as window._activeProvider (the field was already in the response but the frontend never used it) - New _checkProviderMismatch(modelId) helper: compares the selected model's slash-prefix (e.g. 'openai/' from 'openai/gpt-4o') against the active provider. Skips the check for 'openrouter' and 'custom' to avoid false positives on configs that legitimately route any model. 3. static/boot.js — warn on model dropdown change modelSelect.onchange calls _checkProviderMismatch() and shows a toast when the selected model looks incompatible with the configured provider. 4. static/messages.js — distinct UI label for auth errors apperror handler now distinguishes type='auth_mismatch' and shows 'Provider mismatch' as the error label instead of 'Error'. 5. static/i18n.js — provider_mismatch_warning and provider_mismatch_label keys added to all 5 locales (en, es, de, zh-Hans, zh-Hant). Tests: 21 new tests in tests/test_provider_mismatch.py covering all five change areas. 679/679 total pass (658 baseline + 21 new). * fix: t() call args spread + use i18n label for auth mismatch 1. ui.js: _checkProviderMismatch passed [modelId, ap] as a single array arg to t(). Since t(key, ...args) spreads, the function received the array as m and undefined as p. Fixed to pass as separate args: t('provider_mismatch_warning', modelId, ap). 2. messages.js: 'Provider mismatch' label was hardcoded instead of using t('provider_mismatch_label'). Now uses the i18n key with fallback for when t() isn't available. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 04:25:18 +00:00
const slash=modelId.indexOf('/');
if(slash<0) return null; // bare model name, no provider prefix
const modelProvider=modelId.substring(0,slash).toLowerCase();
// Normalise common aliases
const aliases={'claude':'anthropic','gpt':'openai','gemini':'google'};
const norm=p=>aliases[p]||p;
if(norm(modelProvider)!==norm(ap)){
return (window.t?window.t('provider_mismatch_warning',modelId,ap):
`"${modelId}" may not work with your configured provider (${ap}). Send anyway or run \`hermes model\` to switch.`);
}
return null;
}
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
function _selectedModelOption(){
const sel=$('modelSelect');
if(!sel) return null;
return sel.options[sel.selectedIndex]||null;
}
function _normalizeConfiguredModelKey(modelId){
let s=String(modelId||'').trim().toLowerCase();
// Strip @provider: prefix (e.g., @custom:jingdong:GLM-5 -> GLM-5).
// Defensive: trailing-colon / trailing-slash falls back to the original key
// so malformed configs don't collapse distinct ids to '' (matches backend _norm_model_id).
if(s.startsWith('@')&&s.includes(':')){const last=s.split(':').pop();s=last||s;}
if(s.includes('/')){const last=s.split('/').pop();s=last||s;}
return s.replace(/-/g,'.');
}
function _getConfiguredModelBadge(modelId,badgeMap,providerId){
const map=badgeMap||window._configuredModelBadges||{};
if(!modelId||!map) return null;
const provider=String(providerId||'').toLowerCase();
const exact=map[modelId];
if(exact && (!provider || !exact.provider || String(exact.provider).toLowerCase()===provider)) return exact;
const targetNorm=_normalizeConfiguredModelKey(modelId);
const matches=[];
for(const [candidate,badge] of Object.entries(map)){
if(_normalizeConfiguredModelKey(candidate)===targetNorm) matches.push(badge);
}
if(!matches.length) return null;
if(provider){
const providerMatch=matches.find(badge=>String(badge&&badge.provider||'').toLowerCase()===provider);
if(providerMatch) return providerMatch;
return matches.length===1 ? matches[0] : null;
}
return matches[0];
}
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
function syncModelChip(){
const sel=$('modelSelect');
const chip=$('composerModelChip');
const label=$('composerModelLabel');
const mobileLabel=$('composerMobileModelLabel');
const mobileAction=$('composerMobileModelAction');
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
const dd=$('composerModelDropdown');
if(!sel||!chip||!label) return;
// Don't show a model label until boot has finished loading to prevent flash of wrong default
if(!S._bootReady){
label.textContent='';
if(mobileLabel) mobileLabel.textContent='';
chip.title='Conversation model';
return;
}
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
const opt=_selectedModelOption();
const text=opt?opt.textContent:getModelLabel(sel.value||'');
const gatewayRouting=_latestGatewayRoutingForSession(S.session);
const displayText=_formatGatewayModelLabel(sel.value||'',text,gatewayRouting)||text;
label.textContent=displayText;
if(mobileLabel) mobileLabel.textContent=displayText;
chip.title=gatewayRouting?`${sel.value||'Conversation model'} ${_gatewayRoutingLabel(gatewayRouting)}`:(sel.value||'Conversation model');
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
chip.classList.toggle('active',!!(dd&&dd.classList.contains('open')));
if(mobileAction) mobileAction.classList.toggle('active',!!(dd&&dd.classList.contains('open')));
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
}
function _positionModelDropdown(){
const dd=$('composerModelDropdown');
const chip=$('composerModelChip');
const mobileAction=$('composerMobileModelAction');
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
const footer=document.querySelector('.composer-footer');
if(!dd||!footer) return;
const panel=$('composerMobileConfigPanel');
const anchor=(panel&&panel.classList.contains('open')&&mobileAction)?mobileAction:(chip&&chip.offsetParent?chip:mobileAction);
if(!anchor) return;
const chipRect=anchor.getBoundingClientRect();
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
const footerRect=footer.getBoundingClientRect();
let left=chipRect.left-footerRect.left;
const maxLeft=Math.max(0, footer.clientWidth-dd.offsetWidth);
left=Math.max(0, Math.min(left, maxLeft));
dd.style.left=`${left}px`;
}
function renderModelDropdown(){
const dd=$('composerModelDropdown');
const sel=$('modelSelect');
if(!dd||!sel) return;
// Store model data for filtering
const _modelData=[];
const _badgeMap=window._configuredModelBadges||{};
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
for(const child of Array.from(sel.children)){
if(child.tagName==='OPTGROUP'){
const providerId=child.dataset&&child.dataset.provider?child.dataset.provider:'';
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
for(const opt of Array.from(child.children)){
_modelData.push({value:opt.value,name:esc(opt.textContent||getModelLabel(opt.value)),id:esc(opt.value),group:child.label||'',badge:_getConfiguredModelBadge(opt.value,_badgeMap,providerId)});
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
}
}
if(child.tagName==='OPTION'){
_modelData.push({value:child.value,name:esc(child.textContent||getModelLabel(child.value)),id:esc(child.value),group:'',badge:_getConfiguredModelBadge(child.value,_badgeMap)});
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
}
}
const _existingConfiguredKeys=new Set(_modelData.map(existing=>_normalizeConfiguredModelKey(existing.value)));
for(const [modelId,badge] of Object.entries(_badgeMap)){
if(_existingConfiguredKeys.has(_normalizeConfiguredModelKey(modelId))) continue;
_modelData.push({
value:modelId,
name:esc(getModelLabel(modelId)),
id:esc(modelId),
group:'',
badge,
});
_existingConfiguredKeys.add(_normalizeConfiguredModelKey(modelId));
}
// Create search input FIRST before filterModels definition
const _scopeNote=document.createElement('div');
_scopeNote.className='model-scope-note';
_scopeNote.textContent=t('model_scope_advisory')||'Applies to this conversation from your next message.';
const _searchRow=document.createElement('div');
_searchRow.className='model-search-row';
_searchRow.innerHTML=`<input class="model-search-input" type="text" placeholder="${esc(t('model_search_placeholder')||'Search models…')}" spellcheck="false" autocomplete="off"><button class="model-search-clear" title="Clear search">${li('x',10)}</button>`;
const _si=_searchRow.querySelector('.model-search-input');
const _sc=_searchRow.querySelector('.model-search-clear');
// Create custom model section elements
const _custSep=document.createElement('div');
_custSep.className='model-group model-custom-sep';
_custSep.textContent=t('model_custom_label')||'Custom model ID';
const _custRow=document.createElement('div');
_custRow.className='model-custom-row';
_custRow.innerHTML=`<input class="model-custom-input" type="text" placeholder="${esc(t('model_custom_placeholder')||'e.g. openai/gpt-5.4')}" spellcheck="false" autocomplete="off"><button class="model-custom-btn" title="Use this model">${li('plus',12)}</button>`;
const _ci=_custRow.querySelector('.model-custom-input');
const _cb=_custRow.querySelector('.model-custom-btn');
const _configuredRank=(badge)=>{
if(!badge) return Number.POSITIVE_INFINITY;
if(badge.role==='primary') return 0;
if(badge.role==='fallback'){
const m=String(badge.label||'').match(/fallback\s+(\d+)/i);
return m?Number(m[1]):999;
}
return 500;
};
// Filter function (defined AFTER _searchRow and _cust* are created)
const _filterModels=(term)=>{
term=term.trim().toLowerCase();
const found=new Set();
for(const m of _modelData){
const name=m.name.toLowerCase();
const id=m.id.toLowerCase();
if(name.includes(term)||id.includes(term)){
found.add(m.value);
}
}
const matches=(m)=>!term||found.has(m.value);
const configuredModels=_modelData
.filter(m=>m.badge&&matches(m))
.sort((a,b)=>{
const configuredRankA=_configuredRank(a.badge);
const configuredRankB=_configuredRank(b.badge);
if(configuredRankA!==configuredRankB) return configuredRankA-configuredRankB;
return a.name.localeCompare(b.name);
});
const configuredIds=new Set(configuredModels.map(m=>m.value));
// Clear and rebuild
dd.innerHTML='';
// Add search and custom elements first (CRITICAL: must be before models)
dd.appendChild(_scopeNote);
dd.appendChild(_searchRow);
dd.appendChild(_custSep);
dd.appendChild(_custRow);
if(configuredModels.length){
const configuredHeading=document.createElement('div');
configuredHeading.className='model-group';
configuredHeading.textContent=t('model_group_configured')||'Configured';
dd.appendChild(configuredHeading);
for(const m of configuredModels){
const row=document.createElement('div');
row.className='model-opt'+(m.value===sel.value?' active':'');
// Add provider info to badge label (e.g., "Primary (jingdong)")
let badgeLabel=m.badge?(m.badge.label||'Configured'):'';
if(m.badge&&m.badge.provider){
const providerName=m.badge.provider.replace(/^custom:/,'').split('/')[0];
badgeLabel+=` (${providerName})`;
}
const badgeHtml=m.badge?`<span class="model-opt-badge model-opt-badge--${esc(m.badge.role||'configured')}">${esc(badgeLabel)}</span>`:'';
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${m.name}</span>${badgeHtml}</div><span class="model-opt-id">${m.id}</span>`;
row.onclick=()=>selectModelFromDropdown(m.value);
dd.appendChild(row);
}
}
// Add remaining models matching filter
let _lastGroup=null;
// Count models per group for heading labels (#1425)
const _groupCounts={};
for(const m of _modelData){
if(configuredIds.has(m.value)) continue;
if(m.group) _groupCounts[m.group]=(_groupCounts[m.group]||0)+1;
}
for(const m of _modelData){
if(configuredIds.has(m.value)||!matches(m)) continue;
if(m.group&&m.group!==_lastGroup){
const heading=document.createElement('div');
heading.className='model-group';
const count=_groupCounts[m.group]||0;
heading.textContent=count>1?`${m.group} (${count})`:m.group;
dd.appendChild(heading);
_lastGroup=m.group;
}
const row=document.createElement('div');
row.className='model-opt'+(m.value===sel.value?' active':'');
const badgeHtml=m.badge?`<span class="model-opt-badge model-opt-badge--${esc(m.badge.role||'configured')}">${esc(m.badge.label||'Configured')}</span>`:'';
// Inline provider chip on every row that has a group (#1425)
const providerChip=m.group?`<span class="model-opt-provider">${esc(m.group)}</span>`:'';
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${m.name}</span>${badgeHtml}${providerChip}</div><span class="model-opt-id">${m.id}</span>`;
row.onclick=()=>selectModelFromDropdown(m.value);
dd.appendChild(row);
}
// Show "No results" if filtered and nothing matched
if(term&&found.size===0){
const noResult=document.createElement('div');
noResult.className='model-search-no-results';
noResult.textContent=t('model_search_no_results')||'No models found';
noResult.style.padding='12px 14px';
noResult.style.color='var(--muted)';
noResult.style.textAlign='center';
dd.appendChild(noResult);
}
// Restore focus to search input
_si.focus();
};
// Event handlers for search input
_si.addEventListener('input',()=>_filterModels(_si.value));
_si.addEventListener('keydown',e=>{if(e.key==='Enter') {e.preventDefault();}if(e.key==='Escape') {closeModelDropdown();}});
_si.addEventListener('click',e=>e.stopPropagation());
// Event handlers for clear button
_sc.onclick=()=>{ _si.value=''; _filterModels(''); _si.focus(); };
_sc.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){ _si.value=''; _filterModels(''); _si.focus(); e.preventDefault(); }});
// Event handlers for custom input
const _applyCustom=()=>{const v=_ci.value.trim();if(!v)return;selectModelFromDropdown(v);_ci.value='';};
_cb.onclick=_applyCustom;
_ci.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();_applyCustom();}if(e.key==='Escape'){closeModelDropdown();}});
_ci.addEventListener('click',e=>e.stopPropagation());
// Add search and custom elements to dropdown (initial render)
dd.appendChild(_scopeNote);
dd.appendChild(_searchRow);
dd.appendChild(_custSep);
dd.appendChild(_custRow);
// Apply initial filter (empty shows all)
_filterModels('');
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
}
async function selectModelFromDropdown(value){
const sel=$('modelSelect');
if(!sel||sel.value===value) { closeModelDropdown(); return; }
// If the value isn't in the option list (custom model ID), add a temporary option
// so sel.value assignment succeeds and the model chip shows the custom ID.
if(!Array.from(sel.options).some(o=>o.value===value)){
const opt=document.createElement('option');
opt.value=value;
opt.textContent=getModelLabel(value);
opt.dataset.custom='1';
// Remove any previous custom option before adding new one
sel.querySelectorAll('option[data-custom]').forEach(o=>o.remove());
sel.appendChild(opt);
}
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
sel.value=value;
syncModelChip();
closeModelDropdown();
if(typeof sel.onchange==='function') await sel.onchange();
}
function toggleModelDropdown(){
const dd=$('composerModelDropdown');
const chip=$('composerModelChip');
const sel=$('modelSelect');
if(!dd||!chip||!sel) return;
const open=dd.classList.contains('open');
if(open){closeModelDropdown(); return;}
if(typeof closeProfileDropdown==='function') closeProfileDropdown();
if(typeof closeWsDropdown==='function') closeWsDropdown();
if(typeof closeReasoningDropdown==='function') closeReasoningDropdown();
if(typeof closeToolsetsDropdown==='function') closeToolsetsDropdown();
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
renderModelDropdown();
dd.classList.add('open');
_positionModelDropdown();
chip.classList.add('active');
const mobileAction=$('composerMobileModelAction');
if(mobileAction) mobileAction.classList.add('active');
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
}
function closeModelDropdown(){
const dd=$('composerModelDropdown');
const chip=$('composerModelChip');
const mobileAction=$('composerMobileModelAction');
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(dd) dd.classList.remove('open');
if(chip) chip.classList.remove('active');
if(mobileAction) mobileAction.classList.remove('active');
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
}
document.addEventListener('click',e=>{
if(
!e.target.closest('#composerModelChip') &&
!e.target.closest('#composerMobileModelAction') &&
!e.target.closest('#composerModelDropdown')
) closeModelDropdown();
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
});
window.addEventListener('resize',()=>{
const dd=$('composerModelDropdown');
if(dd&&dd.classList.contains('open')) _positionModelDropdown();
fix: reasoning chip dropdown visible + monochrome SVG icon + /btw answer preserved (closes #933) (#934) * fix: reasoning chip dropdown visible + SVG icon + /btw answer no longer wiped (closes #933) * fix(ui): resize handler symmetry + lock regressions for PR #934 fixes Two small additions on top of the core PR: 1. Resize handler now re-positions the reasoning dropdown when the window resizes while it's open, matching the existing model-dropdown branch. Without this, resizing while the dropdown is open leaves it aligned to the pre-resize chip position — fine in practice (most resizes close the dropdown via the global click handler) but inconsistent with the model-dropdown sibling. 2. Regression test file tests/test_reasoning_chip_btw_fixes.py with 10 tests locking all four fixes in place so they can't silently regress: - Dropdown sits OUTSIDE .composer-left (so overflow-y: hidden can't clip it) - Dropdown is grouped with the other composer-level dropdowns - Chip button contains stroke="currentColor" SVG (not a 🧠 emoji) - _applyReasoningChip() body doesn't include 🧠 - cmdReasoning calls _applyReasoningChip(eff) directly with the server-confirmed effort, not syncReasoningChip() (stale cache) - _streamDone flag declared, set in done handler, checked in onerror - _ensureBtwRow() called in done handler (creates bubble when no tokens arrive) - resize handler re-positions composerReasoningDropdown Full suite: 2056 passed, 0 failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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-24 02:18:51 +00:00
// Keep the reasoning dropdown aligned under its chip when the window
// resizes while open — same pattern as the model dropdown above.
const rdd=$('composerReasoningDropdown');
if(rdd&&rdd.classList.contains('open')&&typeof _positionReasoningDropdown==='function'){
_positionReasoningDropdown();
}
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
});
// ── Reasoning effort chip ────────────────────────────────────────────────────
let _currentReasoningEffort=null;
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075) * fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly MIME_MAP was missing entries for .html and .htm. The server fell back to Content-Type: application/octet-stream, which browsers refuse to render as HTML in an iframe — causing a blank white preview. The rest of the pipeline was already correct: the iframe exists in static/index.html, openFile() in static/workspace.js routes .html to showPreview('html'), and _handle_file_raw() in api/routes.py sets the correct CSP sandbox header when ?inline=1 is present. The only missing piece was the MIME type. * test(workspace): lock in MIME_MAP entry for .html/.htm PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py to fix the blank workspace HTML preview iframe. Without a direct assertion on the MIME_MAP entries, the fix could silently regress (the existing test_779_html_preview.py tests cover the iframe wiring, the inline=1 query handling, and the CSP sandbox header — but none of them touch MIME_MAP itself). Add a single regression test that asserts MIME_MAP['.html'] and MIME_MAP['.htm'] are both 'text/html' so any future removal of those entries fails CI immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(composer): raise .approval-card.visible z-index above .queue-card .queue-card has z-index:2. .approval-card.visible had no z-index, so the queue flyout would render on top of the approval card when both were visible simultaneously — obscuring the Allow/Deny buttons. Fix: add z-index:3 to .approval-card.visible so approvals always render above the queue flyout. Approval is a blocking, security-relevant interaction and must never be obscured by passive UI elements. * test(composer): pin approval-card z-index > queue-card invariant PR #1071 raises .approval-card.visible to z-index:3 so the security- relevant Allow / Deny buttons stay clickable when the queue flyout is also open. Without a regression test, a future CSS edit could silently drop the z-index back below queue-card (z-index:2) and reintroduce the bug — there is no automated UI test covering this stacking interaction. Add a focused regex check that pins the invariant: .approval-card.visible z-index must be strictly greater than .queue-card z-index. Modeled on the existing CSS-regex regression style in tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: intercept /steer /interrupt /queue before busy-mode routing in send() Root cause: slash commands entered while the agent is busy never reached the command dispatcher. send() enters the busy block and returns early at line ~50, so the slash-command intercept (~line 56) is never reached. The text was queued as a plain message. When it drained after the turn ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream, and showed "No active task to stop." Fix: at the top of the busy block, before checking busyMode, check if the text starts with / and is one of the three control commands. If so, dispatch the handler immediately and return. This lets the user type /steer, /interrupt, or /queue at any time — including while the agent is mid-stream — and have them execute against the live session. Two new regression tests added: - test_slash_commands_intercepted_before_busymode_routing: verifies the intercept appears before the busyMode routing in the busy block - test_steer_intercept_calls_handler_directly: verifies the intercept calls _bc.fn(_pc.args) and returns, not queues * test(busy-intercept): pin sync input-clear before await in slash intercept PR #1072's intercept clears the msg input before awaiting the handler. Order matters: if the await happens first (or if the clear is moved inside the handler), the input still shows '/steer foo' for the duration of the await. A reflexive second Enter press during that window — common while waiting for the toast — re-runs send(): either re-fires the handler (double-steer) or, if the turn just ended, falls through to the non-busy slash dispatcher and drops a confusing "No active task to stop." Add test_steer_intercept_clears_input_before_await pinning the order so this UX invariant cannot silently regress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: update steer i18n and settings copy — steer no longer interrupts With the real /steer implementation (agent.steer() via /api/chat/steer), steer injects a correction mid-turn WITHOUT interrupting the current stream. The previous copy said "falls back to interrupt", "Steer (interrupt + send)", etc. — accurate only for the old placeholder, not the real implementation. Changes across all 6 locales (en/ru/es/de/zh/zh-Hant): cmd_steer: "falls back to interrupt" removed settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction" cmd_steer_fallback: "interrupted" → "queued for next turn" busy_steer_fallback: "interrupted instead" → "queued for next turn" settings_desc_busy_input_mode: "currently falls back to interrupt" removed Also: static/index.html: inline fallback text updated to match static/commands.js: internal comment clarified (fallback = queue+cancel, not "interrupt mode" which implies the primary action) * fix(renderer): group consecutive blockquote lines into single element Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs: 1. `.+` required at least one character — bare `>` lines (blank continuation lines) did not match and passed through as literal `>` 2. Each matching line became its own `<blockquote>` element — a 10-line blockquote produced 10 stacked `<blockquote>` tags with no grouping 3. When a fenced code block sat inside a blockquote, the fence-stash pass consumed the code content and left orphaned `>` lines that the old `.+` pattern could not match Fix: replace the single-line regex with a group-based approach that matches one or more consecutive `>` lines as a single block, strips the `>` prefix from each line, passes each non-empty line through inlineMd(), turns blank `>` lines into `<br>`, and wraps the entire group in one `<blockquote>`. 14 regression tests added covering: - Single-line blockquotes (regression) - Multi-line grouping (2 and 10 lines) - Two separate blockquotes staying separate - Bare `>` and `>text` (no space) edge cases - Blank continuation lines → <br> - Bold / italic / inline-code inside blockquotes - Blockquote followed by normal paragraph * fix(renderer): drop empty trailing line from blockquote match The new group-based blockquote rule introduced in this PR captures the trailing newline in its (?:\n|$) clause. After block.split('\n') that trailing newline produces an empty final element. The original filter only dropped lone bare '>' artifacts on the last line, so the empty final element survived, and the .map(blank → '<br>') step turned it into a phantom <br> immediately before </blockquote>. Visible symptom: any blockquote whose source ends with \n (the common case — a quote followed by another paragraph or end-of-message) renders with an extra blank line at the bottom of the quote. Reproducer: '> Hello\n\nThe rest of the message.' → '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.' ^^^ phantom <br> Fix: replace the single-line filter with a while-loop that pops trailing lines while they are either empty OR a bare '>'. This matches the intent the Python test mirror in tests/test_blockquote_rendering.py already had (the mirror was correct; the JS was not — that's why the original tests passed despite the bug). Also add four new regression tests in TestNoPhantomTrailingBr that pin the no-trailing-<br> invariant for the common shapes: - input ending with \n - quote followed by paragraph (the real-world case) - multi-line quote ending with \n - quote with blank continuation + trailing \n (internal <br> stays, trailing <br> does not) Verified end-to-end with node against the actual JS regex. 244 renderer-adjacent tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes Five additional fixes on top of the blockquote grouping from the initial commit: 1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows line endings do not produce stray \r characters in rendered output 2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use inside blockquotes/lists) and the outer pass (for plain paragraphs). Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped. 3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐ via task-done/task-todo span wrappers. Checks [X] (uppercase) too. 4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its own <blockquote> element rather than passing through as literal >. Implemented by extracting the blockquote rule into _applyBlockquotes() which calls itself recursively on the stripped inner content. 5. Lists inside blockquotes: > - item now renders <ul><li> inside the blockquote instead of a literal "- item" string. Task list items work inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>). Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF strip at the top of renderMd pushed the autolink regex past the old limit. 68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py covering all constructs, edge cases, and combinations. * fix(renderer): restore space in blockquote prefix-strip regex Commit 04e7b53 changed the blockquote prefix-strip regex from /^>[ \t]?/ (consume "> ", "\t>", or just ">") to /^>[\t]?/ (only consume "\t>" or just ">") The space character was dropped from the character class. Since practically every blockquote an LLM produces is "> " (greater-than followed by a space), this leaves a leading space artifact on every stripped blockquote line. Worse, the leading space breaks the list-detection regex `^(?: )?[-*+] ` inside the new `_applyBlockquotes` helper — that regex requires either zero or two leading spaces, never one — so the new "list inside blockquote" feature never fired for the canonical input shape `> - item`. Reproducer (against the actual ui.js via node, before the fix): > Hello world → <blockquote> Hello world</blockquote> ^ phantom leading space > Steps: → <blockquote>Steps: > - one - one > - two - two</blockquote> ^ literal text, NOT a <ul>; lists-in-quote feature broken > - [x] done → blockquote with literal "[x] done", no checkbox span Tests passed despite the bug because tests/test_blockquote_rendering.py and tests/test_renderer_comprehensive.py validate against a Python mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e. the mirror is correct, the JS is not, and the static-mirror tests can't catch the divergence. Same shape of bug as commit 94d63d0 (phantom <br> in trailing line) where the mirror was right and the JS was wrong. Fix: restore the space character in the strip regex's character class. Add tests/test_renderer_js_behaviour.py — 11 tests that drive the ACTUAL renderMd via node and assert on rendered output for the most common LLM shapes (single-line quote, multi-line quote, list inside quote, task list inside quote, nested >>>, strikethrough inside and outside quote, top-level task list, quote followed by heading, multi-paragraph quote with list, CRLF normalisation). Verified: the buggy regex makes 6 of those 11 tests fail; the corrected regex makes all 11 pass. Suite: 2354 passed, 0 new failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Collapse agent session compression chains * Restore upstream changelog entries * fix(agent_sessions): bubble active compression chains to top by tip last_activity The original PR merge kept the chain head's id/title/started_at and overrode id/model/message_count/ended_at/end_reason from the tip — but did NOT override last_activity. Since the projected list is sorted by last_activity DESC and the WebUI sidebar surfaces updated_at = last_activity, an actively-used compression chain whose tip is being edited NOW would sort by the ROOT's old last_activity and fall below recently touched standalone sessions. Reproducer (with the harness against actual code, before the fix): - root: started 30 days ago, last msg 30 days ago - tip: started 28 days ago (parent_session_id=root), last msg 5 seconds ago - standalone: last msg 2 days ago Sidebar order with original PR: [0] standalone (48h ago) [1] active_tip (last_activity=root's 720h ago) ← wrong Sidebar order after fix: [0] active_tip (last_activity=tip's 0h ago) ← correct [1] standalone (48h ago) This matches Hermes Agent's own list_sessions_rich projection at hermes_state.py:903-909, which overrides "last_active" from the tip exactly so that the agent CLI's session list orders the same way. Add ``last_activity`` to the merge-from-tip key list, update the existing test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to expect tip-derived updated_at, and add test_compression_chain_bubbles_to_top_by_tip_activity locking in the bubble-to-top invariant — without this regression test the previous behaviour passed CI because no test exercised the sort order against a mixed set of chains and standalone sessions. The chain head's started_at (created_at) and title remain preserved, so users can still find the conversation by its original date and name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.216 release notes and version bump Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix. * chore: gitignore local-only review harness directory Adds .local-review/ to .gitignore so renderer drivers, sample inputs, fixture builders, and other reviewer scratch files do not accidentally get committed. Nothing under that path is ever shared in the repo; keeping the entry tracked makes the boundary explicit for any future contributor who creates the directory locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Keep reasoning chip visible for None effort * test(reasoning): pin chip render output via node, not just source regex The PR's static checks in test_reasoning_chip_btw_fixes.py validate the shape of _applyReasoningChip (no display='none' literal, the right classList.toggle call exists, the right label literals are in the function body) but pass even if the runtime detail is wrong — for example if `inactive` were inverted, _normalizeReasoningEffort mishandled whitespace, or _formatReasoningEffortLabel returned the wrong literal for an unknown input. Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive the actual _applyReasoningChip() via node and assert on the rendered DOM state for each effort value: TestChipAlwaysVisible - empty / null -> "Default" label, inactive=true - "none" -> "None" label, inactive=true - "low"/"high" -> verbatim label, inactive=false TestNormalizationEdgeCases - "NONE" -> normalises to "None" - " none " -> trims and normalises - unknown junk -> falls through visible, never hidden TestTitleAttributeAccessibility - title attribute carries the human-readable label for tooltip / screen-reader use Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught). Against this PR's ui.js: 11/11 pass. This pattern (drive the actual JS via node) caught two regex-only regressions in PR #1073 where the Python mirror was correct while the JS was broken. Same protection added here so the chip-visibility contract can't silently break in a future refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add #1074 to v0.50.216 changelog, bump test count to 2428 * fix(i18n): restore broken Unicode in Russian and Spanish steer strings Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally stripped the `\u` prefix from Unicode escape sequences in two locales, producing garbled literal hex strings visible to users: Spanish (es): - cmd_steer: correcci00f3n → corrección - cmd_steer_fallback: 2014 en cola → — en cola - busy_steer_fallback: 2014 en cola → — en cola - settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección - settings_busy_input_mode_steer: correcci00f3n → corrección Russian (ru): - settings_desc_busy_input_mode: the entire Cyrillic string was replaced with raw 4-hex-char code-points without the \u prefix (041e043f... instead of actual Cyrillic). Decoded: "Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer внедряет коррекцию без прерывания." Fix: write the correct characters directly (UTF-8 is the file encoding so embedding them literally is cleaner than \u escapes for long text). All other locales (en, de, zh, zh-Hant) were not affected — confirmed by grepping for bare hex run-ons in the updated file. Verified: node --check static/i18n.js passes; full pytest suite green (2365 passed, 47 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: remove duplicate compression chain entry from [Unreleased] --------- 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> Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-26 04:06:31 +00:00
function _normalizeReasoningEffort(eff){
return String(eff||'').trim().toLowerCase();
}
function _formatReasoningEffortLabel(effort){
if(effort==='none') return 'None';
if(!effort) return 'Default';
return effort;
}
function _applyReasoningChip(eff){
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075) * fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly MIME_MAP was missing entries for .html and .htm. The server fell back to Content-Type: application/octet-stream, which browsers refuse to render as HTML in an iframe — causing a blank white preview. The rest of the pipeline was already correct: the iframe exists in static/index.html, openFile() in static/workspace.js routes .html to showPreview('html'), and _handle_file_raw() in api/routes.py sets the correct CSP sandbox header when ?inline=1 is present. The only missing piece was the MIME type. * test(workspace): lock in MIME_MAP entry for .html/.htm PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py to fix the blank workspace HTML preview iframe. Without a direct assertion on the MIME_MAP entries, the fix could silently regress (the existing test_779_html_preview.py tests cover the iframe wiring, the inline=1 query handling, and the CSP sandbox header — but none of them touch MIME_MAP itself). Add a single regression test that asserts MIME_MAP['.html'] and MIME_MAP['.htm'] are both 'text/html' so any future removal of those entries fails CI immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(composer): raise .approval-card.visible z-index above .queue-card .queue-card has z-index:2. .approval-card.visible had no z-index, so the queue flyout would render on top of the approval card when both were visible simultaneously — obscuring the Allow/Deny buttons. Fix: add z-index:3 to .approval-card.visible so approvals always render above the queue flyout. Approval is a blocking, security-relevant interaction and must never be obscured by passive UI elements. * test(composer): pin approval-card z-index > queue-card invariant PR #1071 raises .approval-card.visible to z-index:3 so the security- relevant Allow / Deny buttons stay clickable when the queue flyout is also open. Without a regression test, a future CSS edit could silently drop the z-index back below queue-card (z-index:2) and reintroduce the bug — there is no automated UI test covering this stacking interaction. Add a focused regex check that pins the invariant: .approval-card.visible z-index must be strictly greater than .queue-card z-index. Modeled on the existing CSS-regex regression style in tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: intercept /steer /interrupt /queue before busy-mode routing in send() Root cause: slash commands entered while the agent is busy never reached the command dispatcher. send() enters the busy block and returns early at line ~50, so the slash-command intercept (~line 56) is never reached. The text was queued as a plain message. When it drained after the turn ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream, and showed "No active task to stop." Fix: at the top of the busy block, before checking busyMode, check if the text starts with / and is one of the three control commands. If so, dispatch the handler immediately and return. This lets the user type /steer, /interrupt, or /queue at any time — including while the agent is mid-stream — and have them execute against the live session. Two new regression tests added: - test_slash_commands_intercepted_before_busymode_routing: verifies the intercept appears before the busyMode routing in the busy block - test_steer_intercept_calls_handler_directly: verifies the intercept calls _bc.fn(_pc.args) and returns, not queues * test(busy-intercept): pin sync input-clear before await in slash intercept PR #1072's intercept clears the msg input before awaiting the handler. Order matters: if the await happens first (or if the clear is moved inside the handler), the input still shows '/steer foo' for the duration of the await. A reflexive second Enter press during that window — common while waiting for the toast — re-runs send(): either re-fires the handler (double-steer) or, if the turn just ended, falls through to the non-busy slash dispatcher and drops a confusing "No active task to stop." Add test_steer_intercept_clears_input_before_await pinning the order so this UX invariant cannot silently regress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: update steer i18n and settings copy — steer no longer interrupts With the real /steer implementation (agent.steer() via /api/chat/steer), steer injects a correction mid-turn WITHOUT interrupting the current stream. The previous copy said "falls back to interrupt", "Steer (interrupt + send)", etc. — accurate only for the old placeholder, not the real implementation. Changes across all 6 locales (en/ru/es/de/zh/zh-Hant): cmd_steer: "falls back to interrupt" removed settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction" cmd_steer_fallback: "interrupted" → "queued for next turn" busy_steer_fallback: "interrupted instead" → "queued for next turn" settings_desc_busy_input_mode: "currently falls back to interrupt" removed Also: static/index.html: inline fallback text updated to match static/commands.js: internal comment clarified (fallback = queue+cancel, not "interrupt mode" which implies the primary action) * fix(renderer): group consecutive blockquote lines into single element Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs: 1. `.+` required at least one character — bare `>` lines (blank continuation lines) did not match and passed through as literal `>` 2. Each matching line became its own `<blockquote>` element — a 10-line blockquote produced 10 stacked `<blockquote>` tags with no grouping 3. When a fenced code block sat inside a blockquote, the fence-stash pass consumed the code content and left orphaned `>` lines that the old `.+` pattern could not match Fix: replace the single-line regex with a group-based approach that matches one or more consecutive `>` lines as a single block, strips the `>` prefix from each line, passes each non-empty line through inlineMd(), turns blank `>` lines into `<br>`, and wraps the entire group in one `<blockquote>`. 14 regression tests added covering: - Single-line blockquotes (regression) - Multi-line grouping (2 and 10 lines) - Two separate blockquotes staying separate - Bare `>` and `>text` (no space) edge cases - Blank continuation lines → <br> - Bold / italic / inline-code inside blockquotes - Blockquote followed by normal paragraph * fix(renderer): drop empty trailing line from blockquote match The new group-based blockquote rule introduced in this PR captures the trailing newline in its (?:\n|$) clause. After block.split('\n') that trailing newline produces an empty final element. The original filter only dropped lone bare '>' artifacts on the last line, so the empty final element survived, and the .map(blank → '<br>') step turned it into a phantom <br> immediately before </blockquote>. Visible symptom: any blockquote whose source ends with \n (the common case — a quote followed by another paragraph or end-of-message) renders with an extra blank line at the bottom of the quote. Reproducer: '> Hello\n\nThe rest of the message.' → '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.' ^^^ phantom <br> Fix: replace the single-line filter with a while-loop that pops trailing lines while they are either empty OR a bare '>'. This matches the intent the Python test mirror in tests/test_blockquote_rendering.py already had (the mirror was correct; the JS was not — that's why the original tests passed despite the bug). Also add four new regression tests in TestNoPhantomTrailingBr that pin the no-trailing-<br> invariant for the common shapes: - input ending with \n - quote followed by paragraph (the real-world case) - multi-line quote ending with \n - quote with blank continuation + trailing \n (internal <br> stays, trailing <br> does not) Verified end-to-end with node against the actual JS regex. 244 renderer-adjacent tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes Five additional fixes on top of the blockquote grouping from the initial commit: 1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows line endings do not produce stray \r characters in rendered output 2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use inside blockquotes/lists) and the outer pass (for plain paragraphs). Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped. 3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐ via task-done/task-todo span wrappers. Checks [X] (uppercase) too. 4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its own <blockquote> element rather than passing through as literal >. Implemented by extracting the blockquote rule into _applyBlockquotes() which calls itself recursively on the stripped inner content. 5. Lists inside blockquotes: > - item now renders <ul><li> inside the blockquote instead of a literal "- item" string. Task list items work inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>). Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF strip at the top of renderMd pushed the autolink regex past the old limit. 68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py covering all constructs, edge cases, and combinations. * fix(renderer): restore space in blockquote prefix-strip regex Commit 04e7b53 changed the blockquote prefix-strip regex from /^>[ \t]?/ (consume "> ", "\t>", or just ">") to /^>[\t]?/ (only consume "\t>" or just ">") The space character was dropped from the character class. Since practically every blockquote an LLM produces is "> " (greater-than followed by a space), this leaves a leading space artifact on every stripped blockquote line. Worse, the leading space breaks the list-detection regex `^(?: )?[-*+] ` inside the new `_applyBlockquotes` helper — that regex requires either zero or two leading spaces, never one — so the new "list inside blockquote" feature never fired for the canonical input shape `> - item`. Reproducer (against the actual ui.js via node, before the fix): > Hello world → <blockquote> Hello world</blockquote> ^ phantom leading space > Steps: → <blockquote>Steps: > - one - one > - two - two</blockquote> ^ literal text, NOT a <ul>; lists-in-quote feature broken > - [x] done → blockquote with literal "[x] done", no checkbox span Tests passed despite the bug because tests/test_blockquote_rendering.py and tests/test_renderer_comprehensive.py validate against a Python mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e. the mirror is correct, the JS is not, and the static-mirror tests can't catch the divergence. Same shape of bug as commit 94d63d0 (phantom <br> in trailing line) where the mirror was right and the JS was wrong. Fix: restore the space character in the strip regex's character class. Add tests/test_renderer_js_behaviour.py — 11 tests that drive the ACTUAL renderMd via node and assert on rendered output for the most common LLM shapes (single-line quote, multi-line quote, list inside quote, task list inside quote, nested >>>, strikethrough inside and outside quote, top-level task list, quote followed by heading, multi-paragraph quote with list, CRLF normalisation). Verified: the buggy regex makes 6 of those 11 tests fail; the corrected regex makes all 11 pass. Suite: 2354 passed, 0 new failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Collapse agent session compression chains * Restore upstream changelog entries * fix(agent_sessions): bubble active compression chains to top by tip last_activity The original PR merge kept the chain head's id/title/started_at and overrode id/model/message_count/ended_at/end_reason from the tip — but did NOT override last_activity. Since the projected list is sorted by last_activity DESC and the WebUI sidebar surfaces updated_at = last_activity, an actively-used compression chain whose tip is being edited NOW would sort by the ROOT's old last_activity and fall below recently touched standalone sessions. Reproducer (with the harness against actual code, before the fix): - root: started 30 days ago, last msg 30 days ago - tip: started 28 days ago (parent_session_id=root), last msg 5 seconds ago - standalone: last msg 2 days ago Sidebar order with original PR: [0] standalone (48h ago) [1] active_tip (last_activity=root's 720h ago) ← wrong Sidebar order after fix: [0] active_tip (last_activity=tip's 0h ago) ← correct [1] standalone (48h ago) This matches Hermes Agent's own list_sessions_rich projection at hermes_state.py:903-909, which overrides "last_active" from the tip exactly so that the agent CLI's session list orders the same way. Add ``last_activity`` to the merge-from-tip key list, update the existing test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to expect tip-derived updated_at, and add test_compression_chain_bubbles_to_top_by_tip_activity locking in the bubble-to-top invariant — without this regression test the previous behaviour passed CI because no test exercised the sort order against a mixed set of chains and standalone sessions. The chain head's started_at (created_at) and title remain preserved, so users can still find the conversation by its original date and name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.216 release notes and version bump Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix. * chore: gitignore local-only review harness directory Adds .local-review/ to .gitignore so renderer drivers, sample inputs, fixture builders, and other reviewer scratch files do not accidentally get committed. Nothing under that path is ever shared in the repo; keeping the entry tracked makes the boundary explicit for any future contributor who creates the directory locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Keep reasoning chip visible for None effort * test(reasoning): pin chip render output via node, not just source regex The PR's static checks in test_reasoning_chip_btw_fixes.py validate the shape of _applyReasoningChip (no display='none' literal, the right classList.toggle call exists, the right label literals are in the function body) but pass even if the runtime detail is wrong — for example if `inactive` were inverted, _normalizeReasoningEffort mishandled whitespace, or _formatReasoningEffortLabel returned the wrong literal for an unknown input. Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive the actual _applyReasoningChip() via node and assert on the rendered DOM state for each effort value: TestChipAlwaysVisible - empty / null -> "Default" label, inactive=true - "none" -> "None" label, inactive=true - "low"/"high" -> verbatim label, inactive=false TestNormalizationEdgeCases - "NONE" -> normalises to "None" - " none " -> trims and normalises - unknown junk -> falls through visible, never hidden TestTitleAttributeAccessibility - title attribute carries the human-readable label for tooltip / screen-reader use Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught). Against this PR's ui.js: 11/11 pass. This pattern (drive the actual JS via node) caught two regex-only regressions in PR #1073 where the Python mirror was correct while the JS was broken. Same protection added here so the chip-visibility contract can't silently break in a future refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add #1074 to v0.50.216 changelog, bump test count to 2428 * fix(i18n): restore broken Unicode in Russian and Spanish steer strings Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally stripped the `\u` prefix from Unicode escape sequences in two locales, producing garbled literal hex strings visible to users: Spanish (es): - cmd_steer: correcci00f3n → corrección - cmd_steer_fallback: 2014 en cola → — en cola - busy_steer_fallback: 2014 en cola → — en cola - settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección - settings_busy_input_mode_steer: correcci00f3n → corrección Russian (ru): - settings_desc_busy_input_mode: the entire Cyrillic string was replaced with raw 4-hex-char code-points without the \u prefix (041e043f... instead of actual Cyrillic). Decoded: "Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer внедряет коррекцию без прерывания." Fix: write the correct characters directly (UTF-8 is the file encoding so embedding them literally is cleaner than \u escapes for long text). All other locales (en, de, zh, zh-Hant) were not affected — confirmed by grepping for bare hex run-ons in the updated file. Verified: node --check static/i18n.js passes; full pytest suite green (2365 passed, 47 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: remove duplicate compression chain entry from [Unreleased] --------- 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> Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-26 04:06:31 +00:00
const effort=_normalizeReasoningEffort(eff);
_currentReasoningEffort=effort;
const wrap=$('composerReasoningWrap');
const label=$('composerReasoningLabel');
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075) * fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly MIME_MAP was missing entries for .html and .htm. The server fell back to Content-Type: application/octet-stream, which browsers refuse to render as HTML in an iframe — causing a blank white preview. The rest of the pipeline was already correct: the iframe exists in static/index.html, openFile() in static/workspace.js routes .html to showPreview('html'), and _handle_file_raw() in api/routes.py sets the correct CSP sandbox header when ?inline=1 is present. The only missing piece was the MIME type. * test(workspace): lock in MIME_MAP entry for .html/.htm PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py to fix the blank workspace HTML preview iframe. Without a direct assertion on the MIME_MAP entries, the fix could silently regress (the existing test_779_html_preview.py tests cover the iframe wiring, the inline=1 query handling, and the CSP sandbox header — but none of them touch MIME_MAP itself). Add a single regression test that asserts MIME_MAP['.html'] and MIME_MAP['.htm'] are both 'text/html' so any future removal of those entries fails CI immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(composer): raise .approval-card.visible z-index above .queue-card .queue-card has z-index:2. .approval-card.visible had no z-index, so the queue flyout would render on top of the approval card when both were visible simultaneously — obscuring the Allow/Deny buttons. Fix: add z-index:3 to .approval-card.visible so approvals always render above the queue flyout. Approval is a blocking, security-relevant interaction and must never be obscured by passive UI elements. * test(composer): pin approval-card z-index > queue-card invariant PR #1071 raises .approval-card.visible to z-index:3 so the security- relevant Allow / Deny buttons stay clickable when the queue flyout is also open. Without a regression test, a future CSS edit could silently drop the z-index back below queue-card (z-index:2) and reintroduce the bug — there is no automated UI test covering this stacking interaction. Add a focused regex check that pins the invariant: .approval-card.visible z-index must be strictly greater than .queue-card z-index. Modeled on the existing CSS-regex regression style in tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: intercept /steer /interrupt /queue before busy-mode routing in send() Root cause: slash commands entered while the agent is busy never reached the command dispatcher. send() enters the busy block and returns early at line ~50, so the slash-command intercept (~line 56) is never reached. The text was queued as a plain message. When it drained after the turn ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream, and showed "No active task to stop." Fix: at the top of the busy block, before checking busyMode, check if the text starts with / and is one of the three control commands. If so, dispatch the handler immediately and return. This lets the user type /steer, /interrupt, or /queue at any time — including while the agent is mid-stream — and have them execute against the live session. Two new regression tests added: - test_slash_commands_intercepted_before_busymode_routing: verifies the intercept appears before the busyMode routing in the busy block - test_steer_intercept_calls_handler_directly: verifies the intercept calls _bc.fn(_pc.args) and returns, not queues * test(busy-intercept): pin sync input-clear before await in slash intercept PR #1072's intercept clears the msg input before awaiting the handler. Order matters: if the await happens first (or if the clear is moved inside the handler), the input still shows '/steer foo' for the duration of the await. A reflexive second Enter press during that window — common while waiting for the toast — re-runs send(): either re-fires the handler (double-steer) or, if the turn just ended, falls through to the non-busy slash dispatcher and drops a confusing "No active task to stop." Add test_steer_intercept_clears_input_before_await pinning the order so this UX invariant cannot silently regress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: update steer i18n and settings copy — steer no longer interrupts With the real /steer implementation (agent.steer() via /api/chat/steer), steer injects a correction mid-turn WITHOUT interrupting the current stream. The previous copy said "falls back to interrupt", "Steer (interrupt + send)", etc. — accurate only for the old placeholder, not the real implementation. Changes across all 6 locales (en/ru/es/de/zh/zh-Hant): cmd_steer: "falls back to interrupt" removed settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction" cmd_steer_fallback: "interrupted" → "queued for next turn" busy_steer_fallback: "interrupted instead" → "queued for next turn" settings_desc_busy_input_mode: "currently falls back to interrupt" removed Also: static/index.html: inline fallback text updated to match static/commands.js: internal comment clarified (fallback = queue+cancel, not "interrupt mode" which implies the primary action) * fix(renderer): group consecutive blockquote lines into single element Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs: 1. `.+` required at least one character — bare `>` lines (blank continuation lines) did not match and passed through as literal `>` 2. Each matching line became its own `<blockquote>` element — a 10-line blockquote produced 10 stacked `<blockquote>` tags with no grouping 3. When a fenced code block sat inside a blockquote, the fence-stash pass consumed the code content and left orphaned `>` lines that the old `.+` pattern could not match Fix: replace the single-line regex with a group-based approach that matches one or more consecutive `>` lines as a single block, strips the `>` prefix from each line, passes each non-empty line through inlineMd(), turns blank `>` lines into `<br>`, and wraps the entire group in one `<blockquote>`. 14 regression tests added covering: - Single-line blockquotes (regression) - Multi-line grouping (2 and 10 lines) - Two separate blockquotes staying separate - Bare `>` and `>text` (no space) edge cases - Blank continuation lines → <br> - Bold / italic / inline-code inside blockquotes - Blockquote followed by normal paragraph * fix(renderer): drop empty trailing line from blockquote match The new group-based blockquote rule introduced in this PR captures the trailing newline in its (?:\n|$) clause. After block.split('\n') that trailing newline produces an empty final element. The original filter only dropped lone bare '>' artifacts on the last line, so the empty final element survived, and the .map(blank → '<br>') step turned it into a phantom <br> immediately before </blockquote>. Visible symptom: any blockquote whose source ends with \n (the common case — a quote followed by another paragraph or end-of-message) renders with an extra blank line at the bottom of the quote. Reproducer: '> Hello\n\nThe rest of the message.' → '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.' ^^^ phantom <br> Fix: replace the single-line filter with a while-loop that pops trailing lines while they are either empty OR a bare '>'. This matches the intent the Python test mirror in tests/test_blockquote_rendering.py already had (the mirror was correct; the JS was not — that's why the original tests passed despite the bug). Also add four new regression tests in TestNoPhantomTrailingBr that pin the no-trailing-<br> invariant for the common shapes: - input ending with \n - quote followed by paragraph (the real-world case) - multi-line quote ending with \n - quote with blank continuation + trailing \n (internal <br> stays, trailing <br> does not) Verified end-to-end with node against the actual JS regex. 244 renderer-adjacent tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes Five additional fixes on top of the blockquote grouping from the initial commit: 1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows line endings do not produce stray \r characters in rendered output 2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use inside blockquotes/lists) and the outer pass (for plain paragraphs). Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped. 3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐ via task-done/task-todo span wrappers. Checks [X] (uppercase) too. 4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its own <blockquote> element rather than passing through as literal >. Implemented by extracting the blockquote rule into _applyBlockquotes() which calls itself recursively on the stripped inner content. 5. Lists inside blockquotes: > - item now renders <ul><li> inside the blockquote instead of a literal "- item" string. Task list items work inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>). Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF strip at the top of renderMd pushed the autolink regex past the old limit. 68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py covering all constructs, edge cases, and combinations. * fix(renderer): restore space in blockquote prefix-strip regex Commit 04e7b53 changed the blockquote prefix-strip regex from /^>[ \t]?/ (consume "> ", "\t>", or just ">") to /^>[\t]?/ (only consume "\t>" or just ">") The space character was dropped from the character class. Since practically every blockquote an LLM produces is "> " (greater-than followed by a space), this leaves a leading space artifact on every stripped blockquote line. Worse, the leading space breaks the list-detection regex `^(?: )?[-*+] ` inside the new `_applyBlockquotes` helper — that regex requires either zero or two leading spaces, never one — so the new "list inside blockquote" feature never fired for the canonical input shape `> - item`. Reproducer (against the actual ui.js via node, before the fix): > Hello world → <blockquote> Hello world</blockquote> ^ phantom leading space > Steps: → <blockquote>Steps: > - one - one > - two - two</blockquote> ^ literal text, NOT a <ul>; lists-in-quote feature broken > - [x] done → blockquote with literal "[x] done", no checkbox span Tests passed despite the bug because tests/test_blockquote_rendering.py and tests/test_renderer_comprehensive.py validate against a Python mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e. the mirror is correct, the JS is not, and the static-mirror tests can't catch the divergence. Same shape of bug as commit 94d63d0 (phantom <br> in trailing line) where the mirror was right and the JS was wrong. Fix: restore the space character in the strip regex's character class. Add tests/test_renderer_js_behaviour.py — 11 tests that drive the ACTUAL renderMd via node and assert on rendered output for the most common LLM shapes (single-line quote, multi-line quote, list inside quote, task list inside quote, nested >>>, strikethrough inside and outside quote, top-level task list, quote followed by heading, multi-paragraph quote with list, CRLF normalisation). Verified: the buggy regex makes 6 of those 11 tests fail; the corrected regex makes all 11 pass. Suite: 2354 passed, 0 new failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Collapse agent session compression chains * Restore upstream changelog entries * fix(agent_sessions): bubble active compression chains to top by tip last_activity The original PR merge kept the chain head's id/title/started_at and overrode id/model/message_count/ended_at/end_reason from the tip — but did NOT override last_activity. Since the projected list is sorted by last_activity DESC and the WebUI sidebar surfaces updated_at = last_activity, an actively-used compression chain whose tip is being edited NOW would sort by the ROOT's old last_activity and fall below recently touched standalone sessions. Reproducer (with the harness against actual code, before the fix): - root: started 30 days ago, last msg 30 days ago - tip: started 28 days ago (parent_session_id=root), last msg 5 seconds ago - standalone: last msg 2 days ago Sidebar order with original PR: [0] standalone (48h ago) [1] active_tip (last_activity=root's 720h ago) ← wrong Sidebar order after fix: [0] active_tip (last_activity=tip's 0h ago) ← correct [1] standalone (48h ago) This matches Hermes Agent's own list_sessions_rich projection at hermes_state.py:903-909, which overrides "last_active" from the tip exactly so that the agent CLI's session list orders the same way. Add ``last_activity`` to the merge-from-tip key list, update the existing test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to expect tip-derived updated_at, and add test_compression_chain_bubbles_to_top_by_tip_activity locking in the bubble-to-top invariant — without this regression test the previous behaviour passed CI because no test exercised the sort order against a mixed set of chains and standalone sessions. The chain head's started_at (created_at) and title remain preserved, so users can still find the conversation by its original date and name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.216 release notes and version bump Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix. * chore: gitignore local-only review harness directory Adds .local-review/ to .gitignore so renderer drivers, sample inputs, fixture builders, and other reviewer scratch files do not accidentally get committed. Nothing under that path is ever shared in the repo; keeping the entry tracked makes the boundary explicit for any future contributor who creates the directory locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Keep reasoning chip visible for None effort * test(reasoning): pin chip render output via node, not just source regex The PR's static checks in test_reasoning_chip_btw_fixes.py validate the shape of _applyReasoningChip (no display='none' literal, the right classList.toggle call exists, the right label literals are in the function body) but pass even if the runtime detail is wrong — for example if `inactive` were inverted, _normalizeReasoningEffort mishandled whitespace, or _formatReasoningEffortLabel returned the wrong literal for an unknown input. Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive the actual _applyReasoningChip() via node and assert on the rendered DOM state for each effort value: TestChipAlwaysVisible - empty / null -> "Default" label, inactive=true - "none" -> "None" label, inactive=true - "low"/"high" -> verbatim label, inactive=false TestNormalizationEdgeCases - "NONE" -> normalises to "None" - " none " -> trims and normalises - unknown junk -> falls through visible, never hidden TestTitleAttributeAccessibility - title attribute carries the human-readable label for tooltip / screen-reader use Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught). Against this PR's ui.js: 11/11 pass. This pattern (drive the actual JS via node) caught two regex-only regressions in PR #1073 where the Python mirror was correct while the JS was broken. Same protection added here so the chip-visibility contract can't silently break in a future refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add #1074 to v0.50.216 changelog, bump test count to 2428 * fix(i18n): restore broken Unicode in Russian and Spanish steer strings Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally stripped the `\u` prefix from Unicode escape sequences in two locales, producing garbled literal hex strings visible to users: Spanish (es): - cmd_steer: correcci00f3n → corrección - cmd_steer_fallback: 2014 en cola → — en cola - busy_steer_fallback: 2014 en cola → — en cola - settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección - settings_busy_input_mode_steer: correcci00f3n → corrección Russian (ru): - settings_desc_busy_input_mode: the entire Cyrillic string was replaced with raw 4-hex-char code-points without the \u prefix (041e043f... instead of actual Cyrillic). Decoded: "Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer внедряет коррекцию без прерывания." Fix: write the correct characters directly (UTF-8 is the file encoding so embedding them literally is cleaner than \u escapes for long text). All other locales (en, de, zh, zh-Hant) were not affected — confirmed by grepping for bare hex run-ons in the updated file. Verified: node --check static/i18n.js passes; full pytest suite green (2365 passed, 47 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: remove duplicate compression chain entry from [Unreleased] --------- 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> Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-26 04:06:31 +00:00
const chip=$('composerReasoningChip');
const mobileLabel=$('composerMobileReasoningLabel');
const mobileAction=$('composerMobileReasoningAction');
if(!wrap||!label) return;
wrap.style.display='';
if(mobileAction) mobileAction.style.display='';
const text=_formatReasoningEffortLabel(effort);
label.textContent=text;
if(mobileLabel) mobileLabel.textContent=text;
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075) * fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly MIME_MAP was missing entries for .html and .htm. The server fell back to Content-Type: application/octet-stream, which browsers refuse to render as HTML in an iframe — causing a blank white preview. The rest of the pipeline was already correct: the iframe exists in static/index.html, openFile() in static/workspace.js routes .html to showPreview('html'), and _handle_file_raw() in api/routes.py sets the correct CSP sandbox header when ?inline=1 is present. The only missing piece was the MIME type. * test(workspace): lock in MIME_MAP entry for .html/.htm PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py to fix the blank workspace HTML preview iframe. Without a direct assertion on the MIME_MAP entries, the fix could silently regress (the existing test_779_html_preview.py tests cover the iframe wiring, the inline=1 query handling, and the CSP sandbox header — but none of them touch MIME_MAP itself). Add a single regression test that asserts MIME_MAP['.html'] and MIME_MAP['.htm'] are both 'text/html' so any future removal of those entries fails CI immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(composer): raise .approval-card.visible z-index above .queue-card .queue-card has z-index:2. .approval-card.visible had no z-index, so the queue flyout would render on top of the approval card when both were visible simultaneously — obscuring the Allow/Deny buttons. Fix: add z-index:3 to .approval-card.visible so approvals always render above the queue flyout. Approval is a blocking, security-relevant interaction and must never be obscured by passive UI elements. * test(composer): pin approval-card z-index > queue-card invariant PR #1071 raises .approval-card.visible to z-index:3 so the security- relevant Allow / Deny buttons stay clickable when the queue flyout is also open. Without a regression test, a future CSS edit could silently drop the z-index back below queue-card (z-index:2) and reintroduce the bug — there is no automated UI test covering this stacking interaction. Add a focused regex check that pins the invariant: .approval-card.visible z-index must be strictly greater than .queue-card z-index. Modeled on the existing CSS-regex regression style in tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: intercept /steer /interrupt /queue before busy-mode routing in send() Root cause: slash commands entered while the agent is busy never reached the command dispatcher. send() enters the busy block and returns early at line ~50, so the slash-command intercept (~line 56) is never reached. The text was queued as a plain message. When it drained after the turn ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream, and showed "No active task to stop." Fix: at the top of the busy block, before checking busyMode, check if the text starts with / and is one of the three control commands. If so, dispatch the handler immediately and return. This lets the user type /steer, /interrupt, or /queue at any time — including while the agent is mid-stream — and have them execute against the live session. Two new regression tests added: - test_slash_commands_intercepted_before_busymode_routing: verifies the intercept appears before the busyMode routing in the busy block - test_steer_intercept_calls_handler_directly: verifies the intercept calls _bc.fn(_pc.args) and returns, not queues * test(busy-intercept): pin sync input-clear before await in slash intercept PR #1072's intercept clears the msg input before awaiting the handler. Order matters: if the await happens first (or if the clear is moved inside the handler), the input still shows '/steer foo' for the duration of the await. A reflexive second Enter press during that window — common while waiting for the toast — re-runs send(): either re-fires the handler (double-steer) or, if the turn just ended, falls through to the non-busy slash dispatcher and drops a confusing "No active task to stop." Add test_steer_intercept_clears_input_before_await pinning the order so this UX invariant cannot silently regress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: update steer i18n and settings copy — steer no longer interrupts With the real /steer implementation (agent.steer() via /api/chat/steer), steer injects a correction mid-turn WITHOUT interrupting the current stream. The previous copy said "falls back to interrupt", "Steer (interrupt + send)", etc. — accurate only for the old placeholder, not the real implementation. Changes across all 6 locales (en/ru/es/de/zh/zh-Hant): cmd_steer: "falls back to interrupt" removed settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction" cmd_steer_fallback: "interrupted" → "queued for next turn" busy_steer_fallback: "interrupted instead" → "queued for next turn" settings_desc_busy_input_mode: "currently falls back to interrupt" removed Also: static/index.html: inline fallback text updated to match static/commands.js: internal comment clarified (fallback = queue+cancel, not "interrupt mode" which implies the primary action) * fix(renderer): group consecutive blockquote lines into single element Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs: 1. `.+` required at least one character — bare `>` lines (blank continuation lines) did not match and passed through as literal `>` 2. Each matching line became its own `<blockquote>` element — a 10-line blockquote produced 10 stacked `<blockquote>` tags with no grouping 3. When a fenced code block sat inside a blockquote, the fence-stash pass consumed the code content and left orphaned `>` lines that the old `.+` pattern could not match Fix: replace the single-line regex with a group-based approach that matches one or more consecutive `>` lines as a single block, strips the `>` prefix from each line, passes each non-empty line through inlineMd(), turns blank `>` lines into `<br>`, and wraps the entire group in one `<blockquote>`. 14 regression tests added covering: - Single-line blockquotes (regression) - Multi-line grouping (2 and 10 lines) - Two separate blockquotes staying separate - Bare `>` and `>text` (no space) edge cases - Blank continuation lines → <br> - Bold / italic / inline-code inside blockquotes - Blockquote followed by normal paragraph * fix(renderer): drop empty trailing line from blockquote match The new group-based blockquote rule introduced in this PR captures the trailing newline in its (?:\n|$) clause. After block.split('\n') that trailing newline produces an empty final element. The original filter only dropped lone bare '>' artifacts on the last line, so the empty final element survived, and the .map(blank → '<br>') step turned it into a phantom <br> immediately before </blockquote>. Visible symptom: any blockquote whose source ends with \n (the common case — a quote followed by another paragraph or end-of-message) renders with an extra blank line at the bottom of the quote. Reproducer: '> Hello\n\nThe rest of the message.' → '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.' ^^^ phantom <br> Fix: replace the single-line filter with a while-loop that pops trailing lines while they are either empty OR a bare '>'. This matches the intent the Python test mirror in tests/test_blockquote_rendering.py already had (the mirror was correct; the JS was not — that's why the original tests passed despite the bug). Also add four new regression tests in TestNoPhantomTrailingBr that pin the no-trailing-<br> invariant for the common shapes: - input ending with \n - quote followed by paragraph (the real-world case) - multi-line quote ending with \n - quote with blank continuation + trailing \n (internal <br> stays, trailing <br> does not) Verified end-to-end with node against the actual JS regex. 244 renderer-adjacent tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes Five additional fixes on top of the blockquote grouping from the initial commit: 1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows line endings do not produce stray \r characters in rendered output 2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use inside blockquotes/lists) and the outer pass (for plain paragraphs). Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped. 3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐ via task-done/task-todo span wrappers. Checks [X] (uppercase) too. 4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its own <blockquote> element rather than passing through as literal >. Implemented by extracting the blockquote rule into _applyBlockquotes() which calls itself recursively on the stripped inner content. 5. Lists inside blockquotes: > - item now renders <ul><li> inside the blockquote instead of a literal "- item" string. Task list items work inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>). Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF strip at the top of renderMd pushed the autolink regex past the old limit. 68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py covering all constructs, edge cases, and combinations. * fix(renderer): restore space in blockquote prefix-strip regex Commit 04e7b53 changed the blockquote prefix-strip regex from /^>[ \t]?/ (consume "> ", "\t>", or just ">") to /^>[\t]?/ (only consume "\t>" or just ">") The space character was dropped from the character class. Since practically every blockquote an LLM produces is "> " (greater-than followed by a space), this leaves a leading space artifact on every stripped blockquote line. Worse, the leading space breaks the list-detection regex `^(?: )?[-*+] ` inside the new `_applyBlockquotes` helper — that regex requires either zero or two leading spaces, never one — so the new "list inside blockquote" feature never fired for the canonical input shape `> - item`. Reproducer (against the actual ui.js via node, before the fix): > Hello world → <blockquote> Hello world</blockquote> ^ phantom leading space > Steps: → <blockquote>Steps: > - one - one > - two - two</blockquote> ^ literal text, NOT a <ul>; lists-in-quote feature broken > - [x] done → blockquote with literal "[x] done", no checkbox span Tests passed despite the bug because tests/test_blockquote_rendering.py and tests/test_renderer_comprehensive.py validate against a Python mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e. the mirror is correct, the JS is not, and the static-mirror tests can't catch the divergence. Same shape of bug as commit 94d63d0 (phantom <br> in trailing line) where the mirror was right and the JS was wrong. Fix: restore the space character in the strip regex's character class. Add tests/test_renderer_js_behaviour.py — 11 tests that drive the ACTUAL renderMd via node and assert on rendered output for the most common LLM shapes (single-line quote, multi-line quote, list inside quote, task list inside quote, nested >>>, strikethrough inside and outside quote, top-level task list, quote followed by heading, multi-paragraph quote with list, CRLF normalisation). Verified: the buggy regex makes 6 of those 11 tests fail; the corrected regex makes all 11 pass. Suite: 2354 passed, 0 new failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Collapse agent session compression chains * Restore upstream changelog entries * fix(agent_sessions): bubble active compression chains to top by tip last_activity The original PR merge kept the chain head's id/title/started_at and overrode id/model/message_count/ended_at/end_reason from the tip — but did NOT override last_activity. Since the projected list is sorted by last_activity DESC and the WebUI sidebar surfaces updated_at = last_activity, an actively-used compression chain whose tip is being edited NOW would sort by the ROOT's old last_activity and fall below recently touched standalone sessions. Reproducer (with the harness against actual code, before the fix): - root: started 30 days ago, last msg 30 days ago - tip: started 28 days ago (parent_session_id=root), last msg 5 seconds ago - standalone: last msg 2 days ago Sidebar order with original PR: [0] standalone (48h ago) [1] active_tip (last_activity=root's 720h ago) ← wrong Sidebar order after fix: [0] active_tip (last_activity=tip's 0h ago) ← correct [1] standalone (48h ago) This matches Hermes Agent's own list_sessions_rich projection at hermes_state.py:903-909, which overrides "last_active" from the tip exactly so that the agent CLI's session list orders the same way. Add ``last_activity`` to the merge-from-tip key list, update the existing test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to expect tip-derived updated_at, and add test_compression_chain_bubbles_to_top_by_tip_activity locking in the bubble-to-top invariant — without this regression test the previous behaviour passed CI because no test exercised the sort order against a mixed set of chains and standalone sessions. The chain head's started_at (created_at) and title remain preserved, so users can still find the conversation by its original date and name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.216 release notes and version bump Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix. * chore: gitignore local-only review harness directory Adds .local-review/ to .gitignore so renderer drivers, sample inputs, fixture builders, and other reviewer scratch files do not accidentally get committed. Nothing under that path is ever shared in the repo; keeping the entry tracked makes the boundary explicit for any future contributor who creates the directory locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Keep reasoning chip visible for None effort * test(reasoning): pin chip render output via node, not just source regex The PR's static checks in test_reasoning_chip_btw_fixes.py validate the shape of _applyReasoningChip (no display='none' literal, the right classList.toggle call exists, the right label literals are in the function body) but pass even if the runtime detail is wrong — for example if `inactive` were inverted, _normalizeReasoningEffort mishandled whitespace, or _formatReasoningEffortLabel returned the wrong literal for an unknown input. Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive the actual _applyReasoningChip() via node and assert on the rendered DOM state for each effort value: TestChipAlwaysVisible - empty / null -> "Default" label, inactive=true - "none" -> "None" label, inactive=true - "low"/"high" -> verbatim label, inactive=false TestNormalizationEdgeCases - "NONE" -> normalises to "None" - " none " -> trims and normalises - unknown junk -> falls through visible, never hidden TestTitleAttributeAccessibility - title attribute carries the human-readable label for tooltip / screen-reader use Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught). Against this PR's ui.js: 11/11 pass. This pattern (drive the actual JS via node) caught two regex-only regressions in PR #1073 where the Python mirror was correct while the JS was broken. Same protection added here so the chip-visibility contract can't silently break in a future refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add #1074 to v0.50.216 changelog, bump test count to 2428 * fix(i18n): restore broken Unicode in Russian and Spanish steer strings Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally stripped the `\u` prefix from Unicode escape sequences in two locales, producing garbled literal hex strings visible to users: Spanish (es): - cmd_steer: correcci00f3n → corrección - cmd_steer_fallback: 2014 en cola → — en cola - busy_steer_fallback: 2014 en cola → — en cola - settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección - settings_busy_input_mode_steer: correcci00f3n → corrección Russian (ru): - settings_desc_busy_input_mode: the entire Cyrillic string was replaced with raw 4-hex-char code-points without the \u prefix (041e043f... instead of actual Cyrillic). Decoded: "Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer внедряет коррекцию без прерывания." Fix: write the correct characters directly (UTF-8 is the file encoding so embedding them literally is cleaner than \u escapes for long text). All other locales (en, de, zh, zh-Hant) were not affected — confirmed by grepping for bare hex run-ons in the updated file. Verified: node --check static/i18n.js passes; full pytest suite green (2365 passed, 47 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: remove duplicate compression chain entry from [Unreleased] --------- 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> Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-26 04:06:31 +00:00
if(chip){
const inactive=!effort||effort==='none';
chip.classList.toggle('inactive',inactive);
chip.title='Reasoning effort: '+text;
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075) * fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly MIME_MAP was missing entries for .html and .htm. The server fell back to Content-Type: application/octet-stream, which browsers refuse to render as HTML in an iframe — causing a blank white preview. The rest of the pipeline was already correct: the iframe exists in static/index.html, openFile() in static/workspace.js routes .html to showPreview('html'), and _handle_file_raw() in api/routes.py sets the correct CSP sandbox header when ?inline=1 is present. The only missing piece was the MIME type. * test(workspace): lock in MIME_MAP entry for .html/.htm PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py to fix the blank workspace HTML preview iframe. Without a direct assertion on the MIME_MAP entries, the fix could silently regress (the existing test_779_html_preview.py tests cover the iframe wiring, the inline=1 query handling, and the CSP sandbox header — but none of them touch MIME_MAP itself). Add a single regression test that asserts MIME_MAP['.html'] and MIME_MAP['.htm'] are both 'text/html' so any future removal of those entries fails CI immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(composer): raise .approval-card.visible z-index above .queue-card .queue-card has z-index:2. .approval-card.visible had no z-index, so the queue flyout would render on top of the approval card when both were visible simultaneously — obscuring the Allow/Deny buttons. Fix: add z-index:3 to .approval-card.visible so approvals always render above the queue flyout. Approval is a blocking, security-relevant interaction and must never be obscured by passive UI elements. * test(composer): pin approval-card z-index > queue-card invariant PR #1071 raises .approval-card.visible to z-index:3 so the security- relevant Allow / Deny buttons stay clickable when the queue flyout is also open. Without a regression test, a future CSS edit could silently drop the z-index back below queue-card (z-index:2) and reintroduce the bug — there is no automated UI test covering this stacking interaction. Add a focused regex check that pins the invariant: .approval-card.visible z-index must be strictly greater than .queue-card z-index. Modeled on the existing CSS-regex regression style in tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: intercept /steer /interrupt /queue before busy-mode routing in send() Root cause: slash commands entered while the agent is busy never reached the command dispatcher. send() enters the busy block and returns early at line ~50, so the slash-command intercept (~line 56) is never reached. The text was queued as a plain message. When it drained after the turn ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream, and showed "No active task to stop." Fix: at the top of the busy block, before checking busyMode, check if the text starts with / and is one of the three control commands. If so, dispatch the handler immediately and return. This lets the user type /steer, /interrupt, or /queue at any time — including while the agent is mid-stream — and have them execute against the live session. Two new regression tests added: - test_slash_commands_intercepted_before_busymode_routing: verifies the intercept appears before the busyMode routing in the busy block - test_steer_intercept_calls_handler_directly: verifies the intercept calls _bc.fn(_pc.args) and returns, not queues * test(busy-intercept): pin sync input-clear before await in slash intercept PR #1072's intercept clears the msg input before awaiting the handler. Order matters: if the await happens first (or if the clear is moved inside the handler), the input still shows '/steer foo' for the duration of the await. A reflexive second Enter press during that window — common while waiting for the toast — re-runs send(): either re-fires the handler (double-steer) or, if the turn just ended, falls through to the non-busy slash dispatcher and drops a confusing "No active task to stop." Add test_steer_intercept_clears_input_before_await pinning the order so this UX invariant cannot silently regress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: update steer i18n and settings copy — steer no longer interrupts With the real /steer implementation (agent.steer() via /api/chat/steer), steer injects a correction mid-turn WITHOUT interrupting the current stream. The previous copy said "falls back to interrupt", "Steer (interrupt + send)", etc. — accurate only for the old placeholder, not the real implementation. Changes across all 6 locales (en/ru/es/de/zh/zh-Hant): cmd_steer: "falls back to interrupt" removed settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction" cmd_steer_fallback: "interrupted" → "queued for next turn" busy_steer_fallback: "interrupted instead" → "queued for next turn" settings_desc_busy_input_mode: "currently falls back to interrupt" removed Also: static/index.html: inline fallback text updated to match static/commands.js: internal comment clarified (fallback = queue+cancel, not "interrupt mode" which implies the primary action) * fix(renderer): group consecutive blockquote lines into single element Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs: 1. `.+` required at least one character — bare `>` lines (blank continuation lines) did not match and passed through as literal `>` 2. Each matching line became its own `<blockquote>` element — a 10-line blockquote produced 10 stacked `<blockquote>` tags with no grouping 3. When a fenced code block sat inside a blockquote, the fence-stash pass consumed the code content and left orphaned `>` lines that the old `.+` pattern could not match Fix: replace the single-line regex with a group-based approach that matches one or more consecutive `>` lines as a single block, strips the `>` prefix from each line, passes each non-empty line through inlineMd(), turns blank `>` lines into `<br>`, and wraps the entire group in one `<blockquote>`. 14 regression tests added covering: - Single-line blockquotes (regression) - Multi-line grouping (2 and 10 lines) - Two separate blockquotes staying separate - Bare `>` and `>text` (no space) edge cases - Blank continuation lines → <br> - Bold / italic / inline-code inside blockquotes - Blockquote followed by normal paragraph * fix(renderer): drop empty trailing line from blockquote match The new group-based blockquote rule introduced in this PR captures the trailing newline in its (?:\n|$) clause. After block.split('\n') that trailing newline produces an empty final element. The original filter only dropped lone bare '>' artifacts on the last line, so the empty final element survived, and the .map(blank → '<br>') step turned it into a phantom <br> immediately before </blockquote>. Visible symptom: any blockquote whose source ends with \n (the common case — a quote followed by another paragraph or end-of-message) renders with an extra blank line at the bottom of the quote. Reproducer: '> Hello\n\nThe rest of the message.' → '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.' ^^^ phantom <br> Fix: replace the single-line filter with a while-loop that pops trailing lines while they are either empty OR a bare '>'. This matches the intent the Python test mirror in tests/test_blockquote_rendering.py already had (the mirror was correct; the JS was not — that's why the original tests passed despite the bug). Also add four new regression tests in TestNoPhantomTrailingBr that pin the no-trailing-<br> invariant for the common shapes: - input ending with \n - quote followed by paragraph (the real-world case) - multi-line quote ending with \n - quote with blank continuation + trailing \n (internal <br> stays, trailing <br> does not) Verified end-to-end with node against the actual JS regex. 244 renderer-adjacent tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes Five additional fixes on top of the blockquote grouping from the initial commit: 1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows line endings do not produce stray \r characters in rendered output 2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use inside blockquotes/lists) and the outer pass (for plain paragraphs). Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped. 3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐ via task-done/task-todo span wrappers. Checks [X] (uppercase) too. 4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its own <blockquote> element rather than passing through as literal >. Implemented by extracting the blockquote rule into _applyBlockquotes() which calls itself recursively on the stripped inner content. 5. Lists inside blockquotes: > - item now renders <ul><li> inside the blockquote instead of a literal "- item" string. Task list items work inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>). Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF strip at the top of renderMd pushed the autolink regex past the old limit. 68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py covering all constructs, edge cases, and combinations. * fix(renderer): restore space in blockquote prefix-strip regex Commit 04e7b53 changed the blockquote prefix-strip regex from /^>[ \t]?/ (consume "> ", "\t>", or just ">") to /^>[\t]?/ (only consume "\t>" or just ">") The space character was dropped from the character class. Since practically every blockquote an LLM produces is "> " (greater-than followed by a space), this leaves a leading space artifact on every stripped blockquote line. Worse, the leading space breaks the list-detection regex `^(?: )?[-*+] ` inside the new `_applyBlockquotes` helper — that regex requires either zero or two leading spaces, never one — so the new "list inside blockquote" feature never fired for the canonical input shape `> - item`. Reproducer (against the actual ui.js via node, before the fix): > Hello world → <blockquote> Hello world</blockquote> ^ phantom leading space > Steps: → <blockquote>Steps: > - one - one > - two - two</blockquote> ^ literal text, NOT a <ul>; lists-in-quote feature broken > - [x] done → blockquote with literal "[x] done", no checkbox span Tests passed despite the bug because tests/test_blockquote_rendering.py and tests/test_renderer_comprehensive.py validate against a Python mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e. the mirror is correct, the JS is not, and the static-mirror tests can't catch the divergence. Same shape of bug as commit 94d63d0 (phantom <br> in trailing line) where the mirror was right and the JS was wrong. Fix: restore the space character in the strip regex's character class. Add tests/test_renderer_js_behaviour.py — 11 tests that drive the ACTUAL renderMd via node and assert on rendered output for the most common LLM shapes (single-line quote, multi-line quote, list inside quote, task list inside quote, nested >>>, strikethrough inside and outside quote, top-level task list, quote followed by heading, multi-paragraph quote with list, CRLF normalisation). Verified: the buggy regex makes 6 of those 11 tests fail; the corrected regex makes all 11 pass. Suite: 2354 passed, 0 new failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Collapse agent session compression chains * Restore upstream changelog entries * fix(agent_sessions): bubble active compression chains to top by tip last_activity The original PR merge kept the chain head's id/title/started_at and overrode id/model/message_count/ended_at/end_reason from the tip — but did NOT override last_activity. Since the projected list is sorted by last_activity DESC and the WebUI sidebar surfaces updated_at = last_activity, an actively-used compression chain whose tip is being edited NOW would sort by the ROOT's old last_activity and fall below recently touched standalone sessions. Reproducer (with the harness against actual code, before the fix): - root: started 30 days ago, last msg 30 days ago - tip: started 28 days ago (parent_session_id=root), last msg 5 seconds ago - standalone: last msg 2 days ago Sidebar order with original PR: [0] standalone (48h ago) [1] active_tip (last_activity=root's 720h ago) ← wrong Sidebar order after fix: [0] active_tip (last_activity=tip's 0h ago) ← correct [1] standalone (48h ago) This matches Hermes Agent's own list_sessions_rich projection at hermes_state.py:903-909, which overrides "last_active" from the tip exactly so that the agent CLI's session list orders the same way. Add ``last_activity`` to the merge-from-tip key list, update the existing test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to expect tip-derived updated_at, and add test_compression_chain_bubbles_to_top_by_tip_activity locking in the bubble-to-top invariant — without this regression test the previous behaviour passed CI because no test exercised the sort order against a mixed set of chains and standalone sessions. The chain head's started_at (created_at) and title remain preserved, so users can still find the conversation by its original date and name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.216 release notes and version bump Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix. * chore: gitignore local-only review harness directory Adds .local-review/ to .gitignore so renderer drivers, sample inputs, fixture builders, and other reviewer scratch files do not accidentally get committed. Nothing under that path is ever shared in the repo; keeping the entry tracked makes the boundary explicit for any future contributor who creates the directory locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Keep reasoning chip visible for None effort * test(reasoning): pin chip render output via node, not just source regex The PR's static checks in test_reasoning_chip_btw_fixes.py validate the shape of _applyReasoningChip (no display='none' literal, the right classList.toggle call exists, the right label literals are in the function body) but pass even if the runtime detail is wrong — for example if `inactive` were inverted, _normalizeReasoningEffort mishandled whitespace, or _formatReasoningEffortLabel returned the wrong literal for an unknown input. Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive the actual _applyReasoningChip() via node and assert on the rendered DOM state for each effort value: TestChipAlwaysVisible - empty / null -> "Default" label, inactive=true - "none" -> "None" label, inactive=true - "low"/"high" -> verbatim label, inactive=false TestNormalizationEdgeCases - "NONE" -> normalises to "None" - " none " -> trims and normalises - unknown junk -> falls through visible, never hidden TestTitleAttributeAccessibility - title attribute carries the human-readable label for tooltip / screen-reader use Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught). Against this PR's ui.js: 11/11 pass. This pattern (drive the actual JS via node) caught two regex-only regressions in PR #1073 where the Python mirror was correct while the JS was broken. Same protection added here so the chip-visibility contract can't silently break in a future refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add #1074 to v0.50.216 changelog, bump test count to 2428 * fix(i18n): restore broken Unicode in Russian and Spanish steer strings Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally stripped the `\u` prefix from Unicode escape sequences in two locales, producing garbled literal hex strings visible to users: Spanish (es): - cmd_steer: correcci00f3n → corrección - cmd_steer_fallback: 2014 en cola → — en cola - busy_steer_fallback: 2014 en cola → — en cola - settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección - settings_busy_input_mode_steer: correcci00f3n → corrección Russian (ru): - settings_desc_busy_input_mode: the entire Cyrillic string was replaced with raw 4-hex-char code-points without the \u prefix (041e043f... instead of actual Cyrillic). Decoded: "Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer внедряет коррекцию без прерывания." Fix: write the correct characters directly (UTF-8 is the file encoding so embedding them literally is cleaner than \u escapes for long text). All other locales (en, de, zh, zh-Hant) were not affected — confirmed by grepping for bare hex run-ons in the updated file. Verified: node --check static/i18n.js passes; full pytest suite green (2365 passed, 47 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: remove duplicate compression chain entry from [Unreleased] --------- 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> Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-26 04:06:31 +00:00
}
if(mobileAction) mobileAction.classList.toggle('inactive',!effort||effort==='none');
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075) * fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly MIME_MAP was missing entries for .html and .htm. The server fell back to Content-Type: application/octet-stream, which browsers refuse to render as HTML in an iframe — causing a blank white preview. The rest of the pipeline was already correct: the iframe exists in static/index.html, openFile() in static/workspace.js routes .html to showPreview('html'), and _handle_file_raw() in api/routes.py sets the correct CSP sandbox header when ?inline=1 is present. The only missing piece was the MIME type. * test(workspace): lock in MIME_MAP entry for .html/.htm PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py to fix the blank workspace HTML preview iframe. Without a direct assertion on the MIME_MAP entries, the fix could silently regress (the existing test_779_html_preview.py tests cover the iframe wiring, the inline=1 query handling, and the CSP sandbox header — but none of them touch MIME_MAP itself). Add a single regression test that asserts MIME_MAP['.html'] and MIME_MAP['.htm'] are both 'text/html' so any future removal of those entries fails CI immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(composer): raise .approval-card.visible z-index above .queue-card .queue-card has z-index:2. .approval-card.visible had no z-index, so the queue flyout would render on top of the approval card when both were visible simultaneously — obscuring the Allow/Deny buttons. Fix: add z-index:3 to .approval-card.visible so approvals always render above the queue flyout. Approval is a blocking, security-relevant interaction and must never be obscured by passive UI elements. * test(composer): pin approval-card z-index > queue-card invariant PR #1071 raises .approval-card.visible to z-index:3 so the security- relevant Allow / Deny buttons stay clickable when the queue flyout is also open. Without a regression test, a future CSS edit could silently drop the z-index back below queue-card (z-index:2) and reintroduce the bug — there is no automated UI test covering this stacking interaction. Add a focused regex check that pins the invariant: .approval-card.visible z-index must be strictly greater than .queue-card z-index. Modeled on the existing CSS-regex regression style in tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: intercept /steer /interrupt /queue before busy-mode routing in send() Root cause: slash commands entered while the agent is busy never reached the command dispatcher. send() enters the busy block and returns early at line ~50, so the slash-command intercept (~line 56) is never reached. The text was queued as a plain message. When it drained after the turn ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream, and showed "No active task to stop." Fix: at the top of the busy block, before checking busyMode, check if the text starts with / and is one of the three control commands. If so, dispatch the handler immediately and return. This lets the user type /steer, /interrupt, or /queue at any time — including while the agent is mid-stream — and have them execute against the live session. Two new regression tests added: - test_slash_commands_intercepted_before_busymode_routing: verifies the intercept appears before the busyMode routing in the busy block - test_steer_intercept_calls_handler_directly: verifies the intercept calls _bc.fn(_pc.args) and returns, not queues * test(busy-intercept): pin sync input-clear before await in slash intercept PR #1072's intercept clears the msg input before awaiting the handler. Order matters: if the await happens first (or if the clear is moved inside the handler), the input still shows '/steer foo' for the duration of the await. A reflexive second Enter press during that window — common while waiting for the toast — re-runs send(): either re-fires the handler (double-steer) or, if the turn just ended, falls through to the non-busy slash dispatcher and drops a confusing "No active task to stop." Add test_steer_intercept_clears_input_before_await pinning the order so this UX invariant cannot silently regress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: update steer i18n and settings copy — steer no longer interrupts With the real /steer implementation (agent.steer() via /api/chat/steer), steer injects a correction mid-turn WITHOUT interrupting the current stream. The previous copy said "falls back to interrupt", "Steer (interrupt + send)", etc. — accurate only for the old placeholder, not the real implementation. Changes across all 6 locales (en/ru/es/de/zh/zh-Hant): cmd_steer: "falls back to interrupt" removed settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction" cmd_steer_fallback: "interrupted" → "queued for next turn" busy_steer_fallback: "interrupted instead" → "queued for next turn" settings_desc_busy_input_mode: "currently falls back to interrupt" removed Also: static/index.html: inline fallback text updated to match static/commands.js: internal comment clarified (fallback = queue+cancel, not "interrupt mode" which implies the primary action) * fix(renderer): group consecutive blockquote lines into single element Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs: 1. `.+` required at least one character — bare `>` lines (blank continuation lines) did not match and passed through as literal `>` 2. Each matching line became its own `<blockquote>` element — a 10-line blockquote produced 10 stacked `<blockquote>` tags with no grouping 3. When a fenced code block sat inside a blockquote, the fence-stash pass consumed the code content and left orphaned `>` lines that the old `.+` pattern could not match Fix: replace the single-line regex with a group-based approach that matches one or more consecutive `>` lines as a single block, strips the `>` prefix from each line, passes each non-empty line through inlineMd(), turns blank `>` lines into `<br>`, and wraps the entire group in one `<blockquote>`. 14 regression tests added covering: - Single-line blockquotes (regression) - Multi-line grouping (2 and 10 lines) - Two separate blockquotes staying separate - Bare `>` and `>text` (no space) edge cases - Blank continuation lines → <br> - Bold / italic / inline-code inside blockquotes - Blockquote followed by normal paragraph * fix(renderer): drop empty trailing line from blockquote match The new group-based blockquote rule introduced in this PR captures the trailing newline in its (?:\n|$) clause. After block.split('\n') that trailing newline produces an empty final element. The original filter only dropped lone bare '>' artifacts on the last line, so the empty final element survived, and the .map(blank → '<br>') step turned it into a phantom <br> immediately before </blockquote>. Visible symptom: any blockquote whose source ends with \n (the common case — a quote followed by another paragraph or end-of-message) renders with an extra blank line at the bottom of the quote. Reproducer: '> Hello\n\nThe rest of the message.' → '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.' ^^^ phantom <br> Fix: replace the single-line filter with a while-loop that pops trailing lines while they are either empty OR a bare '>'. This matches the intent the Python test mirror in tests/test_blockquote_rendering.py already had (the mirror was correct; the JS was not — that's why the original tests passed despite the bug). Also add four new regression tests in TestNoPhantomTrailingBr that pin the no-trailing-<br> invariant for the common shapes: - input ending with \n - quote followed by paragraph (the real-world case) - multi-line quote ending with \n - quote with blank continuation + trailing \n (internal <br> stays, trailing <br> does not) Verified end-to-end with node against the actual JS regex. 244 renderer-adjacent tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes Five additional fixes on top of the blockquote grouping from the initial commit: 1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows line endings do not produce stray \r characters in rendered output 2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use inside blockquotes/lists) and the outer pass (for plain paragraphs). Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped. 3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐ via task-done/task-todo span wrappers. Checks [X] (uppercase) too. 4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its own <blockquote> element rather than passing through as literal >. Implemented by extracting the blockquote rule into _applyBlockquotes() which calls itself recursively on the stripped inner content. 5. Lists inside blockquotes: > - item now renders <ul><li> inside the blockquote instead of a literal "- item" string. Task list items work inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>). Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF strip at the top of renderMd pushed the autolink regex past the old limit. 68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py covering all constructs, edge cases, and combinations. * fix(renderer): restore space in blockquote prefix-strip regex Commit 04e7b53 changed the blockquote prefix-strip regex from /^>[ \t]?/ (consume "> ", "\t>", or just ">") to /^>[\t]?/ (only consume "\t>" or just ">") The space character was dropped from the character class. Since practically every blockquote an LLM produces is "> " (greater-than followed by a space), this leaves a leading space artifact on every stripped blockquote line. Worse, the leading space breaks the list-detection regex `^(?: )?[-*+] ` inside the new `_applyBlockquotes` helper — that regex requires either zero or two leading spaces, never one — so the new "list inside blockquote" feature never fired for the canonical input shape `> - item`. Reproducer (against the actual ui.js via node, before the fix): > Hello world → <blockquote> Hello world</blockquote> ^ phantom leading space > Steps: → <blockquote>Steps: > - one - one > - two - two</blockquote> ^ literal text, NOT a <ul>; lists-in-quote feature broken > - [x] done → blockquote with literal "[x] done", no checkbox span Tests passed despite the bug because tests/test_blockquote_rendering.py and tests/test_renderer_comprehensive.py validate against a Python mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e. the mirror is correct, the JS is not, and the static-mirror tests can't catch the divergence. Same shape of bug as commit 94d63d0 (phantom <br> in trailing line) where the mirror was right and the JS was wrong. Fix: restore the space character in the strip regex's character class. Add tests/test_renderer_js_behaviour.py — 11 tests that drive the ACTUAL renderMd via node and assert on rendered output for the most common LLM shapes (single-line quote, multi-line quote, list inside quote, task list inside quote, nested >>>, strikethrough inside and outside quote, top-level task list, quote followed by heading, multi-paragraph quote with list, CRLF normalisation). Verified: the buggy regex makes 6 of those 11 tests fail; the corrected regex makes all 11 pass. Suite: 2354 passed, 0 new failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Collapse agent session compression chains * Restore upstream changelog entries * fix(agent_sessions): bubble active compression chains to top by tip last_activity The original PR merge kept the chain head's id/title/started_at and overrode id/model/message_count/ended_at/end_reason from the tip — but did NOT override last_activity. Since the projected list is sorted by last_activity DESC and the WebUI sidebar surfaces updated_at = last_activity, an actively-used compression chain whose tip is being edited NOW would sort by the ROOT's old last_activity and fall below recently touched standalone sessions. Reproducer (with the harness against actual code, before the fix): - root: started 30 days ago, last msg 30 days ago - tip: started 28 days ago (parent_session_id=root), last msg 5 seconds ago - standalone: last msg 2 days ago Sidebar order with original PR: [0] standalone (48h ago) [1] active_tip (last_activity=root's 720h ago) ← wrong Sidebar order after fix: [0] active_tip (last_activity=tip's 0h ago) ← correct [1] standalone (48h ago) This matches Hermes Agent's own list_sessions_rich projection at hermes_state.py:903-909, which overrides "last_active" from the tip exactly so that the agent CLI's session list orders the same way. Add ``last_activity`` to the merge-from-tip key list, update the existing test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to expect tip-derived updated_at, and add test_compression_chain_bubbles_to_top_by_tip_activity locking in the bubble-to-top invariant — without this regression test the previous behaviour passed CI because no test exercised the sort order against a mixed set of chains and standalone sessions. The chain head's started_at (created_at) and title remain preserved, so users can still find the conversation by its original date and name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.216 release notes and version bump Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix. * chore: gitignore local-only review harness directory Adds .local-review/ to .gitignore so renderer drivers, sample inputs, fixture builders, and other reviewer scratch files do not accidentally get committed. Nothing under that path is ever shared in the repo; keeping the entry tracked makes the boundary explicit for any future contributor who creates the directory locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Keep reasoning chip visible for None effort * test(reasoning): pin chip render output via node, not just source regex The PR's static checks in test_reasoning_chip_btw_fixes.py validate the shape of _applyReasoningChip (no display='none' literal, the right classList.toggle call exists, the right label literals are in the function body) but pass even if the runtime detail is wrong — for example if `inactive` were inverted, _normalizeReasoningEffort mishandled whitespace, or _formatReasoningEffortLabel returned the wrong literal for an unknown input. Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive the actual _applyReasoningChip() via node and assert on the rendered DOM state for each effort value: TestChipAlwaysVisible - empty / null -> "Default" label, inactive=true - "none" -> "None" label, inactive=true - "low"/"high" -> verbatim label, inactive=false TestNormalizationEdgeCases - "NONE" -> normalises to "None" - " none " -> trims and normalises - unknown junk -> falls through visible, never hidden TestTitleAttributeAccessibility - title attribute carries the human-readable label for tooltip / screen-reader use Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught). Against this PR's ui.js: 11/11 pass. This pattern (drive the actual JS via node) caught two regex-only regressions in PR #1073 where the Python mirror was correct while the JS was broken. Same protection added here so the chip-visibility contract can't silently break in a future refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add #1074 to v0.50.216 changelog, bump test count to 2428 * fix(i18n): restore broken Unicode in Russian and Spanish steer strings Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally stripped the `\u` prefix from Unicode escape sequences in two locales, producing garbled literal hex strings visible to users: Spanish (es): - cmd_steer: correcci00f3n → corrección - cmd_steer_fallback: 2014 en cola → — en cola - busy_steer_fallback: 2014 en cola → — en cola - settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección - settings_busy_input_mode_steer: correcci00f3n → corrección Russian (ru): - settings_desc_busy_input_mode: the entire Cyrillic string was replaced with raw 4-hex-char code-points without the \u prefix (041e043f... instead of actual Cyrillic). Decoded: "Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer внедряет коррекцию без прерывания." Fix: write the correct characters directly (UTF-8 is the file encoding so embedding them literally is cleaner than \u escapes for long text). All other locales (en, de, zh, zh-Hant) were not affected — confirmed by grepping for bare hex run-ons in the updated file. Verified: node --check static/i18n.js passes; full pytest suite green (2365 passed, 47 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: remove duplicate compression chain entry from [Unreleased] --------- 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> Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-26 04:06:31 +00:00
_highlightReasoningOption(effort);
}
function fetchReasoningChip(){
api('/api/reasoning').then(function(st){
_applyReasoningChip((st&&st.reasoning_effort)||'');
}).catch(function(){_applyReasoningChip('');});
}
function syncReasoningChip(){
if(_currentReasoningEffort===null){fetchReasoningChip();return;}
_applyReasoningChip(_currentReasoningEffort);
}
function _highlightReasoningOption(effort){
const dd=$('composerReasoningDropdown');
if(!dd) return;
dd.querySelectorAll('.reasoning-option').forEach(function(opt){
opt.classList.toggle('selected',opt.dataset.effort===effort);
});
}
function toggleReasoningDropdown(){
const dd=$('composerReasoningDropdown');
const chip=$('composerReasoningChip');
if(!dd||!chip) return;
const open=dd.classList.contains('open');
if(open){closeReasoningDropdown();return;}
if(typeof closeProfileDropdown==='function') closeProfileDropdown();
if(typeof closeWsDropdown==='function') closeWsDropdown();
closeModelDropdown();
if(typeof closeToolsetsDropdown==='function') closeToolsetsDropdown();
_highlightReasoningOption(_currentReasoningEffort);
dd.classList.add('open');
fix: reasoning chip dropdown visible + monochrome SVG icon + /btw answer preserved (closes #933) (#934) * fix: reasoning chip dropdown visible + SVG icon + /btw answer no longer wiped (closes #933) * fix(ui): resize handler symmetry + lock regressions for PR #934 fixes Two small additions on top of the core PR: 1. Resize handler now re-positions the reasoning dropdown when the window resizes while it's open, matching the existing model-dropdown branch. Without this, resizing while the dropdown is open leaves it aligned to the pre-resize chip position — fine in practice (most resizes close the dropdown via the global click handler) but inconsistent with the model-dropdown sibling. 2. Regression test file tests/test_reasoning_chip_btw_fixes.py with 10 tests locking all four fixes in place so they can't silently regress: - Dropdown sits OUTSIDE .composer-left (so overflow-y: hidden can't clip it) - Dropdown is grouped with the other composer-level dropdowns - Chip button contains stroke="currentColor" SVG (not a 🧠 emoji) - _applyReasoningChip() body doesn't include 🧠 - cmdReasoning calls _applyReasoningChip(eff) directly with the server-confirmed effort, not syncReasoningChip() (stale cache) - _streamDone flag declared, set in done handler, checked in onerror - _ensureBtwRow() called in done handler (creates bubble when no tokens arrive) - resize handler re-positions composerReasoningDropdown Full suite: 2056 passed, 0 failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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-24 02:18:51 +00:00
_positionReasoningDropdown();
chip.classList.add('active');
const mobileAction=$('composerMobileReasoningAction');
if(mobileAction) mobileAction.classList.add('active');
}
fix: reasoning chip dropdown visible + monochrome SVG icon + /btw answer preserved (closes #933) (#934) * fix: reasoning chip dropdown visible + SVG icon + /btw answer no longer wiped (closes #933) * fix(ui): resize handler symmetry + lock regressions for PR #934 fixes Two small additions on top of the core PR: 1. Resize handler now re-positions the reasoning dropdown when the window resizes while it's open, matching the existing model-dropdown branch. Without this, resizing while the dropdown is open leaves it aligned to the pre-resize chip position — fine in practice (most resizes close the dropdown via the global click handler) but inconsistent with the model-dropdown sibling. 2. Regression test file tests/test_reasoning_chip_btw_fixes.py with 10 tests locking all four fixes in place so they can't silently regress: - Dropdown sits OUTSIDE .composer-left (so overflow-y: hidden can't clip it) - Dropdown is grouped with the other composer-level dropdowns - Chip button contains stroke="currentColor" SVG (not a 🧠 emoji) - _applyReasoningChip() body doesn't include 🧠 - cmdReasoning calls _applyReasoningChip(eff) directly with the server-confirmed effort, not syncReasoningChip() (stale cache) - _streamDone flag declared, set in done handler, checked in onerror - _ensureBtwRow() called in done handler (creates bubble when no tokens arrive) - resize handler re-positions composerReasoningDropdown Full suite: 2056 passed, 0 failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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-24 02:18:51 +00:00
function _positionReasoningDropdown(){
const dd=$('composerReasoningDropdown');
const chip=$('composerReasoningChip');
const mobileAction=$('composerMobileReasoningAction');
fix: reasoning chip dropdown visible + monochrome SVG icon + /btw answer preserved (closes #933) (#934) * fix: reasoning chip dropdown visible + SVG icon + /btw answer no longer wiped (closes #933) * fix(ui): resize handler symmetry + lock regressions for PR #934 fixes Two small additions on top of the core PR: 1. Resize handler now re-positions the reasoning dropdown when the window resizes while it's open, matching the existing model-dropdown branch. Without this, resizing while the dropdown is open leaves it aligned to the pre-resize chip position — fine in practice (most resizes close the dropdown via the global click handler) but inconsistent with the model-dropdown sibling. 2. Regression test file tests/test_reasoning_chip_btw_fixes.py with 10 tests locking all four fixes in place so they can't silently regress: - Dropdown sits OUTSIDE .composer-left (so overflow-y: hidden can't clip it) - Dropdown is grouped with the other composer-level dropdowns - Chip button contains stroke="currentColor" SVG (not a 🧠 emoji) - _applyReasoningChip() body doesn't include 🧠 - cmdReasoning calls _applyReasoningChip(eff) directly with the server-confirmed effort, not syncReasoningChip() (stale cache) - _streamDone flag declared, set in done handler, checked in onerror - _ensureBtwRow() called in done handler (creates bubble when no tokens arrive) - resize handler re-positions composerReasoningDropdown Full suite: 2056 passed, 0 failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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-24 02:18:51 +00:00
const footer=document.querySelector('.composer-footer');
if(!dd||!chip||!footer) return;
const panel=$('composerMobileConfigPanel');
const anchor=(panel&&panel.classList.contains('open')&&mobileAction)?mobileAction:chip;
const chipRect=anchor.getBoundingClientRect();
fix: reasoning chip dropdown visible + monochrome SVG icon + /btw answer preserved (closes #933) (#934) * fix: reasoning chip dropdown visible + SVG icon + /btw answer no longer wiped (closes #933) * fix(ui): resize handler symmetry + lock regressions for PR #934 fixes Two small additions on top of the core PR: 1. Resize handler now re-positions the reasoning dropdown when the window resizes while it's open, matching the existing model-dropdown branch. Without this, resizing while the dropdown is open leaves it aligned to the pre-resize chip position — fine in practice (most resizes close the dropdown via the global click handler) but inconsistent with the model-dropdown sibling. 2. Regression test file tests/test_reasoning_chip_btw_fixes.py with 10 tests locking all four fixes in place so they can't silently regress: - Dropdown sits OUTSIDE .composer-left (so overflow-y: hidden can't clip it) - Dropdown is grouped with the other composer-level dropdowns - Chip button contains stroke="currentColor" SVG (not a 🧠 emoji) - _applyReasoningChip() body doesn't include 🧠 - cmdReasoning calls _applyReasoningChip(eff) directly with the server-confirmed effort, not syncReasoningChip() (stale cache) - _streamDone flag declared, set in done handler, checked in onerror - _ensureBtwRow() called in done handler (creates bubble when no tokens arrive) - resize handler re-positions composerReasoningDropdown Full suite: 2056 passed, 0 failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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-24 02:18:51 +00:00
const footerRect=footer.getBoundingClientRect();
let left=chipRect.left-footerRect.left;
const maxLeft=Math.max(0,footer.clientWidth-dd.offsetWidth);
left=Math.max(0,Math.min(left,maxLeft));
dd.style.left=`${left}px`;
}
function closeReasoningDropdown(){
const dd=$('composerReasoningDropdown');
const chip=$('composerReasoningChip');
const mobileAction=$('composerMobileReasoningAction');
if(dd) dd.classList.remove('open');
if(chip) chip.classList.remove('active');
if(mobileAction) mobileAction.classList.remove('active');
}
document.addEventListener('click',function(e){
if(
!e.target.closest('#composerReasoningChip') &&
!e.target.closest('#composerMobileReasoningAction') &&
!e.target.closest('#composerReasoningDropdown')
) closeReasoningDropdown();
if(e.target.closest('.reasoning-option')){
const opt=e.target.closest('.reasoning-option');
const effort=opt&&opt.dataset.effort;
if(effort){
api('/api/reasoning',{method:'POST',body:JSON.stringify({effort:effort})})
.then(function(st){
_applyReasoningChip((st&&st.reasoning_effort)||effort);
showToast('🧠 Reasoning effort set to '+((st&&st.reasoning_effort)||effort));
})
.catch(function(){showToast('🧠 Failed to set effort');});
closeReasoningDropdown();
}
}
});
// ── Session toolsets chip (#493) ───────────────────────────────────────────
let _currentSessionToolsets = null; // null = global, array = custom list
function _applyToolsetsChip(toolsets) {
_currentSessionToolsets = toolsets;
const wrap = $('composerToolsetsWrap');
const label = $('composerToolsetsLabel');
const chip = $('composerToolsetsChip');
if (!wrap || !label) return;
// Visibility is controlled entirely by responsive CSS — the chip shows only
// at wide composer-footer widths (>= 1100px container query). At narrower
// widths the layout is too cramped (model + reasoning + profile + workspace
// + context-ring + send) to add another chip. Cleared inline style so the
// CSS @container query is the single source of truth. State is still
// tracked so /api/session/toolsets continues to work for cron/scripted
// callers regardless of UI visibility. (#1431)
wrap.style.display = '';
const hasCustom = Array.isArray(toolsets) && toolsets.length > 0;
if (hasCustom) {
label.textContent = toolsets.join(', ');
chip.classList.add('has-custom');
chip.title = t('session_toolsets') + ': ' + toolsets.join(', ');
} else {
label.textContent = t('session_toolsets_global');
chip.classList.remove('has-custom');
chip.title = t('session_toolsets');
}
}
function _syncToolsetsChip() {
if (typeof S === 'undefined' || !S || !S.session) {
_applyToolsetsChip(null);
return;
}
_applyToolsetsChip(S.session.enabled_toolsets || null);
}
function syncToolsetsChip() {
_syncToolsetsChip();
}
function _populateToolsetsDropdown() {
const desc = $('toolsetsDropdownDesc');
const state = $('toolsetsDropdownState');
const input = $('toolsetsInput');
const applyBtn = $('toolsetsApplyBtn');
const clearBtn = $('toolsetsClearBtn');
if (!desc || !state || !input) return;
desc.textContent = t('session_toolsets_desc');
if (applyBtn) applyBtn.textContent = t('session_toolsets_apply');
if (clearBtn) clearBtn.textContent = t('session_toolsets_clear');
input.placeholder = t('session_toolsets_placeholder');
// Escape key handler for toolsets input
input.onkeydown = function(e) { if(e.key === 'Escape') closeToolsetsDropdown(); };
const hasCustom = Array.isArray(_currentSessionToolsets) && _currentSessionToolsets.length > 0;
if (hasCustom) {
state.textContent = '🔧 ' + _currentSessionToolsets.join(', ');
input.value = _currentSessionToolsets.join(', ');
} else {
state.textContent = '🌍 ' + t('session_toolsets_global');
input.value = '';
}
}
function _positionToolsetsDropdown() {
const dd = $('composerToolsetsDropdown');
const chip = $('composerToolsetsChip');
const footer = document.querySelector('.composer-footer');
if (!dd || !chip || !footer) return;
// Defense: if the chip has been hidden by responsive CSS (e.g. resize across
// 1100px container threshold while dropdown was open), don't try to anchor
// to a zero-rect element — close the dropdown instead. (#1431)
if (chip.offsetParent === null) { closeToolsetsDropdown(); return; }
const chipRect = chip.getBoundingClientRect();
const footerRect = footer.getBoundingClientRect();
let left = chipRect.left - footerRect.left;
const maxLeft = Math.max(0, footer.clientWidth - dd.offsetWidth);
left = Math.max(0, Math.min(left, maxLeft));
dd.style.left = left + 'px';
}
function toggleToolsetsDropdown() {
const dd = $('composerToolsetsDropdown');
const chip = $('composerToolsetsChip');
if (!dd || !chip) return;
if (typeof S === 'undefined' || !S || !S.session) return;
// Don't open when the chip itself is hidden by responsive CSS (#1431).
// offsetParent === null catches display:none on the element or any ancestor.
if (chip.offsetParent === null) return;
const open = dd.classList.contains('open');
if (open) { closeToolsetsDropdown(); return; }
if (typeof closeProfileDropdown === 'function') closeProfileDropdown();
if (typeof closeWsDropdown === 'function') closeWsDropdown();
closeModelDropdown();
if (typeof closeReasoningDropdown === 'function') closeReasoningDropdown();
_syncToolsetsChip();
_populateToolsetsDropdown();
dd.classList.add('open');
_positionToolsetsDropdown();
chip.classList.add('active');
// Focus the input after a tick so the layout has settled
setTimeout(() => { const inp = $('toolsetsInput'); if (inp) inp.focus(); }, 50);
}
function closeToolsetsDropdown() {
const dd = $('composerToolsetsDropdown');
const chip = $('composerToolsetsChip');
if (dd) dd.classList.remove('open');
if (chip) chip.classList.remove('active');
}
function _applySessionToolsets(toolsets) {
if (typeof S === 'undefined' || !S || !S.session) return;
const sid = S.session.session_id;
api('/api/session/toolsets', {
method: 'POST',
body: JSON.stringify({ session_id: sid, toolsets: toolsets })
})
.then(function(r) {
if (r && r.ok) {
S.session.enabled_toolsets = r.enabled_toolsets || null;
_applyToolsetsChip(r.enabled_toolsets || null);
if (r.enabled_toolsets && r.enabled_toolsets.length) {
showToast('🔧 ' + t('session_toolsets_applied') + ': ' + r.enabled_toolsets.join(', '));
} else {
showToast('🌍 ' + t('session_toolsets_cleared'));
}
} else {
showToast(t('session_toolsets_failed') + (r && r.error ? r.error : 'Unknown error'), 3000, 'error');
}
})
.catch(function(err) {
showToast(t('session_toolsets_failed') + (err.message || err), 3000, 'error');
});
}
// Click-outside handler for toolsets dropdown
document.addEventListener('click', function(e) {
if (
!e.target.closest('#composerToolsetsChip') &&
!e.target.closest('#composerToolsetsDropdown')
) closeToolsetsDropdown();
// Apply button
if (e.target.closest('#toolsetsApplyBtn')) {
const input = $('toolsetsInput');
if (!input) return;
const raw = input.value.trim();
if (!raw) {
showToast(t('session_toolsets_desc'), 2000);
return;
}
const toolsets = raw.split(',').map(s => s.trim()).filter(Boolean);
if (toolsets.length === 0) {
showToast(t('session_toolsets_desc'), 2000);
return;
}
_applySessionToolsets(toolsets);
closeToolsetsDropdown();
}
// Clear button
if (e.target.closest('#toolsetsClearBtn')) {
_applySessionToolsets(null);
closeToolsetsDropdown();
}
});
// Position toolsets dropdown on resize, OR close it if the chip is no longer
// visible (e.g. resize crossed the 1100px container threshold while dropdown
// was open — the wrap is hidden by CSS but the dropdown sibling stays open
// without an anchor). (#1431)
window.addEventListener('resize', () => {
const dd = $('composerToolsetsDropdown');
if (!dd || !dd.classList.contains('open')) return;
const chip = $('composerToolsetsChip');
if (!chip || chip.offsetParent === null) { closeToolsetsDropdown(); return; }
_positionToolsetsDropdown();
});
function _syncMobileComposerConfigButton(open){
const btn=$('composerMobileConfigBtn');
if(!btn) return;
btn.classList.toggle('active',!!open);
btn.setAttribute('aria-expanded',open?'true':'false');
}
function closeMobileComposerConfig(){
const panel=$('composerMobileConfigPanel');
if(panel) panel.classList.remove('open');
_syncMobileComposerConfigButton(false);
if(typeof closeWsDropdown==='function') closeWsDropdown();
}
function toggleMobileComposerConfig(){
const panel=$('composerMobileConfigPanel');
if(!panel) return;
const open=panel.classList.contains('open');
if(open){
closeMobileComposerConfig();
closeModelDropdown();
closeReasoningDropdown();
if(typeof closeToolsetsDropdown==='function') closeToolsetsDropdown();
return;
}
if(typeof closeProfileDropdown==='function') closeProfileDropdown();
if(typeof closeWsDropdown==='function') closeWsDropdown();
closeModelDropdown();
closeReasoningDropdown();
if(typeof closeToolsetsDropdown==='function') closeToolsetsDropdown();
panel.classList.add('open');
_syncMobileComposerConfigButton(true);
}
document.addEventListener('click',function(e){
if(
e.target.closest('#composerMobileConfigBtn') ||
e.target.closest('#composerMobileConfigPanel') ||
e.target.closest('#composerWsDropdown') ||
e.target.closest('#composerModelDropdown') ||
e.target.closest('#composerReasoningDropdown')
) return;
closeMobileComposerConfig();
});
document.addEventListener('keydown',function(e){
if(e.key!=='Escape') return;
const panel=$('composerMobileConfigPanel');
if(!panel||!panel.classList.contains('open')) return;
e.preventDefault();
closeMobileComposerConfig();
if(typeof closeWsDropdown==='function') closeWsDropdown();
closeModelDropdown();
closeReasoningDropdown();
});
window.addEventListener('resize',function(){
if(window.matchMedia && !window.matchMedia('(max-width: 640px)').matches){
closeMobileComposerConfig();
closeModelDropdown();
closeReasoningDropdown();
if(typeof closeWsDropdown==='function') closeWsDropdown();
}
});
2026-03-31 07:02:47 +00:00
// ── Scroll pinning ──────────────────────────────────────────────────────────
// When streaming, auto-scroll only if the user hasn't manually scrolled up.
// Once the user scrolls back to within 250px of the bottom, re-pin.
// Uses a guard flag to avoid the race where programmatic scrolls (from
// scrollIfPinned / scrollToBottom) re-set _scrollPinned=true, overriding
// the user's explicit scroll-up. Fixes #1469 / #1360.
// rAF-debounced scroll listener (issue #1360): on macOS WKWebView, trackpad
// momentum scrolling fires scroll events that interleave with the
// _programmaticScroll setTimeout(0) guard. A mid-momentum scroll event can
// either get swallowed (_programmaticScroll still true) or falsely report
// nearBottom (momentum hasn't settled). rAF defers the nearBottom check to
// the next paint frame when the browser's scroll position has settled.
// A hysteresis counter requires two consecutive near-bottom samples before
// re-pinning, preventing accidental re-pin during initial deceleration.
2026-03-31 07:02:47 +00:00
let _scrollPinned=true;
let _programmaticScroll=false;
let _nearBottomCount=0;
2026-03-31 07:02:47 +00:00
(function(){
const el=document.getElementById('messages');
if(!el) return;
let _scrollRaf=0;
2026-03-31 07:02:47 +00:00
el.addEventListener('scroll',()=>{
if(_programmaticScroll) return; // ignore scrolls we triggered ourselves
cancelAnimationFrame(_scrollRaf);
_scrollRaf=requestAnimationFrame(()=>{
const nearBottom=el.scrollHeight-el.scrollTop-el.clientHeight<250;
_nearBottomCount=nearBottom?_nearBottomCount+1:0;
_scrollPinned=_nearBottomCount>=2;
const btn=$('scrollToBottomBtn');
if(btn) btn.style.display=_scrollPinned?'none':'flex';
// Load older messages when scrolled near the top
if(el.scrollTop<80 && typeof _messagesTruncated!=='undefined' && _messagesTruncated && typeof _loadOlderMessages==='function'){
_loadOlderMessages();
}
});
2026-03-31 07:02:47 +00:00
});
})();
function _fmtTokens(n){if(!n||n<0)return'0';if(n>=1e6)return(n/1e6).toFixed(1)+'M';if(n>=1e3)return(n/1e3).toFixed(1)+'k';return String(n);}
2026-05-04 03:14:49 +00:00
function _formatTurnDuration(seconds){
const n=Number(seconds);
if(!Number.isFinite(n)||n<0)return'';
const total=Math.max(0,Math.round(n));
if(total<60)return`${total}s`;
const h=Math.floor(total/3600);
const m=Math.floor((total%3600)/60);
const s=total%60;
if(h)return`${h}h ${m}m`;
return`${m}m ${s}s`;
}
feat: Sprint 23 — agentic transparency + polish Track A: Token/cost display - Read agent usage attrs (session_prompt_tokens, session_completion_tokens, session_estimated_cost_usd) after run_conversation in streaming.py - Add input_tokens, output_tokens, estimated_cost fields to Session model - Include usage in done SSE event payload - Store usage on S.lastUsage in messages.js done handler - Render usage badge below last assistant message (input/output/cost) Track B: Subagent delegation cards - Add subagent_progress to toolIcon map with shuffle emoji - Special-case subagent_progress in buildToolCard: "Subagent" label, strip double emoji from preview, add tool-card-subagent CSS class - Indented border-left styling for subagent cards - Clean delegate_task display name Track C: Skill picker in cron create form - Add skill search input + tag chips to cron create form HTML - Skill picker JS in panels.js: search/filter, click-to-add tags, remove tag chips, pre-fetch skill list on form open - submitCronCreate sends skills array in POST body - Skill picker dropdown + tag CSS Track D: Skill linked files viewer - Add file query param to /api/skills/content endpoint - Serve linked files from skill directory with path traversal protection - Ensure linked_files key always present in skill content response - Render linked files section below SKILL.md content in preview panel - openSkillFile function for viewing individual linked files Track E: Bug fixes and code quality - Expand Session.__init__ and compact() to readable multi-line format - Remove inline import json as _j2 inside loop in streaming.py - Fix tool_calls: capture args from assistant messages, skip unresolved names - Store args snapshot in persisted tool_calls for reload display 6 new tests. Total: 421 (409 passing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
const _MOBILE_CONFIG_BASE_LABEL='Workspace, model, reasoning, and context settings';
function _setCtxCompressButton(btn,text){
if(!btn)return;
if(text){
btn.style.display='';
btn.textContent=text;
btn.onclick=function(e){
if(e)e.stopPropagation();
const ta=$('msg');
if(ta){ta.value='/compress ';ta.focus();autoResize();}
};
}else{
btn.style.display='none';
btn.textContent='';
btn.onclick=null;
}
}
function _syncMobileCtxDisplay(state){
const badge=$('composerMobileCtxBadge');
const mobileConfigBtn=$('composerMobileConfigBtn');
const row=$('composerMobileContextAction');
const usageLine=$('composerMobileContextUsage');
const tokensLine=$('composerMobileContextTokens');
const thresholdLine=$('composerMobileContextThreshold');
const costLine=$('composerMobileContextCost');
const compressBtn=$('composerMobileCtxCompressBtn');
if(!state||!state.visible){
if(badge)badge.style.display='none';
if(row)row.style.display='none';
if(mobileConfigBtn){
mobileConfigBtn.setAttribute('aria-label',_MOBILE_CONFIG_BASE_LABEL);
mobileConfigBtn.setAttribute('title',_MOBILE_CONFIG_BASE_LABEL);
}
_setCtxCompressButton(compressBtn,'');
return;
}
if(badge){
badge.style.display='inline-flex';
badge.textContent=state.hasPromptTok?String(state.pct):'\u00b7';
badge.classList.toggle('ctx-mid',state.pct>50&&state.pct<=75);
badge.classList.toggle('ctx-high',state.pct>75);
badge.setAttribute('title',state.label);
}
if(mobileConfigBtn){
mobileConfigBtn.setAttribute('aria-label',`${_MOBILE_CONFIG_BASE_LABEL}; ${state.label}`);
mobileConfigBtn.setAttribute('title',`${_MOBILE_CONFIG_BASE_LABEL} \u00b7 ${state.label}`);
}
if(row){
row.style.display='';
row.setAttribute('aria-label',state.label);
row.classList.toggle('ctx-mid',state.pct>50&&state.pct<=75);
row.classList.toggle('ctx-high',state.pct>75);
}
if(usageLine)usageLine.textContent=state.usageText||'';
if(tokensLine)tokensLine.textContent=state.tokensText||'';
if(thresholdLine){
if(state.thresholdText){
thresholdLine.style.display='';
thresholdLine.textContent=state.thresholdText;
}else{
thresholdLine.style.display='none';
thresholdLine.textContent='';
}
}
if(costLine){
if(state.costText){
costLine.style.display='';
costLine.textContent=state.costText;
}else{
costLine.style.display='none';
costLine.textContent='';
}
}
_setCtxCompressButton(compressBtn,state.compressText||'');
}
// Context usage indicator in composer footer
function _syncCtxIndicator(usage){
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
const wrap=$('ctxIndicatorWrap');
const el=$('ctxIndicator');
if(!el)return;
// #1436: Use last_prompt_tokens only — NEVER fall back to cumulative
// input_tokens for the "context window % used" calculation. input_tokens
// is summed across all turns, so dividing it by the context window gives a
// nonsense percentage (often >100%) on long sessions. When we have no
// last-prompt data we render "·" + "tokens used" via the !hasPromptTok
// branch below — honest "no data" instead of misleading "890% used".
const promptTok=usage.last_prompt_tokens||0;
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
const totalTok=(usage.input_tokens||0)+(usage.output_tokens||0);
// Default context window to 128K when not provided by backend
const DEFAULT_CTX=128*1024;
const ctxWindow=usage.context_length||DEFAULT_CTX;
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
const cost=usage.estimated_cost;
// Show indicator whenever we have any usage data (tokens or cost)
if(!promptTok&&!totalTok&&!cost){
if(wrap) wrap.style.display='none';
_syncMobileCtxDisplay({visible:false});
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
return;
}
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(wrap) wrap.style.display='';
const hasPromptTok=!!promptTok;
const rawPct=hasPromptTok?Math.round((promptTok/ctxWindow)*100):0;
const pct=Math.min(100,rawPct);
const overflowed=rawPct>100;
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
const ring=$('ctxRingValue');
const center=$('ctxPercent');
const usageLine=$('ctxTooltipUsage');
const tokensLine=$('ctxTooltipTokens');
const thresholdLine=$('ctxTooltipThreshold');
const costLine=$('ctxTooltipCost');
if(ring){
const circumference=61.261056745;
ring.style.strokeDasharray=String(circumference);
ring.style.strokeDashoffset=String(circumference*(1-pct/100));
}
if(center) center.textContent=hasPromptTok?String(pct):'\u00b7';
const hasExplicitCtx=!!usage.context_length;
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
el.classList.toggle('ctx-mid',pct>50&&pct<=75);
el.classList.toggle('ctx-high',pct>75);
// ── Compress affordance (#524) ──
// Show a hint in the tooltip when context usage is high so users
// discover /compress without having to know the slash command.
const compressWrap=$('ctxTooltipCompress');
const compressBtn=$('ctxCompressBtn');
const compressText=pct>=75?t('ctx_compress_action'):(pct>=50?t('ctx_compress_hint'):'');
if(compressWrap) compressWrap.style.display=compressText?'':'none';
_setCtxCompressButton(compressBtn,compressText);
let label=hasPromptTok?`Context window ${pct}% used`:`${_fmtTokens(totalTok)} tokens used`;
if(!hasExplicitCtx&&hasPromptTok) label+=' (est. 128K)';
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(cost) label+=` \u00b7 $${cost<0.01?cost.toFixed(4):cost.toFixed(2)}`;
el.setAttribute('aria-label',label);
const usageText=hasPromptTok?(overflowed?`${rawPct}% used (context exceeded)`:`${pct}% used (${100-pct}% left)`):`${_fmtTokens(totalTok)} tokens used`;
const tokensText=hasPromptTok?`${_fmtTokens(promptTok)} / ${_fmtTokens(ctxWindow)} tokens used`:`In: ${_fmtTokens(usage.input_tokens||0)} \u00b7 Out: ${_fmtTokens(usage.output_tokens||0)}`;
if(usageLine) usageLine.textContent=usageText;
if(tokensLine) tokensLine.textContent=tokensText;
const threshold=usage.threshold_tokens||0;
let thresholdText='';
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(thresholdLine){
if(threshold&&ctxWindow){
thresholdText=`Auto-compress at ${_fmtTokens(threshold)} (${Math.round(threshold/ctxWindow*100)}%)`;
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
thresholdLine.style.display='';
thresholdLine.textContent=thresholdText;
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
}else{
thresholdLine.style.display='none';
thresholdLine.textContent='';
}
}
let costText='';
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(costLine){
if(cost){
costText=`Estimated cost: $${cost<0.01?cost.toFixed(4):cost.toFixed(2)}`;
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
costLine.style.display='';
costLine.textContent=costText;
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
}else{
costLine.style.display='none';
costLine.textContent='';
}
}
_syncMobileCtxDisplay({
visible:true,
hasPromptTok,
pct,
label,
usageText,
tokensText,
thresholdText,
costText,
compressText
});
}
// ── Touch support: toggle context tooltip on tap (#524) ──
// On mobile, hover doesn't work — allow tap on the context ring button
// to toggle the tooltip visibility so the compress affordance is reachable.
document.addEventListener('DOMContentLoaded',function(){
const wrap=document.getElementById('ctxIndicatorWrap');
const tooltip=document.getElementById('ctxTooltip');
if(!wrap||!tooltip)return;
const btn=document.getElementById('ctxIndicator');
if(!btn)return;
btn.addEventListener('click',function(e){
e.stopPropagation();
const isOpen=tooltip.classList.contains('ctx-tooltip-active');
tooltip.classList.toggle('ctx-tooltip-active',!isOpen);
tooltip.setAttribute('aria-hidden',String(isOpen));
});
// Close on outside tap
document.addEventListener('click',function(){
tooltip.classList.remove('ctx-tooltip-active');
tooltip.setAttribute('aria-hidden','true');
},{passive:true});
// Prevent tooltip click from closing itself
tooltip.addEventListener('click',function(e){e.stopPropagation();});
});
2026-03-31 07:02:47 +00:00
function scrollIfPinned(){
if(!_scrollPinned) return;
const el=$('messages');
if(el){_programmaticScroll=true;el.scrollTop=el.scrollHeight;setTimeout(()=>{_programmaticScroll=false;},0);}
2026-03-31 07:02:47 +00:00
}
function scrollToBottom(){
_scrollPinned=true;
const el=$('messages');
if(el){_programmaticScroll=true;el.scrollTop=el.scrollHeight;setTimeout(()=>{_programmaticScroll=false;},0);}
const btn=$('scrollToBottomBtn');
if(btn) btn.style.display='none';
2026-03-31 07:02:47 +00:00
}
function _fmtOllamaLabel(mid){
const [namePart, ...variantParts] = mid.split(':');
const variant = variantParts.join(':');
const _fmt = (s) => {
const tokens = s.replace(/[-_]/g, ' ').split(' ');
return tokens.map(t => {
const alphaOnly = t.replace(/\./g, '');
if (t.length <= 3 && /^[a-zA-Z.]+$/.test(t)) return t.toUpperCase();
if (/^\d/.test(alphaOnly)) return t.toUpperCase();
return t.charAt(0).toUpperCase() + t.slice(1);
}).join(' ');
};
let label = _fmt(namePart);
if (variant) label += ' (' + _fmt(variant) + ')';
return label;
}
2026-03-31 07:02:47 +00:00
function getModelLabel(modelId){
if(!modelId) return 'Unknown';
// Check dynamic labels first, then fall back to splitting the ID
if(_dynamicModelLabels[modelId]) return _dynamicModelLabels[modelId];
// Static fallback for common models
const STATIC_LABELS={'openai/gpt-5.4-mini':'GPT-5.4 Mini','openai/gpt-4o':'GPT-4o','openai/o3':'o3','openai/o4-mini':'o4-mini','anthropic/claude-sonnet-4.6':'Sonnet 4.6','anthropic/claude-sonnet-4-5':'Sonnet 4.5','anthropic/claude-haiku-3-5':'Haiku 3.5','google/gemini-3.1-pro-preview':'Gemini 3.1 Pro','google/gemini-3-flash-preview':'Gemini 3 Flash','google/gemini-3.1-flash-lite-preview':'Gemini 3.1 Flash Lite','google/gemini-2.5-pro':'Gemini 2.5 Pro','google/gemini-2.5-flash':'Gemini 2.5 Flash','deepseek/deepseek-v4-flash':'DeepSeek V4 Flash','deepseek/deepseek-v4-pro':'DeepSeek V4 Pro','deepseek/deepseek-chat-v3-0324':'DeepSeek V3 (legacy)','meta-llama/llama-4-scout':'Llama 4 Scout'};
2026-03-31 07:02:47 +00:00
if(STATIC_LABELS[modelId]) return STATIC_LABELS[modelId];
// Safe Ollama-tag fallback formatter before generic split('/').pop()
let _last = modelId.split('/').pop() || modelId;
// Strip @provider: prefix if present (e.g. @ollama-cloud:kimi-k2.6)
if (_last.startsWith('@') && _last.includes(':')) _last = _last.split(':').slice(1).join(':');
const looksLikeOllamaTag = /^[a-z0-9][\w.-]*:[\w.-]+$/i.test(_last);
// Narrow: only apply Ollama formatter to IDs with explicit @ollama prefix or colon-tag format.
// Avoids reformatting bare provider model IDs like claude-sonnet-4-6 or gpt-4o.
const looksLikeBareOllamaId = modelId.startsWith('@ollama') || looksLikeOllamaTag;
const ollamaLabel = _fmtOllamaLabel(_last);
if ((modelId.startsWith('ollama/') || modelId.startsWith('@ollama') || looksLikeOllamaTag || looksLikeBareOllamaId) && ollamaLabel !== _last) {
return ollamaLabel;
}
return _last || 'Unknown';
2026-03-31 07:02:47 +00:00
}
function _gatewayProviderName(provider){
const text=String(provider||'').trim();
if(!text)return'';
return text.replace(/^custom:/,'').replace(/[-_]/g,' ').replace(/\b\w/g,c=>c.toUpperCase());
}
function _gatewayRoutingLabel(routing){
if(!routing)return'';
const provider=_gatewayProviderName(routing.used_provider||routing.provider);
return provider?`via ${provider}`:'';
}
function _formatGatewayModelLabel(modelId,labelText,routing){
if(!routing)return'';
const usedModel=String(routing.used_model||'').trim();
const base=usedModel?getModelLabel(usedModel):(labelText||getModelLabel(modelId));
const via=_gatewayRoutingLabel(routing);
return via?`${base} ${via}`:base;
}
function _gatewayRoutingFailoverText(routing){
if(!routing||!routing.has_failover)return'';
const attempts=Array.isArray(routing.routing)?routing.routing:[];
const providers=attempts.map(a=>_gatewayProviderName(a&&a.provider)).filter(Boolean);
const unique=[];providers.forEach(p=>{if(!unique.includes(p))unique.push(p);});
if(unique.length>=2)return`Failover: ${unique[0]} → ${unique[unique.length-1]}`;
const from=_gatewayProviderName(routing.requested_provider);
const to=_gatewayProviderName(routing.used_provider);
if(from&&to&&from!==to)return`Failover: ${from} → ${to}`;
return'Gateway failover detected';
}
function _gatewayModelWarningText(routing){
if(!routing||!routing.model_changed)return'';
const requested=getModelLabel(routing.requested_model||'requested model');
const used=getModelLabel(routing.used_model||'served model');
return`Model switched: ${requested} → ${used}`;
}
function _latestGatewayRoutingForSession(session){
if(!session)return null;
if(session.gateway_routing)return session.gateway_routing;
const history=Array.isArray(session.gateway_routing_history)?session.gateway_routing_history:[];
return history.length?history[history.length-1]:null;
}
function _stripXmlToolCallsDisplay(s){
// Strip <function_calls>...</function_calls> blocks emitted by DeepSeek and
// similar models in their raw response text. These are processed separately
// as tool calls; leaving them in the content causes them to render visibly
// in the settled chat bubble. (#702)
// Also handles DSML-prefixed variants from DeepSeek/Bedrock, including
// spacing variants like "<|DSML |function_calls" and truncated prefixes.
if(!s) return s;
const lo=String(s).toLowerCase();
if(lo.indexOf('function_calls')===-1 && lo.indexOf('dsml')===-1) return s;
// Support both plain <function_calls> and DSML-prefixed variants.
s=s.replace(/<(?:\s*|\s*DSML\s*[||]\s*)?function_calls>[\s\S]*?<\/(?:\s*|\s*DSML\s*[||]\s*)?function_calls>/gi,'');
// Also remove truncated opening tags (missing closing ">" at stream tail).
s=s.replace(/<(?:\s*|\s*DSML\s*[||]\s*)?function_calls(?:>|$)[\s\S]*$/i,'');
// Remove malformed DSML tag fragments like "<|DSML |" that can leak in tokens.
s=s.replace(/<\s*|\s*DSML\s*[||]\s*/gi,'');
return s.trim();
}
function _sanitizeThinkingDisplayText(text){
const stripped=_stripXmlToolCallsDisplay(String(text||''));
return stripped.trim();
}
function renderMd(raw){
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075) * fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly MIME_MAP was missing entries for .html and .htm. The server fell back to Content-Type: application/octet-stream, which browsers refuse to render as HTML in an iframe — causing a blank white preview. The rest of the pipeline was already correct: the iframe exists in static/index.html, openFile() in static/workspace.js routes .html to showPreview('html'), and _handle_file_raw() in api/routes.py sets the correct CSP sandbox header when ?inline=1 is present. The only missing piece was the MIME type. * test(workspace): lock in MIME_MAP entry for .html/.htm PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py to fix the blank workspace HTML preview iframe. Without a direct assertion on the MIME_MAP entries, the fix could silently regress (the existing test_779_html_preview.py tests cover the iframe wiring, the inline=1 query handling, and the CSP sandbox header — but none of them touch MIME_MAP itself). Add a single regression test that asserts MIME_MAP['.html'] and MIME_MAP['.htm'] are both 'text/html' so any future removal of those entries fails CI immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(composer): raise .approval-card.visible z-index above .queue-card .queue-card has z-index:2. .approval-card.visible had no z-index, so the queue flyout would render on top of the approval card when both were visible simultaneously — obscuring the Allow/Deny buttons. Fix: add z-index:3 to .approval-card.visible so approvals always render above the queue flyout. Approval is a blocking, security-relevant interaction and must never be obscured by passive UI elements. * test(composer): pin approval-card z-index > queue-card invariant PR #1071 raises .approval-card.visible to z-index:3 so the security- relevant Allow / Deny buttons stay clickable when the queue flyout is also open. Without a regression test, a future CSS edit could silently drop the z-index back below queue-card (z-index:2) and reintroduce the bug — there is no automated UI test covering this stacking interaction. Add a focused regex check that pins the invariant: .approval-card.visible z-index must be strictly greater than .queue-card z-index. Modeled on the existing CSS-regex regression style in tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: intercept /steer /interrupt /queue before busy-mode routing in send() Root cause: slash commands entered while the agent is busy never reached the command dispatcher. send() enters the busy block and returns early at line ~50, so the slash-command intercept (~line 56) is never reached. The text was queued as a plain message. When it drained after the turn ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream, and showed "No active task to stop." Fix: at the top of the busy block, before checking busyMode, check if the text starts with / and is one of the three control commands. If so, dispatch the handler immediately and return. This lets the user type /steer, /interrupt, or /queue at any time — including while the agent is mid-stream — and have them execute against the live session. Two new regression tests added: - test_slash_commands_intercepted_before_busymode_routing: verifies the intercept appears before the busyMode routing in the busy block - test_steer_intercept_calls_handler_directly: verifies the intercept calls _bc.fn(_pc.args) and returns, not queues * test(busy-intercept): pin sync input-clear before await in slash intercept PR #1072's intercept clears the msg input before awaiting the handler. Order matters: if the await happens first (or if the clear is moved inside the handler), the input still shows '/steer foo' for the duration of the await. A reflexive second Enter press during that window — common while waiting for the toast — re-runs send(): either re-fires the handler (double-steer) or, if the turn just ended, falls through to the non-busy slash dispatcher and drops a confusing "No active task to stop." Add test_steer_intercept_clears_input_before_await pinning the order so this UX invariant cannot silently regress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: update steer i18n and settings copy — steer no longer interrupts With the real /steer implementation (agent.steer() via /api/chat/steer), steer injects a correction mid-turn WITHOUT interrupting the current stream. The previous copy said "falls back to interrupt", "Steer (interrupt + send)", etc. — accurate only for the old placeholder, not the real implementation. Changes across all 6 locales (en/ru/es/de/zh/zh-Hant): cmd_steer: "falls back to interrupt" removed settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction" cmd_steer_fallback: "interrupted" → "queued for next turn" busy_steer_fallback: "interrupted instead" → "queued for next turn" settings_desc_busy_input_mode: "currently falls back to interrupt" removed Also: static/index.html: inline fallback text updated to match static/commands.js: internal comment clarified (fallback = queue+cancel, not "interrupt mode" which implies the primary action) * fix(renderer): group consecutive blockquote lines into single element Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs: 1. `.+` required at least one character — bare `>` lines (blank continuation lines) did not match and passed through as literal `>` 2. Each matching line became its own `<blockquote>` element — a 10-line blockquote produced 10 stacked `<blockquote>` tags with no grouping 3. When a fenced code block sat inside a blockquote, the fence-stash pass consumed the code content and left orphaned `>` lines that the old `.+` pattern could not match Fix: replace the single-line regex with a group-based approach that matches one or more consecutive `>` lines as a single block, strips the `>` prefix from each line, passes each non-empty line through inlineMd(), turns blank `>` lines into `<br>`, and wraps the entire group in one `<blockquote>`. 14 regression tests added covering: - Single-line blockquotes (regression) - Multi-line grouping (2 and 10 lines) - Two separate blockquotes staying separate - Bare `>` and `>text` (no space) edge cases - Blank continuation lines → <br> - Bold / italic / inline-code inside blockquotes - Blockquote followed by normal paragraph * fix(renderer): drop empty trailing line from blockquote match The new group-based blockquote rule introduced in this PR captures the trailing newline in its (?:\n|$) clause. After block.split('\n') that trailing newline produces an empty final element. The original filter only dropped lone bare '>' artifacts on the last line, so the empty final element survived, and the .map(blank → '<br>') step turned it into a phantom <br> immediately before </blockquote>. Visible symptom: any blockquote whose source ends with \n (the common case — a quote followed by another paragraph or end-of-message) renders with an extra blank line at the bottom of the quote. Reproducer: '> Hello\n\nThe rest of the message.' → '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.' ^^^ phantom <br> Fix: replace the single-line filter with a while-loop that pops trailing lines while they are either empty OR a bare '>'. This matches the intent the Python test mirror in tests/test_blockquote_rendering.py already had (the mirror was correct; the JS was not — that's why the original tests passed despite the bug). Also add four new regression tests in TestNoPhantomTrailingBr that pin the no-trailing-<br> invariant for the common shapes: - input ending with \n - quote followed by paragraph (the real-world case) - multi-line quote ending with \n - quote with blank continuation + trailing \n (internal <br> stays, trailing <br> does not) Verified end-to-end with node against the actual JS regex. 244 renderer-adjacent tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes Five additional fixes on top of the blockquote grouping from the initial commit: 1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows line endings do not produce stray \r characters in rendered output 2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use inside blockquotes/lists) and the outer pass (for plain paragraphs). Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped. 3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐ via task-done/task-todo span wrappers. Checks [X] (uppercase) too. 4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its own <blockquote> element rather than passing through as literal >. Implemented by extracting the blockquote rule into _applyBlockquotes() which calls itself recursively on the stripped inner content. 5. Lists inside blockquotes: > - item now renders <ul><li> inside the blockquote instead of a literal "- item" string. Task list items work inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>). Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF strip at the top of renderMd pushed the autolink regex past the old limit. 68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py covering all constructs, edge cases, and combinations. * fix(renderer): restore space in blockquote prefix-strip regex Commit 04e7b53 changed the blockquote prefix-strip regex from /^>[ \t]?/ (consume "> ", "\t>", or just ">") to /^>[\t]?/ (only consume "\t>" or just ">") The space character was dropped from the character class. Since practically every blockquote an LLM produces is "> " (greater-than followed by a space), this leaves a leading space artifact on every stripped blockquote line. Worse, the leading space breaks the list-detection regex `^(?: )?[-*+] ` inside the new `_applyBlockquotes` helper — that regex requires either zero or two leading spaces, never one — so the new "list inside blockquote" feature never fired for the canonical input shape `> - item`. Reproducer (against the actual ui.js via node, before the fix): > Hello world → <blockquote> Hello world</blockquote> ^ phantom leading space > Steps: → <blockquote>Steps: > - one - one > - two - two</blockquote> ^ literal text, NOT a <ul>; lists-in-quote feature broken > - [x] done → blockquote with literal "[x] done", no checkbox span Tests passed despite the bug because tests/test_blockquote_rendering.py and tests/test_renderer_comprehensive.py validate against a Python mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e. the mirror is correct, the JS is not, and the static-mirror tests can't catch the divergence. Same shape of bug as commit 94d63d0 (phantom <br> in trailing line) where the mirror was right and the JS was wrong. Fix: restore the space character in the strip regex's character class. Add tests/test_renderer_js_behaviour.py — 11 tests that drive the ACTUAL renderMd via node and assert on rendered output for the most common LLM shapes (single-line quote, multi-line quote, list inside quote, task list inside quote, nested >>>, strikethrough inside and outside quote, top-level task list, quote followed by heading, multi-paragraph quote with list, CRLF normalisation). Verified: the buggy regex makes 6 of those 11 tests fail; the corrected regex makes all 11 pass. Suite: 2354 passed, 0 new failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Collapse agent session compression chains * Restore upstream changelog entries * fix(agent_sessions): bubble active compression chains to top by tip last_activity The original PR merge kept the chain head's id/title/started_at and overrode id/model/message_count/ended_at/end_reason from the tip — but did NOT override last_activity. Since the projected list is sorted by last_activity DESC and the WebUI sidebar surfaces updated_at = last_activity, an actively-used compression chain whose tip is being edited NOW would sort by the ROOT's old last_activity and fall below recently touched standalone sessions. Reproducer (with the harness against actual code, before the fix): - root: started 30 days ago, last msg 30 days ago - tip: started 28 days ago (parent_session_id=root), last msg 5 seconds ago - standalone: last msg 2 days ago Sidebar order with original PR: [0] standalone (48h ago) [1] active_tip (last_activity=root's 720h ago) ← wrong Sidebar order after fix: [0] active_tip (last_activity=tip's 0h ago) ← correct [1] standalone (48h ago) This matches Hermes Agent's own list_sessions_rich projection at hermes_state.py:903-909, which overrides "last_active" from the tip exactly so that the agent CLI's session list orders the same way. Add ``last_activity`` to the merge-from-tip key list, update the existing test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to expect tip-derived updated_at, and add test_compression_chain_bubbles_to_top_by_tip_activity locking in the bubble-to-top invariant — without this regression test the previous behaviour passed CI because no test exercised the sort order against a mixed set of chains and standalone sessions. The chain head's started_at (created_at) and title remain preserved, so users can still find the conversation by its original date and name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.216 release notes and version bump Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix. * chore: gitignore local-only review harness directory Adds .local-review/ to .gitignore so renderer drivers, sample inputs, fixture builders, and other reviewer scratch files do not accidentally get committed. Nothing under that path is ever shared in the repo; keeping the entry tracked makes the boundary explicit for any future contributor who creates the directory locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Keep reasoning chip visible for None effort * test(reasoning): pin chip render output via node, not just source regex The PR's static checks in test_reasoning_chip_btw_fixes.py validate the shape of _applyReasoningChip (no display='none' literal, the right classList.toggle call exists, the right label literals are in the function body) but pass even if the runtime detail is wrong — for example if `inactive` were inverted, _normalizeReasoningEffort mishandled whitespace, or _formatReasoningEffortLabel returned the wrong literal for an unknown input. Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive the actual _applyReasoningChip() via node and assert on the rendered DOM state for each effort value: TestChipAlwaysVisible - empty / null -> "Default" label, inactive=true - "none" -> "None" label, inactive=true - "low"/"high" -> verbatim label, inactive=false TestNormalizationEdgeCases - "NONE" -> normalises to "None" - " none " -> trims and normalises - unknown junk -> falls through visible, never hidden TestTitleAttributeAccessibility - title attribute carries the human-readable label for tooltip / screen-reader use Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught). Against this PR's ui.js: 11/11 pass. This pattern (drive the actual JS via node) caught two regex-only regressions in PR #1073 where the Python mirror was correct while the JS was broken. Same protection added here so the chip-visibility contract can't silently break in a future refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add #1074 to v0.50.216 changelog, bump test count to 2428 * fix(i18n): restore broken Unicode in Russian and Spanish steer strings Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally stripped the `\u` prefix from Unicode escape sequences in two locales, producing garbled literal hex strings visible to users: Spanish (es): - cmd_steer: correcci00f3n → corrección - cmd_steer_fallback: 2014 en cola → — en cola - busy_steer_fallback: 2014 en cola → — en cola - settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección - settings_busy_input_mode_steer: correcci00f3n → corrección Russian (ru): - settings_desc_busy_input_mode: the entire Cyrillic string was replaced with raw 4-hex-char code-points without the \u prefix (041e043f... instead of actual Cyrillic). Decoded: "Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer внедряет коррекцию без прерывания." Fix: write the correct characters directly (UTF-8 is the file encoding so embedding them literally is cleaner than \u escapes for long text). All other locales (en, de, zh, zh-Hant) were not affected — confirmed by grepping for bare hex run-ons in the updated file. Verified: node --check static/i18n.js passes; full pytest suite green (2365 passed, 47 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: remove duplicate compression chain entry from [Unreleased] --------- 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> Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-26 04:06:31 +00:00
let s=(raw||'').replace(/\r\n/g,'\n').replace(/\r/g,'\n');
v0.50.218: chat bubble overflow, project color picker, blockquote renderer (#1085) * fix(css): add overflow-wrap:anywhere to chat bubbles — prevents long URL overflow (#1080) * fix(projects): rename now works via dblclick timer guard + right-click color picker (#1078) * fix(renderer): block-level constructs inside blockquotes now render Fenced code blocks, headings, horizontal rules, and ordered lists inside blockquotes now render correctly. Six related bugs documented in blockquote-rendering-bugs.md were collapsed into one architectural fix in renderMd(). Bugs fixed (all 6): 1. Fenced code blocks inside blockquotes -- > prefixes leaked into the <pre> body and the blockquote got fragmented around the rendered code, sometimes leaving raw <pre>/<div class="pre-header"> as visible text. 2. Blank > continuation lines fragmented multi-paragraph blockquotes into separate <blockquote> elements with literal > between them. 3. ## headings inside blockquotes rendered as literal "##" text. 4. Numbered lists inside blockquotes rendered as plain prose. 5. Complex blockquote (mixed headings + code + list + inline code) collapsed into a monospace blob with raw markdown syntax leaking everywhere. 6. Horizontal rules (---) inside blockquotes rendered as literal text. Root cause: The per-line passes for fenced code, headings, hr, ordered lists all ran BEFORE the blockquote handler and could not match lines that started with >, so by the time blockquote stripping ran those constructs had already been mishandled. Fix: A new blockquote pre-pass at the top of renderMd(): - Walks lines fence-aware so > -prefixed lines inside non-blockquote code fences (e.g. shell prompts in bash code blocks) are not miscaptured as a blockquote. - Groups consecutive > -prefixed lines, strips the > prefix, and recursively calls renderMd() on the stripped content. The recursive call handles all block-level constructs (fenced code, headings, hr, ordered/unordered lists, nested blockquotes) using the same pipeline. - Wraps the rendered HTML in <blockquote> and stashes it with a \x00Q token. Restored at the very end of renderMd() so no later pass can mangle the inner HTML. The old _applyBlockquotes regex-replace is removed entirely along with its limited inline branches for nested blockquotes and unordered lists. Behaviour change: Blockquotes now produce CommonMark-compliant <p> wrapping for text content (was: bare text directly inside <blockquote>). The visual output is the same in browsers but the HTML structure is now standard. Tests: - 14 new behavioural tests in tests/test_renderer_js_behaviour.py drive the actual renderMd() via node and lock all 6 bug fixes. - .local-review/test_blockquote_bugs.js -- node harness covering the same scenarios, runnable manually for fast iteration. - 2407/2408 tests pass (1 pre-existing macOS-only failure deselected). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(renderer): entity decode before blockquote pre-pass + CSS margin fix - Move the &gt;/&lt;/&amp; entity-decode to run at the very top of renderMd(), before the blockquote pre-pass. Previously decode() ran at line 756 (after the pre-pass at line 697), so LLM output containing &gt;-encoded blockquotes was never matched by the pre-pass. - Add .msg-body blockquote p{margin:0} and .preview-md blockquote p{margin:0} so the new CommonMark-compliant <p> wrapping inside blockquotes doesn't add extra vertical spacing. Prior shape (bare text) had no default p-margins. - Add Node-driven tests: TestBlockquoteEntityEncodedInput covers &gt; prefix and &gt;-encoded fenced code inside blockquotes. - Add struct test: TestBlockquotePrePassOrdering::test_entity_decode_runs_before_blockquote_pre_pass locks decode < _bq_stash ordering in ui.js. Fixes found during Opus independent review of #1083. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: v0.50.218 release notes, test count 2458, roadmap update --------- 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-26 06:08:59 +00:00
// ── Entity decode: must run FIRST so &gt; lines become > for the blockquote
// pre-pass below. LLMs sometimes emit HTML-entity-encoded output; without this
// a blockquote sent as "&gt; text" would never be recognised as a blockquote.
s=s.replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&').replace(/&quot;/g,'"').replace(/&#39;/g,"'");
// ── Blockquote pre-pass (must run BEFORE every other markdown pass) ────────
// Group consecutive >-prefixed lines, strip the > prefix from each line,
// recursively render the stripped content with the full pipeline, and
// replace the group with a stash token. This is the only way fenced code,
// headings, hr, and ordered lists inside a blockquote can render correctly:
// the per-line passes downstream don't know about > prefixes, and by the
// time the blockquote handler used to run those passes had already mangled
// the >-prefixed lines.
//
// Walks lines (instead of using a single regex) so >-prefixed lines that
// sit inside a non-blockquote fenced block (e.g. a shell prompt in a
// ```bash``` example) are not miscaptured as a blockquote.
const _bq_stash=[];
s=(function _applyBlockquotes(input){
const lines=input.split('\n');
const out=[];
2026-05-05 15:36:17 +00:00
let inFence=false; // inside a non-blockquote backtick fence
let fenceLen=0;
v0.50.218: chat bubble overflow, project color picker, blockquote renderer (#1085) * fix(css): add overflow-wrap:anywhere to chat bubbles — prevents long URL overflow (#1080) * fix(projects): rename now works via dblclick timer guard + right-click color picker (#1078) * fix(renderer): block-level constructs inside blockquotes now render Fenced code blocks, headings, horizontal rules, and ordered lists inside blockquotes now render correctly. Six related bugs documented in blockquote-rendering-bugs.md were collapsed into one architectural fix in renderMd(). Bugs fixed (all 6): 1. Fenced code blocks inside blockquotes -- > prefixes leaked into the <pre> body and the blockquote got fragmented around the rendered code, sometimes leaving raw <pre>/<div class="pre-header"> as visible text. 2. Blank > continuation lines fragmented multi-paragraph blockquotes into separate <blockquote> elements with literal > between them. 3. ## headings inside blockquotes rendered as literal "##" text. 4. Numbered lists inside blockquotes rendered as plain prose. 5. Complex blockquote (mixed headings + code + list + inline code) collapsed into a monospace blob with raw markdown syntax leaking everywhere. 6. Horizontal rules (---) inside blockquotes rendered as literal text. Root cause: The per-line passes for fenced code, headings, hr, ordered lists all ran BEFORE the blockquote handler and could not match lines that started with >, so by the time blockquote stripping ran those constructs had already been mishandled. Fix: A new blockquote pre-pass at the top of renderMd(): - Walks lines fence-aware so > -prefixed lines inside non-blockquote code fences (e.g. shell prompts in bash code blocks) are not miscaptured as a blockquote. - Groups consecutive > -prefixed lines, strips the > prefix, and recursively calls renderMd() on the stripped content. The recursive call handles all block-level constructs (fenced code, headings, hr, ordered/unordered lists, nested blockquotes) using the same pipeline. - Wraps the rendered HTML in <blockquote> and stashes it with a \x00Q token. Restored at the very end of renderMd() so no later pass can mangle the inner HTML. The old _applyBlockquotes regex-replace is removed entirely along with its limited inline branches for nested blockquotes and unordered lists. Behaviour change: Blockquotes now produce CommonMark-compliant <p> wrapping for text content (was: bare text directly inside <blockquote>). The visual output is the same in browsers but the HTML structure is now standard. Tests: - 14 new behavioural tests in tests/test_renderer_js_behaviour.py drive the actual renderMd() via node and lock all 6 bug fixes. - .local-review/test_blockquote_bugs.js -- node harness covering the same scenarios, runnable manually for fast iteration. - 2407/2408 tests pass (1 pre-existing macOS-only failure deselected). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(renderer): entity decode before blockquote pre-pass + CSS margin fix - Move the &gt;/&lt;/&amp; entity-decode to run at the very top of renderMd(), before the blockquote pre-pass. Previously decode() ran at line 756 (after the pre-pass at line 697), so LLM output containing &gt;-encoded blockquotes was never matched by the pre-pass. - Add .msg-body blockquote p{margin:0} and .preview-md blockquote p{margin:0} so the new CommonMark-compliant <p> wrapping inside blockquotes doesn't add extra vertical spacing. Prior shape (bare text) had no default p-margins. - Add Node-driven tests: TestBlockquoteEntityEncodedInput covers &gt; prefix and &gt;-encoded fenced code inside blockquotes. - Add struct test: TestBlockquotePrePassOrdering::test_entity_decode_runs_before_blockquote_pre_pass locks decode < _bq_stash ordering in ui.js. Fixes found during Opus independent review of #1083. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: v0.50.218 release notes, test count 2458, roadmap update --------- 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-26 06:08:59 +00:00
let bqStart=-1;
const flush=(end)=>{
if(bqStart<0) return;
// Strip "> " prefix (and bare ">" → empty) from each line
const stripped=lines.slice(bqStart,end).map(l=>l.replace(/^> ?/,'')).join('\n');
// Recursive call: full pipeline on stripped content. Handles fenced
// code, headings, hr, ordered/unordered lists, nested blockquotes
// (>>) — anything that renderMd handles at the top level.
const rendered=renderMd(stripped);
_bq_stash.push('<blockquote>'+rendered+'</blockquote>');
// Surround the token with blank lines so the paragraph splitter
// isolates it as its own chunk (otherwise the token gets wrapped
// in <p>...<br> with adjacent text, producing invalid HTML).
out.push('');
out.push('\x00Q'+(_bq_stash.length-1)+'\x00');
out.push('');
bqStart=-1;
};
for(let i=0;i<lines.length;i++){
const line=lines[i];
if(inFence){
out.push(line);
2026-05-05 15:36:17 +00:00
if(_isBacktickFenceClose(line,fenceLen)){inFence=false;fenceLen=0;}
v0.50.218: chat bubble overflow, project color picker, blockquote renderer (#1085) * fix(css): add overflow-wrap:anywhere to chat bubbles — prevents long URL overflow (#1080) * fix(projects): rename now works via dblclick timer guard + right-click color picker (#1078) * fix(renderer): block-level constructs inside blockquotes now render Fenced code blocks, headings, horizontal rules, and ordered lists inside blockquotes now render correctly. Six related bugs documented in blockquote-rendering-bugs.md were collapsed into one architectural fix in renderMd(). Bugs fixed (all 6): 1. Fenced code blocks inside blockquotes -- > prefixes leaked into the <pre> body and the blockquote got fragmented around the rendered code, sometimes leaving raw <pre>/<div class="pre-header"> as visible text. 2. Blank > continuation lines fragmented multi-paragraph blockquotes into separate <blockquote> elements with literal > between them. 3. ## headings inside blockquotes rendered as literal "##" text. 4. Numbered lists inside blockquotes rendered as plain prose. 5. Complex blockquote (mixed headings + code + list + inline code) collapsed into a monospace blob with raw markdown syntax leaking everywhere. 6. Horizontal rules (---) inside blockquotes rendered as literal text. Root cause: The per-line passes for fenced code, headings, hr, ordered lists all ran BEFORE the blockquote handler and could not match lines that started with >, so by the time blockquote stripping ran those constructs had already been mishandled. Fix: A new blockquote pre-pass at the top of renderMd(): - Walks lines fence-aware so > -prefixed lines inside non-blockquote code fences (e.g. shell prompts in bash code blocks) are not miscaptured as a blockquote. - Groups consecutive > -prefixed lines, strips the > prefix, and recursively calls renderMd() on the stripped content. The recursive call handles all block-level constructs (fenced code, headings, hr, ordered/unordered lists, nested blockquotes) using the same pipeline. - Wraps the rendered HTML in <blockquote> and stashes it with a \x00Q token. Restored at the very end of renderMd() so no later pass can mangle the inner HTML. The old _applyBlockquotes regex-replace is removed entirely along with its limited inline branches for nested blockquotes and unordered lists. Behaviour change: Blockquotes now produce CommonMark-compliant <p> wrapping for text content (was: bare text directly inside <blockquote>). The visual output is the same in browsers but the HTML structure is now standard. Tests: - 14 new behavioural tests in tests/test_renderer_js_behaviour.py drive the actual renderMd() via node and lock all 6 bug fixes. - .local-review/test_blockquote_bugs.js -- node harness covering the same scenarios, runnable manually for fast iteration. - 2407/2408 tests pass (1 pre-existing macOS-only failure deselected). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(renderer): entity decode before blockquote pre-pass + CSS margin fix - Move the &gt;/&lt;/&amp; entity-decode to run at the very top of renderMd(), before the blockquote pre-pass. Previously decode() ran at line 756 (after the pre-pass at line 697), so LLM output containing &gt;-encoded blockquotes was never matched by the pre-pass. - Add .msg-body blockquote p{margin:0} and .preview-md blockquote p{margin:0} so the new CommonMark-compliant <p> wrapping inside blockquotes doesn't add extra vertical spacing. Prior shape (bare text) had no default p-margins. - Add Node-driven tests: TestBlockquoteEntityEncodedInput covers &gt; prefix and &gt;-encoded fenced code inside blockquotes. - Add struct test: TestBlockquotePrePassOrdering::test_entity_decode_runs_before_blockquote_pre_pass locks decode < _bq_stash ordering in ui.js. Fixes found during Opus independent review of #1083. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: v0.50.218 release notes, test count 2458, roadmap update --------- 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-26 06:08:59 +00:00
continue;
}
2026-05-05 15:36:17 +00:00
const fenceOpen=_matchBacktickFenceLine(line);
if(fenceOpen){
v0.50.218: chat bubble overflow, project color picker, blockquote renderer (#1085) * fix(css): add overflow-wrap:anywhere to chat bubbles — prevents long URL overflow (#1080) * fix(projects): rename now works via dblclick timer guard + right-click color picker (#1078) * fix(renderer): block-level constructs inside blockquotes now render Fenced code blocks, headings, horizontal rules, and ordered lists inside blockquotes now render correctly. Six related bugs documented in blockquote-rendering-bugs.md were collapsed into one architectural fix in renderMd(). Bugs fixed (all 6): 1. Fenced code blocks inside blockquotes -- > prefixes leaked into the <pre> body and the blockquote got fragmented around the rendered code, sometimes leaving raw <pre>/<div class="pre-header"> as visible text. 2. Blank > continuation lines fragmented multi-paragraph blockquotes into separate <blockquote> elements with literal > between them. 3. ## headings inside blockquotes rendered as literal "##" text. 4. Numbered lists inside blockquotes rendered as plain prose. 5. Complex blockquote (mixed headings + code + list + inline code) collapsed into a monospace blob with raw markdown syntax leaking everywhere. 6. Horizontal rules (---) inside blockquotes rendered as literal text. Root cause: The per-line passes for fenced code, headings, hr, ordered lists all ran BEFORE the blockquote handler and could not match lines that started with >, so by the time blockquote stripping ran those constructs had already been mishandled. Fix: A new blockquote pre-pass at the top of renderMd(): - Walks lines fence-aware so > -prefixed lines inside non-blockquote code fences (e.g. shell prompts in bash code blocks) are not miscaptured as a blockquote. - Groups consecutive > -prefixed lines, strips the > prefix, and recursively calls renderMd() on the stripped content. The recursive call handles all block-level constructs (fenced code, headings, hr, ordered/unordered lists, nested blockquotes) using the same pipeline. - Wraps the rendered HTML in <blockquote> and stashes it with a \x00Q token. Restored at the very end of renderMd() so no later pass can mangle the inner HTML. The old _applyBlockquotes regex-replace is removed entirely along with its limited inline branches for nested blockquotes and unordered lists. Behaviour change: Blockquotes now produce CommonMark-compliant <p> wrapping for text content (was: bare text directly inside <blockquote>). The visual output is the same in browsers but the HTML structure is now standard. Tests: - 14 new behavioural tests in tests/test_renderer_js_behaviour.py drive the actual renderMd() via node and lock all 6 bug fixes. - .local-review/test_blockquote_bugs.js -- node harness covering the same scenarios, runnable manually for fast iteration. - 2407/2408 tests pass (1 pre-existing macOS-only failure deselected). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(renderer): entity decode before blockquote pre-pass + CSS margin fix - Move the &gt;/&lt;/&amp; entity-decode to run at the very top of renderMd(), before the blockquote pre-pass. Previously decode() ran at line 756 (after the pre-pass at line 697), so LLM output containing &gt;-encoded blockquotes was never matched by the pre-pass. - Add .msg-body blockquote p{margin:0} and .preview-md blockquote p{margin:0} so the new CommonMark-compliant <p> wrapping inside blockquotes doesn't add extra vertical spacing. Prior shape (bare text) had no default p-margins. - Add Node-driven tests: TestBlockquoteEntityEncodedInput covers &gt; prefix and &gt;-encoded fenced code inside blockquotes. - Add struct test: TestBlockquotePrePassOrdering::test_entity_decode_runs_before_blockquote_pre_pass locks decode < _bq_stash ordering in ui.js. Fixes found during Opus independent review of #1083. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: v0.50.218 release notes, test count 2458, roadmap update --------- 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-26 06:08:59 +00:00
flush(i);
out.push(line);
inFence=true;
2026-05-05 15:36:17 +00:00
fenceLen=fenceOpen.len;
v0.50.218: chat bubble overflow, project color picker, blockquote renderer (#1085) * fix(css): add overflow-wrap:anywhere to chat bubbles — prevents long URL overflow (#1080) * fix(projects): rename now works via dblclick timer guard + right-click color picker (#1078) * fix(renderer): block-level constructs inside blockquotes now render Fenced code blocks, headings, horizontal rules, and ordered lists inside blockquotes now render correctly. Six related bugs documented in blockquote-rendering-bugs.md were collapsed into one architectural fix in renderMd(). Bugs fixed (all 6): 1. Fenced code blocks inside blockquotes -- > prefixes leaked into the <pre> body and the blockquote got fragmented around the rendered code, sometimes leaving raw <pre>/<div class="pre-header"> as visible text. 2. Blank > continuation lines fragmented multi-paragraph blockquotes into separate <blockquote> elements with literal > between them. 3. ## headings inside blockquotes rendered as literal "##" text. 4. Numbered lists inside blockquotes rendered as plain prose. 5. Complex blockquote (mixed headings + code + list + inline code) collapsed into a monospace blob with raw markdown syntax leaking everywhere. 6. Horizontal rules (---) inside blockquotes rendered as literal text. Root cause: The per-line passes for fenced code, headings, hr, ordered lists all ran BEFORE the blockquote handler and could not match lines that started with >, so by the time blockquote stripping ran those constructs had already been mishandled. Fix: A new blockquote pre-pass at the top of renderMd(): - Walks lines fence-aware so > -prefixed lines inside non-blockquote code fences (e.g. shell prompts in bash code blocks) are not miscaptured as a blockquote. - Groups consecutive > -prefixed lines, strips the > prefix, and recursively calls renderMd() on the stripped content. The recursive call handles all block-level constructs (fenced code, headings, hr, ordered/unordered lists, nested blockquotes) using the same pipeline. - Wraps the rendered HTML in <blockquote> and stashes it with a \x00Q token. Restored at the very end of renderMd() so no later pass can mangle the inner HTML. The old _applyBlockquotes regex-replace is removed entirely along with its limited inline branches for nested blockquotes and unordered lists. Behaviour change: Blockquotes now produce CommonMark-compliant <p> wrapping for text content (was: bare text directly inside <blockquote>). The visual output is the same in browsers but the HTML structure is now standard. Tests: - 14 new behavioural tests in tests/test_renderer_js_behaviour.py drive the actual renderMd() via node and lock all 6 bug fixes. - .local-review/test_blockquote_bugs.js -- node harness covering the same scenarios, runnable manually for fast iteration. - 2407/2408 tests pass (1 pre-existing macOS-only failure deselected). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(renderer): entity decode before blockquote pre-pass + CSS margin fix - Move the &gt;/&lt;/&amp; entity-decode to run at the very top of renderMd(), before the blockquote pre-pass. Previously decode() ran at line 756 (after the pre-pass at line 697), so LLM output containing &gt;-encoded blockquotes was never matched by the pre-pass. - Add .msg-body blockquote p{margin:0} and .preview-md blockquote p{margin:0} so the new CommonMark-compliant <p> wrapping inside blockquotes doesn't add extra vertical spacing. Prior shape (bare text) had no default p-margins. - Add Node-driven tests: TestBlockquoteEntityEncodedInput covers &gt; prefix and &gt;-encoded fenced code inside blockquotes. - Add struct test: TestBlockquotePrePassOrdering::test_entity_decode_runs_before_blockquote_pre_pass locks decode < _bq_stash ordering in ui.js. Fixes found during Opus independent review of #1083. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: v0.50.218 release notes, test count 2458, roadmap update --------- 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-26 06:08:59 +00:00
continue;
}
if(/^>/.test(line)){
if(bqStart<0) bqStart=i;
} else {
flush(i);
out.push(line);
}
}
flush(lines.length);
return out.join('\n');
})(s);
// ── MEDIA: token stash (must run first, before any other processing) ───────
// Detect MEDIA:<path-or-url> tokens emitted by the agent (e.g. screenshots,
// generated images) and replace them with inline <img> or download links.
// Stashed so the path/URL is never processed as markdown.
const media_stash=[];
s=s.replace(/MEDIA:([^\s\)\]]+)/g,(_,raw_ref)=>{
media_stash.push(raw_ref);
return '\x00D'+(media_stash.length-1)+'\x00';
});
// ── End MEDIA stash ─────────────────────────────────────────────────────────
v0.46.0: security, Docker UID/GID, model discovery, i18n, cancel fix * fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239) Adds decode() helper in renderMd() to fix double-escaping of HTML entities from LLM output (e.g. &lt;code&gt; becoming &amp;lt;code&amp;gt; instead of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns. Also adds 40+ missing zh (Simplified Chinese) translation keys and a new zh-Hant (Traditional Chinese) locale with 163 keys. Fix applied: removed duplicate settings_label_notifications key in both zh and zh-Hant locales. Fixes #240 * fix: restore custom model list discovery with config api key (#238) get_available_models() now reads api_key from config.yaml before env vars: 1. model.api_key 2. providers.<active>.api_key / providers.custom.api_key 3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.) Also adds OpenAI/Python User-Agent header and a regression test covering authenticated /v1/models discovery. Fixes users with LM Studio / Ollama custom endpoints configured in config.yaml whose model picker silently collapsed to the default model. * feat: Docker UID/GID matching to avoid root-owned .hermes files (#237) Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so container files match the host user UID/GID. Prevents .hermes volume mounts from being owned by root when using a non-root host user. Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000). Readme updated with setup instructions. Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml that was overriding the ${GID:-1000} variable expansion. * security: redact credentials from API responses and fix credential file permissions (#243) Adds response-layer credential redaction to three endpoints: - GET /api/session — messages[], tool_calls[], and title - GET /api/session/export — download also redacted - SSE done event — session payload in stream - GET /api/memory — MEMORY.md and USER.md content Adds api/startup.py with fix_credential_permissions() at server startup. Adds 13 tests in tests/test_security_redaction.py. Merged with #237 container detection changes in server.py. * fix: cancel button now interrupts agent and cleans up UI state (#244) Wires agent.interrupt() into cancel_stream() so the backend actually stops tool execution when the user clicks Cancel, rather than only stopping the SSE stream while the agent keeps running. Changes: - api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent) - api/streaming.py: stores agent in AGENT_INSTANCES after creation, checks CANCEL_FLAGS immediately after store (race condition fix), calls agent.interrupt() in cancel_stream(), cleans up in finally block - static/boot.js: removes stale setStatus(cancelling) call - static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel Race condition fix: after storing agent in AGENT_INSTANCES, immediately checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during agent init) and interrupts before starting. Check is inside the same STREAMS_LOCK acquisition, making it atomic. New test file: tests/test_cancel_interrupt.py with 6 unit tests. * docs: v0.46.0 release notes, bump version, update test counts --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 17:17:52 +00:00
// Pre-pass: decode HTML entities first so markdown processing works correctly.
// This prevents double-escaping when LLM outputs entities like &lt; &gt; &amp;
const decode=s=>s.replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&').replace(/&quot;/g,'"').replace(/&#39;/g,"'");
s=decode(s);
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests renderMd() now correctly renders safe inline HTML tags that AI models emit in their responses: Pre-pass (ui.js): Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown equivalents (**text**, *text*, `text`, newline) before the pipeline runs. Code blocks and backtick spans are stashed first so their content is never modified. inlineMd() helper (ui.js): New helper for processing inline formatting inside list items, blockquotes, and headings. Previously these used esc() directly, which escaped <strong>/<code> tags that had already been converted from HTML by the pre-pass — causing them to appear as literal &lt;strong&gt; text instead of rendering as bold. inlineMd() applies bold/italic/code processing and then escapes only unknown tags. Safety net (ui.js): After the full pipeline, any HTML tags NOT emitted by our own renderer (i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself produces. XSS is fully blocked. Active session gold style (sessions.js, style.css): Active session item now uses gold/amber (#e8a030) instead of blue, matching the logo gradient color for better visual hierarchy. Project color border-left is skipped when the session is active (gold always wins). Session items get border-radius: 0 8px 8px 0 to complement the left border indicator. Tests (tests/test_sprint16.py — 74 tests): - Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present - Behavioural: all safe tags render in paragraphs, list items (ul+ol), blockquotes, headings (h1/h2/h3) - Exact screenshot regression: the 4-item list with <strong> labels and <code> values that was showing as literal text - XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>, <object>, XSS inside bold, XSS nested inside <strong>) - Edge cases: code block protection, double-escaping guards, br tag, mixed markdown+HTML, inlineMd called in list/blockquote handlers Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
// Pre-pass: convert safe inline HTML tags the model may emit into their
// markdown equivalents so the pipeline can render them correctly.
// Only runs OUTSIDE fenced code blocks and backtick spans (stash + restore).
// Unsafe tags (anything not in the allowlist) are left as-is and will be
// HTML-escaped by esc() when they reach an innerHTML assignment -- no XSS risk.
2026-04-28 00:43:36 +00:00
// Fence stash: protect code blocks and backtick spans from all further processing.
// Must run BEFORE math_stash so $..$ inside code spans is not extracted as math.
// Split into fenced blocks (\x00P — kept stashed until after all markdown passes)
// and inline backtick spans (\x00F — restored before bold/italic so **`code`** works).
// Fenced blocks are converted to <pre><code> here so their content is HTML-escaped
// and never exposed to list/heading/table regexes that could corrupt the layout.
// Fixes #1154: diff/patch lines inside fenced blocks (e.g. + added, - removed)
// were matching the unordered-list regex and injecting <ul>/<li> inside <pre>,
// breaking </pre> closure and corrupting all subsequent message rendering.
const _preBlock_stash=[];
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests renderMd() now correctly renders safe inline HTML tags that AI models emit in their responses: Pre-pass (ui.js): Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown equivalents (**text**, *text*, `text`, newline) before the pipeline runs. Code blocks and backtick spans are stashed first so their content is never modified. inlineMd() helper (ui.js): New helper for processing inline formatting inside list items, blockquotes, and headings. Previously these used esc() directly, which escaped <strong>/<code> tags that had already been converted from HTML by the pre-pass — causing them to appear as literal &lt;strong&gt; text instead of rendering as bold. inlineMd() applies bold/italic/code processing and then escapes only unknown tags. Safety net (ui.js): After the full pipeline, any HTML tags NOT emitted by our own renderer (i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself produces. XSS is fully blocked. Active session gold style (sessions.js, style.css): Active session item now uses gold/amber (#e8a030) instead of blue, matching the logo gradient color for better visual hierarchy. Project color border-left is skipped when the session is active (gold always wins). Session items get border-radius: 0 8px 8px 0 to complement the left border indicator. Tests (tests/test_sprint16.py — 74 tests): - Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present - Behavioural: all safe tags render in paragraphs, list items (ul+ol), blockquotes, headings (h1/h2/h3) - Exact screenshot regression: the 4-item list with <strong> labels and <code> values that was showing as literal text - XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>, <object>, XSS inside bold, XSS nested inside <strong>) - Edge cases: code block protection, double-escaping guards, br tag, mixed markdown+HTML, inlineMd called in list/blockquote handlers Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
const fence_stash=[];
fix(renderer): line-anchor fence regex to prevent mid-line ``` corruption (#1438) The markdown fence regex /```([\s\S]*?)```/g had no line anchoring. A literal triple backtick inside code block content (e.g. a regex with ``` in a lookbehind, or a script that documents fences) terminated the outer fence at the wrong place. The leaked tail then went through bold/italic/inline-code passes, eating `*` characters as italic markers and emitting literal </strong> tags into the rendered output. CommonMark §4.5 requires that an opening code fence be the first non-whitespace content of a line (up to 3 spaces of indent allowed) and that the closing fence also start a line. This patch updates 3 sites + the Python mirror to use that invariant: static/ui.js:1559 renderMd() fenced-block stash (assistant messages) static/ui.js:66 _renderUserFencedBlocks() (user messages) static/ui.js:2599 _stripForTTS() (TTS speech pre-strip) tests/test_sprint16.py Python mirror Pattern: (^|\n)[ ]{0,3}```(?:([\s\S]*?)\n)?[ ]{0,3}```(?=\n|$) The non-capturing (?:...\n)? group keeps empty fences (```\n```) working; without it, a body+\n is required and the closing fence on the very next line no longer matches. The lead group (^|\n) is prefixed back to the stash token so paragraphs above don't bleed into the <pre> block. 20 regression tests in tests/test_issue1438_fence_anchoring.py cover: - Cygnus's exact repro from Discord (May 1 2026) - Inline ``` mid-paragraph (must not open fence) - Partial/streaming fence with no close (must not eat content) - Empty fences with and without language tag - 3-space indented fences (allowed) vs 4-space (not a fence) - Multiple adjacent blocks - Bold/italic/inline-code surviving after a fence - Source-level guards on all 3 patched sites + lead-prefix invariant Empirical browser verification (live JS, on bug repro): Before fix: </code></pre>[^\n]<em>|%%[ \t]</em>... ← truncated, italic leak After fix: <pre><code>...```[^\n]*|%%...</code></pre> ← intact, regex preserved Tests: 3678 passed (+20 from new test file, was 3658), 0 failures. Reported-By: Cygnus (Discord) Relayed-By: @AvidFuturist Closes #1438
2026-05-02 02:30:20 +00:00
// CommonMark §4.5: opening fence must start a line (with up to 3 spaces of indent)
2026-05-05 15:36:17 +00:00
// and closing fence must start a line with the same backtick char and at least
// as many backticks as the opener. Without line/fence-length anchoring, a literal
// ``` inside a code block (e.g. a nested markdown example) terminates the outer
// block at the wrong place, leaking content into the markdown stream where
// bold/italic/inline-code passes corrupt it. Fixes #1438 and #1696.
s=s.replace(/(^|\n)[ ]{0,3}(`{3,})([^\n`]*)\n(?:([\s\S]*?)\n)?[ ]{0,3}\2`*[ \t]*(?=\n|$)/g,(_,lead,_fence,info,code)=>{
const langInfo=(info||'').trim();
const langMatch=langInfo.match(/^(\w[\w+-]*)$/);
const lang=langMatch?(langMatch[1]||'').trim().toLowerCase():'';
code=code||'';
const codeLines=code.split('\n');
const firstCodeLine=codeLines.find(line=>line.trim())||'';
const firstMermaidLine=codeLines.map(line=>line.trim()).find(line=>line&&!line.startsWith('%%'))||'';
const looksLikeLineNumberedToolOutput=/^\s*\d+\|/.test(firstCodeLine);
const looksLikeMermaidStart=firstMermaidLine==='---'||/^(graph|flowchart|sequenceDiagram|classDiagram|classDiagram-v2|stateDiagram|stateDiagram-v2|erDiagram|journey|gantt|pie|gitGraph|mindmap|timeline|quadrantChart|requirementDiagram|C4Context|C4Container|C4Component|C4Dynamic|c4Context|c4Container|c4Component|c4Dynamic|sankey-beta|block-beta|packet-beta|xychart-beta|kanban|architecture-beta)\b/.test(firstMermaidLine);
if(lang==='mermaid'&&!looksLikeLineNumberedToolOutput&&looksLikeMermaidStart){
2026-04-28 00:43:36 +00:00
const id='mermaid-'+Math.random().toString(36).slice(2,10);
_preBlock_stash.push(`<div class="mermaid-block" data-mermaid-id="${id}">${esc(code.trim())}</div>`);
2026-04-28 00:43:36 +00:00
} else {
const h=lang?`<div class="pre-header">${esc(lang)}</div>`:'';
const langAttr=lang?` class="language-${esc(lang)}"`:'';
// For diff/patch blocks, wrap each line in a colored span
if(lang==='diff'||lang==='patch'){
const colored=esc(code.replace(/\n$/,'')).split('\n').map(line=>{
if(line.startsWith('@@')) return `<span class="diff-line diff-hunk">${line}</span>`;
if(line.startsWith('+')) return `<span class="diff-line diff-plus">${line}</span>`;
if(line.startsWith('-')) return `<span class="diff-line diff-minus">${line}</span>`;
return `<span class="diff-line">${line}</span>`;
}).join('\n');
_preBlock_stash.push(`${h}<pre class="diff-block"><code${langAttr}>${colored}</code></pre>`);
// For JSON/YAML blocks, add tree-view placeholder with raw data
} else if(lang==='json'||lang==='yaml'){
const rawCode=esc(code.replace(/\n$/,''));
// Encode newlines as &#10; to prevent HTML attribute normalization
// (browsers collapse \n to spaces inside attribute values).
const rawAttr=rawCode.replace(/"/g,'&quot;').replace(/\n/g,'&#10;');
const blockId='tree-'+Math.random().toString(36).slice(2,10);
_preBlock_stash.push(`<div class="code-tree-wrap" data-raw="${rawAttr}" data-lang="${lang}" id="${blockId}">${h}<pre class="tree-raw-view"><code${langAttr}>${rawCode}</code></pre></div>`);
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
// CSV blocks → render as styled table
} else if(lang==='csv'){
const rows=code.replace(/\n$/,'').split('\n').filter(r=>r.trim());
if(rows.length>=2){
const headers=rows[0].split(',').map(c=>c.trim());
const body=rows.slice(1).map(r=>'<tr>'+r.split(',').map(c=>`<td>${esc(c.trim())}</td>`).join('')+'</tr>').join('');
_preBlock_stash.push(`${h}<div class="csv-table-wrap"><table class="csv-table"><thead><tr>${headers.map(h=>`<th>${esc(h)}</th>`).join('')}</tr></thead><tbody>${body}</tbody></table></div>`);
} else {
_preBlock_stash.push(`${h}<pre><code${langAttr}>${esc(code.replace(/\n$/,''))}</code></pre>`);
}
} else {
_preBlock_stash.push(`${h}<pre><code${langAttr}>${esc(code.replace(/\n$/,''))}</code></pre>`);
}
2026-04-28 00:43:36 +00:00
}
fix(renderer): line-anchor fence regex to prevent mid-line ``` corruption (#1438) The markdown fence regex /```([\s\S]*?)```/g had no line anchoring. A literal triple backtick inside code block content (e.g. a regex with ``` in a lookbehind, or a script that documents fences) terminated the outer fence at the wrong place. The leaked tail then went through bold/italic/inline-code passes, eating `*` characters as italic markers and emitting literal </strong> tags into the rendered output. CommonMark §4.5 requires that an opening code fence be the first non-whitespace content of a line (up to 3 spaces of indent allowed) and that the closing fence also start a line. This patch updates 3 sites + the Python mirror to use that invariant: static/ui.js:1559 renderMd() fenced-block stash (assistant messages) static/ui.js:66 _renderUserFencedBlocks() (user messages) static/ui.js:2599 _stripForTTS() (TTS speech pre-strip) tests/test_sprint16.py Python mirror Pattern: (^|\n)[ ]{0,3}```(?:([\s\S]*?)\n)?[ ]{0,3}```(?=\n|$) The non-capturing (?:...\n)? group keeps empty fences (```\n```) working; without it, a body+\n is required and the closing fence on the very next line no longer matches. The lead group (^|\n) is prefixed back to the stash token so paragraphs above don't bleed into the <pre> block. 20 regression tests in tests/test_issue1438_fence_anchoring.py cover: - Cygnus's exact repro from Discord (May 1 2026) - Inline ``` mid-paragraph (must not open fence) - Partial/streaming fence with no close (must not eat content) - Empty fences with and without language tag - 3-space indented fences (allowed) vs 4-space (not a fence) - Multiple adjacent blocks - Bold/italic/inline-code surviving after a fence - Source-level guards on all 3 patched sites + lead-prefix invariant Empirical browser verification (live JS, on bug repro): Before fix: </code></pre>[^\n]<em>|%%[ \t]</em>... ← truncated, italic leak After fix: <pre><code>...```[^\n]*|%%...</code></pre> ← intact, regex preserved Tests: 3678 passed (+20 from new test file, was 3658), 0 failures. Reported-By: Cygnus (Discord) Relayed-By: @AvidFuturist Closes #1438
2026-05-02 02:30:20 +00:00
return lead+'\x00P'+(_preBlock_stash.length-1)+'\x00';
2026-04-28 00:43:36 +00:00
});
s=s.replace(/`([^`\n]+)`/g,(_,c)=>{fence_stash.push('<code>'+esc(c)+'</code>');return '\x00F'+(fence_stash.length-1)+'\x00';});
feat: KaTeX math rendering for LaTeX in chat + workspace previews (#352) * feat: KaTeX math rendering for $..$ and $$..$$ in chat and previews (fixes #347) - Stash math delimiters before markdown pipeline, restore as .katex-block/.katex-inline elements - KaTeX JS lazy-loaded from CDN on first math block (mirrors mermaid pattern) - KaTeX CSS loaded eagerly in <head> to prevent layout shift - SRI hashes on both CDN tags - throwOnError:false — bad LaTeX degrades to code span - Supports $$, $, \\(...\\), \\[...\\] delimiters - 18 new tests, 831/831 passing * fix: remove invalid \' escape sequences in math stash lines Lines 311, 314, 316, 317 had \' (backslash-quote) instead of plain ' in the arrow function bodies. This is a JS syntax error — node --check fails with 'Invalid or unexpected token'. Likely caused by a serialization artifact during code generation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: swap stash order (fence before math) to protect code spans; add renderKatexBlocks to workspace preview - static/ui.js: fence_stash now runs BEFORE math_stash so dollar signs inside backtick code spans are not extracted as math. Previously `$x$` would render as KaTeX inside a <code> tag instead of showing the literal string $x$. - static/workspace.js: add requestAnimationFrame(renderKatexBlocks) after markdown preview renders so math works in workspace file previews, not only in chat messages. * feat: KaTeX math rendering + stash order fix + workspace wiring (#352) - tests/test_issue347.py: 11 new tests (29 total) covering fence-before-math ordering, workspace.js renderKatexBlocks call, stash token distinctness, false-positive prevention, safe-tags boundary check - CHANGELOG.md: v0.50.15 entry; 870 tests total (up from 841) * fix: use literal null byte (\x00M) in math stash token — matches restore regex The original PR's second commit (fix: remove invalid \' escapes) accidentally doubled the backslash in the math stash tokens: '\\x00M' is a 5-char string (backslash + x + 0 + 0 + M) but the restore regex /\x00M/ expects a null byte. Result: $...$ in messages produced visible \x00M0\x00 tokens instead of KaTeX spans. Changed all 4 math stash return statements to use '\x00M' (single backslash = null byte, same convention as fence_stash's '\x00F'). Also updates test_stash_tokens_distinct to check for the correct pattern. * fix: add null-byte token test; update CHANGELOG to v0.50.15 with fixes - tests/test_issue347.py: add test_math_stash_token_uses_single_backslash_null_byte to catch the \\x00M double-backslash regression; 30 tests total (up from 29) - CHANGELOG.md: v0.50.15 entry documents all fixes including the token bug and workspace preview wiring; 871 tests total --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:40:15 +00:00
// Math stash: protect $$..$$ and $..$ from markdown processing
// Runs AFTER fence_stash so backtick code spans protect their dollar-sign contents
const math_stash=[];
// Display math: $$...$$ (must come before inline to avoid mis-parsing)
s=s.replace(/\$\$([\s\S]+?)\$\$/g,(_,m)=>{math_stash.push({type:'display',src:m});return '\x00M'+(math_stash.length-1)+'\x00';});
// Inline math: $...$ — require non-space at boundaries to avoid false positives
// e.g. "costs $5 and $10" should not trigger (space after opening $)
s=s.replace(/\$([^\s$\n][^$\n]*?[^\s$\n]|\S)\$/g,(_,m)=>{math_stash.push({type:'inline',src:m});return '\x00M'+(math_stash.length-1)+'\x00';});
// Also stash \(...\) and \[...\] LaTeX delimiters
s=s.replace(/\\\\\((.+?)\\\\\)/g,(_,m)=>{math_stash.push({type:'inline',src:m});return '\x00M'+(math_stash.length-1)+'\x00';});
s=s.replace(/\\\\\[(.+?)\\\\\]/gs,(_,m)=>{math_stash.push({type:'display',src:m});return '\x00M'+(math_stash.length-1)+'\x00';});
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests renderMd() now correctly renders safe inline HTML tags that AI models emit in their responses: Pre-pass (ui.js): Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown equivalents (**text**, *text*, `text`, newline) before the pipeline runs. Code blocks and backtick spans are stashed first so their content is never modified. inlineMd() helper (ui.js): New helper for processing inline formatting inside list items, blockquotes, and headings. Previously these used esc() directly, which escaped <strong>/<code> tags that had already been converted from HTML by the pre-pass — causing them to appear as literal &lt;strong&gt; text instead of rendering as bold. inlineMd() applies bold/italic/code processing and then escapes only unknown tags. Safety net (ui.js): After the full pipeline, any HTML tags NOT emitted by our own renderer (i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself produces. XSS is fully blocked. Active session gold style (sessions.js, style.css): Active session item now uses gold/amber (#e8a030) instead of blue, matching the logo gradient color for better visual hierarchy. Project color border-left is skipped when the session is active (gold always wins). Session items get border-radius: 0 8px 8px 0 to complement the left border indicator. Tests (tests/test_sprint16.py — 74 tests): - Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present - Behavioural: all safe tags render in paragraphs, list items (ul+ol), blockquotes, headings (h1/h2/h3) - Exact screenshot regression: the 4-item list with <strong> labels and <code> values that was showing as literal text - XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>, <object>, XSS inside bold, XSS nested inside <strong>) - Edge cases: code block protection, double-escaping guards, br tag, mixed markdown+HTML, inlineMd called in list/blockquote handlers Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
// Safe tag → markdown equivalent (these produce the same output as **text** etc.)
// Stash raw <pre> blocks so the inline <code> rewrite below does not run
// inside them. Running that rewrite in <pre> content can introduce stray
// backticks for multiline code and break subsequent code-box rendering.
const rawPreStash=[];
s=s.replace(/(<pre\b[^>]*>[\s\S]*?<\/pre>)/gi,m=>{rawPreStash.push(m);return `\x00R${rawPreStash.length-1}\x00`;});
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests renderMd() now correctly renders safe inline HTML tags that AI models emit in their responses: Pre-pass (ui.js): Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown equivalents (**text**, *text*, `text`, newline) before the pipeline runs. Code blocks and backtick spans are stashed first so their content is never modified. inlineMd() helper (ui.js): New helper for processing inline formatting inside list items, blockquotes, and headings. Previously these used esc() directly, which escaped <strong>/<code> tags that had already been converted from HTML by the pre-pass — causing them to appear as literal &lt;strong&gt; text instead of rendering as bold. inlineMd() applies bold/italic/code processing and then escapes only unknown tags. Safety net (ui.js): After the full pipeline, any HTML tags NOT emitted by our own renderer (i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself produces. XSS is fully blocked. Active session gold style (sessions.js, style.css): Active session item now uses gold/amber (#e8a030) instead of blue, matching the logo gradient color for better visual hierarchy. Project color border-left is skipped when the session is active (gold always wins). Session items get border-radius: 0 8px 8px 0 to complement the left border indicator. Tests (tests/test_sprint16.py — 74 tests): - Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present - Behavioural: all safe tags render in paragraphs, list items (ul+ol), blockquotes, headings (h1/h2/h3) - Exact screenshot regression: the 4-item list with <strong> labels and <code> values that was showing as literal text - XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>, <object>, XSS inside bold, XSS nested inside <strong>) - Edge cases: code block protection, double-escaping guards, br tag, mixed markdown+HTML, inlineMd called in list/blockquote handlers Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
s=s.replace(/<strong>([\s\S]*?)<\/strong>/gi,(_,t)=>'**'+t+'**');
s=s.replace(/<b>([\s\S]*?)<\/b>/gi,(_,t)=>'**'+t+'**');
s=s.replace(/<em>([\s\S]*?)<\/em>/gi,(_,t)=>'*'+t+'*');
s=s.replace(/<i>([\s\S]*?)<\/i>/gi,(_,t)=>'*'+t+'*');
s=s.replace(/<code>([^<]*?)<\/code>/gi,(_,t)=>'`'+t+'`');
s=s.replace(/<br\s*\/?>/gi,'\n');
v0.50.264 polish followups: i18n parity + assistant-output readability Closes #1442 (server-side _LOGIN_LOCALE missing ja/pt/ko) Closes #1443 (promote _isImeEnter helper to 6 other Safari Enter guards) Closes #1446 (glued-bold-heading lift for LLM thinking-block output) Closes #1447 (markdown heading visual hierarchy in chat messages) All four issues were filed by the Opus pre-release advisor on the v0.50.264 batch or by Cygnus via Discord (relayed by @AvidFuturist, May 1 2026). They share a common shape — narrow, well-scoped, independent of each other, all adding regression tests. == #1442: _LOGIN_LOCALE parity (api/routes.py + static/i18n.js) == Added entries for ja/pt/ko to the server-side _LOGIN_LOCALE dict that renders the localized login page BEFORE the JS i18n bundle loads. With v0.50.264 shipping Japanese as the 8th built-in locale, ja/pt/ko users were seeing the English login page even with their language preference set. While auditing static/i18n.js for English leakage, also fixed: - ko: 10 user-facing login/sign-out/password keys still in English - es: 3 sign-out/auth-disabled keys still in English Tests: tests/test_login_locale_parity.py (20 tests) — pins both invariants: (a) every locale in i18n.js LOCALES has a matching _LOGIN_LOCALE entry (b) every locale's login-flow keys (13 of them) are translated, not English == #1443: window._isImeEnter promotion == PR #1441 fixed the Safari IME-composition Enter race in the chat composer (`#msg`) by widening the guard from `e.isComposing` to a `_isImeEnter(e)` helper that combines three signals (isComposing || keyCode===229 || _imeComposing flag). Six other Enter-input handlers were left on the original narrow guard and would still drop IME composition Enters on Safari for Japanese/Chinese/Korean users. Promoted the helper to `window._isImeEnter` (defined in static/boot.js) and replaced the `e.isComposing` guards at all six sites: - static/sessions.js: session rename, project create, project rename - static/ui.js: app dialog (confirm/prompt), message edit, workspace rename The state-free part of the helper (`isComposing || keyCode===229`) handles Safari's race for any focused input without needing per-input composition listeners — only `#msg` keeps the local `_imeComposing` flag. Tests: - tests/test_issue1443_ime_helper_promotion.py (9 tests) — pins each site + verifies no raw `e.isComposing` Enter-guards remain in sessions.js/ui.js - tests/test_ime_composition.py — alternation regex extended to accept the windowed helper form (loosen-test-on-shape-change pattern from v0.50.264 reflection notes) == #1446: glued-bold-heading lift (static/ui.js renderMd + Python mirror) == LLMs in thinking/reasoning mode emit "section headers" glued to the end of the previous paragraph with no whitespace: Para 1 text.**Heading to Para 2** Para 2 text.**Heading to Para 3** The renderer correctly produces inline `<strong>` per CommonMark, but it looks like trailing emphasis on the body text rather than a section break. Cygnus reported this as "Markdown feedback 2 of 3." Added a single regex pre-pass in renderMd(): s.replace(/([.!?])\*\*([^*\n]{1,80})\*\*\n\n/g, '$1\n\n**$2**\n\n') Constraints chosen to avoid false positives: - Trigger only on `[.!?]` IMMEDIATELY before `**` (no space) — almost always an LLM-glued heading, not intentional emphasis - Inner text ≤80 chars, no `*` or newline (single-line only) - Trailing `\n\n` required — preserves "this is **important** to know." mid-paragraph emphasis untouched - Position: after rawPreStash restore, before fence_stash restore — fenced code blocks stay protected (their content is `\x00P` / `\x00F` tokens when the lift runs) Mirrored in tests/test_sprint16.py render_md() so both stay in sync. Tests: tests/test_issue1446_glued_heading_lift.py (17 tests, 5 of which drive the actual ui.js renderMd via node) — covers all 3 trigger forms (.!?), all 4 preserve-emphasis cases the issue spec'd, fenced/inline code protection, chained glued headings, source-level position pin, regex shape pin. == #1447: markdown heading visual hierarchy (static/style.css) == Pre-fix sizes in `.msg-body`: h1 18px, h2 16px, h3 14px (= body), h4 13px, h5 12px, h6 11px So h3 was indistinguishable from body and h4/h5/h6 were SMALLER than body. Cygnus's report: "Markdown feedback 3 of 3 — Headings seem to be missing across the board in Hermes. They're there, but all plaintext." New sizes: h1 24px (border-bottom) h2 20px (border-bottom) h3 17px h4 15px h5 14px (uppercase, tracked) h6 13px (uppercase, tracked, muted) All headings now `font-weight:700` + `color:var(--strong)` for stronger ink. h5/h6 use uppercase + letter-spacing for "label-style" affordance instead of being smaller-than-body. Synced .preview-md (file preview pane) to match exactly so a markdown file preview and a chat message render identically. Added missing h4/h5/h6 rules to .preview-md (it only had h1-h3 before). Updated data-font-size="small"/"large" h1-h6 overrides to scale proportionally with the new defaults. Hierarchy preserved at all three font-size settings. Tests: tests/test_issue1447_heading_hierarchy.py (9 tests) — pins the size hierarchy, the bottom borders on h1/h2, the uppercase affordance on h5/h6, the .preview-md sync, and the small/large override scaling. == Verification == pytest tests/ -q → 3748 passed (+56 new) bash ~/WebUI/scripts/run-browser-tests.sh → 20 + 11 PASS bash ~/WebUI/scripts/webui_qa_agent.sh 8789 → 23/23 PASS Visual confirmation in browser at port 8789: - Heading hierarchy clearly visible at all 6 levels - Glued-bold lift produces separate paragraphs as designed - window._isImeEnter accessible from any module after boot.js - Login page renders ja/pt/ko strings correctly (curl -s /login)
2026-05-02 04:04:23 +00:00
// ── Glued-bold-heading lift (issue #1446) ────────────────────────────────
// LLMs in thinking/reasoning mode frequently emit a "section header" glued
// to the end of the previous paragraph with no whitespace, like:
//
// Para 1 text.**Heading to Para 2**
//
// Para 2 text.**Heading to Para 3**
//
// CommonMark renders that correctly as paragraph-end inline bold, but the
// visual effect is a run-on label rather than a section break. Lift the
// glued bold into its own paragraph when it follows a sentence terminator
// and is followed by a blank line.
//
// Constraints (avoid false positives):
// - Trigger only on a sentence terminator (.!?) IMMEDIATELY before `**`
// (no space) — that pattern is almost always a glued heading, not
// intentional emphasis.
// - Inner text length ≤ 80 chars — long bold runs are usually emphasis
// prose, not headings.
// - Trailing `\n\n` required — preserves mid-paragraph emphasis like
// "this is **important**." untouched.
// - Inner text must not contain newlines or `*` (single-line bold only).
// - Runs after fenced code, math, and raw <pre> are stashed, so code
// content is protected (see pipeline notes).
s=s.replace(/([.!?])\*\*([^*\n]{1,80})\*\*\n\n/g,'$1\n\n**$2**\n\n');
2026-04-28 00:43:36 +00:00
// Inline backtick spans: restore <code> tags produced in the stash callback above.
// Must happen BEFORE bold/italic so **`code`** → <strong><code>code</code></strong>.
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests renderMd() now correctly renders safe inline HTML tags that AI models emit in their responses: Pre-pass (ui.js): Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown equivalents (**text**, *text*, `text`, newline) before the pipeline runs. Code blocks and backtick spans are stashed first so their content is never modified. inlineMd() helper (ui.js): New helper for processing inline formatting inside list items, blockquotes, and headings. Previously these used esc() directly, which escaped <strong>/<code> tags that had already been converted from HTML by the pre-pass — causing them to appear as literal &lt;strong&gt; text instead of rendering as bold. inlineMd() applies bold/italic/code processing and then escapes only unknown tags. Safety net (ui.js): After the full pipeline, any HTML tags NOT emitted by our own renderer (i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself produces. XSS is fully blocked. Active session gold style (sessions.js, style.css): Active session item now uses gold/amber (#e8a030) instead of blue, matching the logo gradient color for better visual hierarchy. Project color border-left is skipped when the session is active (gold always wins). Session items get border-radius: 0 8px 8px 0 to complement the left border indicator. Tests (tests/test_sprint16.py — 74 tests): - Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present - Behavioural: all safe tags render in paragraphs, list items (ul+ol), blockquotes, headings (h1/h2/h3) - Exact screenshot regression: the 4-item list with <strong> labels and <code> values that was showing as literal text - XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>, <object>, XSS inside bold, XSS nested inside <strong>) - Edge cases: code block protection, double-escaping guards, br tag, mixed markdown+HTML, inlineMd called in list/blockquote handlers Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
s=s.replace(/\x00F(\d+)\x00/g,(_,i)=>fence_stash[+i]);
// inlineMd: process bold/italic/code/links within a single line of text.
// Used inside list items and blockquotes where the text may already contain
// HTML from the pre-pass → bold pipeline, so we cannot call esc() directly.
function inlineMd(t){
// Stash backtick code spans first so bold/italic never esc() their content
const _code_stash=[];
t=t.replace(/`([^`\n]+)`/g,(_,x)=>{_code_stash.push(`<code>${esc(x)}</code>`);return `\x00C${_code_stash.length-1}\x00`;});
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests renderMd() now correctly renders safe inline HTML tags that AI models emit in their responses: Pre-pass (ui.js): Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown equivalents (**text**, *text*, `text`, newline) before the pipeline runs. Code blocks and backtick spans are stashed first so their content is never modified. inlineMd() helper (ui.js): New helper for processing inline formatting inside list items, blockquotes, and headings. Previously these used esc() directly, which escaped <strong>/<code> tags that had already been converted from HTML by the pre-pass — causing them to appear as literal &lt;strong&gt; text instead of rendering as bold. inlineMd() applies bold/italic/code processing and then escapes only unknown tags. Safety net (ui.js): After the full pipeline, any HTML tags NOT emitted by our own renderer (i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself produces. XSS is fully blocked. Active session gold style (sessions.js, style.css): Active session item now uses gold/amber (#e8a030) instead of blue, matching the logo gradient color for better visual hierarchy. Project color border-left is skipped when the session is active (gold always wins). Session items get border-radius: 0 8px 8px 0 to complement the left border indicator. Tests (tests/test_sprint16.py — 74 tests): - Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present - Behavioural: all safe tags render in paragraphs, list items (ul+ol), blockquotes, headings (h1/h2/h3) - Exact screenshot regression: the 4-item list with <strong> labels and <code> values that was showing as literal text - XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>, <object>, XSS inside bold, XSS nested inside <strong>) - Edge cases: code block protection, double-escaping guards, br tag, mixed markdown+HTML, inlineMd called in list/blockquote handlers Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
t=t.replace(/\*\*\*(.+?)\*\*\*/g,(_,x)=>`<strong><em>${esc(x)}</em></strong>`);
t=t.replace(/\*\*(.+?)\*\*/g,(_,x)=>`<strong>${esc(x)}</strong>`);
t=t.replace(/\*([^*\n]+)\*/g,(_,x)=>`<em>${esc(x)}</em>`);
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075) * fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly MIME_MAP was missing entries for .html and .htm. The server fell back to Content-Type: application/octet-stream, which browsers refuse to render as HTML in an iframe — causing a blank white preview. The rest of the pipeline was already correct: the iframe exists in static/index.html, openFile() in static/workspace.js routes .html to showPreview('html'), and _handle_file_raw() in api/routes.py sets the correct CSP sandbox header when ?inline=1 is present. The only missing piece was the MIME type. * test(workspace): lock in MIME_MAP entry for .html/.htm PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py to fix the blank workspace HTML preview iframe. Without a direct assertion on the MIME_MAP entries, the fix could silently regress (the existing test_779_html_preview.py tests cover the iframe wiring, the inline=1 query handling, and the CSP sandbox header — but none of them touch MIME_MAP itself). Add a single regression test that asserts MIME_MAP['.html'] and MIME_MAP['.htm'] are both 'text/html' so any future removal of those entries fails CI immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(composer): raise .approval-card.visible z-index above .queue-card .queue-card has z-index:2. .approval-card.visible had no z-index, so the queue flyout would render on top of the approval card when both were visible simultaneously — obscuring the Allow/Deny buttons. Fix: add z-index:3 to .approval-card.visible so approvals always render above the queue flyout. Approval is a blocking, security-relevant interaction and must never be obscured by passive UI elements. * test(composer): pin approval-card z-index > queue-card invariant PR #1071 raises .approval-card.visible to z-index:3 so the security- relevant Allow / Deny buttons stay clickable when the queue flyout is also open. Without a regression test, a future CSS edit could silently drop the z-index back below queue-card (z-index:2) and reintroduce the bug — there is no automated UI test covering this stacking interaction. Add a focused regex check that pins the invariant: .approval-card.visible z-index must be strictly greater than .queue-card z-index. Modeled on the existing CSS-regex regression style in tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: intercept /steer /interrupt /queue before busy-mode routing in send() Root cause: slash commands entered while the agent is busy never reached the command dispatcher. send() enters the busy block and returns early at line ~50, so the slash-command intercept (~line 56) is never reached. The text was queued as a plain message. When it drained after the turn ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream, and showed "No active task to stop." Fix: at the top of the busy block, before checking busyMode, check if the text starts with / and is one of the three control commands. If so, dispatch the handler immediately and return. This lets the user type /steer, /interrupt, or /queue at any time — including while the agent is mid-stream — and have them execute against the live session. Two new regression tests added: - test_slash_commands_intercepted_before_busymode_routing: verifies the intercept appears before the busyMode routing in the busy block - test_steer_intercept_calls_handler_directly: verifies the intercept calls _bc.fn(_pc.args) and returns, not queues * test(busy-intercept): pin sync input-clear before await in slash intercept PR #1072's intercept clears the msg input before awaiting the handler. Order matters: if the await happens first (or if the clear is moved inside the handler), the input still shows '/steer foo' for the duration of the await. A reflexive second Enter press during that window — common while waiting for the toast — re-runs send(): either re-fires the handler (double-steer) or, if the turn just ended, falls through to the non-busy slash dispatcher and drops a confusing "No active task to stop." Add test_steer_intercept_clears_input_before_await pinning the order so this UX invariant cannot silently regress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: update steer i18n and settings copy — steer no longer interrupts With the real /steer implementation (agent.steer() via /api/chat/steer), steer injects a correction mid-turn WITHOUT interrupting the current stream. The previous copy said "falls back to interrupt", "Steer (interrupt + send)", etc. — accurate only for the old placeholder, not the real implementation. Changes across all 6 locales (en/ru/es/de/zh/zh-Hant): cmd_steer: "falls back to interrupt" removed settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction" cmd_steer_fallback: "interrupted" → "queued for next turn" busy_steer_fallback: "interrupted instead" → "queued for next turn" settings_desc_busy_input_mode: "currently falls back to interrupt" removed Also: static/index.html: inline fallback text updated to match static/commands.js: internal comment clarified (fallback = queue+cancel, not "interrupt mode" which implies the primary action) * fix(renderer): group consecutive blockquote lines into single element Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs: 1. `.+` required at least one character — bare `>` lines (blank continuation lines) did not match and passed through as literal `>` 2. Each matching line became its own `<blockquote>` element — a 10-line blockquote produced 10 stacked `<blockquote>` tags with no grouping 3. When a fenced code block sat inside a blockquote, the fence-stash pass consumed the code content and left orphaned `>` lines that the old `.+` pattern could not match Fix: replace the single-line regex with a group-based approach that matches one or more consecutive `>` lines as a single block, strips the `>` prefix from each line, passes each non-empty line through inlineMd(), turns blank `>` lines into `<br>`, and wraps the entire group in one `<blockquote>`. 14 regression tests added covering: - Single-line blockquotes (regression) - Multi-line grouping (2 and 10 lines) - Two separate blockquotes staying separate - Bare `>` and `>text` (no space) edge cases - Blank continuation lines → <br> - Bold / italic / inline-code inside blockquotes - Blockquote followed by normal paragraph * fix(renderer): drop empty trailing line from blockquote match The new group-based blockquote rule introduced in this PR captures the trailing newline in its (?:\n|$) clause. After block.split('\n') that trailing newline produces an empty final element. The original filter only dropped lone bare '>' artifacts on the last line, so the empty final element survived, and the .map(blank → '<br>') step turned it into a phantom <br> immediately before </blockquote>. Visible symptom: any blockquote whose source ends with \n (the common case — a quote followed by another paragraph or end-of-message) renders with an extra blank line at the bottom of the quote. Reproducer: '> Hello\n\nThe rest of the message.' → '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.' ^^^ phantom <br> Fix: replace the single-line filter with a while-loop that pops trailing lines while they are either empty OR a bare '>'. This matches the intent the Python test mirror in tests/test_blockquote_rendering.py already had (the mirror was correct; the JS was not — that's why the original tests passed despite the bug). Also add four new regression tests in TestNoPhantomTrailingBr that pin the no-trailing-<br> invariant for the common shapes: - input ending with \n - quote followed by paragraph (the real-world case) - multi-line quote ending with \n - quote with blank continuation + trailing \n (internal <br> stays, trailing <br> does not) Verified end-to-end with node against the actual JS regex. 244 renderer-adjacent tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes Five additional fixes on top of the blockquote grouping from the initial commit: 1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows line endings do not produce stray \r characters in rendered output 2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use inside blockquotes/lists) and the outer pass (for plain paragraphs). Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped. 3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐ via task-done/task-todo span wrappers. Checks [X] (uppercase) too. 4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its own <blockquote> element rather than passing through as literal >. Implemented by extracting the blockquote rule into _applyBlockquotes() which calls itself recursively on the stripped inner content. 5. Lists inside blockquotes: > - item now renders <ul><li> inside the blockquote instead of a literal "- item" string. Task list items work inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>). Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF strip at the top of renderMd pushed the autolink regex past the old limit. 68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py covering all constructs, edge cases, and combinations. * fix(renderer): restore space in blockquote prefix-strip regex Commit 04e7b53 changed the blockquote prefix-strip regex from /^>[ \t]?/ (consume "> ", "\t>", or just ">") to /^>[\t]?/ (only consume "\t>" or just ">") The space character was dropped from the character class. Since practically every blockquote an LLM produces is "> " (greater-than followed by a space), this leaves a leading space artifact on every stripped blockquote line. Worse, the leading space breaks the list-detection regex `^(?: )?[-*+] ` inside the new `_applyBlockquotes` helper — that regex requires either zero or two leading spaces, never one — so the new "list inside blockquote" feature never fired for the canonical input shape `> - item`. Reproducer (against the actual ui.js via node, before the fix): > Hello world → <blockquote> Hello world</blockquote> ^ phantom leading space > Steps: → <blockquote>Steps: > - one - one > - two - two</blockquote> ^ literal text, NOT a <ul>; lists-in-quote feature broken > - [x] done → blockquote with literal "[x] done", no checkbox span Tests passed despite the bug because tests/test_blockquote_rendering.py and tests/test_renderer_comprehensive.py validate against a Python mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e. the mirror is correct, the JS is not, and the static-mirror tests can't catch the divergence. Same shape of bug as commit 94d63d0 (phantom <br> in trailing line) where the mirror was right and the JS was wrong. Fix: restore the space character in the strip regex's character class. Add tests/test_renderer_js_behaviour.py — 11 tests that drive the ACTUAL renderMd via node and assert on rendered output for the most common LLM shapes (single-line quote, multi-line quote, list inside quote, task list inside quote, nested >>>, strikethrough inside and outside quote, top-level task list, quote followed by heading, multi-paragraph quote with list, CRLF normalisation). Verified: the buggy regex makes 6 of those 11 tests fail; the corrected regex makes all 11 pass. Suite: 2354 passed, 0 new failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Collapse agent session compression chains * Restore upstream changelog entries * fix(agent_sessions): bubble active compression chains to top by tip last_activity The original PR merge kept the chain head's id/title/started_at and overrode id/model/message_count/ended_at/end_reason from the tip — but did NOT override last_activity. Since the projected list is sorted by last_activity DESC and the WebUI sidebar surfaces updated_at = last_activity, an actively-used compression chain whose tip is being edited NOW would sort by the ROOT's old last_activity and fall below recently touched standalone sessions. Reproducer (with the harness against actual code, before the fix): - root: started 30 days ago, last msg 30 days ago - tip: started 28 days ago (parent_session_id=root), last msg 5 seconds ago - standalone: last msg 2 days ago Sidebar order with original PR: [0] standalone (48h ago) [1] active_tip (last_activity=root's 720h ago) ← wrong Sidebar order after fix: [0] active_tip (last_activity=tip's 0h ago) ← correct [1] standalone (48h ago) This matches Hermes Agent's own list_sessions_rich projection at hermes_state.py:903-909, which overrides "last_active" from the tip exactly so that the agent CLI's session list orders the same way. Add ``last_activity`` to the merge-from-tip key list, update the existing test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to expect tip-derived updated_at, and add test_compression_chain_bubbles_to_top_by_tip_activity locking in the bubble-to-top invariant — without this regression test the previous behaviour passed CI because no test exercised the sort order against a mixed set of chains and standalone sessions. The chain head's started_at (created_at) and title remain preserved, so users can still find the conversation by its original date and name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.216 release notes and version bump Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix. * chore: gitignore local-only review harness directory Adds .local-review/ to .gitignore so renderer drivers, sample inputs, fixture builders, and other reviewer scratch files do not accidentally get committed. Nothing under that path is ever shared in the repo; keeping the entry tracked makes the boundary explicit for any future contributor who creates the directory locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Keep reasoning chip visible for None effort * test(reasoning): pin chip render output via node, not just source regex The PR's static checks in test_reasoning_chip_btw_fixes.py validate the shape of _applyReasoningChip (no display='none' literal, the right classList.toggle call exists, the right label literals are in the function body) but pass even if the runtime detail is wrong — for example if `inactive` were inverted, _normalizeReasoningEffort mishandled whitespace, or _formatReasoningEffortLabel returned the wrong literal for an unknown input. Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive the actual _applyReasoningChip() via node and assert on the rendered DOM state for each effort value: TestChipAlwaysVisible - empty / null -> "Default" label, inactive=true - "none" -> "None" label, inactive=true - "low"/"high" -> verbatim label, inactive=false TestNormalizationEdgeCases - "NONE" -> normalises to "None" - " none " -> trims and normalises - unknown junk -> falls through visible, never hidden TestTitleAttributeAccessibility - title attribute carries the human-readable label for tooltip / screen-reader use Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught). Against this PR's ui.js: 11/11 pass. This pattern (drive the actual JS via node) caught two regex-only regressions in PR #1073 where the Python mirror was correct while the JS was broken. Same protection added here so the chip-visibility contract can't silently break in a future refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add #1074 to v0.50.216 changelog, bump test count to 2428 * fix(i18n): restore broken Unicode in Russian and Spanish steer strings Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally stripped the `\u` prefix from Unicode escape sequences in two locales, producing garbled literal hex strings visible to users: Spanish (es): - cmd_steer: correcci00f3n → corrección - cmd_steer_fallback: 2014 en cola → — en cola - busy_steer_fallback: 2014 en cola → — en cola - settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección - settings_busy_input_mode_steer: correcci00f3n → corrección Russian (ru): - settings_desc_busy_input_mode: the entire Cyrillic string was replaced with raw 4-hex-char code-points without the \u prefix (041e043f... instead of actual Cyrillic). Decoded: "Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer внедряет коррекцию без прерывания." Fix: write the correct characters directly (UTF-8 is the file encoding so embedding them literally is cleaner than \u escapes for long text). All other locales (en, de, zh, zh-Hant) were not affected — confirmed by grepping for bare hex run-ons in the updated file. Verified: node --check static/i18n.js passes; full pytest suite green (2365 passed, 47 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: remove duplicate compression chain entry from [Unreleased] --------- 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> Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-26 04:06:31 +00:00
// Strikethrough: ~~text~~ → <del>text</del>
t=t.replace(/~~(.+?)~~/g,(_,x)=>`<del>${esc(x)}</del>`);
// #487: Image pass — runs while code stash is active so ![x](url) inside
// backticks stays protected as a \x00C token and is never rendered as <img>.
// Must run before _code_stash restore and before _link_stash so the image
// is not consumed by the [label](url) link regex.
t=t.replace(/!\[([^\]]*)\]\((https?:\/\/[^\)]+)\)/g,(_,alt,url)=>`<img src="${url.replace(/"/g,'%22')}" alt="${esc(alt)}" class="msg-media-img" loading="lazy">`);
// Stash rendered <img> tags so autolink never matches URLs inside src=
const _img_stash=[];
t=t.replace(/(<img\b[^>]*>)/g,m=>{_img_stash.push(m);return `\x00G${_img_stash.length-1}\x00`;});
t=t.replace(/\x00C(\d+)\x00/g,(_,i)=>_code_stash[+i]);
// Stash [label](url) links before autolink so the URL in href= is not re-linked
const _link_stash=[];
t=t.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,(_,lb,u)=>{_link_stash.push(`<a href="${u.replace(/"/g,'%22')}" target="_blank" rel="noopener">${esc(lb)}</a>`);return `\x00L${_link_stash.length-1}\x00`;});
t=t.replace(/(https?:\/\/[^\s<>"')\]]+)/g,(url)=>{const trail=url.match(/[.,;:!?)]$/)?url.slice(-1):'';const clean=trail?url.slice(0,-1):url;return `<a href="${clean}" target="_blank" rel="noopener">${esc(clean)}</a>${trail}`;});
t=t.replace(/\x00L(\d+)\x00/g,(_,i)=>_link_stash[+i]);
t=t.replace(/\x00G(\d+)\x00/g,(_,i)=>_img_stash[+i]);
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests renderMd() now correctly renders safe inline HTML tags that AI models emit in their responses: Pre-pass (ui.js): Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown equivalents (**text**, *text*, `text`, newline) before the pipeline runs. Code blocks and backtick spans are stashed first so their content is never modified. inlineMd() helper (ui.js): New helper for processing inline formatting inside list items, blockquotes, and headings. Previously these used esc() directly, which escaped <strong>/<code> tags that had already been converted from HTML by the pre-pass — causing them to appear as literal &lt;strong&gt; text instead of rendering as bold. inlineMd() applies bold/italic/code processing and then escapes only unknown tags. Safety net (ui.js): After the full pipeline, any HTML tags NOT emitted by our own renderer (i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself produces. XSS is fully blocked. Active session gold style (sessions.js, style.css): Active session item now uses gold/amber (#e8a030) instead of blue, matching the logo gradient color for better visual hierarchy. Project color border-left is skipped when the session is active (gold always wins). Session items get border-radius: 0 8px 8px 0 to complement the left border indicator. Tests (tests/test_sprint16.py — 74 tests): - Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present - Behavioural: all safe tags render in paragraphs, list items (ul+ol), blockquotes, headings (h1/h2/h3) - Exact screenshot regression: the 4-item list with <strong> labels and <code> values that was showing as literal text - XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>, <object>, XSS inside bold, XSS nested inside <strong>) - Edge cases: code block protection, double-escaping guards, br tag, mixed markdown+HTML, inlineMd called in list/blockquote handlers Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
// Escape any plain text that isn't already wrapped in a tag we produced
// by escaping bare < > that are not part of our own tags
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075) * fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly MIME_MAP was missing entries for .html and .htm. The server fell back to Content-Type: application/octet-stream, which browsers refuse to render as HTML in an iframe — causing a blank white preview. The rest of the pipeline was already correct: the iframe exists in static/index.html, openFile() in static/workspace.js routes .html to showPreview('html'), and _handle_file_raw() in api/routes.py sets the correct CSP sandbox header when ?inline=1 is present. The only missing piece was the MIME type. * test(workspace): lock in MIME_MAP entry for .html/.htm PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py to fix the blank workspace HTML preview iframe. Without a direct assertion on the MIME_MAP entries, the fix could silently regress (the existing test_779_html_preview.py tests cover the iframe wiring, the inline=1 query handling, and the CSP sandbox header — but none of them touch MIME_MAP itself). Add a single regression test that asserts MIME_MAP['.html'] and MIME_MAP['.htm'] are both 'text/html' so any future removal of those entries fails CI immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(composer): raise .approval-card.visible z-index above .queue-card .queue-card has z-index:2. .approval-card.visible had no z-index, so the queue flyout would render on top of the approval card when both were visible simultaneously — obscuring the Allow/Deny buttons. Fix: add z-index:3 to .approval-card.visible so approvals always render above the queue flyout. Approval is a blocking, security-relevant interaction and must never be obscured by passive UI elements. * test(composer): pin approval-card z-index > queue-card invariant PR #1071 raises .approval-card.visible to z-index:3 so the security- relevant Allow / Deny buttons stay clickable when the queue flyout is also open. Without a regression test, a future CSS edit could silently drop the z-index back below queue-card (z-index:2) and reintroduce the bug — there is no automated UI test covering this stacking interaction. Add a focused regex check that pins the invariant: .approval-card.visible z-index must be strictly greater than .queue-card z-index. Modeled on the existing CSS-regex regression style in tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: intercept /steer /interrupt /queue before busy-mode routing in send() Root cause: slash commands entered while the agent is busy never reached the command dispatcher. send() enters the busy block and returns early at line ~50, so the slash-command intercept (~line 56) is never reached. The text was queued as a plain message. When it drained after the turn ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream, and showed "No active task to stop." Fix: at the top of the busy block, before checking busyMode, check if the text starts with / and is one of the three control commands. If so, dispatch the handler immediately and return. This lets the user type /steer, /interrupt, or /queue at any time — including while the agent is mid-stream — and have them execute against the live session. Two new regression tests added: - test_slash_commands_intercepted_before_busymode_routing: verifies the intercept appears before the busyMode routing in the busy block - test_steer_intercept_calls_handler_directly: verifies the intercept calls _bc.fn(_pc.args) and returns, not queues * test(busy-intercept): pin sync input-clear before await in slash intercept PR #1072's intercept clears the msg input before awaiting the handler. Order matters: if the await happens first (or if the clear is moved inside the handler), the input still shows '/steer foo' for the duration of the await. A reflexive second Enter press during that window — common while waiting for the toast — re-runs send(): either re-fires the handler (double-steer) or, if the turn just ended, falls through to the non-busy slash dispatcher and drops a confusing "No active task to stop." Add test_steer_intercept_clears_input_before_await pinning the order so this UX invariant cannot silently regress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: update steer i18n and settings copy — steer no longer interrupts With the real /steer implementation (agent.steer() via /api/chat/steer), steer injects a correction mid-turn WITHOUT interrupting the current stream. The previous copy said "falls back to interrupt", "Steer (interrupt + send)", etc. — accurate only for the old placeholder, not the real implementation. Changes across all 6 locales (en/ru/es/de/zh/zh-Hant): cmd_steer: "falls back to interrupt" removed settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction" cmd_steer_fallback: "interrupted" → "queued for next turn" busy_steer_fallback: "interrupted instead" → "queued for next turn" settings_desc_busy_input_mode: "currently falls back to interrupt" removed Also: static/index.html: inline fallback text updated to match static/commands.js: internal comment clarified (fallback = queue+cancel, not "interrupt mode" which implies the primary action) * fix(renderer): group consecutive blockquote lines into single element Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs: 1. `.+` required at least one character — bare `>` lines (blank continuation lines) did not match and passed through as literal `>` 2. Each matching line became its own `<blockquote>` element — a 10-line blockquote produced 10 stacked `<blockquote>` tags with no grouping 3. When a fenced code block sat inside a blockquote, the fence-stash pass consumed the code content and left orphaned `>` lines that the old `.+` pattern could not match Fix: replace the single-line regex with a group-based approach that matches one or more consecutive `>` lines as a single block, strips the `>` prefix from each line, passes each non-empty line through inlineMd(), turns blank `>` lines into `<br>`, and wraps the entire group in one `<blockquote>`. 14 regression tests added covering: - Single-line blockquotes (regression) - Multi-line grouping (2 and 10 lines) - Two separate blockquotes staying separate - Bare `>` and `>text` (no space) edge cases - Blank continuation lines → <br> - Bold / italic / inline-code inside blockquotes - Blockquote followed by normal paragraph * fix(renderer): drop empty trailing line from blockquote match The new group-based blockquote rule introduced in this PR captures the trailing newline in its (?:\n|$) clause. After block.split('\n') that trailing newline produces an empty final element. The original filter only dropped lone bare '>' artifacts on the last line, so the empty final element survived, and the .map(blank → '<br>') step turned it into a phantom <br> immediately before </blockquote>. Visible symptom: any blockquote whose source ends with \n (the common case — a quote followed by another paragraph or end-of-message) renders with an extra blank line at the bottom of the quote. Reproducer: '> Hello\n\nThe rest of the message.' → '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.' ^^^ phantom <br> Fix: replace the single-line filter with a while-loop that pops trailing lines while they are either empty OR a bare '>'. This matches the intent the Python test mirror in tests/test_blockquote_rendering.py already had (the mirror was correct; the JS was not — that's why the original tests passed despite the bug). Also add four new regression tests in TestNoPhantomTrailingBr that pin the no-trailing-<br> invariant for the common shapes: - input ending with \n - quote followed by paragraph (the real-world case) - multi-line quote ending with \n - quote with blank continuation + trailing \n (internal <br> stays, trailing <br> does not) Verified end-to-end with node against the actual JS regex. 244 renderer-adjacent tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes Five additional fixes on top of the blockquote grouping from the initial commit: 1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows line endings do not produce stray \r characters in rendered output 2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use inside blockquotes/lists) and the outer pass (for plain paragraphs). Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped. 3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐ via task-done/task-todo span wrappers. Checks [X] (uppercase) too. 4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its own <blockquote> element rather than passing through as literal >. Implemented by extracting the blockquote rule into _applyBlockquotes() which calls itself recursively on the stripped inner content. 5. Lists inside blockquotes: > - item now renders <ul><li> inside the blockquote instead of a literal "- item" string. Task list items work inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>). Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF strip at the top of renderMd pushed the autolink regex past the old limit. 68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py covering all constructs, edge cases, and combinations. * fix(renderer): restore space in blockquote prefix-strip regex Commit 04e7b53 changed the blockquote prefix-strip regex from /^>[ \t]?/ (consume "> ", "\t>", or just ">") to /^>[\t]?/ (only consume "\t>" or just ">") The space character was dropped from the character class. Since practically every blockquote an LLM produces is "> " (greater-than followed by a space), this leaves a leading space artifact on every stripped blockquote line. Worse, the leading space breaks the list-detection regex `^(?: )?[-*+] ` inside the new `_applyBlockquotes` helper — that regex requires either zero or two leading spaces, never one — so the new "list inside blockquote" feature never fired for the canonical input shape `> - item`. Reproducer (against the actual ui.js via node, before the fix): > Hello world → <blockquote> Hello world</blockquote> ^ phantom leading space > Steps: → <blockquote>Steps: > - one - one > - two - two</blockquote> ^ literal text, NOT a <ul>; lists-in-quote feature broken > - [x] done → blockquote with literal "[x] done", no checkbox span Tests passed despite the bug because tests/test_blockquote_rendering.py and tests/test_renderer_comprehensive.py validate against a Python mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e. the mirror is correct, the JS is not, and the static-mirror tests can't catch the divergence. Same shape of bug as commit 94d63d0 (phantom <br> in trailing line) where the mirror was right and the JS was wrong. Fix: restore the space character in the strip regex's character class. Add tests/test_renderer_js_behaviour.py — 11 tests that drive the ACTUAL renderMd via node and assert on rendered output for the most common LLM shapes (single-line quote, multi-line quote, list inside quote, task list inside quote, nested >>>, strikethrough inside and outside quote, top-level task list, quote followed by heading, multi-paragraph quote with list, CRLF normalisation). Verified: the buggy regex makes 6 of those 11 tests fail; the corrected regex makes all 11 pass. Suite: 2354 passed, 0 new failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Collapse agent session compression chains * Restore upstream changelog entries * fix(agent_sessions): bubble active compression chains to top by tip last_activity The original PR merge kept the chain head's id/title/started_at and overrode id/model/message_count/ended_at/end_reason from the tip — but did NOT override last_activity. Since the projected list is sorted by last_activity DESC and the WebUI sidebar surfaces updated_at = last_activity, an actively-used compression chain whose tip is being edited NOW would sort by the ROOT's old last_activity and fall below recently touched standalone sessions. Reproducer (with the harness against actual code, before the fix): - root: started 30 days ago, last msg 30 days ago - tip: started 28 days ago (parent_session_id=root), last msg 5 seconds ago - standalone: last msg 2 days ago Sidebar order with original PR: [0] standalone (48h ago) [1] active_tip (last_activity=root's 720h ago) ← wrong Sidebar order after fix: [0] active_tip (last_activity=tip's 0h ago) ← correct [1] standalone (48h ago) This matches Hermes Agent's own list_sessions_rich projection at hermes_state.py:903-909, which overrides "last_active" from the tip exactly so that the agent CLI's session list orders the same way. Add ``last_activity`` to the merge-from-tip key list, update the existing test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to expect tip-derived updated_at, and add test_compression_chain_bubbles_to_top_by_tip_activity locking in the bubble-to-top invariant — without this regression test the previous behaviour passed CI because no test exercised the sort order against a mixed set of chains and standalone sessions. The chain head's started_at (created_at) and title remain preserved, so users can still find the conversation by its original date and name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.216 release notes and version bump Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix. * chore: gitignore local-only review harness directory Adds .local-review/ to .gitignore so renderer drivers, sample inputs, fixture builders, and other reviewer scratch files do not accidentally get committed. Nothing under that path is ever shared in the repo; keeping the entry tracked makes the boundary explicit for any future contributor who creates the directory locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Keep reasoning chip visible for None effort * test(reasoning): pin chip render output via node, not just source regex The PR's static checks in test_reasoning_chip_btw_fixes.py validate the shape of _applyReasoningChip (no display='none' literal, the right classList.toggle call exists, the right label literals are in the function body) but pass even if the runtime detail is wrong — for example if `inactive` were inverted, _normalizeReasoningEffort mishandled whitespace, or _formatReasoningEffortLabel returned the wrong literal for an unknown input. Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive the actual _applyReasoningChip() via node and assert on the rendered DOM state for each effort value: TestChipAlwaysVisible - empty / null -> "Default" label, inactive=true - "none" -> "None" label, inactive=true - "low"/"high" -> verbatim label, inactive=false TestNormalizationEdgeCases - "NONE" -> normalises to "None" - " none " -> trims and normalises - unknown junk -> falls through visible, never hidden TestTitleAttributeAccessibility - title attribute carries the human-readable label for tooltip / screen-reader use Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught). Against this PR's ui.js: 11/11 pass. This pattern (drive the actual JS via node) caught two regex-only regressions in PR #1073 where the Python mirror was correct while the JS was broken. Same protection added here so the chip-visibility contract can't silently break in a future refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add #1074 to v0.50.216 changelog, bump test count to 2428 * fix(i18n): restore broken Unicode in Russian and Spanish steer strings Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally stripped the `\u` prefix from Unicode escape sequences in two locales, producing garbled literal hex strings visible to users: Spanish (es): - cmd_steer: correcci00f3n → corrección - cmd_steer_fallback: 2014 en cola → — en cola - busy_steer_fallback: 2014 en cola → — en cola - settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección - settings_busy_input_mode_steer: correcci00f3n → corrección Russian (ru): - settings_desc_busy_input_mode: the entire Cyrillic string was replaced with raw 4-hex-char code-points without the \u prefix (041e043f... instead of actual Cyrillic). Decoded: "Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer внедряет коррекцию без прерывания." Fix: write the correct characters directly (UTF-8 is the file encoding so embedding them literally is cleaner than \u escapes for long text). All other locales (en, de, zh, zh-Hant) were not affected — confirmed by grepping for bare hex run-ons in the updated file. Verified: node --check static/i18n.js passes; full pytest suite green (2365 passed, 47 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: remove duplicate compression chain entry from [Unreleased] --------- 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> Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-26 04:06:31 +00:00
const SAFE_INLINE=/^<\/?(strong|em|del|code|a|img)([\s>]|$)/i;
feat: safe HTML rendering in AI responses + active session gold style + Sprint 16 tests renderMd() now correctly renders safe inline HTML tags that AI models emit in their responses: Pre-pass (ui.js): Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown equivalents (**text**, *text*, `text`, newline) before the pipeline runs. Code blocks and backtick spans are stashed first so their content is never modified. inlineMd() helper (ui.js): New helper for processing inline formatting inside list items, blockquotes, and headings. Previously these used esc() directly, which escaped <strong>/<code> tags that had already been converted from HTML by the pre-pass — causing them to appear as literal &lt;strong&gt; text instead of rendering as bold. inlineMd() applies bold/italic/code processing and then escapes only unknown tags. Safety net (ui.js): After the full pipeline, any HTML tags NOT emitted by our own renderer (i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself produces. XSS is fully blocked. Active session gold style (sessions.js, style.css): Active session item now uses gold/amber (#e8a030) instead of blue, matching the logo gradient color for better visual hierarchy. Project color border-left is skipped when the session is active (gold always wins). Session items get border-radius: 0 8px 8px 0 to complement the left border indicator. Tests (tests/test_sprint16.py — 74 tests): - Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present - Behavioural: all safe tags render in paragraphs, list items (ul+ol), blockquotes, headings (h1/h2/h3) - Exact screenshot regression: the 4-item list with <strong> labels and <code> values that was showing as literal text - XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>, <object>, XSS inside bold, XSS nested inside <strong>) - Edge cases: code block protection, double-escaping guards, br tag, mixed markdown+HTML, inlineMd called in list/blockquote handlers Tests: 312 passed, 0 failed.
2026-04-03 00:27:43 +00:00
t=t.replace(/<\/?[a-z][^>]*>/gi,tag=>SAFE_INLINE.test(tag)?tag:esc(tag));
return t;
}
// Stash <code> tags from the backtick pass above so the outer bold/italic
// regexes don't esc() their content (e.g. **`code`** → <strong><code>code</code></strong>)
const _ob_stash=[];
s=s.replace(/(<code\b[^>]*>[\s\S]*?<\/code>)/g,m=>{_ob_stash.push(m);return `\x00O${_ob_stash.length-1}\x00`;});
s=s.replace(/\*\*\*(.+?)\*\*\*/g,(_,t)=>`<strong><em>${esc(t)}</em></strong>`);
s=s.replace(/\*\*(.+?)\*\*/g,(_,t)=>`<strong>${esc(t)}</strong>`);
s=s.replace(/\*([^*\n]+)\*/g,(_,t)=>`<em>${esc(t)}</em>`);
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075) * fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly MIME_MAP was missing entries for .html and .htm. The server fell back to Content-Type: application/octet-stream, which browsers refuse to render as HTML in an iframe — causing a blank white preview. The rest of the pipeline was already correct: the iframe exists in static/index.html, openFile() in static/workspace.js routes .html to showPreview('html'), and _handle_file_raw() in api/routes.py sets the correct CSP sandbox header when ?inline=1 is present. The only missing piece was the MIME type. * test(workspace): lock in MIME_MAP entry for .html/.htm PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py to fix the blank workspace HTML preview iframe. Without a direct assertion on the MIME_MAP entries, the fix could silently regress (the existing test_779_html_preview.py tests cover the iframe wiring, the inline=1 query handling, and the CSP sandbox header — but none of them touch MIME_MAP itself). Add a single regression test that asserts MIME_MAP['.html'] and MIME_MAP['.htm'] are both 'text/html' so any future removal of those entries fails CI immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(composer): raise .approval-card.visible z-index above .queue-card .queue-card has z-index:2. .approval-card.visible had no z-index, so the queue flyout would render on top of the approval card when both were visible simultaneously — obscuring the Allow/Deny buttons. Fix: add z-index:3 to .approval-card.visible so approvals always render above the queue flyout. Approval is a blocking, security-relevant interaction and must never be obscured by passive UI elements. * test(composer): pin approval-card z-index > queue-card invariant PR #1071 raises .approval-card.visible to z-index:3 so the security- relevant Allow / Deny buttons stay clickable when the queue flyout is also open. Without a regression test, a future CSS edit could silently drop the z-index back below queue-card (z-index:2) and reintroduce the bug — there is no automated UI test covering this stacking interaction. Add a focused regex check that pins the invariant: .approval-card.visible z-index must be strictly greater than .queue-card z-index. Modeled on the existing CSS-regex regression style in tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: intercept /steer /interrupt /queue before busy-mode routing in send() Root cause: slash commands entered while the agent is busy never reached the command dispatcher. send() enters the busy block and returns early at line ~50, so the slash-command intercept (~line 56) is never reached. The text was queued as a plain message. When it drained after the turn ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream, and showed "No active task to stop." Fix: at the top of the busy block, before checking busyMode, check if the text starts with / and is one of the three control commands. If so, dispatch the handler immediately and return. This lets the user type /steer, /interrupt, or /queue at any time — including while the agent is mid-stream — and have them execute against the live session. Two new regression tests added: - test_slash_commands_intercepted_before_busymode_routing: verifies the intercept appears before the busyMode routing in the busy block - test_steer_intercept_calls_handler_directly: verifies the intercept calls _bc.fn(_pc.args) and returns, not queues * test(busy-intercept): pin sync input-clear before await in slash intercept PR #1072's intercept clears the msg input before awaiting the handler. Order matters: if the await happens first (or if the clear is moved inside the handler), the input still shows '/steer foo' for the duration of the await. A reflexive second Enter press during that window — common while waiting for the toast — re-runs send(): either re-fires the handler (double-steer) or, if the turn just ended, falls through to the non-busy slash dispatcher and drops a confusing "No active task to stop." Add test_steer_intercept_clears_input_before_await pinning the order so this UX invariant cannot silently regress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: update steer i18n and settings copy — steer no longer interrupts With the real /steer implementation (agent.steer() via /api/chat/steer), steer injects a correction mid-turn WITHOUT interrupting the current stream. The previous copy said "falls back to interrupt", "Steer (interrupt + send)", etc. — accurate only for the old placeholder, not the real implementation. Changes across all 6 locales (en/ru/es/de/zh/zh-Hant): cmd_steer: "falls back to interrupt" removed settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction" cmd_steer_fallback: "interrupted" → "queued for next turn" busy_steer_fallback: "interrupted instead" → "queued for next turn" settings_desc_busy_input_mode: "currently falls back to interrupt" removed Also: static/index.html: inline fallback text updated to match static/commands.js: internal comment clarified (fallback = queue+cancel, not "interrupt mode" which implies the primary action) * fix(renderer): group consecutive blockquote lines into single element Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs: 1. `.+` required at least one character — bare `>` lines (blank continuation lines) did not match and passed through as literal `>` 2. Each matching line became its own `<blockquote>` element — a 10-line blockquote produced 10 stacked `<blockquote>` tags with no grouping 3. When a fenced code block sat inside a blockquote, the fence-stash pass consumed the code content and left orphaned `>` lines that the old `.+` pattern could not match Fix: replace the single-line regex with a group-based approach that matches one or more consecutive `>` lines as a single block, strips the `>` prefix from each line, passes each non-empty line through inlineMd(), turns blank `>` lines into `<br>`, and wraps the entire group in one `<blockquote>`. 14 regression tests added covering: - Single-line blockquotes (regression) - Multi-line grouping (2 and 10 lines) - Two separate blockquotes staying separate - Bare `>` and `>text` (no space) edge cases - Blank continuation lines → <br> - Bold / italic / inline-code inside blockquotes - Blockquote followed by normal paragraph * fix(renderer): drop empty trailing line from blockquote match The new group-based blockquote rule introduced in this PR captures the trailing newline in its (?:\n|$) clause. After block.split('\n') that trailing newline produces an empty final element. The original filter only dropped lone bare '>' artifacts on the last line, so the empty final element survived, and the .map(blank → '<br>') step turned it into a phantom <br> immediately before </blockquote>. Visible symptom: any blockquote whose source ends with \n (the common case — a quote followed by another paragraph or end-of-message) renders with an extra blank line at the bottom of the quote. Reproducer: '> Hello\n\nThe rest of the message.' → '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.' ^^^ phantom <br> Fix: replace the single-line filter with a while-loop that pops trailing lines while they are either empty OR a bare '>'. This matches the intent the Python test mirror in tests/test_blockquote_rendering.py already had (the mirror was correct; the JS was not — that's why the original tests passed despite the bug). Also add four new regression tests in TestNoPhantomTrailingBr that pin the no-trailing-<br> invariant for the common shapes: - input ending with \n - quote followed by paragraph (the real-world case) - multi-line quote ending with \n - quote with blank continuation + trailing \n (internal <br> stays, trailing <br> does not) Verified end-to-end with node against the actual JS regex. 244 renderer-adjacent tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes Five additional fixes on top of the blockquote grouping from the initial commit: 1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows line endings do not produce stray \r characters in rendered output 2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use inside blockquotes/lists) and the outer pass (for plain paragraphs). Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped. 3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐ via task-done/task-todo span wrappers. Checks [X] (uppercase) too. 4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its own <blockquote> element rather than passing through as literal >. Implemented by extracting the blockquote rule into _applyBlockquotes() which calls itself recursively on the stripped inner content. 5. Lists inside blockquotes: > - item now renders <ul><li> inside the blockquote instead of a literal "- item" string. Task list items work inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>). Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF strip at the top of renderMd pushed the autolink regex past the old limit. 68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py covering all constructs, edge cases, and combinations. * fix(renderer): restore space in blockquote prefix-strip regex Commit 04e7b53 changed the blockquote prefix-strip regex from /^>[ \t]?/ (consume "> ", "\t>", or just ">") to /^>[\t]?/ (only consume "\t>" or just ">") The space character was dropped from the character class. Since practically every blockquote an LLM produces is "> " (greater-than followed by a space), this leaves a leading space artifact on every stripped blockquote line. Worse, the leading space breaks the list-detection regex `^(?: )?[-*+] ` inside the new `_applyBlockquotes` helper — that regex requires either zero or two leading spaces, never one — so the new "list inside blockquote" feature never fired for the canonical input shape `> - item`. Reproducer (against the actual ui.js via node, before the fix): > Hello world → <blockquote> Hello world</blockquote> ^ phantom leading space > Steps: → <blockquote>Steps: > - one - one > - two - two</blockquote> ^ literal text, NOT a <ul>; lists-in-quote feature broken > - [x] done → blockquote with literal "[x] done", no checkbox span Tests passed despite the bug because tests/test_blockquote_rendering.py and tests/test_renderer_comprehensive.py validate against a Python mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e. the mirror is correct, the JS is not, and the static-mirror tests can't catch the divergence. Same shape of bug as commit 94d63d0 (phantom <br> in trailing line) where the mirror was right and the JS was wrong. Fix: restore the space character in the strip regex's character class. Add tests/test_renderer_js_behaviour.py — 11 tests that drive the ACTUAL renderMd via node and assert on rendered output for the most common LLM shapes (single-line quote, multi-line quote, list inside quote, task list inside quote, nested >>>, strikethrough inside and outside quote, top-level task list, quote followed by heading, multi-paragraph quote with list, CRLF normalisation). Verified: the buggy regex makes 6 of those 11 tests fail; the corrected regex makes all 11 pass. Suite: 2354 passed, 0 new failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Collapse agent session compression chains * Restore upstream changelog entries * fix(agent_sessions): bubble active compression chains to top by tip last_activity The original PR merge kept the chain head's id/title/started_at and overrode id/model/message_count/ended_at/end_reason from the tip — but did NOT override last_activity. Since the projected list is sorted by last_activity DESC and the WebUI sidebar surfaces updated_at = last_activity, an actively-used compression chain whose tip is being edited NOW would sort by the ROOT's old last_activity and fall below recently touched standalone sessions. Reproducer (with the harness against actual code, before the fix): - root: started 30 days ago, last msg 30 days ago - tip: started 28 days ago (parent_session_id=root), last msg 5 seconds ago - standalone: last msg 2 days ago Sidebar order with original PR: [0] standalone (48h ago) [1] active_tip (last_activity=root's 720h ago) ← wrong Sidebar order after fix: [0] active_tip (last_activity=tip's 0h ago) ← correct [1] standalone (48h ago) This matches Hermes Agent's own list_sessions_rich projection at hermes_state.py:903-909, which overrides "last_active" from the tip exactly so that the agent CLI's session list orders the same way. Add ``last_activity`` to the merge-from-tip key list, update the existing test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to expect tip-derived updated_at, and add test_compression_chain_bubbles_to_top_by_tip_activity locking in the bubble-to-top invariant — without this regression test the previous behaviour passed CI because no test exercised the sort order against a mixed set of chains and standalone sessions. The chain head's started_at (created_at) and title remain preserved, so users can still find the conversation by its original date and name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.216 release notes and version bump Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix. * chore: gitignore local-only review harness directory Adds .local-review/ to .gitignore so renderer drivers, sample inputs, fixture builders, and other reviewer scratch files do not accidentally get committed. Nothing under that path is ever shared in the repo; keeping the entry tracked makes the boundary explicit for any future contributor who creates the directory locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Keep reasoning chip visible for None effort * test(reasoning): pin chip render output via node, not just source regex The PR's static checks in test_reasoning_chip_btw_fixes.py validate the shape of _applyReasoningChip (no display='none' literal, the right classList.toggle call exists, the right label literals are in the function body) but pass even if the runtime detail is wrong — for example if `inactive` were inverted, _normalizeReasoningEffort mishandled whitespace, or _formatReasoningEffortLabel returned the wrong literal for an unknown input. Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive the actual _applyReasoningChip() via node and assert on the rendered DOM state for each effort value: TestChipAlwaysVisible - empty / null -> "Default" label, inactive=true - "none" -> "None" label, inactive=true - "low"/"high" -> verbatim label, inactive=false TestNormalizationEdgeCases - "NONE" -> normalises to "None" - " none " -> trims and normalises - unknown junk -> falls through visible, never hidden TestTitleAttributeAccessibility - title attribute carries the human-readable label for tooltip / screen-reader use Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught). Against this PR's ui.js: 11/11 pass. This pattern (drive the actual JS via node) caught two regex-only regressions in PR #1073 where the Python mirror was correct while the JS was broken. Same protection added here so the chip-visibility contract can't silently break in a future refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add #1074 to v0.50.216 changelog, bump test count to 2428 * fix(i18n): restore broken Unicode in Russian and Spanish steer strings Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally stripped the `\u` prefix from Unicode escape sequences in two locales, producing garbled literal hex strings visible to users: Spanish (es): - cmd_steer: correcci00f3n → corrección - cmd_steer_fallback: 2014 en cola → — en cola - busy_steer_fallback: 2014 en cola → — en cola - settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección - settings_busy_input_mode_steer: correcci00f3n → corrección Russian (ru): - settings_desc_busy_input_mode: the entire Cyrillic string was replaced with raw 4-hex-char code-points without the \u prefix (041e043f... instead of actual Cyrillic). Decoded: "Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer внедряет коррекцию без прерывания." Fix: write the correct characters directly (UTF-8 is the file encoding so embedding them literally is cleaner than \u escapes for long text). All other locales (en, de, zh, zh-Hant) were not affected — confirmed by grepping for bare hex run-ons in the updated file. Verified: node --check static/i18n.js passes; full pytest suite green (2365 passed, 47 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: remove duplicate compression chain entry from [Unreleased] --------- 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> Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-26 04:06:31 +00:00
s=s.replace(/~~(.+?)~~/g,(_,t)=>`<del>${esc(t)}</del>`);
s=s.replace(/\x00O(\d+)\x00/g,(_,i)=>_ob_stash[+i]);
s=s.replace(/^###### (.+)$/gm,(_,t)=>`<h6>${inlineMd(t)}</h6>`).replace(/^##### (.+)$/gm,(_,t)=>`<h5>${inlineMd(t)}</h5>`).replace(/^#### (.+)$/gm,(_,t)=>`<h4>${inlineMd(t)}</h4>`).replace(/^### (.+)$/gm,(_,t)=>`<h3>${inlineMd(t)}</h3>`).replace(/^## (.+)$/gm,(_,t)=>`<h2>${inlineMd(t)}</h2>`).replace(/^# (.+)$/gm,(_,t)=>`<h1>${inlineMd(t)}</h1>`);
s=s.replace(/^---+$/gm,'<hr>');
v0.50.218: chat bubble overflow, project color picker, blockquote renderer (#1085) * fix(css): add overflow-wrap:anywhere to chat bubbles — prevents long URL overflow (#1080) * fix(projects): rename now works via dblclick timer guard + right-click color picker (#1078) * fix(renderer): block-level constructs inside blockquotes now render Fenced code blocks, headings, horizontal rules, and ordered lists inside blockquotes now render correctly. Six related bugs documented in blockquote-rendering-bugs.md were collapsed into one architectural fix in renderMd(). Bugs fixed (all 6): 1. Fenced code blocks inside blockquotes -- > prefixes leaked into the <pre> body and the blockquote got fragmented around the rendered code, sometimes leaving raw <pre>/<div class="pre-header"> as visible text. 2. Blank > continuation lines fragmented multi-paragraph blockquotes into separate <blockquote> elements with literal > between them. 3. ## headings inside blockquotes rendered as literal "##" text. 4. Numbered lists inside blockquotes rendered as plain prose. 5. Complex blockquote (mixed headings + code + list + inline code) collapsed into a monospace blob with raw markdown syntax leaking everywhere. 6. Horizontal rules (---) inside blockquotes rendered as literal text. Root cause: The per-line passes for fenced code, headings, hr, ordered lists all ran BEFORE the blockquote handler and could not match lines that started with >, so by the time blockquote stripping ran those constructs had already been mishandled. Fix: A new blockquote pre-pass at the top of renderMd(): - Walks lines fence-aware so > -prefixed lines inside non-blockquote code fences (e.g. shell prompts in bash code blocks) are not miscaptured as a blockquote. - Groups consecutive > -prefixed lines, strips the > prefix, and recursively calls renderMd() on the stripped content. The recursive call handles all block-level constructs (fenced code, headings, hr, ordered/unordered lists, nested blockquotes) using the same pipeline. - Wraps the rendered HTML in <blockquote> and stashes it with a \x00Q token. Restored at the very end of renderMd() so no later pass can mangle the inner HTML. The old _applyBlockquotes regex-replace is removed entirely along with its limited inline branches for nested blockquotes and unordered lists. Behaviour change: Blockquotes now produce CommonMark-compliant <p> wrapping for text content (was: bare text directly inside <blockquote>). The visual output is the same in browsers but the HTML structure is now standard. Tests: - 14 new behavioural tests in tests/test_renderer_js_behaviour.py drive the actual renderMd() via node and lock all 6 bug fixes. - .local-review/test_blockquote_bugs.js -- node harness covering the same scenarios, runnable manually for fast iteration. - 2407/2408 tests pass (1 pre-existing macOS-only failure deselected). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(renderer): entity decode before blockquote pre-pass + CSS margin fix - Move the &gt;/&lt;/&amp; entity-decode to run at the very top of renderMd(), before the blockquote pre-pass. Previously decode() ran at line 756 (after the pre-pass at line 697), so LLM output containing &gt;-encoded blockquotes was never matched by the pre-pass. - Add .msg-body blockquote p{margin:0} and .preview-md blockquote p{margin:0} so the new CommonMark-compliant <p> wrapping inside blockquotes doesn't add extra vertical spacing. Prior shape (bare text) had no default p-margins. - Add Node-driven tests: TestBlockquoteEntityEncodedInput covers &gt; prefix and &gt;-encoded fenced code inside blockquotes. - Add struct test: TestBlockquotePrePassOrdering::test_entity_decode_runs_before_blockquote_pre_pass locks decode < _bq_stash ordering in ui.js. Fixes found during Opus independent review of #1083. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: v0.50.218 release notes, test count 2458, roadmap update --------- 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-26 06:08:59 +00:00
// (Blockquotes are handled by the pre-pass at the top of renderMd, before
// fence_stash. The per-line passes below never see > prefixes.)
// B8: improved list handling supporting up to 2 levels of indentation
s=s.replace(/((?:^(?: )?[-*+] .+\n?)+)/gm,block=>{
const lines=block.trimEnd().split('\n');
let html='<ul>';
for(const l of lines){
const indent=/^ {2,}/.test(l);
const text=l.replace(/^ {0,4}[-*+] /,'');
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075) * fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly MIME_MAP was missing entries for .html and .htm. The server fell back to Content-Type: application/octet-stream, which browsers refuse to render as HTML in an iframe — causing a blank white preview. The rest of the pipeline was already correct: the iframe exists in static/index.html, openFile() in static/workspace.js routes .html to showPreview('html'), and _handle_file_raw() in api/routes.py sets the correct CSP sandbox header when ?inline=1 is present. The only missing piece was the MIME type. * test(workspace): lock in MIME_MAP entry for .html/.htm PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py to fix the blank workspace HTML preview iframe. Without a direct assertion on the MIME_MAP entries, the fix could silently regress (the existing test_779_html_preview.py tests cover the iframe wiring, the inline=1 query handling, and the CSP sandbox header — but none of them touch MIME_MAP itself). Add a single regression test that asserts MIME_MAP['.html'] and MIME_MAP['.htm'] are both 'text/html' so any future removal of those entries fails CI immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(composer): raise .approval-card.visible z-index above .queue-card .queue-card has z-index:2. .approval-card.visible had no z-index, so the queue flyout would render on top of the approval card when both were visible simultaneously — obscuring the Allow/Deny buttons. Fix: add z-index:3 to .approval-card.visible so approvals always render above the queue flyout. Approval is a blocking, security-relevant interaction and must never be obscured by passive UI elements. * test(composer): pin approval-card z-index > queue-card invariant PR #1071 raises .approval-card.visible to z-index:3 so the security- relevant Allow / Deny buttons stay clickable when the queue flyout is also open. Without a regression test, a future CSS edit could silently drop the z-index back below queue-card (z-index:2) and reintroduce the bug — there is no automated UI test covering this stacking interaction. Add a focused regex check that pins the invariant: .approval-card.visible z-index must be strictly greater than .queue-card z-index. Modeled on the existing CSS-regex regression style in tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: intercept /steer /interrupt /queue before busy-mode routing in send() Root cause: slash commands entered while the agent is busy never reached the command dispatcher. send() enters the busy block and returns early at line ~50, so the slash-command intercept (~line 56) is never reached. The text was queued as a plain message. When it drained after the turn ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream, and showed "No active task to stop." Fix: at the top of the busy block, before checking busyMode, check if the text starts with / and is one of the three control commands. If so, dispatch the handler immediately and return. This lets the user type /steer, /interrupt, or /queue at any time — including while the agent is mid-stream — and have them execute against the live session. Two new regression tests added: - test_slash_commands_intercepted_before_busymode_routing: verifies the intercept appears before the busyMode routing in the busy block - test_steer_intercept_calls_handler_directly: verifies the intercept calls _bc.fn(_pc.args) and returns, not queues * test(busy-intercept): pin sync input-clear before await in slash intercept PR #1072's intercept clears the msg input before awaiting the handler. Order matters: if the await happens first (or if the clear is moved inside the handler), the input still shows '/steer foo' for the duration of the await. A reflexive second Enter press during that window — common while waiting for the toast — re-runs send(): either re-fires the handler (double-steer) or, if the turn just ended, falls through to the non-busy slash dispatcher and drops a confusing "No active task to stop." Add test_steer_intercept_clears_input_before_await pinning the order so this UX invariant cannot silently regress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: update steer i18n and settings copy — steer no longer interrupts With the real /steer implementation (agent.steer() via /api/chat/steer), steer injects a correction mid-turn WITHOUT interrupting the current stream. The previous copy said "falls back to interrupt", "Steer (interrupt + send)", etc. — accurate only for the old placeholder, not the real implementation. Changes across all 6 locales (en/ru/es/de/zh/zh-Hant): cmd_steer: "falls back to interrupt" removed settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction" cmd_steer_fallback: "interrupted" → "queued for next turn" busy_steer_fallback: "interrupted instead" → "queued for next turn" settings_desc_busy_input_mode: "currently falls back to interrupt" removed Also: static/index.html: inline fallback text updated to match static/commands.js: internal comment clarified (fallback = queue+cancel, not "interrupt mode" which implies the primary action) * fix(renderer): group consecutive blockquote lines into single element Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs: 1. `.+` required at least one character — bare `>` lines (blank continuation lines) did not match and passed through as literal `>` 2. Each matching line became its own `<blockquote>` element — a 10-line blockquote produced 10 stacked `<blockquote>` tags with no grouping 3. When a fenced code block sat inside a blockquote, the fence-stash pass consumed the code content and left orphaned `>` lines that the old `.+` pattern could not match Fix: replace the single-line regex with a group-based approach that matches one or more consecutive `>` lines as a single block, strips the `>` prefix from each line, passes each non-empty line through inlineMd(), turns blank `>` lines into `<br>`, and wraps the entire group in one `<blockquote>`. 14 regression tests added covering: - Single-line blockquotes (regression) - Multi-line grouping (2 and 10 lines) - Two separate blockquotes staying separate - Bare `>` and `>text` (no space) edge cases - Blank continuation lines → <br> - Bold / italic / inline-code inside blockquotes - Blockquote followed by normal paragraph * fix(renderer): drop empty trailing line from blockquote match The new group-based blockquote rule introduced in this PR captures the trailing newline in its (?:\n|$) clause. After block.split('\n') that trailing newline produces an empty final element. The original filter only dropped lone bare '>' artifacts on the last line, so the empty final element survived, and the .map(blank → '<br>') step turned it into a phantom <br> immediately before </blockquote>. Visible symptom: any blockquote whose source ends with \n (the common case — a quote followed by another paragraph or end-of-message) renders with an extra blank line at the bottom of the quote. Reproducer: '> Hello\n\nThe rest of the message.' → '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.' ^^^ phantom <br> Fix: replace the single-line filter with a while-loop that pops trailing lines while they are either empty OR a bare '>'. This matches the intent the Python test mirror in tests/test_blockquote_rendering.py already had (the mirror was correct; the JS was not — that's why the original tests passed despite the bug). Also add four new regression tests in TestNoPhantomTrailingBr that pin the no-trailing-<br> invariant for the common shapes: - input ending with \n - quote followed by paragraph (the real-world case) - multi-line quote ending with \n - quote with blank continuation + trailing \n (internal <br> stays, trailing <br> does not) Verified end-to-end with node against the actual JS regex. 244 renderer-adjacent tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes Five additional fixes on top of the blockquote grouping from the initial commit: 1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows line endings do not produce stray \r characters in rendered output 2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use inside blockquotes/lists) and the outer pass (for plain paragraphs). Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped. 3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐ via task-done/task-todo span wrappers. Checks [X] (uppercase) too. 4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its own <blockquote> element rather than passing through as literal >. Implemented by extracting the blockquote rule into _applyBlockquotes() which calls itself recursively on the stripped inner content. 5. Lists inside blockquotes: > - item now renders <ul><li> inside the blockquote instead of a literal "- item" string. Task list items work inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>). Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF strip at the top of renderMd pushed the autolink regex past the old limit. 68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py covering all constructs, edge cases, and combinations. * fix(renderer): restore space in blockquote prefix-strip regex Commit 04e7b53 changed the blockquote prefix-strip regex from /^>[ \t]?/ (consume "> ", "\t>", or just ">") to /^>[\t]?/ (only consume "\t>" or just ">") The space character was dropped from the character class. Since practically every blockquote an LLM produces is "> " (greater-than followed by a space), this leaves a leading space artifact on every stripped blockquote line. Worse, the leading space breaks the list-detection regex `^(?: )?[-*+] ` inside the new `_applyBlockquotes` helper — that regex requires either zero or two leading spaces, never one — so the new "list inside blockquote" feature never fired for the canonical input shape `> - item`. Reproducer (against the actual ui.js via node, before the fix): > Hello world → <blockquote> Hello world</blockquote> ^ phantom leading space > Steps: → <blockquote>Steps: > - one - one > - two - two</blockquote> ^ literal text, NOT a <ul>; lists-in-quote feature broken > - [x] done → blockquote with literal "[x] done", no checkbox span Tests passed despite the bug because tests/test_blockquote_rendering.py and tests/test_renderer_comprehensive.py validate against a Python mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e. the mirror is correct, the JS is not, and the static-mirror tests can't catch the divergence. Same shape of bug as commit 94d63d0 (phantom <br> in trailing line) where the mirror was right and the JS was wrong. Fix: restore the space character in the strip regex's character class. Add tests/test_renderer_js_behaviour.py — 11 tests that drive the ACTUAL renderMd via node and assert on rendered output for the most common LLM shapes (single-line quote, multi-line quote, list inside quote, task list inside quote, nested >>>, strikethrough inside and outside quote, top-level task list, quote followed by heading, multi-paragraph quote with list, CRLF normalisation). Verified: the buggy regex makes 6 of those 11 tests fail; the corrected regex makes all 11 pass. Suite: 2354 passed, 0 new failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Collapse agent session compression chains * Restore upstream changelog entries * fix(agent_sessions): bubble active compression chains to top by tip last_activity The original PR merge kept the chain head's id/title/started_at and overrode id/model/message_count/ended_at/end_reason from the tip — but did NOT override last_activity. Since the projected list is sorted by last_activity DESC and the WebUI sidebar surfaces updated_at = last_activity, an actively-used compression chain whose tip is being edited NOW would sort by the ROOT's old last_activity and fall below recently touched standalone sessions. Reproducer (with the harness against actual code, before the fix): - root: started 30 days ago, last msg 30 days ago - tip: started 28 days ago (parent_session_id=root), last msg 5 seconds ago - standalone: last msg 2 days ago Sidebar order with original PR: [0] standalone (48h ago) [1] active_tip (last_activity=root's 720h ago) ← wrong Sidebar order after fix: [0] active_tip (last_activity=tip's 0h ago) ← correct [1] standalone (48h ago) This matches Hermes Agent's own list_sessions_rich projection at hermes_state.py:903-909, which overrides "last_active" from the tip exactly so that the agent CLI's session list orders the same way. Add ``last_activity`` to the merge-from-tip key list, update the existing test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to expect tip-derived updated_at, and add test_compression_chain_bubbles_to_top_by_tip_activity locking in the bubble-to-top invariant — without this regression test the previous behaviour passed CI because no test exercised the sort order against a mixed set of chains and standalone sessions. The chain head's started_at (created_at) and title remain preserved, so users can still find the conversation by its original date and name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v0.50.216 release notes and version bump Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix. * chore: gitignore local-only review harness directory Adds .local-review/ to .gitignore so renderer drivers, sample inputs, fixture builders, and other reviewer scratch files do not accidentally get committed. Nothing under that path is ever shared in the repo; keeping the entry tracked makes the boundary explicit for any future contributor who creates the directory locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Keep reasoning chip visible for None effort * test(reasoning): pin chip render output via node, not just source regex The PR's static checks in test_reasoning_chip_btw_fixes.py validate the shape of _applyReasoningChip (no display='none' literal, the right classList.toggle call exists, the right label literals are in the function body) but pass even if the runtime detail is wrong — for example if `inactive` were inverted, _normalizeReasoningEffort mishandled whitespace, or _formatReasoningEffortLabel returned the wrong literal for an unknown input. Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive the actual _applyReasoningChip() via node and assert on the rendered DOM state for each effort value: TestChipAlwaysVisible - empty / null -> "Default" label, inactive=true - "none" -> "None" label, inactive=true - "low"/"high" -> verbatim label, inactive=false TestNormalizationEdgeCases - "NONE" -> normalises to "None" - " none " -> trims and normalises - unknown junk -> falls through visible, never hidden TestTitleAttributeAccessibility - title attribute carries the human-readable label for tooltip / screen-reader use Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught). Against this PR's ui.js: 11/11 pass. This pattern (drive the actual JS via node) caught two regex-only regressions in PR #1073 where the Python mirror was correct while the JS was broken. Same protection added here so the chip-visibility contract can't silently break in a future refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add #1074 to v0.50.216 changelog, bump test count to 2428 * fix(i18n): restore broken Unicode in Russian and Spanish steer strings Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally stripped the `\u` prefix from Unicode escape sequences in two locales, producing garbled literal hex strings visible to users: Spanish (es): - cmd_steer: correcci00f3n → corrección - cmd_steer_fallback: 2014 en cola → — en cola - busy_steer_fallback: 2014 en cola → — en cola - settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección - settings_busy_input_mode_steer: correcci00f3n → corrección Russian (ru): - settings_desc_busy_input_mode: the entire Cyrillic string was replaced with raw 4-hex-char code-points without the \u prefix (041e043f... instead of actual Cyrillic). Decoded: "Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer внедряет коррекцию без прерывания." Fix: write the correct characters directly (UTF-8 is the file encoding so embedding them literally is cleaner than \u escapes for long text). All other locales (en, de, zh, zh-Hant) were not affected — confirmed by grepping for bare hex run-ons in the updated file. Verified: node --check static/i18n.js passes; full pytest suite green (2365 passed, 47 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: remove duplicate compression chain entry from [Unreleased] --------- 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> Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-26 04:06:31 +00:00
let _ih;
if(/^\[x\] /i.test(text)) _ih='<span class="task-done">✅</span> '+inlineMd(text.slice(4));
else if(/^\[ \] /.test(text)) _ih='<span class="task-todo">☐</span> '+inlineMd(text.slice(4));
else _ih=inlineMd(text);
if(indent) html+=`<li style="margin-left:16px">${_ih}</li>`;
else html+=`<li>${_ih}</li>`;
}
return html+'</ul>';
});
// Ordered lists: use value= on each <li> so the correct number is preserved
// even when blank lines between items cause the paragraph splitter to place
// each item in its own <ol> container — without value= every <ol> restarts
// at 1, producing "1. 1. 1." instead of "1. 2. 3." (#886).
s=s.replace(/((?:^(?: )?\d+\. .+\n?)+)/gm,block=>{
const lines=block.trimEnd().split('\n');
let html='<ol>';
for(const l of lines){
const numMatch=l.match(/^\s*(\d+)\. /);
const num=numMatch?parseInt(numMatch[1],10):null;
const text=l.replace(/^ {0,4}\d+\. /,'');
const valAttr=num!==null?` value="${num}"`:'';
html+=`<li${valAttr}>${inlineMd(text)}</li>`;
}
return html+'</ol>';
});
// Tables: | col | col | header row followed by | --- | --- | separator then data rows
// NOTE: table pass runs BEFORE outer link pass so [label](url) in table cells
// is handled by inlineMd() only — prevents double-linking.
s=s.replace(/((?:^\|.+\|\n?)+)/gm,block=>{
const rows=block.trim().split('\n').filter(r=>r.trim());
if(rows.length<2)return block;
const isSep=r=>/^\|[\s|:-]+\|$/.test(r.trim());
if(!isSep(rows[1]))return block;
const parseRow=r=>r.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(c=>`<td>${inlineMd(c.trim())}</td>`).join('');
const parseHeader=r=>r.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(c=>`<th>${inlineMd(c.trim())}</th>`).join('');
const header=`<tr>${parseHeader(rows[0])}</tr>`;
const body=rows.slice(2).map(r=>`<tr>${parseRow(r)}</tr>`).join('');
return `<table><thead>${header}</thead><tbody>${body}</tbody></table>`;
});
// #487: Outer image pass — handles ![alt](url) in plain paragraphs (outside tables/lists).
// Runs AFTER the table pass (images in table cells are handled by inlineMd() above).
// Runs BEFORE the outer [label](url) link pass so the image is not consumed as a plain link.
s=s.replace(/!\[([^\]]*)\]\((https?:\/\/[^\)]+)\)/g,(_,alt,url)=>`<img src="${url.replace(/"/g,'%22')}" alt="${esc(alt)}" class="msg-media-img" loading="lazy">`);
// Outer link pass for labeled links in plain paragraphs (outside table cells).
// Runs AFTER the table pass so table cells are processed by inlineMd() only.
// Stash existing <a> tags first to avoid re-linking already-linked URLs.
const _a_stash=[];
s=s.replace(/(<a\b[^>]*>[\s\S]*?<\/a>)/g,m=>{_a_stash.push(m);return `\x00A${_a_stash.length-1}\x00`;});
s=s.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,(_,label,url)=>`<a href="${url.replace(/"/g,'%22')}" target="_blank" rel="noopener">${esc(label)}</a>`);
s=s.replace(/\x00A(\d+)\x00/g,(_,i)=>_a_stash[+i]);
// Restore raw <pre> only after markdown rewrites so literal preformatted
// content stays placeholder-protected, then let the sanitizer normalize tags.
s=s.replace(/\x00R(\d+)\x00/g,(_,i)=>rawPreStash[+i]);
// Sanitize any remaining HTML tags. The renderer intentionally returns
// HTML and inserts it with innerHTML later, so tag names alone are not enough:
// raw/model-provided HTML like <img onerror=...> or <a href="javascript:...">
// must lose executable attributes and dangerous schemes while preserving the
// small set of attributes generated by this markdown pipeline.
// Reference only — documents the allowed tag set. Superseded by _tag() allowlists.
// Tests verify this list is complete; _tag() enforces it.
const SAFE_TAGS=/^<\/?(?:strong|em|del|code|pre|h[1-6]|ul|ol|li|table|thead|tbody|tr|th|td|hr|blockquote|p|br|a|div|span|img)([\s>]|$)/i;
function _safeAttrValue(v){
return String(v||'').replace(/&quot;/g,'"').replace(/&#39;/g,"'").replace(/&amp;/g,'&').trim();
}
function _isSafeUrl(v, img){
const raw=_safeAttrValue(v);
const compact=raw.replace(/[\u0000-\u001f\u007f\s]+/g,'').toLowerCase();
if(!compact) return false;
if(/^(javascript|data|vbscript):/i.test(compact)) return false;
if(/^https?:\/\//i.test(raw)) return true;
if(img && /^api\//i.test(raw)) return true;
if(!img && (/^api\//i.test(raw) || /^#/.test(raw))) return true;
return false;
}
function _attrs(raw){
const out={};
String(raw||'').replace(/([a-zA-Z0-9:_-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>`]+)))?/g,(_,k,dq,sq,bare)=>{
out[String(k).toLowerCase()]=dq!==undefined?dq:(sq!==undefined?sq:(bare!==undefined?bare:''));
return '';
});
return out;
}
function _cls(v, allowed){
const got=String(v||'').split(/\s+/).filter(c=>allowed.includes(c));
return got.length?` class="${esc(got.join(' '))}"`:'';
}
function _tag(tag){
const m=String(tag||'').match(/^<\s*(\/)?\s*([a-zA-Z][\w:-]*)([\s\S]*?)(\/)?\s*>$/);
if(!m) return esc(tag);
const closing=!!m[1];
const name=m[2].toLowerCase();
const rawAttrs=m[3]||'';
const plain=['strong','em','del','pre','h1','h2','h3','h4','h5','h6','ul','ol','table','thead','tbody','tr','th','td','blockquote','p','br','hr'];
if(closing) return plain.includes(name)||['a','div','span','li','code'].includes(name)?`</${name}>`:'';
if(name==='code'){
const a=_attrs(rawAttrs);
const cls=/^language-[a-z0-9_+-]+$/i.test(a.class||'')?` class="${esc(a.class)}"`:'';
return `<code${cls}>`;
}
if(plain.includes(name)) return `<${name}>`;
const a=_attrs(rawAttrs);
if(name==='li'){
const value=/^\d+$/.test(a.value||'')?` value="${esc(a.value)}"`:'';
const style=(a.style||'').replace(/\s+/g,'').toLowerCase()==='margin-left:16px'?` style="margin-left:16px"`:'';
return `<li${value}${style}>`;
}
if(name==='span'){
return `<span${_cls(a.class,['task-done','task-todo','katex-inline'])}${a['data-katex']==='inline'?' data-katex="inline"':''}>`;
}
if(name==='div'){
const cls=_cls(a.class,['pre-header','mermaid-block','katex-block']);
const mermaid=a['data-mermaid-id']?` data-mermaid-id="${esc(a['data-mermaid-id'])}"`:'';
const katex=a['data-katex']==='display'?' data-katex="display"':'';
return `<div${cls}${mermaid}${katex}>`;
}
if(name==='a'){
if(!_isSafeUrl(a.href,false)) return '<a>';
const target=a.target==='_blank'?' target="_blank"':'';
const rel=a.rel==='noopener'?' rel="noopener"':'';
const cls=_cls(a.class,['msg-media-link','skill-linked-file','skill-file-back']);
const download=a.download?` download="${esc(a.download)}"`:'';
return `<a${cls} href="${esc(_safeAttrValue(a.href))}"${target}${rel}${download}>`;
}
if(name==='img'){
if(!_isSafeUrl(a.src,true)) return '';
const cls=_cls(a.class,['msg-media-img']);
const alt=` alt="${esc(_safeAttrValue(a.alt||''))}"`;
const loading=a.loading==='lazy'?' loading="lazy"':'';
return `<img${cls} src="${esc(_safeAttrValue(a.src))}"${alt}${loading}>`;
}
return '';
}
s=s.replace(/<\/?[a-z][^>]*>/gi,tag=>_tag(tag));
// Incomplete raw tags must not survive until paragraph wrapping, where the
// renderer's generated </p> could provide a closing ">" and turn them into
// executable HTML in innerHTML (for example: <img src=x onerror=...//).
s=s.replace(/<[a-zA-Z][\w:-]*[^>\n]*$/gm,tag=>esc(tag));
// Autolink: convert plain URLs to clickable links.
// Stash <a>, <img> and <pre> blocks so autolink never runs inside them.
const _al_stash=[];
s=s.replace(/(<a\b[^>]*>[\s\S]*?<\/a>|<img\b[^>]*>|<pre\b[^>]*>[\s\S]*?<\/pre>)/g,m=>{_al_stash.push(m);return `\x00B${_al_stash.length-1}\x00`;});
s=s.replace(/(https?:\/\/[^\s<>"'\)\]]+)/g,(url)=>{
// Strip trailing punctuation that was likely not part of the URL
const trail=url.match(/[.,;:!?)]$/)?url.slice(-1):'';
const clean=trail?url.slice(0,-1):url;
return `<a href="${clean}" target="_blank" rel="noopener">${esc(clean)}</a>${trail}`;
});
s=s.replace(/\x00B(\d+)\x00/g,(_,i)=>_al_stash[+i]);
feat: KaTeX math rendering for LaTeX in chat + workspace previews (#352) * feat: KaTeX math rendering for $..$ and $$..$$ in chat and previews (fixes #347) - Stash math delimiters before markdown pipeline, restore as .katex-block/.katex-inline elements - KaTeX JS lazy-loaded from CDN on first math block (mirrors mermaid pattern) - KaTeX CSS loaded eagerly in <head> to prevent layout shift - SRI hashes on both CDN tags - throwOnError:false — bad LaTeX degrades to code span - Supports $$, $, \\(...\\), \\[...\\] delimiters - 18 new tests, 831/831 passing * fix: remove invalid \' escape sequences in math stash lines Lines 311, 314, 316, 317 had \' (backslash-quote) instead of plain ' in the arrow function bodies. This is a JS syntax error — node --check fails with 'Invalid or unexpected token'. Likely caused by a serialization artifact during code generation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: swap stash order (fence before math) to protect code spans; add renderKatexBlocks to workspace preview - static/ui.js: fence_stash now runs BEFORE math_stash so dollar signs inside backtick code spans are not extracted as math. Previously `$x$` would render as KaTeX inside a <code> tag instead of showing the literal string $x$. - static/workspace.js: add requestAnimationFrame(renderKatexBlocks) after markdown preview renders so math works in workspace file previews, not only in chat messages. * feat: KaTeX math rendering + stash order fix + workspace wiring (#352) - tests/test_issue347.py: 11 new tests (29 total) covering fence-before-math ordering, workspace.js renderKatexBlocks call, stash token distinctness, false-positive prevention, safe-tags boundary check - CHANGELOG.md: v0.50.15 entry; 870 tests total (up from 841) * fix: use literal null byte (\x00M) in math stash token — matches restore regex The original PR's second commit (fix: remove invalid \' escapes) accidentally doubled the backslash in the math stash tokens: '\\x00M' is a 5-char string (backslash + x + 0 + 0 + M) but the restore regex /\x00M/ expects a null byte. Result: $...$ in messages produced visible \x00M0\x00 tokens instead of KaTeX spans. Changed all 4 math stash return statements to use '\x00M' (single backslash = null byte, same convention as fence_stash's '\x00F'). Also updates test_stash_tokens_distinct to check for the correct pattern. * fix: add null-byte token test; update CHANGELOG to v0.50.15 with fixes - tests/test_issue347.py: add test_math_stash_token_uses_single_backslash_null_byte to catch the \\x00M double-backslash regression; 30 tests total (up from 29) - CHANGELOG.md: v0.50.15 entry documents all fixes including the token bug and workspace preview wiring; 871 tests total --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:40:15 +00:00
// Restore math stash → katex placeholder spans/divs
// These will be rendered by renderKatexBlocks() after DOM insertion
s=s.replace(/\x00M(\d+)\x00/g,(_,i)=>{
const item=math_stash[+i];
if(item.type==='display'){
return `<div class="katex-block" data-katex="display">${esc(item.src)}</div>`;
}
return `<span class="katex-inline" data-katex="inline">${esc(item.src)}</span>`;
});
2026-04-28 00:43:36 +00:00
// Restore fenced block stash (\x00P) → <pre><code> HTML.
// Happens AFTER all markdown passes (lists, headings, tables, etc.) so
// diff/patch content inside code blocks is never misinterpreted as markdown.
// The _pre_stash below then protects these blocks from paragraph splitting.
s=s.replace(/\x00P(\d+)\x00/g,(_,i)=>_preBlock_stash[+i]);
// Stash rendered <pre> blocks (with optional pre-header div) and mermaid/katex
// divs before paragraph splitting so \n inside code blocks is never replaced
// with <br>. Token \x00E (next free after B D F G L M C O A).
// Fixes #745: code blocks collapse to single line when not preceded by blank line.
const _pre_stash=[];
fix(renderer): YAML/JSON/diff code blocks lose newlines (#1618 / #1463) Closes #1618 (reported by @Zixim) and corrects #1463's previous fix. Bug: YAML, JSON, and diff/patch fenced code blocks render flattened to a single line. Reporter noted the bug persisted v0.50.279 -> v0.50.291 -> v0.50.292 despite PR #1516's CSS-only "fix". Root cause: PR #484 (v0.50.237) added a JSON/YAML tree-viewer that routes those languages through <div class="code-tree-wrap">...<pre class="tree-raw-view"> instead of bare <pre>. Same release added the diff/patch coloring path that emits <pre class="diff-block">. The _pre_stash regex at static/ui.js:1914 matched only literal <pre> with no attributes: <pre>[\s\S]*?<\/pre> Both new shapes failed to match, fell through to the paragraph-wrap pass, and \n characters inside the code blocks got replaced with <br> tags inside <code>. By the time Prism ran, no newlines remained for the CSS rule (PR #1516, language-yaml .token { white-space: pre !important }) to preserve. Fix: relax the regex to accept any attribute on <pre>: <pre>[\s\S]*?<\/pre> -> <pre[^>]*>[\s\S]*?<\/pre> One regex character. Pulls JSON, YAML, and diff/patch blocks into the stash so paragraph-wrap can't mangle them. Bash, Python, Go, etc. were never affected because they emit bare <pre>. Tests: 9 new (2 source-string invariants + 7 behavioural via node-driver against the actual static/ui.js renderMd()). 6 of the 7 behavioural tests fail on master and pass with the fix; the 3 sanity checks (yml-alias, bash, mermaid) pass on both. Plus widened source-scan window in 3 pre-existing test_745 assertions from 400 to 1500 chars. The new comment block above the fixed regex pushed it past the previous scan window. Pure window-narrowness bug, not a behavior regression. 4245 -> 4254 passing.
2026-05-04 18:11:58 +00:00
// #1463 / #1618: regex must match <pre> with ANY attributes — PR #484 added
// <pre class="tree-raw-view"> for JSON/YAML and <pre class="diff-block"> for
// diff/patch which the literal-<pre> shape missed. Newlines inside those
// blocks were falling through to the paragraph wrap below and getting
// converted to <br>, causing the YAML/JSON/diff collapse. PR #1516's CSS
// fix targeted the wrong layer (Prism token white-space) — by the time it
// ran, the \n had already been replaced. The CSS rule is kept as defense
// in depth.
s=s.replace(/(<div class="pre-header">[\s\S]*?<\/div>)?<pre[^>]*>[\s\S]*?<\/pre>|<div class="(mermaid-block|katex-block)"[\s\S]*?<\/div>/g,m=>{
_pre_stash.push(m);
return '\x00E'+(_pre_stash.length-1)+'\x00';
});
const parts=s.split(/\n{2,}/);
v0.50.218: chat bubble overflow, project color picker, blockquote renderer (#1085) * fix(css): add overflow-wrap:anywhere to chat bubbles — prevents long URL overflow (#1080) * fix(projects): rename now works via dblclick timer guard + right-click color picker (#1078) * fix(renderer): block-level constructs inside blockquotes now render Fenced code blocks, headings, horizontal rules, and ordered lists inside blockquotes now render correctly. Six related bugs documented in blockquote-rendering-bugs.md were collapsed into one architectural fix in renderMd(). Bugs fixed (all 6): 1. Fenced code blocks inside blockquotes -- > prefixes leaked into the <pre> body and the blockquote got fragmented around the rendered code, sometimes leaving raw <pre>/<div class="pre-header"> as visible text. 2. Blank > continuation lines fragmented multi-paragraph blockquotes into separate <blockquote> elements with literal > between them. 3. ## headings inside blockquotes rendered as literal "##" text. 4. Numbered lists inside blockquotes rendered as plain prose. 5. Complex blockquote (mixed headings + code + list + inline code) collapsed into a monospace blob with raw markdown syntax leaking everywhere. 6. Horizontal rules (---) inside blockquotes rendered as literal text. Root cause: The per-line passes for fenced code, headings, hr, ordered lists all ran BEFORE the blockquote handler and could not match lines that started with >, so by the time blockquote stripping ran those constructs had already been mishandled. Fix: A new blockquote pre-pass at the top of renderMd(): - Walks lines fence-aware so > -prefixed lines inside non-blockquote code fences (e.g. shell prompts in bash code blocks) are not miscaptured as a blockquote. - Groups consecutive > -prefixed lines, strips the > prefix, and recursively calls renderMd() on the stripped content. The recursive call handles all block-level constructs (fenced code, headings, hr, ordered/unordered lists, nested blockquotes) using the same pipeline. - Wraps the rendered HTML in <blockquote> and stashes it with a \x00Q token. Restored at the very end of renderMd() so no later pass can mangle the inner HTML. The old _applyBlockquotes regex-replace is removed entirely along with its limited inline branches for nested blockquotes and unordered lists. Behaviour change: Blockquotes now produce CommonMark-compliant <p> wrapping for text content (was: bare text directly inside <blockquote>). The visual output is the same in browsers but the HTML structure is now standard. Tests: - 14 new behavioural tests in tests/test_renderer_js_behaviour.py drive the actual renderMd() via node and lock all 6 bug fixes. - .local-review/test_blockquote_bugs.js -- node harness covering the same scenarios, runnable manually for fast iteration. - 2407/2408 tests pass (1 pre-existing macOS-only failure deselected). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(renderer): entity decode before blockquote pre-pass + CSS margin fix - Move the &gt;/&lt;/&amp; entity-decode to run at the very top of renderMd(), before the blockquote pre-pass. Previously decode() ran at line 756 (after the pre-pass at line 697), so LLM output containing &gt;-encoded blockquotes was never matched by the pre-pass. - Add .msg-body blockquote p{margin:0} and .preview-md blockquote p{margin:0} so the new CommonMark-compliant <p> wrapping inside blockquotes doesn't add extra vertical spacing. Prior shape (bare text) had no default p-margins. - Add Node-driven tests: TestBlockquoteEntityEncodedInput covers &gt; prefix and &gt;-encoded fenced code inside blockquotes. - Add struct test: TestBlockquotePrePassOrdering::test_entity_decode_runs_before_blockquote_pre_pass locks decode < _bq_stash ordering in ui.js. Fixes found during Opus independent review of #1083. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: v0.50.218 release notes, test count 2458, roadmap update --------- 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-26 06:08:59 +00:00
s=parts.map(p=>{p=p.trim();if(!p)return '';if(/^<(h[1-6]|ul|ol|pre|hr|blockquote)|^\x00[EQ]/.test(p))return p;return `<p>${p.replace(/\n/g,'<br>')}</p>`;}).join('\n');
s=s.replace(/\x00E(\d+)\x00/g,(_,i)=>_pre_stash[+i]);
// ── Restore MEDIA stash → inline images or download links ─────────────────
s=s.replace(/\x00D(\d+)\x00/g,(_,i)=>{
const ref=media_stash[+i];
// Keep this logic self-contained: some tests extract renderMd() alone and
// execute it in node, without the top-level helper functions from ui.js.
const mediaKindForName=(name='')=>{
const clean=String(name||'').split('?')[0].toLowerCase();
if(/\.(mp3|wav|m4a|aac|ogg|oga|opus|flac)$/i.test(clean)) return 'audio';
if(/\.(mp4|mov|m4v|webm|ogv|avi|mkv)$/i.test(clean)) return 'video';
if(_IMAGE_EXTS.test(clean)) return 'image';
return '';
};
const mediaPlayerHtml=(kind,src,name)=>{
if(typeof _mediaPlayerHtml==='function') return _mediaPlayerHtml(kind,src,name);
const safeName=esc(name||kind||'media');
const safeSrc=esc(src);
const tag=kind==='video'
? `<video class="msg-media-player msg-media-video" src="${safeSrc}" controls preload="metadata" playsinline title="${safeName}"></video>`
: `<audio class="msg-media-player msg-media-audio" src="${safeSrc}" controls preload="metadata" title="${safeName}"></audio>`;
return `<div class="msg-media-editor msg-media-editor--${kind}" data-media-kind="${kind}">${tag}<div class="msg-media-meta"><span class="msg-media-name">${safeName}</span></div></div>`;
};
// HTTP(S) URL
if(/^https?:\/\//i.test(ref)){
release: v0.50.102–v0.50.108 batch (code blocks, utf-8, image URLs, deletion warning, PermissionError, Docker docs, kimi-k2.5) (#755) ## Batch release: v0.50.102 – v0.50.108 Seven self-built PRs reviewed and approved by @nesquena, now consolidated into a single release branch. ### Included fixes | Version | PR | What it fixes | |---|---|---| | v0.50.102 | #746 | Code blocks lose newlines when not preceded by blank line (fixes #745) | | v0.50.103 | #743 | `encoding='utf-8'` on `write_text()` in `api/profiles.py` — Windows `.env` detection (fixes #741) | | v0.50.104 | #735 | Agent `MEDIA:localhost:*` image URLs rewritten to `document.baseURI` — remote users get working images (fixes #642) | | v0.50.105 | #736 | Profile deletion warning strengthened: "permanently deleted, cannot be undone" across all 6 locales (fixes #637) | | v0.50.106 | #738 | Catch `PermissionError` in `_signing_key()` — three-container Docker UID mismatch no longer crashes all HTTP requests | | v0.50.107 | #737 | Docs: three-container UID/GID alignment guide in README + `HERMES_UID`/`HERMES_GID` forwarded in compose (fixes #645) | | v0.50.108 | #742 | Add `kimi-k2.5` to Kimi/Moonshot provider model list (fixes #740) | ### Testing - **pytest**: 1510 passed, 1 warning (1 pre-existing unrelated failure excluded) - **QA harness**: 20/20 passed (`~/WebUI/scripts/run-browser-tests.sh`) - **Browser**: layout, slash autocomplete width, edit button, image URL rewrite, profile deletion dialog all verified All PRs reviewed and approved by @nesquena. Ready to merge and tag **v0.50.108**.
2026-04-20 07:26:55 +00:00
// Rewrite localhost/127.0.0.1 to the actual server base URL so remote
// users (VPN, Docker, deployed) can load agent-generated images (#642).
// Strip the trailing slash from document.baseURI so the URL's own path
// joins cleanly — this preserves any subpath mount (e.g. /hermes/).
let src=ref;
if(/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/i.test(src)){
const base=(document.baseURI||'').replace(/\/$/,'');
release: v0.50.102–v0.50.108 batch (code blocks, utf-8, image URLs, deletion warning, PermissionError, Docker docs, kimi-k2.5) (#755) ## Batch release: v0.50.102 – v0.50.108 Seven self-built PRs reviewed and approved by @nesquena, now consolidated into a single release branch. ### Included fixes | Version | PR | What it fixes | |---|---|---| | v0.50.102 | #746 | Code blocks lose newlines when not preceded by blank line (fixes #745) | | v0.50.103 | #743 | `encoding='utf-8'` on `write_text()` in `api/profiles.py` — Windows `.env` detection (fixes #741) | | v0.50.104 | #735 | Agent `MEDIA:localhost:*` image URLs rewritten to `document.baseURI` — remote users get working images (fixes #642) | | v0.50.105 | #736 | Profile deletion warning strengthened: "permanently deleted, cannot be undone" across all 6 locales (fixes #637) | | v0.50.106 | #738 | Catch `PermissionError` in `_signing_key()` — three-container Docker UID mismatch no longer crashes all HTTP requests | | v0.50.107 | #737 | Docs: three-container UID/GID alignment guide in README + `HERMES_UID`/`HERMES_GID` forwarded in compose (fixes #645) | | v0.50.108 | #742 | Add `kimi-k2.5` to Kimi/Moonshot provider model list (fixes #740) | ### Testing - **pytest**: 1510 passed, 1 warning (1 pre-existing unrelated failure excluded) - **QA harness**: 20/20 passed (`~/WebUI/scripts/run-browser-tests.sh`) - **Browser**: layout, slash autocomplete width, edit button, image URL rewrite, profile deletion dialog all verified All PRs reviewed and approved by @nesquena. Ready to merge and tag **v0.50.108**.
2026-04-20 07:26:55 +00:00
src=src.replace(/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/i,base);
}
// MEDIA: tokens are usually tool-generated images. Render all https://
// URLs as <img> so extensionless CDN paths still work (#853), while
// preserving explicit audio/video/SVG URLs with their proper handlers.
const urlPath=src.split('?')[0];
const mediaKind=mediaKindForName(urlPath);
// SVG URLs → render inline as image
if(_SVG_EXTS.test(urlPath)){
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
return `<img class="msg-media-svg" src="${esc(src)}" alt="${t('media_svg_label')}" loading="lazy">`;
}
if(mediaKind==='audio'||mediaKind==='video') return mediaPlayerHtml(mediaKind,src,urlPath.split('/').pop()||mediaKind);
// Render all https:// URLs as <img> — extensionless CDN paths like fal.media still work (#853)
if(_IMAGE_EXTS.test(urlPath) || /^https?:\/\//i.test(src)){
return `<img class="msg-media-img" src="${esc(src)}" alt="image" loading="lazy">`;
release: v0.50.102–v0.50.108 batch (code blocks, utf-8, image URLs, deletion warning, PermissionError, Docker docs, kimi-k2.5) (#755) ## Batch release: v0.50.102 – v0.50.108 Seven self-built PRs reviewed and approved by @nesquena, now consolidated into a single release branch. ### Included fixes | Version | PR | What it fixes | |---|---|---| | v0.50.102 | #746 | Code blocks lose newlines when not preceded by blank line (fixes #745) | | v0.50.103 | #743 | `encoding='utf-8'` on `write_text()` in `api/profiles.py` — Windows `.env` detection (fixes #741) | | v0.50.104 | #735 | Agent `MEDIA:localhost:*` image URLs rewritten to `document.baseURI` — remote users get working images (fixes #642) | | v0.50.105 | #736 | Profile deletion warning strengthened: "permanently deleted, cannot be undone" across all 6 locales (fixes #637) | | v0.50.106 | #738 | Catch `PermissionError` in `_signing_key()` — three-container Docker UID mismatch no longer crashes all HTTP requests | | v0.50.107 | #737 | Docs: three-container UID/GID alignment guide in README + `HERMES_UID`/`HERMES_GID` forwarded in compose (fixes #645) | | v0.50.108 | #742 | Add `kimi-k2.5` to Kimi/Moonshot provider model list (fixes #740) | ### Testing - **pytest**: 1510 passed, 1 warning (1 pre-existing unrelated failure excluded) - **QA harness**: 20/20 passed (`~/WebUI/scripts/run-browser-tests.sh`) - **Browser**: layout, slash autocomplete width, edit button, image URL rewrite, profile deletion dialog all verified All PRs reviewed and approved by @nesquena. Ready to merge and tag **v0.50.108**.
2026-04-20 07:26:55 +00:00
}
return `<a href="${esc(src)}" target="_blank" rel="noopener">${esc(src)}</a>`;
}
// Local file path
const apiUrl='api/media?path='+encodeURIComponent(ref);
const localKind=mediaKindForName(ref);
if(localKind==='image'){
return `<img class="msg-media-img" src="${esc(apiUrl)}" alt="${esc(ref.split('/').pop())}" loading="lazy">`;
}
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
// SVG → inline image (no download, render directly)
if(_SVG_EXTS.test(ref)){
return `<img class="msg-media-svg" src="${esc(apiUrl)}" alt="${t('media_svg_label')}" loading="lazy">`;
}
// Audio/video → inline player with speed controls; use &inline=1 for byte-range seeking
if(_AUDIO_EXTS.test(ref)||_VIDEO_EXTS.test(ref)){
const kind=_AUDIO_EXTS.test(ref)?'audio':'video';
return _mediaPlayerHtml(kind,apiUrl+'&inline=1',ref.split('/').pop()||ref);
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
}
// PDF files → render first page preview with lazy-load
if(_PDF_EXTS.test(ref)){
const fname=esc(ref.split('/').pop()||ref);
return `<div class="pdf-preview-load" data-path="${esc(ref)}"><span class="pdf-preview-spinner">⏳</span> ${t('pdf_loading')} ${fname}...</div>`;
}
// HTML files → render inline in sandboxed iframe with lazy-load
if(_HTML_EXTS.test(ref)){
return `<div class="html-preview-load" data-path="${esc(ref)}"><span class="html-preview-spinner">⏳</span> ${t('html_loading')}</div>`;
}
// .patch/.diff files → render inline as colored diff instead of download
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const fname=esc(ref.split('/').pop()||ref);
if(/\.(patch|diff)$/i.test(ref)){
return `<div class="diff-inline-load" data-path="${esc(ref)}">${t('diff_loading')} ${fname}...</div>`;
}
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
// CSV files → lazy-load and render as table
if(_CSV_EXTS.test(ref)){
return `<div class="csv-inline-load" data-path="${esc(ref)}">${t('csv_loading')} ${fname}...</div>`;
}
// Excalidraw files → lazy-load inline embed
if(_EXCALIDRAW_EXTS.test(ref)){
return `<div class="excalidraw-inline-load" data-path="${esc(ref)}">${t('excalidraw_loading')} ${fname}...</div>`;
}
return `<a class="msg-media-link" href="${esc(apiUrl+'&download=1')}" download="${fname}">📎 ${fname}</a>`;
});
// ── End MEDIA restore ──────────────────────────────────────────────────────
v0.50.218: chat bubble overflow, project color picker, blockquote renderer (#1085) * fix(css): add overflow-wrap:anywhere to chat bubbles — prevents long URL overflow (#1080) * fix(projects): rename now works via dblclick timer guard + right-click color picker (#1078) * fix(renderer): block-level constructs inside blockquotes now render Fenced code blocks, headings, horizontal rules, and ordered lists inside blockquotes now render correctly. Six related bugs documented in blockquote-rendering-bugs.md were collapsed into one architectural fix in renderMd(). Bugs fixed (all 6): 1. Fenced code blocks inside blockquotes -- > prefixes leaked into the <pre> body and the blockquote got fragmented around the rendered code, sometimes leaving raw <pre>/<div class="pre-header"> as visible text. 2. Blank > continuation lines fragmented multi-paragraph blockquotes into separate <blockquote> elements with literal > between them. 3. ## headings inside blockquotes rendered as literal "##" text. 4. Numbered lists inside blockquotes rendered as plain prose. 5. Complex blockquote (mixed headings + code + list + inline code) collapsed into a monospace blob with raw markdown syntax leaking everywhere. 6. Horizontal rules (---) inside blockquotes rendered as literal text. Root cause: The per-line passes for fenced code, headings, hr, ordered lists all ran BEFORE the blockquote handler and could not match lines that started with >, so by the time blockquote stripping ran those constructs had already been mishandled. Fix: A new blockquote pre-pass at the top of renderMd(): - Walks lines fence-aware so > -prefixed lines inside non-blockquote code fences (e.g. shell prompts in bash code blocks) are not miscaptured as a blockquote. - Groups consecutive > -prefixed lines, strips the > prefix, and recursively calls renderMd() on the stripped content. The recursive call handles all block-level constructs (fenced code, headings, hr, ordered/unordered lists, nested blockquotes) using the same pipeline. - Wraps the rendered HTML in <blockquote> and stashes it with a \x00Q token. Restored at the very end of renderMd() so no later pass can mangle the inner HTML. The old _applyBlockquotes regex-replace is removed entirely along with its limited inline branches for nested blockquotes and unordered lists. Behaviour change: Blockquotes now produce CommonMark-compliant <p> wrapping for text content (was: bare text directly inside <blockquote>). The visual output is the same in browsers but the HTML structure is now standard. Tests: - 14 new behavioural tests in tests/test_renderer_js_behaviour.py drive the actual renderMd() via node and lock all 6 bug fixes. - .local-review/test_blockquote_bugs.js -- node harness covering the same scenarios, runnable manually for fast iteration. - 2407/2408 tests pass (1 pre-existing macOS-only failure deselected). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(renderer): entity decode before blockquote pre-pass + CSS margin fix - Move the &gt;/&lt;/&amp; entity-decode to run at the very top of renderMd(), before the blockquote pre-pass. Previously decode() ran at line 756 (after the pre-pass at line 697), so LLM output containing &gt;-encoded blockquotes was never matched by the pre-pass. - Add .msg-body blockquote p{margin:0} and .preview-md blockquote p{margin:0} so the new CommonMark-compliant <p> wrapping inside blockquotes doesn't add extra vertical spacing. Prior shape (bare text) had no default p-margins. - Add Node-driven tests: TestBlockquoteEntityEncodedInput covers &gt; prefix and &gt;-encoded fenced code inside blockquotes. - Add struct test: TestBlockquotePrePassOrdering::test_entity_decode_runs_before_blockquote_pre_pass locks decode < _bq_stash ordering in ui.js. Fixes found during Opus independent review of #1083. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: v0.50.218 release notes, test count 2458, roadmap update --------- 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-26 06:08:59 +00:00
// Restore blockquote stash. Done last so the inner HTML (already produced
// by the recursive renderMd in the pre-pass) is dropped into the final
// string verbatim — no further passes can mangle it.
s=s.replace(/\x00Q(\d+)\x00/g,(_,i)=>_bq_stash[+i]);
return s;
}
function setStatus(t){
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(!t)return;
showToast(t, 4000);
}
function setComposerStatus(t){
const el=$('composerStatus');
if(!el)return;
if(!t){
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
el.style.display='none';
el.textContent='';
return;
}
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
el.textContent=t;
el.style.display='';
}
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
let _composerLockState=null;
function lockComposerForClarify(placeholderText){
const input=$('msg');
if(!input) return;
if(!_composerLockState){
_composerLockState={
disabled: input.disabled,
placeholder: input.placeholder,
};
}
input.disabled=true;
if(placeholderText) input.placeholder=placeholderText;
updateSendBtn();
}
function unlockComposerForClarify(){
const input=$('msg');
if(!input) return;
if(_composerLockState){
input.disabled=!!_composerLockState.disabled;
if(typeof _composerLockState.placeholder==='string'){
input.placeholder=_composerLockState.placeholder;
}
_composerLockState=null;
}else{
input.disabled=false;
}
updateSendBtn();
}
function _composerHasContent(){
const msg=$('msg');
return !!((msg&&msg.value.trim().length>0)||S.pendingFiles.length>0);
}
function _getExplicitBusyCommandAction(text){
const trimmed=(text||'').trim();
if(!trimmed.startsWith('/')) return null;
const body=trimmed.slice(1);
const name=(body.split(/\s+/)[0]||'').toLowerCase();
const args=body.slice(name.length).trim();
if(!args) return null;
if(name==='queue') return 'queue';
if(name==='steer'){
if(S.activeStreamId&&typeof _trySteer==='function') return 'steer';
return 'queue';
}
if(name==='interrupt'){
if(S.activeStreamId&&typeof cancelStream==='function') return 'interrupt';
return 'queue';
}
return null;
}
function getComposerPrimaryAction(){
const msg=$('msg');
const hasContent=_composerHasContent();
const locked=!!(msg&&msg.disabled);
if(locked) return 'disabled';
const compressionRunning=typeof isCompressionUiRunning==='function'&&isCompressionUiRunning();
const isBusy=!!S.busy||compressionRunning;
if(!isBusy) return hasContent?'send':'disabled';
if(!hasContent){
if(S.activeStreamId&&typeof cancelStream==='function') return 'stop';
return 'disabled';
}
const explicitAction=_getExplicitBusyCommandAction(msg&&msg.value);
if(explicitAction) return explicitAction;
const busyMode=window._busyInputMode||'queue';
if(busyMode==='steer'){
if(S.activeStreamId&&typeof _trySteer==='function') return 'steer';
return 'queue';
}
if(busyMode==='interrupt'){
if(S.activeStreamId&&typeof cancelStream==='function') return 'interrupt';
return 'queue';
}
return 'queue';
}
function _setComposerPrimaryButtonIcon(btn,action){
// Queue/interrupt/steer icons are inline Lucide SVGs (ISC):
// https://lucide.dev/icons/
const icons={
send:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>',
queue:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 5H3"/><path d="M16 12H3"/><path d="M9 19H3"/><path d="m16 16-3 3 3 3"/><path d="M21 5v12a2 2 0 0 1-2 2h-6"/></svg>',
interrupt:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 4v16"/><path d="M6.029 4.285A2 2 0 0 0 3 6v12a2 2 0 0 0 3.029 1.715l9.997-5.998a2 2 0 0 0 .003-3.432z"/></svg>',
steer:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z"/></svg>',
stop:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="5" width="14" height="14" rx="2"></rect></svg>',
disabled:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>'
};
const next=icons[action]||icons.send;
if(btn.innerHTML!==next) btn.innerHTML=next;
}
function updateSendBtn(){
const btn=$('btnSend');
if(!btn) return;
const action=getComposerPrimaryAction();
btn.dataset.action=action;
btn.classList.toggle('stop',action==='stop');
btn.classList.toggle('queue',action==='queue');
btn.classList.toggle('interrupt',action==='interrupt');
btn.classList.toggle('steer',action==='steer');
const _tt=(key,fb)=>{if(typeof t!=='function')return fb;const val=t(key);return val===key?fb:(val||fb);};
let _btnTitle;
if(action==='disabled'){
const _dmsg=$('msg');
const _dcompr=typeof isCompressionUiRunning==='function'&&isCompressionUiRunning();
if(_dmsg&&_dmsg.disabled) _btnTitle=_tt('composer_disabled_clarify','Respond to the clarification request');
else if(_dcompr) _btnTitle=_tt('composer_disabled_compression','Waiting for compression to finish');
else _btnTitle=_tt('composer_disabled_empty','Type a message to send');
}else{
const _tmap={send:'Send message',queue:'Queue message',interrupt:'Interrupt and send',steer:'Steer current response',stop:'Stop generation'};
_btnTitle=_tt('composer_'+action,_tmap[action]||'Send message');
}
btn.title=_btnTitle;
btn.setAttribute('aria-label',_btnTitle);
_setComposerPrimaryButtonIcon(btn,action);
// Single primary action button: while busy/no-draft it becomes the red Stop
// action; while busy with a draft it reflects queue/interrupt/steer.
btn.style.display='';
btn.disabled=action==='disabled';
if(action!=='disabled'&&!btn.classList.contains('visible')){
btn.classList.remove('visible');
requestAnimationFrame(()=>btn.classList.add('visible'));
} else if(action==='disabled'){
btn.classList.remove('visible');
}
}
async function handleComposerPrimaryAction(){
if(window._micActive){
window._micPendingSend=true;
_stopMic();
return;
}
const action=typeof getComposerPrimaryAction==='function'?getComposerPrimaryAction():'send';
if(action==='disabled') return;
if(action==='stop'){
if(typeof cancelStream==='function') await cancelStream();
return;
}
await send();
}
function setBusy(v){
S.busy=v;
updateSendBtn();
if(!v){
setStatus('');
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
setComposerStatus('');
const sid=_queueDrainSid||(S.session&&S.session.session_id);
_queueDrainSid=null;
updateQueueBadge(sid);
// Drain one queued message for the finished session after UI settles
const _isViewedSid=!S.session||sid===S.session.session_id;
const next=sid&&_isViewedSid?shiftQueuedSessionMessage(sid):null;
if(next){
updateQueueBadge(sid);
setTimeout(()=>{
$('msg').value=next.text||'';
S.pendingFiles=Array.isArray(next.files)?[...next.files]:[];
feat(queue): Codex-style message queue flyout above composer (#1040) * chore: apply pending #965 queue flyout patches on local master Queue flyout implementation (PR #965 — pending merge) applied on top of upstream v0.50.205. Features: - Queue card slides up from behind composer (approval-card pattern) - Lucide icons via li(), CSS class system, no inline SVG dumps - Drag-to-reorder by _queued_at timestamp (survives re-renders) - Inline contenteditable edit with focus guard and blur-commit - Combine preserves first item files, merge immediate (no 200ms race) - Files/model compact badges per item - Hide/expand via header chevron + composer pill + titlebar chip - All 3 expand paths sync correctly - border-bottom CSS order fixed, fingerprint improved, _dragTs guards CF CSP domains also applied (deployment-specific, not in upstream PR). * fix(queue): harden merge closure, toggleQueue sid, and drain flash - mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q - toggleQueue reads activeSid from S.session at call time, not captured param - updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears * style(queue): contain:paint on inner, pill fade-in animation * feat(queue): pill outside composer, compact collapsed state matching card width - Move #queuePill out of .composer-box to between .composer-flyout and .composer-box - Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom) - Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered - Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer hides pill when card is manually collapsed) - Remove all queue-active/queue-pill-active composer modifications — composer untouched - Fix: mergeBtn reads live queue not stale closure - Fix: toggleQueue uses S.session.session_id at call time not captured param - Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash * fix(queue): collapsed state persists + cross-session DOM isolation - Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain - _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed - updateQueueBadge else-branch: DOM mutations now gated on sid===active session - _syncQueueTitlebar only fires for active session in else-branch - Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption * fix(queue): proper pill wrapper matching queue-card structure - Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px) identical to .queue-card outer — positions pill button at exact card-inner width - .queue-pill button fills slot with width:100% - Removes hardcoded 740px — width is derived correctly from the same CSS variables the card uses, scales with --msg-max across all viewports - JS toggles .show on pillOuter (parentElement), not on pill button directly --------- Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
// Restore model from queued item (sent in /api/chat/start payload)
// Note: profile is NOT restored — full profile switch requires server interaction
if(next.model&&S.session&&next.model!==S.session.model){
S.session.model=next.model;
}
if(next.model_provider&&S.session) S.session.model_provider=next.model_provider;
if(next.model&&S.session){
if(typeof _applyModelToDropdown==='function'&&$('modelSelect')) _applyModelToDropdown(next.model,$('modelSelect'),S.session.model_provider||null);
feat(queue): Codex-style message queue flyout above composer (#1040) * chore: apply pending #965 queue flyout patches on local master Queue flyout implementation (PR #965 — pending merge) applied on top of upstream v0.50.205. Features: - Queue card slides up from behind composer (approval-card pattern) - Lucide icons via li(), CSS class system, no inline SVG dumps - Drag-to-reorder by _queued_at timestamp (survives re-renders) - Inline contenteditable edit with focus guard and blur-commit - Combine preserves first item files, merge immediate (no 200ms race) - Files/model compact badges per item - Hide/expand via header chevron + composer pill + titlebar chip - All 3 expand paths sync correctly - border-bottom CSS order fixed, fingerprint improved, _dragTs guards CF CSP domains also applied (deployment-specific, not in upstream PR). * fix(queue): harden merge closure, toggleQueue sid, and drain flash - mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q - toggleQueue reads activeSid from S.session at call time, not captured param - updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears * style(queue): contain:paint on inner, pill fade-in animation * feat(queue): pill outside composer, compact collapsed state matching card width - Move #queuePill out of .composer-box to between .composer-flyout and .composer-box - Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom) - Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered - Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer hides pill when card is manually collapsed) - Remove all queue-active/queue-pill-active composer modifications — composer untouched - Fix: mergeBtn reads live queue not stale closure - Fix: toggleQueue uses S.session.session_id at call time not captured param - Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash * fix(queue): collapsed state persists + cross-session DOM isolation - Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain - _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed - updateQueueBadge else-branch: DOM mutations now gated on sid===active session - _syncQueueTitlebar only fires for active session in else-branch - Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption * fix(queue): proper pill wrapper matching queue-card structure - Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px) identical to .queue-card outer — positions pill button at exact card-inner width - .queue-pill button fills slot with width:100% - Removes hardcoded 740px — width is derived correctly from the same CSS variables the card uses, scales with --msg-max across all viewports - JS toggles .show on pillOuter (parentElement), not on pill button directly --------- Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
if(typeof syncModelChip==='function') syncModelChip();
}
autoResize();
renderTray();
send();
},120);
}
}
}
feat(queue): Codex-style message queue flyout above composer (#1040) * chore: apply pending #965 queue flyout patches on local master Queue flyout implementation (PR #965 — pending merge) applied on top of upstream v0.50.205. Features: - Queue card slides up from behind composer (approval-card pattern) - Lucide icons via li(), CSS class system, no inline SVG dumps - Drag-to-reorder by _queued_at timestamp (survives re-renders) - Inline contenteditable edit with focus guard and blur-commit - Combine preserves first item files, merge immediate (no 200ms race) - Files/model compact badges per item - Hide/expand via header chevron + composer pill + titlebar chip - All 3 expand paths sync correctly - border-bottom CSS order fixed, fingerprint improved, _dragTs guards CF CSP domains also applied (deployment-specific, not in upstream PR). * fix(queue): harden merge closure, toggleQueue sid, and drain flash - mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q - toggleQueue reads activeSid from S.session at call time, not captured param - updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears * style(queue): contain:paint on inner, pill fade-in animation * feat(queue): pill outside composer, compact collapsed state matching card width - Move #queuePill out of .composer-box to between .composer-flyout and .composer-box - Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom) - Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered - Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer hides pill when card is manually collapsed) - Remove all queue-active/queue-pill-active composer modifications — composer untouched - Fix: mergeBtn reads live queue not stale closure - Fix: toggleQueue uses S.session.session_id at call time not captured param - Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash * fix(queue): collapsed state persists + cross-session DOM isolation - Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain - _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed - updateQueueBadge else-branch: DOM mutations now gated on sid===active session - _syncQueueTitlebar only fires for active session in else-branch - Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption * fix(queue): proper pill wrapper matching queue-card structure - Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px) identical to .queue-card outer — positions pill button at exact card-inner width - .queue-pill button fills slot with width:100% - Removes hardcoded 740px — width is derived correctly from the same CSS variables the card uses, scales with --msg-max across all viewports - JS toggles .show on pillOuter (parentElement), not on pill button directly --------- Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
// ── Queue chip display (Codex Desktop pattern) ─────────────────────────────
// Queued messages appear as chips inside #queueChips (above the textarea)
// while pending. When the session fires the queued message it becomes a
// normal user bubble in the chat — the chip is removed at drain time.
const _queueRenderKeys={}; // per-session fingerprint to avoid redundant rebuilds
const _queueCollapsed={}; // per-session: true when user explicitly collapsed the card
function _renderQueueChips(sid){
const card=document.getElementById('queueCard');
const inner=document.getElementById('queueChips');
if(!card||!inner) return;
const q=_getSessionQueue(sid,false);
const key=q.map(e=>{const t=e&&(e.text||e.message||e.content||'');return(e&&e._queued_at||0)+':'+t.length+':'+t.slice(0,20);}).join('|');
if(key===(_queueRenderKeys[sid]||'')&&key!='') return;
// Skip re-render if user is actively editing inside the queue panel
if(inner.contains(document.activeElement)&&document.activeElement!==inner) return;
_queueRenderKeys[sid]=key;
inner.innerHTML='';
if(!q.length){
card.classList.remove('visible');
const _msgs=document.getElementById('messages');
if(_msgs) _msgs.classList.remove('queue-open');
return;
}
// Respect user-collapsed state — don't reopen if user explicitly hid the card
if(_queueCollapsed[sid]){
// Update chips content without showing card (so data is fresh if user re-expands)
inner.innerHTML='';
// fall through to render rows into inner but skip making card visible
} else {
card.classList.add('visible');
}
// Push messages area up so content isn't hidden behind the flyout
const _msgs=document.getElementById('messages');
if(_msgs&&!_queueCollapsed[sid]){
_msgs.classList.add('queue-open');
// Measure after 350ms transition completes (not mid-animation — height would be wrong)
setTimeout(()=>{
if(!card.classList.contains('visible')) return;
const h=card.getBoundingClientRect().height;
if(h>0) _msgs.style.setProperty('--queue-card-height', h+'px');
if(S.activeStreamId&&typeof scrollIfPinned==='function') scrollIfPinned();
else if(!S.activeStreamId&&typeof scrollToBottom==='function') scrollToBottom();
feat(queue): Codex-style message queue flyout above composer (#1040) * chore: apply pending #965 queue flyout patches on local master Queue flyout implementation (PR #965 — pending merge) applied on top of upstream v0.50.205. Features: - Queue card slides up from behind composer (approval-card pattern) - Lucide icons via li(), CSS class system, no inline SVG dumps - Drag-to-reorder by _queued_at timestamp (survives re-renders) - Inline contenteditable edit with focus guard and blur-commit - Combine preserves first item files, merge immediate (no 200ms race) - Files/model compact badges per item - Hide/expand via header chevron + composer pill + titlebar chip - All 3 expand paths sync correctly - border-bottom CSS order fixed, fingerprint improved, _dragTs guards CF CSP domains also applied (deployment-specific, not in upstream PR). * fix(queue): harden merge closure, toggleQueue sid, and drain flash - mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q - toggleQueue reads activeSid from S.session at call time, not captured param - updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears * style(queue): contain:paint on inner, pill fade-in animation * feat(queue): pill outside composer, compact collapsed state matching card width - Move #queuePill out of .composer-box to between .composer-flyout and .composer-box - Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom) - Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered - Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer hides pill when card is manually collapsed) - Remove all queue-active/queue-pill-active composer modifications — composer untouched - Fix: mergeBtn reads live queue not stale closure - Fix: toggleQueue uses S.session.session_id at call time not captured param - Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash * fix(queue): collapsed state persists + cross-session DOM isolation - Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain - _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed - updateQueueBadge else-branch: DOM mutations now gated on sid===active session - _syncQueueTitlebar only fires for active session in else-branch - Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption * fix(queue): proper pill wrapper matching queue-card structure - Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px) identical to .queue-card outer — positions pill button at exact card-inner width - .queue-pill button fills slot with width:100% - Removes hardcoded 740px — width is derived correctly from the same CSS variables the card uses, scales with --msg-max across all viewports - JS toggles .show on pillOuter (parentElement), not on pill button directly --------- Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
}, 360);
}
function _saveAndRefresh(){
const liveQ=_getSessionQueue(sid,false);
if(!liveQ.length){delete SESSION_QUEUES[sid];try{sessionStorage.removeItem('hermes-queue-'+sid);}catch(_){}}
else{SESSION_QUEUES[sid]=[...liveQ];try{sessionStorage.setItem('hermes-queue-'+sid,JSON.stringify(liveQ));}catch(_){}}
delete _queueRenderKeys[sid];
updateQueueBadge(sid);
}
// Header (2+ items)
if(q.length>1){
const header=document.createElement('div');
header.className='queue-card-header';
const lbl=document.createElement('span');
lbl.textContent=typeof t==='function'?t('queued_count',q.length):(q.length===1?'1 queued':`${q.length} queued`);
lbl.title='Sends automatically after the current response completes';
const actions=document.createElement('span');
actions.className='queue-card-header-actions';
const hasFiles=q.some(e=>e&&Array.isArray(e.files)&&e.files.length>0);
const mergeBtn=document.createElement('button');
mergeBtn.className='queue-card-btn';
mergeBtn.title='Combine all into one message'+(hasFiles?' — attachments will be removed':'');
mergeBtn.innerHTML=li('layers',12)+'Combine';
mergeBtn.onclick=()=>{
const _doMerge=(snapshot)=>{
const combined=snapshot.map(e=>e&&(e.text||e.message||e.content||'')).filter(Boolean).join('\n\n');
const liveQ=_getSessionQueue(sid,false);
const first=snapshot.find(e=>e)||{};
feat(queue): Codex-style message queue flyout above composer (#1040) * chore: apply pending #965 queue flyout patches on local master Queue flyout implementation (PR #965 — pending merge) applied on top of upstream v0.50.205. Features: - Queue card slides up from behind composer (approval-card pattern) - Lucide icons via li(), CSS class system, no inline SVG dumps - Drag-to-reorder by _queued_at timestamp (survives re-renders) - Inline contenteditable edit with focus guard and blur-commit - Combine preserves first item files, merge immediate (no 200ms race) - Files/model compact badges per item - Hide/expand via header chevron + composer pill + titlebar chip - All 3 expand paths sync correctly - border-bottom CSS order fixed, fingerprint improved, _dragTs guards CF CSP domains also applied (deployment-specific, not in upstream PR). * fix(queue): harden merge closure, toggleQueue sid, and drain flash - mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q - toggleQueue reads activeSid from S.session at call time, not captured param - updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears * style(queue): contain:paint on inner, pill fade-in animation * feat(queue): pill outside composer, compact collapsed state matching card width - Move #queuePill out of .composer-box to between .composer-flyout and .composer-box - Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom) - Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered - Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer hides pill when card is manually collapsed) - Remove all queue-active/queue-pill-active composer modifications — composer untouched - Fix: mergeBtn reads live queue not stale closure - Fix: toggleQueue uses S.session.session_id at call time not captured param - Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash * fix(queue): collapsed state persists + cross-session DOM isolation - Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain - _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed - updateQueueBadge else-branch: DOM mutations now gated on sid===active session - _syncQueueTitlebar only fires for active session in else-branch - Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption * fix(queue): proper pill wrapper matching queue-card structure - Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px) identical to .queue-card outer — positions pill button at exact card-inner width - .queue-pill button fills slot with width:100% - Removes hardcoded 740px — width is derived correctly from the same CSS variables the card uses, scales with --msg-max across all viewports - JS toggles .show on pillOuter (parentElement), not on pill button directly --------- Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
const firstFiles=(snapshot.find(e=>e&&Array.isArray(e.files)&&e.files.length)||{files:[]}).files;
liveQ.length=0;liveQ.push({text:combined,files:firstFiles,model:first.model||'',model_provider:first.model_provider||null,_queued_at:Date.now()});
feat(queue): Codex-style message queue flyout above composer (#1040) * chore: apply pending #965 queue flyout patches on local master Queue flyout implementation (PR #965 — pending merge) applied on top of upstream v0.50.205. Features: - Queue card slides up from behind composer (approval-card pattern) - Lucide icons via li(), CSS class system, no inline SVG dumps - Drag-to-reorder by _queued_at timestamp (survives re-renders) - Inline contenteditable edit with focus guard and blur-commit - Combine preserves first item files, merge immediate (no 200ms race) - Files/model compact badges per item - Hide/expand via header chevron + composer pill + titlebar chip - All 3 expand paths sync correctly - border-bottom CSS order fixed, fingerprint improved, _dragTs guards CF CSP domains also applied (deployment-specific, not in upstream PR). * fix(queue): harden merge closure, toggleQueue sid, and drain flash - mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q - toggleQueue reads activeSid from S.session at call time, not captured param - updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears * style(queue): contain:paint on inner, pill fade-in animation * feat(queue): pill outside composer, compact collapsed state matching card width - Move #queuePill out of .composer-box to between .composer-flyout and .composer-box - Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom) - Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered - Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer hides pill when card is manually collapsed) - Remove all queue-active/queue-pill-active composer modifications — composer untouched - Fix: mergeBtn reads live queue not stale closure - Fix: toggleQueue uses S.session.session_id at call time not captured param - Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash * fix(queue): collapsed state persists + cross-session DOM isolation - Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain - _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed - updateQueueBadge else-branch: DOM mutations now gated on sid===active session - _syncQueueTitlebar only fires for active session in else-branch - Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption * fix(queue): proper pill wrapper matching queue-card structure - Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px) identical to .queue-card outer — positions pill button at exact card-inner width - .queue-pill button fills slot with width:100% - Removes hardcoded 740px — width is derived correctly from the same CSS variables the card uses, scales with --msg-max across all viewports - JS toggles .show on pillOuter (parentElement), not on pill button directly --------- Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
SESSION_QUEUES[sid]=liveQ;
try{sessionStorage.setItem('hermes-queue-'+sid,JSON.stringify(liveQ));}catch(_){}
delete _queueRenderKeys[sid];
updateQueueBadge(sid);
};
if(hasFiles){
if(typeof showToast==='function') showToast('Attachments on queued items will be removed',2600,'warning');
}
// Merge from current live queue (no delay — snapshot + defer caused data-loss races)
_doMerge([..._getSessionQueue(sid,false)]);
};
const clearBtn=document.createElement('button');
clearBtn.className='queue-card-icon-btn';
clearBtn.title='Clear all queued messages';
clearBtn.setAttribute('aria-label','Clear all queued messages');
clearBtn.innerHTML=li('x',13);
clearBtn.onclick=()=>{q.length=0;_saveAndRefresh();};
actions.appendChild(mergeBtn);
actions.appendChild(clearBtn);
// Hide button — collapses flyout entirely; queue pill re-shows it
feat(queue): Codex-style message queue flyout above composer (#1040) * chore: apply pending #965 queue flyout patches on local master Queue flyout implementation (PR #965 — pending merge) applied on top of upstream v0.50.205. Features: - Queue card slides up from behind composer (approval-card pattern) - Lucide icons via li(), CSS class system, no inline SVG dumps - Drag-to-reorder by _queued_at timestamp (survives re-renders) - Inline contenteditable edit with focus guard and blur-commit - Combine preserves first item files, merge immediate (no 200ms race) - Files/model compact badges per item - Hide/expand via header chevron + composer pill + titlebar chip - All 3 expand paths sync correctly - border-bottom CSS order fixed, fingerprint improved, _dragTs guards CF CSP domains also applied (deployment-specific, not in upstream PR). * fix(queue): harden merge closure, toggleQueue sid, and drain flash - mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q - toggleQueue reads activeSid from S.session at call time, not captured param - updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears * style(queue): contain:paint on inner, pill fade-in animation * feat(queue): pill outside composer, compact collapsed state matching card width - Move #queuePill out of .composer-box to between .composer-flyout and .composer-box - Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom) - Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered - Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer hides pill when card is manually collapsed) - Remove all queue-active/queue-pill-active composer modifications — composer untouched - Fix: mergeBtn reads live queue not stale closure - Fix: toggleQueue uses S.session.session_id at call time not captured param - Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash * fix(queue): collapsed state persists + cross-session DOM isolation - Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain - _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed - updateQueueBadge else-branch: DOM mutations now gated on sid===active session - _syncQueueTitlebar only fires for active session in else-branch - Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption * fix(queue): proper pill wrapper matching queue-card structure - Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px) identical to .queue-card outer — positions pill button at exact card-inner width - .queue-pill button fills slot with width:100% - Removes hardcoded 740px — width is derived correctly from the same CSS variables the card uses, scales with --msg-max across all viewports - JS toggles .show on pillOuter (parentElement), not on pill button directly --------- Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
const hideBtn=document.createElement('button');
hideBtn.className='queue-card-icon-btn';
hideBtn.title='Hide queue (click the queue pill to show again)';
feat(queue): Codex-style message queue flyout above composer (#1040) * chore: apply pending #965 queue flyout patches on local master Queue flyout implementation (PR #965 — pending merge) applied on top of upstream v0.50.205. Features: - Queue card slides up from behind composer (approval-card pattern) - Lucide icons via li(), CSS class system, no inline SVG dumps - Drag-to-reorder by _queued_at timestamp (survives re-renders) - Inline contenteditable edit with focus guard and blur-commit - Combine preserves first item files, merge immediate (no 200ms race) - Files/model compact badges per item - Hide/expand via header chevron + composer pill + titlebar chip - All 3 expand paths sync correctly - border-bottom CSS order fixed, fingerprint improved, _dragTs guards CF CSP domains also applied (deployment-specific, not in upstream PR). * fix(queue): harden merge closure, toggleQueue sid, and drain flash - mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q - toggleQueue reads activeSid from S.session at call time, not captured param - updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears * style(queue): contain:paint on inner, pill fade-in animation * feat(queue): pill outside composer, compact collapsed state matching card width - Move #queuePill out of .composer-box to between .composer-flyout and .composer-box - Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom) - Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered - Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer hides pill when card is manually collapsed) - Remove all queue-active/queue-pill-active composer modifications — composer untouched - Fix: mergeBtn reads live queue not stale closure - Fix: toggleQueue uses S.session.session_id at call time not captured param - Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash * fix(queue): collapsed state persists + cross-session DOM isolation - Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain - _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed - updateQueueBadge else-branch: DOM mutations now gated on sid===active session - _syncQueueTitlebar only fires for active session in else-branch - Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption * fix(queue): proper pill wrapper matching queue-card structure - Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px) identical to .queue-card outer — positions pill button at exact card-inner width - .queue-pill button fills slot with width:100% - Removes hardcoded 740px — width is derived correctly from the same CSS variables the card uses, scales with --msg-max across all viewports - JS toggles .show on pillOuter (parentElement), not on pill button directly --------- Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
hideBtn.setAttribute('aria-label','Hide queue panel');
hideBtn.innerHTML=li('chevron-down',14);
hideBtn.onclick=()=>{
_queueCollapsed[sid]=true;
card.classList.remove('visible');
// Read live count at click time (not stale closure q)
_updateQueuePill(sid,_getSessionQueue(sid,false).length);
};
actions.appendChild(hideBtn);
header.appendChild(lbl);
header.appendChild(actions);
inner.appendChild(header);
}
let _dragTs=null; // use _queued_at timestamp — survives re-renders, not an index
q.forEach((entry,i)=>{
const _entryTs=entry&&entry._queued_at;
const entryText=entry&&(entry.text||entry.message||entry.content||'');
const _files=entry&&Array.isArray(entry.files)?entry.files.filter(Boolean):[];
const row=document.createElement('div');
row.className='queue-card-row';
row.setAttribute('role','listitem');
row.setAttribute('draggable','true');
row.ondragstart=(e)=>{if(_entryTs==null) return;_dragTs=_entryTs;row.style.opacity='.4';e.dataTransfer.effectAllowed='move';};
row.ondragend=()=>{row.style.opacity='';};
row.ondragover=(e)=>{e.preventDefault();row.style.background='var(--hover-bg)';};
row.ondragleave=()=>{row.style.background='';};
row.ondrop=(e)=>{
e.preventDefault();row.style.background='';
if(_dragTs!=null&&_dragTs!==_entryTs){
const fromIdx=q.findIndex(e=>e&&e._queued_at===_dragTs);
if(fromIdx!==-1&&fromIdx!==i){const moved=q.splice(fromIdx,1)[0];q.splice(i,0,moved);}
_dragTs=null;_saveAndRefresh();
}
};
// Drag handle
const drag=document.createElement('span');
drag.className='queue-card-drag';
drag.setAttribute('aria-hidden','true');
drag.innerHTML=typeof li==='function'?li('list-todo',13):'≡';
// Inline-editable text
const msgSpan=document.createElement('span');
msgSpan.className='queue-card-text';
msgSpan.setAttribute('contenteditable','true');
msgSpan.setAttribute('role','textbox');
msgSpan.setAttribute('aria-label','Queued message — edit in place');
msgSpan.textContent=entryText||(_files.length?'':'—');
msgSpan.setAttribute('draggable','false');
msgSpan.onfocus=()=>{msgSpan.style.overflow='auto';msgSpan.style.whiteSpace='pre-wrap';msgSpan.style.textOverflow='clip';};
msgSpan.onblur=()=>{
msgSpan.style.overflow='';msgSpan.style.whiteSpace='';msgSpan.style.textOverflow='';
const newText=msgSpan.textContent.trim();
if(newText===''&&!_files.length){ msgSpan.textContent=entryText||'—'; return; }
if(newText!==entryText){
const liveQ=_getSessionQueue(sid,false);
const idx=_entryTs!=null?liveQ.findIndex(e=>e&&e._queued_at===_entryTs):i;
if(idx!==-1){
liveQ[idx]={...liveQ[idx],text:newText};
try{sessionStorage.setItem('hermes-queue-'+sid,JSON.stringify(liveQ));}catch(_){}
delete _queueRenderKeys[sid];
updateQueueBadge(sid);
}
}
};
msgSpan.onkeydown=(e)=>{if(e.key==='Enter'){e.preventDefault();msgSpan.blur();}if(e.key==='Escape'){msgSpan.textContent=entryText||'—';msgSpan.blur();}};
// Compact badges (files, model, profile)
const badges=document.createElement('span');
badges.className='queue-card-badges';
if(_files.length>0){
const fb=document.createElement('span');
fb.className='queue-card-file-badge';
fb.title=_files.map(f=>f&&f.name||'file').join(', ');
fb.innerHTML=li('paperclip',11)+_files.length;
badges.appendChild(fb);
}
const _model=entry&&entry.model;
if(_model){
const mb=document.createElement('span');
mb.title='Model: '+_model;
// Use the app's friendly label system if available
const _modelLabel=(typeof _dynamicModelLabels!=='undefined'&&_dynamicModelLabels[_model])
||_model.split('/').pop().replace(/^(gpt-|claude-3\.?5?-|claude-|gemini-)/,'').replace(/-\d{4}-\d{2}-\d{2}$/,'').slice(0,12);
mb.textContent=_modelLabel;
badges.appendChild(mb);
}
// Profile badge removed — drain cannot server-switch profiles so badge was misleading
// Delete button
const delBtn=document.createElement('button');
delBtn.className='queue-card-icon-btn';
delBtn.setAttribute('aria-label',typeof t==='function'?t('queued_cancel'):'Remove queued message');
delBtn.setAttribute('draggable','false');
delBtn.title='Remove from queue';
delBtn.innerHTML=li('x',13);
delBtn.onclick=()=>{
const liveQ=_getSessionQueue(sid,false);
const idx=_entryTs!=null?liveQ.findIndex(e=>e&&e._queued_at===_entryTs):i;
if(idx!==-1) liveQ.splice(idx,1);
if(!liveQ.length){delete SESSION_QUEUES[sid];try{sessionStorage.removeItem('hermes-queue-'+sid);}catch(_){}}
else{SESSION_QUEUES[sid]=[...liveQ];try{sessionStorage.setItem('hermes-queue-'+sid,JSON.stringify(liveQ));}catch(_){}}
delete _queueRenderKeys[sid];
updateQueueBadge(sid);
};
row.appendChild(drag);
row.appendChild(msgSpan);
if(badges.childNodes.length) row.appendChild(badges);
row.appendChild(delBtn);
inner.appendChild(row);
});
}
function _updateQueuePill(sid,count){
const pill=document.getElementById('queuePill');
if(!pill) return;
const pillOuter=pill.parentElement; // .queue-pill-outer — same wrapper as .queue-card
const card=document.getElementById('queueCard');
const flyoutVisible=card&&card.classList.contains('visible');
if(count>0&&!flyoutVisible){
const label=typeof t==='function'?t('queued_count',count):(count===1?'1 queued':`${count} queued`);
pill.innerHTML=(typeof li==='function'?li('list-todo',12):'')+
`<span class="queue-pill-count">${label}</span>`+
`<span class="queue-pill-chevron">`+(typeof li==='function'?li('chevron-up',12):'▲')+`</span>`;
pill.title='Show queued messages';
if(pillOuter) pillOuter.classList.add('show');
pill.onclick=()=>{
delete _queueCollapsed[sid];
const c=document.getElementById('queueCard');
if(c){
c.classList.add('visible');
setTimeout(()=>{
const firstFocusable=c.querySelector('.queue-card-text, .queue-card-icon-btn');
if(firstFocusable) firstFocusable.focus();
}, 360);
}
if(pillOuter) pillOuter.classList.remove('show');
if(S.activeStreamId&&typeof scrollIfPinned==='function') scrollIfPinned();
else if(!S.activeStreamId&&typeof scrollToBottom==='function') scrollToBottom();
feat(queue): Codex-style message queue flyout above composer (#1040) * chore: apply pending #965 queue flyout patches on local master Queue flyout implementation (PR #965 — pending merge) applied on top of upstream v0.50.205. Features: - Queue card slides up from behind composer (approval-card pattern) - Lucide icons via li(), CSS class system, no inline SVG dumps - Drag-to-reorder by _queued_at timestamp (survives re-renders) - Inline contenteditable edit with focus guard and blur-commit - Combine preserves first item files, merge immediate (no 200ms race) - Files/model compact badges per item - Hide/expand via header chevron + composer pill + titlebar chip - All 3 expand paths sync correctly - border-bottom CSS order fixed, fingerprint improved, _dragTs guards CF CSP domains also applied (deployment-specific, not in upstream PR). * fix(queue): harden merge closure, toggleQueue sid, and drain flash - mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q - toggleQueue reads activeSid from S.session at call time, not captured param - updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears * style(queue): contain:paint on inner, pill fade-in animation * feat(queue): pill outside composer, compact collapsed state matching card width - Move #queuePill out of .composer-box to between .composer-flyout and .composer-box - Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom) - Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered - Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer hides pill when card is manually collapsed) - Remove all queue-active/queue-pill-active composer modifications — composer untouched - Fix: mergeBtn reads live queue not stale closure - Fix: toggleQueue uses S.session.session_id at call time not captured param - Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash * fix(queue): collapsed state persists + cross-session DOM isolation - Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain - _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed - updateQueueBadge else-branch: DOM mutations now gated on sid===active session - _syncQueueTitlebar only fires for active session in else-branch - Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption * fix(queue): proper pill wrapper matching queue-card structure - Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px) identical to .queue-card outer — positions pill button at exact card-inner width - .queue-pill button fills slot with width:100% - Removes hardcoded 740px — width is derived correctly from the same CSS variables the card uses, scales with --msg-max across all viewports - JS toggles .show on pillOuter (parentElement), not on pill button directly --------- Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
};
} else {
if(pillOuter) pillOuter.classList.remove('show');
pill.onclick=null;
}
}
function updateQueueBadge(sessionId){
const sid=sessionId||(S.session&&S.session.session_id);
const count=sid?getQueuedSessionCount(sid):0;
feat(queue): Codex-style message queue flyout above composer (#1040) * chore: apply pending #965 queue flyout patches on local master Queue flyout implementation (PR #965 — pending merge) applied on top of upstream v0.50.205. Features: - Queue card slides up from behind composer (approval-card pattern) - Lucide icons via li(), CSS class system, no inline SVG dumps - Drag-to-reorder by _queued_at timestamp (survives re-renders) - Inline contenteditable edit with focus guard and blur-commit - Combine preserves first item files, merge immediate (no 200ms race) - Files/model compact badges per item - Hide/expand via header chevron + composer pill + titlebar chip - All 3 expand paths sync correctly - border-bottom CSS order fixed, fingerprint improved, _dragTs guards CF CSP domains also applied (deployment-specific, not in upstream PR). * fix(queue): harden merge closure, toggleQueue sid, and drain flash - mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q - toggleQueue reads activeSid from S.session at call time, not captured param - updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears * style(queue): contain:paint on inner, pill fade-in animation * feat(queue): pill outside composer, compact collapsed state matching card width - Move #queuePill out of .composer-box to between .composer-flyout and .composer-box - Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom) - Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered - Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer hides pill when card is manually collapsed) - Remove all queue-active/queue-pill-active composer modifications — composer untouched - Fix: mergeBtn reads live queue not stale closure - Fix: toggleQueue uses S.session.session_id at call time not captured param - Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash * fix(queue): collapsed state persists + cross-session DOM isolation - Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain - _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed - updateQueueBadge else-branch: DOM mutations now gated on sid===active session - _syncQueueTitlebar only fires for active session in else-branch - Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption * fix(queue): proper pill wrapper matching queue-card structure - Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px) identical to .queue-card outer — positions pill button at exact card-inner width - .queue-pill button fills slot with width:100% - Removes hardcoded 740px — width is derived correctly from the same CSS variables the card uses, scales with --msg-max across all viewports - JS toggles .show on pillOuter (parentElement), not on pill button directly --------- Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com>
2026-04-25 21:21:50 +00:00
if(count>0&&S.session&&sid===S.session.session_id){
_renderQueueChips(sid);
// If card is visible, hide pill. If card is collapsed, update pill count.
const _cardEl=document.getElementById('queueCard');
_updateQueuePill(sid,(_cardEl&&_cardEl.classList.contains('visible'))?0:count);
} else {
// Always clean up per-session data
if(sid){delete _queueRenderKeys[sid];delete _queueCollapsed[sid];}
// Only wipe global DOM if this is the currently active session
const isActive=S.session&&sid===S.session.session_id;
if(isActive){
const card=document.getElementById('queueCard');
const chips=document.getElementById('queueChips');
if(card) card.classList.remove('visible');
// Defer clear until after slide-out transition so content doesn't vanish mid-animation
if(chips){const _chips=chips;const _card=card;setTimeout(()=>{if(!_card||!_card.classList.contains('visible'))_chips.innerHTML='';},360);}
const _msgsEl=document.getElementById('messages');
if(_msgsEl) _msgsEl.classList.remove('queue-open');
_updateQueuePill(sid,0);
}
}
}
refactor(ui): three-column layout with left rail + main-view migration (#899) refactor(ui): three-column layout with left rail + main-view migration (#899) Unifies the shell into a three-column layout (rail + sidebar + main) matching the hermes-desktop reference, and migrates every per-item detail/edit surface into a shared main-view canvas with consistent headers, empty states, and action buttons. Changes: - New desktop-only left rail (48px) with 8 nav tabs (chat/tasks/skills/memory/workspaces/profiles/todos/settings) - Persistent app titlebar (replaces per-chat topbar), active conversation title shown - All panel detail/create/edit views migrated to #mainSkills, #mainTasks, #mainSettings, #mainWorkspaces, #mainProfiles, #mainMemory - Settings moved out of modal into main-view page; ESC closes it - YAML frontmatter rendered in collapsible <details> block in skill detail - Toasts repositioned from bottom-center to top-right with theme-aware success/error/warning/info variants - Composer workspace chip split into two-button group: files-icon toggles file panel, label opens workspace picker - .settings-menu → .side-menu / .side-menu-item (generalised, shared by memory and settings panels) - i18n: ~25 new keys across en/ru/es/de/zh/zh-Hant for all new form labels, placeholders, and empty states - Mobile: hamburger in titlebar, slide-in sidebar; box-shadow removed from sidebar - New regression test: tests/test_settings_navigation_and_detail_refresh.py (9 tests) Co-authored-by: Aron Prins <pwf.aron@gmail.com>
2026-04-24 16:05:25 +00:00
function showToast(msg,ms,type){const el=$('toast');if(!el)return;const s=String(msg==null?'':msg);let t=type;if(!t){const low=s.toLowerCase();if(/fail|error|denied|invalid|unavailable|no active|no workspace match|no model match|no personalities/.test(low))t='error';else if(/warn|queued|takes effect|skipped|fallback/.test(low))t='warning';else if(/saved|created|imported|restored|switched|set to|updated|duplicated|moved to|renamed|deleted|complete|pinned|archived|cleared|stopped/.test(low))t='success';else t='info';}el.textContent=s;el.className='toast show '+t;clearTimeout(el._t);el._t=setTimeout(()=>{el.classList.remove('show');},ms||2800);}
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite * fix: custom provider with slash model name no longer rerouted to OpenRouter (#255) When base_url is configured in config.yaml, resolve_model_provider() now trusts the configured provider/base_url entirely and skips the slash-based OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom being silently routed to OpenRouter, resulting in 401 errors. Fixes #230 * test: mobile layout regression suite — 14 tests for every QA run (#254) Adds tests/test_mobile_layout.py with 14 static regression tests that run on every QA pass to catch mobile layout breakage before it reaches prod. Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile overlay, bottom nav, files button, profile dropdown z-index, chip overflow, workspace close, 100dvh, 44px touch targets, 16px font-size on textarea. * feat: /skills slash command lists and filters available Hermes skills (#257) Adds /skills [query] command to commands.js. Fetches from /api/skills, groups by category (alphabetically sorted), displays as a formatted assistant message. Optional query filters by name, description, or category. i18n keys added for en, de, zh, zh-Hant. 1 regression test added. Fixes #248 * feat: shared app dialogs replace native confirm()/prompt() calls (#251) Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt call sites across panels.js, sessions.js, ui.js, workspace.js. Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore, ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant. 5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of native dialog calls. Extracted from PR #242. * fix: Android Chrome mobile — workspace panel close + profile dropdown (#256) Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when toggling the right workspace panel. New closeMobileFiles() helper closes the panel with correct overlay state tracking. Overlay onclick calls both closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x) added to workspace panel header. Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at max-width:900px, escaping the overflow-x:auto stacking context that was clipping it on Android Chrome. Fix applied during review: closeMobileSidebar() now checks if the right panel is still open before hiding the overlay, preventing the overlay from disappearing when only the sidebar is closed. Fixes #247 Fixes #246 * feat: session ⋯ action dropdown replaces per-row buttons (#252) Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash) with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full keyboard (Escape), click-outside, scroll, and resize-reposition handling. Position:fixed prevents sidebar clipping. 5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete (danger style). Each with icon and descriptive subtitle. Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons asserts the new trigger and menu functions exist, old per-row classes are gone. Extracted from PR #242. * docs: v0.47.0 release notes, bump version, update test counts (645) --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
// ── Shared app dialogs ───────────────────────────────────────────────────────
// showConfirmDialog(opts) and showPromptDialog(opts) replace browser-native dialog calls
// throughout the UI. Both return Promises and support: title, message, confirmLabel,
// cancelLabel, danger (confirm only), placeholder/value/inputType (prompt only).
const APP_DIALOG={resolve:null,kind:null,lastFocus:null};
let _appDialogBound=false;
function _isAppDialogOpen(){
const overlay=$('appDialogOverlay');
return !!(overlay&&overlay.style.display!=='none');
}
function _getAppDialogFocusable(){
return [$('appDialogInput'), $('appDialogCancel'), $('appDialogConfirm'), $('appDialogClose')]
.filter(el=>el&&el.style.display!=='none'&&!el.disabled);
}
function _finishAppDialog(result, restoreFocus=true){
const overlay=$('appDialogOverlay');
const dialog=$('appDialog');
const input=$('appDialogInput');
const confirmBtn=$('appDialogConfirm');
const resolve=APP_DIALOG.resolve;
const lastFocus=APP_DIALOG.lastFocus;
APP_DIALOG.resolve=null;
APP_DIALOG.kind=null;
APP_DIALOG.lastFocus=null;
if(overlay){overlay.style.display='none';overlay.setAttribute('aria-hidden','true');}
if(dialog) dialog.setAttribute('role','dialog');
if(input){input.value='';input.style.display='none';input.placeholder='';}
if(confirmBtn){confirmBtn.classList.remove('danger');confirmBtn.textContent=t('dialog_confirm_btn');}
if(restoreFocus&&lastFocus&&typeof lastFocus.focus==='function'){setTimeout(()=>lastFocus.focus(),0);}
if(resolve) resolve(result);
}
function _ensureAppDialogBindings(){
if(_appDialogBound) return;
_appDialogBound=true;
const overlay=$('appDialogOverlay');
const cancelBtn=$('appDialogCancel');
const confirmBtn=$('appDialogConfirm');
const closeBtn=$('appDialogClose');
if(overlay){
overlay.addEventListener('click',e=>{
if(e.target===overlay) _finishAppDialog(APP_DIALOG.kind==='prompt'?null:false);
});
}
if(cancelBtn) cancelBtn.addEventListener('click',()=>_finishAppDialog(APP_DIALOG.kind==='prompt'?null:false));
if(closeBtn) closeBtn.addEventListener('click',()=>_finishAppDialog(APP_DIALOG.kind==='prompt'?null:false));
if(confirmBtn){
confirmBtn.addEventListener('click',()=>{
if(APP_DIALOG.kind==='prompt'){
const input=$('appDialogInput');
_finishAppDialog(input?input.value:null);
}else{
_finishAppDialog(true);
}
});
}
document.addEventListener('keydown',e=>{
if(!_isAppDialogOpen()) return;
if(e.key==='Escape'){
e.preventDefault();
_finishAppDialog(APP_DIALOG.kind==='prompt'?null:false);
return;
}
if(e.key==='Enter'){
v0.50.264 polish followups: i18n parity + assistant-output readability Closes #1442 (server-side _LOGIN_LOCALE missing ja/pt/ko) Closes #1443 (promote _isImeEnter helper to 6 other Safari Enter guards) Closes #1446 (glued-bold-heading lift for LLM thinking-block output) Closes #1447 (markdown heading visual hierarchy in chat messages) All four issues were filed by the Opus pre-release advisor on the v0.50.264 batch or by Cygnus via Discord (relayed by @AvidFuturist, May 1 2026). They share a common shape — narrow, well-scoped, independent of each other, all adding regression tests. == #1442: _LOGIN_LOCALE parity (api/routes.py + static/i18n.js) == Added entries for ja/pt/ko to the server-side _LOGIN_LOCALE dict that renders the localized login page BEFORE the JS i18n bundle loads. With v0.50.264 shipping Japanese as the 8th built-in locale, ja/pt/ko users were seeing the English login page even with their language preference set. While auditing static/i18n.js for English leakage, also fixed: - ko: 10 user-facing login/sign-out/password keys still in English - es: 3 sign-out/auth-disabled keys still in English Tests: tests/test_login_locale_parity.py (20 tests) — pins both invariants: (a) every locale in i18n.js LOCALES has a matching _LOGIN_LOCALE entry (b) every locale's login-flow keys (13 of them) are translated, not English == #1443: window._isImeEnter promotion == PR #1441 fixed the Safari IME-composition Enter race in the chat composer (`#msg`) by widening the guard from `e.isComposing` to a `_isImeEnter(e)` helper that combines three signals (isComposing || keyCode===229 || _imeComposing flag). Six other Enter-input handlers were left on the original narrow guard and would still drop IME composition Enters on Safari for Japanese/Chinese/Korean users. Promoted the helper to `window._isImeEnter` (defined in static/boot.js) and replaced the `e.isComposing` guards at all six sites: - static/sessions.js: session rename, project create, project rename - static/ui.js: app dialog (confirm/prompt), message edit, workspace rename The state-free part of the helper (`isComposing || keyCode===229`) handles Safari's race for any focused input without needing per-input composition listeners — only `#msg` keeps the local `_imeComposing` flag. Tests: - tests/test_issue1443_ime_helper_promotion.py (9 tests) — pins each site + verifies no raw `e.isComposing` Enter-guards remain in sessions.js/ui.js - tests/test_ime_composition.py — alternation regex extended to accept the windowed helper form (loosen-test-on-shape-change pattern from v0.50.264 reflection notes) == #1446: glued-bold-heading lift (static/ui.js renderMd + Python mirror) == LLMs in thinking/reasoning mode emit "section headers" glued to the end of the previous paragraph with no whitespace: Para 1 text.**Heading to Para 2** Para 2 text.**Heading to Para 3** The renderer correctly produces inline `<strong>` per CommonMark, but it looks like trailing emphasis on the body text rather than a section break. Cygnus reported this as "Markdown feedback 2 of 3." Added a single regex pre-pass in renderMd(): s.replace(/([.!?])\*\*([^*\n]{1,80})\*\*\n\n/g, '$1\n\n**$2**\n\n') Constraints chosen to avoid false positives: - Trigger only on `[.!?]` IMMEDIATELY before `**` (no space) — almost always an LLM-glued heading, not intentional emphasis - Inner text ≤80 chars, no `*` or newline (single-line only) - Trailing `\n\n` required — preserves "this is **important** to know." mid-paragraph emphasis untouched - Position: after rawPreStash restore, before fence_stash restore — fenced code blocks stay protected (their content is `\x00P` / `\x00F` tokens when the lift runs) Mirrored in tests/test_sprint16.py render_md() so both stay in sync. Tests: tests/test_issue1446_glued_heading_lift.py (17 tests, 5 of which drive the actual ui.js renderMd via node) — covers all 3 trigger forms (.!?), all 4 preserve-emphasis cases the issue spec'd, fenced/inline code protection, chained glued headings, source-level position pin, regex shape pin. == #1447: markdown heading visual hierarchy (static/style.css) == Pre-fix sizes in `.msg-body`: h1 18px, h2 16px, h3 14px (= body), h4 13px, h5 12px, h6 11px So h3 was indistinguishable from body and h4/h5/h6 were SMALLER than body. Cygnus's report: "Markdown feedback 3 of 3 — Headings seem to be missing across the board in Hermes. They're there, but all plaintext." New sizes: h1 24px (border-bottom) h2 20px (border-bottom) h3 17px h4 15px h5 14px (uppercase, tracked) h6 13px (uppercase, tracked, muted) All headings now `font-weight:700` + `color:var(--strong)` for stronger ink. h5/h6 use uppercase + letter-spacing for "label-style" affordance instead of being smaller-than-body. Synced .preview-md (file preview pane) to match exactly so a markdown file preview and a chat message render identically. Added missing h4/h5/h6 rules to .preview-md (it only had h1-h3 before). Updated data-font-size="small"/"large" h1-h6 overrides to scale proportionally with the new defaults. Hierarchy preserved at all three font-size settings. Tests: tests/test_issue1447_heading_hierarchy.py (9 tests) — pins the size hierarchy, the bottom borders on h1/h2, the uppercase affordance on h5/h6, the .preview-md sync, and the small/large override scaling. == Verification == pytest tests/ -q → 3748 passed (+56 new) bash ~/WebUI/scripts/run-browser-tests.sh → 20 + 11 PASS bash ~/WebUI/scripts/webui_qa_agent.sh 8789 → 23/23 PASS Visual confirmation in browser at port 8789: - Heading hierarchy clearly visible at all 6 levels - Glued-bold lift produces separate paragraphs as designed - window._isImeEnter accessible from any module after boot.js - Login page renders ja/pt/ko strings correctly (curl -s /login)
2026-05-02 04:04:23 +00:00
if(window._isImeEnter&&window._isImeEnter(e)) return;
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite * fix: custom provider with slash model name no longer rerouted to OpenRouter (#255) When base_url is configured in config.yaml, resolve_model_provider() now trusts the configured provider/base_url entirely and skips the slash-based OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom being silently routed to OpenRouter, resulting in 401 errors. Fixes #230 * test: mobile layout regression suite — 14 tests for every QA run (#254) Adds tests/test_mobile_layout.py with 14 static regression tests that run on every QA pass to catch mobile layout breakage before it reaches prod. Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile overlay, bottom nav, files button, profile dropdown z-index, chip overflow, workspace close, 100dvh, 44px touch targets, 16px font-size on textarea. * feat: /skills slash command lists and filters available Hermes skills (#257) Adds /skills [query] command to commands.js. Fetches from /api/skills, groups by category (alphabetically sorted), displays as a formatted assistant message. Optional query filters by name, description, or category. i18n keys added for en, de, zh, zh-Hant. 1 regression test added. Fixes #248 * feat: shared app dialogs replace native confirm()/prompt() calls (#251) Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt call sites across panels.js, sessions.js, ui.js, workspace.js. Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore, ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant. 5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of native dialog calls. Extracted from PR #242. * fix: Android Chrome mobile — workspace panel close + profile dropdown (#256) Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when toggling the right workspace panel. New closeMobileFiles() helper closes the panel with correct overlay state tracking. Overlay onclick calls both closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x) added to workspace panel header. Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at max-width:900px, escaping the overflow-x:auto stacking context that was clipping it on Android Chrome. Fix applied during review: closeMobileSidebar() now checks if the right panel is still open before hiding the overlay, preventing the overlay from disappearing when only the sidebar is closed. Fixes #247 Fixes #246 * feat: session ⋯ action dropdown replaces per-row buttons (#252) Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash) with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full keyboard (Escape), click-outside, scroll, and resize-reposition handling. Position:fixed prevents sidebar clipping. 5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete (danger style). Each with icon and descriptive subtitle. Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons asserts the new trigger and menu functions exist, old per-row classes are gone. Extracted from PR #242. * docs: v0.47.0 release notes, bump version, update test counts (645) --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
const target=e.target;
const isTextarea=target&&target.tagName==='TEXTAREA';
if(!isTextarea){
e.preventDefault();
if(target===cancelBtn||target===closeBtn){
_finishAppDialog(APP_DIALOG.kind==='prompt'?null:false);
}else if(APP_DIALOG.kind==='prompt'){
const input=$('appDialogInput');
_finishAppDialog(input?input.value:null);
}else{
_finishAppDialog(true);
}
}
return;
}
if(e.key==='Tab'){
const nodes=_getAppDialogFocusable();
if(!nodes.length) return;
const idx=nodes.indexOf(document.activeElement);
let nextIdx=idx;
if(e.shiftKey){nextIdx=idx<=0?nodes.length-1:idx-1;}
else{nextIdx=idx===-1||idx===nodes.length-1?0:idx+1;}
e.preventDefault();
nodes[nextIdx].focus();
}
}, true);
}
function showConfirmDialog(opts={}){
_ensureAppDialogBindings();
if(APP_DIALOG.resolve) _finishAppDialog(false,false);
const overlay=$('appDialogOverlay'),dialog=$('appDialog'),title=$('appDialogTitle'),
desc=$('appDialogDesc'),input=$('appDialogInput'),cancelBtn=$('appDialogCancel'),confirmBtn=$('appDialogConfirm');
APP_DIALOG.resolve=null;APP_DIALOG.kind='confirm';APP_DIALOG.lastFocus=document.activeElement;
if(title) title.textContent=opts.title||t('dialog_confirm_title');
if(desc) desc.textContent=opts.message||'';
if(input){input.style.display='none';input.value='';}
if(cancelBtn) cancelBtn.textContent=opts.cancelLabel||t('cancel');
if(confirmBtn){
confirmBtn.textContent=opts.confirmLabel||t('dialog_confirm_btn');
confirmBtn.classList.toggle('danger',!!opts.danger);
}
if(dialog) dialog.setAttribute('role',opts.danger?'alertdialog':'dialog');
if(overlay){overlay.style.display='flex';overlay.setAttribute('aria-hidden','false');}
return new Promise(resolve=>{
APP_DIALOG.resolve=resolve;
setTimeout(()=>((opts.focusCancel?cancelBtn:confirmBtn)||confirmBtn||cancelBtn).focus(),0);
});
}
function showPromptDialog(opts={}){
_ensureAppDialogBindings();
if(APP_DIALOG.resolve) _finishAppDialog(null,false);
const overlay=$('appDialogOverlay'),dialog=$('appDialog'),title=$('appDialogTitle'),
desc=$('appDialogDesc'),input=$('appDialogInput'),cancelBtn=$('appDialogCancel'),confirmBtn=$('appDialogConfirm');
APP_DIALOG.resolve=null;APP_DIALOG.kind='prompt';APP_DIALOG.lastFocus=document.activeElement;
if(title) title.textContent=opts.title||t('dialog_prompt_title');
if(desc) desc.textContent=opts.message||'';
if(input){
input.type=opts.inputType||'text';input.style.display='';
input.value=opts.value||'';input.placeholder=opts.placeholder||'';
input.autocomplete='off';input.spellcheck=false;
}
if(cancelBtn) cancelBtn.textContent=opts.cancelLabel||t('cancel');
if(confirmBtn){confirmBtn.textContent=opts.confirmLabel||t('create');confirmBtn.classList.remove('danger');}
if(dialog) dialog.setAttribute('role','dialog');
if(overlay){overlay.style.display='flex';overlay.setAttribute('aria-hidden','false');}
return new Promise(resolve=>{
APP_DIALOG.resolve=resolve;
setTimeout(()=>{if(input&&input.style.display!=='none')input.focus();else if(confirmBtn)confirmBtn.focus();},0);
});
}
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117) * fix(#1096): copy buttons fall back to execCommand on HTTP contexts - Add _copyText() helper: tries navigator.clipboard first, falls back to document.execCommand('copy') with hidden textarea when not in secure context - Update copyMsg() and addCopyButtons() to use helper instead of direct navigator.clipboard.writeText() - Code block copy button now has .catch() handler (was silently failing) - Error messages use t('copy_failed') for i18n instead of hardcoded string - Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant) - Add 10 regression tests * fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge - User message attachments with image extensions now render as <img> via api/media endpoint, with click-to-fullscreen support - Non-image attachments still show paperclip + filename badge - Extracts filename from full path for display - Add 5 regression tests * fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix) * fix: improve mobile touch responsiveness for session list items iPad Safari has known issues with the click/dblclick pattern on touch: - :hover-triggered padding-right layout shift causes the first tap click to target the wrong element (actions button that just appeared) - No touch-action:manipulation means iOS still delays taps for double-tap zoom detection - The old onclick+ondblclick pattern is designed for mouse, not touch Changes: - CSS: Remove :hover from padding-right rule to prevent layout shift - CSS: Add touch-action:manipulation and -webkit-tap-highlight-color to .session-item for immediate tap response - JS: Replace onclick/ondblclick with onpointerup + manual 350ms double-tap detection — works consistently on mouse and touch * fix(#1106): iterate custom_providers[].models dict keys for dropdown population - After reading singular 'model' field, also iterate 'models' dict keys - Deduplicate: model field value not repeated if also in models dict - Skip non-string keys gracefully - Works for both named and unnamed custom_providers entries - Add 7 regression tests * fix(#1105): allow custom_providers hostnames through SSRF check - Build trusted hostname set from custom_providers[].base_url in config.yaml - These are user-explicitly configured endpoints — not SSRF risks - Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active - Unknown private IPs still blocked - Add 7 tests (5 source analysis + 2 functional with mocked socket) * fix(tests): update hover padding assertions for #1110 touch fix (absorb) * fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb) * fix: filter right/middle-click from pointerup handler (absorb) * docs: v0.50.221 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
function _copyText(text){
if(navigator.clipboard && window.isSecureContext){
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
return navigator.clipboard.writeText(text).catch(()=>{
// Fallback if clipboard API fails (e.g. permissions)
return _fallbackCopy(text);
});
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117) * fix(#1096): copy buttons fall back to execCommand on HTTP contexts - Add _copyText() helper: tries navigator.clipboard first, falls back to document.execCommand('copy') with hidden textarea when not in secure context - Update copyMsg() and addCopyButtons() to use helper instead of direct navigator.clipboard.writeText() - Code block copy button now has .catch() handler (was silently failing) - Error messages use t('copy_failed') for i18n instead of hardcoded string - Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant) - Add 10 regression tests * fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge - User message attachments with image extensions now render as <img> via api/media endpoint, with click-to-fullscreen support - Non-image attachments still show paperclip + filename badge - Extracts filename from full path for display - Add 5 regression tests * fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix) * fix: improve mobile touch responsiveness for session list items iPad Safari has known issues with the click/dblclick pattern on touch: - :hover-triggered padding-right layout shift causes the first tap click to target the wrong element (actions button that just appeared) - No touch-action:manipulation means iOS still delays taps for double-tap zoom detection - The old onclick+ondblclick pattern is designed for mouse, not touch Changes: - CSS: Remove :hover from padding-right rule to prevent layout shift - CSS: Add touch-action:manipulation and -webkit-tap-highlight-color to .session-item for immediate tap response - JS: Replace onclick/ondblclick with onpointerup + manual 350ms double-tap detection — works consistently on mouse and touch * fix(#1106): iterate custom_providers[].models dict keys for dropdown population - After reading singular 'model' field, also iterate 'models' dict keys - Deduplicate: model field value not repeated if also in models dict - Skip non-string keys gracefully - Works for both named and unnamed custom_providers entries - Add 7 regression tests * fix(#1105): allow custom_providers hostnames through SSRF check - Build trusted hostname set from custom_providers[].base_url in config.yaml - These are user-explicitly configured endpoints — not SSRF risks - Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active - Unknown private IPs still blocked - Add 7 tests (5 source analysis + 2 functional with mocked socket) * fix(tests): update hover padding assertions for #1110 touch fix (absorb) * fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb) * fix: filter right/middle-click from pointerup handler (absorb) * docs: v0.50.221 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +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
return _fallbackCopy(text);
}
function _fallbackCopy(text){
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117) * fix(#1096): copy buttons fall back to execCommand on HTTP contexts - Add _copyText() helper: tries navigator.clipboard first, falls back to document.execCommand('copy') with hidden textarea when not in secure context - Update copyMsg() and addCopyButtons() to use helper instead of direct navigator.clipboard.writeText() - Code block copy button now has .catch() handler (was silently failing) - Error messages use t('copy_failed') for i18n instead of hardcoded string - Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant) - Add 10 regression tests * fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge - User message attachments with image extensions now render as <img> via api/media endpoint, with click-to-fullscreen support - Non-image attachments still show paperclip + filename badge - Extracts filename from full path for display - Add 5 regression tests * fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix) * fix: improve mobile touch responsiveness for session list items iPad Safari has known issues with the click/dblclick pattern on touch: - :hover-triggered padding-right layout shift causes the first tap click to target the wrong element (actions button that just appeared) - No touch-action:manipulation means iOS still delays taps for double-tap zoom detection - The old onclick+ondblclick pattern is designed for mouse, not touch Changes: - CSS: Remove :hover from padding-right rule to prevent layout shift - CSS: Add touch-action:manipulation and -webkit-tap-highlight-color to .session-item for immediate tap response - JS: Replace onclick/ondblclick with onpointerup + manual 350ms double-tap detection — works consistently on mouse and touch * fix(#1106): iterate custom_providers[].models dict keys for dropdown population - After reading singular 'model' field, also iterate 'models' dict keys - Deduplicate: model field value not repeated if also in models dict - Skip non-string keys gracefully - Works for both named and unnamed custom_providers entries - Add 7 regression tests * fix(#1105): allow custom_providers hostnames through SSRF check - Build trusted hostname set from custom_providers[].base_url in config.yaml - These are user-explicitly configured endpoints — not SSRF risks - Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active - Unknown private IPs still blocked - Add 7 tests (5 source analysis + 2 functional with mocked socket) * fix(tests): update hover padding assertions for #1110 touch fix (absorb) * fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb) * fix: filter right/middle-click from pointerup handler (absorb) * docs: v0.50.221 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
return new Promise((resolve,reject)=>{
const ta=document.createElement('textarea');
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
ta.value=text;ta.style.cssText='position:fixed;left:0;top:0;width:2em;height:2em;padding:0;border:none;outline:none;box-shadow:none;background:transparent;z-index:-1';
document.body.appendChild(ta);
ta.focus();ta.select();
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117) * fix(#1096): copy buttons fall back to execCommand on HTTP contexts - Add _copyText() helper: tries navigator.clipboard first, falls back to document.execCommand('copy') with hidden textarea when not in secure context - Update copyMsg() and addCopyButtons() to use helper instead of direct navigator.clipboard.writeText() - Code block copy button now has .catch() handler (was silently failing) - Error messages use t('copy_failed') for i18n instead of hardcoded string - Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant) - Add 10 regression tests * fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge - User message attachments with image extensions now render as <img> via api/media endpoint, with click-to-fullscreen support - Non-image attachments still show paperclip + filename badge - Extracts filename from full path for display - Add 5 regression tests * fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix) * fix: improve mobile touch responsiveness for session list items iPad Safari has known issues with the click/dblclick pattern on touch: - :hover-triggered padding-right layout shift causes the first tap click to target the wrong element (actions button that just appeared) - No touch-action:manipulation means iOS still delays taps for double-tap zoom detection - The old onclick+ondblclick pattern is designed for mouse, not touch Changes: - CSS: Remove :hover from padding-right rule to prevent layout shift - CSS: Add touch-action:manipulation and -webkit-tap-highlight-color to .session-item for immediate tap response - JS: Replace onclick/ondblclick with onpointerup + manual 350ms double-tap detection — works consistently on mouse and touch * fix(#1106): iterate custom_providers[].models dict keys for dropdown population - After reading singular 'model' field, also iterate 'models' dict keys - Deduplicate: model field value not repeated if also in models dict - Skip non-string keys gracefully - Works for both named and unnamed custom_providers entries - Add 7 regression tests * fix(#1105): allow custom_providers hostnames through SSRF check - Build trusted hostname set from custom_providers[].base_url in config.yaml - These are user-explicitly configured endpoints — not SSRF risks - Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active - Unknown private IPs still blocked - Add 7 tests (5 source analysis + 2 functional with mocked socket) * fix(tests): update hover padding assertions for #1110 touch fix (absorb) * fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb) * fix: filter right/middle-click from pointerup handler (absorb) * docs: v0.50.221 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
try{document.execCommand('copy');resolve();}
catch(e){reject(e);}
finally{document.body.removeChild(ta);}
});
}
2026-05-04 23:58:54 +00:00
function copyStatusSessionId(btn){
const text=btn&&btn.getAttribute('data-copy-status-session');
if(!text)return;
_copyText(text).then(()=>{
const orig=btn.innerHTML;
btn.innerHTML=(typeof li==='function')?li('check',13):t('copied');
btn.classList.add('copied');
setTimeout(()=>{btn.innerHTML=orig;btn.classList.remove('copied');},1500);
}).catch(()=>showToast(t('copy_failed')));
}
function copyMsg(btn){
const row=btn.closest('[data-raw-text]');
const text=row?row.dataset.rawText:'';
if(!text)return;
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117) * fix(#1096): copy buttons fall back to execCommand on HTTP contexts - Add _copyText() helper: tries navigator.clipboard first, falls back to document.execCommand('copy') with hidden textarea when not in secure context - Update copyMsg() and addCopyButtons() to use helper instead of direct navigator.clipboard.writeText() - Code block copy button now has .catch() handler (was silently failing) - Error messages use t('copy_failed') for i18n instead of hardcoded string - Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant) - Add 10 regression tests * fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge - User message attachments with image extensions now render as <img> via api/media endpoint, with click-to-fullscreen support - Non-image attachments still show paperclip + filename badge - Extracts filename from full path for display - Add 5 regression tests * fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix) * fix: improve mobile touch responsiveness for session list items iPad Safari has known issues with the click/dblclick pattern on touch: - :hover-triggered padding-right layout shift causes the first tap click to target the wrong element (actions button that just appeared) - No touch-action:manipulation means iOS still delays taps for double-tap zoom detection - The old onclick+ondblclick pattern is designed for mouse, not touch Changes: - CSS: Remove :hover from padding-right rule to prevent layout shift - CSS: Add touch-action:manipulation and -webkit-tap-highlight-color to .session-item for immediate tap response - JS: Replace onclick/ondblclick with onpointerup + manual 350ms double-tap detection — works consistently on mouse and touch * fix(#1106): iterate custom_providers[].models dict keys for dropdown population - After reading singular 'model' field, also iterate 'models' dict keys - Deduplicate: model field value not repeated if also in models dict - Skip non-string keys gracefully - Works for both named and unnamed custom_providers entries - Add 7 regression tests * fix(#1105): allow custom_providers hostnames through SSRF check - Build trusted hostname set from custom_providers[].base_url in config.yaml - These are user-explicitly configured endpoints — not SSRF risks - Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active - Unknown private IPs still blocked - Add 7 tests (5 source analysis + 2 functional with mocked socket) * fix(tests): update hover padding assertions for #1110 touch fix (absorb) * fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb) * fix: filter right/middle-click from pointerup handler (absorb) * docs: v0.50.221 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
_copyText(text).then(()=>{
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
const orig=btn.innerHTML;btn.innerHTML=li('check',13);btn.style.color='var(--blue)';
setTimeout(()=>{btn.innerHTML=orig;btn.style.color='';},1500);
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117) * fix(#1096): copy buttons fall back to execCommand on HTTP contexts - Add _copyText() helper: tries navigator.clipboard first, falls back to document.execCommand('copy') with hidden textarea when not in secure context - Update copyMsg() and addCopyButtons() to use helper instead of direct navigator.clipboard.writeText() - Code block copy button now has .catch() handler (was silently failing) - Error messages use t('copy_failed') for i18n instead of hardcoded string - Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant) - Add 10 regression tests * fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge - User message attachments with image extensions now render as <img> via api/media endpoint, with click-to-fullscreen support - Non-image attachments still show paperclip + filename badge - Extracts filename from full path for display - Add 5 regression tests * fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix) * fix: improve mobile touch responsiveness for session list items iPad Safari has known issues with the click/dblclick pattern on touch: - :hover-triggered padding-right layout shift causes the first tap click to target the wrong element (actions button that just appeared) - No touch-action:manipulation means iOS still delays taps for double-tap zoom detection - The old onclick+ondblclick pattern is designed for mouse, not touch Changes: - CSS: Remove :hover from padding-right rule to prevent layout shift - CSS: Add touch-action:manipulation and -webkit-tap-highlight-color to .session-item for immediate tap response - JS: Replace onclick/ondblclick with onpointerup + manual 350ms double-tap detection — works consistently on mouse and touch * fix(#1106): iterate custom_providers[].models dict keys for dropdown population - After reading singular 'model' field, also iterate 'models' dict keys - Deduplicate: model field value not repeated if also in models dict - Skip non-string keys gracefully - Works for both named and unnamed custom_providers entries - Add 7 regression tests * fix(#1105): allow custom_providers hostnames through SSRF check - Build trusted hostname set from custom_providers[].base_url in config.yaml - These are user-explicitly configured endpoints — not SSRF risks - Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active - Unknown private IPs still blocked - Add 7 tests (5 source analysis + 2 functional with mocked socket) * fix(tests): update hover padding assertions for #1110 touch fix (absorb) * fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb) * fix: filter right/middle-click from pointerup handler (absorb) * docs: v0.50.221 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
}).catch(()=>showToast(t('copy_failed')));
}
// ── TTS: Text-to-Speech via Web Speech API (#499) ──
// Strips markdown, code blocks, and MEDIA: paths for clean speech output.
function _stripForTTS(text){
fix(renderer): line-anchor fence regex to prevent mid-line ``` corruption (#1438) The markdown fence regex /```([\s\S]*?)```/g had no line anchoring. A literal triple backtick inside code block content (e.g. a regex with ``` in a lookbehind, or a script that documents fences) terminated the outer fence at the wrong place. The leaked tail then went through bold/italic/inline-code passes, eating `*` characters as italic markers and emitting literal </strong> tags into the rendered output. CommonMark §4.5 requires that an opening code fence be the first non-whitespace content of a line (up to 3 spaces of indent allowed) and that the closing fence also start a line. This patch updates 3 sites + the Python mirror to use that invariant: static/ui.js:1559 renderMd() fenced-block stash (assistant messages) static/ui.js:66 _renderUserFencedBlocks() (user messages) static/ui.js:2599 _stripForTTS() (TTS speech pre-strip) tests/test_sprint16.py Python mirror Pattern: (^|\n)[ ]{0,3}```(?:([\s\S]*?)\n)?[ ]{0,3}```(?=\n|$) The non-capturing (?:...\n)? group keeps empty fences (```\n```) working; without it, a body+\n is required and the closing fence on the very next line no longer matches. The lead group (^|\n) is prefixed back to the stash token so paragraphs above don't bleed into the <pre> block. 20 regression tests in tests/test_issue1438_fence_anchoring.py cover: - Cygnus's exact repro from Discord (May 1 2026) - Inline ``` mid-paragraph (must not open fence) - Partial/streaming fence with no close (must not eat content) - Empty fences with and without language tag - 3-space indented fences (allowed) vs 4-space (not a fence) - Multiple adjacent blocks - Bold/italic/inline-code surviving after a fence - Source-level guards on all 3 patched sites + lead-prefix invariant Empirical browser verification (live JS, on bug repro): Before fix: </code></pre>[^\n]<em>|%%[ \t]</em>... ← truncated, italic leak After fix: <pre><code>...```[^\n]*|%%...</code></pre> ← intact, regex preserved Tests: 3678 passed (+20 from new test file, was 3658), 0 failures. Reported-By: Cygnus (Discord) Relayed-By: @AvidFuturist Closes #1438
2026-05-02 02:30:20 +00:00
// Remove code blocks entirely (```) — line-anchored to match #1438 fix
text=text.replace(/(^|\n)[ ]{0,3}```(?:[\s\S]*?\n)?[ ]{0,3}```(?=\n|$)/g,' ');
// Remove inline code
text=text.replace(/`[^`]+`/g,' ');
// Strip bold/italic
text=text.replace(/\*\*(.+?)\*\*/g,'$1');
text=text.replace(/\*(.+?)\*/g,'$1');
text=text.replace(/__(.+?)__/g,'$1');
text=text.replace(/_(.+?)_/g,'$1');
// Strip headings
text=text.replace(/^#{1,6}\s+/gm,'');
// Strip links, keep text
text=text.replace(/\[([^\]]+)\]\([^)]+\)/g,'$1');
// Replace MEDIA: paths with a simple label
text=text.replace(/MEDIA:[^\s]+/g,'a file');
// Strip HTML tags that may leak through markdown
text=text.replace(/<[^>]+>/g,' ');
// Collapse whitespace
text=text.replace(/\s+/g,' ').trim();
return text;
}
let _ttsSpeaking=false;
let _ttsCurrentUtterance=null;
function speakMessage(btn){
if(!('speechSynthesis' in window)){
showToast(t('tts_not_supported')||'Speech synthesis not supported in this browser.');
return;
}
// If already speaking this message, stop
if(btn&&btn.dataset.speaking==='1'){
stopTTS();
return;
}
// Stop any current speech
stopTTS();
const row=btn?btn.closest('[data-raw-text]'):null;
const text=row?row.dataset.rawText:'';
if(!text) return;
const clean=_stripForTTS(text);
if(!clean) return;
const utter=new SpeechSynthesisUtterance(clean);
// Apply saved voice preference
const savedVoice=localStorage.getItem('hermes-tts-voice');
const voices=speechSynthesis.getVoices();
if(savedVoice&&voices.length){
const match=voices.find(v=>v.name===savedVoice);
if(match) utter.voice=match;
}
// Apply saved rate/pitch
const savedRate=parseFloat(localStorage.getItem('hermes-tts-rate'));
if(!isNaN(savedRate)) utter.rate= Math.min(2,Math.max(0.5,savedRate));
const savedPitch=parseFloat(localStorage.getItem('hermes-tts-pitch'));
if(!isNaN(savedPitch)) utter.pitch=Math.min(2,Math.max(0,savedPitch));
_ttsCurrentUtterance=utter;
_ttsSpeaking=true;
if(btn) btn.dataset.speaking='1';
utter.onend=()=>{ _ttsSpeaking=false; _ttsCurrentUtterance=null; if(btn) btn.dataset.speaking='0'; };
utter.onerror=()=>{ _ttsSpeaking=false; _ttsCurrentUtterance=null; if(btn) btn.dataset.speaking='0'; };
speechSynthesis.speak(utter);
}
function stopTTS(){
if('speechSynthesis' in window){
speechSynthesis.cancel();
}
_ttsSpeaking=false;
_ttsCurrentUtterance=null;
// Reset all speaking buttons
document.querySelectorAll('[data-speaking="1"]').forEach(btn=>{ btn.dataset.speaking='0'; });
}
function autoReadLastAssistant(){
if(!('speechSynthesis' in window)) return;
const pref=localStorage.getItem('hermes-tts-auto-read');
if(pref!=='true') return;
// Find the last assistant message segment in the DOM
const rows=document.querySelectorAll('.msg-row[data-role="assistant"], .assistant-segment[data-raw-text]');
if(!rows.length) return;
const last=rows[rows.length-1];
const text=last.dataset.rawText||'';
if(!text.trim()) return;
const clean=_stripForTTS(text);
if(!clean) return;
const utter=new SpeechSynthesisUtterance(clean);
const savedVoice=localStorage.getItem('hermes-tts-voice');
const voices=speechSynthesis.getVoices();
if(savedVoice&&voices.length){
const match=voices.find(v=>v.name===savedVoice);
if(match) utter.voice=match;
}
const savedRate=parseFloat(localStorage.getItem('hermes-tts-rate'));
if(!isNaN(savedRate)) utter.rate=Math.min(2,Math.max(0.5,savedRate));
const savedPitch=parseFloat(localStorage.getItem('hermes-tts-pitch'));
if(!isNaN(savedPitch)) utter.pitch=Math.min(2,Math.max(0,savedPitch));
speechSynthesis.speak(utter);
}
// ── Reconnect banner (B4/B5: reload resilience) ──
const INFLIGHT_KEY = 'hermes-webui-inflight'; // localStorage key for in-flight session tracking
const INFLIGHT_STATE_KEY = 'hermes-webui-inflight-state'; // localStorage snapshots for mid-stream reload recovery
function _readInflightStateMap(){
try{
const raw=localStorage.getItem(INFLIGHT_STATE_KEY);
const parsed=raw?JSON.parse(raw):{};
return parsed&&typeof parsed==='object'?parsed:{};
}catch(_){
return {};
}
}
function saveInflightState(sid, state){
if(!sid||!state) return;
try{
const all=_readInflightStateMap();
all[sid]={...state,updated_at:Date.now()};
localStorage.setItem(INFLIGHT_STATE_KEY, JSON.stringify(all));
}catch(_){ }
}
function loadInflightState(sid, streamId){
if(!sid) return null;
const all=_readInflightStateMap();
const entry=all[sid];
if(!entry) return null;
if(streamId&&entry.streamId&&entry.streamId!==streamId) return null;
if(entry.updated_at&&Date.now()-entry.updated_at>10*60*1000){
clearInflightState(sid);
return null;
}
return entry;
}
function clearInflightState(sid){
if(!sid) return;
try{
const all=_readInflightStateMap();
if(!(sid in all)) return;
delete all[sid];
if(Object.keys(all).length) localStorage.setItem(INFLIGHT_STATE_KEY, JSON.stringify(all));
else localStorage.removeItem(INFLIGHT_STATE_KEY);
}catch(_){ }
}
function markInflight(sid, streamId) {
localStorage.setItem(INFLIGHT_KEY, JSON.stringify({sid, streamId, ts: Date.now()}));
}
function clearInflight() {
localStorage.removeItem(INFLIGHT_KEY);
}
function showReconnectBanner(msg) {
$('reconnectMsg').textContent = msg || 'A response may have been in progress when you last left.';
$('reconnectBanner').classList.add('visible');
}
function dismissReconnect() {
$('reconnectBanner').classList.remove('visible');
clearInflight();
}
2026-05-05 00:19:21 +00:00
2026-05-05 06:57:58 +00:00
// ── Live host resource health panel (#693) ──
const SYSTEM_HEALTH_INTERVAL_MS=5000;
let _systemHealthTimer=null;
function _systemHealthPercent(metric){
const percent=Number(metric&&metric.percent);
if(!Number.isFinite(percent)) return null;
return Math.max(0,Math.min(100,Math.round(percent*10)/10));
}
function _formatSystemHealthPercent(percent){
if(percent == null) return '—';
return `${percent.toFixed(percent%1?1:0)}%`;
}
function _formatSystemHealthBytes(metric){
if(!metric||!metric.used_bytes||!metric.total_bytes) return '';
const units=['B','KB','MB','GB','TB'];
const fmt=(bytes)=>{
let value=Number(bytes)||0, idx=0;
while(value>=1024&&idx<units.length-1){value/=1024;idx++;}
return `${value.toFixed(value>=10||idx===0?0:1)} ${units[idx]}`;
};
return `${fmt(metric.used_bytes)} / ${fmt(metric.total_bytes)}`;
}
function _updateSystemHealthMetric(name,metric){
const row=document.querySelector(`[data-system-health-metric="${name}"]`);
if(!row) return;
const rawPercent=_systemHealthPercent(metric);
const percent=rawPercent == null ? 0 : rawPercent;
const label=row.querySelector('[data-system-health-value]');
const bar=row.querySelector('.system-health-bar');
const fill=row.querySelector('.system-health-bar-fill');
const text=_formatSystemHealthPercent(rawPercent);
if(label){
label.textContent=text;
const bytes=(name==='memory'||name==='disk')?_formatSystemHealthBytes(metric):'';
label.title=bytes||text;
}
if(bar) bar.setAttribute('aria-valuenow',String(percent));
if(fill) fill.style.width=`${percent}%`;
}
function setSystemHealthUnavailable(message){
const panel=$('systemHealthPanel');
const status=$('systemHealthStatus');
if(!panel) return;
panel.classList.remove('loading');
panel.classList.add('unavailable');
if(status) status.textContent=message||'Unavailable';
['cpu','memory','disk'].forEach(name=>_updateSystemHealthMetric(name,null));
}
function renderSystemHealth(payload){
const panel=$('systemHealthPanel');
const status=$('systemHealthStatus');
if(!panel) return;
if(!payload||payload.available===false){
setSystemHealthUnavailable('Unavailable');
return;
}
panel.classList.remove('loading','unavailable');
if(status) status.textContent=payload.status==='partial'?'Partial':'Live';
_updateSystemHealthMetric('cpu',payload.cpu);
_updateSystemHealthMetric('memory',payload.memory);
_updateSystemHealthMetric('disk',payload.disk);
}
async function pollSystemHealth(){
if(document.visibilityState !== 'visible') return;
try{
const payload=await api('/api/system/health');
renderSystemHealth(payload);
}catch(_){
setSystemHealthUnavailable('Unavailable');
}
}
function startSystemHealthMonitor(){
if(document.visibilityState !== 'visible') return;
if(_systemHealthTimer) return;
void pollSystemHealth();
_systemHealthTimer=setInterval(pollSystemHealth,SYSTEM_HEALTH_INTERVAL_MS);
}
function stopSystemHealthMonitor(){
if(_systemHealthTimer){clearInterval(_systemHealthTimer);_systemHealthTimer=null;}
}
function _syncSystemHealthMonitorVisibility(){
if(document.visibilityState === 'visible') startSystemHealthMonitor();
else stopSystemHealthMonitor();
}
document.addEventListener('visibilitychange',_syncSystemHealthMonitorVisibility);
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',startSystemHealthMonitor);
else startSystemHealthMonitor();
2026-05-05 00:19:21 +00:00
// ── Hermes agent/gateway heartbeat alert (#716) ──
const AGENT_HEALTH_INTERVAL_MS=30000;
const AGENT_HEALTH_DISMISSED_KEY='agent-health-dismissed';
let _agentHealthTimer=null;
let _agentHealthLastState='unknown';
function _agentHealthDismissed(){
try{return localStorage.getItem(AGENT_HEALTH_DISMISSED_KEY)==='1';}
catch(_){return false;}
}
function _setAgentHealthDismissed(value){
try{
if(value)localStorage.setItem(AGENT_HEALTH_DISMISSED_KEY,'1');
else localStorage.removeItem(AGENT_HEALTH_DISMISSED_KEY);
}catch(_){ }
}
function _hideAgentHealthAlert(){
const banner=$('agentHealthBanner');
if(banner){banner.classList.remove('visible');banner.hidden=true;}
}
function _showAgentHealthAlert(payload){
if(_agentHealthDismissed()) return;
const banner=$('agentHealthBanner');
const title=$('agentHealthTitle');
const details=$('agentHealthDetails');
if(!banner) return;
if(title) title.textContent='Hermes agent is not responding';
const state=payload&&payload.details&&payload.details.gateway_state?` State: ${payload.details.gateway_state}.`:'';
if(details) details.textContent=`Gateway heartbeat failed.${state} Messages may not be delivered until it comes back.`;
banner.hidden=false;
banner.classList.add('visible');
}
function dismissAgentHealthAlert(){
_setAgentHealthDismissed(true);
_hideAgentHealthAlert();
}
async function pollAgentHealth(){
if(document.visibilityState !== 'visible') return;
try{
const payload=await api('/api/health/agent');
if(payload.alive === true){
_agentHealthLastState='alive';
_setAgentHealthDismissed(false);
_hideAgentHealthAlert();
return;
}
if(payload.alive === false){
_agentHealthLastState='down';
_showAgentHealthAlert(payload);
return;
}
if(payload.alive == null){
_agentHealthLastState='unknown';
_hideAgentHealthAlert();
}
}catch(_){
_agentHealthLastState='unknown';
_hideAgentHealthAlert();
}
}
function startAgentHealthMonitor(){
if(document.visibilityState !== 'visible') return;
if(_agentHealthTimer) return;
void pollAgentHealth();
_agentHealthTimer=setInterval(pollAgentHealth, AGENT_HEALTH_INTERVAL_MS);
}
function stopAgentHealthMonitor(){
if(_agentHealthTimer){clearInterval(_agentHealthTimer);_agentHealthTimer=null;}
}
function _syncAgentHealthMonitorVisibility(){
if(document.visibilityState === 'visible') startAgentHealthMonitor();
else stopAgentHealthMonitor();
}
document.addEventListener('visibilitychange',_syncAgentHealthMonitorVisibility);
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',startAgentHealthMonitor);
else startAgentHealthMonitor();
async function refreshSession() {
// When the banner is in post-update restart mode, the "Reload" button
// should do a full page reload — a session refresh would just 502 while
// the server is still restarting.
if (window._restartingForUpdate) { location.reload(); return; }
dismissReconnect();
if (!S.session) return;
try {
const data = await api(`/api/session?session_id=${encodeURIComponent(S.session.session_id)}`);
S.session = data.session;
S.messages = data.session.messages || [];
const pendingMsg=getPendingSessionMessage(data.session);
if(pendingMsg) S.messages.push(pendingMsg);
S.activeStreamId=data.session.active_stream_id||null;
syncTopbar(); renderMessages();
showToast('Conversation refreshed');
} catch(e) { setStatus('Refresh failed: ' + e.message); }
}
// ── Update banner ──
function _formatUpdateTargetStatus(label,info){
if(!info||!(info.behind>0)) return null;
const branch=info.branch?` (${info.branch})`:'';
return `${label}${branch}: ${info.behind} update${info.behind>1?'s':''}`;
}
function _showUpdateBanner(data){
const parts=[];
const webuiPart=_formatUpdateTargetStatus('WebUI',data.webui);
const agentPart=_formatUpdateTargetStatus('Agent',data.agent);
if(webuiPart) parts.push(webuiPart);
if(agentPart) parts.push(agentPart);
if(!parts.length)return;
const msg=$('updateMsg');
if(msg) msg.textContent='\u2B06 '+parts.join(', ')+' available';
const banner=$('updateBanner');
if(banner) banner.classList.add('visible');
window._updateData=data;
fix(updates): use merge-base for compare URL so 'What's new?' link resolves Closes #1579. api/updates.py was building the GitHub compare URL from local HEAD short SHA: repoUrl + '/compare/' + curSha + '...' + newSha where curSha = `git rev-parse --short HEAD` Whenever local HEAD diverges from upstream — unpushed work, dirty stage branches, forks, in-flight rebases, release-time merge commits whose SHA only lives in the maintainer's local history — the compare URL points at a SHA github.com has never seen and returns the standard 404 page. Reporter (@ai-ag2026) observed: https://github.com/nesquena/hermes-webui/compare/c660c7f...86cb22e → 404 because c660c7f was an unpushed local commit. The right base is `git merge-base HEAD <compare_ref>` — the most recent commit local and upstream share. Since `git fetch` succeeded just before, the merge-base is guaranteed to exist on the upstream GitHub repo. Behavior matrix: Pure-behind clone (no local commits): merge-base == HEAD; URL unchanged. Behind + local-only commits (#1579): merge-base != HEAD; URL points at public ancestor instead of local HEAD. merge-base failure (shallow clone): current_sha=None; JS link guard suppresses link rather than emitting a known-broken URL. Also hardens static/ui.js: reset the link's href and display:none on every banner render, so a stale link from a prior render can't survive a re-render where the new payload has current_sha=null. Tests: - test_current_sha_is_merge_base_not_local_HEAD — reporter's scenario - test_current_sha_equals_HEAD_when_no_local_commits — backward compat - test_current_sha_falls_back_to_None_when_merge_base_fails — defensive - test_whats_new_link_resets_display_and_href_on_every_render - test_whats_new_link_suppressed_when_curSha_falsy - test_reporter_url_shape_no_longer_produces_invalid_compare_url 4094 → 4100 passing. 0 regressions.
2026-05-04 05:08:00 +00:00
// Wire up "What's new?" link.
//
// Reset display:none + clear the href on every render — otherwise a stale
// link from a prior update banner can stay visible after we've moved past
// a state where the new payload no longer carries usable SHAs (#1579 case
// when the local HEAD diverges from upstream and the compare URL would 404).
const link=$('updateWhatsNew');
fix(updates): use merge-base for compare URL so 'What's new?' link resolves Closes #1579. api/updates.py was building the GitHub compare URL from local HEAD short SHA: repoUrl + '/compare/' + curSha + '...' + newSha where curSha = `git rev-parse --short HEAD` Whenever local HEAD diverges from upstream — unpushed work, dirty stage branches, forks, in-flight rebases, release-time merge commits whose SHA only lives in the maintainer's local history — the compare URL points at a SHA github.com has never seen and returns the standard 404 page. Reporter (@ai-ag2026) observed: https://github.com/nesquena/hermes-webui/compare/c660c7f...86cb22e → 404 because c660c7f was an unpushed local commit. The right base is `git merge-base HEAD <compare_ref>` — the most recent commit local and upstream share. Since `git fetch` succeeded just before, the merge-base is guaranteed to exist on the upstream GitHub repo. Behavior matrix: Pure-behind clone (no local commits): merge-base == HEAD; URL unchanged. Behind + local-only commits (#1579): merge-base != HEAD; URL points at public ancestor instead of local HEAD. merge-base failure (shallow clone): current_sha=None; JS link guard suppresses link rather than emitting a known-broken URL. Also hardens static/ui.js: reset the link's href and display:none on every banner render, so a stale link from a prior render can't survive a re-render where the new payload has current_sha=null. Tests: - test_current_sha_is_merge_base_not_local_HEAD — reporter's scenario - test_current_sha_equals_HEAD_when_no_local_commits — backward compat - test_current_sha_falls_back_to_None_when_merge_base_fails — defensive - test_whats_new_link_resets_display_and_href_on_every_render - test_whats_new_link_suppressed_when_curSha_falsy - test_reporter_url_shape_no_longer_produces_invalid_compare_url 4094 → 4100 passing. 0 regressions.
2026-05-04 05:08:00 +00:00
if(link){
link.style.display='none';
link.removeAttribute('href');
if(data.webui){
const repoUrl=data.webui.repo_url;
const curSha=data.webui.current_sha;
const newSha=data.webui.latest_sha;
if(repoUrl && curSha && newSha){
link.href=repoUrl+'/compare/'+curSha+'...'+newSha;
link.style.display='inline';
}
}
}
}
function dismissUpdate(){
const b=$('updateBanner');if(b)b.classList.remove('visible');
sessionStorage.setItem('hermes-update-dismissed','1');
}
2026-05-05 04:02:03 +00:00
function _isUpdateApplyNetworkError(error){
if(error && error.status) return false;
2026-05-05 04:02:03 +00:00
const message=(error&&error.message)||String(error||'');
return /Failed to fetch|NetworkError|Load failed/i.test(message);
}
function _formatUpdateApplyExceptionMessage(error){
if(_isUpdateApplyNetworkError(error)){
return 'Update failed: could not reach the WebUI server. It may have restarted or the connection was interrupted. Please wait a few seconds, reload the page, then check the server if it still does not come back.';
}
const message=(error&&error.message)||String(error||'unknown error');
return 'Update failed: '+message;
}
async function applyUpdates(){
2026-05-05 04:02:03 +00:00
if(window._updateApplyInFlight) return;
window._updateApplyInFlight=true;
const btn=$('btnApplyUpdate');
2026-05-05 04:02:03 +00:00
const resetApplyButton=(delayMs)=>{
const reset=()=>{
window._updateApplyInFlight=false;
if(btn){btn.disabled=false;btn.textContent='Update Now';}
};
if(delayMs>0) setTimeout(reset,delayMs);
else reset();
};
if(btn){btn.disabled=true;btn.textContent='Updating\u2026';}
fix: update banner — conflict recovery path + server self-restart after update (#816) * fix: update banner conflict recovery + server self-restart after update (#813 #814) * fix(update): restart must wait for in-flight update + reset force button on retry Two defects in the update banner flow found during review of PR #816: 1. Two-target race (webui + agent sequential) The client posts targets sequentially: webui succeeds and schedules a restart timer (2 s delay); client then posts agent; server begins agent fetch+pull; at T=2 s the restart timer fires os.execv mid-pull, killing the agent update and closing the client connection. User sees "Update failed (agent): Failed to fetch" even though webui did update, and the agent repo is in an unknown partial state. Fix: _schedule_restart() now blocks on _apply_lock before calling os.execv. If a second update is in flight when the timer fires, the restart thread waits until it completes. If nothing is in flight the lock acquire is instant, so no-op updates still restart immediately. 2. Stale force-update button across retries _showUpdateError sets btnForceUpdate to display:inline-block when res.conflict / res.diverged. Nothing resets it on the next retry, so a subsequent non-conflict error (e.g. network) leaves the stale force button visible pointing at the previous target. Fix: applyUpdates() now hides the force button and clears its data-target at the start of each attempt. Tests: - test_schedule_restart_waits_for_apply_lock: holds _apply_lock from a helper thread, verifies execv is delayed until the lock is released. - test_schedule_restart_still_fires_when_no_update_in_flight: sanity check that the common path still works with no contention. - test_apply_updates_resets_force_button_at_start: regression guard that the reset appears before the update loop begins. Full suite: 1683 passed, 0 failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(update): hold _apply_lock through execv + fix banner error layout Two fixes from Opus review: 1. TOCTOU gap in _schedule_restart (api/updates.py): the original pattern acquired _apply_lock, released it, then called os.execv — leaving a brief window where a new update could start between release and execv. Fixed by moving os.execv inside the 'with _apply_lock:' block so the process is replaced while still holding the lock; no new update can acquire it. 2. Banner CSS layout (static/index.html): #updateError was a direct flex child of .update-banner (display:flex row), so long error messages sat inline between #updateMsg and the buttons instead of below the message. Wrapped #updateMsg + #updateError in a flex-column container so errors stack vertically under the status line. * docs: add v0.50.134 CHANGELOG entry --------- 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-22 00:10:41 +00:00
const errEl=$('updateError');
if(errEl){errEl.style.display='none';errEl.textContent='';}
// Hide any leftover force-update button from a prior conflict so a fresh
// retry starts clean (otherwise stale state points at the wrong target).
const forceBtnReset=$('btnForceUpdate');
if(forceBtnReset){forceBtnReset.style.display='none';forceBtnReset.dataset.target='';}
const targets=[];
if(window._updateData?.webui?.behind>0) targets.push('webui');
if(window._updateData?.agent?.behind>0) targets.push('agent');
try{
for(const target of targets){
const res=await api('/api/updates/apply',{method:'POST',body:JSON.stringify({target})});
if(!res.ok){
fix: update banner — conflict recovery path + server self-restart after update (#816) * fix: update banner conflict recovery + server self-restart after update (#813 #814) * fix(update): restart must wait for in-flight update + reset force button on retry Two defects in the update banner flow found during review of PR #816: 1. Two-target race (webui + agent sequential) The client posts targets sequentially: webui succeeds and schedules a restart timer (2 s delay); client then posts agent; server begins agent fetch+pull; at T=2 s the restart timer fires os.execv mid-pull, killing the agent update and closing the client connection. User sees "Update failed (agent): Failed to fetch" even though webui did update, and the agent repo is in an unknown partial state. Fix: _schedule_restart() now blocks on _apply_lock before calling os.execv. If a second update is in flight when the timer fires, the restart thread waits until it completes. If nothing is in flight the lock acquire is instant, so no-op updates still restart immediately. 2. Stale force-update button across retries _showUpdateError sets btnForceUpdate to display:inline-block when res.conflict / res.diverged. Nothing resets it on the next retry, so a subsequent non-conflict error (e.g. network) leaves the stale force button visible pointing at the previous target. Fix: applyUpdates() now hides the force button and clears its data-target at the start of each attempt. Tests: - test_schedule_restart_waits_for_apply_lock: holds _apply_lock from a helper thread, verifies execv is delayed until the lock is released. - test_schedule_restart_still_fires_when_no_update_in_flight: sanity check that the common path still works with no contention. - test_apply_updates_resets_force_button_at_start: regression guard that the reset appears before the update loop begins. Full suite: 1683 passed, 0 failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(update): hold _apply_lock through execv + fix banner error layout Two fixes from Opus review: 1. TOCTOU gap in _schedule_restart (api/updates.py): the original pattern acquired _apply_lock, released it, then called os.execv — leaving a brief window where a new update could start between release and execv. Fixed by moving os.execv inside the 'with _apply_lock:' block so the process is replaced while still holding the lock; no new update can acquire it. 2. Banner CSS layout (static/index.html): #updateError was a direct flex child of .update-banner (display:flex row), so long error messages sat inline between #updateMsg and the buttons instead of below the message. Wrapped #updateMsg + #updateError in a flex-column container so errors stack vertically under the status line. * docs: add v0.50.134 CHANGELOG entry --------- 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-22 00:10:41 +00:00
_showUpdateError(target,res);
2026-05-05 04:02:03 +00:00
resetApplyButton(0);
return;
}
}
showToast('Update applied — restarting…');
sessionStorage.removeItem('hermes-update-checked');
sessionStorage.removeItem('hermes-update-dismissed');
_waitForServerThenReload();
}catch(e){
2026-05-05 04:02:03 +00:00
const msg=_formatUpdateApplyExceptionMessage(e);
if(errEl){errEl.textContent=msg;errEl.style.display='block';}
else showToast(msg);
resetApplyButton(_isUpdateApplyNetworkError(e)?5000:0);
}
}
fix: update banner — conflict recovery path + server self-restart after update (#816) * fix: update banner conflict recovery + server self-restart after update (#813 #814) * fix(update): restart must wait for in-flight update + reset force button on retry Two defects in the update banner flow found during review of PR #816: 1. Two-target race (webui + agent sequential) The client posts targets sequentially: webui succeeds and schedules a restart timer (2 s delay); client then posts agent; server begins agent fetch+pull; at T=2 s the restart timer fires os.execv mid-pull, killing the agent update and closing the client connection. User sees "Update failed (agent): Failed to fetch" even though webui did update, and the agent repo is in an unknown partial state. Fix: _schedule_restart() now blocks on _apply_lock before calling os.execv. If a second update is in flight when the timer fires, the restart thread waits until it completes. If nothing is in flight the lock acquire is instant, so no-op updates still restart immediately. 2. Stale force-update button across retries _showUpdateError sets btnForceUpdate to display:inline-block when res.conflict / res.diverged. Nothing resets it on the next retry, so a subsequent non-conflict error (e.g. network) leaves the stale force button visible pointing at the previous target. Fix: applyUpdates() now hides the force button and clears its data-target at the start of each attempt. Tests: - test_schedule_restart_waits_for_apply_lock: holds _apply_lock from a helper thread, verifies execv is delayed until the lock is released. - test_schedule_restart_still_fires_when_no_update_in_flight: sanity check that the common path still works with no contention. - test_apply_updates_resets_force_button_at_start: regression guard that the reset appears before the update loop begins. Full suite: 1683 passed, 0 failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(update): hold _apply_lock through execv + fix banner error layout Two fixes from Opus review: 1. TOCTOU gap in _schedule_restart (api/updates.py): the original pattern acquired _apply_lock, released it, then called os.execv — leaving a brief window where a new update could start between release and execv. Fixed by moving os.execv inside the 'with _apply_lock:' block so the process is replaced while still holding the lock; no new update can acquire it. 2. Banner CSS layout (static/index.html): #updateError was a direct flex child of .update-banner (display:flex row), so long error messages sat inline between #updateMsg and the buttons instead of below the message. Wrapped #updateMsg + #updateError in a flex-column container so errors stack vertically under the status line. * docs: add v0.50.134 CHANGELOG entry --------- 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-22 00:10:41 +00:00
function _showUpdateError(target,res){
const errEl=$('updateError');
const forceBtn=$('btnForceUpdate');
const msg='Update failed ('+target+'): '+(res.message||'unknown error');
if(errEl){
errEl.textContent=msg;
errEl.style.display='block';
} else {
showToast(msg);
}
// Show "Force update" button when the error is recoverable by a hard reset
if(forceBtn&&(res.conflict||res.diverged)){
forceBtn.dataset.target=target;
forceBtn.style.display='inline-block';
}
}
async function forceUpdate(btn){
const target=btn&&btn.dataset.target;
if(!target) return;
const confirmed=await showConfirmDialog({
title:'Force update '+target+'?',
message:'This will discard all local changes in the '+target+' repo and reset to the latest remote version. This cannot be undone.',
confirmLabel:'Force update',
danger:true,
focusCancel:true,
});
if(!confirmed) return;
btn.disabled=true;btn.textContent='Force updating\u2026';
const errEl=$('updateError');
if(errEl){errEl.style.display='none';}
try{
const res=await api('/api/updates/force',{method:'POST',body:JSON.stringify({target})});
if(!res.ok){
if(errEl){errEl.textContent='Force update failed: '+(res.message||'unknown error');errEl.style.display='block';}
btn.disabled=false;btn.textContent='Force update';
return;
}
showToast('Force update applied — restarting…');
fix: update banner — conflict recovery path + server self-restart after update (#816) * fix: update banner conflict recovery + server self-restart after update (#813 #814) * fix(update): restart must wait for in-flight update + reset force button on retry Two defects in the update banner flow found during review of PR #816: 1. Two-target race (webui + agent sequential) The client posts targets sequentially: webui succeeds and schedules a restart timer (2 s delay); client then posts agent; server begins agent fetch+pull; at T=2 s the restart timer fires os.execv mid-pull, killing the agent update and closing the client connection. User sees "Update failed (agent): Failed to fetch" even though webui did update, and the agent repo is in an unknown partial state. Fix: _schedule_restart() now blocks on _apply_lock before calling os.execv. If a second update is in flight when the timer fires, the restart thread waits until it completes. If nothing is in flight the lock acquire is instant, so no-op updates still restart immediately. 2. Stale force-update button across retries _showUpdateError sets btnForceUpdate to display:inline-block when res.conflict / res.diverged. Nothing resets it on the next retry, so a subsequent non-conflict error (e.g. network) leaves the stale force button visible pointing at the previous target. Fix: applyUpdates() now hides the force button and clears its data-target at the start of each attempt. Tests: - test_schedule_restart_waits_for_apply_lock: holds _apply_lock from a helper thread, verifies execv is delayed until the lock is released. - test_schedule_restart_still_fires_when_no_update_in_flight: sanity check that the common path still works with no contention. - test_apply_updates_resets_force_button_at_start: regression guard that the reset appears before the update loop begins. Full suite: 1683 passed, 0 failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(update): hold _apply_lock through execv + fix banner error layout Two fixes from Opus review: 1. TOCTOU gap in _schedule_restart (api/updates.py): the original pattern acquired _apply_lock, released it, then called os.execv — leaving a brief window where a new update could start between release and execv. Fixed by moving os.execv inside the 'with _apply_lock:' block so the process is replaced while still holding the lock; no new update can acquire it. 2. Banner CSS layout (static/index.html): #updateError was a direct flex child of .update-banner (display:flex row), so long error messages sat inline between #updateMsg and the buttons instead of below the message. Wrapped #updateMsg + #updateError in a flex-column container so errors stack vertically under the status line. * docs: add v0.50.134 CHANGELOG entry --------- 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-22 00:10:41 +00:00
sessionStorage.removeItem('hermes-update-checked');
sessionStorage.removeItem('hermes-update-dismissed');
_waitForServerThenReload();
fix: update banner — conflict recovery path + server self-restart after update (#816) * fix: update banner conflict recovery + server self-restart after update (#813 #814) * fix(update): restart must wait for in-flight update + reset force button on retry Two defects in the update banner flow found during review of PR #816: 1. Two-target race (webui + agent sequential) The client posts targets sequentially: webui succeeds and schedules a restart timer (2 s delay); client then posts agent; server begins agent fetch+pull; at T=2 s the restart timer fires os.execv mid-pull, killing the agent update and closing the client connection. User sees "Update failed (agent): Failed to fetch" even though webui did update, and the agent repo is in an unknown partial state. Fix: _schedule_restart() now blocks on _apply_lock before calling os.execv. If a second update is in flight when the timer fires, the restart thread waits until it completes. If nothing is in flight the lock acquire is instant, so no-op updates still restart immediately. 2. Stale force-update button across retries _showUpdateError sets btnForceUpdate to display:inline-block when res.conflict / res.diverged. Nothing resets it on the next retry, so a subsequent non-conflict error (e.g. network) leaves the stale force button visible pointing at the previous target. Fix: applyUpdates() now hides the force button and clears its data-target at the start of each attempt. Tests: - test_schedule_restart_waits_for_apply_lock: holds _apply_lock from a helper thread, verifies execv is delayed until the lock is released. - test_schedule_restart_still_fires_when_no_update_in_flight: sanity check that the common path still works with no contention. - test_apply_updates_resets_force_button_at_start: regression guard that the reset appears before the update loop begins. Full suite: 1683 passed, 0 failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(update): hold _apply_lock through execv + fix banner error layout Two fixes from Opus review: 1. TOCTOU gap in _schedule_restart (api/updates.py): the original pattern acquired _apply_lock, released it, then called os.execv — leaving a brief window where a new update could start between release and execv. Fixed by moving os.execv inside the 'with _apply_lock:' block so the process is replaced while still holding the lock; no new update can acquire it. 2. Banner CSS layout (static/index.html): #updateError was a direct flex child of .update-banner (display:flex row), so long error messages sat inline between #updateMsg and the buttons instead of below the message. Wrapped #updateMsg + #updateError in a flex-column container so errors stack vertically under the status line. * docs: add v0.50.134 CHANGELOG entry --------- 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-22 00:10:41 +00:00
}catch(e){
if(errEl){errEl.textContent='Force update failed: '+e.message;errEl.style.display='block';}
btn.disabled=false;btn.textContent='Force update';
}
}
// Poll /health after an update-triggered restart, then reload. Replaces the
// blind setTimeout(reload, 2500) that race-lost against slow hardware or
// reverse proxies that 502 immediately when the upstream socket closes (#874).
async function _waitForServerThenReload(opts){
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
// Polls the /health endpoint; implementation uses a relative URL so subpath mounts keep working.
opts=opts||{};
const interval=opts.interval||500;
const maxMs=opts.maxMs||15000;
window._restartingForUpdate=true;
const msgEl=$('reconnectMsg');
const banner=$('reconnectBanner');
if(msgEl) msgEl.textContent='⏳ Restarting… please wait';
if(banner) banner.classList.add('visible');
const deadline=Date.now()+maxMs;
// Give the server a moment to actually begin its restart before the first
// probe — otherwise the old process may still respond ok on the first poll.
await new Promise(r=>setTimeout(r, interval));
while(Date.now()<deadline){
try{
const r=await fetch(new URL('health', document.baseURI||location.href).href,{cache:'no-store'});
if(r.ok){
let data={};
try{ data=await r.json(); }catch(_){}
if(data && data.status==='ok'){
location.reload();
return;
}
}
}catch(_){ /* socket closed during restart — retry */ }
await new Promise(r=>setTimeout(r, interval));
}
if(msgEl) msgEl.textContent='⚠️ Server is taking longer than expected — click Reload when ready';
}
function getPendingSessionMessage(session){
const text=String(session?.pending_user_message||'').trim();
if(!text) return null;
const attachments=Array.isArray(session?.pending_attachments)?session.pending_attachments.filter(Boolean):[];
const messages=Array.isArray(session?.messages)?session.messages:[];
const lastUser=[...messages].reverse().find(m=>m&&m.role==='user');
if(lastUser){
const lastText=String(msgContent(lastUser)||'').trim();
if(lastText===text){
if(attachments.length&&!lastUser.attachments?.length) lastUser.attachments=attachments;
return null;
}
}
return {
role:'user',
content:text,
attachments:attachments.length?attachments:undefined,
_ts:session?.pending_started_at||Date.now()/1000,
_pending:true,
};
}
async function checkInflightOnBoot(sid) {
const raw = localStorage.getItem(INFLIGHT_KEY);
if (!raw) return;
try {
const {sid: inflightSid, streamId, ts} = JSON.parse(raw);
if (inflightSid !== sid) { clearInflight(); return; }
if (S.activeStreamId && S.activeStreamId === streamId) return;
// Only show banner if the in-flight entry is less than 10 minutes old
if (Date.now() - ts > 10 * 60 * 1000) { clearInflight(); return; }
// Check if stream is still active
const status = await api(`/api/chat/stream/status?stream_id=${encodeURIComponent(streamId || '')}`);
if (status.active) {
// Stream is genuinely still running -- show the banner
showReconnectBanner(t('reconnect_active'));
} else {
// Stream finished. Only show banner if reload happened within 90 seconds
// (longer gap = normal completed session, not a mid-stream reload)
if (Date.now() - ts < 90 * 1000) {
showReconnectBanner(t('reconnect_finished'));
} else {
clearInflight(); // completed normally, no banner needed
}
}
} catch(e) { clearInflight(); }
}
function syncTopbar(){
if(!S.session){
2026-04-06 05:10:59 +00:00
document.title=window._botName||'Hermes';
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(typeof syncWorkspaceDisplays==='function') syncWorkspaceDisplays();
if(typeof syncModelChip==='function') syncModelChip();
2026-04-26 14:47:10 +00:00
if(typeof syncTerminalButton==='function') syncTerminalButton();
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(typeof _syncHermesPanelSessionActions==='function') _syncHermesPanelSessionActions();
else {
const sidebarName=$('sidebarWsName');
if(sidebarName && sidebarName.textContent==='Workspace'){
sidebarName.textContent=t('no_workspace');
}
}
refactor(ui): three-column layout with left rail + main-view migration (#899) refactor(ui): three-column layout with left rail + main-view migration (#899) Unifies the shell into a three-column layout (rail + sidebar + main) matching the hermes-desktop reference, and migrates every per-item detail/edit surface into a shared main-view canvas with consistent headers, empty states, and action buttons. Changes: - New desktop-only left rail (48px) with 8 nav tabs (chat/tasks/skills/memory/workspaces/profiles/todos/settings) - Persistent app titlebar (replaces per-chat topbar), active conversation title shown - All panel detail/create/edit views migrated to #mainSkills, #mainTasks, #mainSettings, #mainWorkspaces, #mainProfiles, #mainMemory - Settings moved out of modal into main-view page; ESC closes it - YAML frontmatter rendered in collapsible <details> block in skill detail - Toasts repositioned from bottom-center to top-right with theme-aware success/error/warning/info variants - Composer workspace chip split into two-button group: files-icon toggles file panel, label opens workspace picker - .settings-menu → .side-menu / .side-menu-item (generalised, shared by memory and settings panels) - i18n: ~25 new keys across en/ru/es/de/zh/zh-Hant for all new form labels, placeholders, and empty states - Mobile: hamburger in titlebar, slide-in sidebar; box-shadow removed from sidebar - New regression test: tests/test_settings_navigation_and_detail_refresh.py (9 tests) Co-authored-by: Aron Prins <pwf.aron@gmail.com>
2026-04-24 16:05:25 +00:00
if(typeof syncAppTitlebar==='function') syncAppTitlebar();
// Update profile chip even when no session is active (e.g. right after profile switch)
const _profileLabel=$('profileChipLabel');
if(_profileLabel) _profileLabel.textContent=S.activeProfile||'default';
return;
}
const sessionTitle=S.session.title||t('untitled');
refactor(ui): three-column layout with left rail + main-view migration (#899) refactor(ui): three-column layout with left rail + main-view migration (#899) Unifies the shell into a three-column layout (rail + sidebar + main) matching the hermes-desktop reference, and migrates every per-item detail/edit surface into a shared main-view canvas with consistent headers, empty states, and action buttons. Changes: - New desktop-only left rail (48px) with 8 nav tabs (chat/tasks/skills/memory/workspaces/profiles/todos/settings) - Persistent app titlebar (replaces per-chat topbar), active conversation title shown - All panel detail/create/edit views migrated to #mainSkills, #mainTasks, #mainSettings, #mainWorkspaces, #mainProfiles, #mainMemory - Settings moved out of modal into main-view page; ESC closes it - YAML frontmatter rendered in collapsible <details> block in skill detail - Toasts repositioned from bottom-center to top-right with theme-aware success/error/warning/info variants - Composer workspace chip split into two-button group: files-icon toggles file panel, label opens workspace picker - .settings-menu → .side-menu / .side-menu-item (generalised, shared by memory and settings panels) - i18n: ~25 new keys across en/ru/es/de/zh/zh-Hant for all new form labels, placeholders, and empty states - Mobile: hamburger in titlebar, slide-in sidebar; box-shadow removed from sidebar - New regression test: tests/test_settings_navigation_and_detail_refresh.py (9 tests) Co-authored-by: Aron Prins <pwf.aron@gmail.com>
2026-04-24 16:05:25 +00:00
const _topbarTitle=$('topbarTitle');if(_topbarTitle)_topbarTitle.textContent=sessionTitle;
2026-04-06 05:10:59 +00:00
document.title=sessionTitle+' \u2014 '+(window._botName||'Hermes');
const vis=S.messages.filter(m=>m&&m.role&&m.role!=='tool');
2026-05-05 00:24:49 +00:00
const _topbarMeta=$('topbarMeta');
if(_topbarMeta){
const sourceLabel=(S.session&&S.session.is_cli_session&&(S.session.source_label||S.session.source_tag||S.session.raw_source))||'';
const metaText=t('n_messages',vis.length);
_topbarMeta.textContent=metaText;
if(sourceLabel){
const badge=document.createElement('span');
badge.className='topbar-source-badge';
badge.textContent=sourceLabel+(S.session.read_only?' · read-only':'');
_topbarMeta.appendChild(document.createTextNode(' '));
_topbarMeta.appendChild(badge);
}
}
refactor(ui): three-column layout with left rail + main-view migration (#899) refactor(ui): three-column layout with left rail + main-view migration (#899) Unifies the shell into a three-column layout (rail + sidebar + main) matching the hermes-desktop reference, and migrates every per-item detail/edit surface into a shared main-view canvas with consistent headers, empty states, and action buttons. Changes: - New desktop-only left rail (48px) with 8 nav tabs (chat/tasks/skills/memory/workspaces/profiles/todos/settings) - Persistent app titlebar (replaces per-chat topbar), active conversation title shown - All panel detail/create/edit views migrated to #mainSkills, #mainTasks, #mainSettings, #mainWorkspaces, #mainProfiles, #mainMemory - Settings moved out of modal into main-view page; ESC closes it - YAML frontmatter rendered in collapsible <details> block in skill detail - Toasts repositioned from bottom-center to top-right with theme-aware success/error/warning/info variants - Composer workspace chip split into two-button group: files-icon toggles file panel, label opens workspace picker - .settings-menu → .side-menu / .side-menu-item (generalised, shared by memory and settings panels) - i18n: ~25 new keys across en/ru/es/de/zh/zh-Hant for all new form labels, placeholders, and empty states - Mobile: hamburger in titlebar, slide-in sidebar; box-shadow removed from sidebar - New regression test: tests/test_settings_navigation_and_detail_refresh.py (9 tests) Co-authored-by: Aron Prins <pwf.aron@gmail.com>
2026-04-24 16:05:25 +00:00
if(typeof syncAppTitlebar==='function') syncAppTitlebar();
fix: model picker correctly updates on profile switch without flicker or raw injection Root cause: three interacting bugs caused the model picker to show the wrong model or flicker after a profile switch. Bug 1 — syncTopbar() fought switchToProfile(). After switchToProfile() set the picker to the profile's model, syncTopbar() was called (via renderSessionList -> loadSession, then explicitly at the end) and overwrote it with S.session.model -- the old session's model. Fix: added S._pendingProfileModel flag. switchToProfile() sets it; syncTopbar() checks it first, applies the override, then clears it. S.session.model is also updated to the resolved value so subsequent syncTopbar() calls are consistent. Bug 2 — Raw option injected at top of list for mismatched model IDs. Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent format: hyphens, no namespace prefix) but the dropdown has 'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix). The old code did sel.value = id, found no match, then injected a new <option> at the top of the list -- creating a lowercase duplicate that didn't match any real provider group entry. Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots, lowercase) and finds the best matching existing option. No new options are ever injected for profile switching. Bug 3 — populateModelDropdown() injected raw option on cold load. Same issue: if default_model from /api/models didn't exactly match a dropdown value, an extra option was added. Fixed by using _applyModelToDropdown() which only selects existing options. New helpers in ui.js: _findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value _applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
// If a profile switch just happened, apply its model rather than the session's stale value.
// S._pendingProfileModel is set by switchToProfile() and cleared here after one application.
const modelOverride=S._pendingProfileModel;
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
let currentModel=S.session.model||'';
fix: model picker correctly updates on profile switch without flicker or raw injection Root cause: three interacting bugs caused the model picker to show the wrong model or flicker after a profile switch. Bug 1 — syncTopbar() fought switchToProfile(). After switchToProfile() set the picker to the profile's model, syncTopbar() was called (via renderSessionList -> loadSession, then explicitly at the end) and overwrote it with S.session.model -- the old session's model. Fix: added S._pendingProfileModel flag. switchToProfile() sets it; syncTopbar() checks it first, applies the override, then clears it. S.session.model is also updated to the resolved value so subsequent syncTopbar() calls are consistent. Bug 2 — Raw option injected at top of list for mismatched model IDs. Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent format: hyphens, no namespace prefix) but the dropdown has 'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix). The old code did sel.value = id, found no match, then injected a new <option> at the top of the list -- creating a lowercase duplicate that didn't match any real provider group entry. Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots, lowercase) and finds the best matching existing option. No new options are ever injected for profile switching. Bug 3 — populateModelDropdown() injected raw option on cold load. Same issue: if default_model from /api/models didn't exactly match a dropdown value, an extra option was added. Fixed by using _applyModelToDropdown() which only selects existing options. New helpers in ui.js: _findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value _applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
if(modelOverride){
S._pendingProfileModel=null;
const providerOverride=S._pendingProfileModelProvider||null;
S._pendingProfileModelProvider=null;
_applyModelToDropdown(modelOverride,$('modelSelect'),providerOverride);
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
currentModel=modelOverride;
fix: model picker correctly updates on profile switch without flicker or raw injection Root cause: three interacting bugs caused the model picker to show the wrong model or flicker after a profile switch. Bug 1 — syncTopbar() fought switchToProfile(). After switchToProfile() set the picker to the profile's model, syncTopbar() was called (via renderSessionList -> loadSession, then explicitly at the end) and overwrote it with S.session.model -- the old session's model. Fix: added S._pendingProfileModel flag. switchToProfile() sets it; syncTopbar() checks it first, applies the override, then clears it. S.session.model is also updated to the resolved value so subsequent syncTopbar() calls are consistent. Bug 2 — Raw option injected at top of list for mismatched model IDs. Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent format: hyphens, no namespace prefix) but the dropdown has 'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix). The old code did sel.value = id, found no match, then injected a new <option> at the top of the list -- creating a lowercase duplicate that didn't match any real provider group entry. Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots, lowercase) and finds the best matching existing option. No new options are ever injected for profile switching. Bug 3 — populateModelDropdown() injected raw option on cold load. Same issue: if default_model from /api/models didn't exactly match a dropdown value, an extra option was added. Fixed by using _applyModelToDropdown() which only selects existing options. New helpers in ui.js: _findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value _applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
} else {
const applied=_applyModelToDropdown(currentModel,$('modelSelect'),S.session.model_provider||null);
fix(models): stale cross-provider model no longer shows as unavailable in picker (closes #829) * fix(models): stale cross-provider model no longer shows as unavailable in picker Two bugs allowed an openai/gpt-5.4-mini stale session model to appear as '(unavailable)' under a custom provider group for users who never configured OpenAI (#829). Backend (api/routes.py): _resolve_compatible_session_model() had a blanket early-return for active_provider in {custom, openrouter} that skipped all normalization regardless of whether any catalog group could route the model's prefix. A custom_providers-only user with a stale openai/... session model was never corrected. Fixed: only skip normalization when the model prefix is actually routable (matches a catalog group provider_id, or an openrouter group is present that can route any provider/model). Frontend (static/ui.js): renderSession() injected a bare <option> (not in any <optgroup>) for models not found in the dropdown. renderModelDropdown() rendered bare options without emitting a group heading, so they visually inherited the last rendered provider heading — making the stale model appear to belong to the custom provider group. Fixed: silently reset to the first available model and fire a PATCH to persist the correction instead of injecting a misleading (unavailable) option. 5 new tests in test_provider_mismatch.py cover: - stale openai model cleared when custom_providers-only + no default_model - stale openai model cleared when custom_providers-only + default_model set - openrouter model preserved when openrouter group present - custom/ namespace always preserved - ui.js no longer injects model_unavailable option * fix(ui): declare modelSel locally in syncTopbar reset path; fix test assertion - Use const modelSel=$('modelSelect') instead of undeclared sel in the stale-model reset branch of syncTopbar() (caught in Opus review) - Fix test assertion: or → and for model_unavailable key absence check --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-22 05:20:08 +00:00
// If the model isn't in the current provider list, silently reset to the
// first available model so stale values don't pollute the picker (#829).
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(!applied && currentModel){
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031) * fix: remove orphaned i18n keys from top-level LOCALES object Three Traditional Chinese translation keys (cmd_status, memory_saved, profile_delete_title) were placed outside any locale block between the en and ru blocks in static/i18n.js. They became top-level properties of the LOCALES object, causing them to appear as invalid language options in the Settings > Preferences dropdown. The correct translations already exist in the zh-Hant locale block. Fixes #1008 * fix: block stale SSE events from polluting new session's DOM - appendThinking(): guard with !S.session||!S.activeStreamId to drop events from a previous session's SSE stream during a session switch - appendLiveToolCard(): same guard for consistency - finalizeThinkingCard(): scroll thinking-card-body to top when scroll is pinned, so completed response is immediately visible - appendThinking(): auto-scroll thinking card body to bottom while streaming if user is watching (scroll pinned) * Fix empty agent sessions in sidebar * fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status Fixes #995 — three sub-issues in the Cron Jobs UI: 1. Dual play icons ambiguous: Resume button now shows a distinct play+bar icon (play triangle + vertical line) instead of the identical triangle used by Run now. 2. Toast notification overlapping header buttons: Added position:relative; z-index:10 to .main-view-header so it stacks above the fixed toast (z-index:100 within its layer). 3. No running status after trigger: After triggering a job, the status badge immediately shows 'running…' with a CSS spinner animation, and polls the cron list every 3s (up to 30s) to refresh when the job completes. - Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant) - Added .detail-badge.running CSS class with spinner animation - New functions: _setCronDetailStatus(), _startCronRunningPoll() * fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback - _clearCronDetail() now clears _cronRunningPoll interval on navigation - Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker) - When poll ends (30s max), detail re-renders with actual status as fallback * feat: create folder and add space directly from UI (#782) - After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog - Add Create folder if it doesnt exist checkbox in the New Space form - Backend: support create flag in /api/workspaces/add to mkdir before validation - i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales * fix: validate workspace path before mkdir to prevent orphan directories Review feedback (critical): the previous code called mkdir() before validate_workspace_to_add(), which meant a rejected path (e.g. system dir) would leave an orphan directory on disk. New flow: 1. Resolve path and check against blocked system roots BEFORE any mutation 2. mkdir() only if path passes the blocklist check 3. Full validation (exists, is_dir) after mkdir Also imports _workspace_blocked_roots for the pre-mutation blocklist check. * fix(#1014): classify model-not-found errors with helpful message - Add model_not_found error type to streaming.py exception classifier - Detect 404, 'not found', 'does not exist', 'invalid model' patterns - Strip HTML tags from provider error messages (nginx 404 pages, etc.) - Add model_not_found branch to apperror handler in messages.js - Add i18n key model_not_found_label in all 6 locales - 15 tests covering detection, sanitization, frontend, and i18n * feat(ui): add live TPS stat to header Adds a TPS (Tokens Per Second) chip to the right of the header title bar that updates live while AI output is streaming. Metering (api/metering.py) - Tracks per-session output + reasoning tokens via GlobalMeter singleton - Per-session TPS = total_tokens / elapsed_time - Global TPS = average of active sessions' TPS values - HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling window (only recorded when > 0, so idle periods are excluded) - Thread-safe with a single lock Metering events emitted from streaming.py - Throttled at 100ms from token/reasoning/tool callbacks so the display updates rapidly during fast token streams - 1Hz ticker as fallback for slow streams (exits when no active sessions) - Final stats emitted on stream end Routes (api/routes.py) - Removed POST /api/metering/interval endpoint (dynamic interval via focus/blur was replaced with simple always-1s-when-active approach) UI (static/messages.js, index.html, style.css) - TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low' - Default: '0.0 t/s . 0.0 high' when idle - Display updates on every metering SSE event (throttled to 100ms) * feat: session restore speed + title gen reasoning hardening (#1025, #1026) PR #1025 (@franksong2702): Speed up large session restore paths - GET /api/session?messages=0 now parses only metadata before the messages array - Metadata-only loads no longer populate the full-session LRU cache - Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup - Hard reload no longer waits for populateModelDropdown() before restoring session PR #1026 (@franksong2702): Harden auto title generation for reasoning models - Raises title-gen completion budget to 512 tokens (reasoning-safe) - Retries once with 1024 tokens on empty content / finish_reason:length - Applies retry to both auxiliary and active-agent fallback routes - Preserves underlying failure reason in title_status on local fallback Co-authored-by: Frank Song <franksong2702@gmail.com> * feat: session attention indicators in right slot + last_message_at timestamps (#1024) PR #1024 (@franksong2702): Polish session attention indicators - Streaming spinners and unread dots now reuse the right-side actions slot - Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps - Date group carets point down when expanded, right when collapsed - Pinned group no longer repeats pinned-star icon per row - Running indicators appear immediately after send (local busy state while /api/sessions catches up) - Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message) so metadata-only saves don't make old sessions appear under Today Co-authored-by: Frank Song <franksong2702@gmail.com> * docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36) --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Josh <josh@fyul.link> Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
const deferModelCorrection=Boolean(S.session._modelResolutionDeferred);
// Also defer if a live model fetch is still in flight — the model may be
// in the list once the fetch completes. Persisting now would corrupt the
// session with the wrong model before live models arrive (#1169).
const liveStillPending=window._activeProvider&&_liveModelFetchPending.has(window._activeProvider);
if(liveStillPending){
// Live fetch in flight — don't touch sel.value or S.session.model yet.
// _addLiveModelsToSelect() will re-apply S.session.model once done (#1169).
} else {
// Stale session model not in the current provider catalog — reset to the
// first available model rather than injecting an "(unavailable)" option
// that visually appears under the wrong provider group (#829).
const modelSel=$('modelSelect');
const first=modelSel&&modelSel.querySelector('optgroup > option, option');
if(first){
modelSel.value=first.value;
if(!deferModelCorrection){
S.session.model=first.value;
S.session.model_provider=_getOptionProviderId(first)||null;
// Persist the correction so the session doesn't re-inject on next load.
fetch(new URL('api/session/update',document.baseURI||location.href).href,{
method:'POST',credentials:'include',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({session_id:S.session.id||S.session.session_id,model:first.value,model_provider:S.session.model_provider||null})
}).catch(()=>{});
}
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031) * fix: remove orphaned i18n keys from top-level LOCALES object Three Traditional Chinese translation keys (cmd_status, memory_saved, profile_delete_title) were placed outside any locale block between the en and ru blocks in static/i18n.js. They became top-level properties of the LOCALES object, causing them to appear as invalid language options in the Settings > Preferences dropdown. The correct translations already exist in the zh-Hant locale block. Fixes #1008 * fix: block stale SSE events from polluting new session's DOM - appendThinking(): guard with !S.session||!S.activeStreamId to drop events from a previous session's SSE stream during a session switch - appendLiveToolCard(): same guard for consistency - finalizeThinkingCard(): scroll thinking-card-body to top when scroll is pinned, so completed response is immediately visible - appendThinking(): auto-scroll thinking card body to bottom while streaming if user is watching (scroll pinned) * Fix empty agent sessions in sidebar * fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status Fixes #995 — three sub-issues in the Cron Jobs UI: 1. Dual play icons ambiguous: Resume button now shows a distinct play+bar icon (play triangle + vertical line) instead of the identical triangle used by Run now. 2. Toast notification overlapping header buttons: Added position:relative; z-index:10 to .main-view-header so it stacks above the fixed toast (z-index:100 within its layer). 3. No running status after trigger: After triggering a job, the status badge immediately shows 'running…' with a CSS spinner animation, and polls the cron list every 3s (up to 30s) to refresh when the job completes. - Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant) - Added .detail-badge.running CSS class with spinner animation - New functions: _setCronDetailStatus(), _startCronRunningPoll() * fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback - _clearCronDetail() now clears _cronRunningPoll interval on navigation - Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker) - When poll ends (30s max), detail re-renders with actual status as fallback * feat: create folder and add space directly from UI (#782) - After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog - Add Create folder if it doesnt exist checkbox in the New Space form - Backend: support create flag in /api/workspaces/add to mkdir before validation - i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales * fix: validate workspace path before mkdir to prevent orphan directories Review feedback (critical): the previous code called mkdir() before validate_workspace_to_add(), which meant a rejected path (e.g. system dir) would leave an orphan directory on disk. New flow: 1. Resolve path and check against blocked system roots BEFORE any mutation 2. mkdir() only if path passes the blocklist check 3. Full validation (exists, is_dir) after mkdir Also imports _workspace_blocked_roots for the pre-mutation blocklist check. * fix(#1014): classify model-not-found errors with helpful message - Add model_not_found error type to streaming.py exception classifier - Detect 404, 'not found', 'does not exist', 'invalid model' patterns - Strip HTML tags from provider error messages (nginx 404 pages, etc.) - Add model_not_found branch to apperror handler in messages.js - Add i18n key model_not_found_label in all 6 locales - 15 tests covering detection, sanitization, frontend, and i18n * feat(ui): add live TPS stat to header Adds a TPS (Tokens Per Second) chip to the right of the header title bar that updates live while AI output is streaming. Metering (api/metering.py) - Tracks per-session output + reasoning tokens via GlobalMeter singleton - Per-session TPS = total_tokens / elapsed_time - Global TPS = average of active sessions' TPS values - HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling window (only recorded when > 0, so idle periods are excluded) - Thread-safe with a single lock Metering events emitted from streaming.py - Throttled at 100ms from token/reasoning/tool callbacks so the display updates rapidly during fast token streams - 1Hz ticker as fallback for slow streams (exits when no active sessions) - Final stats emitted on stream end Routes (api/routes.py) - Removed POST /api/metering/interval endpoint (dynamic interval via focus/blur was replaced with simple always-1s-when-active approach) UI (static/messages.js, index.html, style.css) - TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low' - Default: '0.0 t/s . 0.0 high' when idle - Display updates on every metering SSE event (throttled to 100ms) * feat: session restore speed + title gen reasoning hardening (#1025, #1026) PR #1025 (@franksong2702): Speed up large session restore paths - GET /api/session?messages=0 now parses only metadata before the messages array - Metadata-only loads no longer populate the full-session LRU cache - Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup - Hard reload no longer waits for populateModelDropdown() before restoring session PR #1026 (@franksong2702): Harden auto title generation for reasoning models - Raises title-gen completion budget to 512 tokens (reasoning-safe) - Retries once with 1024 tokens on empty content / finish_reason:length - Applies retry to both auxiliary and active-agent fallback routes - Preserves underlying failure reason in title_status on local fallback Co-authored-by: Frank Song <franksong2702@gmail.com> * feat: session attention indicators in right slot + last_message_at timestamps (#1024) PR #1024 (@franksong2702): Polish session attention indicators - Streaming spinners and unread dots now reuse the right-side actions slot - Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps - Date group carets point down when expanded, right when collapsed - Pinned group no longer repeats pinned-star icon per row - Running indicators appear immediately after send (local busy state while /api/sessions catches up) - Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message) so metadata-only saves don't make old sessions appear under Today Co-authored-by: Frank Song <franksong2702@gmail.com> * docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36) --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Josh <josh@fyul.link> Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
}
fix(models): stale cross-provider model no longer shows as unavailable in picker (closes #829) * fix(models): stale cross-provider model no longer shows as unavailable in picker Two bugs allowed an openai/gpt-5.4-mini stale session model to appear as '(unavailable)' under a custom provider group for users who never configured OpenAI (#829). Backend (api/routes.py): _resolve_compatible_session_model() had a blanket early-return for active_provider in {custom, openrouter} that skipped all normalization regardless of whether any catalog group could route the model's prefix. A custom_providers-only user with a stale openai/... session model was never corrected. Fixed: only skip normalization when the model prefix is actually routable (matches a catalog group provider_id, or an openrouter group is present that can route any provider/model). Frontend (static/ui.js): renderSession() injected a bare <option> (not in any <optgroup>) for models not found in the dropdown. renderModelDropdown() rendered bare options without emitting a group heading, so they visually inherited the last rendered provider heading — making the stale model appear to belong to the custom provider group. Fixed: silently reset to the first available model and fire a PATCH to persist the correction instead of injecting a misleading (unavailable) option. 5 new tests in test_provider_mismatch.py cover: - stale openai model cleared when custom_providers-only + no default_model - stale openai model cleared when custom_providers-only + default_model set - openrouter model preserved when openrouter group present - custom/ namespace always preserved - ui.js no longer injects model_unavailable option * fix(ui): declare modelSel locally in syncTopbar reset path; fix test assertion - Use const modelSel=$('modelSelect') instead of undeclared sel in the stale-model reset branch of syncTopbar() (caught in Opus review) - Fix test assertion: or → and for model_unavailable key absence check --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-22 05:20:08 +00:00
}
fix: model picker correctly updates on profile switch without flicker or raw injection Root cause: three interacting bugs caused the model picker to show the wrong model or flicker after a profile switch. Bug 1 — syncTopbar() fought switchToProfile(). After switchToProfile() set the picker to the profile's model, syncTopbar() was called (via renderSessionList -> loadSession, then explicitly at the end) and overwrote it with S.session.model -- the old session's model. Fix: added S._pendingProfileModel flag. switchToProfile() sets it; syncTopbar() checks it first, applies the override, then clears it. S.session.model is also updated to the resolved value so subsequent syncTopbar() calls are consistent. Bug 2 — Raw option injected at top of list for mismatched model IDs. Profile configs store model IDs like 'claude-sonnet-4-6' (hermes-agent format: hyphens, no namespace prefix) but the dropdown has 'anthropic/claude-sonnet-4.6' (OpenRouter format: dots, with prefix). The old code did sel.value = id, found no match, then injected a new <option> at the top of the list -- creating a lowercase duplicate that didn't match any real provider group entry. Fix: _findModelInDropdown() normalises both sides (strip prefix, hyphens->dots, lowercase) and finds the best matching existing option. No new options are ever injected for profile switching. Bug 3 — populateModelDropdown() injected raw option on cold load. Same issue: if default_model from /api/models didn't exactly match a dropdown value, an extra option was added. Fixed by using _applyModelToDropdown() which only selects existing options. New helpers in ui.js: _findModelInDropdown(modelId, sel) -- smart fuzzy match, returns matched value _applyModelToDropdown(modelId, sel) -- sets picker, returns resolved value Tests: 426 passed, 0 failed.
2026-04-03 20:10:47 +00:00
}
2026-03-31 07:02:47 +00:00
}
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(typeof syncModelChip==='function') syncModelChip();
if(typeof syncReasoningChip==='function') syncReasoningChip();
if(typeof syncToolsetsChip==='function') syncToolsetsChip();
// Show Clear button only when session has messages
const clearBtn=$('btnClearConv');
2026-03-31 07:02:47 +00:00
if(clearBtn) clearBtn.style.display=(S.messages&&S.messages.filter(msg=>msg.role!=='tool').length>0)?'':'none';
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(typeof _syncHermesPanelSessionActions==='function') _syncHermesPanelSessionActions();
if(typeof syncWorkspaceDisplays==='function') syncWorkspaceDisplays();
2026-04-26 14:47:10 +00:00
if(typeof syncTerminalButton==='function') syncTerminalButton();
// modelSelect already set above
// Update profile chip label
const profileLabel=$('profileChipLabel');
if(profileLabel) profileLabel.textContent=S.activeProfile||'default';
}
function msgContent(m){
// Extract plain text content from a message for filtering
let c=m.content||'';
if(Array.isArray(c))c=c.filter(p=>p&&p.type==='text').map(p=>p.text||'').join('').trim();
return String(c).trim();
}
function _fmtDateSep(d){
const todayStart=new Date();todayStart.setHours(0,0,0,0);
const dStart=new Date(d);dStart.setHours(0,0,0,0);
const diffDays=Math.round((todayStart-dStart)/86400000);
if(diffDays===0) return 'Today';
if(diffDays===1) return 'Yesterday';
if(diffDays>0 && diffDays<7) return dStart.toLocaleDateString([], {weekday:'long'});
const opts={month:'short', day:'numeric'};
if(todayStart.getFullYear()!==dStart.getFullYear()) opts.year='numeric';
return dStart.toLocaleDateString([], opts);
}
const _ERR_MSG_RE=/^(?:\*\*error\b|error:|connection lost|no response received)/i;
function _messageHasReasoningPayload(m){
if(!m||m.role!=='assistant') return false;
if(m.reasoning) return true;
if(Array.isArray(m.content)) return m.content.some(p=>p&&(p.type==='thinking'||p.type==='reasoning'));
return /<think>[\s\S]*?<\/think>|<\|channel>thought\n[\s\S]*?<channel\|>|<\|turn\|>thinking\n[\s\S]*?<turn\|>/.test(String(m.content||''));
}
function _formatTurnTps(value){
const n=Number(value);
if(!Number.isFinite(n)||n<=0) return '';
const fixed=n>=100?Math.round(n).toLocaleString():n>=10?n.toFixed(1):n.toFixed(1);
return `${fixed} t/s`;
}
2026-05-04 19:08:07 +00:00
function isTpsDisplayEnabled(){
return window._showTps===true;
}
function _assistantRoleHtml(tsTitle='', tpsText=''){
const _bn=window._botName||'Hermes';
2026-05-04 19:08:07 +00:00
const tps=(isTpsDisplayEnabled()&&tpsText)?`<span class="msg-tps-inline" title="Tokens per second">${esc(tpsText)}</span>`:'';
return `<div class="msg-role assistant" ${tsTitle?`title="${esc(tsTitle)}"`:''}><div class="role-icon assistant">${esc(_bn.charAt(0).toUpperCase())}</div><span style="font-size:12px">${esc(_bn)}</span>${tps}</div>`;
}
function _setAssistantTurnTps(turn, tpsText=''){
if(!turn) return;
const role=turn.querySelector('.msg-role.assistant');
if(!role) return;
let chip=role.querySelector('.msg-tps-inline');
const text=String(tpsText||'').trim();
if(!text){if(chip) chip.remove();return;}
if(!chip){
chip=document.createElement('span');
chip.className='msg-tps-inline';
chip.title='Tokens per second';
role.appendChild(chip);
}
chip.textContent=text;
}
function _setLiveAssistantTps(value){
2026-05-04 19:08:07 +00:00
_setAssistantTurnTps($('liveAssistantTurn'), isTpsDisplayEnabled()?_formatTurnTps(value):'');
}
function _createAssistantTurn(tsTitle='', tpsText=''){
const row=document.createElement('div');
row.className='msg-row assistant-turn';
row.dataset.role='assistant';
row.innerHTML=`${_assistantRoleHtml(tsTitle, tpsText)}<div class="assistant-turn-blocks"></div>`;
return row;
}
function _assistantTurnBlocks(turn){
return turn?turn.querySelector('.assistant-turn-blocks'):null;
}
function _thinkingCardHtml(text){
const clean=_sanitizeThinkingDisplayText(text);
return `<div class="thinking-card"><div class="thinking-card-header" onclick="this.parentElement.classList.toggle('open')"><span class="thinking-card-icon">${li('lightbulb',14)}</span><span class="thinking-card-label">${t('thinking')}</span><span class="thinking-card-toggle">${li('chevron-right',12)}</span></div><div class="thinking-card-body"><pre>${esc(clean)}</pre></div></div>`;
}
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
function isSimplifiedToolCalling(){
return window._simplifiedToolCalling!==false;
}
function _thinkingActivityNode(text){
const row=document.createElement('div');
row.className='agent-activity-thinking';
row.innerHTML=_thinkingCardHtml(text);
return row;
}
// ── Activity-group user expand intent (#1298) ──────────────────────────────
// When the user manually expands the live "Activity" dropdown during streaming,
// preserve that intent across the destroy/recreate cycle that fires on every
// thinking/tool event. Without this, ensureActivityGroup() re-creates the group
// with the default collapsed state and finalizeThinkingCard() force-collapses
// it whenever the assistant transitions from thinking → tool → thinking, so
// the panel snaps shut every few seconds while the user is trying to read it.
//
// The tracker is a singleton boolean: there is at most one live activity group
// at a time (selector .tool-call-group[data-live-tool-call-group="1"]). It is
// set to true when the user clicks the summary to expand, false when they
// click to collapse, and cleared back to undefined when the live group is
// finalized into a settled assistant turn (the live attribute is removed in
// _convertLiveActivityGroupToSettled / when liveAssistantTurn loses its id).
let _liveActivityUserExpanded;
function _onLiveActivityToggle(group){
if(!group) return;
// Only track explicit user clicks on the live group, not programmatic toggles.
if(group.getAttribute('data-live-tool-call-group')!=='1') return;
_liveActivityUserExpanded = !group.classList.contains('tool-call-group-collapsed');
}
function _clearLiveActivityUserIntent(){
_liveActivityUserExpanded = undefined;
}
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
function ensureActivityGroup(inner, opts){
opts=opts||{};
if(!inner) return null;
const live=!!opts.live;
const selector=live?'.tool-call-group[data-live-tool-call-group="1"]':'.tool-call-group[data-agent-activity-group="1"]';
let group=inner.querySelector(selector);
if(!group){
group=document.createElement('div');
let collapsed=opts.collapsed!==false;
// Restore the user's explicit expand intent when recreating the live
// activity group within the same turn (#1298).
if(live && _liveActivityUserExpanded === true) collapsed=false;
else if(live && _liveActivityUserExpanded === false) collapsed=true;
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
group.className='tool-call-group agent-activity-group'+(collapsed?' tool-call-group-collapsed':'');
group.setAttribute('data-tool-call-group','1');
group.setAttribute('data-agent-activity-group','1');
if(live) group.setAttribute('data-live-tool-call-group','1');
2026-05-04 03:14:49 +00:00
group.innerHTML=`<button type="button" class="tool-call-group-summary" aria-expanded="${collapsed?'false':'true'}" onclick="const g=this.closest('.tool-call-group');const c=g.classList.toggle('tool-call-group-collapsed');this.setAttribute('aria-expanded',String(!c));if(typeof _onLiveActivityToggle==='function')_onLiveActivityToggle(g);"><span class="tool-call-group-chevron">${li('chevron-right',12)}</span><span class="tool-call-group-label">Activity</span><span class="tool-call-group-list">tools / thinking</span><span class="tool-call-group-duration"></span><span class="tool-call-group-count">0</span></button><div class="tool-call-group-body"></div>`;
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const anchor=opts.anchor||null;
if(anchor&&anchor.parentElement===inner) anchor.insertAdjacentElement('afterend', group);
else inner.appendChild(group);
}
_syncToolCallGroupSummary(group);
return group;
}
function _compressionStateForCurrentSession(){
const state=window._compressionUi;
if(!state||!S.session||state.sessionId!==S.session.session_id) return null;
return state;
}
function isCompressionUiRunning(){
const state=_compressionStateForCurrentSession();
const lock=_compressionSessionLock();
return !!((state&&state.phase==='running') || (lock && S.session && lock===S.session.session_id));
}
function clearCompressionUi(){
window._compressionUi=null;
_setCompressionSessionLock(null);
renderCompressionUi();
}
function setCompressionUi(state){
if(!state){
clearCompressionUi();
return;
}
window._compressionUi={...state};
if(state.sessionId) _setCompressionSessionLock(state.sessionId);
renderCompressionUi();
}
function _compressionCardsHtml(state){
if(!state) return '';
if(state.automatic) return _autoCompressionCardsHtml(state);
const cmdText=state.commandText||'/compress';
const focusText=state.focusTopic?`${t('focus_label')}: ${state.focusTopic}`:'';
const headerText=state.phase==='done'
? (state.summary?.headline||t('compress_complete_label'))
: state.phase==='error'
? (state.errorText||t('compress_failed_label'))
: (typeof state.beforeCount==='number' ? t('n_messages', state.beforeCount) : '');
const statusBody=state.phase==='error'
? [state.errorText||t('compress_failed_label'), focusText].filter(Boolean).join('\n')
: [t('compressing'), focusText].filter(Boolean).join('\n');
const statusLabel=state.phase==='done'
? t('compress_complete_label')
: state.phase==='error'
? t('compress_failed_label')
: t('compress_running_label');
const statusIcon=state.phase==='done'
? li('check',13)
: state.phase==='error'
? li('x',13)
: `<span class="tool-card-running-dot"></span>`;
const doneCardHtml=state.phase==='done'
? _compressionStatusCardHtml({
statusLabel,
previewText: headerText,
detail: [state.summary?.token_line, state.summary?.note, focusText].filter(Boolean).join('\n'),
icon: statusIcon,
open: true,
variantClass: 'tool-card-compress-complete',
})
: '';
const referenceHtml=(state.phase==='done'&&state.referenceText)
? _compressionReferenceCardHtml(state.referenceText, false)
: '';
return `
<div class="tool-card-row compression-card-row" data-compression-card="1">
<div class="tool-card tool-card-compress-command">
<div class="tool-card-header" onclick="this.closest('.tool-card').classList.toggle('open')">
<span class="tool-card-icon">${li('settings',13)}</span>
<span class="tool-card-name">${esc(t('command_label'))}</span>
<span class="tool-card-preview">${esc(cmdText)}</span>
</div>
</div>
</div>
<div class="tool-card-row compression-card-row" data-compression-card="1">
${state.phase==='done'
? doneCardHtml
: _compressionStatusCardHtml({
statusLabel,
previewText: headerText,
detail: statusBody,
icon: statusIcon,
open: false,
variantClass: state.phase==='error'
? 'tool-card-compress-error'
: 'tool-card-compress-running',
})
}
</div>
${referenceHtml}`;
}
function _autoCompressionCardsHtml(state){
const fallback='Context auto-compressed to continue the conversation';
const detail=String(state.message||fallback).trim()||fallback;
const preview=String(state.summary?.headline||detail).trim()||detail;
return `
<div class="tool-card-row compression-card-row" data-compression-card="1">
${_compressionStatusCardHtml({
statusLabel: t('auto_compress_label'),
previewText: preview,
detail,
icon: li('check',13),
open: false,
variantClass: 'tool-card-compress-complete tool-card-compress-auto',
})}
</div>`;
}
function _compressionCardsNode(state){
const wrap=document.createElement('div');
wrap.className='compression-turn';
wrap.innerHTML=`<div class="compression-turn-blocks">${_compressionCardsHtml(state)}</div>`;
return wrap;
}
2026-05-03 04:59:07 +00:00
function _isHandoffSummaryToolPayload(value){
if(!value||typeof value!=='object'||Array.isArray(value)) return false;
return value._handoff_summary_card === true;
}
function _parseHandoffSummaryPayload(content){
if(!content) return null;
if(typeof content==='object' && !Array.isArray(content)) return _isHandoffSummaryToolPayload(content)?content:null;
if(typeof content!=='string') return null;
try {
const parsed=JSON.parse(content);
return _isHandoffSummaryToolPayload(parsed)?parsed:null;
} catch (e) {
return null;
}
}
function _handoffSummaryStateFromMessage(m){
if(!m||m.role!=='tool') return null;
const payload = _parseHandoffSummaryPayload(m.content);
if(!payload) return null;
if(String(payload.session_id||'') && S.session && String(m.session_id||'') && String(payload.session_id)!==String(S.session.session_id||'')) {
return null;
}
const summary = String(payload.summary||'').trim();
if(!summary) return null;
return {
phase: 'done',
channel: payload.channel || null,
rounds: Number.isFinite(payload.rounds)?payload.rounds:null,
summary,
fallback: !!payload.fallback,
generatedAt: Number(payload.generated_at) || null,
};
}
function _collectHandoffSummaryStates(messages){
const states=[];
if(!Array.isArray(messages)) return states;
for(let i=0;i<messages.length;i++){
const state=_handoffSummaryStateFromMessage(messages[i]);
if(state) states.push({state, rawIdx:i});
}
return states;
}
function _isContextCompactionMessage(m){
if(!m||!m.role||m.role==='tool') return false;
const text=msgContent(m)||String(m.content||'');
return /^\s*\[context compaction/i.test(text) || /^\s*context compaction/i.test(text);
}
function _isPreservedCompressionTaskListMessage(m){
if(!m||m.role!=='user') return false;
const text=msgContent(m)||String(m.content||'');
return /^\s*\[your active task list was preserved across context compression\]/i.test(text);
}
function _preservedCompressionTaskListPreview(text){
const body=String(text||'')
.replace(/^\s*\[your active task list was preserved across context compression\]\s*/i,'')
.trim();
return (body.split(/\n+/).map(line=>line.trim()).filter(Boolean).slice(0,2).join(' ') || t('preserved_task_list_label'));
}
function _compressionMessageAnchorKey(m){
if(!m||!m.role||m.role==='tool') return null;
let content='';
try{
content=String(msgContent(m)||'');
}catch(_){
content=String(m.content||'');
}
const norm=content.replace(/\s+/g,' ').trim().slice(0,160);
const ts=m._ts||m.timestamp||null;
const attachments=Array.isArray(m.attachments)?m.attachments.length:0;
if(!norm && !attachments && !ts) return null;
return {role:String(m.role||''), ts, text:norm, attachments};
}
function _compressionAnchorIndex(visWithIdx, anchorKey, fallbackIdx=null){
if(anchorKey&&Array.isArray(visWithIdx)){
for(let i=visWithIdx.length-1;i>=0;i--){
const candidate=_compressionMessageAnchorKey(visWithIdx[i].m);
if(!candidate) continue;
if(
candidate.role===String(anchorKey.role||'') &&
String(candidate.ts??'')===String(anchorKey.ts??'') &&
String(candidate.text||'')===String(anchorKey.text||'') &&
Number(candidate.attachments||0)===Number(anchorKey.attachments||0)
){
return i;
}
}
}
return typeof fallbackIdx==='number' ? fallbackIdx : null;
}
function _compressionReferenceCardHtml(text, open=false){
const preview=text.split(/\n+/).filter(Boolean).slice(0,2).join(' ');
return `
<div class="tool-card-row compression-card-row" data-compression-card="1" data-raw-text="${esc(text)}">
<div class="tool-card tool-card-compress-reference${open?' open':''}">
<div class="tool-card-header" onclick="this.closest('.tool-card').classList.toggle('open')">
<span class="tool-card-icon">${li('star',13)}</span>
<span class="tool-card-name">${esc(t('context_compaction_label'))}</span>
<span class="tool-card-preview">${esc(t('reference_only_label'))} · ${esc(preview)}</span>
<span class="tool-card-toggle">${li('chevron-right',12)}</span>
<button class="msg-copy-btn msg-action-btn tool-card-copy compression-reference-copy" title="${t('copy')}" onclick="copyMsg(this);event.stopPropagation()">${li('copy',13)}</button>
</div>
<div class="tool-card-detail">
<div class="tool-card-result">
<pre>${esc(text)}</pre>
</div>
</div>
</div>
</div>`;
}
function _preservedCompressionTaskListCardHtml(m, open=false){
const text=msgContent(m)||String(m.content||'');
return `
<div class="tool-card-row compression-card-row" data-compression-card="1" data-raw-text="${esc(text)}">
${_compressionStatusCardHtml({
statusLabel: t('preserved_task_list_label'),
previewText: _preservedCompressionTaskListPreview(text),
detail: text,
icon: li('list-todo',13),
open,
variantClass: 'tool-card-compress-reference',
})}
</div>`;
}
function _preservedCompressionTaskListCardsHtml(messages){
return (messages||[]).map(m=>_preservedCompressionTaskListCardHtml(m, false)).join('');
}
function _latestPreservedCompressionTaskListMessages(messages){
const latest=[...(messages||[])].reverse().find(m=>_isPreservedCompressionTaskListMessage(m));
return latest?[latest]:[];
}
function _isSameLocalDay(dateA, dateB){
return dateA.getFullYear()===dateB.getFullYear()
&& dateA.getMonth()===dateB.getMonth()
&& dateA.getDate()===dateB.getDate();
}
function _formatMessageFooterTimestamp(tsVal){
if(!tsVal) return '';
const date=new Date(tsVal*1000);
const now=new Date();
// Use _formatInServerTz when available — it correctly handles fractional-hour
// offsets like India +0530 that Etc/GMT cannot express. Falls back to plain
// toLocaleString when sessions.js hasn't loaded yet.
const fmt=(typeof _formatInServerTz==='function')?_formatInServerTz:null;
if(_isSameLocalDay(date, now)){
const opts={hour:'2-digit', minute:'2-digit'};
return fmt?fmt(date,opts):date.toLocaleTimeString([], opts);
}
const opts={month:'short', day:'numeric', hour:'numeric', minute:'2-digit'};
return fmt?fmt(date,opts):date.toLocaleString([], opts);
}
function _compressionStatusCardHtml({
statusLabel,
previewText,
detail,
icon,
open=false,
variantClass='',
}){
const statusDetail = String(detail || '').trim();
const hasBody = !!statusDetail;
const openClass = open ? ' open' : '';
const statusIcon = icon;
const bodyHtml = hasBody ? `<div class="tool-card-detail"><div class="tool-card-result"><pre>${esc(statusDetail)}</pre></div></div>` : '';
const toggleHtml = hasBody ? `<span class="tool-card-toggle">${li('chevron-right',12)}</span>` : '';
return `
<div class="tool-card ${variantClass}${openClass}">
<div class="tool-card-header" onclick="this.closest('.tool-card').classList.toggle('open')">
${statusIcon}
<span class="tool-card-name">${esc(statusLabel)}</span>
<span class="tool-card-preview">${esc(previewText)}</span>
${toggleHtml}
</div>
${bodyHtml}
</div>`;
}
2026-05-01 13:29:10 +00:00
function _handoffStateForCurrentSession(){
const state=window._handoffUi;
if(!state||!S.session||state.sessionId!==S.session.session_id) return null;
return state;
}
function clearHandoffUi(){
window._handoffUi=null;
renderMessages();
}
function setHandoffUi(state){
if(!state){
clearHandoffUi();
return;
}
window._handoffUi={...state};
renderMessages();
}
function _handoffCardsHtml(state){
if(!state) return '';
const channel=String(state.channel||'').trim();
const label=channel?`${channel} handoff summary`:'Handoff summary';
const isError=state.phase==='error';
const isDone=state.phase==='done';
2026-05-03 04:59:07 +00:00
const isFallback=!!state.fallback;
2026-05-01 13:29:10 +00:00
const detail=isError
? String(state.errorText||'Could not generate summary. Please try again.')
: isDone
? String(state.summary||'')
: 'Generating handoff summary...';
const meta=typeof state.rounds==='number'
? `${state.rounds} external conversation rounds`
: '';
const icon=isError
? li('x',13)
: isDone
? li('check',13)
: '<span class="tool-card-running-dot"></span>';
const bodyHtml=isDone&&!isError
2026-05-03 04:59:07 +00:00
? (
`${renderMd(detail)}${
isFallback
? '<p class="handoff-summary-fallback-note">Fallback summary generated from recent turns; no model-based rewrite was used.</p>'
: ''
}`
)
2026-05-01 13:29:10 +00:00
: `<p>${esc(detail)}</p>`;
return `
<div class="tool-card-row compression-card-row handoff-card-row" data-compression-card="1" data-handoff-card="1">
<div class="tool-card tool-card-handoff-summary${isError?' tool-card-compress-error':''} open">
<div class="tool-card-header" onclick="this.closest('.tool-card').classList.toggle('open')">
${icon}
<span class="tool-card-name">${esc(label)}</span>
${meta?`<span class="tool-card-preview">${esc(meta)}</span>`:''}
<span class="tool-card-toggle">${li('chevron-right',12)}</span>
</div>
<div class="tool-card-detail">
<div class="tool-card-result handoff-summary-body">${bodyHtml}</div>
</div>
</div>
</div>`;
}
function _handoffCardsNode(state){
const wrap=document.createElement('div');
wrap.className='compression-turn handoff-turn';
wrap.innerHTML=`<div class="compression-turn-blocks">${_handoffCardsHtml(state)}</div>`;
return wrap;
}
function _contextCompactionMessageHtml(m, tsTitle='', preservedMessages=[]){
const text=msgContent(m)||String(m.content||'');
return `<div class="compression-turn"><div class="compression-turn-blocks">${_compressionReferenceCardHtml(text, false, tsTitle)}${_preservedCompressionTaskListCardsHtml(preservedMessages)}</div></div>`;
}
function renderCompressionUi(){
const el=$('liveCompressionCards');
if(!el) return;
el.innerHTML='';
el.style.display='none';
}
// Session render cache: avoids full markdown+DOM rebuild when switching back
// to a session that was already rendered with the same message count.
// Keyed by session_id. Only used on cross-session navigation, never for
// in-session updates (new messages, edits, stream events).
//
// Known limitation: cache key is session_id + message count. Edits and retries
// that mutate message content without changing the count will serve stale HTML
// on back-navigation until the user triggers an in-session update. Acceptable
// for the common read-only back-navigation case; not suitable as a general cache.
const _sessionHtmlCache=new Map();
let _sessionHtmlCacheSid=null; // session_id currently rendered in the DOM
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
function clearMessageRenderCache(){
_sessionHtmlCache.clear();
_sessionHtmlCacheSid=null;
}
function renderMessages(){
const inner=$('msgInner');
const sid=S.session?S.session.session_id:null;
const msgCount=S.messages.length;
if(sid!==_messageRenderWindowSid) _resetMessageRenderWindow(sid);
const renderWindowSize=_currentMessageRenderWindowSize();
2026-05-01 13:29:10 +00:00
const hasTransientTranscriptUi=!!(
(window._compressionUi&&(!window._compressionUi.sessionId||window._compressionUi.sessionId===sid)) ||
(window._handoffUi&&(!window._handoffUi.sessionId||window._handoffUi.sessionId===sid))
);
// Fast path: switching back to a previously rendered session with same count.
// Guard: sid !== _sessionHtmlCacheSid ensures in-session updates (edits,
// new messages, tool_complete) always get a fresh rebuild.
// Skip cache if this session is still streaming — the live smd parser writes
// into a DOM node inside the cached subtree; serving cached HTML detaches it.
2026-05-01 13:29:10 +00:00
// Also skip cache for transient transcript cards such as /compress and
// cross-channel handoff summaries; otherwise the cached transcript returns
// before those cards can be inserted.
if(sid&&sid!==_sessionHtmlCacheSid&&!INFLIGHT[sid]&&!hasTransientTranscriptUi){
const cached=_sessionHtmlCache.get(sid);
if(cached&&cached.msgCount===msgCount&&cached.renderWindowSize===renderWindowSize){
inner.innerHTML=cached.html;
_sessionHtmlCacheSid=sid;
_wireMessageWindowLoadEarlierButton();
if(S.activeStreamId){scrollIfPinned();}else{scrollToBottom();}
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
requestAnimationFrame(()=>{highlightCode();addCopyButtons();loadDiffInline();loadCsvInline();loadExcalidrawInline();loadPdfInline();loadHtmlInline();renderMermaidBlocks();renderKatexBlocks();});
requestAnimationFrame(()=>{highlightCode();addCopyButtons();initTreeViews();loadPdfInline();loadHtmlInline();renderMermaidBlocks();renderKatexBlocks();});
if(typeof _initMediaPlaybackObserver==='function') _initMediaPlaybackObserver();
if(typeof loadTodos==='function'&&document.getElementById('panelTodos')&&document.getElementById('panelTodos').classList.contains('active')){loadTodos();}
return;
}
}
const compressionState=_compressionStateForCurrentSession();
if(window._compressionUi && !compressionState) clearCompressionUi();
2026-05-01 13:29:10 +00:00
const handoffState=_handoffStateForCurrentSession();
if(window._handoffUi && !handoffState) window._handoffUi=null;
const sessionCompressionAnchor=(
S.session && typeof S.session.compression_anchor_visible_idx==='number'
) ? S.session.compression_anchor_visible_idx : null;
const sessionCompressionAnchorKey=(
S.session && S.session.compression_anchor_message_key && typeof S.session.compression_anchor_message_key==='object'
) ? S.session.compression_anchor_message_key : null;
const preservedCompressionTaskMessages=_latestPreservedCompressionTaskListMessages(S.messages);
const vis=S.messages.filter(m=>{
if(!m||!m.role||m.role==='tool')return false;
if(_isContextCompactionMessage(m)) return false;
if(_isPreservedCompressionTaskListMessage(m)) return false;
if(m.role==='assistant'){
const hasTc=Array.isArray(m.tool_calls)&&m.tool_calls.length>0;
const hasTu=Array.isArray(m.content)&&m.content.some(p=>p&&p.type==='tool_use');
if(hasTc||hasTu||_messageHasReasoningPayload(m)) return true;
}
2026-05-04 23:58:54 +00:00
return m._statusCard||msgContent(m)||m.attachments?.length;
});
$('emptyState').style.display=(vis.length||preservedCompressionTaskMessages.length)?'none':'';
inner.innerHTML='';
const compressionNode=compressionState?_compressionCardsNode(compressionState):null;
const referenceMessage=S.messages.find(m=>_isContextCompactionMessage(m));
const referenceText=referenceMessage?msgContent(referenceMessage)||String(referenceMessage.content||''):'';
const referenceNode=(!compressionState && referenceMessage && (sessionCompressionAnchor!==null || sessionCompressionAnchorKey))
? (()=>{const row=document.createElement('div');row.innerHTML=`<div class="compression-turn"><div class="compression-turn-blocks">${_compressionReferenceCardHtml(referenceText,false)}${_preservedCompressionTaskListCardsHtml(preservedCompressionTaskMessages)}</div></div>`;return row.firstElementChild;})()
: null;
let preservedCompressionTaskCardsAttached=!!referenceNode;
const visWithIdx=[];
const preservedCompressionRawIdxs=[];
let rawIdx=0;
for(const m of S.messages){
if(!m||!m.role||m.role==='tool'){rawIdx++;continue;}
if(_isPreservedCompressionTaskListMessage(m)){preservedCompressionRawIdxs.push(rawIdx);rawIdx++;continue;}
const hasTc=Array.isArray(m.tool_calls)&&m.tool_calls.length>0;
const hasTu=Array.isArray(m.content)&&m.content.some(p=>p&&p.type==='tool_use');
2026-05-04 23:58:54 +00:00
if(msgContent(m)||m._statusCard||m.attachments?.length||(m.role==='assistant'&&(hasTc||hasTu||_messageHasReasoningPayload(m)))) visWithIdx.push({m,rawIdx});
rawIdx++;
}
// Show a top affordance when earlier transcript content exists either in
// memory (DOM windowing) or on the server (paginated session fetch).
// Prefer expanding the local render window first so a fully loaded long
// session can reduce DOM nodes without losing in-memory transcript data.
const windowStart=Math.max(0, visWithIdx.length-renderWindowSize);
const hiddenBeforeCount=windowStart;
const renderVisWithIdx=visWithIdx.slice(windowStart);
const firstRenderedRawIdx=renderVisWithIdx.length?renderVisWithIdx[0].rawIdx:Infinity;
const hasServerOlder=!!(typeof _messagesTruncated!=='undefined' && _messagesTruncated && S.messages.length>0);
if(hiddenBeforeCount>0 || hasServerOlder){
const indicator=document.createElement('button');
indicator.type='button';
indicator.id='loadOlderIndicator';
indicator.className='load-older-indicator message-window-load-earlier';
indicator.textContent=hiddenBeforeCount>0
? `Load earlier messages (${hiddenBeforeCount} hidden)`
: (typeof t==='function'?t('load_older_messages'):'Load earlier messages');
indicator.onclick=()=>{
if(hiddenBeforeCount>0) _showEarlierRenderedMessages();
else if(typeof _loadOlderMessages==='function') _loadOlderMessages();
};
inner.appendChild(indicator);
_wireMessageWindowLoadEarlierButton();
}
let lastUserRawIdx=-1;
for(let i=visWithIdx.length-1;i>=0;i--){
if(visWithIdx[i].m&&visWithIdx[i].m.role==='user'){
lastUserRawIdx=visWithIdx[i].rawIdx;
break;
}
}
const insertionAnchor=_compressionAnchorIndex(
renderVisWithIdx,
compressionState ? compressionState.anchorMessageKey : sessionCompressionAnchorKey,
compressionState
? (typeof compressionState.anchorVisibleIdx==='number' ? compressionState.anchorVisibleIdx : compressionState.anchorRawIdx)
: sessionCompressionAnchor
);
let _prevSepKey=null;
let currentAssistantTurn=null;
const assistantSegments=new Map();
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const assistantThinking=new Map();
const userRows=new Map();
// Windowed render loop replaces the legacy full loop:
// for(let vi=0;vi<visWithIdx.length;vi++)
for(let vi=0;vi<renderVisWithIdx.length;vi++){
const {m,rawIdx}=renderVisWithIdx[vi];
const _tsSep=m._ts||m.timestamp;
if(_tsSep){
const _d=new Date(_tsSep*1000);
const _key=_d.toDateString();
if(_prevSepKey && _prevSepKey!==_key){
const sep=document.createElement('div');
sep.className='msg-date-sep';
sep.textContent=_fmtDateSep(_d);
inner.appendChild(sep);
}
_prevSepKey=_key;
}
let content=m.content||'';
let thinkingText='';
if(Array.isArray(content)){
thinkingText=content.filter(p=>p&&(p.type==='thinking'||p.type==='reasoning')).map(p=>p.thinking||p.reasoning||p.text||'').join('\n');
content=content.filter(p=>p&&p.type==='text').map(p=>p.text||p.content||'').join('\n');
}
if(!thinkingText && m.reasoning) thinkingText=m.reasoning;
if(!thinkingText && typeof content==='string'){
const thinkMatch=content.match(/<think>([\s\S]*?)<\/think>/);
if(thinkMatch){
thinkingText=thinkMatch[1].trim();
content=content.replace(/<think>[\s\S]*?<\/think>\s*/,'').trimStart();
}
if(!thinkingText){
// Historical name "gemmaMatch" refers to MiniMax <|channel>thought format.
const gemmaMatch=content.match(/<\|channel>thought\n([\s\S]*?)<channel\|>/);
if(gemmaMatch){
thinkingText=gemmaMatch[1].trim();
content=content.replace(/<\|channel>thought\n[\s\S]*?<channel\|>\s*/,'').trimStart();
}
}
if(!thinkingText){
// Gemma 4 uses asymmetric <|turn|>thinking\n...<turn|> delimiters.
const gemmaTurnMatch=content.match(/<\|turn\|>thinking\n([\s\S]*?)<turn\|>/);
if(gemmaTurnMatch){
thinkingText=gemmaTurnMatch[1].trim();
content=content.replace(/<\|turn\|>thinking\n[\s\S]*?<turn\|>\s*/,'').trimStart();
}
}
}
const isUser=m.role==='user';
const isLastAssistant=!isUser&&vi===renderVisWithIdx.length-1;
let filesHtml='';
if(m.attachments&&m.attachments.length){
// Static regression tests intentionally look for msg-media-img/msg-file-badge near this branch.
const _attachSid=(S.session&&S.session.session_id)||'';
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117) * fix(#1096): copy buttons fall back to execCommand on HTTP contexts - Add _copyText() helper: tries navigator.clipboard first, falls back to document.execCommand('copy') with hidden textarea when not in secure context - Update copyMsg() and addCopyButtons() to use helper instead of direct navigator.clipboard.writeText() - Code block copy button now has .catch() handler (was silently failing) - Error messages use t('copy_failed') for i18n instead of hardcoded string - Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant) - Add 10 regression tests * fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge - User message attachments with image extensions now render as <img> via api/media endpoint, with click-to-fullscreen support - Non-image attachments still show paperclip + filename badge - Extracts filename from full path for display - Add 5 regression tests * fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix) * fix: improve mobile touch responsiveness for session list items iPad Safari has known issues with the click/dblclick pattern on touch: - :hover-triggered padding-right layout shift causes the first tap click to target the wrong element (actions button that just appeared) - No touch-action:manipulation means iOS still delays taps for double-tap zoom detection - The old onclick+ondblclick pattern is designed for mouse, not touch Changes: - CSS: Remove :hover from padding-right rule to prevent layout shift - CSS: Add touch-action:manipulation and -webkit-tap-highlight-color to .session-item for immediate tap response - JS: Replace onclick/ondblclick with onpointerup + manual 350ms double-tap detection — works consistently on mouse and touch * fix(#1106): iterate custom_providers[].models dict keys for dropdown population - After reading singular 'model' field, also iterate 'models' dict keys - Deduplicate: model field value not repeated if also in models dict - Skip non-string keys gracefully - Works for both named and unnamed custom_providers entries - Add 7 regression tests * fix(#1105): allow custom_providers hostnames through SSRF check - Build trusted hostname set from custom_providers[].base_url in config.yaml - These are user-explicitly configured endpoints — not SSRF risks - Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active - Unknown private IPs still blocked - Add 7 tests (5 source analysis + 2 functional with mocked socket) * fix(tests): update hover padding assertions for #1110 touch fix (absorb) * fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb) * fix: filter right/middle-click from pointerup handler (absorb) * docs: v0.50.221 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
filesHtml=`<div class="msg-files">${m.attachments.map(f=>{
const fLabel=typeof f==='string'?f:(f&&(f.name||f.filename||f.path))||'';
const fname=String(fLabel).split('/').pop()||String(fLabel);
// Use api/file/raw which resolves filename relative to the session workspace.
const fileUrl='api/file/raw?session_id='+encodeURIComponent(_attachSid)+'&path='+encodeURIComponent(fname);
return _renderAttachmentHtml(fname,fileUrl);
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117) * fix(#1096): copy buttons fall back to execCommand on HTTP contexts - Add _copyText() helper: tries navigator.clipboard first, falls back to document.execCommand('copy') with hidden textarea when not in secure context - Update copyMsg() and addCopyButtons() to use helper instead of direct navigator.clipboard.writeText() - Code block copy button now has .catch() handler (was silently failing) - Error messages use t('copy_failed') for i18n instead of hardcoded string - Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant) - Add 10 regression tests * fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge - User message attachments with image extensions now render as <img> via api/media endpoint, with click-to-fullscreen support - Non-image attachments still show paperclip + filename badge - Extracts filename from full path for display - Add 5 regression tests * fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix) * fix: improve mobile touch responsiveness for session list items iPad Safari has known issues with the click/dblclick pattern on touch: - :hover-triggered padding-right layout shift causes the first tap click to target the wrong element (actions button that just appeared) - No touch-action:manipulation means iOS still delays taps for double-tap zoom detection - The old onclick+ondblclick pattern is designed for mouse, not touch Changes: - CSS: Remove :hover from padding-right rule to prevent layout shift - CSS: Add touch-action:manipulation and -webkit-tap-highlight-color to .session-item for immediate tap response - JS: Replace onclick/ondblclick with onpointerup + manual 350ms double-tap detection — works consistently on mouse and touch * fix(#1106): iterate custom_providers[].models dict keys for dropdown population - After reading singular 'model' field, also iterate 'models' dict keys - Deduplicate: model field value not repeated if also in models dict - Skip non-string keys gracefully - Works for both named and unnamed custom_providers entries - Add 7 regression tests * fix(#1105): allow custom_providers hostnames through SSRF check - Build trusted hostname set from custom_providers[].base_url in config.yaml - These are user-explicitly configured endpoints — not SSRF risks - Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active - Unknown private IPs still blocked - Add 7 tests (5 source analysis + 2 functional with mocked socket) * fix(tests): update hover padding assertions for #1110 touch fix (absorb) * fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb) * fix: filter right/middle-click from pointerup handler (absorb) * docs: v0.50.221 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
}).join('')}</div>`;
}
const bodyHtml = isUser ? _renderUserFencedBlocks(content) : renderMd(_stripXmlToolCallsDisplay(String(content)));
2026-05-04 23:58:54 +00:00
const statusHtml = (!isUser&&m._statusCard) ? _statusCardHtml(m._statusCard) : '';
const isEditableUser=isUser&&rawIdx===lastUserRawIdx;
const editBtn = isEditableUser ? `<button class="msg-action-btn" title="${t('edit_message')}" onclick="editMessage(this)">${li('pencil',13)}</button>` : '';
refactor(ui): three-column layout with left rail + main-view migration (#899) refactor(ui): three-column layout with left rail + main-view migration (#899) Unifies the shell into a three-column layout (rail + sidebar + main) matching the hermes-desktop reference, and migrates every per-item detail/edit surface into a shared main-view canvas with consistent headers, empty states, and action buttons. Changes: - New desktop-only left rail (48px) with 8 nav tabs (chat/tasks/skills/memory/workspaces/profiles/todos/settings) - Persistent app titlebar (replaces per-chat topbar), active conversation title shown - All panel detail/create/edit views migrated to #mainSkills, #mainTasks, #mainSettings, #mainWorkspaces, #mainProfiles, #mainMemory - Settings moved out of modal into main-view page; ESC closes it - YAML frontmatter rendered in collapsible <details> block in skill detail - Toasts repositioned from bottom-center to top-right with theme-aware success/error/warning/info variants - Composer workspace chip split into two-button group: files-icon toggles file panel, label opens workspace picker - .settings-menu → .side-menu / .side-menu-item (generalised, shared by memory and settings panels) - i18n: ~25 new keys across en/ru/es/de/zh/zh-Hant for all new form labels, placeholders, and empty states - Mobile: hamburger in titlebar, slide-in sidebar; box-shadow removed from sidebar - New regression test: tests/test_settings_navigation_and_detail_refresh.py (9 tests) Co-authored-by: Aron Prins <pwf.aron@gmail.com>
2026-04-24 16:05:25 +00:00
const undoBtn = isLastAssistant ? `<button class="msg-action-btn" title="${t('undo_exchange')}" onclick="undoLastExchange()">${li('undo',13)}</button>` : '';
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
const retryBtn = isLastAssistant ? `<button class="msg-action-btn" title="${t('regenerate')}" onclick="regenerateResponse(this)">${li('rotate-ccw',13)}</button>` : '';
const copyBtn = `<button class="msg-copy-btn msg-action-btn" title="${t('copy')}" onclick="copyMsg(this)">${li('copy',13)}</button>`;
const forkBtn = `<button class="msg-action-btn" title="${t('fork_from_here')}" onclick="forkFromMessage(${rawIdx+1})">${li('git-branch',13)}</button>`;
const ttsBtn = !isUser ? `<button class="msg-action-btn msg-tts-btn" title="${t('tts_listen')||'Listen'}" onclick="speakMessage(this)">${li('volume-2',13)}</button>` : '';
2026-03-31 07:02:47 +00:00
const tsVal=m._ts||m.timestamp;
// _formatInServerTz handles fractional-hour offsets (India +0530 etc.)
// correctly via offset arithmetic; bare toLocaleString is the browser-tz fallback.
const _fmtSv=(typeof _formatInServerTz==='function')?_formatInServerTz:null;
const tsTitle=tsVal?(_fmtSv?_fmtSv(new Date(tsVal*1000),{}):new Date(tsVal*1000).toLocaleString()):'';
const tsTime=_formatMessageFooterTimestamp(tsVal);
const timeHtml = tsTime ? `<span class="msg-time" title="${esc(tsTitle)}">${tsTime}</span>` : '';
const footHtml = `<div class="msg-foot">${timeHtml}<span class="msg-actions">${editBtn}${ttsBtn}${forkBtn}${copyBtn}${retryBtn}</span></div>`;
if(_isContextCompactionMessage(m)){
if(compressionState || referenceNode){
continue;
}else{
currentAssistantTurn=null;
const row=document.createElement('div');
const preservedForThisCard=preservedCompressionTaskCardsAttached?[]:preservedCompressionTaskMessages;
row.innerHTML=_contextCompactionMessageHtml(m, tsTitle, preservedForThisCard);
if(preservedForThisCard.length) preservedCompressionTaskCardsAttached=true;
inner.appendChild(row.firstElementChild);
continue;
}
}
if(isUser){
currentAssistantTurn=null;
const row=document.createElement('div');
row.className='msg-row';
row.dataset.msgIdx=rawIdx;
row.dataset.role='user';
row.dataset.rawText=String(content).trim();
row.innerHTML=`${filesHtml}<div class="msg-body">${bodyHtml}</div>${footHtml}`;
inner.appendChild(row);
userRows.set(rawIdx, row);
continue;
}
if(!currentAssistantTurn){
2026-05-04 19:08:07 +00:00
currentAssistantTurn=_createAssistantTurn(tsTitle, isTpsDisplayEnabled()?_formatTurnTps(m._turnTps):'');
inner.appendChild(currentAssistantTurn);
}
const seg=document.createElement('div');
seg.className='assistant-segment';
seg.dataset.msgIdx=rawIdx;
seg.dataset.rawText=String(content).trim();
if(m._live){
currentAssistantTurn.id='liveAssistantTurn';
// Stamp the session id on the live turn so finalizeThinkingCard()
// and other late callbacks can verify they're operating on the
// right session's DOM (the user may have switched tabs/sessions
// while this stream is still streaming). See #1366.
if(S.session) currentAssistantTurn.dataset.sessionId=S.session.session_id;
seg.setAttribute('data-live-assistant','1');
}
if(_ERR_MSG_RE.test(String(content||'').trim())) seg.dataset.error='1';
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
if(thinkingText&&window._showThinking!==false){
if(isSimplifiedToolCalling()) assistantThinking.set(rawIdx, thinkingText);
else if(window._showThinking!==false) seg.insertAdjacentHTML('beforeend', _thinkingCardHtml(thinkingText));
}
2026-05-04 23:58:54 +00:00
const hasVisibleBody=!!(String(content||'').trim()||filesHtml||statusHtml);
if(statusHtml){
seg.insertAdjacentHTML('beforeend', statusHtml);
}else if(hasVisibleBody){
seg.insertAdjacentHTML('beforeend', `${filesHtml}<div class="msg-body">${bodyHtml}</div>${footHtml}`);
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
}else if(!(thinkingText&&window._showThinking!==false&&!isSimplifiedToolCalling())){
seg.classList.add('assistant-segment-anchor');
}
_assistantTurnBlocks(currentAssistantTurn).appendChild(seg);
assistantSegments.set(rawIdx, seg);
}
function _insertCompressionLikeNode(node, anchorIndex){
if(!node) return;
const anchorIdx=anchorIndex===undefined?insertionAnchor:anchorIndex;
if(anchorIdx!==null && renderVisWithIdx[anchorIdx]){
const anchorRawIdx=renderVisWithIdx[anchorIdx].rawIdx;
const anchorSeg=assistantSegments.get(anchorRawIdx);
if(anchorSeg){
const turn=anchorSeg.closest('.assistant-turn');
const blocks=_assistantTurnBlocks(turn);
if(blocks){
blocks.appendChild(node);
return;
}
}
const userRow=userRows.get(anchorRawIdx);
if(userRow && userRow.parentElement){
userRow.parentElement.insertBefore(node, userRow.nextSibling);
return;
}
}
inner.appendChild(node);
}
2026-05-03 04:59:07 +00:00
function _insertCompressionLikeNodeByRawIdx(node, rawIdx){
if(!node) return;
if(!renderVisWithIdx.length){
2026-05-03 04:59:07 +00:00
inner.appendChild(node);
return;
}
let anchorIdx=null;
for(let i=0;i<renderVisWithIdx.length;i++){
if(renderVisWithIdx[i].rawIdx > rawIdx){
2026-05-03 04:59:07 +00:00
anchorIdx=i;
break;
}
}
if(anchorIdx===null){
inner.appendChild(node);
return;
}
const anchorRawIdx=renderVisWithIdx[anchorIdx].rawIdx;
2026-05-03 04:59:07 +00:00
const anchorSeg=assistantSegments.get(anchorRawIdx);
if(anchorSeg){
const turn=anchorSeg.closest('.assistant-turn');
const blocks=_assistantTurnBlocks(turn);
if(blocks){
blocks.appendChild(node);
return;
}
const turnParent=turn && turn.parentElement;
if(turnParent){
turnParent.insertBefore(node, turn);
return;
}
}
const userRow=userRows.get(anchorRawIdx);
if(userRow && userRow.parentElement){
userRow.parentElement.insertBefore(node, userRow);
return;
}
inner.appendChild(node);
}
const preservedOnlyNode=(!preservedCompressionTaskCardsAttached&&(!referenceMessage||compressionState)&&preservedCompressionTaskMessages.length)
? (()=>{const row=document.createElement('div');row.innerHTML=`<div class="compression-turn"><div class="compression-turn-blocks">${_preservedCompressionTaskListCardsHtml(preservedCompressionTaskMessages)}</div></div>`;return row.firstElementChild;})()
: null;
const preservedOnlyAnchor=preservedCompressionRawIdxs.length
? (()=>{let idx=null;for(let i=0;i<renderVisWithIdx.length;i++){if(renderVisWithIdx[i].rawIdx<preservedCompressionRawIdxs[0]) idx=i;}return idx;})()
: null;
2026-05-03 04:59:07 +00:00
const handoffSummaryStates=_collectHandoffSummaryStates(S.messages);
_insertCompressionLikeNode(compressionNode);
_insertCompressionLikeNode(referenceNode);
_insertCompressionLikeNode(preservedOnlyNode, preservedOnlyAnchor);
_insertCompressionLikeNode(handoffState?_handoffCardsNode(handoffState):null, renderVisWithIdx.length?renderVisWithIdx.length-1:null);
2026-05-03 04:59:07 +00:00
for(const entry of handoffSummaryStates){
if(!entry||!entry.state) continue;
if(entry.rawIdx<firstRenderedRawIdx) continue;
2026-05-03 04:59:07 +00:00
_insertCompressionLikeNodeByRawIdx(_handoffCardsNode(entry.state), entry.rawIdx);
}
renderCompressionUi();
// Insert settled tool call cards (history view only).
// During live streaming, tool cards are rendered in #liveToolCards by the
// tool SSE handler and never mixed into the message list until done fires.
//
// Fallback: if S.toolCalls is empty (sessions that predate session-level tool
// tracking, or runs that didn't go through the normal streaming path), build
// a display list from per-message tool_calls (OpenAI format) stored in each
// assistant message. This covers the reload case described in issue #140.
if(!S.busy && (!S.toolCalls||!S.toolCalls.length)){
// Pass 1: index tool outputs by tool_call_id / tool_use_id so the
// fallback-built cards carry their result snippet (not just the command).
// Without this step CLI-origin sessions reload with empty tool cards.
const resultsByTid={};
const _snipFromRaw=(raw)=>{
const s=String(raw||'');
try{
const rd=JSON.parse(s);
if(rd && typeof rd==='object') return String(rd.output||rd.result||rd.error||s).slice(0,200);
}catch(e){}
return s.slice(0,200);
};
S.messages.forEach(m=>{
if(!m) return;
// OpenAI / Hermes CLI format: role=tool with tool_call_id
if(m.role==='tool'){
const tid=m.tool_call_id||m.tool_use_id||'';
if(tid) resultsByTid[tid]=_snipFromRaw(m.content);
return;
}
// Anthropic format: tool_result blocks inside a user message content array
if(Array.isArray(m.content)){
m.content.forEach(p=>{
if(!p||typeof p!=='object'||p.type!=='tool_result') return;
const tid=p.tool_use_id||'';
if(!tid) return;
const raw=typeof p.content==='string'?p.content
:Array.isArray(p.content)?p.content.map(c=>c&&c.text?c.text:'').join('')
:'';
resultsByTid[tid]=_snipFromRaw(raw);
});
}
});
const derived=[];
S.messages.forEach((m,rawIdx)=>{
if(m.role!=='assistant') return;
// OpenAI format: top-level tool_calls field on the assistant message
(m.tool_calls||[]).forEach(tc=>{
if(!tc||typeof tc!=='object') return;
const fn=tc.function||{};
const name=fn.name||tc.name||'tool';
let args={};
try{ args=JSON.parse(fn.arguments||'{}'); }catch(e){}
let argsSnap={};
Object.keys(args).slice(0,4).forEach(k=>{ const v=String(args[k]); argsSnap[k]=v.slice(0,120)+(v.length>120?'...':''); });
const tid=tc.id||tc.call_id||'';
derived.push({name,snippet:resultsByTid[tid]||'',tid,assistant_msg_idx:rawIdx,args:argsSnap,done:true});
});
// Anthropic format: tool_use blocks inside assistant content array
if(Array.isArray(m.content)){
m.content.forEach(p=>{
if(!p||typeof p!=='object'||p.type!=='tool_use') return;
const name=p.name||'tool';
const args=p.input||{};
const argsSnap={};
if(args && typeof args==='object'){
Object.keys(args).slice(0,4).forEach(k=>{ const v=String(args[k]); argsSnap[k]=v.slice(0,120)+(v.length>120?'...':''); });
}
const tid=p.id||'';
derived.push({name,snippet:resultsByTid[tid]||'',tid,assistant_msg_idx:rawIdx,args:argsSnap,done:true});
});
}
});
if(derived.length) S.toolCalls=derived;
}
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
if(!S.busy){
inner.querySelectorAll('.tool-call-group:not([data-compression-card]),.tool-card-row:not([data-compression-card])').forEach(el=>el.remove());
const byAssistant = {};
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
for(const tc of (S.toolCalls||[])){
const key = tc.assistant_msg_idx !== undefined ? tc.assistant_msg_idx : -1;
if(!byAssistant[key]) byAssistant[key] = [];
byAssistant[key].push(tc);
}
const assistantIdxs=[...assistantSegments.keys()].sort((a,b)=>a-b);
fix: tool call cards persist correctly after page reload (#141) * fix: tool call cards persist correctly after page reload Root cause: the insertion logic looked for the NEXT assistant row to insert BEFORE, but when the triggering assistant message contained only tool_use blocks (no text), it was filtered from the DOM by msgContent() and no anchor row existed. Cards were silently dropped. Fix: find the row AT the assistant_msg_idx first (exact match), fall back to the nearest preceding visible row, then insert AFTER it (not before the next). This handles both text+tool and tool-only assistant messages correctly. Closes #140 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: tool card ordering — role-filter fallback anchor, preserve order for same-anchor groups Two bugs in the initial PR: - Fallback 'nearest row' had no role filter, could anchor to a user message row causing cards to appear after a user bubble. Fixed: check role==='assistant' in the fallback loop. - Back-to-back tool-card groups sharing a filtered anchor were inserted in reverse order because each group re-read anchorRow.nextSibling from the live DOM. Fixed: track the last inserted node per anchor via anchorInsertAfter Map, so each subsequent group appends after the previous one. Also restores the 'Collect card elements before they get moved to DOM' comment explaining why Array.from() is used on frag before DOM insertion. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:02:25 +00:00
const anchorInsertAfter = new Map();
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
if(isSimplifiedToolCalling()){
const activityIdxs=[...new Set([...Object.keys(byAssistant).map(k=>parseInt(k)), ...assistantThinking.keys()])].sort((a,b)=>a-b);
for(const aIdx of activityIdxs){
const cards=byAssistant[aIdx]||[];
let anchorRow=assistantSegments.get(aIdx)||null;
if(!anchorRow&&assistantIdxs.length){
if(aIdx<assistantIdxs[0]) continue;
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const fallbackIdx=[...assistantIdxs].reverse().find(idx=>idx<=aIdx);
anchorRow=fallbackIdx!==undefined?assistantSegments.get(fallbackIdx):assistantSegments.get(assistantIdxs[assistantIdxs.length-1]);
}
if(!anchorRow) continue;
const anchorParent=anchorRow.parentElement;
const insertAfterNode = anchorInsertAfter.get(anchorRow) || anchorRow;
const group=ensureActivityGroup(anchorParent,{collapsed:true,anchor:insertAfterNode});
2026-05-04 03:14:49 +00:00
const sourceMsg=S.messages[aIdx]||{};
if(sourceMsg._turnDuration!==undefined) group.setAttribute('data-turn-duration', String(sourceMsg._turnDuration));
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const body=group&&group.querySelector('.tool-call-group-body');
if(!body) continue;
const thinkingText=assistantThinking.get(aIdx);
if(thinkingText) body.appendChild(_thinkingActivityNode(thinkingText));
for(const tc of cards){
body.appendChild(buildToolCard(tc));
}
_syncToolCallGroupSummary(group);
if(anchorRow) anchorInsertAfter.set(anchorRow, group);
}
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
}else if(S.toolCalls && S.toolCalls.length){
for(const [key, cards] of Object.entries(byAssistant)){
const aIdx = parseInt(key);
let anchorRow=assistantSegments.get(aIdx)||null;
if(!anchorRow&&assistantIdxs.length){
if(aIdx<assistantIdxs[0]) continue;
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const fallbackIdx=[...assistantIdxs].reverse().find(idx=>idx<=aIdx);
anchorRow=fallbackIdx!==undefined?assistantSegments.get(fallbackIdx):assistantSegments.get(assistantIdxs[assistantIdxs.length-1]);
}
if(!anchorRow) continue;
const anchorParent=anchorRow.parentElement;
const frag=document.createDocumentFragment();
let lastInsertedNode=null;
for(const tc of cards){
const card=buildToolCard(tc);
frag.appendChild(card);
lastInsertedNode=card;
}
// Add expand/collapse toggle for groups with 2+ cards
if(cards.length>=2){
const toggle=document.createElement('div');
toggle.className='tool-cards-toggle';
// Collect card elements before they get moved to DOM
const cardEls=Array.from(frag.querySelectorAll('.tool-card'));
const expandBtn=document.createElement('button');
expandBtn.textContent=t('expand_all');
expandBtn.onclick=()=>cardEls.forEach(c=>c.classList.add('open'));
const collapseBtn=document.createElement('button');
collapseBtn.textContent=t('collapse_all');
collapseBtn.onclick=()=>cardEls.forEach(c=>c.classList.remove('open'));
toggle.appendChild(expandBtn);
toggle.appendChild(collapseBtn);
frag.insertBefore(toggle,frag.firstChild);
}
const insertAfterNode = anchorInsertAfter.get(anchorRow) || anchorRow;
const refNode = insertAfterNode ? insertAfterNode.nextSibling : null;
if(refNode) anchorParent.insertBefore(frag,refNode);
else anchorParent.appendChild(frag);
if(anchorRow&&lastInsertedNode) anchorInsertAfter.set(anchorRow, lastInsertedNode);
}
}
}
2026-05-04 03:14:49 +00:00
// Render per-turn duration and optional token usage on assistant messages.
// Duration stays visible even when token usage is disabled, because it answers
// the basic "how long did that turn take?" UX question. Only walk rendered
// assistant segments so hidden messages above the DOM window cannot skew the
// footer-to-message mapping.
2026-05-04 03:14:49 +00:00
{
const renderedAssistantIdxs=[...assistantSegments.keys()].sort((a,b)=>a-b);
for(const mi of renderedAssistantIdxs){
const msg=S.messages[mi]||{};
if(msg.role!=='assistant') continue;
const routing=msg._gatewayRouting||null;
const gatewayText=_formatGatewayModelLabel(S.session&&S.session.model||'', '', routing);
const failoverText=_gatewayRoutingFailoverText(routing);
const modelWarningText=_gatewayModelWarningText(routing);
2026-05-04 03:14:49 +00:00
const hasTurnUsage=!!msg._turnUsage;
const compactActivityForMessage=isSimplifiedToolCalling()&&(
assistantThinking.has(mi)||
(S.toolCalls||[]).some(tc=>tc&&(tc.assistant_msg_idx!==undefined?tc.assistant_msg_idx:-1)===mi)
);
const durationText=compactActivityForMessage?'':_formatTurnDuration(msg._turnDuration);
if(!hasTurnUsage&&!durationText&&!gatewayText&&!failoverText&&!modelWarningText) continue;
const seg=assistantSegments.get(mi);
const row=seg?seg.closest('.assistant-turn'):null;
const footerRows=row?row.querySelectorAll('.msg-foot'):[];
const targetFoot=footerRows.length?footerRows[footerRows.length-1]:null;
if(!targetFoot||targetFoot.querySelector('.msg-usage-inline,.msg-duration-inline,.msg-gateway-inline,.gateway-failover-inline,.msg-model-warning-inline')) continue;
2026-05-04 03:14:49 +00:00
const fragments=[];
if(modelWarningText){
const warning=document.createElement('span');
warning.className='msg-model-warning-inline';
warning.textContent=modelWarningText;
fragments.push(warning);
}
if(failoverText){
const failover=document.createElement('span');
failover.className='gateway-failover-inline';
failover.textContent=failoverText;
fragments.push(failover);
}
if(gatewayText){
const gateway=document.createElement('span');
gateway.className='msg-gateway-inline';
gateway.textContent=gatewayText;
fragments.push(gateway);
}
2026-05-04 03:14:49 +00:00
if(durationText){
const duration=document.createElement('span');
duration.className='msg-duration-inline';
duration.textContent=`Done in ${durationText}`;
fragments.push(duration);
}
if(window._showTokenUsage&&hasTurnUsage){
const usage=document.createElement('span');
usage.className='msg-usage-inline';
const inTok=msg._turnUsage.input_tokens||0;
const outTok=msg._turnUsage.output_tokens||0;
const cost=msg._turnUsage.estimated_cost;
let text=`${_fmtTokens(inTok)} in · ${_fmtTokens(outTok)} out`;
if(cost) text+=` · ~$${cost<0.01?cost.toFixed(4):cost.toFixed(2)}`;
usage.textContent=text;
fragments.push(usage);
}
if(fragments.length){
targetFoot.classList.add('msg-foot-with-usage');
for(let i=fragments.length-1;i>=0;i--) targetFoot.insertBefore(fragments[i], targetFoot.firstChild);
}
feat: Sprint 23 — agentic transparency + polish Track A: Token/cost display - Read agent usage attrs (session_prompt_tokens, session_completion_tokens, session_estimated_cost_usd) after run_conversation in streaming.py - Add input_tokens, output_tokens, estimated_cost fields to Session model - Include usage in done SSE event payload - Store usage on S.lastUsage in messages.js done handler - Render usage badge below last assistant message (input/output/cost) Track B: Subagent delegation cards - Add subagent_progress to toolIcon map with shuffle emoji - Special-case subagent_progress in buildToolCard: "Subagent" label, strip double emoji from preview, add tool-card-subagent CSS class - Indented border-left styling for subagent cards - Clean delegate_task display name Track C: Skill picker in cron create form - Add skill search input + tag chips to cron create form HTML - Skill picker JS in panels.js: search/filter, click-to-add tags, remove tag chips, pre-fetch skill list on form open - submitCronCreate sends skills array in POST body - Skill picker dropdown + tag CSS Track D: Skill linked files viewer - Add file query param to /api/skills/content endpoint - Serve linked files from skill directory with path traversal protection - Ensure linked_files key always present in skill content response - Render linked files section below SKILL.md content in preview panel - openSkillFile function for viewing individual linked files Track E: Bug fixes and code quality - Expand Session.__init__ and compact() to readable multi-line format - Remove inline import json as _j2 inside loop in streaming.py - Fix tool_calls: capture args from assistant messages, skip unresolved names - Store args snapshot in persisted tool_calls for reload display 6 new tests. Total: 421 (409 passing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
}
}
// Only force-scroll when not actively streaming — mid-stream re-renders
// (tool completion, session switch) must not override the user's scroll position.
// scrollIfPinned() respects _scrollPinned, so it's a no-op if user scrolled up.
if(S.activeStreamId){
scrollIfPinned();
} else {
scrollToBottom();
}
// Apply syntax highlighting after DOM is built
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
requestAnimationFrame(()=>{highlightCode();addCopyButtons();loadDiffInline();loadCsvInline();loadExcalidrawInline();loadPdfInline();loadHtmlInline();renderMermaidBlocks();renderKatexBlocks();});
requestAnimationFrame(()=>{highlightCode();addCopyButtons();initTreeViews();loadPdfInline();loadHtmlInline();renderMermaidBlocks();renderKatexBlocks();});
// Refresh todo panel if it's currently open
if(typeof loadTodos==='function' && document.getElementById('panelTodos') && document.getElementById('panelTodos').classList.contains('active')){
loadTodos();
}
// Apply persisted playback speed after media nodes are rendered.
if(typeof _applyMediaPlaybackPreferences==='function') _applyMediaPlaybackPreferences(inner);
// Populate session cache so switching back here skips a full rebuild.
_sessionHtmlCacheSid=sid;
2026-05-01 13:29:10 +00:00
if(sid&&!hasTransientTranscriptUi){
const _html=inner.innerHTML;
// Only cache sessions with <300KB rendered HTML; evict oldest beyond 8 sessions.
if(_html.length<300_000){
_sessionHtmlCache.set(sid,{html:_html,msgCount,renderWindowSize});
if(_sessionHtmlCache.size>8){_sessionHtmlCache.delete(_sessionHtmlCache.keys().next().value);}
}
}
}
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
function _toolDisplayName(tc){
const name=(tc&&tc.name)||'tool';
if(name==='subagent_progress') return 'Subagent';
if(name==='delegate_task') return 'Delegate task';
return name;
}
function toolIcon(name){
v0.44.0: approval fix, login CSP, update diagnostics, Lucide icons * fix: approval pending check broken by stale has_pending import (#228) api/routes.py imported has_pending/pop_pending from tools.approval, but the agent module renamed has_pending to has_blocking_approval (checks gateway queue, not _pending dict) and removed pop_pending. The import fell through to fallback lambdas that always returned False, making GET /api/approval/pending always return {pending:null} even after a successful inject_test. Fix: check _pending directly under _lock — same dict submit_pending writes to. Stale imports removed. Before: 554 pass, 1 fail | After: 555 pass, 0 fail * fix: move login JS into external file, remove inline handlers (#226) Login page used inline onsubmit/onkeydown handlers and an inline <script> block — all blocked by strict script-src CSP, causing silent login failure. Fix: extract doLogin() and Enter key listener into static/login.js (served from /static/, already a public path). Form uses id='login-form' and data-* attributes for i18n strings instead of injected JS literals. Also guards res.json() parse with try/catch so non-JSON error bodies (e.g. HTTP 500) show the password-error fallback instead of 'Connection failed'. Fixes #222. * fix: improve update error messages when pull fails (#227) _apply_update_inner() ran git pull --ff-only and returned only raw stderr on failure, making all failure modes indistinguishable. Fix: explicit git fetch before pull; if fetch fails, returns human-readable network error. Diverged history and missing upstream tracking branch each get distinct messages with exact recovery commands. Generic fallback truncates to 300 chars and shows sentinel when git produces no output. Also adds tests/test_update_checker.py with 13 tests covering all 4 new diagnostic code paths (0 tests existed before). Fixes #223. * fix: stabilize 30s terminal approval prompt visibility (#225) Adds minimum 30-second visibility guard for the approval card using _approvalVisibleSince, _approvalHideTimer, and a signature fingerprint to deduplicate repeated poll ticks. Fix: respondApproval() and all stream-end paths (done/cancel/apperror/ error/start-error) now call hideApprovalCard(true) so the card hides immediately when the user responds or the session ends. The 30s guard only applies to mid-session poll ticks where the approval is still live but briefly absent. Adds 11 structural tests covering the new timer variables, force parameter, force-on-respond, force-on-stream-end, and poll-loop no-force behavior. * feat: replace emoji icons with self-hosted Lucide SVG icons (#221) Replaces all sidebar/button emoji icons with SVG paths from Lucide bundled in static/icons.js (no CDN dependency). Adds li(name) function returning inline SVG geometry from a hardcoded whitelist — unknown keys return '' so dynamic server-supplied names never inject arbitrary SVG. Changes: - static/icons.js: new file with 21 icon paths + li() renderer - static/index.html: all nav/action buttons now use li() icons - static/ui.js: toolIcon(), fileIcon() use li() for tool/file icons - static/messages.js: cancelStream button uses SVG square stop icon - .gitignore: adds node_modules/ entry Verified: all 35 onclick= functions exist in JS, all 21 li() calls reference defined icons, applyBotName() selectors intact, version label present, no removed IDs referenced by JS. * docs: v0.44.0 release notes, bump version, update test counts --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 17:02:28 +00:00
const icons={
terminal: li('terminal'),
read_file: li('file-text'),
write_file: li('file-pen'),
search_files: li('search'),
web_search: li('globe'),
web_extract: li('globe'),
execute_code: li('play'),
patch: li('wrench'),
memory: li('brain'),
skill_manage: li('book-open'),
todo: li('list-todo'),
cronjob: li('clock'),
delegate_task: li('bot'),
send_message: li('message-square'),
browser_navigate:li('globe'),
vision_analyze: li('eye'),
subagent_progress:li('shuffle'),
};
return icons[name]||li('wrench');
}
function buildToolCard(tc){
const row=document.createElement('div');
row.className='tool-card-row';
const icon=toolIcon(tc.name);
const hasDetail=tc.snippet||(tc.args&&Object.keys(tc.args).length>0);
let displaySnippet='';
if(tc.snippet){
const s=tc.snippet;
if(s.length<=800){displaySnippet=s;}
else{
const cutoff=s.slice(0,800);
const lastBreak=Math.max(cutoff.lastIndexOf('. '),cutoff.lastIndexOf('\n'),cutoff.lastIndexOf('; '));
displaySnippet=lastBreak>80?s.slice(0,lastBreak+1):cutoff;
}
}
const hasMore=tc.snippet&&tc.snippet.length>displaySnippet.length;
const runIndicator=tc.done===false?'<span class="tool-card-running-dot"></span>':'';
feat: Sprint 23 — agentic transparency + polish Track A: Token/cost display - Read agent usage attrs (session_prompt_tokens, session_completion_tokens, session_estimated_cost_usd) after run_conversation in streaming.py - Add input_tokens, output_tokens, estimated_cost fields to Session model - Include usage in done SSE event payload - Store usage on S.lastUsage in messages.js done handler - Render usage badge below last assistant message (input/output/cost) Track B: Subagent delegation cards - Add subagent_progress to toolIcon map with shuffle emoji - Special-case subagent_progress in buildToolCard: "Subagent" label, strip double emoji from preview, add tool-card-subagent CSS class - Indented border-left styling for subagent cards - Clean delegate_task display name Track C: Skill picker in cron create form - Add skill search input + tag chips to cron create form HTML - Skill picker JS in panels.js: search/filter, click-to-add tags, remove tag chips, pre-fetch skill list on form open - submitCronCreate sends skills array in POST body - Skill picker dropdown + tag CSS Track D: Skill linked files viewer - Add file query param to /api/skills/content endpoint - Serve linked files from skill directory with path traversal protection - Ensure linked_files key always present in skill content response - Render linked files section below SKILL.md content in preview panel - openSkillFile function for viewing individual linked files Track E: Bug fixes and code quality - Expand Session.__init__ and compact() to readable multi-line format - Remove inline import json as _j2 inside loop in streaming.py - Fix tool_calls: capture args from assistant messages, skip unresolved names - Store args snapshot in persisted tool_calls for reload display 6 new tests. Total: 421 (409 passing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
const isSubagent=tc.name==='subagent_progress';
const isDelegation=tc.name==='delegate_task';
const cardClass='tool-card'+(tc.done===false?' tool-card-running':'')+(isSubagent?' tool-card-subagent':'');
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
// Clean up legacy subagent prefixes since the Lucide icon already shows it
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
let displayName=_toolDisplayName(tc);
feat: Sprint 23 — agentic transparency + polish Track A: Token/cost display - Read agent usage attrs (session_prompt_tokens, session_completion_tokens, session_estimated_cost_usd) after run_conversation in streaming.py - Add input_tokens, output_tokens, estimated_cost fields to Session model - Include usage in done SSE event payload - Store usage on S.lastUsage in messages.js done handler - Render usage badge below last assistant message (input/output/cost) Track B: Subagent delegation cards - Add subagent_progress to toolIcon map with shuffle emoji - Special-case subagent_progress in buildToolCard: "Subagent" label, strip double emoji from preview, add tool-card-subagent CSS class - Indented border-left styling for subagent cards - Clean delegate_task display name Track C: Skill picker in cron create form - Add skill search input + tag chips to cron create form HTML - Skill picker JS in panels.js: search/filter, click-to-add tags, remove tag chips, pre-fetch skill list on form open - submitCronCreate sends skills array in POST body - Skill picker dropdown + tag CSS Track D: Skill linked files viewer - Add file query param to /api/skills/content endpoint - Serve linked files from skill directory with path traversal protection - Ensure linked_files key always present in skill content response - Render linked files section below SKILL.md content in preview panel - openSkillFile function for viewing individual linked files Track E: Bug fixes and code quality - Expand Session.__init__ and compact() to readable multi-line format - Remove inline import json as _j2 inside loop in streaming.py - Fix tool_calls: capture args from assistant messages, skip unresolved names - Store args snapshot in persisted tool_calls for reload display 6 new tests. Total: 421 (409 passing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
let previewText=tc.preview||displaySnippet||'';
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
if(isSubagent) previewText=previewText.replace(/^(?:\u{1F500}|↳)\s*/u,'');
row.innerHTML=`
feat: Sprint 23 — agentic transparency + polish Track A: Token/cost display - Read agent usage attrs (session_prompt_tokens, session_completion_tokens, session_estimated_cost_usd) after run_conversation in streaming.py - Add input_tokens, output_tokens, estimated_cost fields to Session model - Include usage in done SSE event payload - Store usage on S.lastUsage in messages.js done handler - Render usage badge below last assistant message (input/output/cost) Track B: Subagent delegation cards - Add subagent_progress to toolIcon map with shuffle emoji - Special-case subagent_progress in buildToolCard: "Subagent" label, strip double emoji from preview, add tool-card-subagent CSS class - Indented border-left styling for subagent cards - Clean delegate_task display name Track C: Skill picker in cron create form - Add skill search input + tag chips to cron create form HTML - Skill picker JS in panels.js: search/filter, click-to-add tags, remove tag chips, pre-fetch skill list on form open - submitCronCreate sends skills array in POST body - Skill picker dropdown + tag CSS Track D: Skill linked files viewer - Add file query param to /api/skills/content endpoint - Serve linked files from skill directory with path traversal protection - Ensure linked_files key always present in skill content response - Render linked files section below SKILL.md content in preview panel - openSkillFile function for viewing individual linked files Track E: Bug fixes and code quality - Expand Session.__init__ and compact() to readable multi-line format - Remove inline import json as _j2 inside loop in streaming.py - Fix tool_calls: capture args from assistant messages, skip unresolved names - Store args snapshot in persisted tool_calls for reload display 6 new tests. Total: 421 (409 passing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
<div class="${cardClass}">
<div class="tool-card-header" onclick="this.closest('.tool-card').classList.toggle('open')">
${runIndicator}
<span class="tool-card-icon">${icon}</span>
feat: Sprint 23 — agentic transparency + polish Track A: Token/cost display - Read agent usage attrs (session_prompt_tokens, session_completion_tokens, session_estimated_cost_usd) after run_conversation in streaming.py - Add input_tokens, output_tokens, estimated_cost fields to Session model - Include usage in done SSE event payload - Store usage on S.lastUsage in messages.js done handler - Render usage badge below last assistant message (input/output/cost) Track B: Subagent delegation cards - Add subagent_progress to toolIcon map with shuffle emoji - Special-case subagent_progress in buildToolCard: "Subagent" label, strip double emoji from preview, add tool-card-subagent CSS class - Indented border-left styling for subagent cards - Clean delegate_task display name Track C: Skill picker in cron create form - Add skill search input + tag chips to cron create form HTML - Skill picker JS in panels.js: search/filter, click-to-add tags, remove tag chips, pre-fetch skill list on form open - submitCronCreate sends skills array in POST body - Skill picker dropdown + tag CSS Track D: Skill linked files viewer - Add file query param to /api/skills/content endpoint - Serve linked files from skill directory with path traversal protection - Ensure linked_files key always present in skill content response - Render linked files section below SKILL.md content in preview panel - openSkillFile function for viewing individual linked files Track E: Bug fixes and code quality - Expand Session.__init__ and compact() to readable multi-line format - Remove inline import json as _j2 inside loop in streaming.py - Fix tool_calls: capture args from assistant messages, skip unresolved names - Store args snapshot in persisted tool_calls for reload display 6 new tests. Total: 421 (409 passing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
<span class="tool-card-name">${esc(displayName)}</span>
<span class="tool-card-preview">${esc(previewText)}</span>
${hasDetail?`<span class="tool-card-toggle">${li('chevron-right',12)}</span>`:''}
</div>
${hasDetail?`<div class="tool-card-detail">
${tc.args&&Object.keys(tc.args).length?`<div class="tool-card-args">${
Object.entries(tc.args).map(([k,v])=>`<div><span class="tool-arg-key">${esc(k)}</span> <span class="tool-arg-val">${esc(String(v))}</span></div>`).join('')
}</div>`:''}
${displaySnippet?`<div class="tool-card-result">
<pre>${esc(displaySnippet)}</pre>
${hasMore?`<button class="tool-card-more" data-full="${esc(tc.snippet||'').replace(/"/g,'&quot;')}" data-short="${esc(displaySnippet||'').replace(/"/g,'&quot;')}" onclick="event.stopPropagation();const p=this.previousElementSibling;const full=this.dataset.full;const short=this.dataset.short;p.textContent=p.textContent===short?full:short;this.textContent=p.textContent===short?'Show more':'Show less'">Show more</button>`:''}
</div>`:''}
</div>`:''}
</div>`;
return row;
}
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
function _syncToolCallGroupSummary(group){
if(!group) return;
const cards=Array.from(group.querySelectorAll('.tool-card-row .tool-card'));
const toolCount=cards.length;
const thinkingCount=group.querySelectorAll('.agent-activity-thinking .thinking-card').length;
const names=cards.map(card=>{
const el=card.querySelector('.tool-card-name');
return el?String(el.textContent||'').trim():'';
}).filter(Boolean);
const uniqueNames=[...new Set(names)];
const label=group.querySelector('.tool-call-group-label');
const list=group.querySelector('.tool-call-group-list');
const badge=group.querySelector('.tool-call-group-count');
2026-05-04 03:14:49 +00:00
const durationEl=group.querySelector('.tool-call-group-duration');
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const parts=[];
if(thinkingCount) parts.push('thinking');
if(uniqueNames.length) parts.push(uniqueNames.slice(0,5).join(', ')+(uniqueNames.length>5?'…':''));
const total=toolCount+thinkingCount;
if(label){
if(thinkingCount&&toolCount) label.textContent=`Activity: thinking + ${toolCount} tool${toolCount===1?'':'s'}`;
else if(thinkingCount) label.textContent='Activity: thinking';
else if(toolCount) label.textContent=`Activity: ${toolCount} tool${toolCount===1?'':'s'}`;
else label.textContent='Activity';
}
if(list) list.textContent=parts.join(' · ')||'tools / thinking';
2026-05-04 03:14:49 +00:00
if(durationEl){
const durationText=_formatTurnDuration(group.dataset.turnDuration);
durationEl.textContent=durationText?`Done in ${durationText}`:'';
durationEl.style.display=durationText?'':'none';
}
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
if(badge) badge.textContent=String(total);
}
// ── Live tool card helpers (called during SSE streaming) ──
// Live cards are inserted INLINE inside #msgInner (tagged with data-live-tid)
// so the streaming layout matches the settled layout produced by renderMessages
// (user → thinking → tool cards → response). The legacy #liveToolCards
// sibling container is no longer used for placement — keeping the cards in the
// message column eliminates the visible "jump" users saw when renderMessages
// fired on the done event.
function appendLiveToolCard(tc){
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031) * fix: remove orphaned i18n keys from top-level LOCALES object Three Traditional Chinese translation keys (cmd_status, memory_saved, profile_delete_title) were placed outside any locale block between the en and ru blocks in static/i18n.js. They became top-level properties of the LOCALES object, causing them to appear as invalid language options in the Settings > Preferences dropdown. The correct translations already exist in the zh-Hant locale block. Fixes #1008 * fix: block stale SSE events from polluting new session's DOM - appendThinking(): guard with !S.session||!S.activeStreamId to drop events from a previous session's SSE stream during a session switch - appendLiveToolCard(): same guard for consistency - finalizeThinkingCard(): scroll thinking-card-body to top when scroll is pinned, so completed response is immediately visible - appendThinking(): auto-scroll thinking card body to bottom while streaming if user is watching (scroll pinned) * Fix empty agent sessions in sidebar * fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status Fixes #995 — three sub-issues in the Cron Jobs UI: 1. Dual play icons ambiguous: Resume button now shows a distinct play+bar icon (play triangle + vertical line) instead of the identical triangle used by Run now. 2. Toast notification overlapping header buttons: Added position:relative; z-index:10 to .main-view-header so it stacks above the fixed toast (z-index:100 within its layer). 3. No running status after trigger: After triggering a job, the status badge immediately shows 'running…' with a CSS spinner animation, and polls the cron list every 3s (up to 30s) to refresh when the job completes. - Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant) - Added .detail-badge.running CSS class with spinner animation - New functions: _setCronDetailStatus(), _startCronRunningPoll() * fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback - _clearCronDetail() now clears _cronRunningPoll interval on navigation - Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker) - When poll ends (30s max), detail re-renders with actual status as fallback * feat: create folder and add space directly from UI (#782) - After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog - Add Create folder if it doesnt exist checkbox in the New Space form - Backend: support create flag in /api/workspaces/add to mkdir before validation - i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales * fix: validate workspace path before mkdir to prevent orphan directories Review feedback (critical): the previous code called mkdir() before validate_workspace_to_add(), which meant a rejected path (e.g. system dir) would leave an orphan directory on disk. New flow: 1. Resolve path and check against blocked system roots BEFORE any mutation 2. mkdir() only if path passes the blocklist check 3. Full validation (exists, is_dir) after mkdir Also imports _workspace_blocked_roots for the pre-mutation blocklist check. * fix(#1014): classify model-not-found errors with helpful message - Add model_not_found error type to streaming.py exception classifier - Detect 404, 'not found', 'does not exist', 'invalid model' patterns - Strip HTML tags from provider error messages (nginx 404 pages, etc.) - Add model_not_found branch to apperror handler in messages.js - Add i18n key model_not_found_label in all 6 locales - 15 tests covering detection, sanitization, frontend, and i18n * feat(ui): add live TPS stat to header Adds a TPS (Tokens Per Second) chip to the right of the header title bar that updates live while AI output is streaming. Metering (api/metering.py) - Tracks per-session output + reasoning tokens via GlobalMeter singleton - Per-session TPS = total_tokens / elapsed_time - Global TPS = average of active sessions' TPS values - HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling window (only recorded when > 0, so idle periods are excluded) - Thread-safe with a single lock Metering events emitted from streaming.py - Throttled at 100ms from token/reasoning/tool callbacks so the display updates rapidly during fast token streams - 1Hz ticker as fallback for slow streams (exits when no active sessions) - Final stats emitted on stream end Routes (api/routes.py) - Removed POST /api/metering/interval endpoint (dynamic interval via focus/blur was replaced with simple always-1s-when-active approach) UI (static/messages.js, index.html, style.css) - TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low' - Default: '0.0 t/s . 0.0 high' when idle - Display updates on every metering SSE event (throttled to 100ms) * feat: session restore speed + title gen reasoning hardening (#1025, #1026) PR #1025 (@franksong2702): Speed up large session restore paths - GET /api/session?messages=0 now parses only metadata before the messages array - Metadata-only loads no longer populate the full-session LRU cache - Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup - Hard reload no longer waits for populateModelDropdown() before restoring session PR #1026 (@franksong2702): Harden auto title generation for reasoning models - Raises title-gen completion budget to 512 tokens (reasoning-safe) - Retries once with 1024 tokens on empty content / finish_reason:length - Applies retry to both auxiliary and active-agent fallback routes - Preserves underlying failure reason in title_status on local fallback Co-authored-by: Frank Song <franksong2702@gmail.com> * feat: session attention indicators in right slot + last_message_at timestamps (#1024) PR #1024 (@franksong2702): Polish session attention indicators - Streaming spinners and unread dots now reuse the right-side actions slot - Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps - Date group carets point down when expanded, right when collapsed - Pinned group no longer repeats pinned-star icon per row - Running indicators appear immediately after send (local busy state while /api/sessions catches up) - Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message) so metadata-only saves don't make old sessions appear under Today Co-authored-by: Frank Song <franksong2702@gmail.com> * docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36) --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Josh <josh@fyul.link> Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
// Guard: ignore if session was switched. Prevents stale tool events from
// a previous session's SSE stream from manipulating the new session's DOM.
if(!S.session||!S.activeStreamId) return;
let turn=$('liveAssistantTurn');
if(!turn){
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
turn=_createAssistantTurn();
turn.id='liveAssistantTurn';
if(S.session) turn.dataset.sessionId=S.session.session_id; // see #1366
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
$('msgInner').appendChild(turn);
}
const inner=_assistantTurnBlocks(turn);
if(!inner) return;
const tid=tc.tid||'';
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
if(!isSimplifiedToolCalling()){
// Update existing card in place (tool_complete after tool_start)
if(tid){
const existing=inner.querySelector(`.tool-card-row[data-live-tid="${CSS.escape(tid)}"]`);
if(existing){
const replacement=buildToolCard(tc);
replacement.dataset.liveTid=tid;
existing.replaceWith(replacement);
// Keep #toolRunningRow alive — dots stay until text starts streaming
// or the next tool fires (which replaces them). Removing here caused
// a gap between tool completion and the first text token arriving.
return;
}
}
const row=buildToolCard(tc);
if(tid) row.dataset.liveTid=tid;
// Insert after whichever comes last: the current live assistant segment or
// the last tool card. This handles both cases:
// text → tool1 → tool2 (no text between tools: anchor is card1)
// text1 → tool1 → text2 → tool2 (text between tools: anchor is text2)
const children=Array.from(inner.children);
// Include .thinking-card-row so tool cards land AFTER a finalized thinking
// card, not between the text segment and thinking.
const anchor=children.filter(el=>el.matches('[data-live-assistant="1"],.tool-card-row,.thinking-card-row')).pop();
if(anchor) anchor.insertAdjacentElement('afterend', row);
else inner.appendChild(row);
// Add a 3-dot waiting indicator below the tool card so there's visual
// feedback while the tool is running. Removed when text starts streaming
// (ensureAssistantRow) or when tool_complete fires.
const oldWait=$('toolRunningRow');if(oldWait)oldWait.remove();
const waitRow=document.createElement('div');
waitRow.id='toolRunningRow';
waitRow.className='assistant-segment';
waitRow.innerHTML='<div class="thinking"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>';
row.insertAdjacentElement('afterend', waitRow);
if(typeof scrollIfPinned==='function') scrollIfPinned();
return;
}
const children=Array.from(inner.children);
const anchor=children.filter(el=>el.matches('[data-live-assistant="1"],.tool-call-group,.tool-card-row,.agent-activity-thinking')).pop();
const group=ensureActivityGroup(inner,{live:true,collapsed:false,anchor});
const body=group.querySelector('.tool-call-group-body');
// Update existing card in place (tool_complete after tool_start)
if(tid){
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const existing=body.querySelector(`.tool-card-row[data-live-tid="${CSS.escape(tid)}"]`);
if(existing){
const replacement=buildToolCard(tc);
replacement.dataset.liveTid=tid;
existing.replaceWith(replacement);
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
_syncToolCallGroupSummary(group);
return;
}
}
const row=buildToolCard(tc);
if(tid) row.dataset.liveTid=tid;
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
body.appendChild(row);
_syncToolCallGroupSummary(group);
if(typeof scrollIfPinned==='function') scrollIfPinned();
}
function clearLiveToolCards(){
const inner=_assistantTurnBlocks($('liveAssistantTurn'));
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
if(inner) inner.querySelectorAll('.tool-call-group[data-live-tool-call-group],.tool-card-row[data-live-tid]').forEach(el=>el.remove());
// Reset the per-turn user expand intent so the next turn starts at the
// default collapsed state (#1298).
if(typeof _clearLiveActivityUserIntent==='function') _clearLiveActivityUserIntent();
// Legacy #liveToolCards container cleanup — kept for safety in case any
// leftover cards were inserted there before this refactor took effect.
const container=$('liveToolCards');
if(container){container.innerHTML='';container.style.display='none';}
}
// ── Edit + Regenerate ──
function editMessage(btn) {
if(S.busy) return;
const row = btn.closest('[data-msg-idx]');
if(!row) return;
const msgIdx = parseInt(row.dataset.msgIdx, 10);
const originalText = row.dataset.rawText || '';
const body = row.querySelector('.msg-body');
if(!body || row.dataset.editing) return;
row.dataset.editing = '1';
// Replace msg-body with an editable textarea
const ta = document.createElement('textarea');
ta.className = 'msg-edit-area';
ta.value = originalText;
body.replaceWith(ta);
// Resize after DOM insertion so scrollHeight is correct
requestAnimationFrame(() => { autoResizeTextarea(ta); ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); });
ta.addEventListener('input', () => autoResizeTextarea(ta));
// Action bar below the textarea
const bar = document.createElement('div');
bar.className = 'msg-edit-bar';
bar.innerHTML = `<button class="msg-edit-send">Send edit</button><button class="msg-edit-cancel">Cancel</button>`;
ta.after(bar);
bar.querySelector('.msg-edit-send').onclick = async () => {
const newText = ta.value.trim();
if(!newText) return;
await submitEdit(msgIdx, newText);
};
bar.querySelector('.msg-edit-cancel').onclick = () => cancelEdit(row, originalText, body);
ta.addEventListener('keydown', e => {
v0.50.264 polish followups: i18n parity + assistant-output readability Closes #1442 (server-side _LOGIN_LOCALE missing ja/pt/ko) Closes #1443 (promote _isImeEnter helper to 6 other Safari Enter guards) Closes #1446 (glued-bold-heading lift for LLM thinking-block output) Closes #1447 (markdown heading visual hierarchy in chat messages) All four issues were filed by the Opus pre-release advisor on the v0.50.264 batch or by Cygnus via Discord (relayed by @AvidFuturist, May 1 2026). They share a common shape — narrow, well-scoped, independent of each other, all adding regression tests. == #1442: _LOGIN_LOCALE parity (api/routes.py + static/i18n.js) == Added entries for ja/pt/ko to the server-side _LOGIN_LOCALE dict that renders the localized login page BEFORE the JS i18n bundle loads. With v0.50.264 shipping Japanese as the 8th built-in locale, ja/pt/ko users were seeing the English login page even with their language preference set. While auditing static/i18n.js for English leakage, also fixed: - ko: 10 user-facing login/sign-out/password keys still in English - es: 3 sign-out/auth-disabled keys still in English Tests: tests/test_login_locale_parity.py (20 tests) — pins both invariants: (a) every locale in i18n.js LOCALES has a matching _LOGIN_LOCALE entry (b) every locale's login-flow keys (13 of them) are translated, not English == #1443: window._isImeEnter promotion == PR #1441 fixed the Safari IME-composition Enter race in the chat composer (`#msg`) by widening the guard from `e.isComposing` to a `_isImeEnter(e)` helper that combines three signals (isComposing || keyCode===229 || _imeComposing flag). Six other Enter-input handlers were left on the original narrow guard and would still drop IME composition Enters on Safari for Japanese/Chinese/Korean users. Promoted the helper to `window._isImeEnter` (defined in static/boot.js) and replaced the `e.isComposing` guards at all six sites: - static/sessions.js: session rename, project create, project rename - static/ui.js: app dialog (confirm/prompt), message edit, workspace rename The state-free part of the helper (`isComposing || keyCode===229`) handles Safari's race for any focused input without needing per-input composition listeners — only `#msg` keeps the local `_imeComposing` flag. Tests: - tests/test_issue1443_ime_helper_promotion.py (9 tests) — pins each site + verifies no raw `e.isComposing` Enter-guards remain in sessions.js/ui.js - tests/test_ime_composition.py — alternation regex extended to accept the windowed helper form (loosen-test-on-shape-change pattern from v0.50.264 reflection notes) == #1446: glued-bold-heading lift (static/ui.js renderMd + Python mirror) == LLMs in thinking/reasoning mode emit "section headers" glued to the end of the previous paragraph with no whitespace: Para 1 text.**Heading to Para 2** Para 2 text.**Heading to Para 3** The renderer correctly produces inline `<strong>` per CommonMark, but it looks like trailing emphasis on the body text rather than a section break. Cygnus reported this as "Markdown feedback 2 of 3." Added a single regex pre-pass in renderMd(): s.replace(/([.!?])\*\*([^*\n]{1,80})\*\*\n\n/g, '$1\n\n**$2**\n\n') Constraints chosen to avoid false positives: - Trigger only on `[.!?]` IMMEDIATELY before `**` (no space) — almost always an LLM-glued heading, not intentional emphasis - Inner text ≤80 chars, no `*` or newline (single-line only) - Trailing `\n\n` required — preserves "this is **important** to know." mid-paragraph emphasis untouched - Position: after rawPreStash restore, before fence_stash restore — fenced code blocks stay protected (their content is `\x00P` / `\x00F` tokens when the lift runs) Mirrored in tests/test_sprint16.py render_md() so both stay in sync. Tests: tests/test_issue1446_glued_heading_lift.py (17 tests, 5 of which drive the actual ui.js renderMd via node) — covers all 3 trigger forms (.!?), all 4 preserve-emphasis cases the issue spec'd, fenced/inline code protection, chained glued headings, source-level position pin, regex shape pin. == #1447: markdown heading visual hierarchy (static/style.css) == Pre-fix sizes in `.msg-body`: h1 18px, h2 16px, h3 14px (= body), h4 13px, h5 12px, h6 11px So h3 was indistinguishable from body and h4/h5/h6 were SMALLER than body. Cygnus's report: "Markdown feedback 3 of 3 — Headings seem to be missing across the board in Hermes. They're there, but all plaintext." New sizes: h1 24px (border-bottom) h2 20px (border-bottom) h3 17px h4 15px h5 14px (uppercase, tracked) h6 13px (uppercase, tracked, muted) All headings now `font-weight:700` + `color:var(--strong)` for stronger ink. h5/h6 use uppercase + letter-spacing for "label-style" affordance instead of being smaller-than-body. Synced .preview-md (file preview pane) to match exactly so a markdown file preview and a chat message render identically. Added missing h4/h5/h6 rules to .preview-md (it only had h1-h3 before). Updated data-font-size="small"/"large" h1-h6 overrides to scale proportionally with the new defaults. Hierarchy preserved at all three font-size settings. Tests: tests/test_issue1447_heading_hierarchy.py (9 tests) — pins the size hierarchy, the bottom borders on h1/h2, the uppercase affordance on h5/h6, the .preview-md sync, and the small/large override scaling. == Verification == pytest tests/ -q → 3748 passed (+56 new) bash ~/WebUI/scripts/run-browser-tests.sh → 20 + 11 PASS bash ~/WebUI/scripts/webui_qa_agent.sh 8789 → 23/23 PASS Visual confirmation in browser at port 8789: - Heading hierarchy clearly visible at all 6 levels - Glued-bold lift produces separate paragraphs as designed - window._isImeEnter accessible from any module after boot.js - Login page renders ja/pt/ko strings correctly (curl -s /login)
2026-05-02 04:04:23 +00:00
if(e.key==='Enter' && !e.shiftKey) { if(window._isImeEnter&&window._isImeEnter(e)) return; e.preventDefault(); bar.querySelector('.msg-edit-send').click(); }
if(e.key==='Escape') { e.preventDefault(); cancelEdit(row, originalText, body); }
});
}
function cancelEdit(row, originalText, originalBody) {
delete row.dataset.editing;
const ta = row.querySelector('.msg-edit-area');
const bar = row.querySelector('.msg-edit-bar');
if(ta) ta.replaceWith(originalBody);
if(bar) bar.remove();
}
function autoResizeTextarea(ta) {
ta.style.height = 'auto';
ta.style.height = Math.min(ta.scrollHeight, 300) + 'px';
}
async function submitEdit(msgIdx, newText) {
if(!S.session || S.busy) return;
// Truncate session at msgIdx (keep messages before the edited one)
// then re-send the edited text
try {
await api('/api/session/truncate', {method:'POST', body:JSON.stringify({
session_id: S.session.session_id,
keep_count: msgIdx // keep messages[0..msgIdx-1], discard from msgIdx onward
})});
S.messages = S.messages.slice(0, msgIdx);
renderMessages();
// Now send the edited message as a new chat
$('msg').value = newText;
await send();
} catch(e) { setStatus(t('edit_failed') + e.message); }
}
async function regenerateResponse(btn) {
if(!S.session || S.busy) return;
// Find the last user message and re-run it
// Remove the last assistant message first (truncate to before it)
const row = btn.closest('[data-msg-idx]');
if(!row) return;
const assistantIdx = parseInt(row.dataset.msgIdx, 10);
// Find the last user message text (one before this assistant message)
let lastUserText = '';
for(let i = assistantIdx - 1; i >= 0; i--) {
const m = S.messages[i];
if(m && m.role === 'user') { lastUserText = msgContent(m); break; }
}
if(!lastUserText) return;
try {
await api('/api/session/truncate', {method:'POST', body:JSON.stringify({
session_id: S.session.session_id,
keep_count: assistantIdx // remove the assistant message
})});
S.messages = S.messages.slice(0, assistantIdx);
renderMessages();
$('msg').value = lastUserText;
await send();
} catch(e) { setStatus(t('regen_failed') + e.message); }
}
function highlightCode(container) {
// Apply Prism.js syntax highlighting to all code blocks in container (or whole messages area)
if(typeof Prism === 'undefined' || !Prism.highlightAllUnder) return;
const el = container || $('msgInner');
if(!el) return;
Prism.highlightAllUnder(el);
}
// Lazy load js-yaml for YAML tree view support
let _jsyamlLoading=false;
function _loadJsyamlThen(cb){
if(typeof jsyaml!=='undefined'){ cb(); return; }
if(_jsyamlLoading){ setTimeout(()=>_loadJsyamlThen(cb),100); return; }
_jsyamlLoading=true;
const s=document.createElement('script');
s.src='https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js';
s.integrity='sha384-8pLvVQkv7pCQqFk7AChLpdEe7gXz9h8GAb7cS0zVeJuKhxR5PU5aEET5pRpHZvxUorzdM';
s.crossOrigin='anonymous';
s.onload=()=>{ _jsyamlLoading=false; cb(); };
s.onerror=()=>{ _jsyamlLoading=false; }; // CDN blocked, fall back to raw
document.head.appendChild(s);
}
function initTreeViews(){
document.querySelectorAll('.code-tree-wrap:not([data-tree-init])').forEach(wrap=>{
const rawText=wrap.dataset.raw;
const lang=wrap.dataset.lang;
let parsed=null;
let parseFailed=false;
// Try JSON parse
try{ parsed=JSON.parse(rawText); }catch(e){ parseFailed=(lang==='json'); }
// YAML: lazy-load js-yaml if needed
if(!parsed && lang==='yaml'){
if(typeof jsyaml!=='undefined'){
try{ parsed=jsyaml.load(rawText); }catch(e){ parseFailed=true; }
}else{
// Defer: remove init marker so we retry after load.
// Note: if CDN load fails, s.onerror does NOT call back —
// the wrap stays un-initialised (raw view only), which is safe.
wrap.removeAttribute('data-tree-init');
_loadJsyamlThen(initTreeViews);
return;
}
}
// Mark as initialised only after we've committed to a render decision
wrap.setAttribute('data-tree-init','1');
if(!parsed || typeof parsed!=='object'){
if(parseFailed){
const hint=wrap.querySelector('.tree-raw-view');
if(hint&&!hint.querySelector('.tree-parse-note')){
const note=document.createElement('div');
note.className='tree-parse-note';
note.textContent=t('parse_failed_note')||'parse failed';
hint.parentNode.insertBefore(note,hint.nextSibling);
}
}
return; // leave as raw view
}
const lineCount=rawText.split('\n').length;
// Default to raw for short blocks (<10 lines), tree for longer
const showTree=lineCount>=10;
// Build tree DOM
const treeDiv=document.createElement('div');
treeDiv.className='tree-view'+(showTree?'':' tree-hidden');
treeDiv.appendChild(_buildTreeDOM(parsed, 0));
// Toggle button in header
const header=wrap.querySelector('.pre-header');
if(header){
const toggle=document.createElement('button');
toggle.className='tree-toggle-btn';
toggle.textContent=showTree?t('raw_view'):t('tree_view');
toggle.onclick=(e)=>{
e.stopPropagation();
const isTreeHidden=treeDiv.classList.contains('tree-hidden');
treeDiv.classList.toggle('tree-hidden',!isTreeHidden);
const rawPre=wrap.querySelector('.tree-raw-view');
if(rawPre) rawPre.style.display=isTreeHidden?'none':'';
toggle.textContent=isTreeHidden?t('raw_view'):t('tree_view');
};
header.style.display='flex';
header.style.justifyContent='space-between';
header.style.alignItems='center';
header.appendChild(toggle);
}
if(!showTree){
const rawPre=wrap.querySelector('.tree-raw-view');
if(rawPre) rawPre.style.display='';
} else {
const rawPre=wrap.querySelector('.tree-raw-view');
if(rawPre) rawPre.style.display='none';
}
wrap.appendChild(treeDiv);
});
}
function _buildTreeDOM(val, depth){
const el=document.createElement('div');
el.className='tree-node';
if(val===null){ el.innerHTML=`<span class="tree-val tree-null">null</span>`; return el; }
if(typeof val==='boolean'){ el.innerHTML=`<span class="tree-val tree-bool">${val}</span>`; return el; }
if(typeof val==='number'){ el.innerHTML=`<span class="tree-val tree-num">${val}</span>`; return el; }
if(typeof val==='string'){ el.innerHTML=`<span class="tree-val tree-str">&quot;${esc(val)}&quot;</span>`; return el; }
if(Array.isArray(val)){
el.classList.add('tree-array');
const collapsed=depth>=2;
const header=document.createElement('span');
header.className='tree-collapsible';
header.innerHTML=(collapsed?'▸ ': '▾ ')+`<span class="tree-bracket">[</span><span class="tree-count">${val.length}</span><span class="tree-bracket">]</span>`;
const body=document.createElement('div');
body.className='tree-children'+(collapsed?' tree-collapsed':'');
val.forEach((item,i)=>{
const child=document.createElement('div');
child.className='tree-item';
child.appendChild(_buildTreeDOM(item, depth+1));
if(i<val.length-1) child.innerHTML+='<span class="tree-comma">,</span>';
body.appendChild(child);
});
el.appendChild(header);
el.appendChild(body);
header.onclick=(()=>{const c=body.classList.contains('tree-collapsed'); body.classList.toggle('tree-collapsed'); header.innerHTML=(c?'▾ ':'▸ ')+`<span class="tree-bracket">[</span><span class="tree-count">${val.length}</span><span class="tree-bracket">]</span>`;});
return el;
}
if(typeof val==='object'){
el.classList.add('tree-object');
const keys=Object.keys(val);
const collapsed=depth>=2;
const header=document.createElement('span');
header.className='tree-collapsible';
header.innerHTML=(collapsed?'▸ ': '▾ ')+`<span class="tree-bracket">{</span><span class="tree-count">${keys.length}</span><span class="tree-bracket">}</span>`;
const body=document.createElement('div');
body.className='tree-children'+(collapsed?' tree-collapsed':'');
keys.forEach((key,i)=>{
const child=document.createElement('div');
child.className='tree-item';
child.innerHTML=`<span class="tree-key">&quot;${esc(key)}&quot;</span><span class="tree-colon">: </span>`;
child.appendChild(_buildTreeDOM(val[key], depth+1));
if(i<keys.length-1) child.innerHTML+='<span class="tree-comma">,</span>';
body.appendChild(child);
});
el.appendChild(header);
el.appendChild(body);
header.onclick=(()=>{const c=body.classList.contains('tree-collapsed'); body.classList.toggle('tree-collapsed'); header.innerHTML=(c?'▾ ':'▸ ')+`<span class="tree-bracket">{</span><span class="tree-count">${keys.length}</span><span class="tree-bracket">}</span>`;});
return el;
}
el.innerHTML=`<span class="tree-val">${esc(String(val))}</span>`;
return el;
}
function addCopyButtons(container){
const el=container||$('msgInner');
if(!el) return;
el.querySelectorAll('pre > code').forEach(codeEl=>{
const pre=codeEl.parentElement;
const header=pre.previousElementSibling;
if(pre.querySelector('.code-copy-btn')||(header&&header.classList.contains('pre-header')&&header.querySelector('.code-copy-btn'))) return;
const btn=document.createElement('button');
btn.className='code-copy-btn';
btn.textContent=t('copy');
btn.onclick=(e)=>{
e.stopPropagation();
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117) * fix(#1096): copy buttons fall back to execCommand on HTTP contexts - Add _copyText() helper: tries navigator.clipboard first, falls back to document.execCommand('copy') with hidden textarea when not in secure context - Update copyMsg() and addCopyButtons() to use helper instead of direct navigator.clipboard.writeText() - Code block copy button now has .catch() handler (was silently failing) - Error messages use t('copy_failed') for i18n instead of hardcoded string - Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant) - Add 10 regression tests * fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge - User message attachments with image extensions now render as <img> via api/media endpoint, with click-to-fullscreen support - Non-image attachments still show paperclip + filename badge - Extracts filename from full path for display - Add 5 regression tests * fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix) * fix: improve mobile touch responsiveness for session list items iPad Safari has known issues with the click/dblclick pattern on touch: - :hover-triggered padding-right layout shift causes the first tap click to target the wrong element (actions button that just appeared) - No touch-action:manipulation means iOS still delays taps for double-tap zoom detection - The old onclick+ondblclick pattern is designed for mouse, not touch Changes: - CSS: Remove :hover from padding-right rule to prevent layout shift - CSS: Add touch-action:manipulation and -webkit-tap-highlight-color to .session-item for immediate tap response - JS: Replace onclick/ondblclick with onpointerup + manual 350ms double-tap detection — works consistently on mouse and touch * fix(#1106): iterate custom_providers[].models dict keys for dropdown population - After reading singular 'model' field, also iterate 'models' dict keys - Deduplicate: model field value not repeated if also in models dict - Skip non-string keys gracefully - Works for both named and unnamed custom_providers entries - Add 7 regression tests * fix(#1105): allow custom_providers hostnames through SSRF check - Build trusted hostname set from custom_providers[].base_url in config.yaml - These are user-explicitly configured endpoints — not SSRF risks - Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active - Unknown private IPs still blocked - Add 7 tests (5 source analysis + 2 functional with mocked socket) * fix(tests): update hover padding assertions for #1110 touch fix (absorb) * fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb) * fix: filter right/middle-click from pointerup handler (absorb) * docs: v0.50.221 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
_copyText(codeEl.textContent).then(()=>{
btn.textContent=t('copied');
setTimeout(()=>{btn.textContent=t('copy');},1500);
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117) * fix(#1096): copy buttons fall back to execCommand on HTTP contexts - Add _copyText() helper: tries navigator.clipboard first, falls back to document.execCommand('copy') with hidden textarea when not in secure context - Update copyMsg() and addCopyButtons() to use helper instead of direct navigator.clipboard.writeText() - Code block copy button now has .catch() handler (was silently failing) - Error messages use t('copy_failed') for i18n instead of hardcoded string - Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant) - Add 10 regression tests * fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge - User message attachments with image extensions now render as <img> via api/media endpoint, with click-to-fullscreen support - Non-image attachments still show paperclip + filename badge - Extracts filename from full path for display - Add 5 regression tests * fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix) * fix: improve mobile touch responsiveness for session list items iPad Safari has known issues with the click/dblclick pattern on touch: - :hover-triggered padding-right layout shift causes the first tap click to target the wrong element (actions button that just appeared) - No touch-action:manipulation means iOS still delays taps for double-tap zoom detection - The old onclick+ondblclick pattern is designed for mouse, not touch Changes: - CSS: Remove :hover from padding-right rule to prevent layout shift - CSS: Add touch-action:manipulation and -webkit-tap-highlight-color to .session-item for immediate tap response - JS: Replace onclick/ondblclick with onpointerup + manual 350ms double-tap detection — works consistently on mouse and touch * fix(#1106): iterate custom_providers[].models dict keys for dropdown population - After reading singular 'model' field, also iterate 'models' dict keys - Deduplicate: model field value not repeated if also in models dict - Skip non-string keys gracefully - Works for both named and unnamed custom_providers entries - Add 7 regression tests * fix(#1105): allow custom_providers hostnames through SSRF check - Build trusted hostname set from custom_providers[].base_url in config.yaml - These are user-explicitly configured endpoints — not SSRF risks - Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active - Unknown private IPs still blocked - Add 7 tests (5 source analysis + 2 functional with mocked socket) * fix(tests): update hover padding assertions for #1110 touch fix (absorb) * fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb) * fix: filter right/middle-click from pointerup handler (absorb) * docs: v0.50.221 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
}).catch(()=>{btn.textContent=t('copy_failed');setTimeout(()=>{btn.textContent=t('copy');},1500);});
};
if(header&&header.classList.contains('pre-header')){
header.style.display='flex';
header.style.justifyContent='space-between';
header.style.alignItems='center';
header.appendChild(btn);
}else{
pre.style.position='relative';
btn.style.cssText='position:absolute;top:6px;right:6px;';
pre.appendChild(btn);
}
});
}
2026-03-31 07:02:47 +00:00
let _mermaidLoading=false;
let _mermaidReady=false;
function loadDiffInline(){
const DIFF_MAX_SIZE=512*1024; // 512 KB cap for inline diff rendering
document.querySelectorAll('.diff-inline-load:not([data-loaded])').forEach(el=>{
el.setAttribute('data-loaded','1');
const path=el.dataset.path;
fetch('api/media?path='+encodeURIComponent(path))
.then(r=>{if(!r.ok) throw new Error(r.status);return r.text();})
.then(text=>{
if(text.length>DIFF_MAX_SIZE){
el.outerHTML=`<div class="diff-inline-error">${esc(path.split('/').pop())}<br><span style="color:var(--muted);font-size:12px">${t('diff_too_large')}</span></div>`;
return;
}
const lines=text.split('\n').map(line=>{
const e=esc(line);
if(e.startsWith('@@')) return `<span class="diff-line diff-hunk">${e}</span>`;
if(e.startsWith('+')) return `<span class="diff-line diff-plus">${e}</span>`;
if(e.startsWith('-')) return `<span class="diff-line diff-minus">${e}</span>`;
return `<span class="diff-line">${e}</span>`;
}).join('\n');
el.outerHTML=`<div class="diff-inline"><div class="pre-header">${esc(path.split('/').pop())}</div><pre class="diff-block"><code>${lines}</code></pre></div>`;
})
.catch(()=>{
el.outerHTML=`<div class="diff-inline-error">${esc(path.split('/').pop())}<br><span style="color:var(--muted);font-size:12px">${t('diff_error')}</span></div>`;
});
});
}
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
function loadCsvInline(){
const CSV_MAX_SIZE=256*1024; // 256 KB cap for inline CSV rendering
document.querySelectorAll('.csv-inline-load:not([data-loaded])').forEach(el=>{
el.setAttribute('data-loaded','1');
const path=el.dataset.path;
fetch('api/media?path='+encodeURIComponent(path))
.then(r=>{if(!r.ok) throw new Error(r.status);return r.text();})
.then(text=>{
if(text.length>CSV_MAX_SIZE){
el.outerHTML=`<div class="diff-inline-error">${esc(path.split('/').pop())}<br><span style="color:var(--muted);font-size:12px">${t('csv_too_large')}</span></div>`;
return;
}
const rows=text.replace(/\r\n/g,'\n').replace(/\r/g,'\n').split('\n').filter(r=>r.trim());
if(rows.length<2){
el.outerHTML=`<div class="diff-inline-error">${esc(path.split('/').pop())}<br><span style="color:var(--muted);font-size:12px">${t('csv_no_data')}</span></div>`;
return;
}
// Auto-detect separator (comma, semicolon, tab)
// Heuristic: uses the first separator found in the header row. Edge case:
// quoted fields containing commas without non-quoted commas in the header
// could cause misdetection — acceptable trade-off for a preview renderer.
const firstLine=rows[0];
const separators=[',',';','\t'];
let sep=separators.find(s=>firstLine.includes(s))||',';
const headers=rows[0].split(sep).map(c=>c.trim().replace(/^["']|["']$/g,''));
const bodyRows=rows.slice(1).map(r=>'<tr>'+r.split(sep).map(c=>`<td>${esc(c.trim().replace(/^["']|["']$/g,''))}</td>`).join('')+'</tr>').join('');
const headerRow=headers.map(h=>`<th>${esc(h)}</th>`).join('');
el.outerHTML=`<div class="csv-table-wrap"><div class="pre-header">${esc(path.split('/').pop())} <span style="opacity:.5;font-size:11px">${t('csv_header_note')}</span></div><table class="csv-table"><thead><tr>${headerRow}</tr></thead><tbody>${bodyRows}</tbody></table></div>`;
})
.catch(()=>{
el.outerHTML=`<div class="diff-inline-error">${esc(path.split('/').pop())}<br><span style="color:var(--muted);font-size:12px">${t('csv_error')}</span></div>`;
});
});
}
function loadExcalidrawInline(){
const EXCALIDRAW_MAX_SIZE=512*1024; // 512 KB cap
document.querySelectorAll('.excalidraw-inline-load:not([data-loaded])').forEach(el=>{
el.setAttribute('data-loaded','1');
const path=el.dataset.path;
fetch('api/media?path='+encodeURIComponent(path))
.then(r=>{if(!r.ok) throw new Error(r.status);return r.text();})
.then(text=>{
if(text.length>EXCALIDRAW_MAX_SIZE){
el.outerHTML=`<div class="diff-inline-error">${esc(path.split('/').pop())}<br><span style="color:var(--muted);font-size:12px">${t('excalidraw_too_large')}</span></div>`;
return;
}
// Validate it looks like Excalidraw JSON
let data;
try{data=JSON.parse(text);}catch(e){
el.outerHTML=`<div class="diff-inline-error">${esc(path.split('/').pop())}<br><span style="color:var(--muted);font-size:12px">${t('excalidraw_invalid')}</span></div>`;
return;
}
if(!data.type||data.type!=='excalidraw'){
el.outerHTML=`<div class="diff-inline-error">${esc(path.split('/').pop())}<br><span style="color:var(--muted);font-size:12px">${t('excalidraw_invalid')}</span></div>`;
return;
}
const fname=esc(path.split('/').pop());
const downloadUrl='api/media?path='+encodeURIComponent(path)+'&download=1';
el.outerHTML=`<div class="excalidraw-embed-wrap" title="${t('excalidraw_simplified')}">
<div class="msg-artifact-header">
<span class="msg-media-label">${t('excalidraw_label')}</span>
<a class="excalidraw-open-link" href="${downloadUrl}" download="${fname}">${t('excalidraw_download')} ${fname}</a>
</div>
<div class="excalidraw-canvas" data-excalidraw='${esc(text)}'></div>
</div>`;
// Lazy-init Excalidraw render after DOM insertion
requestAnimationFrame(()=>_renderExcalidrawCanvases());
})
.catch(()=>{
el.outerHTML=`<div class="diff-inline-error">${esc(path.split('/').pop())}<br><span style="color:var(--muted);font-size:12px">${t('excalidraw_error')}</span></div>`;
});
});
}
let _excalidrawScriptLoaded=false;
function _renderExcalidrawCanvases(){
document.querySelectorAll('.excalidraw-canvas:not([data-rendered])').forEach(el=>{
el.setAttribute('data-rendered','1');
const dataStr=el.getAttribute('data-excalidraw');
if(!dataStr) return;
// Render a simple SVG preview using the Excalidraw elements
try{
const data=JSON.parse(dataStr);
const elements=data.elements||[];
if(!elements.length){el.innerHTML=`<div class="excalidraw-empty">${t('excalidraw_empty')}</div>`;return;}
// Calculate bounds
let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity;
elements.forEach(el=>{
const b=[el.x||0,el.y||0,(el.x||0)+(el.width||0),(el.y||0)+(el.height||0)];
minX=Math.min(minX,b[0]);minY=Math.min(minY,b[1]);
maxX=Math.max(maxX,b[2]);maxY=Math.max(maxY,b[3]);
});
const pad=20;minX-=pad;minY-=pad;maxX+=pad;maxY+=pad;
const w=Math.max(maxX-minX,200);const h=Math.max(maxY-minY,150);
// SVG attributes are rendered via innerHTML below, so attacker-controlled
// values from JSON (e.g. strokeColor='red"/><script>...') would break out
// of the attribute. Escape strings; coerce numerics.
const _sa=v=>String(v==null?'':v).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const _num=(v,fb)=>{const n=Number(v);return Number.isFinite(n)?n:fb;};
const svgParts=[`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${_num(minX,0)} ${_num(minY,0)} ${_num(w,200)} ${_num(h,150)}" class="excalidraw-svg">`];
elements.forEach(el=>{
const stroke=_sa(el.strokeColor||'#1e1e1e');
const fill=_sa(el.backgroundColor||'transparent');
const sw=_num(el.strokeWidth,2);
const x=_num(el.x,0),y=_num(el.y,0),w=_num(el.width,0),h=_num(el.height,0);
if(el.type==='rectangle'){
svgParts.push(`<rect x="${x}" y="${y}" width="${w}" height="${h}" stroke="${stroke}" stroke-width="${sw}" fill="${fill}" rx="${el.roundness?.type===3?8:0}"/>`);
}else if(el.type==='diamond'){
const cx=x+w/2,cy=y+h/2;
svgParts.push(`<polygon points="${cx},${y} ${x+w},${cy} ${cx},${y+h} ${x},${cy}" stroke="${stroke}" stroke-width="${sw}" fill="${fill}"/>`);
}else if(el.type==='ellipse'){
svgParts.push(`<ellipse cx="${x+w/2}" cy="${y+h/2}" rx="${w/2}" ry="${h/2}" stroke="${stroke}" stroke-width="${sw}" fill="${fill}"/>`);
}else if(el.type==='line'){
const pts=(el.points||[]).filter(p=>Array.isArray(p)&&p.length>=2);
if(!pts.length) return;
let d=`M ${_num(x+_num(pts[0][0],0),0)} ${_num(y+_num(pts[0][1],0),0)}`;
for(let i=1;i<pts.length;i++) d+=` L ${_num(x+_num(pts[i][0],0),0)} ${_num(y+_num(pts[i][1],0),0)}`;
svgParts.push(`<path d="${d}" stroke="${stroke}" stroke-width="${sw}" fill="none" stroke-linecap="round" stroke-linejoin="round"/>`);
}else if(el.type==='arrow'){
const pts=(el.points||[]).filter(p=>Array.isArray(p)&&p.length>=2);
if(!pts.length) return;
let d=`M ${_num(x+_num(pts[0][0],0),0)} ${_num(y+_num(pts[0][1],0),0)}`;
for(let i=1;i<pts.length;i++) d+=` L ${_num(x+_num(pts[i][0],0),0)} ${_num(y+_num(pts[i][1],0),0)}`;
svgParts.push(`<path d="${d}" stroke="${stroke}" stroke-width="${sw}" fill="none" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowhead)"/>`);
}else if(el.type==='text'){
const fontSize=_num(el.fontSize,20);
const txt=String(el.text==null?'':el.text);
const lines=txt.split('\n');
lines.forEach((line,i)=>{
svgParts.push(`<text x="${x}" y="${y+i*fontSize*1.2+fontSize}" fill="${stroke}" font-size="${fontSize}" font-family="Virgil, Segoe UI Emoji, sans-serif">${esc(line)}</text>`);
});
}else if(el.type==='draw'){
const pts=(el.points||[]).filter(p=>Array.isArray(p)&&p.length>=2);
if(pts.length>1){
let d=`M ${_num(x+_num(pts[0][0],0),0)} ${_num(y+_num(pts[0][1],0),0)}`;
for(let i=1;i<pts.length;i++) d+=` L ${_num(x+_num(pts[i][0],0),0)} ${_num(y+_num(pts[i][1],0),0)}`;
svgParts.push(`<path d="${d}" stroke="${stroke}" stroke-width="${sw}" fill="none" stroke-linecap="round" stroke-linejoin="round"/>`);
}
}
// Unknown element types (e.g. image, frame, group, freedraw) are
// silently skipped to avoid breaking the render. This is a simplified
// SVG preview, not a pixel-identical Excalidraw canvas reproduction.
});
// Arrow marker definition
svgParts.unshift(`<defs><marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#1e1e1e"/></marker></defs>`);
svgParts.push('</svg>');
el.innerHTML=svgParts.join('');
}catch(e){
el.innerHTML=`<div class="excalidraw-empty">${t('excalidraw_render_error')}</div>`;
}
});
}
// ── PDF inline preview (first page) ────────────────────────────────────────
// NOTE: PDF.js is loaded from CDN (jsdelivr). Offline/air-gapped deployments
// will not get inline previews; the 15 s fallback timeout degrades to a
// download link in that case. The 4 MB size cap is checked client-side after
// the full buffer is received — ideally the server would enforce it before
// streaming (out of scope for this client-side PR).
let _pdfjsReady=false, _pdfjsLoading=false;
function loadPdfInline(){
const PDF_MAX_SIZE=4*1024*1024; // 4 MB cap for inline PDF preview
document.querySelectorAll('.pdf-preview-load:not([data-loaded])').forEach(el=>{
el.setAttribute('data-loaded','1');
const path=el.dataset.path;
const fname=path.split('/').pop()||path;
const loadPdf=(pdfjsLib)=>{
fetch('api/media?path='+encodeURIComponent(path))
.then(r=>{if(!r.ok) throw new Error(r.status); return r.arrayBuffer();})
.then(buf=>{
if(buf.byteLength>PDF_MAX_SIZE){
el.outerHTML=`<div class="pdf-preview-fallback"><a class="msg-media-link" href="api/media?path=${encodeURIComponent(path)}&download=1" download="${esc(fname)}">📎 ${esc(fname)}</a><br><span style="color:var(--muted);font-size:12px">${t('pdf_too_large')}</span></div>`;
return;
}
return pdfjsLib.getDocument({data:buf}).promise;
})
.then(pdf=>{
if(!pdf) return;
pdf.getPage(1).then(page=>{
const canvas=document.createElement('canvas');
const scale=1.5;
const viewport=page.getViewport({scale});
canvas.width=viewport.width;
canvas.height=viewport.height;
canvas.className='pdf-preview-canvas';
page.render({canvasContext:canvas.getContext('2d'),viewport}).promise.then(()=>{
// Canvas bitmap is runtime state, not part of HTML serialization.
// Attach the canvas as a DOM node — interpolating its serialized
// form into a template string parses back as an empty canvas.
const dlUrl='api/media?path='+encodeURIComponent(path)+'&download=1';
const wrap=document.createElement('div');
wrap.className='pdf-preview-wrap';
wrap.innerHTML=`<div class="pdf-preview-header"><span>📄 ${esc(fname)}</span><a href="${dlUrl}" download="${esc(fname)}" class="pdf-download-link">${t('pdf_download')} ↓</a></div><div class="pdf-preview-body"></div>`;
wrap.querySelector('.pdf-preview-body').appendChild(canvas);
el.replaceWith(wrap);
});
});
})
.catch(()=>{
const dlUrl='api/media?path='+encodeURIComponent(path)+'&download=1';
el.outerHTML=`<div class="pdf-preview-fallback"><a class="msg-media-link" href="${dlUrl}" download="${esc(fname)}">📎 ${esc(fname)}</a><br><span style="color:var(--muted);font-size:12px">${t('pdf_error')}</span></div>`;
});
};
if(_pdfjsReady){
loadPdf(window._pdfjsLib);
} else if(!_pdfjsLoading){
_pdfjsLoading=true;
const s=document.createElement('script');
s.src='https://cdn.jsdelivr.net/npm/pdfjs-dist@4.9.155/build/pdf.min.mjs';
s.type='module';
s.textContent=`
import * as pdfjsLib from '${s.src}';
pdfjsLib.GlobalWorkerOptions.workerSrc='https://cdn.jsdelivr.net/npm/pdfjs-dist@4.9.155/build/pdf.worker.min.mjs';
window._pdfjsLib=pdfjsLib;
window._pdfjsReady=true;
window.dispatchEvent(new Event('pdfjs-ready'));
`;
document.head.appendChild(s);
window.addEventListener('pdfjs-ready',()=>{ _pdfjsReady=true; loadPdf(window._pdfjsLib); },{once:true});
setTimeout(()=>{
if(!_pdfjsReady){
const dlUrl='api/media?path='+encodeURIComponent(path)+'&download=1';
if(el.parentNode){
el.outerHTML=`<div class="pdf-preview-fallback"><a class="msg-media-link" href="${dlUrl}" download="${esc(fname)}">📎 ${esc(fname)}</a><br><span style="color:var(--muted);font-size:12px">${t('pdf_error')}</span></div>`;
}
}
},15000);
} else {
window.addEventListener('pdfjs-ready',()=>{ loadPdf(window._pdfjsLib); },{once:true});
}
});
}
// ── HTML inline preview (sandboxed iframe) ─────────────────────────────────
function loadHtmlInline(){
const HTML_MAX_SIZE=256*1024; // 256 KB cap for inline HTML preview
document.querySelectorAll('.html-preview-load:not([data-loaded])').forEach(el=>{
el.setAttribute('data-loaded','1');
const path=el.dataset.path;
const fname=path.split('/').pop()||path;
fetch('api/media?path='+encodeURIComponent(path))
.then(r=>{if(!r.ok) throw new Error(r.status); return r.text();})
.then(html=>{
if(html.length>HTML_MAX_SIZE){
const dlUrl='api/media?path='+encodeURIComponent(path)+'&download=1';
el.outerHTML=`<div class="html-preview-fallback"><a class="msg-media-link" href="${dlUrl}" download="${esc(fname)}">📎 ${esc(fname)}</a><br><span style="color:var(--muted);font-size:12px">${t('html_too_large')}</span></div>`;
return;
}
const dlUrl='api/media?path='+encodeURIComponent(path)+'&download=1';
const safeHtml=html.replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
el.outerHTML=`<div class="html-preview-wrap"><div class="html-preview-header"><span>${t('html_sandbox_label')}</span><a href="${dlUrl}" download="${esc(fname)}" class="html-open-link">${t('html_open_full')} ↗</a></div><iframe srcdoc="${safeHtml}" sandbox="allow-scripts" class="html-preview-iframe" loading="lazy"></iframe></div>`;
})
.catch(()=>{
const dlUrl='api/media?path='+encodeURIComponent(path)+'&download=1';
el.outerHTML=`<div class="html-preview-fallback"><a class="msg-media-link" href="${dlUrl}" download="${esc(fname)}">📎 ${esc(fname)}</a><br><span style="color:var(--muted);font-size:12px">${t('html_error')}</span></div>`;
});
});
}
2026-03-31 07:02:47 +00:00
function renderMermaidBlocks(){
const blocks=document.querySelectorAll('.mermaid-block:not([data-rendered])');
if(!blocks.length) return;
if(!_mermaidReady){
if(!_mermaidLoading){
_mermaidLoading=true;
const script=document.createElement('script');
script.src='https://cdn.jsdelivr.net/npm/mermaid@10.9.3/dist/mermaid.min.js';
script.integrity='sha384-R63zfMfSwJF4xCR11wXii+QUsbiBIdiDzDbtxia72oGWfkT7WHJfmD/I/eeHPJyT';
script.crossOrigin='anonymous';
2026-03-31 07:02:47 +00:00
script.onload=()=>{
if(typeof mermaid!=='undefined'){
mermaid.initialize({startOnLoad:false,theme:document.documentElement.classList.contains('dark')?'dark':'default',themeVariables:{
v0.50.210: gpt-5.5, cron titles, agent cache, bfcache fix, onboarding fix, mermaid CSP, PWA auth (#1056) * feat(models): add gpt-5.5 to openai, openai-codex, copilot catalogs Adds GPT-5.5 and GPT-5.5 Mini entries to the static _PROVIDER_MODELS catalog so they appear in the model picker for the openai, openai-codex, and copilot providers. Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent * fix(models): add gpt-5.5-mini to copilot provider catalog * fix(renderer): suppress Mermaid Google Fonts CSP violation via fontFamily inherit (#1044) Mermaid's built-in 'dark' and 'default' themes inject an @import for fonts.googleapis.com/Manrope into every generated SVG. The CSP style-src only allows cdn.jsdelivr.net, so this request is blocked on every diagram render, filling the console with CSP errors. Fix: pass fontFamily:'inherit' (and fontSize:'14px') in the themeVariables block of mermaid.initialize() in renderMermaidBlocks(). This suppresses Mermaid's external font import and uses the page's existing font stack. Avoids adding fonts.googleapis.com to the CSP — no new external dependency, no font FOUT, consistent with the rest of the UI typography. 3 regression tests added in tests/test_1044_mermaid_csp_font.py. 2215/2215 tests passing. * fix(onboarding): non-standard provider/path cluster (#1029) * fix(bfcache): restore full layout on tab/session restore — rail, topbar, panels (#1045) The pageshow handler added for #822 only cleared the session search filter and re-rendered the session list. This left the rest of the layout chrome (topbar, rail icons, workspace panel, resize handles, gateway SSE) in the stale bfcache DOM state, causing a broken layout (oversized search icon, uninitialized rail) that required a hard refresh to fix. Fix: extend the pageshow handler to re-run the full set of layout sync calls that the boot IIFE runs on a fresh page load: syncTopbar() — restores model chip, title, topbar state syncWorkspacePanelState() — restores workspace panel open/closed _initResizePanels() — reattaches panel resize drag listeners startGatewaySSE() — reconnects the gateway SSE watcher (bfcache-persisted connections are dead) All four calls are typeof-guarded for safe degradation if a helper is not yet defined. The existing #822 fixes (sessionSearch clear + renderSessionListFromCache) are preserved unchanged. loadSession() is intentionally NOT re-called — it would cause message flicker; the sync calls above are sufficient to restore visual state. 7 regression tests added in tests/test_1045_bfcache_layout_restore.py. 2219/2219 tests passing. * fix(bfcache): also close open dropdowns on bfcache restore (#1045) Additional symptom noted in issue #1045: bfcache freezes the DOM including any open dropdown/popover state. The thinking-level selector (and other composer dropdowns) left open when navigating away would appear open without user interaction on tab restore. Extend the pageshow handler to call all four named close functions before the layout sync: closeModelDropdown() — composer model selector closeReasoningDropdown() — thinking/reasoning effort selector closeWsDropdown() — workspace chip dropdown closeProfileDropdown() — profile switcher dropdown All calls are typeof-guarded, matching the style of the layout sync calls already in the handler. 2 new tests (9 total in test_1045_bfcache_layout_restore.py): - pageshow closes all four named dropdowns - dropdown closes appear before layout sync calls (clean state first) 2221/2221 tests passing. * fix(bfcache): remove _initResizePanels() — bfcache preserves listeners * fix(bfcache): remove _initResizePanels from pageshow — bfcache preserves listeners; update test * fix(sessions): use cron job name as session title when available (#1032) * fix(test): add id column to messages table in cron title test fixture * fix(merge): inject cron title lookup into read_importable loop, remove stale sqlite3 block * fix(pwa): redirect to /login client-side on 401 — fixes iOS PWA auth expiry trap (#1038) When an auth session expires, the server returns a 302→/login for page requests. In a normal browser this works fine, but in an iOS PWA running in standalone mode the redirect navigates out of the PWA shell into Safari, leaving the app permanently stuck on 'Authentication required' with no recovery path. Fix: intercept 401 responses client-side before surfacing any error. - workspace.js api(): check res.status===401 first; call window.location.href='/login' and return immediately (no throw) - ui.js: add _redirectIfUnauth() helper; wire into all direct fetch() calls that bypass api() — api/models, api/models/live, api/upload All fetch paths that could receive a 401 now redirect cleanly within the PWA frame rather than opening Safari. 6 regression tests added in tests/test_1038_pwa_auth_redirect.py. 2175/2175 tests passing. * fix(pwa): preserve current URL in ?next= param on 401 redirect * fix(test): update 401-redirect assertion to accept ?next= URL format * feat(pwa): add _safeNextPath() to login.js so ?next= param is honored after re-login Addresses reviewer suggestion: the ?next= URL set on 401 redirect was ignored by the login success handler (always redirected to ./). _safeNextPath() validates and returns the ?next= param with open-redirect guards: rejects non-path-absolute inputs, // protocol-relative URLs, backslash variants, and control characters. 4 new regression tests added. * Implement session agent cache for AIAgent reuse Added session agent cache to reuse AIAgent across messages. * Implement agent caching for session management * Implement session agent eviction on session deletion Added session agent eviction to prevent turn count leakage in recycled sessions. * docs: v0.50.210 release notes — 7 PRs, 2239 tests (+27) * docs(changelog): drop stale [Unreleased] entries duplicated by v0.50.210 Three entries in the [Unreleased] section are duplicates of items now listed under v0.50.210: - Mermaid CSP font fix (#1044) → v0.50.210 / Mermaid Google Fonts CSP - bfcache layout restore (#1045) → v0.50.210 / bfcache layout and dropdown restore - iOS PWA auth redirect (#1038) → v0.50.210 / Login redirects back to original URL The original drafts landed in [Unreleased] when individual PRs (#1047, #1048, #1043) were approved; the v0.50.210 release-notes commit then added the same items under the version section without removing the [Unreleased] copies. Drop the duplicates so users reading the CHANGELOG don't see the same fix listed twice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent Co-authored-by: Pix (Hermes) <aliceisjustplaying@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: qxxaa <mrhanoi@outlook.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:47:44 +00:00
fontFamily:'inherit',fontSize:'14px',
2026-03-31 07:02:47 +00:00
primaryColor:'#4a6fa5',primaryTextColor:'#e2e8f0',lineColor:'#718096',
secondaryColor:'#2d3748',tertiaryColor:'#1a202c',primaryBorderColor:'#4a5568',
}});
_mermaidReady=true;
renderMermaidBlocks();
}
};
document.head.appendChild(script);
}
return;
}
blocks.forEach(async(block)=>{
block.dataset.rendered='true';
const code=block.textContent;
const id=block.dataset.mermaidId||('m-'+Math.random().toString(36).slice(2));
try{
const {svg}=await mermaid.render(id,code);
const tmp=document.getElementById('d'+id);
if(tmp) tmp.remove();
2026-03-31 07:02:47 +00:00
block.innerHTML=svg;
block.classList.add('mermaid-rendered');
}catch(e){
const tmp=document.getElementById('d'+id);
if(tmp) tmp.remove();
// Fall back to showing as a code block. Remove the mermaid marker so a
// later render pass cannot retry this already-failed block.
block.classList.remove('mermaid-block');
block.classList.add('prewrap');
2026-03-31 07:02:47 +00:00
block.innerHTML=`<div class="pre-header">mermaid</div><pre><code>${esc(code)}</code></pre>`;
}
});
}
feat: KaTeX math rendering for LaTeX in chat + workspace previews (#352) * feat: KaTeX math rendering for $..$ and $$..$$ in chat and previews (fixes #347) - Stash math delimiters before markdown pipeline, restore as .katex-block/.katex-inline elements - KaTeX JS lazy-loaded from CDN on first math block (mirrors mermaid pattern) - KaTeX CSS loaded eagerly in <head> to prevent layout shift - SRI hashes on both CDN tags - throwOnError:false — bad LaTeX degrades to code span - Supports $$, $, \\(...\\), \\[...\\] delimiters - 18 new tests, 831/831 passing * fix: remove invalid \' escape sequences in math stash lines Lines 311, 314, 316, 317 had \' (backslash-quote) instead of plain ' in the arrow function bodies. This is a JS syntax error — node --check fails with 'Invalid or unexpected token'. Likely caused by a serialization artifact during code generation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: swap stash order (fence before math) to protect code spans; add renderKatexBlocks to workspace preview - static/ui.js: fence_stash now runs BEFORE math_stash so dollar signs inside backtick code spans are not extracted as math. Previously `$x$` would render as KaTeX inside a <code> tag instead of showing the literal string $x$. - static/workspace.js: add requestAnimationFrame(renderKatexBlocks) after markdown preview renders so math works in workspace file previews, not only in chat messages. * feat: KaTeX math rendering + stash order fix + workspace wiring (#352) - tests/test_issue347.py: 11 new tests (29 total) covering fence-before-math ordering, workspace.js renderKatexBlocks call, stash token distinctness, false-positive prevention, safe-tags boundary check - CHANGELOG.md: v0.50.15 entry; 870 tests total (up from 841) * fix: use literal null byte (\x00M) in math stash token — matches restore regex The original PR's second commit (fix: remove invalid \' escapes) accidentally doubled the backslash in the math stash tokens: '\\x00M' is a 5-char string (backslash + x + 0 + 0 + M) but the restore regex /\x00M/ expects a null byte. Result: $...$ in messages produced visible \x00M0\x00 tokens instead of KaTeX spans. Changed all 4 math stash return statements to use '\x00M' (single backslash = null byte, same convention as fence_stash's '\x00F'). Also updates test_stash_tokens_distinct to check for the correct pattern. * fix: add null-byte token test; update CHANGELOG to v0.50.15 with fixes - tests/test_issue347.py: add test_math_stash_token_uses_single_backslash_null_byte to catch the \\x00M double-backslash regression; 30 tests total (up from 29) - CHANGELOG.md: v0.50.15 entry documents all fixes including the token bug and workspace preview wiring; 871 tests total --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:40:15 +00:00
let _katexLoading=false;
let _katexReady=false;
function renderKatexBlocks(){
const blocks=document.querySelectorAll('.katex-block:not([data-rendered]),.katex-inline:not([data-rendered])');
if(!blocks.length) return;
if(!_katexReady){
if(!_katexLoading){
_katexLoading=true;
const script=document.createElement('script');
script.src='https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js';
script.integrity='sha384-cMkvdD8LoxVzGF/RPUKAcvmm49FQ0oxwDF3BGKtDXcEc+T1b2N+teh/OJfpU0jr6';
script.crossOrigin='anonymous';
script.onload=()=>{
if(typeof katex!=='undefined'){
_katexReady=true;
renderKatexBlocks();
}
};
document.head.appendChild(script);
}
return;
}
blocks.forEach(el=>{
el.dataset.rendered='true';
const src=el.textContent||'';
const displayMode=el.dataset.katex==='display';
try{
katex.render(src,el,{
displayMode,
throwOnError:false,
trust:false,
strict:'ignore',
});
}catch(e){
// Leave as raw text in a code span on failure
el.outerHTML=`<code>${esc(src)}</code>`;
}
});
}
function _thinkingMarkup(text=''){
const clean=_sanitizeThinkingDisplayText(text);
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const openClass=isSimplifiedToolCalling()?'':' open';
return (clean&&String(clean).trim())
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
? `<div class="thinking-card${openClass}"><div class="thinking-card-header" onclick="this.parentElement.classList.toggle('open')"><span class="thinking-card-icon">${li('lightbulb',14)}</span><span class="thinking-card-label">${t('thinking')}</span><span class="thinking-card-toggle">${li('chevron-right',12)}</span></div><div class="thinking-card-body"><pre>${esc(String(clean).trim())}</pre></div></div>`
: `<div class="thinking"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>`;
}
function finalizeThinkingCard(){
// Guard: only finalize thinking card if we're looking at the session that started it.
// Without this check, switching tabs while a stream is running causes finalizeThinkingCard
// to remove/modify the thinking card DOM of the wrong session — the card belongs to the
// stream that started it, not the session currently displayed.
const _guardTurn = $('liveAssistantTurn');
if(_guardTurn && S.session && _guardTurn.dataset.sessionId !== S.session.session_id) return;
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
if(!isSimplifiedToolCalling()){
const row=$('thinkingRow');
if(!row) return;
// If the row is still just a spinner (no thinking content rendered),
// remove it entirely — it's the initial waiting dots.
const hasContent=row.querySelector('.thinking-card') || row.classList.contains('thinking-card-row');
if(!hasContent && row.getAttribute('data-thinking-active')==='1'){
row.remove();
return;
}
// If the user was watching (scroll pinned = at bottom), scroll the thinking
// card back to the top so the completed response is visible underneath without
// the thinking content blocking it. If they scrolled up to read history,
// leave their scroll position intact.
if(_scrollPinned){
const body=row&&row.querySelector('.thinking-card-body');
if(body) body.scrollTop=0;
}
row.removeAttribute('id');
row.removeAttribute('data-thinking-active');
return;
}
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const turn=$('liveAssistantTurn');
const group=turn&&turn.querySelector('.tool-call-group[data-live-tool-call-group="1"]');
if(group){
// Respect the user's explicit expand intent (#1298) — only force-collapse
// when the user has not manually expanded this turn's activity group, or
// has manually collapsed it. Otherwise the panel snaps shut whenever new
// activity arrives, even mid-read.
if(_liveActivityUserExpanded !== true){
group.classList.add('tool-call-group-collapsed');
const summary=group.querySelector('.tool-call-group-summary');
if(summary) summary.setAttribute('aria-expanded','false');
}
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const active=group.querySelector('.agent-activity-thinking[data-thinking-active="1"]');
if(active) active.removeAttribute('data-thinking-active');
_syncToolCallGroupSummary(group);
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031) * fix: remove orphaned i18n keys from top-level LOCALES object Three Traditional Chinese translation keys (cmd_status, memory_saved, profile_delete_title) were placed outside any locale block between the en and ru blocks in static/i18n.js. They became top-level properties of the LOCALES object, causing them to appear as invalid language options in the Settings > Preferences dropdown. The correct translations already exist in the zh-Hant locale block. Fixes #1008 * fix: block stale SSE events from polluting new session's DOM - appendThinking(): guard with !S.session||!S.activeStreamId to drop events from a previous session's SSE stream during a session switch - appendLiveToolCard(): same guard for consistency - finalizeThinkingCard(): scroll thinking-card-body to top when scroll is pinned, so completed response is immediately visible - appendThinking(): auto-scroll thinking card body to bottom while streaming if user is watching (scroll pinned) * Fix empty agent sessions in sidebar * fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status Fixes #995 — three sub-issues in the Cron Jobs UI: 1. Dual play icons ambiguous: Resume button now shows a distinct play+bar icon (play triangle + vertical line) instead of the identical triangle used by Run now. 2. Toast notification overlapping header buttons: Added position:relative; z-index:10 to .main-view-header so it stacks above the fixed toast (z-index:100 within its layer). 3. No running status after trigger: After triggering a job, the status badge immediately shows 'running…' with a CSS spinner animation, and polls the cron list every 3s (up to 30s) to refresh when the job completes. - Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant) - Added .detail-badge.running CSS class with spinner animation - New functions: _setCronDetailStatus(), _startCronRunningPoll() * fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback - _clearCronDetail() now clears _cronRunningPoll interval on navigation - Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker) - When poll ends (30s max), detail re-renders with actual status as fallback * feat: create folder and add space directly from UI (#782) - After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog - Add Create folder if it doesnt exist checkbox in the New Space form - Backend: support create flag in /api/workspaces/add to mkdir before validation - i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales * fix: validate workspace path before mkdir to prevent orphan directories Review feedback (critical): the previous code called mkdir() before validate_workspace_to_add(), which meant a rejected path (e.g. system dir) would leave an orphan directory on disk. New flow: 1. Resolve path and check against blocked system roots BEFORE any mutation 2. mkdir() only if path passes the blocklist check 3. Full validation (exists, is_dir) after mkdir Also imports _workspace_blocked_roots for the pre-mutation blocklist check. * fix(#1014): classify model-not-found errors with helpful message - Add model_not_found error type to streaming.py exception classifier - Detect 404, 'not found', 'does not exist', 'invalid model' patterns - Strip HTML tags from provider error messages (nginx 404 pages, etc.) - Add model_not_found branch to apperror handler in messages.js - Add i18n key model_not_found_label in all 6 locales - 15 tests covering detection, sanitization, frontend, and i18n * feat(ui): add live TPS stat to header Adds a TPS (Tokens Per Second) chip to the right of the header title bar that updates live while AI output is streaming. Metering (api/metering.py) - Tracks per-session output + reasoning tokens via GlobalMeter singleton - Per-session TPS = total_tokens / elapsed_time - Global TPS = average of active sessions' TPS values - HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling window (only recorded when > 0, so idle periods are excluded) - Thread-safe with a single lock Metering events emitted from streaming.py - Throttled at 100ms from token/reasoning/tool callbacks so the display updates rapidly during fast token streams - 1Hz ticker as fallback for slow streams (exits when no active sessions) - Final stats emitted on stream end Routes (api/routes.py) - Removed POST /api/metering/interval endpoint (dynamic interval via focus/blur was replaced with simple always-1s-when-active approach) UI (static/messages.js, index.html, style.css) - TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low' - Default: '0.0 t/s . 0.0 high' when idle - Display updates on every metering SSE event (throttled to 100ms) * feat: session restore speed + title gen reasoning hardening (#1025, #1026) PR #1025 (@franksong2702): Speed up large session restore paths - GET /api/session?messages=0 now parses only metadata before the messages array - Metadata-only loads no longer populate the full-session LRU cache - Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup - Hard reload no longer waits for populateModelDropdown() before restoring session PR #1026 (@franksong2702): Harden auto title generation for reasoning models - Raises title-gen completion budget to 512 tokens (reasoning-safe) - Retries once with 1024 tokens on empty content / finish_reason:length - Applies retry to both auxiliary and active-agent fallback routes - Preserves underlying failure reason in title_status on local fallback Co-authored-by: Frank Song <franksong2702@gmail.com> * feat: session attention indicators in right slot + last_message_at timestamps (#1024) PR #1024 (@franksong2702): Polish session attention indicators - Streaming spinners and unread dots now reuse the right-side actions slot - Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps - Date group carets point down when expanded, right when collapsed - Pinned group no longer repeats pinned-star icon per row - Running indicators appear immediately after send (local busy state while /api/sessions catches up) - Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message) so metadata-only saves don't make old sessions appear under Today Co-authored-by: Frank Song <franksong2702@gmail.com> * docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36) --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Josh <josh@fyul.link> Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
}
}
function appendThinking(text=''){
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031) * fix: remove orphaned i18n keys from top-level LOCALES object Three Traditional Chinese translation keys (cmd_status, memory_saved, profile_delete_title) were placed outside any locale block between the en and ru blocks in static/i18n.js. They became top-level properties of the LOCALES object, causing them to appear as invalid language options in the Settings > Preferences dropdown. The correct translations already exist in the zh-Hant locale block. Fixes #1008 * fix: block stale SSE events from polluting new session's DOM - appendThinking(): guard with !S.session||!S.activeStreamId to drop events from a previous session's SSE stream during a session switch - appendLiveToolCard(): same guard for consistency - finalizeThinkingCard(): scroll thinking-card-body to top when scroll is pinned, so completed response is immediately visible - appendThinking(): auto-scroll thinking card body to bottom while streaming if user is watching (scroll pinned) * Fix empty agent sessions in sidebar * fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status Fixes #995 — three sub-issues in the Cron Jobs UI: 1. Dual play icons ambiguous: Resume button now shows a distinct play+bar icon (play triangle + vertical line) instead of the identical triangle used by Run now. 2. Toast notification overlapping header buttons: Added position:relative; z-index:10 to .main-view-header so it stacks above the fixed toast (z-index:100 within its layer). 3. No running status after trigger: After triggering a job, the status badge immediately shows 'running…' with a CSS spinner animation, and polls the cron list every 3s (up to 30s) to refresh when the job completes. - Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant) - Added .detail-badge.running CSS class with spinner animation - New functions: _setCronDetailStatus(), _startCronRunningPoll() * fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback - _clearCronDetail() now clears _cronRunningPoll interval on navigation - Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker) - When poll ends (30s max), detail re-renders with actual status as fallback * feat: create folder and add space directly from UI (#782) - After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog - Add Create folder if it doesnt exist checkbox in the New Space form - Backend: support create flag in /api/workspaces/add to mkdir before validation - i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales * fix: validate workspace path before mkdir to prevent orphan directories Review feedback (critical): the previous code called mkdir() before validate_workspace_to_add(), which meant a rejected path (e.g. system dir) would leave an orphan directory on disk. New flow: 1. Resolve path and check against blocked system roots BEFORE any mutation 2. mkdir() only if path passes the blocklist check 3. Full validation (exists, is_dir) after mkdir Also imports _workspace_blocked_roots for the pre-mutation blocklist check. * fix(#1014): classify model-not-found errors with helpful message - Add model_not_found error type to streaming.py exception classifier - Detect 404, 'not found', 'does not exist', 'invalid model' patterns - Strip HTML tags from provider error messages (nginx 404 pages, etc.) - Add model_not_found branch to apperror handler in messages.js - Add i18n key model_not_found_label in all 6 locales - 15 tests covering detection, sanitization, frontend, and i18n * feat(ui): add live TPS stat to header Adds a TPS (Tokens Per Second) chip to the right of the header title bar that updates live while AI output is streaming. Metering (api/metering.py) - Tracks per-session output + reasoning tokens via GlobalMeter singleton - Per-session TPS = total_tokens / elapsed_time - Global TPS = average of active sessions' TPS values - HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling window (only recorded when > 0, so idle periods are excluded) - Thread-safe with a single lock Metering events emitted from streaming.py - Throttled at 100ms from token/reasoning/tool callbacks so the display updates rapidly during fast token streams - 1Hz ticker as fallback for slow streams (exits when no active sessions) - Final stats emitted on stream end Routes (api/routes.py) - Removed POST /api/metering/interval endpoint (dynamic interval via focus/blur was replaced with simple always-1s-when-active approach) UI (static/messages.js, index.html, style.css) - TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low' - Default: '0.0 t/s . 0.0 high' when idle - Display updates on every metering SSE event (throttled to 100ms) * feat: session restore speed + title gen reasoning hardening (#1025, #1026) PR #1025 (@franksong2702): Speed up large session restore paths - GET /api/session?messages=0 now parses only metadata before the messages array - Metadata-only loads no longer populate the full-session LRU cache - Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup - Hard reload no longer waits for populateModelDropdown() before restoring session PR #1026 (@franksong2702): Harden auto title generation for reasoning models - Raises title-gen completion budget to 512 tokens (reasoning-safe) - Retries once with 1024 tokens on empty content / finish_reason:length - Applies retry to both auxiliary and active-agent fallback routes - Preserves underlying failure reason in title_status on local fallback Co-authored-by: Frank Song <franksong2702@gmail.com> * feat: session attention indicators in right slot + last_message_at timestamps (#1024) PR #1024 (@franksong2702): Polish session attention indicators - Streaming spinners and unread dots now reuse the right-side actions slot - Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps - Date group carets point down when expanded, right when collapsed - Pinned group no longer repeats pinned-star icon per row - Running indicators appear immediately after send (local busy state while /api/sessions catches up) - Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message) so metadata-only saves don't make old sessions appear under Today Co-authored-by: Frank Song <franksong2702@gmail.com> * docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36) --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Josh <josh@fyul.link> Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
// Guard: ignore if session was switched during an async SSE stream.
// The old stream's reasoning events can still fire after switch;
// without this check they would pollute the new session's DOM.
if(!S.session||!S.activeStreamId) return;
$('emptyState').style.display='none';
let turn=$('liveAssistantTurn');
if(!turn){
turn=_createAssistantTurn();
turn.id='liveAssistantTurn';
if(S.session) turn.dataset.sessionId=S.session.session_id; // see #1366
$('msgInner').appendChild(turn);
}
const blocks=_assistantTurnBlocks(turn);
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
if(!blocks) return;
if(!isSimplifiedToolCalling()){
let row=$('thinkingRow');
if(!row){
row=document.createElement('div');
row.className='assistant-segment';
row.id='thinkingRow';
row.setAttribute('data-thinking-active','1');
// Insert after whichever comes last: a live assistant segment or a tool card.
// This mirrors appendLiveToolCard's anchor logic so thinking always appears
// in the right position in the interleaved sequence.
// Also skip #toolRunningRow (dots) — thinking should go before dots, not after.
const allChildren=Array.from(blocks.children);
const anchor=allChildren.filter(el=>
el.id!=='toolRunningRow' &&
el.matches('[data-live-assistant="1"],.tool-card-row')
).pop();
if(anchor) anchor.insertAdjacentElement('afterend', row);
else blocks.appendChild(row);
}
row.className=(text&&String(text).trim())?'assistant-segment thinking-card-row':'assistant-segment';
row.innerHTML=_thinkingMarkup(text);
scrollIfPinned();
// Auto-scroll the thinking card body to bottom if the user is watching
// (scroll pinned). If the user scrolled up to read history, leave it alone.
if(_scrollPinned){
const body=row&&row.querySelector('.thinking-card-body');
if(body) body.scrollTop=body.scrollHeight;
}
return;
}
if(!String(text||'').trim()){
scrollIfPinned();
return;
}
const allChildren=Array.from(blocks.children);
const anchor=allChildren.filter(el=>
el.id!=='toolRunningRow' &&
el.matches('[data-live-assistant="1"],.tool-call-group,.tool-card-row,.agent-activity-thinking')
).pop();
const group=ensureActivityGroup(blocks,{live:true,collapsed:true,anchor});
const body=group&&group.querySelector('.tool-call-group-body');
if(!body) return;
let row=body.querySelector('.agent-activity-thinking[data-thinking-active="1"]');
if(!row){
row=document.createElement('div');
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
row.className='agent-activity-thinking';
row.setAttribute('data-thinking-active','1');
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
body.insertBefore(row, body.firstChild);
}
row.innerHTML=_thinkingMarkup(text);
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
_syncToolCallGroupSummary(group);
scrollIfPinned();
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031) * fix: remove orphaned i18n keys from top-level LOCALES object Three Traditional Chinese translation keys (cmd_status, memory_saved, profile_delete_title) were placed outside any locale block between the en and ru blocks in static/i18n.js. They became top-level properties of the LOCALES object, causing them to appear as invalid language options in the Settings > Preferences dropdown. The correct translations already exist in the zh-Hant locale block. Fixes #1008 * fix: block stale SSE events from polluting new session's DOM - appendThinking(): guard with !S.session||!S.activeStreamId to drop events from a previous session's SSE stream during a session switch - appendLiveToolCard(): same guard for consistency - finalizeThinkingCard(): scroll thinking-card-body to top when scroll is pinned, so completed response is immediately visible - appendThinking(): auto-scroll thinking card body to bottom while streaming if user is watching (scroll pinned) * Fix empty agent sessions in sidebar * fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status Fixes #995 — three sub-issues in the Cron Jobs UI: 1. Dual play icons ambiguous: Resume button now shows a distinct play+bar icon (play triangle + vertical line) instead of the identical triangle used by Run now. 2. Toast notification overlapping header buttons: Added position:relative; z-index:10 to .main-view-header so it stacks above the fixed toast (z-index:100 within its layer). 3. No running status after trigger: After triggering a job, the status badge immediately shows 'running…' with a CSS spinner animation, and polls the cron list every 3s (up to 30s) to refresh when the job completes. - Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant) - Added .detail-badge.running CSS class with spinner animation - New functions: _setCronDetailStatus(), _startCronRunningPoll() * fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback - _clearCronDetail() now clears _cronRunningPoll interval on navigation - Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker) - When poll ends (30s max), detail re-renders with actual status as fallback * feat: create folder and add space directly from UI (#782) - After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog - Add Create folder if it doesnt exist checkbox in the New Space form - Backend: support create flag in /api/workspaces/add to mkdir before validation - i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales * fix: validate workspace path before mkdir to prevent orphan directories Review feedback (critical): the previous code called mkdir() before validate_workspace_to_add(), which meant a rejected path (e.g. system dir) would leave an orphan directory on disk. New flow: 1. Resolve path and check against blocked system roots BEFORE any mutation 2. mkdir() only if path passes the blocklist check 3. Full validation (exists, is_dir) after mkdir Also imports _workspace_blocked_roots for the pre-mutation blocklist check. * fix(#1014): classify model-not-found errors with helpful message - Add model_not_found error type to streaming.py exception classifier - Detect 404, 'not found', 'does not exist', 'invalid model' patterns - Strip HTML tags from provider error messages (nginx 404 pages, etc.) - Add model_not_found branch to apperror handler in messages.js - Add i18n key model_not_found_label in all 6 locales - 15 tests covering detection, sanitization, frontend, and i18n * feat(ui): add live TPS stat to header Adds a TPS (Tokens Per Second) chip to the right of the header title bar that updates live while AI output is streaming. Metering (api/metering.py) - Tracks per-session output + reasoning tokens via GlobalMeter singleton - Per-session TPS = total_tokens / elapsed_time - Global TPS = average of active sessions' TPS values - HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling window (only recorded when > 0, so idle periods are excluded) - Thread-safe with a single lock Metering events emitted from streaming.py - Throttled at 100ms from token/reasoning/tool callbacks so the display updates rapidly during fast token streams - 1Hz ticker as fallback for slow streams (exits when no active sessions) - Final stats emitted on stream end Routes (api/routes.py) - Removed POST /api/metering/interval endpoint (dynamic interval via focus/blur was replaced with simple always-1s-when-active approach) UI (static/messages.js, index.html, style.css) - TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low' - Default: '0.0 t/s . 0.0 high' when idle - Display updates on every metering SSE event (throttled to 100ms) * feat: session restore speed + title gen reasoning hardening (#1025, #1026) PR #1025 (@franksong2702): Speed up large session restore paths - GET /api/session?messages=0 now parses only metadata before the messages array - Metadata-only loads no longer populate the full-session LRU cache - Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup - Hard reload no longer waits for populateModelDropdown() before restoring session PR #1026 (@franksong2702): Harden auto title generation for reasoning models - Raises title-gen completion budget to 512 tokens (reasoning-safe) - Retries once with 1024 tokens on empty content / finish_reason:length - Applies retry to both auxiliary and active-agent fallback routes - Preserves underlying failure reason in title_status on local fallback Co-authored-by: Frank Song <franksong2702@gmail.com> * feat: session attention indicators in right slot + last_message_at timestamps (#1024) PR #1024 (@franksong2702): Polish session attention indicators - Streaming spinners and unread dots now reuse the right-side actions slot - Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps - Date group carets point down when expanded, right when collapsed - Pinned group no longer repeats pinned-star icon per row - Running indicators appear immediately after send (local busy state while /api/sessions catches up) - Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message) so metadata-only saves don't make old sessions appear under Today Co-authored-by: Frank Song <franksong2702@gmail.com> * docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36) --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Josh <josh@fyul.link> Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
if(_scrollPinned){
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
const thinkingBody=row&&row.querySelector('.thinking-card-body');
if(thinkingBody) thinkingBody.scrollTop=thinkingBody.scrollHeight;
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031) * fix: remove orphaned i18n keys from top-level LOCALES object Three Traditional Chinese translation keys (cmd_status, memory_saved, profile_delete_title) were placed outside any locale block between the en and ru blocks in static/i18n.js. They became top-level properties of the LOCALES object, causing them to appear as invalid language options in the Settings > Preferences dropdown. The correct translations already exist in the zh-Hant locale block. Fixes #1008 * fix: block stale SSE events from polluting new session's DOM - appendThinking(): guard with !S.session||!S.activeStreamId to drop events from a previous session's SSE stream during a session switch - appendLiveToolCard(): same guard for consistency - finalizeThinkingCard(): scroll thinking-card-body to top when scroll is pinned, so completed response is immediately visible - appendThinking(): auto-scroll thinking card body to bottom while streaming if user is watching (scroll pinned) * Fix empty agent sessions in sidebar * fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status Fixes #995 — three sub-issues in the Cron Jobs UI: 1. Dual play icons ambiguous: Resume button now shows a distinct play+bar icon (play triangle + vertical line) instead of the identical triangle used by Run now. 2. Toast notification overlapping header buttons: Added position:relative; z-index:10 to .main-view-header so it stacks above the fixed toast (z-index:100 within its layer). 3. No running status after trigger: After triggering a job, the status badge immediately shows 'running…' with a CSS spinner animation, and polls the cron list every 3s (up to 30s) to refresh when the job completes. - Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant) - Added .detail-badge.running CSS class with spinner animation - New functions: _setCronDetailStatus(), _startCronRunningPoll() * fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback - _clearCronDetail() now clears _cronRunningPoll interval on navigation - Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker) - When poll ends (30s max), detail re-renders with actual status as fallback * feat: create folder and add space directly from UI (#782) - After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog - Add Create folder if it doesnt exist checkbox in the New Space form - Backend: support create flag in /api/workspaces/add to mkdir before validation - i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales * fix: validate workspace path before mkdir to prevent orphan directories Review feedback (critical): the previous code called mkdir() before validate_workspace_to_add(), which meant a rejected path (e.g. system dir) would leave an orphan directory on disk. New flow: 1. Resolve path and check against blocked system roots BEFORE any mutation 2. mkdir() only if path passes the blocklist check 3. Full validation (exists, is_dir) after mkdir Also imports _workspace_blocked_roots for the pre-mutation blocklist check. * fix(#1014): classify model-not-found errors with helpful message - Add model_not_found error type to streaming.py exception classifier - Detect 404, 'not found', 'does not exist', 'invalid model' patterns - Strip HTML tags from provider error messages (nginx 404 pages, etc.) - Add model_not_found branch to apperror handler in messages.js - Add i18n key model_not_found_label in all 6 locales - 15 tests covering detection, sanitization, frontend, and i18n * feat(ui): add live TPS stat to header Adds a TPS (Tokens Per Second) chip to the right of the header title bar that updates live while AI output is streaming. Metering (api/metering.py) - Tracks per-session output + reasoning tokens via GlobalMeter singleton - Per-session TPS = total_tokens / elapsed_time - Global TPS = average of active sessions' TPS values - HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling window (only recorded when > 0, so idle periods are excluded) - Thread-safe with a single lock Metering events emitted from streaming.py - Throttled at 100ms from token/reasoning/tool callbacks so the display updates rapidly during fast token streams - 1Hz ticker as fallback for slow streams (exits when no active sessions) - Final stats emitted on stream end Routes (api/routes.py) - Removed POST /api/metering/interval endpoint (dynamic interval via focus/blur was replaced with simple always-1s-when-active approach) UI (static/messages.js, index.html, style.css) - TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low' - Default: '0.0 t/s . 0.0 high' when idle - Display updates on every metering SSE event (throttled to 100ms) * feat: session restore speed + title gen reasoning hardening (#1025, #1026) PR #1025 (@franksong2702): Speed up large session restore paths - GET /api/session?messages=0 now parses only metadata before the messages array - Metadata-only loads no longer populate the full-session LRU cache - Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup - Hard reload no longer waits for populateModelDropdown() before restoring session PR #1026 (@franksong2702): Harden auto title generation for reasoning models - Raises title-gen completion budget to 512 tokens (reasoning-safe) - Retries once with 1024 tokens on empty content / finish_reason:length - Applies retry to both auxiliary and active-agent fallback routes - Preserves underlying failure reason in title_status on local fallback Co-authored-by: Frank Song <franksong2702@gmail.com> * feat: session attention indicators in right slot + last_message_at timestamps (#1024) PR #1024 (@franksong2702): Polish session attention indicators - Streaming spinners and unread dots now reuse the right-side actions slot - Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps - Date group carets point down when expanded, right when collapsed - Pinned group no longer repeats pinned-star icon per row - Running indicators appear immediately after send (local busy state while /api/sessions catches up) - Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message) so metadata-only saves don't make old sessions appear under Today Co-authored-by: Frank Song <franksong2702@gmail.com> * docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36) --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Josh <josh@fyul.link> Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
}
}
function updateThinking(text=''){appendThinking(text);}
function removeThinking(){
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
if(!isSimplifiedToolCalling()){
const el=$('thinkingRow');
if(el) el.remove();
const turn=$('liveAssistantTurn');
const blocks=_assistantTurnBlocks(turn);
if(turn&&blocks&&!blocks.children.length) turn.remove();
return;
}
const turn=$('liveAssistantTurn');
const blocks=_assistantTurnBlocks(turn);
release: v0.50.240 ## Release v0.50.240 Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures). --- ### Added - **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282 - **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482 - **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479 - **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485 - **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481 - **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568 - **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281 - **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268 - **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269 ### Fixed - **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266 - **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278 - **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267 - **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273 --- ### Test results ``` 3199 passed, 2 skipped, 3 xpassed in 72.79s ``` ### PRs on hold (not included) #1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
if(blocks) blocks.querySelectorAll('.agent-activity-thinking').forEach(el=>el.remove());
if(blocks) blocks.querySelectorAll('.tool-call-group[data-agent-activity-group="1"]').forEach(group=>{
_syncToolCallGroupSummary(group);
if(!group.querySelector('.tool-card-row,.agent-activity-thinking')) group.remove();
});
if(turn&&blocks&&!blocks.children.length) turn.remove();
}
function fileIcon(name, type){
v0.44.0: approval fix, login CSP, update diagnostics, Lucide icons * fix: approval pending check broken by stale has_pending import (#228) api/routes.py imported has_pending/pop_pending from tools.approval, but the agent module renamed has_pending to has_blocking_approval (checks gateway queue, not _pending dict) and removed pop_pending. The import fell through to fallback lambdas that always returned False, making GET /api/approval/pending always return {pending:null} even after a successful inject_test. Fix: check _pending directly under _lock — same dict submit_pending writes to. Stale imports removed. Before: 554 pass, 1 fail | After: 555 pass, 0 fail * fix: move login JS into external file, remove inline handlers (#226) Login page used inline onsubmit/onkeydown handlers and an inline <script> block — all blocked by strict script-src CSP, causing silent login failure. Fix: extract doLogin() and Enter key listener into static/login.js (served from /static/, already a public path). Form uses id='login-form' and data-* attributes for i18n strings instead of injected JS literals. Also guards res.json() parse with try/catch so non-JSON error bodies (e.g. HTTP 500) show the password-error fallback instead of 'Connection failed'. Fixes #222. * fix: improve update error messages when pull fails (#227) _apply_update_inner() ran git pull --ff-only and returned only raw stderr on failure, making all failure modes indistinguishable. Fix: explicit git fetch before pull; if fetch fails, returns human-readable network error. Diverged history and missing upstream tracking branch each get distinct messages with exact recovery commands. Generic fallback truncates to 300 chars and shows sentinel when git produces no output. Also adds tests/test_update_checker.py with 13 tests covering all 4 new diagnostic code paths (0 tests existed before). Fixes #223. * fix: stabilize 30s terminal approval prompt visibility (#225) Adds minimum 30-second visibility guard for the approval card using _approvalVisibleSince, _approvalHideTimer, and a signature fingerprint to deduplicate repeated poll ticks. Fix: respondApproval() and all stream-end paths (done/cancel/apperror/ error/start-error) now call hideApprovalCard(true) so the card hides immediately when the user responds or the session ends. The 30s guard only applies to mid-session poll ticks where the approval is still live but briefly absent. Adds 11 structural tests covering the new timer variables, force parameter, force-on-respond, force-on-stream-end, and poll-loop no-force behavior. * feat: replace emoji icons with self-hosted Lucide SVG icons (#221) Replaces all sidebar/button emoji icons with SVG paths from Lucide bundled in static/icons.js (no CDN dependency). Adds li(name) function returning inline SVG geometry from a hardcoded whitelist — unknown keys return '' so dynamic server-supplied names never inject arbitrary SVG. Changes: - static/icons.js: new file with 21 icon paths + li() renderer - static/index.html: all nav/action buttons now use li() icons - static/ui.js: toolIcon(), fileIcon() use li() for tool/file icons - static/messages.js: cancelStream button uses SVG square stop icon - .gitignore: adds node_modules/ entry Verified: all 35 onclick= functions exist in JS, all 21 li() calls reference defined icons, applyBotName() selectors intact, version label present, no removed IDs referenced by JS. * docs: v0.44.0 release notes, bump version, update test counts --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 17:02:28 +00:00
if(type==='dir') return li('folder',14);
const e=fileExt(name);
v0.44.0: approval fix, login CSP, update diagnostics, Lucide icons * fix: approval pending check broken by stale has_pending import (#228) api/routes.py imported has_pending/pop_pending from tools.approval, but the agent module renamed has_pending to has_blocking_approval (checks gateway queue, not _pending dict) and removed pop_pending. The import fell through to fallback lambdas that always returned False, making GET /api/approval/pending always return {pending:null} even after a successful inject_test. Fix: check _pending directly under _lock — same dict submit_pending writes to. Stale imports removed. Before: 554 pass, 1 fail | After: 555 pass, 0 fail * fix: move login JS into external file, remove inline handlers (#226) Login page used inline onsubmit/onkeydown handlers and an inline <script> block — all blocked by strict script-src CSP, causing silent login failure. Fix: extract doLogin() and Enter key listener into static/login.js (served from /static/, already a public path). Form uses id='login-form' and data-* attributes for i18n strings instead of injected JS literals. Also guards res.json() parse with try/catch so non-JSON error bodies (e.g. HTTP 500) show the password-error fallback instead of 'Connection failed'. Fixes #222. * fix: improve update error messages when pull fails (#227) _apply_update_inner() ran git pull --ff-only and returned only raw stderr on failure, making all failure modes indistinguishable. Fix: explicit git fetch before pull; if fetch fails, returns human-readable network error. Diverged history and missing upstream tracking branch each get distinct messages with exact recovery commands. Generic fallback truncates to 300 chars and shows sentinel when git produces no output. Also adds tests/test_update_checker.py with 13 tests covering all 4 new diagnostic code paths (0 tests existed before). Fixes #223. * fix: stabilize 30s terminal approval prompt visibility (#225) Adds minimum 30-second visibility guard for the approval card using _approvalVisibleSince, _approvalHideTimer, and a signature fingerprint to deduplicate repeated poll ticks. Fix: respondApproval() and all stream-end paths (done/cancel/apperror/ error/start-error) now call hideApprovalCard(true) so the card hides immediately when the user responds or the session ends. The 30s guard only applies to mid-session poll ticks where the approval is still live but briefly absent. Adds 11 structural tests covering the new timer variables, force parameter, force-on-respond, force-on-stream-end, and poll-loop no-force behavior. * feat: replace emoji icons with self-hosted Lucide SVG icons (#221) Replaces all sidebar/button emoji icons with SVG paths from Lucide bundled in static/icons.js (no CDN dependency). Adds li(name) function returning inline SVG geometry from a hardcoded whitelist — unknown keys return '' so dynamic server-supplied names never inject arbitrary SVG. Changes: - static/icons.js: new file with 21 icon paths + li() renderer - static/index.html: all nav/action buttons now use li() icons - static/ui.js: toolIcon(), fileIcon() use li() for tool/file icons - static/messages.js: cancelStream button uses SVG square stop icon - .gitignore: adds node_modules/ entry Verified: all 35 onclick= functions exist in JS, all 21 li() calls reference defined icons, applyBotName() selectors intact, version label present, no removed IDs referenced by JS. * docs: v0.44.0 release notes, bump version, update test counts --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 17:02:28 +00:00
if(IMAGE_EXTS.has(e)) return li('image',14);
if(MD_EXTS.has(e)) return li('file-text',14);
if(typeof DOWNLOAD_EXTS!=='undefined'&&DOWNLOAD_EXTS.has(e)) return li('download',14);
if(e==='.py') return li('file-code',14);
if(e==='.js'||e==='.ts'||e==='.jsx'||e==='.tsx') return li('zap',14);
if(e==='.json'||e==='.yaml'||e==='.yml'||e==='.toml') return li('settings',14);
if(e==='.sh'||e==='.bash') return li('terminal',14);
if(e==='.pdf') return li('download',14);
return li('file-text',14);
}
function renderBreadcrumb(){
const bar=$('breadcrumbBar');
const upBtn=$('btnUpDir');
if(!bar)return;
if(S.currentDir==='.'){
bar.style.display='none';
if(upBtn)upBtn.style.display='none';
return;
}
bar.style.display='flex';
if(upBtn)upBtn.style.display='';
bar.innerHTML='';
// Root segment
const root=document.createElement('span');
root.className='breadcrumb-seg breadcrumb-link';
root.textContent='~';
root.onclick=()=>loadDir('.');
bar.appendChild(root);
// Path segments
const parts=S.currentDir.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;
seg.onclick=()=>loadDir(target);
} else {
seg.className='breadcrumb-seg breadcrumb-current';
}
bar.appendChild(seg);
}
}
// Track expanded directories for tree view
if(!S._expandedDirs) S._expandedDirs=new Set();
// Cache of fetched directory contents: path -> entries[]
if(!S._dirCache) S._dirCache={};
function renderFileTree(){
const box=$('fileTree');box.innerHTML='';
// Cache current dir entries
S._dirCache[S.currentDir||'.']=S.entries;
// Show empty-state when no workspace is set or the directory is empty (#703)
const emptyEl=$('wsEmptyState');
const hasWorkspace=!!(S.session&&S.session.workspace);
if(!hasWorkspace){
if(emptyEl){emptyEl.textContent=t('workspace_empty_no_path');emptyEl.style.display='flex';}
box.style.display='none';
return;
}
if(emptyEl) emptyEl.style.display='none';
box.style.display='';
if(!S.entries||!S.entries.length){
if(emptyEl){emptyEl.textContent=t('workspace_empty_dir');emptyEl.style.display='flex';}
return;
}
_renderTreeItems(box, S.entries, 0);
}
function _renderTreeItems(container, entries, depth){
for(const item of entries){
const el=document.createElement('div');el.className='file-item';
el.style.paddingLeft=(8+depth*16)+'px';
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
el.setAttribute('draggable','true');
el.oncontextmenu=(e)=>{e.preventDefault();e.stopPropagation();_showFileContextMenu(e,item);};
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
el.ondragstart=(e)=>{e.dataTransfer.setData('application/ws-path',item.path);e.dataTransfer.setData('application/ws-type',item.type);e.dataTransfer.effectAllowed='copy';};
if(item.type==='dir'){
// Toggle arrow for directories
const arrow=document.createElement('span');
arrow.className='file-tree-toggle';
const isExpanded=S._expandedDirs.has(item.path);
arrow.textContent=isExpanded?'\u25BE':'\u25B8';
el.appendChild(arrow);
}
// Icon
const iconEl=document.createElement('span');
v0.44.0: approval fix, login CSP, update diagnostics, Lucide icons * fix: approval pending check broken by stale has_pending import (#228) api/routes.py imported has_pending/pop_pending from tools.approval, but the agent module renamed has_pending to has_blocking_approval (checks gateway queue, not _pending dict) and removed pop_pending. The import fell through to fallback lambdas that always returned False, making GET /api/approval/pending always return {pending:null} even after a successful inject_test. Fix: check _pending directly under _lock — same dict submit_pending writes to. Stale imports removed. Before: 554 pass, 1 fail | After: 555 pass, 0 fail * fix: move login JS into external file, remove inline handlers (#226) Login page used inline onsubmit/onkeydown handlers and an inline <script> block — all blocked by strict script-src CSP, causing silent login failure. Fix: extract doLogin() and Enter key listener into static/login.js (served from /static/, already a public path). Form uses id='login-form' and data-* attributes for i18n strings instead of injected JS literals. Also guards res.json() parse with try/catch so non-JSON error bodies (e.g. HTTP 500) show the password-error fallback instead of 'Connection failed'. Fixes #222. * fix: improve update error messages when pull fails (#227) _apply_update_inner() ran git pull --ff-only and returned only raw stderr on failure, making all failure modes indistinguishable. Fix: explicit git fetch before pull; if fetch fails, returns human-readable network error. Diverged history and missing upstream tracking branch each get distinct messages with exact recovery commands. Generic fallback truncates to 300 chars and shows sentinel when git produces no output. Also adds tests/test_update_checker.py with 13 tests covering all 4 new diagnostic code paths (0 tests existed before). Fixes #223. * fix: stabilize 30s terminal approval prompt visibility (#225) Adds minimum 30-second visibility guard for the approval card using _approvalVisibleSince, _approvalHideTimer, and a signature fingerprint to deduplicate repeated poll ticks. Fix: respondApproval() and all stream-end paths (done/cancel/apperror/ error/start-error) now call hideApprovalCard(true) so the card hides immediately when the user responds or the session ends. The 30s guard only applies to mid-session poll ticks where the approval is still live but briefly absent. Adds 11 structural tests covering the new timer variables, force parameter, force-on-respond, force-on-stream-end, and poll-loop no-force behavior. * feat: replace emoji icons with self-hosted Lucide SVG icons (#221) Replaces all sidebar/button emoji icons with SVG paths from Lucide bundled in static/icons.js (no CDN dependency). Adds li(name) function returning inline SVG geometry from a hardcoded whitelist — unknown keys return '' so dynamic server-supplied names never inject arbitrary SVG. Changes: - static/icons.js: new file with 21 icon paths + li() renderer - static/index.html: all nav/action buttons now use li() icons - static/ui.js: toolIcon(), fileIcon() use li() for tool/file icons - static/messages.js: cancelStream button uses SVG square stop icon - .gitignore: adds node_modules/ entry Verified: all 35 onclick= functions exist in JS, all 21 li() calls reference defined icons, applyBotName() selectors intact, version label present, no removed IDs referenced by JS. * docs: v0.44.0 release notes, bump version, update test counts --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 17:02:28 +00:00
iconEl.className='file-icon';iconEl.innerHTML=fileIcon(item.name,item.type);
el.appendChild(iconEl);
// Name
const nameEl=document.createElement('span');
nameEl.className='file-name';nameEl.textContent=item.name;nameEl.title=t('double_click_rename');
fix(workspace): preserve single-click open + double-click rename on filename (#1707) Closes #1707 — single-click on a workspace tree filename did nothing. #1698 was a regression where the filename's dblclick rename handler was unreachable because the row's el.onclick (openFile) fired synchronously on the first click. The fix in #1702 stopped click propagation on nameEl — but that broke single-click activation entirely (#1707): clicking the filename now did nothing, you had to click the icon or row whitespace to open the file. Restored fix preserves both intents via a 300ms debounced delegator: let _nameClickTimer = null; nameEl.onclick = (e) => { e.stopPropagation(); if (_nameClickTimer) { clearTimeout(_nameClickTimer); _nameClickTimer = null; } _nameClickTimer = setTimeout(() => { _nameClickTimer = null; if (typeof el.onclick === 'function') el.onclick(e); }, 300); }; nameEl.ondblclick = (e) => { e.stopPropagation(); if (_nameClickTimer) { clearTimeout(_nameClickTimer); _nameClickTimer = null; } // ... existing rename body }; Single-click on nameEl schedules a setTimeout that calls el.onclick(e) after the dblclick threshold passes (300ms — matches the OS dblclick threshold on most platforms). Double-click cancels the pending timer and triggers the existing rename input. Cost: 300ms latency on file-open clicks. Acceptable trade for keeping rename reachable on single-click. Also updated tests/test_workspace_tree_rename.py to accept both the pre-#1707 (pure stopPropagation) and post-#1707 (debounced delegator) shapes — the original assertion was too narrow and would have rejected the correct fix. 9 new regression tests in tests/test_1707_workspace_filename_click.py: - 6 source-level static-analysis checks on the patched handler shape - 3 behavioral tests via Node VM (synthesize click → 300ms delay, click → dblclick within tick → assert rename mounts + openFile is not called). 7 of 9 tests fail on master pre-fix (verified); all 9 pass after.
2026-05-05 16:13:58 +00:00
// Single-click opens (file) or expand-toggles (dir) but is debounced 300ms so a
// double-click can cancel it and trigger rename instead. Without the debounce, the
// click bubbles to el.onclick before dblclick can fire — that's #1698. Without the
// restored activation, single-click on the filename does nothing — that's #1707.
let _nameClickTimer=null;
nameEl.onclick=(e)=>{
e.stopPropagation();
if(_nameClickTimer){clearTimeout(_nameClickTimer);_nameClickTimer=null;}
_nameClickTimer=setTimeout(()=>{
_nameClickTimer=null;
// Delegate to the row's existing single-click handler (openFile / dir toggle).
if(typeof el.onclick==='function')el.onclick(e);
},300);
};
2026-03-31 07:02:47 +00:00
nameEl.ondblclick=(e)=>{
e.stopPropagation();
fix(workspace): preserve single-click open + double-click rename on filename (#1707) Closes #1707 — single-click on a workspace tree filename did nothing. #1698 was a regression where the filename's dblclick rename handler was unreachable because the row's el.onclick (openFile) fired synchronously on the first click. The fix in #1702 stopped click propagation on nameEl — but that broke single-click activation entirely (#1707): clicking the filename now did nothing, you had to click the icon or row whitespace to open the file. Restored fix preserves both intents via a 300ms debounced delegator: let _nameClickTimer = null; nameEl.onclick = (e) => { e.stopPropagation(); if (_nameClickTimer) { clearTimeout(_nameClickTimer); _nameClickTimer = null; } _nameClickTimer = setTimeout(() => { _nameClickTimer = null; if (typeof el.onclick === 'function') el.onclick(e); }, 300); }; nameEl.ondblclick = (e) => { e.stopPropagation(); if (_nameClickTimer) { clearTimeout(_nameClickTimer); _nameClickTimer = null; } // ... existing rename body }; Single-click on nameEl schedules a setTimeout that calls el.onclick(e) after the dblclick threshold passes (300ms — matches the OS dblclick threshold on most platforms). Double-click cancels the pending timer and triggers the existing rename input. Cost: 300ms latency on file-open clicks. Acceptable trade for keeping rename reachable on single-click. Also updated tests/test_workspace_tree_rename.py to accept both the pre-#1707 (pure stopPropagation) and post-#1707 (debounced delegator) shapes — the original assertion was too narrow and would have rejected the correct fix. 9 new regression tests in tests/test_1707_workspace_filename_click.py: - 6 source-level static-analysis checks on the patched handler shape - 3 behavioral tests via Node VM (synthesize click → 300ms delay, click → dblclick within tick → assert rename mounts + openFile is not called). 7 of 9 tests fail on master pre-fix (verified); all 9 pass after.
2026-05-05 16:13:58 +00:00
if(_nameClickTimer){clearTimeout(_nameClickTimer);_nameClickTimer=null;}
// For directories, double-click navigates (breadcrumb view)
if(item.type==='dir'){loadDir(item.path);return;}
2026-03-31 07:02:47 +00:00
const inp=document.createElement('input');
inp.className='file-rename-input';inp.value=item.name;
inp.onclick=(e2)=>e2.stopPropagation();
const finish=async(save)=>{
inp.onblur=null;
2026-03-31 07:02:47 +00:00
if(save){
const newName=inp.value.trim();
if(newName&&newName!==item.name){
try{
await api('/api/file/rename',{method:'POST',body:JSON.stringify({
session_id:S.session.session_id,path:item.path,new_name:newName
})});
showToast(t('renamed_to')+newName);
// Update expanded dirs cache key if renaming a directory
if(item.type==='dir'&&S._expandedDirs){
S._expandedDirs.delete(item.path);
const parent=item.path.includes('/')?item.path.substring(0,item.path.lastIndexOf('/')):'.';
const newPath=parent==='.'?newName:parent+'/'+newName;
S._expandedDirs.add(newPath);
if(S._dirCache[item.path]){S._dirCache[newPath]=S._dirCache[item.path];delete S._dirCache[item.path];}
if(typeof _saveExpandedDirs==='function')_saveExpandedDirs();
}
// Invalidate cache and re-render
delete S._dirCache[S.currentDir];
await loadDir(S.currentDir);
}catch(err){showToast(t('rename_failed')+err.message);}
2026-03-31 07:02:47 +00:00
}
}
inp.replaceWith(nameEl);
};
inp.onkeydown=(e2)=>{
if(e2.key==='Enter'){
v0.50.264 polish followups: i18n parity + assistant-output readability Closes #1442 (server-side _LOGIN_LOCALE missing ja/pt/ko) Closes #1443 (promote _isImeEnter helper to 6 other Safari Enter guards) Closes #1446 (glued-bold-heading lift for LLM thinking-block output) Closes #1447 (markdown heading visual hierarchy in chat messages) All four issues were filed by the Opus pre-release advisor on the v0.50.264 batch or by Cygnus via Discord (relayed by @AvidFuturist, May 1 2026). They share a common shape — narrow, well-scoped, independent of each other, all adding regression tests. == #1442: _LOGIN_LOCALE parity (api/routes.py + static/i18n.js) == Added entries for ja/pt/ko to the server-side _LOGIN_LOCALE dict that renders the localized login page BEFORE the JS i18n bundle loads. With v0.50.264 shipping Japanese as the 8th built-in locale, ja/pt/ko users were seeing the English login page even with their language preference set. While auditing static/i18n.js for English leakage, also fixed: - ko: 10 user-facing login/sign-out/password keys still in English - es: 3 sign-out/auth-disabled keys still in English Tests: tests/test_login_locale_parity.py (20 tests) — pins both invariants: (a) every locale in i18n.js LOCALES has a matching _LOGIN_LOCALE entry (b) every locale's login-flow keys (13 of them) are translated, not English == #1443: window._isImeEnter promotion == PR #1441 fixed the Safari IME-composition Enter race in the chat composer (`#msg`) by widening the guard from `e.isComposing` to a `_isImeEnter(e)` helper that combines three signals (isComposing || keyCode===229 || _imeComposing flag). Six other Enter-input handlers were left on the original narrow guard and would still drop IME composition Enters on Safari for Japanese/Chinese/Korean users. Promoted the helper to `window._isImeEnter` (defined in static/boot.js) and replaced the `e.isComposing` guards at all six sites: - static/sessions.js: session rename, project create, project rename - static/ui.js: app dialog (confirm/prompt), message edit, workspace rename The state-free part of the helper (`isComposing || keyCode===229`) handles Safari's race for any focused input without needing per-input composition listeners — only `#msg` keeps the local `_imeComposing` flag. Tests: - tests/test_issue1443_ime_helper_promotion.py (9 tests) — pins each site + verifies no raw `e.isComposing` Enter-guards remain in sessions.js/ui.js - tests/test_ime_composition.py — alternation regex extended to accept the windowed helper form (loosen-test-on-shape-change pattern from v0.50.264 reflection notes) == #1446: glued-bold-heading lift (static/ui.js renderMd + Python mirror) == LLMs in thinking/reasoning mode emit "section headers" glued to the end of the previous paragraph with no whitespace: Para 1 text.**Heading to Para 2** Para 2 text.**Heading to Para 3** The renderer correctly produces inline `<strong>` per CommonMark, but it looks like trailing emphasis on the body text rather than a section break. Cygnus reported this as "Markdown feedback 2 of 3." Added a single regex pre-pass in renderMd(): s.replace(/([.!?])\*\*([^*\n]{1,80})\*\*\n\n/g, '$1\n\n**$2**\n\n') Constraints chosen to avoid false positives: - Trigger only on `[.!?]` IMMEDIATELY before `**` (no space) — almost always an LLM-glued heading, not intentional emphasis - Inner text ≤80 chars, no `*` or newline (single-line only) - Trailing `\n\n` required — preserves "this is **important** to know." mid-paragraph emphasis untouched - Position: after rawPreStash restore, before fence_stash restore — fenced code blocks stay protected (their content is `\x00P` / `\x00F` tokens when the lift runs) Mirrored in tests/test_sprint16.py render_md() so both stay in sync. Tests: tests/test_issue1446_glued_heading_lift.py (17 tests, 5 of which drive the actual ui.js renderMd via node) — covers all 3 trigger forms (.!?), all 4 preserve-emphasis cases the issue spec'd, fenced/inline code protection, chained glued headings, source-level position pin, regex shape pin. == #1447: markdown heading visual hierarchy (static/style.css) == Pre-fix sizes in `.msg-body`: h1 18px, h2 16px, h3 14px (= body), h4 13px, h5 12px, h6 11px So h3 was indistinguishable from body and h4/h5/h6 were SMALLER than body. Cygnus's report: "Markdown feedback 3 of 3 — Headings seem to be missing across the board in Hermes. They're there, but all plaintext." New sizes: h1 24px (border-bottom) h2 20px (border-bottom) h3 17px h4 15px h5 14px (uppercase, tracked) h6 13px (uppercase, tracked, muted) All headings now `font-weight:700` + `color:var(--strong)` for stronger ink. h5/h6 use uppercase + letter-spacing for "label-style" affordance instead of being smaller-than-body. Synced .preview-md (file preview pane) to match exactly so a markdown file preview and a chat message render identically. Added missing h4/h5/h6 rules to .preview-md (it only had h1-h3 before). Updated data-font-size="small"/"large" h1-h6 overrides to scale proportionally with the new defaults. Hierarchy preserved at all three font-size settings. Tests: tests/test_issue1447_heading_hierarchy.py (9 tests) — pins the size hierarchy, the bottom borders on h1/h2, the uppercase affordance on h5/h6, the .preview-md sync, and the small/large override scaling. == Verification == pytest tests/ -q → 3748 passed (+56 new) bash ~/WebUI/scripts/run-browser-tests.sh → 20 + 11 PASS bash ~/WebUI/scripts/webui_qa_agent.sh 8789 → 23/23 PASS Visual confirmation in browser at port 8789: - Heading hierarchy clearly visible at all 6 levels - Glued-bold lift produces separate paragraphs as designed - window._isImeEnter accessible from any module after boot.js - Login page renders ja/pt/ko strings correctly (curl -s /login)
2026-05-02 04:04:23 +00:00
if(window._isImeEnter&&window._isImeEnter(e2)){return;}
e2.preventDefault();
finish(true);
}
2026-03-31 07:02:47 +00:00
if(e2.key==='Escape'){e2.preventDefault();finish(false);}
};
inp.onblur=()=>finish(false);
nameEl.replaceWith(inp);
setTimeout(()=>{inp.focus();inp.select();},10);
};
el.appendChild(nameEl);
// Size -- only for files
if(item.type==='file'&&item.size){
const sizeEl=document.createElement('span');
sizeEl.className='file-size';
sizeEl.textContent=`${(item.size/1024).toFixed(1)}k`;
el.appendChild(sizeEl);
}
// Delete button -- for files and directories
if(item.type==='file'){
const del=document.createElement('button');
del.className='file-del-btn';del.title=t('delete_title');del.textContent='\u00d7';
del.onclick=async(e)=>{e.stopPropagation();await deleteWorkspaceFile(item.path,item.name);};
el.appendChild(del);
}else if(item.type==='dir'){
const del=document.createElement('button');
del.className='file-del-btn';del.title=t('delete_title');del.textContent='\u00d7';
del.onclick=async(e)=>{e.stopPropagation();await deleteWorkspaceDir(item.path,item.name);};
el.appendChild(del);
}
if(item.type==='dir'){
// Single-click toggles expand/collapse
el.onclick=async(e)=>{
e.stopPropagation();
if(S._expandedDirs.has(item.path)){
S._expandedDirs.delete(item.path);
if(typeof _saveExpandedDirs==='function')_saveExpandedDirs();
renderFileTree();
}else{
S._expandedDirs.add(item.path);
if(typeof _saveExpandedDirs==='function')_saveExpandedDirs();
// Fetch children if not cached
if(!S._dirCache[item.path]){
try{
const data=await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(item.path)}`);
S._dirCache[item.path]=data.entries||[];
}catch(e2){S._dirCache[item.path]=[];}
}
renderFileTree();
}
};
}else{
el.onclick=async()=>openFile(item.path);
}
container.appendChild(el);
// Render children if directory is expanded
if(item.type==='dir'&&S._expandedDirs.has(item.path)){
const children=S._dirCache[item.path]||[];
if(children.length){
_renderTreeItems(container, children, depth+1);
}else{
const empty=document.createElement('div');
empty.className='file-item file-empty';
empty.style.paddingLeft=(8+(depth+1)*16)+'px';
empty.textContent=t('empty_dir');
container.appendChild(empty);
}
}
}
}
async function deleteWorkspaceDir(relPath, name){
if(!S.session)return;
const ok=await showConfirmDialog({title:t('delete_dir_confirm',name),message:'',confirmLabel:'Delete',danger:true,focusCancel:true});
if(!ok)return;
try{
await api('/api/file/delete',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:relPath,recursive:true})});
showToast(t('deleted')+name);
// Remove from expanded dirs cache
if(S._expandedDirs){S._expandedDirs.delete(relPath);if(typeof _saveExpandedDirs==='function')_saveExpandedDirs();}
delete S._dirCache[relPath];
await loadDir(S.currentDir);
}catch(e){setStatus(t('delete_failed')+e.message);}
}
function _showFileContextMenu(e, item){
document.querySelectorAll('.file-ctx-menu').forEach(el=>el.remove());
const menu=document.createElement('div');
menu.className='file-ctx-menu';
menu.style.cssText='position:fixed;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:6px 0;z-index:9999;min-width:140px;box-shadow:0 4px 16px rgba(0,0,0,.35);';
// Keep menu within viewport
const vw=window.innerWidth,vh=window.innerHeight;
menu.style.left=(e.clientX+140>vw?e.clientX-150:e.clientX)+'px';
menu.style.top=(e.clientY+100>vh?e.clientY-100:e.clientY)+'px';
// Rename
const renameItem=document.createElement('div');
renameItem.textContent=t('rename_title');
renameItem.style.cssText='padding:7px 14px;cursor:pointer;font-size:13px;color:var(--text);';
renameItem.onmouseenter=()=>renameItem.style.background='var(--hover)';
renameItem.onmouseleave=()=>renameItem.style.background='';
renameItem.onclick=()=>{menu.remove();_inlineRenameFileItem(item);};
menu.appendChild(renameItem);
// Reveal in File Manager
const revealItem=document.createElement('div');
revealItem.textContent=t('reveal_in_finder');
revealItem.style.cssText='padding:7px 14px;cursor:pointer;font-size:13px;color:var(--text);';
revealItem.onmouseenter=()=>revealItem.style.background='var(--hover)';
revealItem.onmouseleave=()=>revealItem.style.background='';
revealItem.onclick=async()=>{menu.remove();try{await api('/api/file/reveal',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:item.path})});}catch(err){showToast(t('reveal_failed')+err.message);}};
menu.appendChild(revealItem);
// Divider + Delete
const sep=document.createElement('hr');
sep.style.cssText='border:none;border-top:1px solid var(--border);margin:4px 0;';
menu.appendChild(sep);
const delItem=document.createElement('div');
delItem.textContent=t('delete_title');
delItem.style.cssText='padding:7px 14px;cursor:pointer;font-size:13px;color:var(--error,#e94560);';
delItem.onmouseenter=()=>delItem.style.background='var(--hover)';
delItem.onmouseleave=()=>delItem.style.background='';
delItem.onclick=()=>{menu.remove();if(item.type==='dir')deleteWorkspaceDir(item.path,item.name);else deleteWorkspaceFile(item.path,item.name);};
menu.appendChild(delItem);
document.body.appendChild(menu);
const dismiss=()=>{menu.remove();document.removeEventListener('click',dismiss);};
setTimeout(()=>document.addEventListener('click',dismiss),0);
}
async function _inlineRenameFileItem(item){
if(!S.session)return;
const newName=await showPromptDialog({message:t('rename_prompt'),defaultValue:item.name,placeholder:item.name,confirmLabel:t('rename_title')});
if(!newName||newName===item.name)return;
try{
await api('/api/file/rename',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:item.path,new_name:newName})});
showToast(t('renamed_to')+newName);
// Update expanded dirs cache key if renaming a directory
if(item.type==='dir'&&S._expandedDirs){
S._expandedDirs.delete(item.path);
const parent=item.path.includes('/')?item.path.substring(0,item.path.lastIndexOf('/')):'.';
const newPath=parent==='.'?newName:parent+'/'+newName;
S._expandedDirs.add(newPath);
if(S._dirCache[item.path]){S._dirCache[newPath]=S._dirCache[item.path];delete S._dirCache[item.path];}
if(typeof _saveExpandedDirs==='function')_saveExpandedDirs();
}
delete S._dirCache[S.currentDir];
await loadDir(S.currentDir);
}catch(err){showToast(t('rename_failed')+err.message);}
}
async function deleteWorkspaceFile(relPath, name){
if(!S.session)return;
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite * fix: custom provider with slash model name no longer rerouted to OpenRouter (#255) When base_url is configured in config.yaml, resolve_model_provider() now trusts the configured provider/base_url entirely and skips the slash-based OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom being silently routed to OpenRouter, resulting in 401 errors. Fixes #230 * test: mobile layout regression suite — 14 tests for every QA run (#254) Adds tests/test_mobile_layout.py with 14 static regression tests that run on every QA pass to catch mobile layout breakage before it reaches prod. Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile overlay, bottom nav, files button, profile dropdown z-index, chip overflow, workspace close, 100dvh, 44px touch targets, 16px font-size on textarea. * feat: /skills slash command lists and filters available Hermes skills (#257) Adds /skills [query] command to commands.js. Fetches from /api/skills, groups by category (alphabetically sorted), displays as a formatted assistant message. Optional query filters by name, description, or category. i18n keys added for en, de, zh, zh-Hant. 1 regression test added. Fixes #248 * feat: shared app dialogs replace native confirm()/prompt() calls (#251) Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt call sites across panels.js, sessions.js, ui.js, workspace.js. Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore, ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant. 5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of native dialog calls. Extracted from PR #242. * fix: Android Chrome mobile — workspace panel close + profile dropdown (#256) Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when toggling the right workspace panel. New closeMobileFiles() helper closes the panel with correct overlay state tracking. Overlay onclick calls both closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x) added to workspace panel header. Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at max-width:900px, escaping the overflow-x:auto stacking context that was clipping it on Android Chrome. Fix applied during review: closeMobileSidebar() now checks if the right panel is still open before hiding the overlay, preventing the overlay from disappearing when only the sidebar is closed. Fixes #247 Fixes #246 * feat: session ⋯ action dropdown replaces per-row buttons (#252) Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash) with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full keyboard (Escape), click-outside, scroll, and resize-reposition handling. Position:fixed prevents sidebar clipping. 5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete (danger style). Each with icon and descriptive subtitle. Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons asserts the new trigger and menu functions exist, old per-row classes are gone. Extracted from PR #242. * docs: v0.47.0 release notes, bump version, update test counts (645) --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
const _delFile=await showConfirmDialog({title:t('delete_confirm',name),message:'',confirmLabel:'Delete',danger:true,focusCancel:true});
if(!_delFile) return;
try{
await api('/api/file/delete',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:relPath})});
showToast(t('deleted')+name);
// Close preview if we just deleted the viewed file
if($('previewPathText').textContent===relPath)$('btnClearPreview').onclick();
await loadDir(S.currentDir);
}catch(e){setStatus(t('delete_failed')+e.message);}
}
async function promptNewFile(){
// If no active session but a default workspace is configured, auto-create
// a session bound to it so workspace actions work on the blank new-chat page.
if(!S.session){
const ws=(typeof S._profileDefaultWorkspace==='string'&&S._profileDefaultWorkspace)||'';
if(!ws) return;
try{
const r=await api('/api/session/new',{method:'POST',body:JSON.stringify({workspace:ws})});
if(r&&r.session){S.session=r.session;S.messages=[];syncTopbar();renderMessages();await renderSessionList();}
}catch(e){setStatus(t('create_failed')+e.message);return;}
}
if(!S.session)return;
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite * fix: custom provider with slash model name no longer rerouted to OpenRouter (#255) When base_url is configured in config.yaml, resolve_model_provider() now trusts the configured provider/base_url entirely and skips the slash-based OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom being silently routed to OpenRouter, resulting in 401 errors. Fixes #230 * test: mobile layout regression suite — 14 tests for every QA run (#254) Adds tests/test_mobile_layout.py with 14 static regression tests that run on every QA pass to catch mobile layout breakage before it reaches prod. Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile overlay, bottom nav, files button, profile dropdown z-index, chip overflow, workspace close, 100dvh, 44px touch targets, 16px font-size on textarea. * feat: /skills slash command lists and filters available Hermes skills (#257) Adds /skills [query] command to commands.js. Fetches from /api/skills, groups by category (alphabetically sorted), displays as a formatted assistant message. Optional query filters by name, description, or category. i18n keys added for en, de, zh, zh-Hant. 1 regression test added. Fixes #248 * feat: shared app dialogs replace native confirm()/prompt() calls (#251) Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt call sites across panels.js, sessions.js, ui.js, workspace.js. Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore, ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant. 5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of native dialog calls. Extracted from PR #242. * fix: Android Chrome mobile — workspace panel close + profile dropdown (#256) Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when toggling the right workspace panel. New closeMobileFiles() helper closes the panel with correct overlay state tracking. Overlay onclick calls both closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x) added to workspace panel header. Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at max-width:900px, escaping the overflow-x:auto stacking context that was clipping it on Android Chrome. Fix applied during review: closeMobileSidebar() now checks if the right panel is still open before hiding the overlay, preventing the overlay from disappearing when only the sidebar is closed. Fixes #247 Fixes #246 * feat: session ⋯ action dropdown replaces per-row buttons (#252) Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash) with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full keyboard (Escape), click-outside, scroll, and resize-reposition handling. Position:fixed prevents sidebar clipping. 5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete (danger style). Each with icon and descriptive subtitle. Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons asserts the new trigger and menu functions exist, old per-row classes are gone. Extracted from PR #242. * docs: v0.47.0 release notes, bump version, update test counts (645) --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
const name=await showPromptDialog({title:t('new_file_prompt'),placeholder:'filename.txt',confirmLabel:t('create')});
if(!name||!name.trim())return;
const relPath=S.currentDir==='.'?name.trim():(S.currentDir+'/'+name.trim());
try{
await api('/api/file/create',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:relPath,content:''})});
showToast(t('created')+name.trim());
await loadDir(S.currentDir);
openFile(relPath);
}catch(e){setStatus(t('create_failed')+e.message);}
}
2026-03-31 07:02:47 +00:00
async function promptNewFolder(){
// Same auto-create-session logic as promptNewFile for the blank page.
if(!S.session){
const ws=(typeof S._profileDefaultWorkspace==='string'&&S._profileDefaultWorkspace)||'';
if(!ws) return;
try{
const r=await api('/api/session/new',{method:'POST',body:JSON.stringify({workspace:ws})});
if(r&&r.session){S.session=r.session;S.messages=[];syncTopbar();renderMessages();await renderSessionList();}
}catch(e){setStatus(t('folder_create_failed')+e.message);return;}
}
2026-03-31 07:02:47 +00:00
if(!S.session)return;
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite * fix: custom provider with slash model name no longer rerouted to OpenRouter (#255) When base_url is configured in config.yaml, resolve_model_provider() now trusts the configured provider/base_url entirely and skips the slash-based OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom being silently routed to OpenRouter, resulting in 401 errors. Fixes #230 * test: mobile layout regression suite — 14 tests for every QA run (#254) Adds tests/test_mobile_layout.py with 14 static regression tests that run on every QA pass to catch mobile layout breakage before it reaches prod. Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile overlay, bottom nav, files button, profile dropdown z-index, chip overflow, workspace close, 100dvh, 44px touch targets, 16px font-size on textarea. * feat: /skills slash command lists and filters available Hermes skills (#257) Adds /skills [query] command to commands.js. Fetches from /api/skills, groups by category (alphabetically sorted), displays as a formatted assistant message. Optional query filters by name, description, or category. i18n keys added for en, de, zh, zh-Hant. 1 regression test added. Fixes #248 * feat: shared app dialogs replace native confirm()/prompt() calls (#251) Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt call sites across panels.js, sessions.js, ui.js, workspace.js. Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore, ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant. 5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of native dialog calls. Extracted from PR #242. * fix: Android Chrome mobile — workspace panel close + profile dropdown (#256) Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when toggling the right workspace panel. New closeMobileFiles() helper closes the panel with correct overlay state tracking. Overlay onclick calls both closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x) added to workspace panel header. Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at max-width:900px, escaping the overflow-x:auto stacking context that was clipping it on Android Chrome. Fix applied during review: closeMobileSidebar() now checks if the right panel is still open before hiding the overlay, preventing the overlay from disappearing when only the sidebar is closed. Fixes #247 Fixes #246 * feat: session ⋯ action dropdown replaces per-row buttons (#252) Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash) with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full keyboard (Escape), click-outside, scroll, and resize-reposition handling. Position:fixed prevents sidebar clipping. 5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete (danger style). Each with icon and descriptive subtitle. Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons asserts the new trigger and menu functions exist, old per-row classes are gone. Extracted from PR #242. * docs: v0.47.0 release notes, bump version, update test counts (645) --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
const name=await showPromptDialog({title:t('new_folder_prompt'),placeholder:'folder-name',confirmLabel:t('create')});
2026-03-31 07:02:47 +00:00
if(!name||!name.trim())return;
const relPath=S.currentDir==='.'?name.trim():(S.currentDir+'/'+name.trim());
2026-03-31 07:02:47 +00:00
try{
await api('/api/file/create-dir',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,path:relPath})});
showToast(t('folder_created')+name.trim());
await loadDir(S.currentDir);
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031) * fix: remove orphaned i18n keys from top-level LOCALES object Three Traditional Chinese translation keys (cmd_status, memory_saved, profile_delete_title) were placed outside any locale block between the en and ru blocks in static/i18n.js. They became top-level properties of the LOCALES object, causing them to appear as invalid language options in the Settings > Preferences dropdown. The correct translations already exist in the zh-Hant locale block. Fixes #1008 * fix: block stale SSE events from polluting new session's DOM - appendThinking(): guard with !S.session||!S.activeStreamId to drop events from a previous session's SSE stream during a session switch - appendLiveToolCard(): same guard for consistency - finalizeThinkingCard(): scroll thinking-card-body to top when scroll is pinned, so completed response is immediately visible - appendThinking(): auto-scroll thinking card body to bottom while streaming if user is watching (scroll pinned) * Fix empty agent sessions in sidebar * fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status Fixes #995 — three sub-issues in the Cron Jobs UI: 1. Dual play icons ambiguous: Resume button now shows a distinct play+bar icon (play triangle + vertical line) instead of the identical triangle used by Run now. 2. Toast notification overlapping header buttons: Added position:relative; z-index:10 to .main-view-header so it stacks above the fixed toast (z-index:100 within its layer). 3. No running status after trigger: After triggering a job, the status badge immediately shows 'running…' with a CSS spinner animation, and polls the cron list every 3s (up to 30s) to refresh when the job completes. - Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant) - Added .detail-badge.running CSS class with spinner animation - New functions: _setCronDetailStatus(), _startCronRunningPoll() * fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback - _clearCronDetail() now clears _cronRunningPoll interval on navigation - Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker) - When poll ends (30s max), detail re-renders with actual status as fallback * feat: create folder and add space directly from UI (#782) - After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog - Add Create folder if it doesnt exist checkbox in the New Space form - Backend: support create flag in /api/workspaces/add to mkdir before validation - i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales * fix: validate workspace path before mkdir to prevent orphan directories Review feedback (critical): the previous code called mkdir() before validate_workspace_to_add(), which meant a rejected path (e.g. system dir) would leave an orphan directory on disk. New flow: 1. Resolve path and check against blocked system roots BEFORE any mutation 2. mkdir() only if path passes the blocklist check 3. Full validation (exists, is_dir) after mkdir Also imports _workspace_blocked_roots for the pre-mutation blocklist check. * fix(#1014): classify model-not-found errors with helpful message - Add model_not_found error type to streaming.py exception classifier - Detect 404, 'not found', 'does not exist', 'invalid model' patterns - Strip HTML tags from provider error messages (nginx 404 pages, etc.) - Add model_not_found branch to apperror handler in messages.js - Add i18n key model_not_found_label in all 6 locales - 15 tests covering detection, sanitization, frontend, and i18n * feat(ui): add live TPS stat to header Adds a TPS (Tokens Per Second) chip to the right of the header title bar that updates live while AI output is streaming. Metering (api/metering.py) - Tracks per-session output + reasoning tokens via GlobalMeter singleton - Per-session TPS = total_tokens / elapsed_time - Global TPS = average of active sessions' TPS values - HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling window (only recorded when > 0, so idle periods are excluded) - Thread-safe with a single lock Metering events emitted from streaming.py - Throttled at 100ms from token/reasoning/tool callbacks so the display updates rapidly during fast token streams - 1Hz ticker as fallback for slow streams (exits when no active sessions) - Final stats emitted on stream end Routes (api/routes.py) - Removed POST /api/metering/interval endpoint (dynamic interval via focus/blur was replaced with simple always-1s-when-active approach) UI (static/messages.js, index.html, style.css) - TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low' - Default: '0.0 t/s . 0.0 high' when idle - Display updates on every metering SSE event (throttled to 100ms) * feat: session restore speed + title gen reasoning hardening (#1025, #1026) PR #1025 (@franksong2702): Speed up large session restore paths - GET /api/session?messages=0 now parses only metadata before the messages array - Metadata-only loads no longer populate the full-session LRU cache - Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup - Hard reload no longer waits for populateModelDropdown() before restoring session PR #1026 (@franksong2702): Harden auto title generation for reasoning models - Raises title-gen completion budget to 512 tokens (reasoning-safe) - Retries once with 1024 tokens on empty content / finish_reason:length - Applies retry to both auxiliary and active-agent fallback routes - Preserves underlying failure reason in title_status on local fallback Co-authored-by: Frank Song <franksong2702@gmail.com> * feat: session attention indicators in right slot + last_message_at timestamps (#1024) PR #1024 (@franksong2702): Polish session attention indicators - Streaming spinners and unread dots now reuse the right-side actions slot - Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps - Date group carets point down when expanded, right when collapsed - Pinned group no longer repeats pinned-star icon per row - Running indicators appear immediately after send (local busy state while /api/sessions catches up) - Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message) so metadata-only saves don't make old sessions appear under Today Co-authored-by: Frank Song <franksong2702@gmail.com> * docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36) --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Josh <josh@fyul.link> Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
// Offer to add the new folder as a space (#782)
const absPath=S.session.workspace?((S.currentDir==='.'?S.session.workspace:S.session.workspace+'/'+S.currentDir)+'/'+name.trim()):null;
if(absPath){
const addAsSpace=await showConfirmDialog({
title:t('folder_add_as_space_title'),
message:t('folder_add_as_space_msg'),
confirmLabel:t('folder_add_as_space_btn'),
focusCancel:true
});
if(addAsSpace){
try{
const data=await api('/api/workspaces/add',{method:'POST',body:JSON.stringify({path:absPath})});
if(typeof _workspaceList!=='undefined')_workspaceList=data.workspaces||_workspaceList||[];
if(typeof renderWorkspacesPanel==='function')renderWorkspacesPanel(_workspaceList);
showToast(t('workspace_added'));
}catch(e2){setStatus((t('error_prefix')||'Error: ')+e2.message);}
}
}
}catch(e){setStatus(t('folder_create_failed')+e.message);}
2026-03-31 07:02:47 +00:00
}
function renderTray(){ // non-media files use paperclip chip
const tray=$('attachTray');tray.innerHTML='';
if(!S.pendingFiles.length){tray.classList.remove('has-files');updateSendBtn();return;}
tray.classList.add('has-files');
updateSendBtn();
S.pendingFiles.forEach((f,i)=>{
const chip=document.createElement('div');chip.className='attach-chip';
const mediaKind=_mediaKindForName(f.name);
if(_IMAGE_EXTS.test(f.name)||mediaKind==='audio'||mediaKind==='video'){
const blobUrl=URL.createObjectURL(f);
chip.className='attach-chip attach-chip--media attach-chip--'+mediaKind; // attach-chip--audio attach-chip--video
chip.dataset.blobUrl=blobUrl;
if(mediaKind==='image'){
chip.innerHTML=`<img class="attach-thumb" src="${esc(blobUrl)}" alt="${esc(f.name)}" title="${esc(f.name)}"><button title="${t('remove_title')}">${li('x',12)}</button>`;
} else if(_SVG_EXTS.test(f.name)){
chip.innerHTML=`<img class="attach-thumb attach-thumb--svg" src="${esc(blobUrl)}" alt="${esc(f.name)}" title="${esc(f.name)}"><button title="${t('remove_title')}">${li('x',12)}</button>`;
} else if(mediaKind==='audio'){
chip.innerHTML=`<span class="attach-chip-media">🎵 ${esc(f.name)}</span><audio controls preload="metadata" src="${esc(blobUrl)}"></audio><button title="${t('remove_title')}">${li('x',12)}</button>`;
} else if(mediaKind==='video'){
chip.innerHTML=`<span class="attach-chip-media">🎬 ${esc(f.name)}</span><video controls preload="metadata" src="${esc(blobUrl)}"></video><button title="${t('remove_title')}">${li('x',12)}</button>`;
}
} else {
chip.innerHTML=`${li('paperclip',12)} ${esc(f.name)} <button title="${t('remove_title')}">${li('x',12)}</button>`;
}
chip.querySelector('button').onclick=()=>{
// Revoke blob URL to avoid memory leak before removing
if(chip.dataset.blobUrl) URL.revokeObjectURL(chip.dataset.blobUrl);
S.pendingFiles.splice(i,1);renderTray();
};
tray.appendChild(chip);
});
}
function addFiles(files){for(const f of files){if(!S.pendingFiles.find(p=>p.name===f.name))S.pendingFiles.push(f);}renderTray();}
async function uploadPendingFiles(){
if(!S.pendingFiles.length||!S.session)return[];
const names=[];let failures=0;
const bar=$('uploadBar');const barWrap=$('uploadBarWrap');
barWrap.classList.add('active');bar.style.width='0%';
const total=S.pendingFiles.length;
for(let i=0;i<total;i++){
const f=S.pendingFiles[i];const fd=new FormData();
fd.append('session_id',S.session.session_id);fd.append('file',f,f.name);
try{
const isArchive=_ARCHIVE_EXTS.test(f.name);
const url=new URL(isArchive?'api/upload/extract':'api/upload',document.baseURI||location.href).href;
const res=await fetch(url,{method:'POST',credentials:'include',body:fd});
v0.50.210: gpt-5.5, cron titles, agent cache, bfcache fix, onboarding fix, mermaid CSP, PWA auth (#1056) * feat(models): add gpt-5.5 to openai, openai-codex, copilot catalogs Adds GPT-5.5 and GPT-5.5 Mini entries to the static _PROVIDER_MODELS catalog so they appear in the model picker for the openai, openai-codex, and copilot providers. Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent * fix(models): add gpt-5.5-mini to copilot provider catalog * fix(renderer): suppress Mermaid Google Fonts CSP violation via fontFamily inherit (#1044) Mermaid's built-in 'dark' and 'default' themes inject an @import for fonts.googleapis.com/Manrope into every generated SVG. The CSP style-src only allows cdn.jsdelivr.net, so this request is blocked on every diagram render, filling the console with CSP errors. Fix: pass fontFamily:'inherit' (and fontSize:'14px') in the themeVariables block of mermaid.initialize() in renderMermaidBlocks(). This suppresses Mermaid's external font import and uses the page's existing font stack. Avoids adding fonts.googleapis.com to the CSP — no new external dependency, no font FOUT, consistent with the rest of the UI typography. 3 regression tests added in tests/test_1044_mermaid_csp_font.py. 2215/2215 tests passing. * fix(onboarding): non-standard provider/path cluster (#1029) * fix(bfcache): restore full layout on tab/session restore — rail, topbar, panels (#1045) The pageshow handler added for #822 only cleared the session search filter and re-rendered the session list. This left the rest of the layout chrome (topbar, rail icons, workspace panel, resize handles, gateway SSE) in the stale bfcache DOM state, causing a broken layout (oversized search icon, uninitialized rail) that required a hard refresh to fix. Fix: extend the pageshow handler to re-run the full set of layout sync calls that the boot IIFE runs on a fresh page load: syncTopbar() — restores model chip, title, topbar state syncWorkspacePanelState() — restores workspace panel open/closed _initResizePanels() — reattaches panel resize drag listeners startGatewaySSE() — reconnects the gateway SSE watcher (bfcache-persisted connections are dead) All four calls are typeof-guarded for safe degradation if a helper is not yet defined. The existing #822 fixes (sessionSearch clear + renderSessionListFromCache) are preserved unchanged. loadSession() is intentionally NOT re-called — it would cause message flicker; the sync calls above are sufficient to restore visual state. 7 regression tests added in tests/test_1045_bfcache_layout_restore.py. 2219/2219 tests passing. * fix(bfcache): also close open dropdowns on bfcache restore (#1045) Additional symptom noted in issue #1045: bfcache freezes the DOM including any open dropdown/popover state. The thinking-level selector (and other composer dropdowns) left open when navigating away would appear open without user interaction on tab restore. Extend the pageshow handler to call all four named close functions before the layout sync: closeModelDropdown() — composer model selector closeReasoningDropdown() — thinking/reasoning effort selector closeWsDropdown() — workspace chip dropdown closeProfileDropdown() — profile switcher dropdown All calls are typeof-guarded, matching the style of the layout sync calls already in the handler. 2 new tests (9 total in test_1045_bfcache_layout_restore.py): - pageshow closes all four named dropdowns - dropdown closes appear before layout sync calls (clean state first) 2221/2221 tests passing. * fix(bfcache): remove _initResizePanels() — bfcache preserves listeners * fix(bfcache): remove _initResizePanels from pageshow — bfcache preserves listeners; update test * fix(sessions): use cron job name as session title when available (#1032) * fix(test): add id column to messages table in cron title test fixture * fix(merge): inject cron title lookup into read_importable loop, remove stale sqlite3 block * fix(pwa): redirect to /login client-side on 401 — fixes iOS PWA auth expiry trap (#1038) When an auth session expires, the server returns a 302→/login for page requests. In a normal browser this works fine, but in an iOS PWA running in standalone mode the redirect navigates out of the PWA shell into Safari, leaving the app permanently stuck on 'Authentication required' with no recovery path. Fix: intercept 401 responses client-side before surfacing any error. - workspace.js api(): check res.status===401 first; call window.location.href='/login' and return immediately (no throw) - ui.js: add _redirectIfUnauth() helper; wire into all direct fetch() calls that bypass api() — api/models, api/models/live, api/upload All fetch paths that could receive a 401 now redirect cleanly within the PWA frame rather than opening Safari. 6 regression tests added in tests/test_1038_pwa_auth_redirect.py. 2175/2175 tests passing. * fix(pwa): preserve current URL in ?next= param on 401 redirect * fix(test): update 401-redirect assertion to accept ?next= URL format * feat(pwa): add _safeNextPath() to login.js so ?next= param is honored after re-login Addresses reviewer suggestion: the ?next= URL set on 401 redirect was ignored by the login success handler (always redirected to ./). _safeNextPath() validates and returns the ?next= param with open-redirect guards: rejects non-path-absolute inputs, // protocol-relative URLs, backslash variants, and control characters. 4 new regression tests added. * Implement session agent cache for AIAgent reuse Added session agent cache to reuse AIAgent across messages. * Implement agent caching for session management * Implement session agent eviction on session deletion Added session agent eviction to prevent turn count leakage in recycled sessions. * docs: v0.50.210 release notes — 7 PRs, 2239 tests (+27) * docs(changelog): drop stale [Unreleased] entries duplicated by v0.50.210 Three entries in the [Unreleased] section are duplicates of items now listed under v0.50.210: - Mermaid CSP font fix (#1044) → v0.50.210 / Mermaid Google Fonts CSP - bfcache layout restore (#1045) → v0.50.210 / bfcache layout and dropdown restore - iOS PWA auth redirect (#1038) → v0.50.210 / Login redirects back to original URL The original drafts landed in [Unreleased] when individual PRs (#1047, #1048, #1043) were approved; the v0.50.210 release-notes commit then added the same items under the version section without removing the [Unreleased] copies. Drop the duplicates so users reading the CHANGELOG don't see the same fix listed twice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent Co-authored-by: Pix (Hermes) <aliceisjustplaying@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: qxxaa <mrhanoi@outlook.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:47:44 +00:00
if(_redirectIfUnauth(res)) return;
if(!res.ok){const err=await res.text();throw new Error(err);}
const data=await res.json();
if(data.error)throw new Error(data.error);
if(isArchive){
names.push({name: data.dest, path: data.dest, extracted: data.extracted});
if(typeof loadDir==='function')loadDir(S.currentDir||'.');
}else{
names.push({name: data.filename, path: data.path, mime: data.mime, size: data.size, is_image: !!data.is_image});
}
}catch(e){failures++;setStatus(`\u274c ${t('upload_failed')}${f.name} \u2014 ${e.message}`);}
bar.style.width=`${Math.round((i+1)/total*100)}%`;
}
barWrap.classList.remove('active');bar.style.width='0%';
S.pendingFiles=[];renderTray();
if(failures===total&&total>0)throw new Error(t('all_uploads_failed',total));
// Show extraction summary
const extracted=names.filter(n=>n.extracted);
if(extracted.length)showToast(t('archive_extracted',extracted.reduce((s,n)=>s+n.extracted,0),extracted.length));
return names;
}