Files
devdesk/libexec/devdesk-statusline
T
jaredandClaude Opus 5.5 a2edec966a
build-deb / Notify on failure (push) Successful in 4s
build-deb / Lint, test, package (push) Failing after 2m22s
build-deb / Deploy to CT 126 (push) Skipped
feat: v0.10.0 quieter Matrix, worktrees, usage pace, repo diffs, copy reply, turn timer, compact
- Matrix "needs you": ignore Claude's idle reminder (no red, no post); post
  only when no tab is in view, after 60 s, once per session per 10 min, and
  say what is asked. Closes #50
- devdesk new NAME PROJECT --worktree: own checkout on devdesk/NAME,
  cleaned up on end when clean/merged. Closes #44
- Usage meters: reset time, pace warning; Matrix ping past 90% when away.
  Closes #45
- Repo panel lists changed files; click opens the diff. Closes #46
- Copy Claude's last reply (--last-reply). Closes #47
- Turn timer in chip, title and cards. Closes #48
- Compact button at 80% context when idle. Closes #49

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHRLCCdXYm41Qx5kNhehaP
2026-10-11 23:28:11 -04:00

75 lines
4.1 KiB
Bash
Executable File

#!/bin/bash
# Claude Code statusLine command (wired in via claude-settings.json). Claude pipes
# its status JSON in on every update; we keep a snapshot per DevDesk session for
# the web UI (context, plan usage, PR) and print a one-line status for Claude's footer.
# Runs often while Claude streams, so it stays cheap and never fails loudly.
input=$(cat)
# Only record for panes on DevDesk's own tmux server.
session="" outer=${TMUX:-}
if [ "${outer%%,*}" = "${DEVDESK_SOCKET:-/run/devdesk/tmux.sock}" ] && [ -n "${TMUX_PANE:-}" ]; then
session=$(tmux display-message -p -t "$TMUX_PANE" '#S' 2>/dev/null)
session=${session:-${DEVDESK_SESSION:-}}
fi
if [[ $session =~ ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$ ]]; then
dir="${DEVDESK_STATE_DIR:-/var/lib/devdesk}/context"
if mkdir -p "$dir" 2>/dev/null && tmp=$(mktemp "$dir/.$session.XXXXXX" 2>/dev/null); then
if jq -cM '{
updated: (now | floor),
claude_session_id: .session_id,
model: .model.display_name,
model_id: .model.id,
effort: .effort.level,
used_pct: .context_window.used_percentage,
window: .context_window.context_window_size,
used_tokens: (.context_window.current_usage
| if . then ((.input_tokens // 0) + (.cache_creation_input_tokens // 0)
+ (.cache_read_input_tokens // 0)) else null end),
cost_usd: .cost.total_cost_usd,
lines_added: .cost.total_lines_added,
lines_removed: .cost.total_lines_removed,
# devdesk#25: plan usage (account-wide) and the pull request of the branch
rate_limits: (.rate_limits // null | if . then {
five_hour: (.five_hour | if . then {pct: .used_percentage, resets_at} else null end),
seven_day: (.seven_day | if . then {pct: .used_percentage, resets_at} else null end)} else null end),
pr: (.pr // null | if . then {number, url, review_state, kind} else null end)
}' <<<"$input" >"$tmp" 2>/dev/null; then
mv -f "$tmp" "$dir/$session.json"
else
rm -f "$tmp"
fi
fi
fi
# Plan usage past DEVDESK_USAGE_WARN: ping Matrix if you're away (devdesk#45).
# The marker holds the window already warned about, so this stays one read.
if [[ $session != "" ]] && read -r pct resets < <(jq -rM '.rate_limits.five_hour
| select(.used_percentage != null and .resets_at != null)
| "\(.used_percentage | floor) \(.resets_at)"' <<<"$input" 2>/dev/null) &&
[ "$pct" -ge "${DEVDESK_USAGE_WARN:-90}" ] &&
[ "$(cat "${DEVDESK_STATE_DIR:-/var/lib/devdesk}/usage-warned" 2>/dev/null)" != "$resets" ]; then
setsid -f "${DEVDESK_LIBEXEC:-/usr/lib/devdesk}/devdesk-notify-matrix" --usage "$pct" "$resets" \
</dev/null >/dev/null 2>&1 || true
fi
# 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)\(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)
] | join(c("90"; " · "))' <<<"$input" 2>/dev/null || true