From 7a985a14d440acdda93dc7a43ee7c6bdecc2bf44 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 9 Nov 2026 19:28:18 +0100 Subject: feat: LLM card shows current session models with section headers --- deckit/modules/llm.py | 24 ++++++++++++++++++++++-- static/deck.css | 26 ++++++++++++++++++++++++++ static/modules/llm.js | 29 ++++++++++++++++++++++++----- tests/test_llm.py | 20 +++++++++++++++----- 4 files changed, 87 insertions(+), 12 deletions(-) diff --git a/deckit/modules/llm.py b/deckit/modules/llm.py index 310eabf..fb45097 100644 --- a/deckit/modules/llm.py +++ b/deckit/modules/llm.py @@ -9,7 +9,7 @@ import urllib.request from ..module import Module -ENDPOINTS = ("/api/claude_limits", "/api/claude_session", "/api/summary") +ENDPOINTS = ("/api/claude_limits", "/api/claude_session", "/api/summary", "/api/last_session") def _fetch_for(base): @@ -37,6 +37,22 @@ def _opencode(rows) -> dict: } +def _claude_model(session) -> str | None: + models = (session or {}).get("models") or [] + return models[0].get("model") if models else None + + +def _opencode_session(sessions) -> dict: + """The latest opencode session's dominant model from /api/last_session.""" + for block in sessions or []: + if not isinstance(block, dict) or block.get("program") != "opencode": + continue + rows = block.get("rows") or [] + top = max(rows, key=lambda r: r.get("calls") or 0) if rows else {} + return {"model": top.get("model"), "provider": top.get("provider")} + return {"model": None, "provider": None} + + def make(cfg: dict) -> Module: base = cfg.get("llm", {}).get("url", "http://127.0.0.1:8420").rstrip("/") fetch = cfg.get("fetch") or _fetch_for(base) @@ -53,6 +69,8 @@ def make(cfg: dict) -> Module: latest = limits[-1] else: latest = {} + claude_session = (data.get("/api/claude_session") or {}).get("session") + sessions = (data.get("/api/last_session") or {}).get("sessions") or [] return { "offline": False, "claude": { @@ -65,8 +83,10 @@ def make(cfg: dict) -> Module: "extra_limit": latest.get("extra_monthly_limit"), "extra_utilization": latest.get("extra_utilization"), "extra_currency": latest.get("extra_currency"), + "model": _claude_model(claude_session), }, - "opencode": _opencode(data.get("/api/summary") or []), + "opencode": {**_opencode(data.get("/api/summary") or []), + **_opencode_session(sessions)}, } return Module(name="llm", title="LLM usage", state=state) diff --git a/static/deck.css b/static/deck.css index 0e44646..79bf374 100644 --- a/static/deck.css +++ b/static/deck.css @@ -258,6 +258,32 @@ select { .wclose { flex: none; width: 36px; height: 36px; padding: 0; } .windows .launches { display: flex; flex-wrap: wrap; gap: 8px; } +.llm { display: flex; flex-direction: column; gap: 10px; } +.llm-sec { + padding: 10px 12px; + border-radius: 12px; + background: var(--udt-mantle); + border: 1px solid var(--udt-surface1); +} +.llm-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; } +.llm-head .ico { width: 18px; height: 18px; color: var(--udt-accent); flex: none; } +.llm-head h3 { + font-size: 12px; + text-transform: uppercase; + letter-spacing: .08em; + color: var(--udt-subtext0); + flex: none; +} +.llm-head .model { + margin-left: auto; + max-width: 60%; + font-size: 12px; + color: var(--udt-overlay0); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .gauges { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 12px; } .gauge { display: flex; flex-direction: column; gap: 4px; padding: 10px 12px; border-radius: 12px; background: var(--udt-surface1); } .glabel { font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--udt-overlay0); } diff --git a/static/modules/llm.js b/static/modules/llm.js index 0af8d7b..9d05f29 100644 --- a/static/modules/llm.js +++ b/static/modules/llm.js @@ -8,6 +8,21 @@ const tokens = (n) => { return String(n); }; +const CLAUDE_ICON = ''; +const OPENCODE_ICON = ''; + +function section(icon, title, model) { + const sec = document.createElement("section"); + sec.className = "llm-sec"; + const head = document.createElement("header"); + head.className = "llm-head"; + head.innerHTML = `${icon}

`; + head.querySelector("h3").textContent = title; + head.querySelector(".model").textContent = model || "—"; + sec.append(head); + return sec; +} + function gauge(label, value) { const d = document.createElement("div"); d.className = "gauge"; @@ -26,7 +41,7 @@ function gauge(label, value) { return d; } -function big(label, value, sub) { +function stat(label, value, sub) { const d = document.createElement("div"); d.className = "lbig"; const l = document.createElement("span"); @@ -59,6 +74,8 @@ export default { } const c = state.claude ?? {}; const o = state.opencode ?? {}; + + const claude = section(CLAUDE_ICON, "Claude Code", c.model); const gauges = document.createElement("div"); gauges.className = "gauges"; gauges.append( @@ -66,9 +83,11 @@ export default { gauge("5h", c.five_hour_pct), gauge("7d", c.seven_day_pct), ); - box.append(gauges); - box.append( - big("opencode spend", money(o.cost), `${tokens(o.tokens)} · ${o.calls ?? 0} calls`), - ); + claude.append(gauges); + box.append(claude); + + const opencode = section(OPENCODE_ICON, "opencode", o.model); + opencode.append(stat("spend", money(o.cost), `${tokens(o.tokens)} · ${o.calls ?? 0} calls`)); + box.append(opencode); }, }; diff --git a/tests/test_llm.py b/tests/test_llm.py index 354b5fe..ea28b08 100644 --- a/tests/test_llm.py +++ b/tests/test_llm.py @@ -19,7 +19,7 @@ class Down(unittest.TestCase): class Up(unittest.TestCase): def test_aggregates_opencode_and_ignores_other_programs(self): data = {"/api/claude_limits": [{"context_pct": 12}], - "/api/claude_session": {"context_used": 1200}, + "/api/claude_session": {"session": {"models": [{"model": "claude-opus-4"}]}}, "/api/summary": [ {"program": "opencode", "provider": "deepseek", "model": "v4", "total_cost_usd": 1.5, "tokens": 1000, "calls": 3}, @@ -27,24 +27,34 @@ class Up(unittest.TestCase): "total_cost_usd": 0.5, "tokens": 500, "calls": 1}, {"program": "assistant", "provider": "x", "model": "y", "total_cost_usd": 9.0, "tokens": 9999, "calls": 9}, - ]} + ], + "/api/last_session": {"sessions": [ + {"program": "opencode", "rows": [ + {"provider": "together", "model": "qwen", "calls": 1}, + {"provider": "deepseek", "model": "v4", "calls": 3}, + ]}, + ]}} st = llm.make({"llm": {"url": "http://x"}, "fetch": lambda u, timeout=2: data[u]}).state() self.assertFalse(st["offline"]) self.assertEqual(st["claude"]["context_pct"], 12) + self.assertEqual(st["claude"]["model"], "claude-opus-4") self.assertEqual(st["opencode"]["cost"], 2.0) self.assertEqual(st["opencode"]["tokens"], 1500) self.assertEqual(st["opencode"]["calls"], 4) - self.assertEqual(st["opencode"]["models"][0]["model"], "v4") + self.assertEqual(st["opencode"]["model"], "v4") def test_handles_the_real_latest_shape(self): data = {"/api/claude_limits": {"latest": {"context_pct": 42, "five_hour_pct": 10}}, - "/api/claude_session": {}, - "/api/summary": []} + "/api/claude_session": {"session": None}, + "/api/summary": [], + "/api/last_session": {"sessions": []}} st = llm.make({"llm": {"url": "http://x"}, "fetch": lambda u, timeout=2: data[u]}).state() self.assertEqual(st["claude"]["context_pct"], 42) self.assertEqual(st["claude"]["five_hour_pct"], 10) + self.assertIsNone(st["claude"]["model"]) + self.assertIsNone(st["opencode"]["model"]) if __name__ == "__main__": -- cgit v1.2.3