Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 134 additions & 1 deletion .claude/scripts/statusline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,133 @@ process.stdin.on("end", () => {
const total = j?.context_window?.context_window_size ?? 200000;
const used = Math.floor(total * pct / 100);
const cwd = j?.cwd ?? "~";

// --- Cross-session rate-limit harvest -------------------------------------
// rate_limits is ACCOUNT-WIDE (server-side rolling 5h / 7d windows), but
// Claude Code only refreshes it in THIS session JSON after an API response,
// so between prompts it is frozen. To show a live figure that also reflects
// other sessions, every session writes what it observes to a shared file and
// all sessions display the freshest value across them. Freshness rule per
// window: the later resets_at is the newer reading (windows advance over
// time); on a tie, the higher used_percentage is newer (usage accrues within
// a fixed window). ts is stamped only when a genuinely new reading wins, so
// idle 60s re-renders do not falsely refresh it.
const os = require("os");
const fs = require("fs");
const path = require("path");
const SHARED = (() => {
// Resolve a file that is the SAME physical file from Windows and WSL2 (so
// sessions in both environments aggregate together), and a normal per-home
// file on macOS / plain Linux / native Windows.
const envp = process.env.CLAUDE_USAGE_SHARED_FILE;
if (envp) return envp; // explicit override wins
const isWSL = process.platform === "linux" &&
(/microsoft|wsl/i.test(String(os.release())) || !!process.env.WSL_DISTRO_NAME);
if (isWSL) {
// Write to the Windows-side home so Windows and WSL share one file.
const pick = (home) => {
try { return fs.existsSync(path.join(home, ".claude")) ? path.join(home, ".claude", "rate-limits-shared.json") : undefined; }
catch (e) { return undefined; }
};
let user;
try { user = os.userInfo().username; } catch (e) { user = process.env.USER; }
const skip = ["Public", "Default", "Default User", "All Users", "desktop.ini"];
for (const drv of ["c", "C"]) {
if (user) { const r = pick(`/mnt/${drv}/Users/${user}`); if (r) return r; }
}
for (const drv of ["c", "C"]) {
let names = [];
try { names = fs.readdirSync(`/mnt/${drv}/Users`); } catch (e) {}
for (const n of names) {
if (skip.includes(n)) continue;
const r = pick(`/mnt/${drv}/Users/${n}`); if (r) return r;
}
}
// fall through to WSL-local home if no Windows home with .claude found
}
return path.join(os.homedir(), ".claude", "rate-limits-shared.json");
})();
const STALE_SECS = 300; // values not refreshed within this many seconds get a "*"
const nowSec = Math.floor(Date.now() / 1000);

let saved = {};
try { saved = JSON.parse(fs.readFileSync(SHARED, "utf8")) || {}; } catch { saved = {}; }

const mergeWin = (obs, sv) => {
let cur;
if (obs && typeof obs.used_percentage === "number") {
cur = {
used_percentage: obs.used_percentage,
resets_at: typeof obs.resets_at === "number" ? obs.resets_at : undefined,
};
}
// Expired entries are intentionally kept (not dropped) so a passed
// resets_at can display as "0*" until a fresh reading of the new window
// arrives; the later-resets_at rule below still supersedes it then.
if (!cur && !sv) return undefined;
if (cur && !sv) return { ...cur, ts: nowSec };
if (!cur && sv) return sv;
let curWins;
if (typeof cur.resets_at === "number" && typeof sv.resets_at === "number" && cur.resets_at !== sv.resets_at) {
curWins = cur.resets_at > sv.resets_at;
} else {
curWins = Math.round(cur.used_percentage) > Math.round(sv.used_percentage);
}
return curWins ? { ...cur, ts: nowSec } : sv;
};

const rl = j?.rate_limits;
const merged = {};
const fh = mergeWin(rl?.five_hour, saved.five_hour);
const sd = mergeWin(rl?.seven_day, saved.seven_day);
if (fh) merged.five_hour = fh;
if (sd) merged.seven_day = sd;

// Persist best-effort (temp + rename; readers tolerate a rare torn read).
try {
fs.mkdirSync(path.dirname(SHARED), { recursive: true });
const tmp = `${SHARED}.${process.pid}.tmp`;
fs.writeFileSync(tmp, JSON.stringify(merged));
try { fs.renameSync(tmp, SHARED); }
catch { fs.writeFileSync(SHARED, JSON.stringify(merged)); try { fs.unlinkSync(tmp); } catch (e) {} }
} catch (e) {}

// Each window reads "<countdown> <pct>%": the live countdown to reset is the
// leading label. 5h counts down in hours+minutes ("2h13m") and also shows its
// reset clock in local 24h ("(1900)"); 7d counts down in whole days until
// under a day, then hours ("5d", "15h"), with no clock. A resets_at that has
// already passed shows "0*".
const hhmm = (epoch) => {
const d = new Date(epoch * 1000);
return String(d.getHours()).padStart(2, "0") + String(d.getMinutes()).padStart(2, "0");
};
const cd5h = (secs) => {
const h = Math.floor(secs / 3600);
const m = Math.floor((secs % 3600) / 60);
return `${h}h${String(m).padStart(2, "0")}m`;
};
const cd7d = (secs) => (secs <= 86400 ? `${Math.ceil(secs / 3600)}h` : `${Math.ceil(secs / 86400)}d`);
const fmtWin = (e, cd, paren) => {
if (!e || typeof e.used_percentage !== "number") return undefined;
const pct = Math.round(e.used_percentage);
const stale = typeof e.ts === "number" && nowSec - e.ts > STALE_SECS;
if (typeof e.resets_at !== "number") return `${pct}%${stale ? "*" : ""}`;
const rem = e.resets_at - nowSec;
if (rem <= 0) return `0* ${pct}%`;
const tail = paren ? ` (${paren(e.resets_at)})` : "";
return `${cd(rem)} ${pct}%${stale ? "*" : ""}${tail}`;
};
const rate = [
fmtWin(merged.five_hour, cd5h, hhmm),
fmtWin(merged.seven_day, cd7d, undefined),
].filter(Boolean).join(" | ");

console.log(model);
console.log(pct);
console.log(total);
console.log(used);
console.log(cwd);
console.log(rate);
});
' <<< "$input")

Expand All @@ -31,6 +153,7 @@ process.stdin.on("end", () => {
IFS= read -r TOTAL
IFS= read -r USED
IFS= read -r CWD
IFS= read -r RATE
} <<< "$node_output"

# Default values for robustness when node output is empty (node absent)
Expand Down Expand Up @@ -58,4 +181,14 @@ fmt_tokens() {
fi
}

printf "%s | %s%% ctx | %s/%s tokens | %s%s" "$MODEL" "$PCT" "$(fmt_tokens "$USED")" "$(fmt_tokens "$TOTAL")" "$DIR" "$GIT"
# Append rate-limit usage to the right of the branch, when available. True
# right-alignment isn't possible: Claude Code doesn't pass the terminal width
# to the status line, and stdout is a pipe (no tty) so $COLUMNS / tput cols are
# unreliable.
if [ -n "$RATE" ]; then
RATE_SEG=" | $RATE"
else
RATE_SEG=""
fi

printf "%s | %s%% ctx | %s/%s tokens | %s%s%s" "$MODEL" "$PCT" "$(fmt_tokens "$USED")" "$(fmt_tokens "$TOTAL")" "$DIR" "$GIT" "$RATE_SEG"
3 changes: 2 additions & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"statusLine": {
"type": "command",
"command": ".claude/scripts/statusline.sh",
"padding": 2
"padding": 2,
"refreshInterval": 60
},

"permissions": {
Expand Down
Loading