@ogulcancelik/pi-minimal-footer 0.1.4 → 0.1.6

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 (2) hide show
  1. package/index.ts +112 -63
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -669,10 +669,54 @@ export default function (pi: ExtensionAPI) {
669
669
  return `${tokens}`;
670
670
  }
671
671
 
672
- function renderContextGauge(percentage: number, theme: any, used?: number, total?: number): string {
672
+ function fitFooterSegment(width: number, variants: string[]): string {
673
+ const safeWidth = Math.max(1, width);
674
+
675
+ for (const variant of variants) {
676
+ if (visibleWidth(variant) <= safeWidth) return variant;
677
+ }
678
+
679
+ return truncateToWidth(variants[variants.length - 1] || "", safeWidth);
680
+ }
681
+
682
+ function wrapFooterSegments(segments: string[], width: number, sep: string): string[] {
683
+ const safeWidth = Math.max(1, width);
684
+ const lines: string[] = [];
685
+ let current = "";
686
+
687
+ for (const segment of segments.filter(Boolean)) {
688
+ const fitted = truncateToWidth(segment, safeWidth);
689
+
690
+ if (!current) {
691
+ current = fitted;
692
+ continue;
693
+ }
694
+
695
+ const candidate = current + sep + fitted;
696
+ if (visibleWidth(candidate) <= safeWidth) {
697
+ current = candidate;
698
+ continue;
699
+ }
700
+
701
+ lines.push(truncateToWidth(current, safeWidth));
702
+ current = fitted;
703
+ }
704
+
705
+ if (current) lines.push(truncateToWidth(current, safeWidth));
706
+ return lines;
707
+ }
708
+
709
+ function renderContextGauge(
710
+ percentage: number,
711
+ theme: any,
712
+ used?: number,
713
+ total?: number,
714
+ options?: { barWidth?: number; includeCounts?: boolean }
715
+ ): string {
716
+ const barWidth = Math.max(4, options?.barWidth ?? CTX_GAUGE_WIDTH);
673
717
  const clamped = Math.max(0, Math.min(100, percentage));
674
- const filled = Math.round((clamped / 100) * CTX_GAUGE_WIDTH);
675
- const empty = CTX_GAUGE_WIDTH - filled;
718
+ const filled = Math.round((clamped / 100) * barWidth);
719
+ const empty = barWidth - filled;
676
720
 
677
721
  let color: string;
678
722
  if (clamped >= 90) color = "error";
@@ -682,7 +726,10 @@ export default function (pi: ExtensionAPI) {
682
726
 
683
727
  const bar = theme.fg(color, BAR_FILLED.repeat(filled)) + theme.fg("dim", BAR_EMPTY.repeat(empty));
684
728
  const pct = `${Math.round(clamped)}%`;
685
- const counts = used !== undefined && total ? ` ${formatTokenCount(used)}/${formatTokenCount(total)}` : "";
729
+ const counts =
730
+ options?.includeCounts === false || used === undefined || !total
731
+ ? ""
732
+ : ` ${formatTokenCount(used)}/${formatTokenCount(total)}`;
686
733
 
687
734
  return theme.fg("dim", "ctx ") + bar + " " + theme.fg("dim", pct + counts);
688
735
  }
@@ -700,38 +747,38 @@ export default function (pi: ExtensionAPI) {
700
747
  return theme.fg(color, BAR_FILLED.repeat(filled)) + theme.fg("dim", BAR_EMPTY.repeat(empty));
701
748
  }
702
749
 
750
+ function renderUsageWindow(
751
+ window: RateWindow,
752
+ theme: any,
753
+ options?: { barWidth?: number; includeReset?: boolean }
754
+ ): string {
755
+ const dim = (s: string) => theme.fg("dim", s);
756
+ const bar = renderUsageBar(window.usedPercent, Math.max(4, options?.barWidth ?? 10), theme);
757
+ const pct = dim(`${Math.round(window.usedPercent)}%`);
758
+ const timeStr = options?.includeReset === false || !window.resetsIn ? "" : " " + dim(window.resetsIn);
759
+ return `${dim(window.label)} ${bar} ${pct}${timeStr}`;
760
+ }
761
+
703
762
  function renderUsageLine(usage: UsageSnapshot, width: number, theme: any): string[] {
704
763
  if (!usage.windows.length) return [];
705
764
 
706
765
  const dim = (s: string) => theme.fg("dim", s);
707
766
  const sep = " " + dim(">") + " ";
708
- const USAGE_BAR_WIDTH = 10;
709
- const NARROW_THRESHOLD = 60;
710
-
711
- if (width < NARROW_THRESHOLD) {
712
- // Narrow: one line per window
713
- const lines: string[] = [];
714
- for (const w of usage.windows) {
715
- const bar = renderUsageBar(w.usedPercent, USAGE_BAR_WIDTH, theme);
716
- const pct = dim(`${Math.round(w.usedPercent)}%`);
717
- const timeStr = w.resetsIn ? " " + dim(w.resetsIn) : "";
718
- lines.push(`${dim(w.label)} ${bar} ${pct}${timeStr}`);
719
- }
720
- return lines;
721
- }
722
-
723
- // Wide: single line with > separators, fixed-width bars
724
- // Format: "Codex > 5h ━━━━━━━━━━ 46% 2h38m > 7d ━━━━━━━━━━ 46% 2d1h"
725
- const parts: string[] = [theme.fg("accent", usage.provider)];
767
+ const segments: string[] = [theme.fg("accent", usage.provider)];
726
768
 
727
769
  for (const w of usage.windows) {
728
- const bar = renderUsageBar(w.usedPercent, USAGE_BAR_WIDTH, theme);
729
- const pct = dim(`${Math.round(w.usedPercent)}%`);
730
- const timeStr = w.resetsIn ? " " + dim(w.resetsIn) : "";
731
- parts.push(`${dim(w.label)} ${bar} ${pct}${timeStr}`);
732
- }
733
-
734
- return [parts.join(sep)];
770
+ segments.push(
771
+ fitFooterSegment(width, [
772
+ renderUsageWindow(w, theme, { barWidth: 10, includeReset: true }),
773
+ renderUsageWindow(w, theme, { barWidth: 8, includeReset: true }),
774
+ renderUsageWindow(w, theme, { barWidth: 8, includeReset: false }),
775
+ renderUsageWindow(w, theme, { barWidth: 6, includeReset: false }),
776
+ renderUsageWindow(w, theme, { barWidth: 4, includeReset: false }),
777
+ ])
778
+ );
779
+ }
780
+
781
+ return wrapFooterSegments(segments, width, sep);
735
782
  }
736
783
 
737
784
  function getThinkingLevel(ctx: any): string {
@@ -754,11 +801,10 @@ export default function (pi: ExtensionAPI) {
754
801
  const lastAssistant = messages
755
802
  .slice()
756
803
  .reverse()
757
- .find((m: any) => m.role === "assistant" && m.stopReason !== "aborted");
758
-
759
- if (!lastAssistant?.usage) return { percentage: 0, used: 0, total: contextWindow };
804
+ .find((m: any) => m.role === "assistant" && m.stopReason !== "aborted") as any;
760
805
 
761
- const usage = lastAssistant.usage;
806
+ const usage = lastAssistant?.usage;
807
+ if (!usage) return { percentage: 0, used: 0, total: contextWindow };
762
808
  const contextTokens = (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
763
809
 
764
810
  return { percentage: (contextTokens / contextWindow) * 100, used: contextTokens, total: contextWindow };
@@ -884,7 +930,8 @@ export default function (pi: ExtensionAPI) {
884
930
 
885
931
  // Model + thinking
886
932
  const modelName = ctx.model?.id?.split("/").pop() || "no-model";
887
- let modelStr = theme.fg("muted", modelName);
933
+ const plainModelStr = theme.fg("muted", modelName);
934
+ let modelStr = plainModelStr;
888
935
  if (ctx.model?.reasoning) {
889
936
  const thinkingLevel = getThinkingLevel(ctx);
890
937
  if (thinkingLevel !== "off") {
@@ -892,41 +939,43 @@ export default function (pi: ExtensionAPI) {
892
939
  }
893
940
  }
894
941
 
895
- // Context gauge
896
- const gauge = renderContextGauge(percentage, theme, ctxUsed, ctxTotal);
897
-
898
942
  const sep = " " + theme.fg("dim", ">") + " ";
899
- const NARROW_THRESHOLD = 60;
900
-
901
943
  const lines: string[] = [];
902
-
903
- if (width < NARROW_THRESHOLD) {
904
- // Narrow: stack vertically
905
- const pwdColored = theme.fg("accent", pwd);
906
- if (branchStr) {
907
- lines.push(truncateToWidth(pwdColored + sep + branchStr, width));
908
- } else {
909
- lines.push(truncateToWidth(pwdColored, width));
910
- }
911
- lines.push(truncateToWidth(modelStr, width));
912
- lines.push(truncateToWidth(gauge, width));
913
- } else {
914
- // Wide: single line
915
- const pwdColored = theme.fg("accent", pwd);
916
- const branchPart = branchStr ? sep + branchStr : "";
917
- const modelPart = sep + modelStr;
918
- const gaugePart = sep + gauge;
919
- lines.push(truncateToWidth(pwdColored + branchPart + modelPart + gaugePart, width));
920
- }
944
+ const pwdStr = theme.fg("accent", pwd);
945
+ const statusBlocks = [
946
+ fitFooterSegment(width, branchStr ? [pwdStr + sep + branchStr, pwdStr] : [pwdStr]),
947
+ fitFooterSegment(width, modelStr === plainModelStr ? [plainModelStr] : [modelStr, plainModelStr]),
948
+ fitFooterSegment(width, [
949
+ renderContextGauge(percentage, theme, ctxUsed, ctxTotal, {
950
+ barWidth: CTX_GAUGE_WIDTH,
951
+ includeCounts: true,
952
+ }),
953
+ renderContextGauge(percentage, theme, ctxUsed, ctxTotal, {
954
+ barWidth: 10,
955
+ includeCounts: false,
956
+ }),
957
+ renderContextGauge(percentage, theme, ctxUsed, ctxTotal, {
958
+ barWidth: 8,
959
+ includeCounts: false,
960
+ }),
961
+ renderContextGauge(percentage, theme, ctxUsed, ctxTotal, {
962
+ barWidth: 6,
963
+ includeCounts: false,
964
+ }),
965
+ renderContextGauge(percentage, theme, ctxUsed, ctxTotal, {
966
+ barWidth: 4,
967
+ includeCounts: false,
968
+ }),
969
+ ]),
970
+ ];
971
+
972
+ lines.push(...wrapFooterSegments(statusBlocks, width, sep));
921
973
 
922
974
  if (latestUsage && latestUsage.windows.length > 0) {
923
- const usageLines = renderUsageLine(latestUsage, width, theme);
924
- for (const line of usageLines) {
925
- lines.push(truncateToWidth(line, width));
926
- }
975
+ lines.push(...renderUsageLine(latestUsage, width, theme));
927
976
  }
928
977
 
929
- return lines;
978
+ return lines.map((line) => truncateToWidth(line, width));
930
979
  },
931
980
  };
932
981
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogulcancelik/pi-minimal-footer",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Minimal footer for pi with context gauge and subscription usage bars",
5
5
  "type": "module",
6
6
  "keywords": [