diff --git a/debian/changelog b/debian/changelog index 6df2ff1..d0386f4 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +devdesk (0.9.5) bookworm; urgency=medium + + * Context use is shown in tokens as well as percent (#43): Claude's + footer shows "ctx 34% 346k/1M", and the Sessions panel and session + switcher show "ctx 34% · 346k/1M". + + -- Jared Vititoe Sun, 11 Oct 2026 23:07:00 -0400 + devdesk (0.9.4) bookworm; urgency=medium * The inbox badge no longer only grows (#42). Opening the bell marks its diff --git a/libexec/devdesk-statusline b/libexec/devdesk-statusline index cd55d7d..99f9fb5 100755 --- a/libexec/devdesk-statusline +++ b/libexec/devdesk-statusline @@ -43,16 +43,20 @@ if [[ $session =~ ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$ ]]; then fi fi -# Footer line: session · model · context % · plan use. ANSI colour numbers, not +# Footer line: session · model · context % and tokens · plan use. ANSI colour numbers, not # RGB, so the browser's dark or light palette applies (devdesk#31). Claude sends # rate_limits only to Pro/Max subscribers; there .cost is an API-price estimate, # not a bill, so show the 5-hour plan use instead and keep $ for API-key billing. jq -rM --arg s "$session" ' def c($sgr; $t): "\u001b[\($sgr)m\($t)\u001b[0m"; def pct: if . == null then "—" else "\(. | floor)%" end; + def tok: if . >= 1000000 then "\(. / 100000 | round / 10)M" elif . >= 1000 then "\(. / 1000 | round)k" else "\(.)" end; + def ctxtok: .context_window | (.current_usage | if . then (.input_tokens // 0) + (.cache_creation_input_tokens // 0) + + (.cache_read_input_tokens // 0) else null end) as $u + | if $u == null then "" else " \($u | tok)\(if .context_window_size then "/\(.context_window_size | tok)" else "" end)" end; [ (if $s != "" then c("33"; "◈ \($s)") else empty end), c("36"; .model.display_name // "Claude"), - c("37"; "ctx \(.context_window.used_percentage | pct)"), + c("37"; "ctx \(.context_window.used_percentage | pct)\(ctxtok)"), (if .rate_limits.five_hour.used_percentage != null then c("37"; "5h \(.rate_limits.five_hour.used_percentage | pct)") else c("37"; "$\((.cost.total_cost_usd // 0) * 100 | round / 100)") end) diff --git a/tests/attach.bats b/tests/attach.bats index d856edd..546c836 100644 --- a/tests/attach.bats +++ b/tests/attach.bats @@ -195,7 +195,7 @@ claude_args() { "cost":{"total_cost_usd":1.5,"total_lines_added":10,"total_lines_removed":2}}' run bash -c "echo '$status_json' | TMUX='$DEVDESK_SOCKET,1,0' TMUX_PANE='$pane' '$REPO/libexec/devdesk-statusline'" [ "$status" -eq 0 ] - [[ $output == *"alpha"* && $output == *"ctx 34%"* ]] + [[ $output == *"alpha"* && $output == *"ctx 34% 346k/1M"* ]] run devdesk attach --list [ "$(jq -r '.sessions[] | select(.name=="alpha") | .context.used_tokens' <<<"$output")" = 346000 ] [ "$(jq -r '.sessions[] | select(.name=="alpha") | .context.model' <<<"$output")" = Opus ] diff --git a/web/src/main.js b/web/src/main.js index 5a093d9..6a75a66 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -5,7 +5,7 @@ import { TtydConnection, fetchAlerts, fetchRepo, fetchToken, investigateAlert, sessionAction, uploadFile, AuthExpiredError, UnauthorizedError, } from './connection.js'; import { parseTitle, pasteSafe, sessionFromSearch } from './protocol.js'; -import { SessionsPanel, recentNames, rememberSession, sessionStatus } from './sessions.js'; +import { SessionsPanel, ctxLabel, recentNames, rememberSession, sessionStatus } from './sessions.js'; import { renderAlerts } from './alerts.js'; import { renderRepo } from './repo.js'; import { bindAskClaude } from './widgets.js'; @@ -547,7 +547,7 @@ function renderSwitcher() { }; const items = sessions.map((s) => { const status = sessionStatus(s); - const ctx = s.context?.used_pct != null ? ` · ctx ${Math.floor(s.context.used_pct)}%` : ''; + const ctx = s.context?.used_pct != null ? ` · ${ctxLabel(s.context)}` : ''; return el('button', { type: 'button', role: 'menuitem', class: `dd-menu-item dd-switch-item is-${status.key}${s.name === state.session ? ' is-current' : ''}`, diff --git a/web/src/sessions.js b/web/src/sessions.js index a0a0596..323ba6b 100644 --- a/web/src/sessions.js +++ b/web/src/sessions.js @@ -46,6 +46,14 @@ function renameRecent(from, to) { writeRecents(readRecents().map((r) => (r.name === from ? { ...r, name: to } : r))); } +// "ctx 34% · 346k/1M": context use as a share of the window and in tokens. +const tokens = (n) => (n >= 1e6 ? `${+(n / 1e6).toFixed(n % 1e6 ? 1 : 0)}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n)); +export function ctxLabel(c) { + if (c?.used_pct == null) return ''; + const count = c.used_tokens != null ? ` · ${tokens(c.used_tokens)}${c.window ? `/${tokens(c.window)}` : ''}` : ''; + return `ctx ${Math.floor(c.used_pct)}%${count}`; +} + export function ago(epochSeconds) { if (!epochSeconds) return ''; const s = Math.max(0, Date.now() / 1000 - epochSeconds); @@ -267,7 +275,7 @@ export class SessionsPanel { renderSession(s) { const isCurrent = s.name === this.current; const status = sessionStatus(s); - const ctx = s.context?.used_pct != null ? `ctx ${Math.floor(s.context.used_pct)}%` : ''; + const ctx = ctxLabel(s.context); const since = s.status?.since ? ago(s.status.since) : ago(s.activity); const model = isCurrent && s.context?.model ? s.context.model.replace(/^Claude\s+/i, '') : ''; const meta = s.running