feat: context usage indicator in composer footer
Shows a compact bar + label in the composer footer after the first response, displaying input/output token counts, context window fill percentage, and estimated cost. Bar turns yellow >50% and red >75%. Updates on every response completion via the existing usage data from the done SSE event. Hidden until first response (no usage data yet). Inspired by PR #75 (@MartinNielsenDev). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -264,6 +264,10 @@
|
||||
</svg>
|
||||
</button>
|
||||
</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">
|
||||
<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>
|
||||
|
||||
@@ -149,7 +149,7 @@ async function send(){
|
||||
// Stamp _ts on the last assistant message if it has no timestamp
|
||||
const lastAsst=[...S.messages].reverse().find(m=>m.role==='assistant');
|
||||
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){
|
||||
S.toolCalls=d.session.tool_calls.map(tc=>({...tc,done:true}));
|
||||
} else {
|
||||
|
||||
@@ -183,6 +183,13 @@
|
||||
textarea#msg::placeholder{color:var(--muted);}
|
||||
.composer-footer{display:flex;align-items:center;justify-content:space-between;padding:6px 10px 10px;}
|
||||
.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;}
|
||||
.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;}
|
||||
|
||||
26
static/ui.js
26
static/ui.js
@@ -84,6 +84,32 @@ 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);}
|
||||
|
||||
// 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 fill (assume 128k default, common for most models)
|
||||
const ctxWindow=128000;
|
||||
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(){
|
||||
if(!_scrollPinned) return;
|
||||
const el=$('messages');
|
||||
|
||||
Reference in New Issue
Block a user