feat: token usage toggle (setting + /usage command) + timestamp fixes

Token usage display:
- Add 'show_token_usage' boolean to settings (default: false, off by default)
- Settings panel: checkbox 'Show token usage after responses'
- /usage slash command: instant toggle with toast feedback, persists to
  server, updates checkbox if settings panel is open, re-renders messages
- Boot: load show_token_usage alongside send_key on startup
- ui.js: gate usage badge on window._showTokenUsage flag

Timestamps:
- streaming.py: stamp 'timestamp' on every message that lacks one at
  conversation completion; old messages (no timestamp field) now get a
  wall-clock time the first time they're touched by a new turn
- messages.js: stamp _ts on the last assistant message at done-event time
  so the time shows immediately on the current turn before next reload
- Timestamps already render in the UI (Sprint 14): faint time on each
  role header line, full opacity on hover, full date in title tooltip
This commit is contained in:
Nathan Esquenazi
2026-04-04 02:04:41 +00:00
parent b1d687ba22
commit 2fb2ddeaaa
8 changed files with 44 additions and 3 deletions

View File

@@ -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),

View File

@@ -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