claude-limitline 1.3.0 → 1.5.0

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.
Files changed (3) hide show
  1. package/README.md +10 -9
  2. package/dist/index.js +93 -83
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -123,7 +123,7 @@ Create a `claude-limitline.json` file in your Claude config directory (`~/.claud
123
123
  | `weekly.displayStyle` | `"bar"` or `"text"` | `"text"` |
124
124
  | `weekly.barWidth` | Width of progress bar in characters | `10` |
125
125
  | `weekly.showWeekProgress` | Show week progress percentage | `true` |
126
- | `weekly.viewMode` | `"simple"`, `"detailed"`, or `"smart"` | `"simple"` |
126
+ | `weekly.viewMode` | `"simple"` or `"smart"` | `"simple"` |
127
127
  | `budget.pollInterval` | Minutes between API calls | `15` |
128
128
  | `budget.warningThreshold` | Percentage to trigger warning color | `80` |
129
129
  | `theme` | Color theme name | `"dark"` |
@@ -132,15 +132,16 @@ Create a `claude-limitline.json` file in your Claude config directory (`~/.claud
132
132
 
133
133
  ### Weekly View Modes
134
134
 
135
- The weekly segment supports three view modes for displaying usage limits:
135
+ The weekly segment supports two view modes for displaying usage limits:
136
+
137
+ ![Mode Preview](imgs/mode-preview.png)
136
138
 
137
139
  | Mode | Description | Example |
138
140
  |------|-------------|---------|
139
141
  | `simple` | Shows overall weekly usage only (default) | `○ 47% (wk 85%)` |
140
- | `detailed` | Shows overall, Opus, and Sonnet usage side by side | `○47% ◈15% ◇7%` |
141
- | `smart` | Shows the most restrictive (bottleneck) limit with indicator | `○47%▲ (wk 85%)` |
142
+ | `smart` | Model-aware: shows Sonnet + Overall when using Sonnet | `◇7% \| ○47% (wk 85%)` |
142
143
 
143
- **Note:** Model-specific limits (Opus/Sonnet) are only available on certain subscription tiers. When a model-specific limit is not available, it will be hidden from the display.
144
+ **Note:** Model-specific limits (Opus/Sonnet) are only available on certain subscription tiers. Smart mode shows only overall usage when using Opus/Haiku, and shows both Sonnet and overall when using Sonnet.
144
145
 
145
146
  ### Available Themes
146
147
 
@@ -190,7 +191,7 @@ npm run dev # Watch mode
190
191
 
191
192
  ## Testing
192
193
 
193
- The project uses [Vitest](https://vitest.dev/) for testing with 166 tests covering config loading, themes, segments, utilities, and rendering.
194
+ The project uses [Vitest](https://vitest.dev/) for testing with 164 tests covering config loading, themes, segments, utilities, and rendering.
194
195
 
195
196
  ```bash
196
197
  npm test # Run tests once
@@ -205,13 +206,13 @@ npm run test:coverage # Coverage report
205
206
  | `src/config/loader.test.ts` | 7 | Config loading, merging, fallbacks |
206
207
  | `src/themes/index.test.ts` | 37 | Theme retrieval, color validation |
207
208
  | `src/segments/block.test.ts` | 8 | Block segment, time calculations |
208
- | `src/segments/weekly.test.ts` | 10 | Weekly segment, week progress |
209
- | `src/utils/oauth.test.ts` | 10 | API responses, caching |
209
+ | `src/segments/weekly.test.ts` | 13 | Weekly segment, week progress |
210
+ | `src/utils/oauth.test.ts` | 13 | API responses, caching, trends |
210
211
  | `src/utils/claude-hook.test.ts` | 21 | Model name formatting |
211
212
  | `src/utils/environment.test.ts` | 20 | Git branch, directory detection |
212
213
  | `src/utils/terminal.test.ts` | 13 | Terminal width, ANSI handling |
213
214
  | `src/utils/logger.test.ts` | 8 | Debug/error logging |
214
- | `src/renderer.test.ts` | 21 | Segment rendering, ordering |
215
+ | `src/renderer.test.ts` | 24 | Segment rendering, ordering, view modes |
215
216
 
216
217
  ## Debug Mode
217
218
 
package/dist/index.js CHANGED
@@ -44,6 +44,9 @@ var DEFAULT_CONFIG = {
44
44
  showWeekProgress: true,
45
45
  viewMode: "simple"
46
46
  },
47
+ context: {
48
+ enabled: true
49
+ },
47
50
  budget: {
48
51
  pollInterval: 15,
49
52
  warningThreshold: 80
@@ -535,6 +538,8 @@ var darkTheme = {
535
538
  // Purple for Opus
536
539
  sonnet: { bg: "#1a1a1a", fg: "#89ddff" },
537
540
  // Light blue for Sonnet
541
+ context: { bg: "#2a2a2a", fg: "#87ceeb" },
542
+ // Sky blue for context
538
543
  warning: { bg: "#d75f00", fg: "#ffffff" },
539
544
  critical: { bg: "#af0000", fg: "#ffffff" }
540
545
  };
@@ -548,6 +553,8 @@ var lightTheme = {
548
553
  // Purple for Opus
549
554
  sonnet: { bg: "#0ea5e9", fg: "#ffffff" },
550
555
  // Sky blue for Sonnet
556
+ context: { bg: "#6366f1", fg: "#ffffff" },
557
+ // Indigo for context
551
558
  warning: { bg: "#f59e0b", fg: "#000000" },
552
559
  critical: { bg: "#ef4444", fg: "#ffffff" }
553
560
  };
@@ -561,6 +568,8 @@ var nordTheme = {
561
568
  // Nord purple for Opus
562
569
  sonnet: { bg: "#2e3440", fg: "#88c0d0" },
563
570
  // Nord frost for Sonnet
571
+ context: { bg: "#3b4252", fg: "#81a1c1" },
572
+ // Nord frost for context
564
573
  warning: { bg: "#d08770", fg: "#2e3440" },
565
574
  critical: { bg: "#bf616a", fg: "#eceff4" }
566
575
  };
@@ -574,6 +583,8 @@ var gruvboxTheme = {
574
583
  // Gruvbox purple for Opus
575
584
  sonnet: { bg: "#282828", fg: "#8ec07c" },
576
585
  // Gruvbox aqua for Sonnet
586
+ context: { bg: "#3c3836", fg: "#83a598" },
587
+ // Gruvbox blue for context
577
588
  warning: { bg: "#d79921", fg: "#282828" },
578
589
  critical: { bg: "#cc241d", fg: "#ebdbb2" }
579
590
  };
@@ -587,6 +598,8 @@ var tokyoNightTheme = {
587
598
  // Tokyo purple for Opus
588
599
  sonnet: { bg: "#1a202c", fg: "#7dcfff" },
589
600
  // Tokyo cyan for Sonnet
601
+ context: { bg: "#2d3748", fg: "#7aa2f7" },
602
+ // Tokyo blue for context
590
603
  warning: { bg: "#e0af68", fg: "#1a1b26" },
591
604
  critical: { bg: "#f7768e", fg: "#1a1b26" }
592
605
  };
@@ -600,6 +613,8 @@ var rosePineTheme = {
600
613
  // Rose Pine iris for Opus
601
614
  sonnet: { bg: "#232136", fg: "#31748f" },
602
615
  // Rose Pine pine for Sonnet
616
+ context: { bg: "#2a273f", fg: "#9ccfd8" },
617
+ // Rose Pine foam for context
603
618
  warning: { bg: "#f6c177", fg: "#191724" },
604
619
  critical: { bg: "#eb6f92", fg: "#191724" }
605
620
  };
@@ -639,9 +654,12 @@ var Renderer = class {
639
654
  sonnet: symbolSet.sonnet_cost,
640
655
  bottleneck: symbolSet.bottleneck,
641
656
  rightArrow: symbolSet.right,
657
+ leftArrow: symbolSet.left,
642
658
  separator: symbolSet.separator,
643
659
  branch: symbolSet.branch,
644
660
  model: symbolSet.model,
661
+ context: "\u25D0",
662
+ // Half-filled circle for context
645
663
  progressFull: symbolSet.progress_full,
646
664
  progressEmpty: symbolSet.progress_empty,
647
665
  trendUp: "\u2191",
@@ -704,6 +722,18 @@ var Renderer = class {
704
722
  output += RESET_CODE;
705
723
  return output;
706
724
  }
725
+ renderRightPowerline(segments) {
726
+ if (segments.length === 0) return "";
727
+ let output = "";
728
+ for (let i = 0; i < segments.length; i++) {
729
+ const seg = segments[i];
730
+ output += RESET_CODE;
731
+ output += ansi.fg(seg.colors.bg) + this.symbols.leftArrow;
732
+ output += ansi.bg(seg.colors.bg) + ansi.fg(seg.colors.fg) + seg.text;
733
+ }
734
+ output += RESET_CODE;
735
+ return output;
736
+ }
707
737
  renderFallback(segments) {
708
738
  return segments.map((seg) => ansi.bg(seg.colors.bg) + ansi.fg(seg.colors.fg) + seg.text + RESET_CODE).join(` ${this.symbols.separator} `);
709
739
  }
@@ -806,94 +836,39 @@ var Renderer = class {
806
836
  colors: this.theme.weekly
807
837
  };
808
838
  }
809
- renderWeeklyDetailed(ctx) {
839
+ renderWeeklySmart(ctx) {
810
840
  const info = ctx.weeklyInfo;
811
841
  const overallIcon = this.usePowerline ? this.symbols.weekly : "All";
812
- const opusIcon = this.usePowerline ? this.symbols.opus : "Op";
813
842
  const sonnetIcon = this.usePowerline ? this.symbols.sonnet : "So";
814
- const parts = [];
815
- if (info.percentUsed !== null) {
816
- const trend = this.getTrendSymbol(ctx.trendInfo?.sevenDayTrend ?? null);
817
- parts.push(`${overallIcon}${Math.round(info.percentUsed)}%${trend}`);
818
- }
819
- if (info.opusPercentUsed !== null) {
820
- const trend = this.getTrendSymbol(ctx.trendInfo?.sevenDayOpusTrend ?? null);
821
- parts.push(`${opusIcon}${Math.round(info.opusPercentUsed)}%${trend}`);
822
- }
823
- if (info.sonnetPercentUsed !== null) {
824
- const trend = this.getTrendSymbol(ctx.trendInfo?.sevenDaySonnetTrend ?? null);
825
- parts.push(`${sonnetIcon}${Math.round(info.sonnetPercentUsed)}%${trend}`);
826
- }
827
- if (parts.length === 0) {
843
+ const showWeekProgress = this.config.weekly?.showWeekProgress ?? true;
844
+ const currentModel = ctx.envInfo.model?.toLowerCase() ?? "";
845
+ const isSonnet = currentModel.includes("sonnet");
846
+ if (isSonnet && info.sonnetPercentUsed !== null && info.percentUsed !== null) {
847
+ const sonnetTrend = this.getTrendSymbol(ctx.trendInfo?.sevenDaySonnetTrend ?? null);
848
+ const overallTrend = this.getTrendSymbol(ctx.trendInfo?.sevenDayTrend ?? null);
849
+ let text2 = `${sonnetIcon}${Math.round(info.sonnetPercentUsed)}%${sonnetTrend} | ${overallIcon}${Math.round(info.percentUsed)}%${overallTrend}`;
850
+ if (showWeekProgress && !ctx.compact) {
851
+ text2 += ` (wk ${info.weekProgressPercent}%)`;
852
+ }
853
+ const maxPercent = Math.max(info.sonnetPercentUsed, info.percentUsed);
854
+ const colors2 = this.getColorsForPercent(maxPercent, this.theme.weekly);
828
855
  return {
829
- text: ` ${overallIcon} -- `,
830
- colors: this.theme.weekly
856
+ text: ` ${text2} `,
857
+ colors: colors2
831
858
  };
832
859
  }
833
- const separator = ctx.compact ? " " : " ";
834
- const text = parts.join(separator);
835
- const maxPercent = Math.max(
836
- info.percentUsed ?? 0,
837
- info.opusPercentUsed ?? 0,
838
- info.sonnetPercentUsed ?? 0
839
- );
840
- const colors = this.getColorsForPercent(maxPercent, this.theme.weekly);
841
- return {
842
- text: ` ${text} `,
843
- colors
844
- };
845
- }
846
- renderWeeklySmart(ctx) {
847
- const info = ctx.weeklyInfo;
848
- const overallIcon = this.usePowerline ? this.symbols.weekly : "All";
849
- const opusIcon = this.usePowerline ? this.symbols.opus : "Op";
850
- const sonnetIcon = this.usePowerline ? this.symbols.sonnet : "So";
851
- const limits = [];
852
- if (info.percentUsed !== null) {
853
- limits.push({
854
- name: "all",
855
- icon: overallIcon,
856
- percent: info.percentUsed,
857
- trend: ctx.trendInfo?.sevenDayTrend ?? null,
858
- colors: this.theme.weekly
859
- });
860
- }
861
- if (info.opusPercentUsed !== null) {
862
- limits.push({
863
- name: "opus",
864
- icon: opusIcon,
865
- percent: info.opusPercentUsed,
866
- trend: ctx.trendInfo?.sevenDayOpusTrend ?? null,
867
- colors: this.theme.opus
868
- });
869
- }
870
- if (info.sonnetPercentUsed !== null) {
871
- limits.push({
872
- name: "sonnet",
873
- icon: sonnetIcon,
874
- percent: info.sonnetPercentUsed,
875
- trend: ctx.trendInfo?.sevenDaySonnetTrend ?? null,
876
- colors: this.theme.sonnet
877
- });
878
- }
879
- if (limits.length === 0) {
860
+ if (info.percentUsed === null) {
880
861
  return {
881
862
  text: ` ${overallIcon} -- `,
882
863
  colors: this.theme.weekly
883
864
  };
884
865
  }
885
- const bottleneck = limits.reduce((a, b) => a.percent >= b.percent ? a : b);
886
- const trend = this.getTrendSymbol(bottleneck.trend);
887
- const bottleneckIndicator = limits.length > 1 ? this.symbols.bottleneck : "";
888
- let text = `${bottleneck.icon}${Math.round(bottleneck.percent)}%${trend}`;
889
- if (bottleneckIndicator && !ctx.compact) {
890
- text += bottleneckIndicator;
891
- }
892
- const showWeekProgress = this.config.weekly?.showWeekProgress ?? true;
866
+ const trend = this.getTrendSymbol(ctx.trendInfo?.sevenDayTrend ?? null);
867
+ let text = `${overallIcon}${Math.round(info.percentUsed)}%${trend}`;
893
868
  if (showWeekProgress && !ctx.compact) {
894
869
  text += ` (wk ${info.weekProgressPercent}%)`;
895
870
  }
896
- const colors = this.getColorsForPercent(bottleneck.percent, bottleneck.colors);
871
+ const colors = this.getColorsForPercent(info.percentUsed, this.theme.weekly);
897
872
  return {
898
873
  text: ` ${text} `,
899
874
  colors
@@ -905,8 +880,6 @@ var Renderer = class {
905
880
  }
906
881
  const viewMode = this.config.weekly?.viewMode ?? "simple";
907
882
  switch (viewMode) {
908
- case "detailed":
909
- return this.renderWeeklyDetailed(ctx);
910
883
  case "smart":
911
884
  return this.renderWeeklySmart(ctx);
912
885
  case "simple":
@@ -914,6 +887,18 @@ var Renderer = class {
914
887
  return this.renderWeeklySimple(ctx);
915
888
  }
916
889
  }
890
+ renderContext(ctx) {
891
+ if (!this.config.context?.enabled) {
892
+ return null;
893
+ }
894
+ const percent = ctx.envInfo.contextPercent;
895
+ const icon = this.usePowerline ? this.symbols.context : "CTX";
896
+ const colors = this.getColorsForPercent(percent, this.theme.context);
897
+ return {
898
+ text: ` ${icon} ${percent}% `,
899
+ colors
900
+ };
901
+ }
917
902
  getSegment(name, ctx) {
918
903
  switch (name) {
919
904
  case "directory":
@@ -926,6 +911,8 @@ var Renderer = class {
926
911
  return this.renderBlock(ctx);
927
912
  case "weekly":
928
913
  return this.renderWeekly(ctx);
914
+ case "context":
915
+ return this.renderContext(ctx);
929
916
  default:
930
917
  return null;
931
918
  }
@@ -939,22 +926,35 @@ var Renderer = class {
939
926
  trendInfo,
940
927
  compact
941
928
  };
942
- const segments = [];
929
+ const leftSegments = [];
943
930
  const order = this.config.segmentOrder ?? ["directory", "git", "model", "block", "weekly"];
944
931
  for (const name of order) {
932
+ if (name === "context") continue;
945
933
  const segment = this.getSegment(name, ctx);
946
934
  if (segment) {
947
- segments.push(segment);
935
+ leftSegments.push(segment);
948
936
  }
949
937
  }
950
- if (segments.length === 0) {
951
- return "";
938
+ const rightSegments = [];
939
+ const contextSegment = this.renderContext(ctx);
940
+ if (contextSegment) {
941
+ rightSegments.push(contextSegment);
952
942
  }
943
+ let output = "";
953
944
  if (this.usePowerline) {
954
- return this.renderPowerline(segments);
945
+ if (leftSegments.length > 0) {
946
+ output += this.renderPowerline(leftSegments);
947
+ }
948
+ if (rightSegments.length > 0) {
949
+ output += this.renderRightPowerline(rightSegments);
950
+ }
955
951
  } else {
956
- return this.renderFallback(segments);
952
+ const allSegments = [...leftSegments, ...rightSegments];
953
+ if (allSegments.length > 0) {
954
+ output = this.renderFallback(allSegments);
955
+ }
957
956
  }
957
+ return output;
958
958
  }
959
959
  };
960
960
 
@@ -1080,12 +1080,22 @@ function getClaudeModel(hookData) {
1080
1080
  }
1081
1081
  return null;
1082
1082
  }
1083
+ function getContextPercent(hookData) {
1084
+ const ctx = hookData?.context_window;
1085
+ if (!ctx?.current_usage || !ctx.context_window_size) {
1086
+ return 0;
1087
+ }
1088
+ const usage = ctx.current_usage;
1089
+ const totalTokens = (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
1090
+ return Math.round(totalTokens / ctx.context_window_size * 100);
1091
+ }
1083
1092
  function getEnvironmentInfo(hookData) {
1084
1093
  return {
1085
1094
  directory: getDirectoryName(hookData),
1086
1095
  gitBranch: getGitBranch(),
1087
1096
  gitDirty: hasGitChanges(),
1088
- model: getClaudeModel(hookData)
1097
+ model: getClaudeModel(hookData),
1098
+ contextPercent: getContextPercent(hookData)
1089
1099
  };
1090
1100
  }
1091
1101
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-limitline",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "A statusline for Claude Code showing real-time usage limits and weekly tracking",
5
5
  "main": "dist/index.js",
6
6
  "bin": {