feat(commands): /background, /btw slash commands + undo button + reasoning chip
Rebased onto master after #931 (aux title routing) to resolve streaming.py conflict. All changes from both PRs are cleanly integrated. 2088 tests passing (2065 master + 23 from #931). Co-authored-by: bergeouss <bergeouss@gmail.com>
This commit is contained in:
@@ -20,6 +20,8 @@ const COMMANDS=[
|
||||
{name:'title', desc:t('cmd_title'), fn:cmdTitle, arg:'[title]'},
|
||||
{name:'retry', desc:t('cmd_retry'), fn:cmdRetry, noEcho:true},
|
||||
{name:'undo', desc:t('cmd_undo'), fn:cmdUndo, noEcho:true},
|
||||
{name:'btw', desc:t('cmd_btw'), fn:cmdBtw, arg:'question', noEcho:true},
|
||||
{name:'background',desc:t('cmd_background'),fn:cmdBackground,arg:'prompt', noEcho:true},
|
||||
{name:'status', desc:t('cmd_status'), fn:cmdStatus},
|
||||
{name:'voice', desc:t('cmd_voice'), fn:cmdVoice, noEcho:true},
|
||||
{name:'reasoning', desc:t('cmd_reasoning'), fn:cmdReasoning, arg:'show|hide|none|minimal|low|medium|high|xhigh', subArgs:['show','hide','none','minimal','low','medium','high','xhigh'], noEcho:true},
|
||||
@@ -573,6 +575,36 @@ async function cmdUndo(){
|
||||
showToast(`↩ ${t('undid_n_messages')} ${r.removed_count} ${t('undid_messages_suffix')}`);
|
||||
}catch(e){showToast(t('undo_failed')+e.message);}
|
||||
}
|
||||
async function undoLastExchange(){await cmdUndo();}
|
||||
async function cmdBtw(args){
|
||||
if(!S.session){showToast(t('no_active_session'));return;}
|
||||
const question=(args||'').trim();
|
||||
if(!question){showToast(t('cmd_btw_usage'));return;}
|
||||
showToast(t('btw_asking'));
|
||||
const activeSid=S.session.session_id;
|
||||
try{
|
||||
const r=await api('/api/btw',{method:'POST',body:JSON.stringify({session_id:activeSid,question})});
|
||||
if(r&&r.error){showToast(r.error);return;}
|
||||
// Connect to the ephemeral SSE stream
|
||||
const streamId=r.stream_id;
|
||||
const parentSid=r.parent_session_id;
|
||||
if(typeof attachBtwStream==='function') attachBtwStream(parentSid,streamId,question);
|
||||
}catch(e){showToast(t('btw_failed')+e.message);}
|
||||
}
|
||||
async function cmdBackground(args){
|
||||
if(!S.session){showToast(t('no_active_session'));return;}
|
||||
const prompt=(args||'').trim();
|
||||
if(!prompt){showToast(t('cmd_background_usage'));return;}
|
||||
showToast(t('bg_running'));
|
||||
const activeSid=S.session.session_id;
|
||||
try{
|
||||
const r=await api('/api/background',{method:'POST',body:JSON.stringify({session_id:activeSid,prompt})});
|
||||
if(r&&r.error){showToast(r.error);return;}
|
||||
// Show background badge and start polling
|
||||
if(typeof showBackgroundBadge==='function') showBackgroundBadge(r.task_id);
|
||||
if(typeof startBackgroundPolling==='function') startBackgroundPolling(activeSid,r.task_id,prompt);
|
||||
}catch(e){showToast(t('bg_failed')+e.message);}
|
||||
}
|
||||
async function cmdStatus(){
|
||||
if(!S.session){showToast(t('no_active_session'));return;}
|
||||
try{
|
||||
@@ -623,6 +655,7 @@ function cmdReasoning(args){
|
||||
.then(function(st){
|
||||
const eff=(st && st.reasoning_effort)||arg;
|
||||
showToast(BRAIN+' Reasoning effort set to '+eff+' (saved; applies to next turn)');
|
||||
if(typeof syncReasoningChip==='function') syncReasoningChip();
|
||||
})
|
||||
.catch(function(e){
|
||||
showToast(BRAIN+' Failed to set effort: '+(e && e.message ? e.message : arg));
|
||||
|
||||
@@ -102,6 +102,21 @@ const LOCALES = {
|
||||
cmd_title:'Get or set the session title',
|
||||
cmd_retry:'Resend the last message',
|
||||
cmd_undo:'Remove the last exchange',
|
||||
cmd_btw:'Ask a side question (ephemeral)',
|
||||
cmd_btw_usage:'/btw <question> — ask a side question using session context',
|
||||
cmd_background:'Run a prompt in background',
|
||||
cmd_background_usage:'/background <prompt> — run in parallel without blocking',
|
||||
btw_asking:'Asking side question...',
|
||||
btw_label:'Side question — not in history',
|
||||
btw_done:'Side question answered',
|
||||
btw_no_answer:'No answer received.',
|
||||
btw_failed:'Side question failed: ',
|
||||
bg_running:'Running in background...',
|
||||
bg_complete:'Background task complete',
|
||||
bg_label:'Background result:',
|
||||
bg_no_answer:'(no answer)',
|
||||
bg_failed:'Background task failed: ',
|
||||
undo_exchange:'Undo last exchange',
|
||||
cmd_status:'Show session info',
|
||||
cmd_voice:'Toggle microphone input',
|
||||
stream_stopped:'Response stopped.',
|
||||
@@ -199,7 +214,7 @@ const LOCALES = {
|
||||
settings_label_language: 'Language',
|
||||
settings_label_token_usage: 'Show token usage',
|
||||
settings_label_sidebar_density: 'Sidebar density',
|
||||
cmd_reasoning: 'Toggle thinking block visibility (show/hide) or set effort level',
|
||||
cmd_reasoning: 'Toggle thinking visibility (show/hide), set effort level, or check current status',
|
||||
settings_label_cli_sessions: 'Show agent sessions',
|
||||
settings_label_sync_insights: 'Sync to insights',
|
||||
settings_label_check_updates: 'Check for updates',
|
||||
@@ -655,7 +670,7 @@ const LOCALES = {
|
||||
settings_label_language: 'Язык',
|
||||
settings_label_token_usage: 'Показывать использование токенов',
|
||||
settings_label_sidebar_density: 'Плотность боковой панели',
|
||||
cmd_reasoning: 'Toggle thinking block visibility (show/hide) or set effort level',
|
||||
cmd_reasoning: 'Toggle thinking visibility (show/hide), set effort level, or check current status',
|
||||
settings_label_cli_sessions: 'Показывать сеансы агента',
|
||||
settings_label_sync_insights: 'Синхронизировать с Insights',
|
||||
settings_label_check_updates: 'Проверять обновления',
|
||||
@@ -1139,7 +1154,7 @@ const LOCALES = {
|
||||
settings_label_language: 'Idioma',
|
||||
settings_label_token_usage: 'Mostrar uso de tokens',
|
||||
settings_label_sidebar_density: 'Densidad de la barra lateral',
|
||||
cmd_reasoning: 'Toggle thinking block visibility (show/hide) or set effort level',
|
||||
cmd_reasoning: 'Toggle thinking visibility (show/hide), set effort level, or check current status',
|
||||
settings_label_cli_sessions: 'Mostrar sesiones de CLI',
|
||||
settings_label_sync_insights: 'Sincronizar con insights',
|
||||
settings_label_check_updates: 'Buscar actualizaciones',
|
||||
@@ -1595,7 +1610,7 @@ const LOCALES = {
|
||||
settings_label_language: 'Sprache',
|
||||
settings_label_token_usage: 'Token-Verbrauch anzeigen',
|
||||
settings_label_sidebar_density: 'Seitenleistendichte',
|
||||
cmd_reasoning: 'Toggle thinking block visibility (show/hide) or set effort level',
|
||||
cmd_reasoning: 'Toggle thinking visibility (show/hide), set effort level, or check current status',
|
||||
settings_label_cli_sessions: 'Agent-Sitzungen anzeigen',
|
||||
settings_label_sync_insights: 'Mit Insights synchronisieren',
|
||||
settings_label_check_updates: 'Nach Updates suchen',
|
||||
@@ -1852,7 +1867,7 @@ const LOCALES = {
|
||||
settings_label_language: '\u8bed\u8a00',
|
||||
settings_label_token_usage: '\u663e\u793a token \u7528\u91cf',
|
||||
settings_label_sidebar_density: '侧边栏密度',
|
||||
cmd_reasoning: 'Toggle thinking block visibility (show/hide) or set effort level',
|
||||
cmd_reasoning: 'Toggle thinking visibility (show/hide), set effort level, or check current status',
|
||||
settings_label_cli_sessions: '\u663e\u793a CLI \u4f1a\u8bdd',
|
||||
settings_label_sync_insights: '\u540c\u6b65\u5230 insights',
|
||||
settings_label_check_updates: '\u68c0\u67e5\u66f4\u65b0',
|
||||
@@ -2292,7 +2307,7 @@ const LOCALES = {
|
||||
settings_label_language: '\u8a9d\u8a00',
|
||||
settings_label_token_usage: '\u986f\u793a token \u7528\u91cf',
|
||||
settings_label_sidebar_density: '側邊欄密度',
|
||||
cmd_reasoning: 'Toggle thinking block visibility (show/hide) or set effort level',
|
||||
cmd_reasoning: 'Toggle thinking visibility (show/hide), set effort level, or check current status',
|
||||
settings_label_cli_sessions: '\u986f\u793a CLI \u6703\u8a71',
|
||||
settings_label_sync_insights: '\u540c\u6b65\u5230 insights',
|
||||
settings_label_check_updates: '\u6aa2\u67e5\u66f4\u65b0',
|
||||
|
||||
@@ -346,6 +346,21 @@
|
||||
<span class="composer-workspace-chevron" aria-hidden="true"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="composer-reasoning-wrap" id="composerReasoningWrap" style="display:none">
|
||||
<button class="composer-reasoning-chip" id="composerReasoningChip" type="button" onclick="toggleReasoningDropdown()" title="Reasoning effort level">
|
||||
<span class="composer-reasoning-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a7 7 0 0 1 7 7c0 2.38-1.19 4.47-3 5.74V17a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 0 1 7-7z"/><line x1="9" y1="21" x2="15" y2="21"/></svg></span>
|
||||
<span class="composer-reasoning-label" id="composerReasoningLabel"></span>
|
||||
<span class="composer-reasoning-chevron" aria-hidden="true"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||
</button>
|
||||
<div class="composer-reasoning-dropdown" id="composerReasoningDropdown">
|
||||
<div class="reasoning-option" data-effort="none">None</div>
|
||||
<div class="reasoning-option" data-effort="minimal">Minimal</div>
|
||||
<div class="reasoning-option" data-effort="low">Low</div>
|
||||
<div class="reasoning-option" data-effort="medium">Medium</div>
|
||||
<div class="reasoning-option" data-effort="high">High</div>
|
||||
<div class="reasoning-option" data-effort="xhigh">Extra High</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="composer-model-wrap">
|
||||
<button class="composer-model-chip" id="composerModelChip" type="button" onclick="toggleModelDropdown()" title="Conversation model">
|
||||
<span class="composer-model-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M15 2v2"/><path d="M15 20v2"/><path d="M2 15h2"/><path d="M2 9h2"/><path d="M20 15h2"/><path d="M20 9h2"/><path d="M9 2v2"/><path d="M9 20v2"/></svg></span>
|
||||
@@ -396,6 +411,7 @@
|
||||
<button class="cancel-btn" id="btnCancel" onclick="cancelStream()" style="display:none" title="Stop generation" aria-label="Stop generation">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="5" width="14" height="14" rx="2"></rect></svg>
|
||||
</button>
|
||||
<span class="bg-badge" id="bgBadge" style="display:none" title="Background tasks running">0</span>
|
||||
<button class="send-btn" id="btnSend" title="Send message" 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"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
|
||||
</button>
|
||||
|
||||
@@ -1323,4 +1323,111 @@ function sendBrowserNotification(title,body){
|
||||
}
|
||||
}
|
||||
|
||||
// ── /btw ephemeral stream ────────────────────────────────────────────────────
|
||||
// Connects to the ephemeral SSE stream from /api/btw and renders the answer
|
||||
// in a visually distinct bubble that is NOT persisted to session history.
|
||||
|
||||
function attachBtwStream(parentSid, streamId, question){
|
||||
if(!parentSid||!streamId) return;
|
||||
const src=new EventSource('/api/stream?stream_id='+encodeURIComponent(streamId));
|
||||
let answer='';
|
||||
let btwRow=null;
|
||||
function _ensureBtwRow(){
|
||||
if(btwRow&&btwRow.isConnected) return;
|
||||
const inner=$('msgInner');
|
||||
if(!inner) return;
|
||||
btwRow=document.createElement('div');
|
||||
btwRow.className='msg-row msg-row-btw';
|
||||
btwRow.dataset.role='assistant';
|
||||
btwRow.dataset.btw='1';
|
||||
const labelEl=document.createElement('div');
|
||||
labelEl.className='msg-btw-label';
|
||||
labelEl.textContent=t('btw_label');
|
||||
const qEl=document.createElement('div');
|
||||
qEl.className='msg-body';
|
||||
qEl.textContent=question;
|
||||
const ansEl=document.createElement('div');
|
||||
ansEl.className='msg-body msg-btw-answer';
|
||||
ansEl.textContent='...';
|
||||
btwRow.appendChild(labelEl);
|
||||
btwRow.appendChild(qEl);
|
||||
btwRow.appendChild(ansEl);
|
||||
inner.appendChild(btwRow);
|
||||
btwRow.scrollIntoView({behavior:'smooth',block:'end'});
|
||||
}
|
||||
src.addEventListener('token',e=>{
|
||||
try{answer+=JSON.parse(e.data).text||'';}catch(_){}
|
||||
_ensureBtwRow();
|
||||
const ansEl=btwRow&&btwRow.querySelector('.msg-btw-answer');
|
||||
if(ansEl) ansEl.innerHTML=renderMd(answer);
|
||||
});
|
||||
src.addEventListener('done',e=>{
|
||||
src.close();
|
||||
try{
|
||||
const d=JSON.parse(e.data);
|
||||
if(d.answer&&!answer) answer=d.answer;
|
||||
}catch(_){}
|
||||
if(btwRow&&btwRow.isConnected){
|
||||
const ansEl=btwRow.querySelector('.msg-btw-answer');
|
||||
if(ansEl) ansEl.innerHTML=renderMd(answer||t('btw_no_answer'));
|
||||
}
|
||||
showToast(t('btw_done'));
|
||||
});
|
||||
src.addEventListener('apperror',e=>{
|
||||
src.close();
|
||||
try{
|
||||
const d=JSON.parse(e.data);
|
||||
showToast(t('btw_failed')+(d.message||''));
|
||||
}catch(_){showToast(t('btw_failed'));}
|
||||
if(btwRow&&btwRow.isConnected) btwRow.remove();
|
||||
});
|
||||
src.addEventListener('stream_end',()=>{src.close();});
|
||||
src.onerror=()=>{src.close();if(btwRow&&btwRow.isConnected) btwRow.remove();};
|
||||
}
|
||||
|
||||
// ── /background task tracking ────────────────────────────────────────────────
|
||||
|
||||
let _bgPollTimers={};
|
||||
let _bgActiveTasks=new Set();
|
||||
|
||||
function showBackgroundBadge(taskId){
|
||||
_bgActiveTasks.add(taskId);
|
||||
const badge=$('bgBadge');
|
||||
if(badge){
|
||||
badge.textContent=String(_bgActiveTasks.size);
|
||||
badge.style.display=_bgActiveTasks.size?'':'none';
|
||||
}
|
||||
}
|
||||
function hideBackgroundBadge(taskId){
|
||||
_bgActiveTasks.delete(taskId);
|
||||
const badge=$('bgBadge');
|
||||
if(badge){
|
||||
badge.textContent=String(_bgActiveTasks.size);
|
||||
badge.style.display=_bgActiveTasks.size?'':'none';
|
||||
}
|
||||
}
|
||||
function startBackgroundPolling(parentSid, taskId, prompt){
|
||||
if(_bgPollTimers[taskId]) return;
|
||||
async function _poll(){
|
||||
try{
|
||||
const r=await api('/api/background/status?session_id='+encodeURIComponent(parentSid));
|
||||
if(r&&r.results){
|
||||
for(const res of r.results){
|
||||
if(res.task_id===taskId){
|
||||
hideBackgroundBadge(taskId);
|
||||
delete _bgPollTimers[taskId];
|
||||
const msg={role:'assistant',content:`**${t('bg_label')}** ${prompt.slice(0,80)}\n\n${res.answer||t('bg_no_answer')}`,'_background':true,_ts:Date.now()/1000};
|
||||
S.messages.push(msg);
|
||||
renderMessages();
|
||||
showToast(t('bg_complete'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(_){}
|
||||
_bgPollTimers[taskId]=setTimeout(_poll,3000);
|
||||
}
|
||||
_poll();
|
||||
}
|
||||
|
||||
// ── Panel navigation (Chat / Tasks / Skills / Memory) ──
|
||||
|
||||
@@ -593,6 +593,17 @@
|
||||
.composer-workspace-chip.active{color:var(--text);background:var(--accent-bg);border-color:var(--accent-bg);}
|
||||
.composer-workspace-icon,.composer-workspace-chevron{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;}
|
||||
.composer-workspace-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.composer-reasoning-wrap{position:relative;flex:0 1 auto;min-width:0;}
|
||||
.composer-reasoning-chip{display:inline-flex;align-items:center;gap:8px;max-width:180px;padding:8px 10px 8px 12px;border-radius:999px;border:1px solid transparent;background-color:transparent;color:var(--muted);font-weight:500;cursor:pointer;transition:color .15s,background-color .15s,border-color .15s;}
|
||||
.composer-reasoning-chip:hover{color:var(--text);background-color:var(--hover-bg);}
|
||||
.composer-reasoning-chip.active{color:var(--text);background:var(--accent-bg);border-color:var(--accent-bg);}
|
||||
.composer-reasoning-icon,.composer-reasoning-chevron{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;}
|
||||
.composer-reasoning-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.composer-reasoning-dropdown{display:none;position:absolute;bottom:calc(100% + 6px);left:0;background:var(--surface);border:1px solid var(--border);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.15);min-width:140px;z-index:100;padding:4px;animation:dropdown-in .12s ease-out;}
|
||||
.composer-reasoning-dropdown.open{display:block;}
|
||||
.reasoning-option{padding:8px 14px;border-radius:6px;cursor:pointer;font-size:13px;color:var(--text);white-space:nowrap;transition:background-color .1s;}
|
||||
.reasoning-option:hover{background:var(--hover-bg);}
|
||||
.reasoning-option.selected{color:var(--accent);font-weight:600;}
|
||||
.composer-model-wrap{position:relative;flex:0 1 auto;min-width:0;}
|
||||
.composer-model-chip{display:inline-flex;align-items:center;gap:8px;max-width:220px;padding:8px 10px 8px 12px;border-radius:999px;border:1px solid transparent;background-color:transparent;color:var(--muted);font-weight:500;cursor:pointer;transition:color .15s,background-color .15s,border-color .15s;}
|
||||
.composer-model-chip:hover{color:var(--text);background-color:var(--hover-bg);}
|
||||
@@ -769,12 +780,15 @@
|
||||
.composer-profile-label,
|
||||
.composer-workspace-label,
|
||||
.composer-model-label,
|
||||
.composer-reasoning-label,
|
||||
.composer-profile-chevron,
|
||||
.composer-workspace-chevron,
|
||||
.composer-model-chevron{display:none;}
|
||||
.composer-model-chevron,
|
||||
.composer-reasoning-chevron{display:none;}
|
||||
.composer-profile-chip,
|
||||
.composer-workspace-chip,
|
||||
.composer-model-chip{max-width:44px;min-width:44px;min-height:44px;padding:6px;justify-content:center;gap:0;font-size:11px;}
|
||||
.composer-model-chip,
|
||||
.composer-reasoning-chip{max-width:44px;min-width:44px;min-height:44px;padding:6px;justify-content:center;gap:0;font-size:11px;}
|
||||
.composer-divider{display:none;}
|
||||
.composer-status{max-width:96px;font-size:10px;}
|
||||
.send-btn{width:32px;height:32px;}
|
||||
@@ -1909,3 +1923,40 @@ body.resizing{user-select:none;cursor:col-resize;}
|
||||
.assistant-turn .msg-foot{opacity:1;}
|
||||
.msg-actions{opacity:1;}
|
||||
}
|
||||
|
||||
/* ── /btw ephemeral side-question bubble ── */
|
||||
.msg-row-btw {
|
||||
border-left: 3px solid var(--accent-bg-strong);
|
||||
border-radius: 8px;
|
||||
background: var(--accent-bg);
|
||||
margin: 6px 0 6px var(--msg-rail);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.msg-row-btw .msg-body { font-size: 14px; line-height: 1.75; }
|
||||
.msg-btw-label {
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
opacity: .8;
|
||||
}
|
||||
.msg-btw-answer {
|
||||
margin-top: 6px;
|
||||
border-top: 1px solid var(--accent-bg-strong);
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
/* ── /background badge in composer footer ── */
|
||||
.bg-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
background: var(--accent-bg-strong);
|
||||
color: var(--accent-text);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
74
static/ui.js
74
static/ui.js
@@ -386,6 +386,7 @@ function toggleModelDropdown(){
|
||||
if(open){closeModelDropdown(); return;}
|
||||
if(typeof closeProfileDropdown==='function') closeProfileDropdown();
|
||||
if(typeof closeWsDropdown==='function') closeWsDropdown();
|
||||
if(typeof closeReasoningDropdown==='function') closeReasoningDropdown();
|
||||
renderModelDropdown();
|
||||
dd.classList.add('open');
|
||||
_positionModelDropdown();
|
||||
@@ -407,6 +408,77 @@ window.addEventListener('resize',()=>{
|
||||
if(dd&&dd.classList.contains('open')) _positionModelDropdown();
|
||||
});
|
||||
|
||||
// ── Reasoning effort chip ────────────────────────────────────────────────────
|
||||
let _currentReasoningEffort=null;
|
||||
|
||||
function _applyReasoningChip(eff){
|
||||
_currentReasoningEffort=eff;
|
||||
const wrap=$('composerReasoningWrap');
|
||||
const label=$('composerReasoningLabel');
|
||||
if(!wrap||!label) return;
|
||||
if(!eff||eff==='none'){wrap.style.display='none';return;}
|
||||
wrap.style.display='';
|
||||
label.textContent='🧠 '+eff;
|
||||
_highlightReasoningOption(eff);
|
||||
}
|
||||
|
||||
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();
|
||||
_highlightReasoningOption(_currentReasoningEffort);
|
||||
dd.classList.add('open');
|
||||
chip.classList.add('active');
|
||||
}
|
||||
|
||||
function closeReasoningDropdown(){
|
||||
const dd=$('composerReasoningDropdown');
|
||||
const chip=$('composerReasoningChip');
|
||||
if(dd) dd.classList.remove('open');
|
||||
if(chip) chip.classList.remove('active');
|
||||
}
|
||||
|
||||
document.addEventListener('click',function(e){
|
||||
if(!e.target.closest('#composerReasoningChip')&&!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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Scroll pinning ──────────────────────────────────────────────────────────
|
||||
// When streaming, auto-scroll only if the user hasn't manually scrolled up.
|
||||
// Once the user scrolls back to within 150px of the bottom, re-pin.
|
||||
@@ -1330,6 +1402,7 @@ function syncTopbar(){
|
||||
}
|
||||
}
|
||||
if(typeof syncModelChip==='function') syncModelChip();
|
||||
if(typeof syncReasoningChip==='function') syncReasoningChip();
|
||||
// Show Clear button only when session has messages
|
||||
const clearBtn=$('btnClearConv');
|
||||
if(clearBtn) clearBtn.style.display=(S.messages&&S.messages.filter(msg=>msg.role!=='tool').length>0)?'':'none';
|
||||
@@ -1693,6 +1766,7 @@ function renderMessages(){
|
||||
const bodyHtml = isUser ? esc(String(content)).replace(/\n/g,'<br>') : renderMd(_stripXmlToolCallsDisplay(String(content)));
|
||||
const isEditableUser=isUser&&rawIdx===lastUserRawIdx;
|
||||
const editBtn = isEditableUser ? `<button class="msg-action-btn" title="${t('edit_message')}" onclick="editMessage(this)">${li('pencil',13)}</button>` : '';
|
||||
const undoBtn = isLastAssistant ? `<button class="msg-action-btn" title="${t('undo_exchange')}" onclick="undoLastExchange()">${li('undo-2',13)}</button>` : '';
|
||||
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 tsVal=m._ts||m.timestamp;
|
||||
|
||||
Reference in New Issue
Block a user