ccstatusline-usage 2.3.17 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,3 @@
1
- <div align="center">
2
-
3
1
  <pre>
4
2
  _ _ _ _
5
3
  ___ ___ ___| |_ __ _| |_ _ _ ___| (_)_ __ ___
@@ -36,6 +34,42 @@ This fork adds API-based usage widgets beyond the upstream:
36
34
  - **Context Window Display** - Visual bar showing context usage
37
35
  - **Off Peak** - Shows peak/off-peak status with countdown timer (peak hours drain sessions faster)
38
36
  - **Two-line Layout** - Session info on line 1, context on line 2
37
+ - **Multi-provider routing** - Usage widgets dispatch per model: Anthropic models hit the usage API; opencode/local models (GLM, Kimi, MiniMax, Qwen, Ollama) skip the fetch and gracefully hide usage bars while keeping the real-time context bar.
38
+
39
+ ### Multi-Provider Routing (Opencode / Local Models)
40
+
41
+ Claude Code can route individual prompts to non-Anthropic backends via opencode or a local Ollama runtime. The status line reads the `model.id` that Claude Code sends on stdin each render and dispatches through a per-provider resolver (`src/utils/usage/resolver.ts`):
42
+
43
+ - **Anthropic** (`opus`, `sonnet`, `haiku` in the id) — fetches Session/Weekly/Reset from the usage API.
44
+ - **Opencode** (`glm`, `kimi`, `minimax`, `mm-`, `qwen`, `owen`, `mimo`) — no usage API call; Session/Weekly/Reset widgets hide themselves. Context Bar still renders when `context_window` is in the payload.
45
+ - **Unknown model id** — same behavior as opencode (hides usage widgets).
46
+
47
+ This means opencode-routed turns don't trigger pointless Anthropic API calls or rate-limiting during heavy local usage.
48
+
49
+ Example — configure Claude Code to route a model through opencode (edit `~/.claude/settings.json`):
50
+
51
+ ```jsonc
52
+ {
53
+ "statusLine": {
54
+ "type": "command",
55
+ "command": "npx -y ccstatusline-usage@latest",
56
+ "padding": 0
57
+ },
58
+ "model": "glm-5.1" // or "kimi-k2.6", "minimax-m2.7", "qwen-3.6-plus", "qwen3.6:35b-a3b-q4_K_M" for local Ollama
59
+ }
60
+ ```
61
+
62
+ What the status line renders per model:
63
+
64
+ ```
65
+ # Anthropic (opus / sonnet / haiku)
66
+ Session: [████░░░░░░░░░░░] 27.0% | Weekly: [████░░░░░░░░░░░] 34.0% | 2:03 hr | Model: Opus 4.7
67
+ Context: [██████░░░░░░░░░] 389k/1M (39%) | Pace: [░░░░░░█|░░░░░░░] D4/7 -8% | Off-peak (4:03 hr)
68
+
69
+ # Opencode / local (glm-5.1, kimi, qwen, …)
70
+ Model: glm-5.1 | Off-peak (4:03 hr)
71
+ Context: [██░░░░░░░░░░░░░] 50k/200k (25%)
72
+ ```
39
73
 
40
74
  ### Enhanced Status Line Preview
41
75
 
@@ -46,7 +80,6 @@ Session: [████░░░░░░░░░░░] 27.0% | Weekly: [██
46
80
 
47
81
  ![Demo](https://raw.githubusercontent.com/sirmalloc/ccstatusline/main/screenshots/demo.gif)
48
82
 
49
- </div>
50
83
  <br />
51
84
 
52
85
  ## 📚 Table of Contents
@@ -67,6 +100,20 @@ Session: [████░░░░░░░░░░░] 27.0% | Weekly: [██
67
100
 
68
101
  ## 🆕 Recent Updates
69
102
 
103
+ ### [v2.4.1](https://github.com/pcvelz/ccstatusline-usage/releases/tag/v2.4.1) - Weekly Pace: showPercent toggle + decimal precision
104
+
105
+ - [pcvelz/ccstatusline-usage](https://github.com/pcvelz/ccstatusline-usage): **`%` keybind — always show delta on `On Pace`** — Previously `On Pace` was the only pace band that hid its delta. Toggle `showPercent` in the editor to render `D4/7: On Pace +3%` (or `-2%`, or `+0%`) so you can see how close to a band edge you are.
106
+ - [pcvelz/ccstatusline-usage](https://github.com/pcvelz/ccstatusline-usage): **`.` keybind — decimal precision** — Cycles 0 → 1 → 2 → 3 → 0 decimal places for all deltas (Warm/Cool/Overcooking/Underusing, `On Pace` when `showPercent` is on, and the pendulum bar). Replaces `Math.round()` with `toFixed(decimals)` in a shared `formatDelta` helper.
107
+ - Both toggles are metadata-gated and orthogonal — omitted keys behave identically to before.
108
+ - Thanks to @BenIsLegit ([#3](https://github.com/pcvelz/ccstatusline-usage/pull/3)).
109
+
110
+ ### [v2.4.0](https://github.com/pcvelz/ccstatusline-usage/releases/tag/v2.4.0) - Multi-provider router for usage widgets
111
+
112
+ - [pcvelz/ccstatusline-usage](https://github.com/pcvelz/ccstatusline-usage): **Provider pattern end-to-end** — Usage widgets now dispatch through `resolveProvider(modelId)` in `src/utils/usage/resolver.ts`. Anthropic models (`opus`/`sonnet`/`haiku`) fetch from the usage API; opencode/local models (`glm`, `kimi`, `minimax`, `mm-`, `qwen`, `owen`, `mimo`) skip the fetch entirely so heavy local-model sessions no longer trigger needless Anthropic API calls or rate-limiting.
113
+ - [pcvelz/ccstatusline-usage](https://github.com/pcvelz/ccstatusline-usage): The prefetch layer (`usage-prefetch.ts`) now reads `data.model.id` from the payload and dispatches via `provider.fetchUsage()` instead of the hardcoded Anthropic path. `opencodeProvider` / `nullProvider` return empty `UsageData`, so Session/Weekly/Reset widgets hide themselves naturally.
114
+ - [pcvelz/ccstatusline-usage](https://github.com/pcvelz/ccstatusline-usage): **Context Bar stays backend-agnostic** — renders whenever `context_window` is present in the payload, regardless of which provider handled the turn.
115
+ - See the new [Multi-Provider Routing](#multi-provider-routing-opencode--local-models) section under Fork Enhancements for example config.
116
+
70
117
  ### [v2.3.17](https://github.com/pcvelz/ccstatusline-usage/releases/tag/v2.3.17) - Local model fallback for API usage widgets
71
118
 
72
119
  - [pcvelz/ccstatusline-usage](https://github.com/pcvelz/ccstatusline-usage): **Local model fallback** — When the active model is not Opus/Sonnet/Haiku (e.g. a local Ollama model like `qwen3-coder:30b`), the Session, Weekly, and Context widgets now render empty-bar placeholders (`[░░░░░░░░░░░░░░░] -.0%`) and the Reset Timer shows `-:00 hr` instead of misleading Claude API values.
@@ -52556,7 +52556,7 @@ class OutputStyleWidget {
52556
52556
  }
52557
52557
 
52558
52558
  // src/utils/git.ts
52559
- import { execSync } from "child_process";
52559
+ import { execFileSync } from "child_process";
52560
52560
  function resolveGitCwd(context) {
52561
52561
  const candidates = [
52562
52562
  context.data?.cwd,
@@ -52571,13 +52571,18 @@ function resolveGitCwd(context) {
52571
52571
  return;
52572
52572
  }
52573
52573
  function runGit(command, context) {
52574
+ const args = command.trim().split(/\s+/).filter(Boolean);
52575
+ return runGitArgs(args, context, command);
52576
+ }
52577
+ function runGitArgs(args, context, cacheCommand) {
52574
52578
  const cwd2 = resolveGitCwd(context);
52575
- const cacheKey = `${command}|${cwd2 ?? ""}`;
52579
+ const cacheToken = cacheCommand ?? args.join("\x00");
52580
+ const cacheKey = `${cacheToken}|${cwd2 ?? ""}`;
52576
52581
  if (gitCommandCache.has(cacheKey)) {
52577
52582
  return gitCommandCache.get(cacheKey) ?? null;
52578
52583
  }
52579
52584
  try {
52580
- const output = execSync(`git ${command}`, {
52585
+ const output = execFileSync("git", args, {
52581
52586
  encoding: "utf8",
52582
52587
  stdio: ["pipe", "pipe", "ignore"],
52583
52588
  ...cwd2 ? { cwd: cwd2 } : {}
@@ -53136,7 +53141,7 @@ var init_GitRootDir = __esm(() => {
53136
53141
  });
53137
53142
 
53138
53143
  // src/utils/gh-pr-cache.ts
53139
- import { execFileSync } from "child_process";
53144
+ import { execFileSync as execFileSync2 } from "child_process";
53140
53145
  import {
53141
53146
  existsSync as existsSync2,
53142
53147
  mkdirSync,
@@ -53278,7 +53283,7 @@ function truncateTitle(title, maxWidth) {
53278
53283
  var PR_CACHE_TTL = 30000, GH_TIMEOUT = 5000, DEFAULT_TITLE_MAX_WIDTH = 30, DEFAULT_PR_CACHE_DEPS;
53279
53284
  var init_gh_pr_cache = __esm(() => {
53280
53285
  DEFAULT_PR_CACHE_DEPS = {
53281
- execFileSync,
53286
+ execFileSync: execFileSync2,
53282
53287
  existsSync: existsSync2,
53283
53288
  mkdirSync,
53284
53289
  readFileSync: readFileSync2,
@@ -53956,7 +53961,7 @@ function parseRemoteUrl(url2) {
53956
53961
  }
53957
53962
  }
53958
53963
  function getRemoteInfo(remoteName, context) {
53959
- const url2 = runGit(`remote get-url ${remoteName}`, context);
53964
+ const url2 = runGitArgs(["remote", "get-url", "--", remoteName], context, `remote get-url -- ${remoteName}`);
53960
53965
  if (!url2) {
53961
53966
  return null;
53962
53967
  }
@@ -55301,6 +55306,15 @@ function getContextConfig(modelIdentifier, contextWindowSize) {
55301
55306
  if (!modelIdentifier) {
55302
55307
  return defaultConfig;
55303
55308
  }
55309
+ const normalizedModel = modelIdentifier.toLowerCase().trim();
55310
+ for (const [modelName, contextSize] of Object.entries(OPENCODE_MODEL_CONTEXT_MAP)) {
55311
+ if (normalizedModel.includes(modelName)) {
55312
+ return {
55313
+ maxTokens: contextSize,
55314
+ usableTokens: Math.floor(contextSize * USABLE_CONTEXT_RATIO)
55315
+ };
55316
+ }
55317
+ }
55304
55318
  const inferredWindowSize = parseContextWindowSize(modelIdentifier);
55305
55319
  if (inferredWindowSize !== null) {
55306
55320
  return {
@@ -55310,7 +55324,24 @@ function getContextConfig(modelIdentifier, contextWindowSize) {
55310
55324
  }
55311
55325
  return defaultConfig;
55312
55326
  }
55313
- var DEFAULT_CONTEXT_WINDOW_SIZE = 200000, USABLE_CONTEXT_RATIO = 0.8;
55327
+ var DEFAULT_CONTEXT_WINDOW_SIZE = 200000, USABLE_CONTEXT_RATIO = 0.8, OPENCODE_MODEL_CONTEXT_MAP;
55328
+ var init_model_context = __esm(() => {
55329
+ OPENCODE_MODEL_CONTEXT_MAP = {
55330
+ "glm-5.1": 1e6,
55331
+ "glm-4.5": 1e6,
55332
+ "glm-4.0": 1e6,
55333
+ "mm-2.7": 1e6,
55334
+ "mm-2.5": 1e6,
55335
+ "kimi-k2.6": 1e6,
55336
+ "kimi-k2.5": 1e6,
55337
+ "owen-3.6": 1e6,
55338
+ "owen-3.5": 1e6,
55339
+ "qwen-2.5": 1e6,
55340
+ "qwen-2.0": 1e6,
55341
+ "qwen-1.5": 1e6,
55342
+ "qwen-1.0": 1e6
55343
+ };
55344
+ });
55314
55345
 
55315
55346
  // src/utils/context-percentage.ts
55316
55347
  function calculateContextPercentage(context) {
@@ -55325,10 +55356,12 @@ function calculateContextPercentage(context) {
55325
55356
  const contextConfig = getContextConfig(modelIdentifier, contextWindowMetrics.windowSize);
55326
55357
  return Math.min(100, context.tokenMetrics.contextLength / contextConfig.maxTokens * 100);
55327
55358
  }
55328
- var init_context_percentage = () => {};
55359
+ var init_context_percentage = __esm(() => {
55360
+ init_model_context();
55361
+ });
55329
55362
 
55330
55363
  // src/utils/terminal.ts
55331
- import { execSync as execSync2 } from "child_process";
55364
+ import { execSync } from "child_process";
55332
55365
  import * as fs2 from "fs";
55333
55366
  import * as path2 from "path";
55334
55367
  function getPackageVersion() {
@@ -55352,7 +55385,7 @@ function getPackageVersion() {
55352
55385
  function probeTerminalWidth() {
55353
55386
  if (process.env.TMUX) {
55354
55387
  try {
55355
- const output = execSync2("tmux display-message -p '#{pane_width}'", { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 2000 }).trim();
55388
+ const output = execSync("tmux display-message -p '#{pane_width}'", { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 2000 }).trim();
55356
55389
  const parsed = parseInt(output, 10);
55357
55390
  if (!isNaN(parsed) && parsed > 0)
55358
55391
  return parsed;
@@ -55378,7 +55411,7 @@ function probeTerminalWidth() {
55378
55411
  }
55379
55412
  }
55380
55413
  try {
55381
- const width = execSync2("tput cols 2>/dev/null", {
55414
+ const width = execSync("tput cols 2>/dev/null", {
55382
55415
  encoding: "utf8",
55383
55416
  stdio: ["pipe", "pipe", "ignore"]
55384
55417
  }).trim();
@@ -55395,7 +55428,7 @@ function parsePositiveInteger(value) {
55395
55428
  }
55396
55429
  function getParentProcessId(pid) {
55397
55430
  try {
55398
- const parentPidOutput = execSync2(`ps -o ppid= -p ${pid}`, {
55431
+ const parentPidOutput = execSync(`ps -o ppid= -p ${pid}`, {
55399
55432
  encoding: "utf8",
55400
55433
  stdio: ["pipe", "pipe", "ignore"],
55401
55434
  shell: "/bin/sh"
@@ -55407,7 +55440,7 @@ function getParentProcessId(pid) {
55407
55440
  }
55408
55441
  function getTTYForProcess(pid) {
55409
55442
  try {
55410
- const tty2 = execSync2(`ps -o tty= -p ${pid}`, {
55443
+ const tty2 = execSync(`ps -o tty= -p ${pid}`, {
55411
55444
  encoding: "utf8",
55412
55445
  stdio: ["pipe", "pipe", "ignore"],
55413
55446
  shell: "/bin/sh"
@@ -55422,7 +55455,7 @@ function getTTYForProcess(pid) {
55422
55455
  }
55423
55456
  function getWidthForTTY(tty2) {
55424
55457
  try {
55425
- const width = execSync2(`stty size < /dev/${tty2} | awk '{print $2}'`, {
55458
+ const width = execSync(`stty size < /dev/${tty2} | awk '{print $2}'`, {
55426
55459
  encoding: "utf8",
55427
55460
  stdio: ["pipe", "pipe", "ignore"],
55428
55461
  shell: "/bin/sh"
@@ -55438,7 +55471,7 @@ function getTerminalWidth() {
55438
55471
  function canDetectTerminalWidth() {
55439
55472
  return probeTerminalWidth() !== null;
55440
55473
  }
55441
- var __dirname = "/Users/peter/Documents/Code/ccstatusline-usage/src/utils", PACKAGE_VERSION = "2.3.17";
55474
+ var __dirname = "/Users/peter/Documents/Code/ccstatusline-usage/src/utils", PACKAGE_VERSION = "2.4.1";
55442
55475
  var init_terminal = () => {};
55443
55476
 
55444
55477
  // src/utils/renderer.ts
@@ -56311,6 +56344,7 @@ class ContextPercentageWidget {
56311
56344
  }
56312
56345
  }
56313
56346
  var init_ContextPercentage = __esm(() => {
56347
+ init_model_context();
56314
56348
  init_context_inverse();
56315
56349
  });
56316
56350
 
@@ -56371,6 +56405,7 @@ class ContextPercentageUsableWidget {
56371
56405
  }
56372
56406
  }
56373
56407
  var init_ContextPercentageUsable = __esm(() => {
56408
+ init_model_context();
56374
56409
  init_context_inverse();
56375
56410
  });
56376
56411
 
@@ -57026,7 +57061,7 @@ var init_CustomSymbol = __esm(async () => {
57026
57061
  });
57027
57062
 
57028
57063
  // src/widgets/CustomCommand.tsx
57029
- import { execSync as execSync3 } from "child_process";
57064
+ import { execSync as execSync2 } from "child_process";
57030
57065
 
57031
57066
  class CustomCommandWidget {
57032
57067
  getDefaultColor() {
@@ -57073,7 +57108,7 @@ class CustomCommandWidget {
57073
57108
  try {
57074
57109
  const timeout = item.timeout ?? 1000;
57075
57110
  const jsonInput = JSON.stringify(context.data);
57076
- let output = execSync3(item.commandPath, {
57111
+ let output = execSync2(item.commandPath, {
57077
57112
  encoding: "utf8",
57078
57113
  input: jsonInput,
57079
57114
  timeout,
@@ -58237,7 +58272,7 @@ var init_usage_types = __esm(() => {
58237
58272
  });
58238
58273
 
58239
58274
  // src/utils/usage-fetch.ts
58240
- import { execFileSync as execFileSync2 } from "child_process";
58275
+ import { execFileSync as execFileSync3 } from "child_process";
58241
58276
  import * as fs3 from "fs";
58242
58277
  import * as https from "https";
58243
58278
  import * as os4 from "os";
@@ -58383,7 +58418,7 @@ function parseMacKeychainCredentialCandidates(rawDump, servicePrefix = MACOS_USA
58383
58418
  }
58384
58419
  function readMacKeychainSecret(service) {
58385
58420
  try {
58386
- return execFileSync2("security", ["find-generic-password", "-s", service, "-w"], { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }).trim();
58421
+ return execFileSync3("security", ["find-generic-password", "-s", service, "-w"], { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }).trim();
58387
58422
  } catch {
58388
58423
  return null;
58389
58424
  }
@@ -58394,7 +58429,7 @@ function readUsageTokenFromMacKeychainService(service) {
58394
58429
  }
58395
58430
  function listMacKeychainCredentialCandidates() {
58396
58431
  try {
58397
- const rawDump = execFileSync2("security", ["dump-keychain"], {
58432
+ const rawDump = execFileSync3("security", ["dump-keychain"], {
58398
58433
  encoding: "utf8",
58399
58434
  maxBuffer: MACOS_SECURITY_DUMP_MAX_BUFFER,
58400
58435
  stdio: ["pipe", "pipe", "ignore"]
@@ -58640,7 +58675,8 @@ var init_usage_fetch = __esm(() => {
58640
58675
  extraUsageLimit: exports_external.number().nullable().optional(),
58641
58676
  extraUsageUsed: exports_external.number().nullable().optional(),
58642
58677
  extraUsageUtilization: exports_external.number().nullable().optional(),
58643
- error: exports_external.string().nullable().optional()
58678
+ error: exports_external.string().nullable().optional(),
58679
+ provider: exports_external.enum(["anthropic", "opencode"]).nullable().optional()
58644
58680
  });
58645
58681
  UsageApiResponseSchema = exports_external.object({
58646
58682
  five_hour: exports_external.object({
@@ -62796,7 +62832,7 @@ var init_TotalSpeed = __esm(async () => {
62796
62832
  });
62797
62833
 
62798
62834
  // src/widgets/FreeMemory.ts
62799
- import { execSync as execSync4 } from "child_process";
62835
+ import { execSync as execSync3 } from "child_process";
62800
62836
  import os7 from "os";
62801
62837
  function formatBytes(bytes) {
62802
62838
  const GB = 1024 ** 3;
@@ -62812,7 +62848,7 @@ function formatBytes(bytes) {
62812
62848
  }
62813
62849
  function getUsedMemoryMacOS() {
62814
62850
  try {
62815
- const output = execSync4("vm_stat", { encoding: "utf8" });
62851
+ const output = execSync3("vm_stat", { encoding: "utf8" });
62816
62852
  const lines = output.split(`
62817
62853
  `);
62818
62854
  const firstLine = lines[0];
@@ -62934,6 +62970,61 @@ class SessionNameWidget {
62934
62970
  }
62935
62971
  var init_SessionName = () => {};
62936
62972
 
62973
+ // src/utils/usage/providers/anthropic.ts
62974
+ var anthropicProvider;
62975
+ var init_anthropic = __esm(() => {
62976
+ init_usage_fetch();
62977
+ anthropicProvider = {
62978
+ name: "anthropic",
62979
+ async fetchUsage() {
62980
+ const data = await fetchUsageData();
62981
+ return { ...data, provider: "anthropic" };
62982
+ }
62983
+ };
62984
+ });
62985
+
62986
+ // src/utils/usage/providers/null.ts
62987
+ var nullProvider;
62988
+ var init_null = __esm(() => {
62989
+ nullProvider = {
62990
+ name: "null",
62991
+ fetchUsage() {
62992
+ return Promise.resolve({ provider: null });
62993
+ }
62994
+ };
62995
+ });
62996
+
62997
+ // src/utils/usage/providers/opencode.ts
62998
+ var opencodeProvider;
62999
+ var init_opencode = __esm(() => {
63000
+ opencodeProvider = {
63001
+ name: "opencode",
63002
+ fetchUsage() {
63003
+ return Promise.resolve({ provider: "opencode" });
63004
+ }
63005
+ };
63006
+ });
63007
+
63008
+ // src/utils/usage/resolver.ts
63009
+ function resolveProvider(modelId) {
63010
+ if (!modelId)
63011
+ return nullProvider;
63012
+ const id = modelId.toLowerCase();
63013
+ if (ANTHROPIC_KEYWORDS.some((k) => id.includes(k)))
63014
+ return anthropicProvider;
63015
+ if (OPENCODE_PATTERN.test(id))
63016
+ return opencodeProvider;
63017
+ return nullProvider;
63018
+ }
63019
+ var OPENCODE_PATTERN, ANTHROPIC_KEYWORDS;
63020
+ var init_resolver = __esm(() => {
63021
+ init_anthropic();
63022
+ init_null();
63023
+ init_opencode();
63024
+ OPENCODE_PATTERN = /(?:^|[^a-z])(glm|kimi|minimax|mm-|qwen|owen|mimo)/i;
63025
+ ANTHROPIC_KEYWORDS = ["opus", "sonnet", "haiku"];
63026
+ });
63027
+
62937
63028
  // src/widgets/ApiUsage.tsx
62938
63029
  function getDisplaySize(context) {
62939
63030
  const w = context.terminalWidth ?? 0;
@@ -62991,17 +63082,6 @@ function getModelId(context) {
62991
63082
  const model = context.data?.model;
62992
63083
  return (typeof model === "string" ? model : model?.id) ?? "";
62993
63084
  }
62994
- function isLocalModel(context) {
62995
- const modelId = getModelId(context);
62996
- if (modelId === "")
62997
- return false;
62998
- return !(modelId.includes("opus") || modelId.includes("sonnet") || modelId.includes("haiku"));
62999
- }
63000
- function renderLocalUsageFallback(label, shortLabel, size2) {
63001
- if (size2 === "mobile")
63002
- return `${shortLabel}: [░░░░] -.0%`;
63003
- return `${label}: [░░░░░░░░░░░░░░░] -.0%`;
63004
- }
63005
63085
 
63006
63086
  class SessionUsageWidget {
63007
63087
  getDefaultColor() {
@@ -63028,8 +63108,8 @@ class SessionUsageWidget {
63028
63108
  if (data.sessionUsage === undefined)
63029
63109
  return null;
63030
63110
  const size2 = getDisplaySize(context);
63031
- if (isLocalModel(context))
63032
- return renderLocalUsageFallback("Session", "S", size2);
63111
+ if (resolveProvider(getModelId(context)).name === "null")
63112
+ return null;
63033
63113
  const extraUsed = data.extraUsageUsed;
63034
63114
  const extraLimit = data.extraUsageLimit;
63035
63115
  if (size2 !== "mobile" && data.extraUsageEnabled === true && extraUsed !== undefined && extraLimit !== undefined && data.sessionUsage >= 100 && (data.weeklyUsage === undefined || data.weeklyUsage < 100)) {
@@ -63071,8 +63151,8 @@ class WeeklyUsageWidget {
63071
63151
  if (data.weeklyUsage === undefined)
63072
63152
  return null;
63073
63153
  const size2 = getDisplaySize(context);
63074
- if (isLocalModel(context))
63075
- return renderLocalUsageFallback("Weekly", "W", size2);
63154
+ if (resolveProvider(getModelId(context)).name === "null")
63155
+ return null;
63076
63156
  const extraUsed = data.extraUsageUsed;
63077
63157
  const extraLimit = data.extraUsageLimit;
63078
63158
  if (data.extraUsageEnabled === true && extraUsed !== undefined && extraLimit !== undefined && data.weeklyUsage >= 100) {
@@ -63111,8 +63191,8 @@ class ResetTimerWidget {
63111
63191
  const data = context.usageData ?? {};
63112
63192
  if (data.error)
63113
63193
  return getUsageErrorMessage(data.error);
63114
- if (isLocalModel(context))
63115
- return "-:00 hr";
63194
+ if (resolveProvider(getModelId(context)).name === "null")
63195
+ return null;
63116
63196
  const modelId = getModelId(context);
63117
63197
  const is1mModel = modelId.includes("[1m]");
63118
63198
  const isOpus = modelId.includes("opus");
@@ -63171,8 +63251,6 @@ class ContextBarWidget {
63171
63251
  const cw = context.data?.context_window;
63172
63252
  if (!cw)
63173
63253
  return null;
63174
- if (isLocalModel(context) && !getModelId(context).includes("qwen"))
63175
- return renderLocalUsageFallback("Context", "C", getDisplaySize(context));
63176
63254
  const total = Number(cw.context_window_size) || 200000;
63177
63255
  let used = 0;
63178
63256
  if (typeof cw.current_usage === "number") {
@@ -63202,6 +63280,7 @@ class ContextBarWidget {
63202
63280
  var DARK_RED_OPEN2 = "\x1B[38;2;204;0;0m", DARK_RED_CLOSE2 = "\x1B[39m", MOBILE_THRESHOLD = 134, MEDIUM_THRESHOLD2 = 178, MOBILE_BAR_WIDTH = 4, MEDIUM_BAR_WIDTH = 8, DEFAULT_BAR_WIDTH = 15;
63203
63281
  var init_ApiUsage = __esm(() => {
63204
63282
  init_usage();
63283
+ init_resolver();
63205
63284
  });
63206
63285
 
63207
63286
  // src/widgets/WeeklyResetTimer.ts
@@ -63775,11 +63854,11 @@ var init_ThinkingEffort = __esm(() => {
63775
63854
  });
63776
63855
 
63777
63856
  // src/widgets/Battery.ts
63778
- import { execSync as execSync5 } from "child_process";
63857
+ import { execSync as execSync4 } from "child_process";
63779
63858
  import { readFileSync as readFileSync11 } from "fs";
63780
63859
  function getMacBatteryInfo() {
63781
63860
  try {
63782
- const output = execSync5("pmset -g batt", { encoding: "utf-8", timeout: 2000 });
63861
+ const output = execSync4("pmset -g batt", { encoding: "utf-8", timeout: 2000 });
63783
63862
  const match = /(\d+)%;\s*(charging|discharging|charged|finishing charge|AC attached)/i.exec(output);
63784
63863
  const percentStr = match?.[1];
63785
63864
  const stateStr = match?.[2];
@@ -63976,18 +64055,28 @@ var init_VimMode = __esm(() => {
63976
64055
  function getPaceDisplayMode(item) {
63977
64056
  return item.metadata?.display === "pendulum" ? "pendulum" : "text";
63978
64057
  }
63979
- function computePace(actualPercent, expectedPercent) {
64058
+ function getDecimalPrecision(item) {
64059
+ const val = Number(item.metadata?.decimals);
64060
+ return val === 1 || val === 2 || val === 3 ? val : 0;
64061
+ }
64062
+ function formatDelta(delta, decimals) {
64063
+ return delta.toFixed(decimals);
64064
+ }
64065
+ function computePace(actualPercent, expectedPercent, showPercent = false, decimals = 0) {
63980
64066
  const delta = actualPercent - expectedPercent;
63981
64067
  const dayOfWeek = Math.max(1, Math.min(7, Math.ceil(expectedPercent * 7 / 100)));
63982
64068
  let status;
63983
64069
  if (delta > 15) {
63984
- status = `Overcooking +${Math.round(delta)}%`;
64070
+ status = `Overcooking +${formatDelta(delta, decimals)}%`;
63985
64071
  } else if (delta > 5) {
63986
- status = `Warm +${Math.round(delta)}%`;
64072
+ status = `Warm +${formatDelta(delta, decimals)}%`;
63987
64073
  } else if (delta < -15) {
63988
- status = `Underusing ${Math.round(delta)}%`;
64074
+ status = `Underusing ${formatDelta(delta, decimals)}%`;
63989
64075
  } else if (delta < -5) {
63990
- status = `Cool ${Math.round(delta)}%`;
64076
+ status = `Cool ${formatDelta(delta, decimals)}%`;
64077
+ } else if (showPercent) {
64078
+ const sign = delta >= 0 ? "+" : "";
64079
+ status = `On Pace ${sign}${formatDelta(delta, decimals)}%`;
63991
64080
  } else {
63992
64081
  status = "On Pace";
63993
64082
  }
@@ -64013,24 +64102,52 @@ class WeeklyPaceWidget {
64013
64102
  if (mode === "pendulum") {
64014
64103
  modifiers.push("pendulum bar");
64015
64104
  }
64105
+ if (item.metadata?.showPercent === "true") {
64106
+ modifiers.push("always %");
64107
+ }
64108
+ const decimals = getDecimalPrecision(item);
64109
+ if (decimals > 0) {
64110
+ modifiers.push(`.${"0".repeat(decimals)}`);
64111
+ }
64016
64112
  return {
64017
64113
  displayText: this.getDisplayName(),
64018
64114
  modifierText: makeModifierText(modifiers)
64019
64115
  };
64020
64116
  }
64021
64117
  handleEditorAction(action, item) {
64022
- if (action !== "toggle-pendulum") {
64023
- return null;
64118
+ if (action === "toggle-pendulum") {
64119
+ const currentMode = getPaceDisplayMode(item);
64120
+ const nextMode = currentMode === "text" ? "pendulum" : "text";
64121
+ return {
64122
+ ...item,
64123
+ metadata: {
64124
+ ...item.metadata ?? {},
64125
+ display: nextMode
64126
+ }
64127
+ };
64024
64128
  }
64025
- const currentMode = getPaceDisplayMode(item);
64026
- const nextMode = currentMode === "text" ? "pendulum" : "text";
64027
- return {
64028
- ...item,
64029
- metadata: {
64030
- ...item.metadata ?? {},
64031
- display: nextMode
64032
- }
64033
- };
64129
+ if (action === "toggle-show-percent") {
64130
+ const current = item.metadata?.showPercent === "true";
64131
+ return {
64132
+ ...item,
64133
+ metadata: {
64134
+ ...item.metadata ?? {},
64135
+ showPercent: current ? "false" : "true"
64136
+ }
64137
+ };
64138
+ }
64139
+ if (action === "cycle-decimals") {
64140
+ const current = getDecimalPrecision(item);
64141
+ const next = current >= 3 ? 0 : current + 1;
64142
+ return {
64143
+ ...item,
64144
+ metadata: {
64145
+ ...item.metadata ?? {},
64146
+ decimals: String(next)
64147
+ }
64148
+ };
64149
+ }
64150
+ return null;
64034
64151
  }
64035
64152
  render(item, context, settings) {
64036
64153
  const displayMode = getPaceDisplayMode(item);
@@ -64052,21 +64169,25 @@ class WeeklyPaceWidget {
64052
64169
  if (!window2)
64053
64170
  return null;
64054
64171
  const actualPercent = Math.max(0, Math.min(100, data.weeklyUsage));
64055
- const { delta, dayOfWeek, status } = computePace(actualPercent, window2.elapsedPercent);
64172
+ const showPercent = item.metadata?.showPercent === "true";
64173
+ const decimals = getDecimalPrecision(item);
64174
+ const { delta, dayOfWeek, status } = computePace(actualPercent, window2.elapsedPercent, showPercent, decimals);
64056
64175
  const width = context.terminalWidth ?? 0;
64057
64176
  const mobile = width > 0 && width < MOBILE_THRESHOLD2;
64058
64177
  const medium = width >= MOBILE_THRESHOLD2 && width < MEDIUM_THRESHOLD3;
64059
64178
  if (displayMode === "pendulum" && !mobile) {
64060
64179
  const halfWidth = medium ? 4 : 7;
64061
64180
  const sign = delta >= 0 ? "+" : "";
64062
- const barDisplay = `${makePendulumBar(delta, halfWidth)} D${dayOfWeek}/7 ${sign}${Math.round(delta)}%`;
64181
+ const barDisplay = `${makePendulumBar(delta, halfWidth)} D${dayOfWeek}/7 ${sign}${formatDelta(delta, decimals)}%`;
64063
64182
  return formatRawOrLabeledValue(item, "Pace: ", barDisplay);
64064
64183
  }
64065
64184
  return formatRawOrLabeledValue(item, "", `D${dayOfWeek}/7: ${status}`);
64066
64185
  }
64067
64186
  getCustomKeybinds() {
64068
64187
  return [
64069
- { key: "p", label: "(p)endulum toggle", action: "toggle-pendulum" }
64188
+ { key: "p", label: "(p)endulum toggle", action: "toggle-pendulum" },
64189
+ { key: "%", label: "(%) always show percent", action: "toggle-show-percent" },
64190
+ { key: ".", label: "(.) decimal precision", action: "cycle-decimals" }
64070
64191
  ];
64071
64192
  }
64072
64193
  supportsRawValue() {
@@ -64904,7 +65025,7 @@ var init_config = __esm(() => {
64904
65025
  });
64905
65026
 
64906
65027
  // src/utils/claude-settings.ts
64907
- import { execSync as execSync6 } from "child_process";
65028
+ import { execSync as execSync5 } from "child_process";
64908
65029
  import * as fs11 from "fs";
64909
65030
  import * as os9 from "os";
64910
65031
  import * as path9 from "path";
@@ -65013,7 +65134,7 @@ async function isInstalled() {
65013
65134
  function isBunxAvailable() {
65014
65135
  try {
65015
65136
  const command = process.platform === "win32" ? "where bunx" : "which bunx";
65016
- execSync6(command, { stdio: "ignore" });
65137
+ execSync5(command, { stdio: "ignore" });
65017
65138
  return true;
65018
65139
  } catch {
65019
65140
  return false;
@@ -65021,7 +65142,7 @@ function isBunxAvailable() {
65021
65142
  }
65022
65143
  function getClaudeCodeVersion() {
65023
65144
  try {
65024
- const output = execSync6("claude --version", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"], timeout: 5000 }).trim();
65145
+ const output = execSync5("claude --version", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"], timeout: 5000 }).trim();
65025
65146
  const match = /^(\d+\.\d+\.\d+)/.exec(output);
65026
65147
  return match?.[1] ?? null;
65027
65148
  } catch {
@@ -65832,7 +65953,7 @@ function openExternalUrl(url2) {
65832
65953
  }
65833
65954
 
65834
65955
  // src/utils/powerline.ts
65835
- import { execSync as execSync7 } from "child_process";
65956
+ import { execSync as execSync6 } from "child_process";
65836
65957
  import * as fs12 from "fs";
65837
65958
  import * as os11 from "os";
65838
65959
  import * as path10 from "path";
@@ -65966,7 +66087,7 @@ async function installPowerlineFonts() {
65966
66087
  if (fs12.existsSync(tempDir)) {
65967
66088
  fs12.rmSync(tempDir, { recursive: true, force: true });
65968
66089
  }
65969
- execSync7(`git clone --depth=1 https://github.com/powerline/fonts.git "${tempDir}"`, {
66090
+ execSync6(`git clone --depth=1 https://github.com/powerline/fonts.git "${tempDir}"`, {
65970
66091
  stdio: "pipe",
65971
66092
  encoding: "utf8"
65972
66093
  });
@@ -65974,14 +66095,14 @@ async function installPowerlineFonts() {
65974
66095
  const installScript = path10.join(tempDir, "install.sh");
65975
66096
  if (fs12.existsSync(installScript)) {
65976
66097
  fs12.chmodSync(installScript, 493);
65977
- execSync7(`cd "${tempDir}" && ./install.sh`, {
66098
+ execSync6(`cd "${tempDir}" && ./install.sh`, {
65978
66099
  stdio: "pipe",
65979
66100
  encoding: "utf8",
65980
66101
  shell: "/bin/bash"
65981
66102
  });
65982
66103
  if (platform4 === "linux") {
65983
66104
  try {
65984
- execSync7("fc-cache -f -v", {
66105
+ execSync6("fc-cache -f -v", {
65985
66106
  stdio: "pipe",
65986
66107
  encoding: "utf8"
65987
66108
  });
@@ -71250,56 +71371,7 @@ var StatusJSONSchema = exports_external.looseObject({
71250
71371
  // src/ccstatusline.ts
71251
71372
  init_ansi();
71252
71373
  init_colors();
71253
- init_config();
71254
- init_jsonl();
71255
- await init_renderer2();
71256
71374
 
71257
- // src/utils/skills.ts
71258
- import * as fs13 from "fs";
71259
- import * as os13 from "os";
71260
- import * as path11 from "path";
71261
- var EMPTY = { totalInvocations: 0, uniqueSkills: [], lastSkill: null };
71262
- function getSkillsDir() {
71263
- return path11.join(os13.homedir(), ".cache", "ccstatusline", "skills");
71264
- }
71265
- function getSkillsFilePath(sessionId) {
71266
- return path11.join(getSkillsDir(), `skills-${sessionId}.jsonl`);
71267
- }
71268
- function getSkillsMetrics(sessionId) {
71269
- const filePath = getSkillsFilePath(sessionId);
71270
- if (!fs13.existsSync(filePath)) {
71271
- return EMPTY;
71272
- }
71273
- try {
71274
- const invocations = fs13.readFileSync(filePath, "utf-8").trim().split(`
71275
- `).filter((line) => line.trim()).map((line) => {
71276
- try {
71277
- return JSON.parse(line);
71278
- } catch {
71279
- return null;
71280
- }
71281
- }).filter((e) => e !== null && typeof e.skill === "string" && typeof e.session_id === "string");
71282
- if (invocations.length === 0) {
71283
- return EMPTY;
71284
- }
71285
- const uniqueSkills = [];
71286
- const seenSkills = new Set;
71287
- for (let i = invocations.length - 1;i >= 0; i--) {
71288
- const skill = invocations[i]?.skill;
71289
- if (skill && !seenSkills.has(skill)) {
71290
- seenSkills.add(skill);
71291
- uniqueSkills.push(skill);
71292
- }
71293
- }
71294
- return {
71295
- totalInvocations: invocations.length,
71296
- uniqueSkills,
71297
- lastSkill: invocations[invocations.length - 1]?.skill ?? null
71298
- };
71299
- } catch {
71300
- return EMPTY;
71301
- }
71302
- }
71303
71375
  // src/utils/compact-renderer.ts
71304
71376
  init_source();
71305
71377
  init_ColorLevel();
@@ -71361,11 +71433,63 @@ function renderCompactOutput(preRenderedLines, settings, maxWidth) {
71361
71433
  }
71362
71434
  }
71363
71435
 
71436
+ // src/ccstatusline.ts
71437
+ init_config();
71438
+ init_jsonl();
71439
+ await init_renderer2();
71440
+
71441
+ // src/utils/skills.ts
71442
+ import * as fs13 from "fs";
71443
+ import * as os13 from "os";
71444
+ import * as path11 from "path";
71445
+ var EMPTY = { totalInvocations: 0, uniqueSkills: [], lastSkill: null };
71446
+ function getSkillsDir() {
71447
+ return path11.join(os13.homedir(), ".cache", "ccstatusline", "skills");
71448
+ }
71449
+ function getSkillsFilePath(sessionId) {
71450
+ return path11.join(getSkillsDir(), `skills-${sessionId}.jsonl`);
71451
+ }
71452
+ function getSkillsMetrics(sessionId) {
71453
+ const filePath = getSkillsFilePath(sessionId);
71454
+ if (!fs13.existsSync(filePath)) {
71455
+ return EMPTY;
71456
+ }
71457
+ try {
71458
+ const invocations = fs13.readFileSync(filePath, "utf-8").trim().split(`
71459
+ `).filter((line) => line.trim()).map((line) => {
71460
+ try {
71461
+ return JSON.parse(line);
71462
+ } catch {
71463
+ return null;
71464
+ }
71465
+ }).filter((e) => e !== null && typeof e.skill === "string" && typeof e.session_id === "string");
71466
+ if (invocations.length === 0) {
71467
+ return EMPTY;
71468
+ }
71469
+ const uniqueSkills = [];
71470
+ const seenSkills = new Set;
71471
+ for (let i = invocations.length - 1;i >= 0; i--) {
71472
+ const skill = invocations[i]?.skill;
71473
+ if (skill && !seenSkills.has(skill)) {
71474
+ seenSkills.add(skill);
71475
+ uniqueSkills.push(skill);
71476
+ }
71477
+ }
71478
+ return {
71479
+ totalInvocations: invocations.length,
71480
+ uniqueSkills,
71481
+ lastSkill: invocations[invocations.length - 1]?.skill ?? null
71482
+ };
71483
+ } catch {
71484
+ return EMPTY;
71485
+ }
71486
+ }
71487
+
71364
71488
  // src/ccstatusline.ts
71365
71489
  init_terminal();
71366
71490
 
71367
71491
  // src/utils/usage-prefetch.ts
71368
- init_usage();
71492
+ init_resolver();
71369
71493
  var USAGE_WIDGET_TYPES = new Set([
71370
71494
  "session-usage",
71371
71495
  "weekly-usage",
@@ -71406,17 +71530,20 @@ async function prefetchUsageDataIfNeeded(lines, data) {
71406
71530
  if (!hasUsageDependentWidgets(lines)) {
71407
71531
  return null;
71408
71532
  }
71533
+ const model = data?.model;
71534
+ const modelId = (typeof model === "string" ? model : model?.id) ?? "";
71535
+ const provider = resolveProvider(modelId);
71409
71536
  const rateLimitsData = extractUsageDataFromRateLimits(data?.rate_limits);
71410
71537
  if (hasCompleteRateLimitsUsageData(rateLimitsData)) {
71411
71538
  if (hasExtraUsageDependentWidgets(lines)) {
71412
- const apiData = await fetchUsageData();
71539
+ const apiData = await provider.fetchUsage();
71413
71540
  if (apiData.error === undefined) {
71414
71541
  return { ...rateLimitsData, extraUsageEnabled: apiData.extraUsageEnabled, extraUsageLimit: apiData.extraUsageLimit, extraUsageUsed: apiData.extraUsageUsed, extraUsageUtilization: apiData.extraUsageUtilization };
71415
71542
  }
71416
71543
  }
71417
71544
  return rateLimitsData;
71418
71545
  }
71419
- return fetchUsageData();
71546
+ return provider.fetchUsage();
71420
71547
  }
71421
71548
 
71422
71549
  // src/ccstatusline.ts
@@ -71460,8 +71587,8 @@ async function ensureWindowsUtf8CodePage() {
71460
71587
  return;
71461
71588
  }
71462
71589
  try {
71463
- const { execFileSync: execFileSync3 } = await import("child_process");
71464
- execFileSync3("chcp.com", ["65001"], { stdio: "ignore" });
71590
+ const { execFileSync: execFileSync4 } = await import("child_process");
71591
+ execFileSync4("chcp.com", ["65001"], { stdio: "ignore" });
71465
71592
  } catch {}
71466
71593
  }
71467
71594
  async function renderMultipleLines(data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccstatusline-usage",
3
- "version": "2.3.17",
3
+ "version": "2.4.1",
4
4
  "description": "A customizable status line formatter for Claude Code CLI",
5
5
  "module": "src/ccstatusline.ts",
6
6
  "type": "module",
@@ -30,8 +30,8 @@
30
30
  "chalk": "^5.5.0",
31
31
  "eslint": "^10.0.0",
32
32
  "eslint-import-resolver-typescript": "^4.4.4",
33
- "eslint-plugin-import": "^2.32.0",
34
33
  "eslint-plugin-import-newlines": "^2.0.0",
34
+ "eslint-plugin-import-x": "^4.16.2",
35
35
  "eslint-plugin-react": "^7.37.5",
36
36
  "eslint-plugin-react-hooks": "^7.0.1",
37
37
  "globals": "^17.3.0",