diff --git a/api/config.py b/api/config.py index dc399b1..8ef8bbf 100644 --- a/api/config.py +++ b/api/config.py @@ -632,6 +632,7 @@ _SETTINGS_DEFAULTS = { 'default_model': DEFAULT_MODEL, 'default_workspace': str(DEFAULT_WORKSPACE), 'send_key': 'enter', # 'enter' or 'ctrl+enter' + 'show_token_usage': False, # show input/output token badge below assistant messages 'password_hash': None, # SHA-256 hash; None = auth disabled } @@ -651,6 +652,7 @@ _SETTINGS_ALLOWED_KEYS = set(_SETTINGS_DEFAULTS.keys()) - {'password_hash'} _SETTINGS_ENUM_VALUES = { 'send_key': {'enter', 'ctrl+enter'}, } +_SETTINGS_BOOL_KEYS = {'show_token_usage'} def save_settings(settings: dict) -> dict: """Save settings to disk. Returns the merged settings. Ignores unknown keys.""" @@ -669,6 +671,9 @@ def save_settings(settings: dict) -> dict: # Validate enum-constrained keys if k in _SETTINGS_ENUM_VALUES and v not in _SETTINGS_ENUM_VALUES[k]: continue + # Coerce bool keys + if k in _SETTINGS_BOOL_KEYS: + v = bool(v) current[k] = v SETTINGS_FILE.write_text( json.dumps(current, ensure_ascii=False, indent=2), diff --git a/api/streaming.py b/api/streaming.py index 29e4f48..c22aea4 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -170,6 +170,11 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta persist_user_message=msg_text, ) s.messages = result.get('messages') or s.messages + # Stamp 'timestamp' on any messages that don't have one yet + _now = time.time() + for _m in s.messages: + if isinstance(_m, dict) and not _m.get('timestamp') and not _m.get('_ts'): + _m['timestamp'] = int(_now) s.title = title_from(s.messages, s.title) # Read token/cost usage from the agent object (if available) input_tokens = getattr(agent, 'session_prompt_tokens', 0) or 0 diff --git a/static/boot.js b/static/boot.js index cff23c6..9aab685 100644 --- a/static/boot.js +++ b/static/boot.js @@ -308,7 +308,7 @@ document.querySelectorAll('.suggestion').forEach(btn=>{ (async()=>{ // Load send key preference - try{const s=await api('/api/settings');window._sendKey=s.send_key||'enter';}catch(e){window._sendKey='enter';} + try{const s=await api('/api/settings');window._sendKey=s.send_key||'enter';window._showTokenUsage=!!s.show_token_usage;}catch(e){window._sendKey='enter';window._showTokenUsage=false;} // Fetch active profile try{const p=await api('/api/profile/active');S.activeProfile=p.name||'default';}catch(e){S.activeProfile='default';} // Update profile chip label immediately diff --git a/static/commands.js b/static/commands.js index 9ab2d7c..33c24b7 100644 --- a/static/commands.js +++ b/static/commands.js @@ -8,6 +8,7 @@ const COMMANDS=[ {name:'model', desc:'Switch model (e.g. /model gpt-4o)', fn:cmdModel, arg:'model_name'}, {name:'workspace', desc:'Switch workspace by name', fn:cmdWorkspace, arg:'name'}, {name:'new', desc:'Start a new chat session', fn:cmdNew}, + {name:'usage', desc:'Toggle token usage display on/off', fn:cmdUsage}, ]; function parseCommand(text){ @@ -98,6 +99,19 @@ async function cmdNew(){ showToast('New session created'); } +async function cmdUsage(){ + const next=!window._showTokenUsage; + window._showTokenUsage=next; + try{ + await api('/api/settings',{method:'POST',body:JSON.stringify({show_token_usage:next})}); + }catch(e){} + // Update the settings checkbox if the panel is open + const cb=$('settingsShowTokenUsage'); + if(cb) cb.checked=next; + renderMessages(); + showToast('Token usage '+(next?'on':'off')); +} + // ── Autocomplete dropdown ─────────────────────────────────────────────────── let _cmdSelectedIdx=-1; diff --git a/static/index.html b/static/index.html index b501569..be31f31 100644 --- a/static/index.html +++ b/static/index.html @@ -324,6 +324,13 @@ +
/usage.