diff --git a/static/messages.js b/static/messages.js
index 5b5a7295..55cd822d 100644
--- a/static/messages.js
+++ b/static/messages.js
@@ -14,7 +14,7 @@ async function send(){
if(S.busy||compressionRunning){
if(text){
if(!S.session){await newSession();await renderSessionList();}
- queueSessionMessage(S.session.session_id,{text,files:[...S.pendingFiles]});
+ queueSessionMessage(S.session.session_id,{text,files:[...S.pendingFiles],model:S.session&&S.session.model||($('modelSelect')&&$('modelSelect').value)||'',profile:S.activeProfile||'default'});
$('msg').value='';autoResize();
S.pendingFiles=[];renderTray();
updateQueueBadge(S.session.session_id);
@@ -137,7 +137,7 @@ async function send(){
stopApprovalPolling();
stopClarifyPolling();
// Keep the user's attempted turn by queueing it for after the current run.
- queueSessionMessage(activeSid,{text:msgText,files:[]});
+ queueSessionMessage(activeSid,{text:msgText,files:[],model:S.session&&S.session.model||($('modelSelect')&&$('modelSelect').value)||'',profile:S.activeProfile||'default'});
updateQueueBadge(activeSid);
showToast('Current session is still running. Reconnected and queued your message.',2600);
try{
diff --git a/static/sessions.js b/static/sessions.js
index d6c8ee23..89e25ec3 100644
--- a/static/sessions.js
+++ b/static/sessions.js
@@ -112,10 +112,14 @@ async function loadSession(sid){
data = await api(`/api/session?session_id=${encodeURIComponent(sid)}&messages=0&resolve_model=0`);
} catch(e) {
const _msgInner = $('msgInner');
- if (_msgInner) {
- _msgInner.innerHTML = '
Failed to load session. Try switching sessions or refreshing.
';
+ if(_msgInner){
+ if(e.status===404){
+ _msgInner.innerHTML='
Session not available in web UI.
';
+ } else {
+ _msgInner.innerHTML='
Failed to load session. Try switching sessions or refreshing.
';
+ if(typeof showToast==='function') showToast('Failed to load session',3000,'error');
+ }
}
- if (typeof showToast === 'function') showToast('Failed to load session', 3000, 'error');
return;
}
S.session=data.session;
diff --git a/static/style.css b/static/style.css
index 688f740f..b4417a28 100644
--- a/static/style.css
+++ b/static/style.css
@@ -412,6 +412,27 @@
.approval-btn:hover{background:rgba(255,255,255,0.12);transform:translateY(-1px);box-shadow:0 2px 8px rgba(0,0,0,0.2);}
.approval-btn:active{transform:translateY(0);box-shadow:none;}
.approval-btn:disabled{opacity:.5;cursor:not-allowed;transform:none;}
+ /* ── Queue flyout (same slide-up pattern as approval-card) ── */
+ .queue-card{position:absolute;left:0;right:0;bottom:0;max-width:var(--msg-max);margin:0 auto;padding:0 20px;box-sizing:border-box;width:100%;overflow:hidden;pointer-events:none;z-index:2;}
+ .queue-card.visible{pointer-events:auto;}
+ /* When queue is visible, add bottom padding to messages so last bubble isn't covered */
+ .messages.queue-open{padding-bottom:var(--queue-card-height,280px);}
+ .queue-card-inner{background:var(--surface);border:1px solid var(--border);border-bottom:none;border-radius:14px 14px 0 0;contain:paint;transform:translateY(100%);opacity:0;transition:transform .35s cubic-bezier(.32,.72,.16,1),opacity .2s ease;overflow:hidden;max-height:240px;overflow-y:auto;padding-bottom:4px;}
+ .queue-card.visible .queue-card-inner{transform:translateY(0);opacity:1;}
+ .queue-card-header{display:flex;align-items:center;gap:8px;padding:9px 14px 8px;border-bottom:1px solid var(--border);font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);}
+ .queue-card-header-actions{display:flex;align-items:center;gap:4px;margin-left:auto;}
+ .queue-card-btn{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:6px;border:1px solid var(--border);background:none;color:var(--muted);font-size:11px;font-weight:500;cursor:pointer;transition:background .12s,color .12s;white-space:nowrap;}
+ .queue-card-btn:hover{background:var(--hover-bg);color:var(--text);}
+ .queue-card-icon-btn{display:inline-flex;align-items:center;padding:3px;border-radius:5px;border:none;background:none;color:var(--muted);cursor:pointer;opacity:.5;transition:opacity .12s,background .12s;}
+ .queue-card-icon-btn:hover{opacity:1;background:var(--hover-bg);}
+ .queue-card-row{display:flex;align-items:center;gap:8px;padding:8px 14px;border-bottom:1px solid var(--border);cursor:grab;}
+ .queue-card-row:last-child{border-bottom:none;}
+ .queue-card-row:hover{background:color-mix(in srgb,var(--hover-bg) 50%,transparent);}
+ .queue-card-drag{color:var(--muted);opacity:.3;flex-shrink:0;line-height:0;}
+ .queue-card-text{flex:1;font-size:13px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;outline:none;cursor:text;min-width:0;}
+ .queue-card-text:focus{white-space:pre-wrap;overflow:auto;text-overflow:clip;}
+ .queue-card-badges{display:flex;align-items:center;gap:5px;flex-shrink:0;font-size:10px;color:var(--muted);opacity:.6;}
+ .queue-card-file-badge{display:flex;align-items:center;gap:2px;}
.approval-btn-icon{font-size:13px;line-height:1;}
.approval-btn-label{line-height:1;}
.approval-kbd{display:inline-flex;align-items:center;justify-content:center;padding:1px 5px;border-radius:4px;font-size:10px;font-family:inherit;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);color:var(--muted);line-height:1.4;margin-left:2px;}
@@ -2261,3 +2282,13 @@ main.main > .main-view:not([id="mainChat"]):not([id="mainSettings"]) .main-view-
font-size: 10px;
font-weight: 600;
}
+
+ /* Queue pill outer: identical wrapper to .queue-card — max-width+padding positions the pill at card-inner width */
+ .queue-pill-outer{display:none;max-width:var(--msg-max);margin:0 auto;padding:0 20px;box-sizing:border-box;width:100%;}
+ .queue-pill-outer.show{display:block;animation:queuePillIn .18s ease;}
+ @keyframes queuePillIn{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:translateY(0);}}
+ /* Queue pill button: fills the outer slot, styled like queue-card-inner */
+ .queue-pill{display:flex;width:100%;box-sizing:border-box;align-items:center;gap:8px;padding:9px 14px;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);cursor:pointer;background:var(--surface);border:1px solid var(--border);border-bottom:none;border-radius:14px 14px 0 0;text-align:left;}
+ .queue-pill:hover{background:color-mix(in srgb,var(--hover-bg) 60%,var(--surface));}
+ .queue-pill-count{color:var(--text);}
+ .queue-pill-chevron{margin-left:auto;opacity:.5;}
diff --git a/static/ui.js b/static/ui.js
index 09fa1e0b..0d31922e 100644
--- a/static/ui.js
+++ b/static/ui.js
@@ -964,6 +964,13 @@ function setBusy(v){
setTimeout(()=>{
$('msg').value=next.text||'';
S.pendingFiles=Array.isArray(next.files)?[...next.files]:[];
+ // 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(typeof _applyModelToDropdown==='function'&&$('modelSelect')) _applyModelToDropdown(next.model,$('modelSelect'));
+ if(typeof syncModelChip==='function') syncModelChip();
+ }
autoResize();
renderTray();
send();
@@ -972,21 +979,309 @@ function setBusy(v){
}
}
+// ── 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(typeof scrollToBottom==='function') scrollToBottom();
+ }, 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 firstFiles=(snapshot.find(e=>e&&Array.isArray(e.files)&&e.files.length)||{files:[]}).files;
+ liveQ.length=0;liveQ.push({text:combined,files:firstFiles,_queued_at:Date.now()});
+ 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; titlebar "N queued" re-shows it
+ const hideBtn=document.createElement('button');
+ hideBtn.className='queue-card-icon-btn';
+ hideBtn.title='Hide queue (click the titlebar badge to show again)';
+ 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):'')+
+ `
${label}`+
+ `
`+(typeof li==='function'?li('chevron-up',12):'▲')+``;
+ 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(typeof scrollToBottom==='function') scrollToBottom();
+ };
+ } else {
+ if(pillOuter) pillOuter.classList.remove('show');
+ pill.onclick=null;
+ }
+}
+
+function _syncQueueTitlebar(sid,count){
+ const sub=document.getElementById('appTitlebarSub');
+ if(!sub) return;
+ if(count>0){
+ const label=typeof t==='function'?t('queued_count',count):(count===1?'1 queued':`${count} queued`);
+ sub.textContent=label;
+ sub.hidden=false;
+ sub.setAttribute('role','button');
+ sub.setAttribute('tabindex','0');
+ sub.setAttribute('aria-label',label);
+ sub.style.cursor='pointer';sub.style.color='var(--muted)';sub.style.fontWeight='600';sub.style.fontSize='11px';sub.style.background='var(--hover-bg)';sub.style.padding='2px 6px';sub.style.borderRadius='6px';sub.style.border='1px solid var(--border)';
+ const toggleQueue=()=>{
+ const activeSid=S.session&&S.session.session_id;
+ if(!activeSid) return;
+ const qCard=document.getElementById('queueCard');
+ if(!qCard) return;
+ if(qCard.classList.contains('visible')){
+ qCard.classList.remove('visible');
+ // Show pill since flyout is now hidden
+ const liveCount=_getSessionQueue(activeSid,false).length;
+ _updateQueuePill(activeSid,liveCount);
+ } else {
+ qCard.classList.add('visible');
+ // Hide pill since flyout is now showing
+ _updateQueuePill(activeSid,0);
+ if(typeof scrollToBottom==='function') scrollToBottom();
+ }
+ };
+ sub.onclick=toggleQueue;
+ sub.onkeydown=(e)=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();toggleQueue();}};
+ sub.title='Click to show/hide queue panel';
+ } else {
+ sub.textContent='';sub.hidden=true;
+ sub.removeAttribute('role');sub.removeAttribute('tabindex');sub.removeAttribute('aria-label');
+ sub.style.cssText='';
+ sub.onclick=null;sub.onkeydown=null;
+ }
+}
+
function updateQueueBadge(sessionId){
const sid=sessionId||(S.session&&S.session.session_id);
const count=sid?getQueuedSessionCount(sid):0;
- let badge=$('queueBadge');
- if(count>0){
- if(!badge){
- badge=document.createElement('div');
- badge.id='queueBadge';
- badge.style.cssText='position:fixed;bottom:80px;right:24px;background:rgba(124,185,255,.18);border:1px solid rgba(124,185,255,.4);color:var(--blue);font-size:12px;font-weight:600;padding:6px 14px;border-radius:20px;z-index:50;pointer-events:none;backdrop-filter:blur(8px);';
- document.body.appendChild(badge);
+ 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);
}
- badge.textContent=count===1?'1 message queued':`${count} messages queued`;
- } else if(badge) {
- badge.remove();
}
+ // Only update titlebar if this is the active session
+ if(!S.session||sid===S.session.session_id) _syncQueueTitlebar(sid,count);
}
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);}
@@ -1415,6 +1710,7 @@ function syncTopbar(){
const vis=S.messages.filter(m=>m&&m.role&&m.role!=='tool');
const _topbarMeta=$('topbarMeta');if(_topbarMeta)_topbarMeta.textContent=t('n_messages',vis.length);
if(typeof syncAppTitlebar==='function') syncAppTitlebar();
+ if(typeof _syncQueueTitlebar==='function'){const _qs=S.session&&S.session.session_id;_syncQueueTitlebar(_qs,_qs?getQueuedSessionCount(_qs):0);}
// 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;