@marckrenn/pi-sub-bar 1.0.1 → 1.0.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @marckrenn/pi-sub-bar
2
2
 
3
+ ## 1.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#3](https://github.com/marckrenn/pi-sub/pull/3) [`4ceb5ad`](https://github.com/marckrenn/pi-sub/commit/4ceb5ad133166237652d197ba9296ad1589a813c) Thanks [@marckrenn](https://github.com/marckrenn)! - Bundle sub-core with sub-bar, refresh Antigravity quotas + settings, and update UI copy/controls.
8
+
9
+ - Updated dependencies [[`4ceb5ad`](https://github.com/marckrenn/pi-sub/commit/4ceb5ad133166237652d197ba9296ad1589a813c)]:
10
+ - @marckrenn/pi-sub-core@1.0.2
11
+ - @marckrenn/pi-sub-shared@1.0.2
12
+
13
+ ## 1.0.1
14
+
15
+ ### Patch Changes
16
+
17
+ - Align repo version with npm publish.
18
+ - Updated dependencies:
19
+ - @marckrenn/pi-sub-shared@1.0.1
20
+
3
21
  ## 1.0.0
4
22
 
5
23
  ### Major Changes
package/README.md CHANGED
@@ -29,10 +29,10 @@ https://github.com/user-attachments/assets/d61d82f6-afd0-45fc-82f3-69910543aa7a
29
29
 
30
30
  | Provider | Usage Data | Status Page |
31
31
  |----------|-----------|-------------|
32
- | Anthropic (Claude) | 5h/7d windows, extra usage | ✅ |
32
+ | Anthropic (Claude) | 5h/Week windows, extra usage | ✅ |
33
33
  | GitHub Copilot | Monthly quota, requests | ✅ |
34
34
  | Google Gemini | Pro/Flash quotas | ✅ |
35
- | Antigravity | Claude/Pro/Flash quotas | ✅ |
35
+ | Antigravity | Model quotas | ✅ |
36
36
  | OpenAI Codex | Primary/secondary windows | ✅ |
37
37
  | AWS Kiro | Credits | - |
38
38
  | z.ai | Tokens/monthly limits | - |
@@ -41,27 +41,25 @@ https://github.com/user-attachments/assets/d61d82f6-afd0-45fc-82f3-69910543aa7a
41
41
 
42
42
  | Provider | Usage Windows | Extra Info | Status Indicator | Tested | Notes |
43
43
  |----------|--------------|------------|------------------|--------|-------|
44
- | Anthropic (Claude) | 5h, 7d, Extra | Extra usage label | ✅ | ✅ | Extra usage can show on/off state |
44
+ | Anthropic (Claude) | 5h, Week, Extra | Extra usage label | ✅ | ✅ | Extra usage can show on/off state |
45
45
  | GitHub Copilot | Month | Model multiplier + requests left | ✅ | ✅ | Requests left uses model multiplier |
46
46
  | Google Gemini | Pro, Flash | - | ✅ | - | Quotas aggregated per model family |
47
- | Antigravity | Claude, Pro, Flash | - | ✅ | - | Sandbox Cloud Code Assist quotas |
47
+ | Antigravity | Models | - | ✅ | | Sandbox Cloud Code Assist quotas |
48
48
  | OpenAI Codex | Primary, Secondary | - | ✅ | ✅ | Credits not yet supported (PRs welcome!) |
49
49
  | AWS Kiro | Credits | - | - | - | - |
50
50
  | z.ai | Tokens, Monthly | - | - | - | API quota limits |
51
51
 
52
52
  ## Installation
53
53
 
54
- Install via the pi package manager (recommended). `sub-bar` depends on `sub-core` for data (it will not render without it):
54
+ Install via the pi package manager (recommended). `sub-bar` bundles `sub-core`, so you only need to install `sub-bar`:
55
55
 
56
56
  ```bash
57
- pi install npm:@marckrenn/pi-sub-core
58
57
  pi install npm:@marckrenn/pi-sub-bar
59
58
  ```
60
59
 
61
60
  Use `-l` to install into project settings instead of global:
62
61
 
63
62
  ```bash
64
- pi install -l npm:@marckrenn/pi-sub-core
65
63
  pi install -l npm:@marckrenn/pi-sub-bar
66
64
  ```
67
65
 
@@ -69,17 +67,19 @@ Manual install (local development):
69
67
 
70
68
  ```bash
71
69
  git clone https://github.com/marckrenn/pi-sub.git
70
+ cd pi-sub
71
+ npm install
72
72
 
73
- ln -s /path/to/pi-sub/packages/sub-core ~/.pi/agent/extensions/sub-core
74
73
  ln -s /path/to/pi-sub/packages/sub-bar ~/.pi/agent/extensions/sub-bar
75
74
  ```
76
75
 
77
- Alternative (no symlink): add both to `~/.pi/agent/settings.json`:
76
+ If you want to develop `sub-core` separately, also symlink `packages/sub-core` into `~/.pi/agent/extensions`.
77
+
78
+ Alternative (no symlink): add `sub-bar` to `~/.pi/agent/settings.json`:
78
79
 
79
80
  ```json
80
81
  {
81
82
  "extensions": [
82
- "/path/to/pi-sub/packages/sub-core/index.ts",
83
83
  "/path/to/pi-sub/packages/sub-bar/index.ts"
84
84
  ]
85
85
  }
@@ -142,7 +142,6 @@ Credentials are loaded by sub-core from:
142
142
  Pi packages use a `pi` field in `package.json` plus the `pi-package` keyword for discoverability. This repo already declares `pi.extensions`, so you can install via:
143
143
 
144
144
  ```bash
145
- pi install npm:@marckrenn/pi-sub-core
146
145
  pi install npm:@marckrenn/pi-sub-bar
147
146
  ```
148
147
 
@@ -186,7 +185,7 @@ npm run check
186
185
 
187
186
  ## Credits
188
187
 
189
- - Hannes Januschka ([barts](https://github.com/hjanuschka/shitty-extensions?tab=readme-ov-file#usage-barts), [@hjanuschka](https://x.com/hjanuschka))
188
+ - ~Hannes~ Helmut Januschka ([usage-bar.ts](https://github.com/hjanuschka/shitty-extensions?tab=readme-ov-file#usage-barts), [@hjanuschka](https://x.com/hjanuschka))
190
189
  - Peter Steinberger ([CodexBar](https://github.com/steipete/CodexBar), [@steipete](https://x.com/steipete))
191
190
 
192
191
  ## License
package/index.ts CHANGED
@@ -7,6 +7,8 @@
7
7
  import type { ExtensionAPI, ExtensionContext, Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
8
8
  import { Container, Input, SelectList, Spacer, Text, truncateToWidth, wrapTextWithAnsi, visibleWidth } from "@mariozechner/pi-tui";
9
9
  import * as fs from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
10
12
  import type { ProviderName, ProviderUsageEntry, SubCoreAllState, SubCoreState, UsageSnapshot } from "./src/types.js";
11
13
  import type { Settings, BaseTextColor } from "./src/settings-types.js";
12
14
  import { isBackgroundColor, resolveBaseTextColor, resolveDividerColor } from "./src/settings-types.js";
@@ -58,6 +60,56 @@ function applyBaseTextColor(theme: Theme, color: BaseTextColor, text: string): s
58
60
  return theme.fg(resolveDividerColor(color), text);
59
61
  }
60
62
 
63
+ type PiSettings = {
64
+ enabledModels?: unknown;
65
+ };
66
+
67
+ const AGENT_SETTINGS_ENV = "PI_CODING_AGENT_DIR";
68
+ const DEFAULT_AGENT_DIR = join(homedir(), ".pi", "agent");
69
+ const PROJECT_SETTINGS_DIR = ".pi";
70
+ const SETTINGS_FILE_NAME = "settings.json";
71
+
72
+ function expandTilde(value: string): string {
73
+ if (value === "~") return homedir();
74
+ if (value.startsWith("~/")) return join(homedir(), value.slice(2));
75
+ return value;
76
+ }
77
+
78
+ function resolveAgentSettingsPath(): string {
79
+ const envDir = process.env[AGENT_SETTINGS_ENV];
80
+ const agentDir = envDir ? expandTilde(envDir) : DEFAULT_AGENT_DIR;
81
+ return join(agentDir, SETTINGS_FILE_NAME);
82
+ }
83
+
84
+ function readPiSettings(path: string): PiSettings | null {
85
+ try {
86
+ if (!fs.existsSync(path)) return null;
87
+ const content = fs.readFileSync(path, "utf-8");
88
+ return JSON.parse(content) as PiSettings;
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ function loadScopedModelPatterns(cwd: string): string[] {
95
+ const globalSettings = readPiSettings(resolveAgentSettingsPath());
96
+ const projectSettingsPath = join(cwd, PROJECT_SETTINGS_DIR, SETTINGS_FILE_NAME);
97
+ const projectSettings = readPiSettings(projectSettingsPath);
98
+
99
+ let enabledModels = Array.isArray(globalSettings?.enabledModels)
100
+ ? (globalSettings?.enabledModels as string[])
101
+ : undefined;
102
+
103
+ if (projectSettings && Object.prototype.hasOwnProperty.call(projectSettings, "enabledModels")) {
104
+ enabledModels = Array.isArray(projectSettings.enabledModels)
105
+ ? (projectSettings.enabledModels as string[])
106
+ : [];
107
+ }
108
+
109
+ if (!enabledModels || enabledModels.length === 0) return [];
110
+ return enabledModels.filter((value) => typeof value === "string");
111
+ }
112
+
61
113
  /**
62
114
  * Create the extension
63
115
  */
@@ -69,6 +121,7 @@ export default function createExtension(pi: ExtensionAPI) {
69
121
  let coreAvailable = false;
70
122
  let coreSettings: CoreSettings = getFallbackCoreSettings(settings);
71
123
  let fetchFailureTimer: NodeJS.Timeout | undefined;
124
+ const antigravityHiddenModels = new Set(["tab_flash_lite_preview"]);
72
125
  let settingsWatcher: fs.FSWatcher | undefined;
73
126
  let settingsPoll: NodeJS.Timeout | undefined;
74
127
  let settingsDebounce: NodeJS.Timeout | undefined;
@@ -233,11 +286,15 @@ export default function createExtension(pi: ExtensionAPI) {
233
286
  const wantsSplit = alignment === "split";
234
287
  const shouldAlign = !hasFill && !wantsSplit && (alignment === "center" || alignment === "right");
235
288
  const baseTextColor = resolveBaseTextColor(settings.display.baseTextColor);
289
+ const scopedModelPatterns = loadScopedModelPatterns(ctx.cwd);
290
+ const modelInfo = ctx.model
291
+ ? { provider: ctx.model.provider, id: ctx.model.id, scopedModelPatterns }
292
+ : { scopedModelPatterns };
236
293
  const formatted = message
237
294
  ? applyBaseTextColor(theme, baseTextColor, message)
238
295
  : (hasFill || wantsSplit)
239
- ? formatUsageStatusWithWidth(theme, usage!, contentWidth, ctx.model?.id, settings, { labelGapFill: wantsSplit })
240
- : formatUsageStatus(theme, usage!, ctx.model?.id, settings);
296
+ ? formatUsageStatusWithWidth(theme, usage!, contentWidth, modelInfo, settings, { labelGapFill: wantsSplit })
297
+ : formatUsageStatus(theme, usage!, modelInfo, settings);
241
298
 
242
299
  const alignLine = (line: string) => {
243
300
  if (!shouldAlign) return line;
@@ -251,7 +308,7 @@ export default function createExtension(pi: ExtensionAPI) {
251
308
  let lines: string[] = [];
252
309
  if (!formatted) {
253
310
  lines = [];
254
- } else if (settings.display.widgetWrapping === "wrap") {
311
+ } else if (settings.display.overflow === "wrap") {
255
312
  lines = wrapTextWithAnsi(formatted, contentWidth).map(alignLine);
256
313
  } else {
257
314
  const trimmed = alignLine(truncateToWidth(formatted, contentWidth, theme.fg("dim", "...")));
@@ -295,6 +352,46 @@ export default function createExtension(pi: ExtensionAPI) {
295
352
  return currentUsage;
296
353
  }
297
354
 
355
+ function syncAntigravityModels(usage?: UsageSnapshot): void {
356
+ if (!usage || usage.provider !== "antigravity") return;
357
+ const normalizeModel = (label: string) => label.toLowerCase().replace(/\s+/g, "_");
358
+ const labels = usage.windows
359
+ .map((window) => window.label?.trim())
360
+ .filter((label): label is string => Boolean(label))
361
+ .filter((label) => !antigravityHiddenModels.has(normalizeModel(label)));
362
+ const uniqueModels = Array.from(new Set(labels));
363
+ const antigravitySettings = settings.providers.antigravity;
364
+ const visibility = { ...(antigravitySettings.modelVisibility ?? {}) };
365
+ const modelSet = new Set(uniqueModels);
366
+ let changed = false;
367
+
368
+ for (const model of uniqueModels) {
369
+ if (!(model in visibility)) {
370
+ visibility[model] = false;
371
+ changed = true;
372
+ }
373
+ }
374
+
375
+ for (const existing of Object.keys(visibility)) {
376
+ if (!modelSet.has(existing)) {
377
+ delete visibility[existing];
378
+ changed = true;
379
+ }
380
+ }
381
+
382
+ const currentOrder = antigravitySettings.modelOrder ?? [];
383
+ const orderChanged = currentOrder.length !== uniqueModels.length
384
+ || currentOrder.some((model, index) => model !== uniqueModels[index]);
385
+ if (orderChanged) {
386
+ changed = true;
387
+ }
388
+
389
+ if (!changed) return;
390
+ antigravitySettings.modelVisibility = visibility;
391
+ antigravitySettings.modelOrder = uniqueModels;
392
+ saveSettings(settings);
393
+ }
394
+
298
395
  function updateEntries(entries: ProviderUsageEntry[] | undefined): void {
299
396
  if (!entries) return;
300
397
  const next: Partial<Record<ProviderName, UsageSnapshot>> = {};
@@ -303,6 +400,7 @@ export default function createExtension(pi: ExtensionAPI) {
303
400
  next[entry.provider] = entry.usage;
304
401
  }
305
402
  usageEntries = next;
403
+ syncAntigravityModels(next.antigravity);
306
404
  updateFetchFailureTicker();
307
405
  }
308
406
 
@@ -323,7 +421,7 @@ export default function createExtension(pi: ExtensionAPI) {
323
421
 
324
422
  function renderCurrent(ctx: ExtensionContext): void {
325
423
  if (!coreAvailable) {
326
- renderUsageWidget(ctx, undefined, "sub-core not installed");
424
+ renderUsageWidget(ctx, undefined, "pi-sub-core required. install with: pi install npm:@marckrenn/pi-sub-core");
327
425
  return;
328
426
  }
329
427
  const usage = resolveDisplayedUsage();
@@ -332,6 +430,7 @@ export default function createExtension(pi: ExtensionAPI) {
332
430
 
333
431
  function updateUsage(usage: UsageSnapshot | undefined): void {
334
432
  currentUsage = usage;
433
+ syncAntigravityModels(usage);
335
434
  updateFetchFailureTicker();
336
435
  if (lastContext) {
337
436
  renderCurrent(lastContext);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marckrenn/pi-sub-bar",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Usage widget extension for pi-coding-agent - shows current provider usage above the editor",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -13,7 +13,8 @@
13
13
  },
14
14
  "pi": {
15
15
  "extensions": [
16
- "./index.ts"
16
+ "./index.ts",
17
+ "node_modules/@marckrenn/pi-sub-core/index.ts"
17
18
  ]
18
19
  },
19
20
  "scripts": {
@@ -28,9 +29,16 @@
28
29
  "typescript": "^5.8.0"
29
30
  },
30
31
  "dependencies": {
31
- "@marckrenn/pi-sub-shared": "^1.0.0"
32
+ "@marckrenn/pi-sub-core": "^1.0.2",
33
+ "@marckrenn/pi-sub-shared": "^1.0.2"
32
34
  },
35
+ "bundledDependencies": [
36
+ "@marckrenn/pi-sub-core"
37
+ ],
33
38
  "peerDependencies": {
34
39
  "@mariozechner/pi-coding-agent": "*"
35
- }
40
+ },
41
+ "bundleDependencies": [
42
+ "@marckrenn/pi-sub-core"
43
+ ]
36
44
  }
package/src/formatting.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type { Theme } from "@mariozechner/pi-coding-agent";
6
6
  import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
7
- import type { RateWindow, UsageSnapshot, ProviderStatus } from "./types.js";
7
+ import type { RateWindow, UsageSnapshot, ProviderStatus, ModelInfo } from "./types.js";
8
8
  import type {
9
9
  BaseTextColor,
10
10
  BarStyle,
@@ -29,6 +29,13 @@ export interface UsageWindowParts {
29
29
  reset: string;
30
30
  }
31
31
 
32
+ type ModelInput = ModelInfo | string | undefined;
33
+
34
+ function resolveModelInfo(model?: ModelInput): ModelInfo | undefined {
35
+ if (!model) return undefined;
36
+ return typeof model === "string" ? { id: model } : model;
37
+ }
38
+
32
39
  /**
33
40
  * Get the characters to use for progress bars
34
41
  */
@@ -272,7 +279,9 @@ function formatProviderLabel(theme: Theme, usage: UsageSnapshot, settings?: Sett
272
279
  const baseStatus = showStatus ? usage.status : undefined;
273
280
  const lastSuccessAt = usage.lastSuccessAt;
274
281
  const elapsed = lastSuccessAt ? formatElapsedSince(lastSuccessAt) : undefined;
275
- const fetchDescription = elapsed ? `Updated: ${elapsed}` : "Fetch failed";
282
+ const fetchDescription = elapsed
283
+ ? (elapsed === "just now" ? "Last upd.: just now" : `Last upd.: ${elapsed} ago`)
284
+ : "Fetch failed";
276
285
  const fetchStatus: ProviderStatus | undefined = fetchError
277
286
  ? { indicator: "minor", description: fetchDescription }
278
287
  : undefined;
@@ -572,7 +581,7 @@ export function formatUsageWindowParts(
572
581
  export function formatUsageStatus(
573
582
  theme: Theme,
574
583
  usage: UsageSnapshot,
575
- modelId?: string,
584
+ model?: ModelInput,
576
585
  settings?: Settings
577
586
  ): string | undefined {
578
587
  const baseTextColor = resolveBaseTextColor(settings?.display.baseTextColor);
@@ -593,10 +602,12 @@ export function formatUsageStatus(
593
602
  const parts: string[] = [];
594
603
  const isCodex = usage.provider === "codex";
595
604
  const invertUsage = isCodex && (settings?.providers.codex.invertUsage ?? false);
605
+ const modelInfo = resolveModelInfo(model);
606
+ const modelId = modelInfo?.id;
596
607
 
597
608
  for (const w of usage.windows) {
598
609
  // Skip windows that are disabled in settings
599
- if (!shouldShowWindow(usage, w, settings)) {
610
+ if (!shouldShowWindow(usage, w, settings, modelInfo)) {
600
611
  continue;
601
612
  }
602
613
  parts.push(formatUsageWindow(theme, w, invertUsage, settings, usage));
@@ -630,7 +641,7 @@ export function formatUsageStatusWithWidth(
630
641
  theme: Theme,
631
642
  usage: UsageSnapshot,
632
643
  width: number,
633
- modelId?: string,
644
+ model?: ModelInput,
634
645
  settings?: Settings,
635
646
  options?: { labelGapFill?: boolean }
636
647
  ): string | undefined {
@@ -671,9 +682,11 @@ export function formatUsageStatusWithWidth(
671
682
  const windows: RateWindow[] = [];
672
683
  const isCodex = usage.provider === "codex";
673
684
  const invertUsage = isCodex && (settings?.providers.codex.invertUsage ?? false);
685
+ const modelInfo = resolveModelInfo(model);
686
+ const modelId = modelInfo?.id;
674
687
 
675
688
  for (const w of usage.windows) {
676
- if (!shouldShowWindow(usage, w, settings)) {
689
+ if (!shouldShowWindow(usage, w, settings, modelInfo)) {
677
690
  continue;
678
691
  }
679
692
  windows.push(w);
@@ -2,9 +2,9 @@
2
2
  * Provider metadata shared across the extension.
3
3
  */
4
4
 
5
- import type { RateWindow, UsageSnapshot, ProviderName } from "../types.js";
5
+ import type { RateWindow, UsageSnapshot, ProviderName, ModelInfo } from "../types.js";
6
6
  import type { Settings } from "../settings-types.js";
7
- import { getModelMultiplier } from "../utils.js";
7
+ import { getModelMultiplier, normalizeTokens } from "../utils.js";
8
8
  import { PROVIDER_METADATA as BASE_METADATA, type ProviderMetadata as BaseProviderMetadata } from "@marckrenn/pi-sub-shared";
9
9
 
10
10
  export { PROVIDERS, PROVIDER_DISPLAY_NAMES } from "@marckrenn/pi-sub-shared";
@@ -15,27 +15,27 @@ export interface UsageExtra {
15
15
  }
16
16
 
17
17
  export interface ProviderMetadata extends BaseProviderMetadata {
18
- isWindowVisible?: (usage: UsageSnapshot, window: RateWindow, settings?: Settings) => boolean;
18
+ isWindowVisible?: (usage: UsageSnapshot, window: RateWindow, settings?: Settings, model?: ModelInfo) => boolean;
19
19
  getExtras?: (usage: UsageSnapshot, settings?: Settings, modelId?: string) => UsageExtra[];
20
20
  }
21
21
 
22
- const anthropicWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings) => {
22
+ const anthropicWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings, _model) => {
23
23
  if (!settings) return true;
24
24
  const ps = settings.providers.anthropic;
25
25
  if (window.label === "5h") return ps.windows.show5h;
26
- if (window.label === "7d") return ps.windows.show7d;
26
+ if (window.label === "Week") return ps.windows.show7d;
27
27
  if (window.label.startsWith("Extra [")) return ps.windows.showExtra;
28
28
  return true;
29
29
  };
30
30
 
31
- const copilotWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings) => {
31
+ const copilotWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings, _model) => {
32
32
  if (!settings) return true;
33
33
  const ps = settings.providers.copilot;
34
34
  if (window.label === "Month") return ps.windows.showMonth;
35
35
  return true;
36
36
  };
37
37
 
38
- const geminiWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings) => {
38
+ const geminiWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings, _model) => {
39
39
  if (!settings) return true;
40
40
  const ps = settings.providers.gemini;
41
41
  if (window.label === "Pro") return ps.windows.showPro;
@@ -43,16 +43,47 @@ const geminiWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window
43
43
  return true;
44
44
  };
45
45
 
46
- const antigravityWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings) => {
46
+ const antigravityWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings, model) => {
47
47
  if (!settings) return true;
48
48
  const ps = settings.providers.antigravity;
49
- if (window.label === "Claude") return ps.windows.showClaude;
50
- if (window.label === "Pro") return ps.windows.showPro;
51
- if (window.label === "Flash") return ps.windows.showFlash;
52
- return true;
49
+ const label = window.label.trim();
50
+ const normalized = label.toLowerCase().replace(/\s+/g, "_");
51
+ if (normalized === "tab_flash_lite_preview") return false;
52
+
53
+ const labelTokens = normalizeTokens(label);
54
+
55
+ const modelProvider = model?.provider?.toLowerCase() ?? "";
56
+ const modelId = model?.id;
57
+ const providerMatches = modelProvider.includes("antigravity");
58
+ if (ps.showCurrentModel && providerMatches && modelId) {
59
+ const modelTokens = normalizeTokens(modelId);
60
+ const match = modelTokens.length > 0 && modelTokens.every((token) => labelTokens.includes(token));
61
+ if (match) return true;
62
+ }
63
+
64
+ if (ps.showScopedModels) {
65
+ const scopedPatterns = model?.scopedModelPatterns ?? [];
66
+ const matchesScoped = scopedPatterns.some((pattern) => {
67
+ if (!pattern) return false;
68
+ const [rawPattern] = pattern.split(":");
69
+ const trimmed = rawPattern?.trim();
70
+ if (!trimmed) return false;
71
+ const hasProvider = trimmed.includes("/");
72
+ if (!hasProvider) return false;
73
+ const providerPart = trimmed.slice(0, trimmed.indexOf("/")).trim().toLowerCase();
74
+ if (!providerPart.includes("antigravity")) return false;
75
+ const base = trimmed.slice(trimmed.lastIndexOf("/") + 1);
76
+ const tokens = normalizeTokens(base);
77
+ return tokens.length > 0 && tokens.every((token) => labelTokens.includes(token));
78
+ });
79
+ if (matchesScoped) return true;
80
+ }
81
+
82
+ const visibility = ps.modelVisibility?.[label];
83
+ return visibility === true;
53
84
  };
54
85
 
55
- const codexWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings) => {
86
+ const codexWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings, _model) => {
56
87
  if (!settings) return true;
57
88
  const ps = settings.providers.codex;
58
89
  if (window.label.match(/^\d+h$/)) return ps.windows.showPrimary;
@@ -60,14 +91,14 @@ const codexWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window,
60
91
  return true;
61
92
  };
62
93
 
63
- const kiroWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings) => {
94
+ const kiroWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings, _model) => {
64
95
  if (!settings) return true;
65
96
  const ps = settings.providers.kiro;
66
97
  if (window.label === "Credits") return ps.windows.showCredits;
67
98
  return true;
68
99
  };
69
100
 
70
- const zaiWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings) => {
101
+ const zaiWindowVisible: ProviderMetadata["isWindowVisible"] = (_usage, window, settings, _model) => {
71
102
  if (!settings) return true;
72
103
  const ps = settings.providers.zai;
73
104
  if (window.label === "Tokens") return ps.windows.showTokens;
@@ -57,10 +57,10 @@ export function buildProviderSettingsItems(settings: Settings, provider: Provide
57
57
  },
58
58
  {
59
59
  id: "show7d",
60
- label: "Show 7d Window",
60
+ label: "Show Week Window",
61
61
  currentValue: anthroSettings.windows.show7d ? "on" : "off",
62
62
  values: ["on", "off"],
63
- description: "Show the 7-day usage window.",
63
+ description: "Show the weekly usage window.",
64
64
  },
65
65
  {
66
66
  id: "showExtra",
@@ -130,27 +130,41 @@ export function buildProviderSettingsItems(settings: Settings, provider: Provide
130
130
  const antigravitySettings = ps as AntigravityProviderSettings;
131
131
  items.push(
132
132
  {
133
- id: "showClaude",
134
- label: "Show Claude Window",
135
- currentValue: antigravitySettings.windows.showClaude ? "on" : "off",
133
+ id: "showCurrentModel",
134
+ label: "Always Show Current Model",
135
+ currentValue: antigravitySettings.showCurrentModel ? "on" : "off",
136
136
  values: ["on", "off"],
137
- description: "Show the Claude quota window.",
137
+ description: "Show the active Antigravity model even if hidden.",
138
138
  },
139
139
  {
140
- id: "showPro",
141
- label: "Show Pro Window",
142
- currentValue: antigravitySettings.windows.showPro ? "on" : "off",
143
- values: ["on", "off"],
144
- description: "Show the Gemini Pro quota window.",
145
- },
146
- {
147
- id: "showFlash",
148
- label: "Show Flash Window",
149
- currentValue: antigravitySettings.windows.showFlash ? "on" : "off",
140
+ id: "showScopedModels",
141
+ label: "Show Scoped Models",
142
+ currentValue: antigravitySettings.showScopedModels ? "on" : "off",
150
143
  values: ["on", "off"],
151
- description: "Show the Gemini Flash quota window.",
144
+ description: "Show Antigravity models that are in the scoped model rotation.",
152
145
  },
153
146
  );
147
+
148
+ const modelVisibility = antigravitySettings.modelVisibility ?? {};
149
+ const modelOrder = antigravitySettings.modelOrder?.length
150
+ ? antigravitySettings.modelOrder
151
+ : Object.keys(modelVisibility).sort((a, b) => a.localeCompare(b));
152
+ const seenModels = new Set<string>();
153
+
154
+ for (const model of modelOrder) {
155
+ if (!model || seenModels.has(model)) continue;
156
+ seenModels.add(model);
157
+ const normalized = model.toLowerCase().replace(/\s+/g, "_");
158
+ if (normalized === "tab_flash_lite_preview") continue;
159
+ const visible = modelVisibility[model] !== false;
160
+ items.push({
161
+ id: `model:${model}`,
162
+ label: model,
163
+ currentValue: visible ? "on" : "off",
164
+ values: ["on", "off"],
165
+ description: "Toggle this model window.",
166
+ });
167
+ }
154
168
  }
155
169
 
156
170
  if (provider === "codex") {
@@ -276,14 +290,31 @@ export function applyProviderSettingsChange(
276
290
  if (provider === "antigravity") {
277
291
  const antigravitySettings = ps as AntigravityProviderSettings;
278
292
  switch (id) {
279
- case "showClaude":
280
- antigravitySettings.windows.showClaude = value === "on";
293
+ case "showModels":
294
+ antigravitySettings.windows.showModels = value === "on";
281
295
  break;
282
- case "showPro":
283
- antigravitySettings.windows.showPro = value === "on";
296
+ case "showCurrentModel":
297
+ antigravitySettings.showCurrentModel = value === "on";
284
298
  break;
285
- case "showFlash":
286
- antigravitySettings.windows.showFlash = value === "on";
299
+ case "showScopedModels":
300
+ antigravitySettings.showScopedModels = value === "on";
301
+ break;
302
+ default:
303
+ if (id.startsWith("model:")) {
304
+ const model = id.slice("model:".length);
305
+ if (model) {
306
+ if (!antigravitySettings.modelVisibility) {
307
+ antigravitySettings.modelVisibility = {};
308
+ }
309
+ antigravitySettings.modelVisibility[model] = value === "on";
310
+ if (!antigravitySettings.modelOrder) {
311
+ antigravitySettings.modelOrder = [];
312
+ }
313
+ if (!antigravitySettings.modelOrder.includes(model)) {
314
+ antigravitySettings.modelOrder.push(model);
315
+ }
316
+ }
317
+ }
287
318
  break;
288
319
  }
289
320
  }
@@ -2,17 +2,22 @@
2
2
  * Provider-specific window visibility rules.
3
3
  */
4
4
 
5
- import type { RateWindow, UsageSnapshot } from "../types.js";
5
+ import type { RateWindow, UsageSnapshot, ModelInfo } from "../types.js";
6
6
  import type { Settings } from "../settings-types.js";
7
7
  import { PROVIDER_METADATA } from "./metadata.js";
8
8
 
9
9
  /**
10
10
  * Check if a window should be shown based on settings.
11
11
  */
12
- export function shouldShowWindow(usage: UsageSnapshot, window: RateWindow, settings?: Settings): boolean {
12
+ export function shouldShowWindow(
13
+ usage: UsageSnapshot,
14
+ window: RateWindow,
15
+ settings?: Settings,
16
+ model?: ModelInfo
17
+ ): boolean {
13
18
  const handler = PROVIDER_METADATA[usage.provider]?.isWindowVisible;
14
19
  if (handler) {
15
- return handler(usage, window, settings);
20
+ return handler(usage, window, settings, model);
16
21
  }
17
22
  return true;
18
23
  }
@@ -42,11 +42,11 @@ export function buildDisplayLayoutItems(settings: Settings): SettingItem[] {
42
42
  description: "Align the usage line inside the widget.",
43
43
  },
44
44
  {
45
- id: "widgetWrapping",
46
- label: "Widget Wrapping",
47
- currentValue: settings.display.widgetWrapping,
45
+ id: "overflow",
46
+ label: "Overflow",
47
+ currentValue: settings.display.overflow,
48
48
  values: ["truncate", "wrap"] as WidgetWrapping[],
49
- description: "Wrap the usage line or truncate with ellipsis.",
49
+ description: "Wrap the usage line or truncate with ellipsis (requires bar width ≠ fill and alignment ≠ split).",
50
50
  },
51
51
  {
52
52
  id: "paddingX",
@@ -666,8 +666,11 @@ export function applyDisplayChange(settings: Settings, id: string, value: string
666
666
  case "showBottomDivider":
667
667
  settings.display.showBottomDivider = value === "on";
668
668
  break;
669
+ case "overflow":
670
+ settings.display.overflow = value as WidgetWrapping;
671
+ break;
669
672
  case "widgetWrapping":
670
- settings.display.widgetWrapping = value as WidgetWrapping;
673
+ settings.display.overflow = value as WidgetWrapping;
671
674
  break;
672
675
  case "errorThreshold": {
673
676
  const parsed = parseClampedNumber(value, 0, 100);
@@ -22,7 +22,7 @@ export function buildMainMenuItems(settings: Settings, pinnedProvider?: Provider
22
22
  {
23
23
  value: "providers",
24
24
  label: "Provider Settings",
25
- description: `${Object.keys(settings.providers).length} providers`,
25
+ description: "provider specific settings",
26
26
  tooltip: "Configure provider display toggles and window visibility.",
27
27
  },
28
28
  {
@@ -134,9 +134,9 @@ export function buildDisplayThemeMenuItems(): TooltipSelectItem[] {
134
134
  },
135
135
  {
136
136
  value: "display-theme-load",
137
- label: "Load theme",
138
- description: "restore or apply",
139
- tooltip: "Load a saved or default theme.",
137
+ label: "Manage & Load themes",
138
+ description: "load, share, delete and restore themes",
139
+ tooltip: "Load, share, delete, and restore saved themes.",
140
140
  },
141
141
  {
142
142
  value: "display-theme-import",
@@ -148,7 +148,13 @@ export function buildDisplayThemeMenuItems(): TooltipSelectItem[] {
148
148
  value: "display-theme-random",
149
149
  label: "Random theme",
150
150
  description: "generate a new theme",
151
- tooltip: "Generate a new random display theme.",
151
+ tooltip: "Generate a random display theme as inspiration or a starting point.",
152
+ },
153
+ {
154
+ value: "display-theme-restore",
155
+ label: "Restore previous state",
156
+ description: "restore your last theme",
157
+ tooltip: "Restore your previous display theme.",
152
158
  },
153
159
  ];
154
160
  }
@@ -9,7 +9,7 @@ type BarWidth = DisplaySettings["barWidth"];
9
9
  type DividerCharacter = DisplaySettings["dividerCharacter"];
10
10
  type DividerBlanks = DisplaySettings["dividerBlanks"];
11
11
  type DisplayAlignment = DisplaySettings["alignment"];
12
- type WidgetWrapping = DisplaySettings["widgetWrapping"];
12
+ type OverflowMode = DisplaySettings["overflow"];
13
13
  type BaseTextColor = DisplaySettings["baseTextColor"];
14
14
  type DividerColor = DisplaySettings["dividerColor"];
15
15
  type ResetTimeFormat = DisplaySettings["resetTimeFormat"];
@@ -34,7 +34,7 @@ const RANDOM_BAR_CHARACTERS: BarCharacter[] = [
34
34
  "🚀_",
35
35
  ];
36
36
  const RANDOM_ALIGNMENTS: DisplayAlignment[] = ["left", "center", "right", "split"];
37
- const RANDOM_WRAPPINGS: WidgetWrapping[] = ["truncate", "wrap"];
37
+ const RANDOM_OVERFLOW: OverflowMode[] = ["truncate", "wrap"];
38
38
  const RANDOM_RESET_POSITIONS: DisplaySettings["resetTimePosition"][] = ["off", "front", "back", "integrated"];
39
39
  const RANDOM_RESET_FORMATS: ResetTimeFormat[] = ["relative", "datetime"];
40
40
  const RANDOM_RESET_CONTAINMENTS: ResetTimerContainment[] = ["none", "blank", "()", "[]", "<>"];
@@ -189,7 +189,7 @@ export function resolveDisplayThemeTarget(
189
189
  widgetPlacement: "belowEditor",
190
190
  errorThreshold: 25,
191
191
  warningThreshold: 50,
192
- widgetWrapping: "truncate",
192
+ overflow: "truncate",
193
193
  successThreshold: 75,
194
194
  },
195
195
  deletable: false,
@@ -208,7 +208,7 @@ export function buildRandomDisplay(base: DisplaySettings): DisplaySettings {
208
208
  const display: DisplaySettings = { ...base };
209
209
 
210
210
  display.alignment = pickRandom(RANDOM_ALIGNMENTS);
211
- display.widgetWrapping = pickRandom(RANDOM_WRAPPINGS);
211
+ display.overflow = pickRandom(RANDOM_OVERFLOW);
212
212
  display.paddingX = pickRandom(RANDOM_PADDING);
213
213
  display.barStyle = pickRandom(RANDOM_BAR_STYLES);
214
214
  display.barType = pickRandom(RANDOM_BAR_TYPES);
@@ -62,6 +62,7 @@ type SettingsCategory =
62
62
  | "display-theme-import"
63
63
  | "display-theme-import-action"
64
64
  | "display-theme-random"
65
+ | "display-theme-restore"
65
66
  | "display-layout"
66
67
  | "display-bar"
67
68
  | "display-provider"
@@ -100,6 +101,7 @@ export async function showSettingsUI(
100
101
  let themeActionTarget: { id?: string; name: string; display: Settings["display"]; deletable: boolean } | null = null;
101
102
  let displayPreviewBackup: Settings["display"] | null = null;
102
103
  let randomThemeBackup: Settings["display"] | null = null;
104
+ let displayThemeSelection: string | null = null;
103
105
  let pinnedProviderBackup: ProviderName | null | undefined;
104
106
  let importCandidate: DecodedDisplayShare | null = null;
105
107
  let importBackup: Settings["display"] | null = null;
@@ -326,6 +328,7 @@ export async function showSettingsUI(
326
328
  "display-theme-load": "Load Theme",
327
329
  "display-theme-action": "Manage Theme",
328
330
  "display-theme-import": "Import Theme",
331
+ "display-theme-restore": "Restore Theme",
329
332
  "display-layout": "Layout & Structure",
330
333
  "display-bar": "Bars",
331
334
  "display-provider": "Labels & Text",
@@ -547,8 +550,15 @@ export async function showSettingsUI(
547
550
  scrollInfo: (t: string) => theme.fg("dim", t),
548
551
  noMatch: (t: string) => theme.fg("warning", t),
549
552
  });
553
+ if (displayThemeSelection) {
554
+ const index = items.findIndex((item) => item.value === displayThemeSelection);
555
+ if (index >= 0) {
556
+ selectList.setSelectedIndex(index);
557
+ }
558
+ }
550
559
  attachTooltip(items, selectList);
551
560
  selectList.onSelect = (item) => {
561
+ displayThemeSelection = item.value;
552
562
  currentCategory = item.value as SettingsCategory;
553
563
  rebuild();
554
564
  tui.requestRender();
@@ -651,6 +661,7 @@ export async function showSettingsUI(
651
661
  randomThemeBackup = { ...settings.display };
652
662
  settings.displayUserTheme = { ...randomThemeBackup };
653
663
  }
664
+ displayThemeSelection = "display-theme-random";
654
665
  const randomDisplay = buildRandomDisplay(settings.display);
655
666
  settings.display = { ...randomDisplay };
656
667
  saveSettings(settings);
@@ -658,6 +669,23 @@ export async function showSettingsUI(
658
669
  currentCategory = "display-theme";
659
670
  rebuild();
660
671
  tui.requestRender();
672
+ } else if (currentCategory === "display-theme-restore") {
673
+ displayThemeSelection = "display-theme-restore";
674
+ const defaults = getDefaultSettings();
675
+ const fallbackUser = settings.displayUserTheme ?? settings.display;
676
+ const target = resolveDisplayThemeTarget("user", settings, defaults, fallbackUser);
677
+ if (target) {
678
+ const backup = displayPreviewBackup ?? settings.display;
679
+ settings.displayUserTheme = { ...backup };
680
+ settings.display = { ...target.display };
681
+ saveSettings(settings);
682
+ if (onSettingsChange) void onSettingsChange(settings);
683
+ if (onDisplayThemeApplied) void onDisplayThemeApplied(target.name, { source: "manual" });
684
+ displayPreviewBackup = null;
685
+ }
686
+ currentCategory = "display-theme";
687
+ rebuild();
688
+ tui.requestRender();
661
689
  } else if (currentCategory === "display-theme-import") {
662
690
  const input = new Input();
663
691
  input.focused = true;
@@ -1018,7 +1046,8 @@ export async function showSettingsUI(
1018
1046
  currentCategory === "display-theme" ||
1019
1047
  currentCategory === "display-theme-load" ||
1020
1048
  currentCategory === "display-theme-action" ||
1021
- currentCategory === "display-theme-random"
1049
+ currentCategory === "display-theme-random" ||
1050
+ currentCategory === "display-theme-restore"
1022
1051
  ) {
1023
1052
  helpText = "↑↓ navigate • Enter/Space select • Esc back";
1024
1053
  } else {
@@ -45,9 +45,10 @@ export type DividerCharacter =
45
45
  | (string & {});
46
46
 
47
47
  /**
48
- * Widget line wrapping mode
48
+ * Widget overflow mode
49
49
  */
50
- export type WidgetWrapping = "truncate" | "wrap";
50
+ export type OverflowMode = "truncate" | "wrap";
51
+ export type WidgetWrapping = OverflowMode;
51
52
 
52
53
  /**
53
54
  * Widget placement
@@ -223,11 +224,13 @@ export interface GeminiProviderSettings extends BaseProviderSettings {
223
224
  }
224
225
 
225
226
  export interface AntigravityProviderSettings extends BaseProviderSettings {
227
+ showCurrentModel: boolean;
228
+ showScopedModels: boolean;
226
229
  windows: {
227
- showClaude: boolean;
228
- showPro: boolean;
229
- showFlash: boolean;
230
+ showModels: boolean;
230
231
  };
232
+ modelVisibility: Record<string, boolean>;
233
+ modelOrder: string[];
231
234
  }
232
235
 
233
236
  export interface CodexProviderSettings extends BaseProviderSettings {
@@ -335,8 +338,8 @@ export interface DisplaySettings {
335
338
  showTopDivider: boolean;
336
339
  /** Show divider line below the bar */
337
340
  showBottomDivider: boolean;
338
- /** Widget line wrapping */
339
- widgetWrapping: WidgetWrapping;
341
+ /** Widget overflow mode */
342
+ overflow: OverflowMode;
340
343
  /** Left/right padding inside widget */
341
344
  paddingX: number;
342
345
  /** Widget placement */
@@ -412,12 +415,14 @@ export function getDefaultSettings(): Settings {
412
415
  },
413
416
  },
414
417
  antigravity: {
415
- showStatus: false,
418
+ showStatus: true,
419
+ showCurrentModel: true,
420
+ showScopedModels: true,
416
421
  windows: {
417
- showClaude: true,
418
- showPro: true,
419
- showFlash: true,
422
+ showModels: true,
420
423
  },
424
+ modelVisibility: {},
425
+ modelOrder: [],
421
426
  },
422
427
  codex: {
423
428
  showStatus: true,
@@ -486,7 +491,7 @@ export function getDefaultSettings(): Settings {
486
491
  widgetPlacement: "belowEditor",
487
492
  errorThreshold: 25,
488
493
  warningThreshold: 50,
489
- widgetWrapping: "truncate",
494
+ overflow: "truncate",
490
495
  successThreshold: 75,
491
496
  },
492
497
 
@@ -538,5 +543,28 @@ function deepMerge<T extends object>(target: T, source: Partial<T>): T {
538
543
  * Merge settings with defaults (no legacy migrations).
539
544
  */
540
545
  export function mergeSettings(loaded: Partial<Settings>): Settings {
541
- return deepMerge(getDefaultSettings(), loaded);
546
+ const migrated = migrateSettings(loaded);
547
+ return deepMerge(getDefaultSettings(), migrated);
548
+ }
549
+
550
+ function migrateDisplaySettings(display?: Partial<DisplaySettings> | null): void {
551
+ if (!display) return;
552
+ const displayAny = display as Partial<DisplaySettings> & { widgetWrapping?: OverflowMode };
553
+ if (displayAny.widgetWrapping !== undefined && displayAny.overflow === undefined) {
554
+ displayAny.overflow = displayAny.widgetWrapping;
555
+ }
556
+ if ("widgetWrapping" in displayAny) {
557
+ delete (displayAny as { widgetWrapping?: unknown }).widgetWrapping;
558
+ }
559
+ }
560
+
561
+ function migrateSettings(loaded: Partial<Settings>): Partial<Settings> {
562
+ migrateDisplaySettings(loaded.display);
563
+ migrateDisplaySettings(loaded.displayUserTheme);
564
+ if (Array.isArray(loaded.displayThemes)) {
565
+ for (const theme of loaded.displayThemes) {
566
+ migrateDisplaySettings(theme.display as Partial<DisplaySettings> | undefined);
567
+ }
568
+ }
569
+ return loaded;
542
570
  }
package/src/types.ts CHANGED
@@ -17,3 +17,9 @@ export type {
17
17
  } from "@marckrenn/pi-sub-shared";
18
18
 
19
19
  export { PROVIDERS } from "@marckrenn/pi-sub-shared";
20
+
21
+ export type ModelInfo = {
22
+ provider?: string;
23
+ id?: string;
24
+ scopedModelPatterns?: string[];
25
+ };
package/src/utils.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { MODEL_MULTIPLIERS } from "@marckrenn/pi-sub-shared";
6
6
 
7
- function normalizeTokens(value: string): string[] {
7
+ export function normalizeTokens(value: string): string[] {
8
8
  return value
9
9
  .toLowerCase()
10
10
  .replace(/[^a-z0-9]+/g, " ")
@@ -172,7 +172,7 @@ test("fetch errors rely on status text instead of appended warning", () => {
172
172
 
173
173
  const output = formatUsageStatus(theme, usage, undefined, settings);
174
174
  assert.ok(output);
175
- assert.ok(output.includes("Updated: 5m"));
175
+ assert.ok(output.includes("Last upd.: 5m ago"));
176
176
  assert.ok(!output.includes("(Fetch failed)"));
177
177
  assert.ok(output.includes("5h"));
178
178
  });
@@ -238,7 +238,7 @@ test("extras render even when usage windows are hidden", () => {
238
238
  displayName: "Anthropic (Claude)",
239
239
  windows: [
240
240
  { label: "5h", usedPercent: 10 },
241
- { label: "7d", usedPercent: 20 },
241
+ { label: "Week", usedPercent: 20 },
242
242
  ],
243
243
  extraUsageEnabled: false,
244
244
  };
@@ -247,7 +247,7 @@ test("extras render even when usage windows are hidden", () => {
247
247
  assert.ok(output);
248
248
  assert.ok(output.includes("Extra [off]"));
249
249
  assert.ok(!output.includes("5h"));
250
- assert.ok(!output.includes("7d"));
250
+ assert.ok(!output.includes("Week"));
251
251
  });
252
252
 
253
253
  test("percentage labels clamp to bounds", () => {