Merge pull request #83 from nesquena/feat/context-usage-indicator
feat: context usage indicator in composer footer
This commit is contained in:
@@ -264,6 +264,10 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ctx-indicator" id="ctxIndicator" style="display:none" title="Context window usage">
|
||||||
|
<span class="ctx-bar-wrap"><span class="ctx-bar" id="ctxBar"></span></span>
|
||||||
|
<span class="ctx-label" id="ctxLabel"></span>
|
||||||
|
</div>
|
||||||
<div class="composer-right">
|
<div class="composer-right">
|
||||||
<button class="send-btn" id="btnSend" title="Send message" style="display:none">
|
<button class="send-btn" id="btnSend" title="Send message" style="display:none">
|
||||||
<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>
|
<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>
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ async function send(){
|
|||||||
// Stamp _ts on the last assistant message if it has no timestamp
|
// Stamp _ts on the last assistant message if it has no timestamp
|
||||||
const lastAsst=[...S.messages].reverse().find(m=>m.role==='assistant');
|
const lastAsst=[...S.messages].reverse().find(m=>m.role==='assistant');
|
||||||
if(lastAsst&&!lastAsst._ts&&!lastAsst.timestamp) lastAsst._ts=Date.now()/1000;
|
if(lastAsst&&!lastAsst._ts&&!lastAsst.timestamp) lastAsst._ts=Date.now()/1000;
|
||||||
if(d.usage) S.lastUsage=d.usage;
|
if(d.usage){S.lastUsage=d.usage;_syncCtxIndicator(d.usage);}
|
||||||
if(d.session.tool_calls&&d.session.tool_calls.length){
|
if(d.session.tool_calls&&d.session.tool_calls.length){
|
||||||
S.toolCalls=d.session.tool_calls.map(tc=>({...tc,done:true}));
|
S.toolCalls=d.session.tool_calls.map(tc=>({...tc,done:true}));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -189,6 +189,13 @@
|
|||||||
textarea#msg::placeholder{color:var(--muted);}
|
textarea#msg::placeholder{color:var(--muted);}
|
||||||
.composer-footer{display:flex;align-items:center;justify-content:space-between;padding:6px 10px 10px;}
|
.composer-footer{display:flex;align-items:center;justify-content:space-between;padding:6px 10px 10px;}
|
||||||
.composer-left{display:flex;gap:2px;align-items:center;}
|
.composer-left{display:flex;gap:2px;align-items:center;}
|
||||||
|
/* Context usage indicator */
|
||||||
|
.ctx-indicator{display:flex;align-items:center;gap:6px;padding:2px 4px;flex-shrink:1;min-width:0;}
|
||||||
|
.ctx-bar-wrap{width:70px;height:5px;border-radius:3px;background:rgba(255,255,255,.08);overflow:hidden;flex-shrink:0;}
|
||||||
|
.ctx-bar{display:block;height:100%;border-radius:3px;transition:width .4s ease,background .4s ease;min-width:2px;background:var(--teal);}
|
||||||
|
.ctx-bar.ctx-mid{background:#e6a817;}
|
||||||
|
.ctx-bar.ctx-high{background:#e05252;}
|
||||||
|
.ctx-label{font-size:9px;color:var(--muted);white-space:nowrap;font-variant-numeric:tabular-nums;}
|
||||||
.composer-right{display:flex;gap:6px;align-items:center;}
|
.composer-right{display:flex;gap:6px;align-items:center;}
|
||||||
.icon-btn{width:34px;height:34px;border-radius:8px;background:none;border:none;color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:all .15s;}
|
.icon-btn{width:34px;height:34px;border-radius:8px;background:none;border:none;color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:all .15s;}
|
||||||
.icon-btn{opacity:.75;}
|
.icon-btn{opacity:.75;}
|
||||||
|
|||||||
30
static/ui.js
30
static/ui.js
@@ -84,6 +84,36 @@ let _scrollPinned=true;
|
|||||||
})();
|
})();
|
||||||
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);}
|
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);}
|
||||||
|
|
||||||
|
// Context usage indicator in composer footer
|
||||||
|
function _syncCtxIndicator(usage){
|
||||||
|
const el=$('ctxIndicator');
|
||||||
|
if(!el)return;
|
||||||
|
const inTok=usage.input_tokens||0;
|
||||||
|
const outTok=usage.output_tokens||0;
|
||||||
|
const total=inTok+outTok;
|
||||||
|
if(!total){el.style.display='none';return;}
|
||||||
|
el.style.display='';
|
||||||
|
// Estimate context window from model name (rough, covers major families)
|
||||||
|
// TODO: fetch exact values from server or model metadata API
|
||||||
|
const _CTX={claude:200000,gemini:1000000,'gpt-4o':128000,'gpt-5':128000,o3:200000,o4:200000,deepseek:128000,llama:128000};
|
||||||
|
const _m=(S.session&&S.session.model||'').toLowerCase();
|
||||||
|
let ctxWindow=128000;
|
||||||
|
for(const[k,v]of Object.entries(_CTX)){if(_m.includes(k)){ctxWindow=v;break;}}
|
||||||
|
const pct=Math.min(100,Math.round((inTok/ctxWindow)*100));
|
||||||
|
const bar=$('ctxBar');
|
||||||
|
const label=$('ctxLabel');
|
||||||
|
if(bar){
|
||||||
|
bar.style.width=pct+'%';
|
||||||
|
bar.className='ctx-bar'+(pct>75?' ctx-high':pct>50?' ctx-mid':'');
|
||||||
|
}
|
||||||
|
if(label){
|
||||||
|
const cost=usage.estimated_cost;
|
||||||
|
let text=`${_fmtTokens(inTok)} in \u00b7 ${_fmtTokens(outTok)} out`;
|
||||||
|
if(cost) text+=` \u00b7 $${cost<0.01?cost.toFixed(4):cost.toFixed(2)}`;
|
||||||
|
label.textContent=text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function scrollIfPinned(){
|
function scrollIfPinned(){
|
||||||
if(!_scrollPinned) return;
|
if(!_scrollPinned) return;
|
||||||
const el=$('messages');
|
const el=$('messages');
|
||||||
|
|||||||
Reference in New Issue
Block a user