·

MIT licence

A rounded frame around gradient pills for model, effort, path, branch, context bar, five-hour usage, diff, cost and session time, with labels underneath. Pure Node with no network calls; creates its own config file in ~/.claude on first run.

0 copies

In Claude Desktop? Add this status line with statusline-anywhere.

Preview

Clean repo
​ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ▐ Opus 4.8 ▌ ▐ high ▌ ▐ …/user/app ▌ ▐ main ▌ ▐ 22% / 200k ━━━━━━━━━━ ▌ ▐ 26% ━━━━━━━━━━ 16:10 ▌ ▐ +128 −34 ▌ ▐ $0.41 ▌ ▐ 10m 12s ▌ │ │ MODEL EFFORT PATH BRANCH CONTEXT USAGE DIFF COST TIME │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ​
New session
​ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │ ▐ Opus 4.8 ▌ ▐ high ▌ ▐ …/user/app ▌ ▐ main ▌ ▐ 0% / 200k ━━━━━━━━━━ ▌ ▐ $0.00 ▌ ▐ 10m 12s ▌ │ │ MODEL EFFORT PATH BRANCH CONTEXT COST TIME │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ​
Dirty branch
​ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ▐ Sonnet 4.6 ▌ ▐ medium ▌ ▐ …/user/app ▌ ▐ feat/auth ▌ ▐ 48% / 200k ━━━━━━━━━━ ▌ ▐ 40% ━━━━━━━━━━ 15:15 ▌ ▐ +128 −34 ▌ ▐ $0.41 ▌ ▐ 10m 12s ▌ │ │ MODEL EFFORT PATH BRANCH CONTEXT USAGE DIFF COST TIME │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ​
Near-full
​ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ▐ Opus 4.8 ▌ ▐ max ▌ ▐ …/user/app ▌ ▐ main ▌ ▐ 91% / 200k ━━━━━━━━━━ ▌ ▐ 88% ━━━━━━━━━━ 14:21 ▌ ▐ +128 −34 ▌ ▐ $4.12 ▌ ▐ 10m 12s ▌ │ │ MODEL EFFORT PATH BRANCH CONTEXT USAGE DIFF COST TIME │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ​
1M context
​ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ▐ Fable 5 ▌ ▐ xhigh ▌ ▐ …/user/app ▌ ▐ main ▌ ▐ 64% / 1.0M ━━━━━━━━━━ ▌ ▐ 33% ━━━━━━━━━━ 17:33 ▌ ▐ +128 −34 ▌ ▐ $0.41 ▌ ▐ 10m 12s ▌ │ │ MODEL EFFORT PATH BRANCH CONTEXT USAGE DIFF COST TIME │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ​
Post-compact
​ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ▐ Haiku 4.5 ▌ ▐ …/user/app ▌ ▐ main ▌ ▐ 0% / 200k ━━━━━━━━━━ ▌ ▐ 52% ━━━━━━━━━━ 18:05 ▌ ▐ +128 −34 ▌ ▐ $0.41 ▌ ▐ 10m 12s ▌ │ │ MODEL PATH BRANCH CONTEXT USAGE DIFF COST TIME │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ​
Worktree
​ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ▐ Opus 4.8 ▌ ▐ low ▌ ▐ …/.wt/feature ▌ ▐ worktree-feature ▌ ▐ 37% / 200k ━━━━━━━━━━ ▌ ▐ 44% ━━━━━━━━━━ 16:53 ▌ ▐ +128 −34 ▌ ▐ $0.41 ▌ ▐ 10m 12s ▌ │ │ MODEL EFFORT PATH BRANCH CONTEXT USAGE DIFF COST TIME │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ​
Non-git
​ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ▐ Opus 4.8 ▌ ▐ high ▌ ▐ …/user/scratch ▌ ▐ 22% / 200k ━━━━━━━━━━ ▌ ▐ +128 −34 ▌ ▐ $0.41 ▌ ▐ 10m 12s ▌ │ │ MODEL EFFORT PATH CONTEXT DIFF COST TIME │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ​

What it does

A rounded frame holds a row of colored pills for model, effort, path, git branch, context, five-hour usage, lines changed, cost and session time. A label row under the pills names each one.

Written in
Node.js
Network access
None
Works in
Terminal

How it works

  1. Reads model, effort, context, rate limits, cost and line counts from the stdin JSON.
  2. Gets the branch by reading .git/HEAD in the working folder instead of running git.
  3. Context and usage bars shift from green to red as they fill past 40, 65, 80 and 92%.
  4. Pills with no data drop out: effort on Haiku, branch outside a repo, usage without limits.

Good to know

  • Writes ~/.claude/statusline_config.yml on first run and keeps it; edit it to reorder, hide or recolor pills.
  • About 140 columns wide with every pill showing, over six rows counting a blank line above and below.
  • Needs a 24-bit color terminal. The default unicode style needs no Nerd Font, but style: nerd does.

Source

#!/usr/bin/env node
/**
 * Claude Code Status Line
 * Truecolor gradient pills · rounded frame · zero dependencies
 *
 * Reads Claude Code's session JSON on stdin and prints a status line to stdout.
 * Config (auto-created on first run): ~/.claude/statusline_config.yml
 * Source & docs: https://github.com/XyzElias/prismatic-claude-statusline
 */

const fs = require("fs");
const path = require("path");
const os = require("os");

// ─── Mini YAML parser (subset — enough for this config) ─────
function parseYaml(text) {
  const lines = text.split("\n");
  const root = {};
  const stack = [{ indent: -1, obj: root, isList: false }];

  for (let i = 0; i < lines.length; i++) {
    const raw = lines[i];
    const stripped = raw.replace(/#.*$/m, "").trimEnd();
    if (!stripped.trim()) continue;

    const indent = raw.search(/\S/);
    const content = stripped.trim();

    // Pop stack to matching indent
    while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
      stack.pop();
    }
    const parent = stack[stack.length - 1];

    // List item
    if (content.startsWith("- ")) {
      const itemStr = content.slice(2).trim();
      if (!Array.isArray(parent.obj)) {
        const prev = stack[stack.length - 1];
        if (!Array.isArray(prev.obj)) continue;
      }
      if (itemStr.includes(":")) {
        const itemObj = {};
        const { key, value } = parseKV(itemStr);
        itemObj[key] = value;
        // Peek ahead for more keys at deeper indent
        let j = i + 1;
        while (j < lines.length) {
          const nRaw = lines[j];
          const nStripped = nRaw.replace(/#.*$/m, "").trimEnd();
          if (!nStripped.trim()) { j++; continue; }
          const nIndent = nRaw.search(/\S/);
          const nContent = nStripped.trim();
          if (nIndent <= indent || nContent.startsWith("- ")) break;
          if (nContent.includes(":")) {
            const kv = parseKV(nContent);
            itemObj[kv.key] = kv.value;
          }
          j++;
        }
        i = j - 1;
        parent.obj.push(itemObj);
      } else {
        parent.obj.push(parseValue(itemStr));
      }
      continue;
    }

    // Key: value
    if (content.includes(":")) {
      const { key, value } = parseKV(content);
      if (value === null) {
        // Peek: is the next line a list item or a nested map?
        let nextI = i + 1;
        while (nextI < lines.length && !lines[nextI].replace(/#.*$/m, "").trim()) nextI++;
        if (nextI < lines.length) {
          const nextLine = lines[nextI].replace(/#.*$/m, "").trim();
          if (nextLine.startsWith("- ")) {
            const arr = [];
            parent.obj[key] = arr;
            stack.push({ indent, obj: arr, isList: true });
          } else {
            const child = {};
            parent.obj[key] = child;
            stack.push({ indent, obj: child, isList: false });
          }
        } else {
          parent.obj[key] = "";
        }
      } else {
        parent.obj[key] = value;
      }
    }
  }
  return root;
}

function parseKV(str) {
  const idx = str.indexOf(":");
  const key = str.slice(0, idx).trim();
  const rest = str.slice(idx + 1).trim();
  if (!rest) return { key, value: null };
  return { key, value: parseValue(rest) };
}

function parseValue(s) {
  if (!s) return "";
  if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'")))
    return s.slice(1, -1);
  // Flow array: [1, 2, 3] or [[1,2],[3,4]]
  if (s.startsWith("[")) {
    try { return JSON.parse(s); } catch {}
    try { return JSON.parse(s.replace(/([a-zA-Z_]\w*)/g, '"$1"')); } catch {}
    return s;
  }
  if (s === "true") return true;
  if (s === "false") return false;
  if (s === "null") return null;
  const n = Number(s);
  if (!isNaN(n) && s !== "") return n;
  return s;
}

// ─── Config ─────────────────────────────────────────────────
const HOME = os.homedir();
const CFG_PATH = path.join(HOME, ".claude", "statusline_config.yml");
const DEFAULT_CFG = `# Claude Code Status Line — Configuration
#
# Pill style: nerd | unicode | ascii
#   nerd    — Powerline glyphs for rounded pill caps (requires a Nerd Font)
#   unicode — Unicode block characters (works in any modern terminal)
#   ascii   — Plain [brackets] (maximum compatibility)
# See docs/nerd-fonts.md if you want the rounded "nerd" caps.

style: unicode

show_labels: true

pills:
  model:
    enabled: true
    label: MODEL
    order: 1

  # Reasoning effort of the current session. Auto-hides when the model does not
  # support the effort parameter. Colored per level (colors.effort_levels).
  effort:
    enabled: true
    label: EFFORT
    order: 2

  path:
    enabled: true
    label: PATH
    order: 3

  # Current Git branch of the working directory. Auto-hides when the directory
  # is not inside a Git repository.
  branch:
    enabled: true
    label: BRANCH
    order: 4

  context:
    enabled: true
    label: CONTEXT
    order: 5
    bar_width: 10

  # Subscription usage (Claude Pro/Max). Auto-hides on API/pay-per-use plans,
  # which do not receive rate-limit data from Claude Code.
  rate:
    enabled: true
    label: USAGE
    order: 6
    bar_width: 10
    show_seven_day: false   # also show the 7-day window next to the 5-hour one

  diff:
    enabled: true
    label: DIFF
    order: 7

  # Estimated cost of the CURRENT session (client-side estimate from Claude Code).
  cost:
    enabled: true
    label: COST
    order: 8

  time:
    enabled: true
    label: TIME
    order: 9

colors:
  pill_bg: [58, 62, 72]
  frame: [120, 120, 138]
  label: [120, 120, 138]
  dim: [200, 185, 175]
  path: [120, 200, 255]
  branch: [180, 160, 255]
  time: [130, 210, 255]
  diff_add: [60, 255, 110]
  diff_sep: [70, 95, 90]
  diff_rm: [255, 75, 75]
  cost_grad: [[255, 220, 60], [255, 170, 30]]

  # Two-color gradient per model, matched by model id/name (case-insensitive).
  model_themes:
    opus: [[175, 100, 255], [255, 120, 230]]
    sonnet: [[40, 230, 160], [120, 255, 180]]
    haiku: [[255, 140, 60], [255, 90, 140]]
    fable: [[64, 224, 208], [255, 198, 88]]
    default: [[200, 200, 230], [240, 240, 255]]

  # Per-level effort colors as [[startRGB], [endRGB]]. Repeat the same color
  # for a flat pill, or use two for a gradient.
  effort_levels:
    low:    [[238, 212, 141], [243, 225, 175]]
    medium: [[183, 189, 248], [205, 209, 250]]
    high:   [[120, 171, 244], [120, 171, 244]]
    xhigh:  [[180, 150, 240], [205, 175, 250]]
    max:    [[240, 140, 140], [240, 140, 140]]

  # Progress-bar / percentage colors by fill level (first match wins).
  thresholds:
    - below: 40
      color: [50, 255, 160]
    - below: 65
      color: [200, 255, 50]
    - below: 80
      color: [255, 210, 30]
    - below: 92
      color: [255, 120, 30]
    - below: 999
      color: [255, 50, 50]

frame:
  enabled: true
  top_left: "╭"
  top_right: "╮"
  bot_left: "╰"
  bot_right: "╯"
  horizontal: "─"
  vertical: "│"

# How many trailing directories of the working path to show.
path_segments: 2
`;

// Auto-create the default config on first run.
function ensureConfig() {
  if (!fs.existsSync(CFG_PATH)) {
    try { fs.writeFileSync(CFG_PATH, DEFAULT_CFG, "utf-8"); } catch {}
  }
}

// ─── Config migration ───────────────────────────────────────
// When the script gains a new pill or color, an EXISTING config file is missing
// the corresponding keys. Rather than overwrite the file (which would wipe a
// user's customizations and comments), we surgically insert only the missing
// blocks at the top of their section — every existing line is left untouched.
//
// `MIGRATIONS` lists each addition: the section header to find, the key that
// must exist there, and the YAML block to insert if it doesn't.
const MIGRATIONS = [
  {
    section: "pills",
    key: "branch",
    block:
      "  # Current Git branch of the working directory (added in v1.1).\n" +
      "  # Auto-hides when the directory is not inside a Git repository.\n" +
      "  branch:\n" +
      "    enabled: true\n" +
      "    label: BRANCH\n" +
      "    order: 3\n",
  },
  {
    section: "colors",
    key: "branch",
    block: "  branch: [180, 160, 255]\n",
  },
  {
    section: "pills",
    key: "effort",
    block:
      "  # Reasoning effort of the current session (added in v1.2). Auto-hides\n" +
      "  # when the model does not support the effort parameter. The fractional\n" +
      "  # order slots it between model and path without renumbering your pills.\n" +
      "  effort:\n" +
      "    enabled: true\n" +
      "    label: EFFORT\n" +
      "    order: 1.5\n",
  },
  {
    section: "colors",
    key: "effort_levels",
    block:
      "  # Per-level effort colors (added in v1.2) as [[startRGB], [endRGB]] —\n" +
      "  # repeat the same color for a flat pill, or use two for a gradient.\n" +
      "  effort_levels:\n" +
      "    low:    [[238, 212, 141], [243, 225, 175]]\n" +
      "    medium: [[183, 189, 248], [205, 209, 250]]\n" +
      "    high:   [[120, 171, 244], [120, 171, 244]]\n" +
      "    xhigh:  [[180, 150, 240], [205, 175, 250]]\n" +
      "    max:    [[240, 140, 140], [240, 140, 140]]\n",
  },
];

// Insert `block` as the first child of the top-level `section:` map, but only
// if a child named `key:` is not already present. Returns the (possibly
// unchanged) text. Pure text surgery — existing lines are never modified.
function insertIntoSection(text, section, key, block) {
  const lines = text.split("\n");
  const headerRe = new RegExp("^" + section + ":\\s*$");
  let h = lines.findIndex((l) => headerRe.test(l));
  if (h === -1) return text; // section absent — leave the file alone

  // Walk the section's direct children (indent > 0) until the next top-level key.
  const childKeyRe = new RegExp("^\\s+" + key + ":");
  for (let i = h + 1; i < lines.length; i++) {
    const line = lines[i];
    if (!line.trim()) continue;
    const indent = line.search(/\S/);
    if (indent === 0) break;       // reached the next top-level section
    if (childKeyRe.test(line)) return text; // key already present — nothing to do
  }

  lines.splice(h + 1, 0, block.replace(/\n$/, ""));
  return lines.join("\n");
}

// Bring an existing config file up to date with any new keys the script knows
// about, without disturbing the user's existing settings or comments.
function migrateConfig() {
  let raw;
  try { raw = fs.readFileSync(CFG_PATH, "utf-8"); } catch { return; }
  let next = raw;
  for (const m of MIGRATIONS) {
    next = insertIntoSection(next, m.section, m.key, m.block);
  }
  if (next !== raw) {
    try { fs.writeFileSync(CFG_PATH, next, "utf-8"); } catch {}
  }
}

ensureConfig();
migrateConfig();

let CFG = {};
try {
  CFG = parseYaml(fs.readFileSync(CFG_PATH, "utf-8"));
} catch {
  CFG = {};
}

function cfg(dotPath, defaultVal) {
  let node = CFG;
  for (const key of dotPath.split(".")) {
    if (node && typeof node === "object" && key in node) {
      node = node[key];
    } else {
      return defaultVal;
    }
  }
  return node != null ? node : defaultVal;
}

function col(dotPath, def) {
  const v = cfg(dotPath, def);
  return Array.isArray(v) ? v : def;
}

// ─── Read stdin (JSON from Claude Code) ─────────────────────
let input = "";
process.stdin.setEncoding("utf-8");
process.stdin.on("data", (chunk) => (input += chunk));
process.stdin.on("end", () => {
  let data;
  try {
    data = JSON.parse(input);
  } catch {
    process.exit(0);
  }
  render(data);
});

// ─── Style presets ──────────────────────────────────────────
const STYLE_PRESETS = {
  nerd:    { left: "", right: "" },
  unicode: { left: "▐", right: "▌" },
  ascii:   { left: "[",      right: "]" },
};

// ─── ANSI helpers ───────────────────────────────────────────
const RST = "\x1b[0m";
const BOLD = "\x1b[1m";
const ANSI_RE = /\x1b\[[^m]*m/g;

function fgC(r, g, b) { return `\x1b[38;2;${r};${g};${b}m`; }
function bgC(r, g, b) { return `\x1b[48;2;${r};${g};${b}m`; }
function stripAnsi(s) { return s.replace(ANSI_RE, ""); }
function visLen(s) { return stripAnsi(s).length; }

function lerp(a, b, t) { return Math.round(a + (b - a) * t); }

function gradient(text, start, end) {
  if (!text) return "";
  const n = Math.max(text.length - 1, 1);
  return [...text].map((ch, i) =>
    fgC(lerp(start[0], end[0], i / n),
        lerp(start[1], end[1], i / n),
        lerp(start[2], end[2], i / n)) + ch
  ).join("");
}

// ─── Git branch (zero-dependency, no subprocess) ────────────
// Walks up from `cwd` to find a Git directory and reads HEAD directly, so it
// adds no latency and works without `git` on PATH. Handles worktrees and
// submodules (where `.git` is a file pointing at the real git dir) and the
// detached-HEAD case (returns a short commit SHA). Returns null when the
// directory is not inside a repository.
function getGitBranch(cwd) {
  if (!cwd) return null;
  try {
    let dir = path.resolve(cwd);
    for (let i = 0; i < 64; i++) {
      const dotGit = path.join(dir, ".git");
      let gitDir = null;
      try {
        const st = fs.statSync(dotGit);
        if (st.isDirectory()) {
          gitDir = dotGit;
        } else if (st.isFile()) {
          // Worktree/submodule: ".git" is a file "gitdir: <path>".
          const m = fs.readFileSync(dotGit, "utf-8").match(/gitdir:\s*(.+)/);
          if (m) gitDir = path.resolve(dir, m[1].trim());
        }
      } catch {}

      if (gitDir) {
        const head = fs.readFileSync(path.join(gitDir, "HEAD"), "utf-8").trim();
        const ref = head.match(/^ref:\s*refs\/heads\/(.+)$/);
        if (ref) return ref[1];
        // Detached HEAD — show a short commit hash.
        return head.slice(0, 7);
      }

      const parent = path.dirname(dir);
      if (parent === dir) break; // reached filesystem root
      dir = parent;
    }
  } catch {}
  return null;
}

// ─── Render ─────────────────────────────────────────────────
function render(data) {
  const BG_PILL = col("colors.pill_bg", [58, 62, 72]);
  const FRAME_COLOR = fgC(...col("colors.frame", [120, 120, 138]));
  const LABEL_COLOR = fgC(...col("colors.label", [120, 120, 138]));
  const DIM_COLOR = fgC(...col("colors.dim", [200, 185, 175]));

  const style = cfg("style", "unicode");
  const preset = STYLE_PRESETS[style] || STYLE_PRESETS.unicode;
  const PL_LEFT = preset.left;
  const PL_RIGHT = preset.right;

  const thresholds = cfg("colors.thresholds", [
    { below: 40, color: [50, 255, 160] },
    { below: 65, color: [200, 255, 50] },
    { below: 80, color: [255, 210, 30] },
    { below: 92, color: [255, 120, 30] },
    { below: 999, color: [255, 50, 50] },
  ]);

  function thresholdColor(val) {
    for (const t of thresholds) {
      if (val < t.below) return t.color;
    }
    return thresholds[thresholds.length - 1].color;
  }

  const modelThemes = cfg("colors.model_themes", {
    opus: [[175, 100, 255], [255, 120, 230]],
    sonnet: [[40, 230, 160], [120, 255, 180]],
    haiku: [[255, 140, 60], [255, 90, 140]],
    fable: [[64, 224, 208], [255, 198, 88]],
    default: [[200, 200, 230], [240, 240, 255]],
  });

  // ── Data extraction ──────────────────────────────────────
  const modelObj = data.model || {};
  const modelName = modelObj.display_name || "?";
  const modelId = modelObj.id || "";

  const cwd = (data.workspace || {}).current_dir || data.cwd || "";
  const parts = cwd.replace(/\\/g, "/").replace(/\/$/, "").split("/");
  const nSeg = cfg("path_segments", 2);
  const pathStr = parts.length > nSeg
    ? "…/" + parts.slice(-nSeg).join("/")
    : parts.join("/");

  const gitBranch = getGitBranch(cwd);

  // Reasoning effort — present only when the current model supports it. Reflects
  // the live session value (including mid-session /effort changes). "ultra"/
  // ultracode is not a distinct level here; Claude Code reports it as "xhigh".
  const effortLevel = typeof (data.effort || {}).level === "string" ? data.effort.level : null;
  const effortLevels = cfg("colors.effort_levels", {
    low:    [[238, 212, 141], [243, 225, 175]],
    medium: [[183, 189, 248], [205, 209, 250]],
    high:   [[120, 171, 244], [120, 171, 244]],
    xhigh:  [[180, 150, 240], [205, 175, 250]],
    max:    [[240, 140, 140], [240, 140, 140]],
  });

  const ctx = data.context_window || {};
  const pct = Math.min(Math.round(ctx.used_percentage || 0), 100);
  const ctxSize = ctx.context_window_size || 0;
  const ctxFmt = ctxSize >= 1e6
    ? (ctxSize / 1e6).toFixed(1) + "M"
    : ctxSize >= 1000 ? Math.floor(ctxSize / 1000) + "k" : String(ctxSize);

  const costData = data.cost || {};
  const costUsd = costData.total_cost_usd || 0;
  const linesAdd = costData.total_lines_added || 0;
  const linesRm = costData.total_lines_removed || 0;

  const durTotal = Math.floor((costData.total_duration_ms || 0) / 1000);
  const durH = Math.floor(durTotal / 3600);
  const durM = Math.floor((durTotal % 3600) / 60);
  const durS = durTotal % 60;
  const duration = durH
    ? `${durH}h ${String(durM).padStart(2, "0")}m`
    : `${durM}m ${String(durS).padStart(2, "0")}s`;

  // ── Rate limits (Claude Pro/Max only; absent on API plans) ──
  function rateWindow(win) {
    if (!win) return null;
    const wPct = Math.round(win.used_percentage || 0);
    let resetStr = "";
    try {
      if (win.resets_at > 0) {
        const d = new Date(win.resets_at * 1000);
        resetStr = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
      }
    } catch {}
    return { pct: wPct, resetStr };
  }
  const rl = data.rate_limits || {};
  const w5 = rateWindow(rl.five_hour);
  const w7 = rateWindow(rl.seven_day);

  // ── Model theme (matched by id/name substring) ───────────
  const modelKey = (modelId || modelName).toLowerCase();
  let themeGrad = null;
  for (const [k, grad] of Object.entries(modelThemes)) {
    if (k !== "default" && modelKey.includes(k)) {
      themeGrad = [grad[0], grad[1]];
      break;
    }
  }
  if (!themeGrad) {
    const d = modelThemes.default || [[200, 200, 230], [240, 240, 255]];
    themeGrad = [d[0], d[1]];
  }

  // ── Progress bar ─────────────────────────────────────────
  function progressBar(pctVal, width, color) {
    const filled = Math.min(Math.floor(pctVal * width / 100), width);
    const track = BG_PILL.map(v => Math.min(255, v + 55));
    let out = "";
    for (let i = 0; i < width; i++) {
      const t = i / Math.max(width - 1, 1);
      const factors = [0.2, 0.12, 0.08];
      if (i < filled) {
        const cc = color.map((v, j) => Math.min(255, Math.round(v * (1 - t * factors[j]))));
        out += fgC(...cc) + "━";
      } else {
        out += fgC(...track) + "━";
      }
    }
    return out;
  }

  // ── Pill builder ─────────────────────────────────────────
  let pills = "";
  let labels = "";
  let first = true;

  function addPill(content, label) {
    const gap = first ? "" : " ";
    first = false;
    const b = BG_PILL;
    const pillStr = fgC(...b) + PL_LEFT + bgC(...b) + " "
      + content + RST + bgC(...b) + " " + RST + fgC(...b) + PL_RIGHT;
    pills += gap + pillStr;
    const pillW = visLen(pillStr);
    const centered = label ? label.padStart(Math.ceil((pillW + label.length) / 2)).padEnd(pillW) : " ".repeat(pillW);
    labels += gap + LABEL_COLOR + centered + RST;
  }

  function pillCfg(name) {
    const p = cfg(`pills.${name}`, { enabled: true, label: name.toUpperCase() });
    return (p && p.enabled !== false) ? p : null;
  }

  // ── Build segments ───────────────────────────────────────
  const segments = [];

  { // Model
    const p = pillCfg("model");
    if (p) segments.push({ order: p.order || 1, build: () => {
      addPill(BOLD + gradient(modelName, themeGrad[0], themeGrad[1]) + RST + bgC(...BG_PILL), p.label || "MODEL");
    }});
  }

  { // Reasoning effort (only when the model reports it)
    const p = pillCfg("effort");
    if (p && effortLevel) {
      const stops = effortLevels[effortLevel] || effortLevels.high || [[200, 200, 230], [240, 240, 255]];
      segments.push({ order: p.order || 2, build: () => {
        addPill(BOLD + gradient(effortLevel, stops[0], stops[1] || stops[0]) + RST + bgC(...BG_PILL), p.label || "EFFORT");
      }});
    }
  }

  { // Path
    const p = pillCfg("path");
    if (p) segments.push({ order: p.order || 2, build: () => {
      addPill(BOLD + fgC(...col("colors.path", [120, 200, 255])) + pathStr + RST + bgC(...BG_PILL), p.label || "PATH");
    }});
  }

  { // Git branch (only inside a repository)
    const p = pillCfg("branch");
    if (p && gitBranch) {
      segments.push({ order: p.order || 3, build: () => {
        addPill(BOLD + fgC(...col("colors.branch", [180, 160, 255])) + gitBranch + RST + bgC(...BG_PILL), p.label || "BRANCH");
      }});
    }
  }

  { // Context window
    const p = pillCfg("context");
    if (p) {
      const bw = p.bar_width || 10;
      const ctxClr = thresholdColor(pct);
      segments.push({ order: p.order || 4, build: () => {
        addPill(
          fgC(...ctxClr) + BOLD + `${pct}%` + RST + bgC(...BG_PILL)
          + DIM_COLOR + ` / ${ctxFmt}  `
          + progressBar(pct, bw, ctxClr) + RST + bgC(...BG_PILL),
          p.label || "CONTEXT"
        );
      }});
    }
  }

  { // Subscription usage (rate limits) — only when Claude Code provides them
    const p = pillCfg("rate");
    const showSeven = p && p.show_seven_day === true;
    if (p && (w5 || (showSeven && w7))) {
      const bw = p.bar_width || 10;
      segments.push({ order: p.order || 5, build: () => {
        let content = "";
        if (w5) {
          const c = thresholdColor(w5.pct);
          content += fgC(...c) + BOLD + `${w5.pct}%` + RST + bgC(...BG_PILL)
            + "  " + progressBar(w5.pct, bw, c) + RST + bgC(...BG_PILL)
            + (w5.resetStr ? DIM_COLOR + `  ${w5.resetStr}` : "");
        }
        if (showSeven && w7) {
          const c = thresholdColor(w7.pct);
          content += (w5 ? DIM_COLOR + "  ·  " : "")
            + fgC(...c) + BOLD + `7d ${w7.pct}%` + RST + bgC(...BG_PILL);
        }
        addPill(content, p.label || "USAGE");
      }});
    }
  }

  { // Diff
    const p = pillCfg("diff");
    if (p && (linesAdd > 0 || linesRm > 0)) {
      segments.push({ order: p.order || 6, build: () => {
        addPill(
          fgC(...col("colors.diff_add", [60, 255, 110])) + BOLD + `+${linesAdd}` + RST + bgC(...BG_PILL)
          + fgC(...col("colors.diff_sep", [70, 95, 90])) + "  "
          + fgC(...col("colors.diff_rm", [255, 75, 75])) + BOLD + `−${linesRm}` + RST + bgC(...BG_PILL),
          p.label || "DIFF"
        );
      }});
    }
  }

  { // Session cost
    const p = pillCfg("cost");
    if (p) {
      const cg = cfg("colors.cost_grad", [[255, 220, 60], [255, 170, 30]]);
      segments.push({ order: p.order || 7, build: () => {
        addPill(
          BOLD + gradient(`$${costUsd.toFixed(2)}`, cg[0], cg[1]) + RST + bgC(...BG_PILL),
          p.label || "COST"
        );
      }});
    }
  }

  { // Session duration
    const p = pillCfg("time");
    if (p) segments.push({ order: p.order || 8, build: () => {
      addPill(BOLD + fgC(...col("colors.time", [130, 210, 255])) + duration + RST + bgC(...BG_PILL), p.label || "TIME");
    }});
  }

  // ── Render (sorted by order) ─────────────────────────────
  segments.sort((a, b) => a.order - b.order);
  for (const seg of segments) seg.build();

  // ── Frame & output ───────────────────────────────────────
  const showLabels = cfg("show_labels", true);
  const w = visLen(pills);

  const fr = cfg("frame", {});
  if (fr.enabled !== false) {
    const h = fr.horizontal || "─";
    const v = fr.vertical || "│";
    const top = FRAME_COLOR + (fr.top_left || "╭") + h.repeat(w + 2) + (fr.top_right || "╮") + RST;
    const bot = FRAME_COLOR + (fr.bot_left || "╰") + h.repeat(w + 2) + (fr.bot_right || "╯") + RST;
    const row = FRAME_COLOR + v + RST + " " + pills + " " + FRAME_COLOR + v + RST;
    process.stdout.write("​\n");
    process.stdout.write(top + "\n");
    process.stdout.write(row + "\n");
    if (showLabels) {
      process.stdout.write(FRAME_COLOR + v + RST + " " + labels + " " + FRAME_COLOR + v + RST + "\n");
    }
    process.stdout.write(bot + "\n");
    process.stdout.write("​\n");
  } else {
    process.stdout.write("​\n");
    process.stdout.write(pills + "\n");
    if (showLabels) {
      process.stdout.write(labels + "\n");
    }
    process.stdout.write("​\n");
  }
}

More status lines

Opus 4.8 [high] ~/app main 16:20:55 | ⛁ ██░░░░░░░░ 22% | 5h ●◔○○○ 26% ↻2h7m | 7d ◔○○○○○○ 7% ↻2d1h │ $0.41 ⏱ 10m

Activity Feed

51 copies

Opus 4.8 high ✦ │ ctx 22% │ app (main) │ PR #1287 ● │ $0.41 · 10m +128 -34 Session ● ● ● ● ● ● ● ● ○ ○ ○ 74% left Resets in 2h 6m Weekly ● ● ● ● ● ● ● ● ● ● ○ 93% left Resets in 2d 0h Fable ● ● ● ● ● ● ● ● ● ○ ○ 85% left Resets in 2d 0h ◐ Read:index.ts ✓ Read×1 ✓ Bash×1 ✓ Edit×1 ✓ Grep×1 ◐ code-reviewer Review the change for regressions
Model: Opus 4.8 | Thinking: high | Context: [▰▰▱▱▱▱▱▱▱▱] 45k/200k (22%) | cwd: ~/app | ⎇ main | (+0,-0) Cost: $0.41 | Block: 2hr 6m | Weekly: 7.0% | Session: 10m | In: 916.7 t/s | Out: 29.2 t/s | Cached: 2k | Session: [▰▱▱▱▱] 26.0% Mem: 0.8G/1.0G | (*゚∀゚*) | Session ID: b8e1c0d2-4a6f-4e2a-9c1b-3f5d7a9e2c10

Pace Arrows

5 copies

Opus 4.8 (200K) high | app (main) ██░░░░░░░░ 22% 200K | 5h 26% ⇣32% 2h 7d 7% ⇣63% 2d
app · main · Opus 4.8 · high ctx 22% 44k · 5h 26% ↻2h6m · 7d 7% ↻2d0h · cost $0.41 · msg $0.757
📁 app 🌿 main 🧠 Opus 🧊 22% [==--------] 💰 $0.41 ($2.41/h) ⏳ 5h: 26% [==--------] 7d: 7% [----------]

Related mods

Terminal

❯ /deck demo ⎿ deck: Sample plan running above the prompt, with sample routing until real ones arrive. [-] 0: deck ▾ · 5 rows 22:11:41 ⇄ Model route opus = opus jev: haiku · not applied effort ▁▃·· medi = kept ✕ ✓ Orders module Done · 12/12 · 2/2 agents 100% ✕ ● Context 44k / 200k 22% ✕ ◔ Session · 5h 5h · 26% used ↻ 2:04:24 ✕ ◔ Weekly · 7d 7d · 7% used ↻ 2d 0h 57m ✕ ❯  ⏵⏵ auto mode on (shift+tab to cycle) · ← for agents [ Deck 1 ]

Claude Desktop

Deck in Claude Desktop

A cockpit for Claude Code: plan bars and subagent strips, context, usage limits with reset countdowns and burn rates, spend, the model route and a pet. Six bar styles, three collapsed looks. CLI and Desktop.

Terminal

BURN ▁ ▂ *▂▆ ▃ $0.41 = 0.04 burritos [-] 5h 26% in 2h 5m ▅▄█▅▅▆█▆███▅█ opus 4.8 44k in 1k out wk 7% in 2d 0h █████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ lifetime $0.41 hide ❯ 

Claude Desktop

Burn Meter in Claude Desktop

Live session cost odometer above the prompt: a burning fuse, real-world comparisons, threshold alerts

Terminal

❯ Run the tests again ● The repo is clean and the tests pass. ✻ Cooked for 0s · done 10:12 PM ☀ 44k │ 5h 26% │ 7d 7% │ cache 5% [-] ❯ 

Claude Desktop

Token Weather Usage in Claude Desktop

One line above the prompt: context tokens with their weather, a bar per recent prompt, your 5-hour and 7-day limits against the time elapsed, the prompt cache with its time left, the session cost with the last prompt's share in dollars and in points of the 5-hour limit, and the subagents running. Labels in English, French or Russian. After Token Weather (Anthropic, Apache-2.0) and the ideas of usage-meter (HolyGrail) and prompt-cache-control (Daniel San).

Terminal

❯ /flightdeck open ⎿ flightdeck: Flightdeck opened. Focus it with ctrl+x tab; 1-6 expand cards, f/s/o open the gate rows. ╭────────────────────────────────────────────────────────────────────────────────────────────────✕─╮ │ Opus 4.8 ○ idle · ctx ▰▱▱▱▱▱ 22% · $0.25 │ │ last turn 0s · 0 agents · 0 edits · 0 errors │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ❯  ⚠ flightdeck: ctx 22%

Claude Desktop

Flightdeck in Claude Desktop

Flightdeck: a live agent dashboard for Claude Code. Main model vitals, an on-call architect, every permission check, subagent cards and swimlanes, a turn receipt and a session log, all from real session events