pi-provider-antigravity 0.11.0 → 0.13.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.
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { createHash, randomBytes } from "node:crypto";
6
6
 
7
7
  // src/protocol.ts
8
8
  var DEFAULT_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
9
- var DEFAULT_USER_AGENT = "antigravity/cli/1.2.3 (aidev_client; os_type=linux; arch=amd64; cl=981443618; auth_method=consumer)";
9
+ var DEFAULT_USER_AGENT = "antigravity/cli/1.2.5 (aidev_client; os_type=linux; arch=amd64; cl=982839923; auth_method=consumer)";
10
10
  var PROVIDER_ID = "antigravity";
11
11
  var AntigravityHttpError = class extends Error {
12
12
  status;
@@ -254,7 +254,24 @@ function parseStoredCredentials(rawApiKey) {
254
254
  return { token: rawApiKey, projectId: FALLBACK_PROJECT_ID };
255
255
  }
256
256
 
257
- // src/models.ts
257
+ // src/model-identity.ts
258
+ function classifyModelFamily(modelId) {
259
+ const bare = ((modelId || "").split("/").pop() || "").toLowerCase();
260
+ if (bare.startsWith("gemini-")) return "gemini";
261
+ if (bare.startsWith("claude-")) return "claude";
262
+ if (bare.startsWith("gpt-")) return "gpt";
263
+ return "unknown";
264
+ }
265
+ function isCompatibleFamily(msgModel, targetModelId) {
266
+ if (!msgModel || !targetModelId) return true;
267
+ if (msgModel === targetModelId) return true;
268
+ const msgFamily = classifyModelFamily(msgModel);
269
+ if (msgFamily !== "unknown" && msgFamily === classifyModelFamily(targetModelId)) return true;
270
+ return false;
271
+ }
272
+
273
+ // src/model-catalog.ts
274
+ var PRIVATE_SNAPSHOT_KEY = "pi-provider-antigravity";
258
275
  var CANONICAL_TIER_SUFFIXES = Object.freeze({
259
276
  low: "-low",
260
277
  medium: "-medium",
@@ -305,23 +322,6 @@ function tierCandidateOrder(effort) {
305
322
  const fallbacks = TIER_FALLBACKS[effort] ?? DEFAULT_TIER_ORDER;
306
323
  return [.../* @__PURE__ */ new Set([...canonical, ...fallbacks])];
307
324
  }
308
- function classifyModelFamily(modelId) {
309
- const bare = ((modelId || "").split("/").pop() || "").toLowerCase();
310
- if (bare.startsWith("gemini-")) return "gemini";
311
- if (bare.startsWith("claude-")) return "claude";
312
- if (bare.startsWith("gpt-")) return "gpt";
313
- return "unknown";
314
- }
315
- function isCompatibleFamily(msgModel, targetModelId) {
316
- if (!msgModel || !targetModelId) return true;
317
- if (msgModel === targetModelId) return true;
318
- const msgFamily = classifyModelFamily(msgModel);
319
- if (msgFamily !== "unknown" && msgFamily === classifyModelFamily(targetModelId)) return true;
320
- return extractBaseModelId(msgModel) === extractBaseModelId(targetModelId);
321
- }
322
-
323
- // src/model-catalog.ts
324
- var PRIVATE_SNAPSHOT_KEY = "pi-provider-antigravity";
325
325
  function buildThinkingMap(models) {
326
326
  return Object.fromEntries(
327
327
  models.map((m) => [
@@ -691,11 +691,268 @@ function createModelCatalog() {
691
691
  };
692
692
  }
693
693
 
694
- // src/quota-status.ts
694
+ // src/config.ts
695
695
  import * as fs from "node:fs";
696
696
  import * as os from "node:os";
697
697
  import * as path from "node:path";
698
698
  import { SettingsList } from "@earendil-works/pi-tui";
699
+ var PROVIDER_CONFIG_FILE = "pi-provider-antigravity.json";
700
+ function defaultConfigFile(env = process.env) {
701
+ const dir = (env.PI_CODING_AGENT_DIR || "").trim() || path.join(os.homedir(), ".pi", "agent");
702
+ return path.join(dir, PROVIDER_CONFIG_FILE);
703
+ }
704
+ function loadProviderConfig(file = defaultConfigFile()) {
705
+ try {
706
+ const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
707
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
708
+ } catch {
709
+ }
710
+ return void 0;
711
+ }
712
+ function writeProviderConfigAtomically(file, data) {
713
+ const tmp = `${file}.tmp-${process.pid}`;
714
+ try {
715
+ fs.mkdirSync(path.dirname(file), { recursive: true });
716
+ const mode = fs.existsSync(file) ? fs.statSync(file).mode & 511 : void 0;
717
+ const fd = fs.openSync(tmp, "w", mode);
718
+ try {
719
+ fs.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf-8");
720
+ fs.fsyncSync(fd);
721
+ } finally {
722
+ fs.closeSync(fd);
723
+ }
724
+ fs.renameSync(tmp, file);
725
+ return true;
726
+ } catch {
727
+ try {
728
+ fs.rmSync(tmp, { force: true });
729
+ } catch {
730
+ }
731
+ return false;
732
+ }
733
+ }
734
+ function updateProviderConfig(file, mutate) {
735
+ try {
736
+ let data = {};
737
+ try {
738
+ const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
739
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
740
+ data = raw;
741
+ } catch (err) {
742
+ if (err?.code !== "ENOENT") return false;
743
+ }
744
+ mutate(data);
745
+ return writeProviderConfigAtomically(file, data);
746
+ } catch {
747
+ return false;
748
+ }
749
+ }
750
+ function loadSubsystemState(subsystem, file = defaultConfigFile()) {
751
+ const state = loadProviderConfig(file)?.states?.[subsystem];
752
+ return state && typeof state === "object" && !Array.isArray(state) ? state : void 0;
753
+ }
754
+ function saveSubsystemState(subsystem, state, file = defaultConfigFile()) {
755
+ return updateProviderConfig(file, (config) => {
756
+ const states = config.states && typeof config.states === "object" && !Array.isArray(config.states) ? config.states : {};
757
+ const current = states[subsystem] && typeof states[subsystem] === "object" && !Array.isArray(states[subsystem]) ? states[subsystem] : {};
758
+ states[subsystem] = { ...current, ...state };
759
+ config.states = states;
760
+ });
761
+ }
762
+ var SettingsRegistry = class {
763
+ fields = [];
764
+ register(field) {
765
+ const existingIndex = this.fields.findIndex((f) => f.key === field.key);
766
+ if (existingIndex >= 0) {
767
+ this.fields[existingIndex] = field;
768
+ } else {
769
+ this.fields.push(field);
770
+ }
771
+ }
772
+ unregister(key) {
773
+ this.fields = this.fields.filter((f) => f.key !== key);
774
+ }
775
+ getFields() {
776
+ return [...this.fields];
777
+ }
778
+ clear() {
779
+ this.fields = [];
780
+ }
781
+ };
782
+ var defaultSettingsRegistry = new SettingsRegistry();
783
+ function registerSettingField(field) {
784
+ defaultSettingsRegistry.register(field);
785
+ }
786
+ function getRegisteredSettingFields() {
787
+ return defaultSettingsRegistry.getFields();
788
+ }
789
+ function fieldDisplayValue(config, field) {
790
+ const raw = config?.settings?.[field.key];
791
+ if (typeof raw === "string" && raw.trim()) return raw.trim();
792
+ return field.defaultValue ?? "unset";
793
+ }
794
+ function saveSettingValue(field, value, file) {
795
+ return updateProviderConfig(file, (config) => {
796
+ const settings = config.settings && typeof config.settings === "object" && !Array.isArray(config.settings) ? config.settings : {};
797
+ settings[field.key] = value;
798
+ config.settings = settings;
799
+ });
800
+ }
801
+ async function applySettingValue(field, value, file, ctx) {
802
+ if (!saveSettingValue(field, value, file)) {
803
+ return false;
804
+ }
805
+ await field.onChange?.(ctx, value);
806
+ return true;
807
+ }
808
+ function writeFailedMessage(file) {
809
+ return `Failed to write ${file}. If the file is not valid JSON, fix or delete it.`;
810
+ }
811
+ function buildSettingsItems(config, fields = getRegisteredSettingFields()) {
812
+ return fields.map((f) => ({
813
+ id: f.key,
814
+ label: f.label,
815
+ description: f.description,
816
+ currentValue: fieldDisplayValue(config, f),
817
+ values: [...f.options]
818
+ }));
819
+ }
820
+ async function settingsListTheme(theme) {
821
+ try {
822
+ const pi = await import("@earendil-works/pi-coding-agent");
823
+ return pi.getSettingsListTheme();
824
+ } catch {
825
+ return {
826
+ label: (text, selected) => selected ? theme.fg("accent", text) : text,
827
+ value: (text, selected) => selected ? theme.fg("accent", text) : theme.fg("muted", text),
828
+ description: (text) => theme.fg("dim", text),
829
+ cursor: theme.fg("accent", "\u2192 "),
830
+ hint: (text) => theme.fg("dim", text)
831
+ };
832
+ }
833
+ }
834
+ function resolveFields(registryOrFields) {
835
+ if (!registryOrFields) return getRegisteredSettingFields();
836
+ if ("getFields" in registryOrFields && typeof registryOrFields.getFields === "function") {
837
+ return registryOrFields.getFields();
838
+ }
839
+ return registryOrFields;
840
+ }
841
+ async function openSettings(ctx, registryOrFields) {
842
+ const fields = resolveFields(registryOrFields);
843
+ const file = defaultConfigFile();
844
+ if (ctx.mode === "tui" && ctx.hasUI) {
845
+ await openSettingsDialog(ctx, fields, file);
846
+ return;
847
+ }
848
+ const config = loadProviderConfig(file);
849
+ if (!ctx.hasUI) {
850
+ const text = fields.map((f) => `${f.label}: ${fieldDisplayValue(config, f)}`).join("\n") + `
851
+ Edit ${file} to change.`;
852
+ if (ctx.hasUI) ctx.ui.notify(text, "info");
853
+ else console.log(text);
854
+ return;
855
+ }
856
+ let field = fields[0];
857
+ if (!field) return;
858
+ if (fields.length > 1) {
859
+ const labels = fields.map(
860
+ (f) => `${f.label} (current: ${fieldDisplayValue(config, f)})`
861
+ );
862
+ const choice = await ctx.ui.select("Antigravity settings", labels);
863
+ if (!choice) return;
864
+ const found = fields[labels.indexOf(choice)];
865
+ if (!found) return;
866
+ field = found;
867
+ }
868
+ const picked = await ctx.ui.select(
869
+ `${field.label} (current: ${fieldDisplayValue(config, field)})`,
870
+ [...field.options]
871
+ );
872
+ if (!picked || !field.options.includes(picked)) return;
873
+ if (await applySettingValue(field, picked, file, ctx)) {
874
+ ctx.ui.notify(`${field.label} set to ${picked}.`, "info");
875
+ } else {
876
+ ctx.ui.notify(writeFailedMessage(file), "error");
877
+ }
878
+ }
879
+ async function openSettingsDialog(ctx, fields, file) {
880
+ for (const field of fields) {
881
+ if (field.prepare) await field.prepare(ctx);
882
+ }
883
+ await ctx.ui.custom(async (tui, theme, _kb, done) => {
884
+ const config = loadProviderConfig(file);
885
+ const items = buildSettingsItems(config, fields);
886
+ const paintPreview = (field, value) => {
887
+ const item = items.find((i) => i.id === field.key);
888
+ if (!item) return;
889
+ const base = field.description ?? "";
890
+ const sample = field.renderPreview?.(value, ctx, theme);
891
+ const head = sample && base ? `${base}: ${sample}` : sample ?? base;
892
+ const note = field.optionNotes?.[value];
893
+ item.description = note ? `${head}
894
+ ${note}` : head;
895
+ };
896
+ for (const field of fields) {
897
+ const current = fieldDisplayValue(config, field);
898
+ paintPreview(field, current);
899
+ }
900
+ const list = new SettingsList(
901
+ items,
902
+ Math.min(items.length, 10),
903
+ await settingsListTheme(theme),
904
+ (id, newValue) => {
905
+ const field = fields.find((f) => f.key === id);
906
+ if (!field) return;
907
+ list.invalidate();
908
+ tui.requestRender();
909
+ void applySettingValue(field, newValue, file, ctx).then((saved) => {
910
+ if (!saved) {
911
+ list.updateValue(field.key, fieldDisplayValue(loadProviderConfig(file), field));
912
+ ctx.ui.notify(writeFailedMessage(file), "error");
913
+ list.invalidate();
914
+ tui.requestRender();
915
+ return;
916
+ }
917
+ paintPreview(field, newValue);
918
+ list.invalidate();
919
+ tui.requestRender();
920
+ });
921
+ },
922
+ () => done()
923
+ );
924
+ const border = {
925
+ render: (width) => [theme.fg("border", "\u2500".repeat(Math.max(1, width)))]
926
+ };
927
+ return {
928
+ render: (width) => [
929
+ ...border.render(width),
930
+ ...list.render(width),
931
+ ...border.render(width)
932
+ ],
933
+ handleInput: (data) => list.handleInput(data),
934
+ invalidate: () => list.invalidate()
935
+ };
936
+ });
937
+ }
938
+
939
+ // src/quota-status.ts
940
+ var FOOTER_MODE_NOTES = Object.freeze({
941
+ smart: "Picks whichever window runs out first (5h or weekly), weighting the weekly pool by a ratio learned from your usage",
942
+ all: "Lists every window of the pool backing the current model (5h or weekly)"
943
+ });
944
+ var FOOTER_MODE_OPTIONS = Object.freeze([
945
+ "off",
946
+ "smart",
947
+ "all"
948
+ ]);
949
+ function normalizeFooterMode(value) {
950
+ const v = typeof value === "string" ? value.trim().toLowerCase() : "";
951
+ return FOOTER_MODE_OPTIONS.includes(v) ? v : void 0;
952
+ }
953
+ function resolveFooterMode(config) {
954
+ return normalizeFooterMode(config?.settings?.quotaFooter) ?? "off";
955
+ }
699
956
  function renderProgressBar(fraction, width = 10) {
700
957
  const clamped = Math.max(0, Math.min(1, fraction));
701
958
  const filled = Math.round(clamped * width);
@@ -718,7 +975,9 @@ function shortWindowLabel(bucket, kind) {
718
975
  return bucket.displayName || bucket.bucketId || "Quota";
719
976
  }
720
977
  function sortQuotaBuckets(buckets) {
721
- return buckets.slice().sort((a, b) => QUOTA_WINDOW_RANK[classifyQuotaWindow(a)] - QUOTA_WINDOW_RANK[classifyQuotaWindow(b)]);
978
+ return buckets.slice().sort(
979
+ (a, b) => QUOTA_WINDOW_RANK[classifyQuotaWindow(a)] - QUOTA_WINDOW_RANK[classifyQuotaWindow(b)]
980
+ );
722
981
  }
723
982
  function formatResetRemaining(resetTime) {
724
983
  if (!resetTime) return null;
@@ -769,7 +1028,9 @@ function formatQuotaSummary(summary) {
769
1028
  const label = shortWindowLabel(b, classifyQuotaWindow(b)).padEnd(3);
770
1029
  const reset = formatResetRemaining(b.resetTime);
771
1030
  const suffix = reset && pct < 100 ? ` (${reset})` : "";
772
- lines.push(` ${label} ${renderProgressBar(b.remainingFraction)} ${String(pct).padStart(3)}% left${suffix}`);
1031
+ lines.push(
1032
+ ` ${label} ${renderProgressBar(b.remainingFraction)} ${String(pct).padStart(3)}% left${suffix}`
1033
+ );
773
1034
  }
774
1035
  }
775
1036
  return lines.join("\n");
@@ -799,7 +1060,11 @@ function extractWindowFractionPairs(summary) {
799
1060
  for (const g of summary?.groups || []) {
800
1061
  const bucket5h = windowBucket(g.buckets, "5h");
801
1062
  const bucketWeekly = windowBucket(g.buckets, "weekly");
802
- if (bucket5h && bucketWeekly) pairs[poolKey(bucket5h)] = { "5h": bucket5h.remainingFraction, weekly: bucketWeekly.remainingFraction };
1063
+ if (bucket5h && bucketWeekly)
1064
+ pairs[poolKey(bucket5h)] = {
1065
+ "5h": bucket5h.remainingFraction,
1066
+ weekly: bucketWeekly.remainingFraction
1067
+ };
803
1068
  }
804
1069
  return pairs;
805
1070
  }
@@ -855,25 +1120,79 @@ function formatQuotaWindowPart(bucket) {
855
1120
  }
856
1121
  var ANSI_RED = "\x1B[31m";
857
1122
  var ANSI_YELLOW = "\x1B[33m";
1123
+ var ANSI_DIM = "\x1B[90m";
858
1124
  var ANSI_FG_RESET = "\x1B[39m";
859
1125
  var QUOTA_WARN_PCT = 30;
860
1126
  var QUOTA_ALERT_PCT = 10;
861
- function colorizeQuotaFooter(footer) {
1127
+ function resolveColorizer(style) {
1128
+ if (typeof style === "function") return style;
1129
+ if (style && typeof style.fg === "function") return (tone, text) => style.fg(tone, text);
1130
+ return (tone, text) => {
1131
+ const code = tone === "error" ? ANSI_RED : tone === "warning" ? ANSI_YELLOW : ANSI_DIM;
1132
+ return `${code}${text}${ANSI_FG_RESET}`;
1133
+ };
1134
+ }
1135
+ function colorizeQuotaFooter(footer, style) {
862
1136
  if (!footer) return void 0;
1137
+ const color = resolveColorizer(style);
863
1138
  const match = footer.match(/(\d+)%/);
864
- if (!match) return footer;
1139
+ if (!match) return color("dim", footer);
865
1140
  const pct = parseInt(match[1], 10);
866
- if (pct <= QUOTA_ALERT_PCT) return `${ANSI_RED}${footer}${ANSI_FG_RESET}`;
867
- if (pct <= QUOTA_WARN_PCT) return `${ANSI_YELLOW}${footer}${ANSI_FG_RESET}`;
868
- return footer;
1141
+ if (pct <= QUOTA_ALERT_PCT) return color("error", footer);
1142
+ if (pct <= QUOTA_WARN_PCT) return color("warning", footer);
1143
+ return color("dim", footer);
869
1144
  }
870
- function colorizeQuotaFooterBoth(footer) {
1145
+ function colorizeQuotaFooterBoth(footer, style) {
871
1146
  if (!footer) return void 0;
872
- return footer.split(" \xB7 ").map((part) => colorizeQuotaFooter(part) ?? part).join(" \xB7 ");
1147
+ const color = resolveColorizer(style);
1148
+ const sep = color("dim", " \xB7 ");
1149
+ return footer.split(" \xB7 ").map((part) => colorizeQuotaFooter(part, style) ?? part).join(sep);
1150
+ }
1151
+ function previewQuotaFooterText(coord, modelId, mode, style) {
1152
+ if (!coord) return void 0;
1153
+ const normalized = normalizeFooterMode(mode);
1154
+ if (!normalized || normalized === "off") return void 0;
1155
+ const { colored } = coord.renderFooter(modelId, normalized, style);
1156
+ if (!colored) return void 0;
1157
+ return ANSI_FG_RESET + colored;
873
1158
  }
874
1159
  var QUOTA_STATUS_KEY = "pi-provider-antigravity-footer-usage";
875
1160
  var QUOTA_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
876
1161
  var MAX_OBSERVATION_AGE_MS = 60 * 60 * 1e3;
1162
+ function fileQuotaStatusStore(file = defaultConfigFile()) {
1163
+ return {
1164
+ loadMode: () => resolveFooterMode(loadProviderConfig(file)),
1165
+ loadQuotaState: () => loadSubsystemState("quota", file),
1166
+ saveQuotaState: (state) => saveSubsystemState("quota", state, file)
1167
+ };
1168
+ }
1169
+ function createQuotaFooterField(quotaStatus) {
1170
+ return {
1171
+ key: "quotaFooter",
1172
+ label: "Quota footer",
1173
+ description: "Show remaining quota in Pi's status footer",
1174
+ options: FOOTER_MODE_OPTIONS,
1175
+ defaultValue: "off",
1176
+ optionNotes: FOOTER_MODE_NOTES,
1177
+ renderPreview: (mode, ctx, theme) => {
1178
+ if (mode === "off") return "hidden";
1179
+ const activeTheme = theme || ctx.ui?.theme;
1180
+ return previewQuotaFooterText(quotaStatus, ctx.model?.id, mode, activeTheme);
1181
+ },
1182
+ prepare: async (ctx) => {
1183
+ await quotaStatus.ensurePreview(ctx);
1184
+ },
1185
+ onChange: async (ctx, _value) => {
1186
+ if (quotaStatus.mode() !== "off") {
1187
+ await quotaStatus.refresh(ctx, { ignoreMode: true });
1188
+ }
1189
+ quotaStatus.paint(ctx);
1190
+ }
1191
+ };
1192
+ }
1193
+ function registerQuotaSettings(quotaStatus) {
1194
+ registerSettingField(createQuotaFooterField(quotaStatus));
1195
+ }
877
1196
  function isFiniteNumber(value) {
878
1197
  return typeof value === "number" && Number.isFinite(value);
879
1198
  }
@@ -903,261 +1222,23 @@ var FOOTER_MODES = Object.freeze({
903
1222
  },
904
1223
  smart: {
905
1224
  key: "smart",
906
- note: "Picks whichever window runs out first (5h or weekly), weighting the weekly pool by a ratio learned from your usage",
907
- render: (summary, modelId, ratio = DEFAULT_WEEKLY_TO_5H_RATIO) => {
1225
+ note: FOOTER_MODE_NOTES.smart,
1226
+ render: (summary, modelId, ratio = DEFAULT_WEEKLY_TO_5H_RATIO, style) => {
908
1227
  if (!summary) return {};
909
1228
  const plain = buildQuotaFooter(summary, modelId, ratio);
910
- return { plain, colored: colorizeQuotaFooter(plain) };
1229
+ return { plain, colored: colorizeQuotaFooter(plain, style) };
911
1230
  }
912
1231
  },
913
1232
  all: {
914
1233
  key: "all",
915
- note: "Lists every window of the pool backing the current model (5h or weekly)",
916
- render: (summary, modelId) => {
1234
+ note: FOOTER_MODE_NOTES.all,
1235
+ render: (summary, modelId, _ratio, style) => {
917
1236
  if (!summary) return {};
918
1237
  const plain = buildQuotaFooterBoth(summary, modelId);
919
- return { plain, colored: colorizeQuotaFooterBoth(plain) };
1238
+ return { plain, colored: colorizeQuotaFooterBoth(plain, style) };
920
1239
  }
921
1240
  }
922
1241
  });
923
- var FOOTER_MODE_NOTES = Object.freeze({
924
- smart: FOOTER_MODES.smart.note,
925
- all: FOOTER_MODES.all.note
926
- });
927
- var FOOTER_MODE_OPTIONS = Object.freeze(
928
- Object.keys(FOOTER_MODES)
929
- );
930
- function normalizeFooterMode(value) {
931
- const v = typeof value === "string" ? value.trim().toLowerCase() : "";
932
- return v in FOOTER_MODES ? v : void 0;
933
- }
934
- var PROVIDER_CONFIG_FILE = "pi-provider-antigravity.json";
935
- function defaultConfigFile(env = process.env) {
936
- const dir = (env.PI_CODING_AGENT_DIR || "").trim() || path.join(os.homedir(), ".pi", "agent");
937
- return path.join(dir, PROVIDER_CONFIG_FILE);
938
- }
939
- function loadProviderConfig(file = defaultConfigFile()) {
940
- try {
941
- const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
942
- if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
943
- } catch {
944
- }
945
- return void 0;
946
- }
947
- function writeProviderConfigAtomically(file, data) {
948
- const tmp = `${file}.tmp-${process.pid}`;
949
- try {
950
- fs.mkdirSync(path.dirname(file), { recursive: true });
951
- const mode = fs.existsSync(file) ? fs.statSync(file).mode & 511 : void 0;
952
- const fd = fs.openSync(tmp, "w", mode);
953
- try {
954
- fs.writeFileSync(fd, JSON.stringify(data, null, 2) + "\n", "utf-8");
955
- fs.fsyncSync(fd);
956
- } finally {
957
- fs.closeSync(fd);
958
- }
959
- fs.renameSync(tmp, file);
960
- return true;
961
- } catch {
962
- try {
963
- fs.rmSync(tmp, { force: true });
964
- } catch {
965
- }
966
- return false;
967
- }
968
- }
969
- function updateProviderConfig(file, mutate) {
970
- try {
971
- let data = {};
972
- try {
973
- const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
974
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
975
- data = raw;
976
- } catch (err) {
977
- if (err?.code !== "ENOENT") return false;
978
- }
979
- mutate(data);
980
- return writeProviderConfigAtomically(file, data);
981
- } catch {
982
- return false;
983
- }
984
- }
985
- function resolveFooterMode(config) {
986
- return normalizeFooterMode(config?.settings?.quotaFooter) ?? "off";
987
- }
988
- function fileQuotaStatusStore(file = defaultConfigFile()) {
989
- return {
990
- loadMode: () => resolveFooterMode(loadProviderConfig(file)),
991
- loadQuotaState: () => {
992
- const quota = loadProviderConfig(file)?.states?.quota;
993
- return quota && typeof quota === "object" && !Array.isArray(quota) ? quota : void 0;
994
- },
995
- // Merges state into the existing file, preserving settings and unknown
996
- // keys. Never clobbers a file we couldn't parse.
997
- saveQuotaState: (state) => updateProviderConfig(file, (config) => {
998
- const states = config.states && typeof config.states === "object" && !Array.isArray(config.states) ? config.states : {};
999
- const quota = states.quota && typeof states.quota === "object" && !Array.isArray(states.quota) ? states.quota : {};
1000
- states.quota = { ...quota, ...state };
1001
- config.states = states;
1002
- })
1003
- };
1004
- }
1005
- var SETTINGS_FIELDS = Object.freeze([
1006
- {
1007
- key: "quotaFooter",
1008
- label: "Quota footer",
1009
- description: "Show remaining quota in Pi's status footer",
1010
- options: FOOTER_MODE_OPTIONS,
1011
- defaultValue: "off",
1012
- optionNotes: FOOTER_MODE_NOTES,
1013
- onChange: async (ctx, quotaStatus) => {
1014
- if (!quotaStatus) return;
1015
- if (quotaStatus.mode() !== "off") await quotaStatus.refresh(ctx, { ignoreMode: true });
1016
- quotaStatus.paint(ctx);
1017
- }
1018
- }
1019
- ]);
1020
- function fieldDisplayValue(config, field) {
1021
- const raw = config?.settings?.[field.key];
1022
- if (typeof raw === "string" && raw.trim()) return raw.trim();
1023
- return field.defaultValue ?? "unset";
1024
- }
1025
- function saveSettingValue(field, value, file) {
1026
- return updateProviderConfig(file, (config) => {
1027
- const settings = config.settings && typeof config.settings === "object" && !Array.isArray(config.settings) ? config.settings : {};
1028
- settings[field.key] = value;
1029
- config.settings = settings;
1030
- });
1031
- }
1032
- async function applySettingValue(field, value, file, ctx, quotaStatus) {
1033
- if (!saveSettingValue(field, value, file)) {
1034
- return false;
1035
- }
1036
- await field.onChange?.(ctx, quotaStatus);
1037
- return true;
1038
- }
1039
- function writeFailedMessage(file) {
1040
- return `Failed to write ${file}. If the file is not valid JSON, fix or delete it.`;
1041
- }
1042
- function previewQuotaFooterText(coord, modelId, mode) {
1043
- if (!coord) return void 0;
1044
- const normalized = normalizeFooterMode(mode);
1045
- if (!normalized || normalized === "off") return void 0;
1046
- const { colored } = coord.renderFooter(modelId, normalized);
1047
- if (!colored) return void 0;
1048
- return ANSI_FG_RESET + colored;
1049
- }
1050
- function buildSettingsItems(config, fields = SETTINGS_FIELDS) {
1051
- return fields.map((f) => ({
1052
- id: f.key,
1053
- label: f.label,
1054
- description: f.description,
1055
- currentValue: fieldDisplayValue(config, f),
1056
- values: [...f.options]
1057
- }));
1058
- }
1059
- async function settingsListTheme(theme) {
1060
- try {
1061
- const pi = await import("@earendil-works/pi-coding-agent");
1062
- return pi.getSettingsListTheme();
1063
- } catch {
1064
- return {
1065
- label: (text, selected) => selected ? theme.fg("accent", text) : text,
1066
- value: (text, selected) => selected ? theme.fg("accent", text) : theme.fg("muted", text),
1067
- description: (text) => theme.fg("dim", text),
1068
- cursor: theme.fg("accent", "\u2192 "),
1069
- hint: (text) => theme.fg("dim", text)
1070
- };
1071
- }
1072
- }
1073
- async function openSettings(ctx, quotaStatus) {
1074
- const file = defaultConfigFile();
1075
- if (ctx.mode === "tui" && ctx.hasUI) {
1076
- await openSettingsDialog(ctx, quotaStatus, file);
1077
- return;
1078
- }
1079
- const config = loadProviderConfig(file);
1080
- if (!ctx.hasUI) {
1081
- const text = SETTINGS_FIELDS.map((f) => `${f.label}: ${fieldDisplayValue(config, f)}`).join("\n") + `
1082
- Edit ${file} to change.`;
1083
- if (ctx.hasUI) ctx.ui.notify(text, "info");
1084
- else console.log(text);
1085
- return;
1086
- }
1087
- let field = SETTINGS_FIELDS[0];
1088
- if (!field) return;
1089
- if (SETTINGS_FIELDS.length > 1) {
1090
- const labels = SETTINGS_FIELDS.map(
1091
- (f) => `${f.label} (current: ${fieldDisplayValue(config, f)})`
1092
- );
1093
- const choice = await ctx.ui.select("Antigravity settings", labels);
1094
- if (!choice) return;
1095
- const found = SETTINGS_FIELDS[labels.indexOf(choice)];
1096
- if (!found) return;
1097
- field = found;
1098
- }
1099
- const picked = await ctx.ui.select(
1100
- `${field.label} (current: ${fieldDisplayValue(config, field)})`,
1101
- [...field.options]
1102
- );
1103
- if (!picked || !field.options.includes(picked)) return;
1104
- if (await applySettingValue(field, picked, file, ctx, quotaStatus)) {
1105
- ctx.ui.notify(`${field.label} set to ${picked}.`, "info");
1106
- } else {
1107
- ctx.ui.notify(writeFailedMessage(file), "error");
1108
- }
1109
- }
1110
- async function openSettingsDialog(ctx, quotaStatus, file) {
1111
- if (quotaStatus) await quotaStatus.ensurePreview(ctx);
1112
- await ctx.ui.custom(async (tui, theme, _kb, done) => {
1113
- const config = loadProviderConfig(file);
1114
- const items = buildSettingsItems(config);
1115
- const quotaField = SETTINGS_FIELDS.find((f) => f.key === "quotaFooter");
1116
- const paintPreview = (mode) => {
1117
- const item = items.find((i) => i.id === "quotaFooter");
1118
- if (!item) return;
1119
- const base = quotaField?.description ?? "";
1120
- const sample = mode === "off" ? "hidden" : previewQuotaFooterText(quotaStatus, ctx.model?.id, mode);
1121
- const head = sample && base ? `${base}: ${sample}` : sample ?? base;
1122
- const note = quotaField?.optionNotes?.[mode];
1123
- item.description = note ? `${head}
1124
- ${note}` : head;
1125
- };
1126
- paintPreview(resolveFooterMode(config));
1127
- const list = new SettingsList(
1128
- items,
1129
- Math.min(items.length, 10),
1130
- await settingsListTheme(theme),
1131
- (id, newValue) => {
1132
- const field = SETTINGS_FIELDS.find((f) => f.key === id);
1133
- if (!field) return;
1134
- list.invalidate();
1135
- tui.requestRender();
1136
- void applySettingValue(field, newValue, file, ctx, quotaStatus).then((saved) => {
1137
- if (!saved) {
1138
- list.updateValue(field.key, fieldDisplayValue(loadProviderConfig(file), field));
1139
- ctx.ui.notify(writeFailedMessage(file), "error");
1140
- list.invalidate();
1141
- tui.requestRender();
1142
- return;
1143
- }
1144
- paintPreview(newValue);
1145
- list.invalidate();
1146
- tui.requestRender();
1147
- });
1148
- },
1149
- () => done()
1150
- );
1151
- const border = {
1152
- render: (width) => [theme.fg("border", "\u2500".repeat(Math.max(1, width)))]
1153
- };
1154
- return {
1155
- render: (width) => [...border.render(width), ...list.render(width), ...border.render(width)],
1156
- handleInput: (data) => list.handleInput(data),
1157
- invalidate: () => list.invalidate()
1158
- };
1159
- });
1160
- }
1161
1242
  function isAntigravityModel(model) {
1162
1243
  return model?.provider === PROVIDER_ID;
1163
1244
  }
@@ -1187,8 +1268,8 @@ var QuotaStatusCoordinator = class {
1187
1268
  footerFor(modelId, mode = "smart") {
1188
1269
  return FOOTER_MODES[mode]?.render(this.summary, modelId, this.weeklyTo5hRatio).plain;
1189
1270
  }
1190
- renderFooter(modelId, mode = "smart") {
1191
- return FOOTER_MODES[mode]?.render(this.summary, modelId, this.weeklyTo5hRatio) ?? {};
1271
+ renderFooter(modelId, mode = "smart", style) {
1272
+ return FOOTER_MODES[mode]?.render(this.summary, modelId, this.weeklyTo5hRatio, style) ?? {};
1192
1273
  }
1193
1274
  get isFresh() {
1194
1275
  return !!this.summary && Date.now() - this.fetchedAt < QUOTA_REFRESH_INTERVAL_MS;
@@ -1218,7 +1299,8 @@ var QuotaStatusCoordinator = class {
1218
1299
  ctx.ui.setStatus(QUOTA_STATUS_KEY, void 0);
1219
1300
  return;
1220
1301
  }
1221
- const { colored } = this.renderFooter(ctx.model?.id, mode);
1302
+ const theme = ctx.ui?.theme;
1303
+ const { colored } = this.renderFooter(ctx.model?.id, mode, theme);
1222
1304
  ctx.ui.setStatus(QUOTA_STATUS_KEY, colored);
1223
1305
  }
1224
1306
  // Atomic inspect for /antigravity usage: forces fresh quota fetch, repaints
@@ -1231,10 +1313,6 @@ var QuotaStatusCoordinator = class {
1231
1313
  this.paint(ctx);
1232
1314
  return formatQuotaSummary(summary);
1233
1315
  }
1234
- // Opens provider settings dialog/menu and updates state atomically.
1235
- async openSettings(ctx) {
1236
- await openSettings(ctx, this);
1237
- }
1238
1316
  // One fetch for preview purposes even when the footer slot is off or the
1239
1317
  // current model belongs to another provider: opening quota settings is an
1240
1318
  // explicit look at quota (like /antigravity usage), and the preview is the
@@ -1270,7 +1348,11 @@ var QuotaStatusCoordinator = class {
1270
1348
  // Accepts an externally fetched summary into the shared cache: same
1271
1349
  // calibration, persistence, and freshness as fetch().
1272
1350
  ingest(summary) {
1273
- const calibrated = calibrateWeeklyTo5hRatio(this.lastPairs, extractWindowFractionPairs(summary), this.weeklyTo5hRatio);
1351
+ const calibrated = calibrateWeeklyTo5hRatio(
1352
+ this.lastPairs,
1353
+ extractWindowFractionPairs(summary),
1354
+ this.weeklyTo5hRatio
1355
+ );
1274
1356
  this.weeklyTo5hRatio = calibrated;
1275
1357
  this.lastPairs = extractWindowFractionPairs(summary);
1276
1358
  this.summary = summary;
@@ -2031,7 +2113,10 @@ var SUBCOMMANDS = Object.freeze([
2031
2113
  name: "settings",
2032
2114
  description: "Configure provider settings",
2033
2115
  aliases: Object.freeze(["setting"]),
2034
- run: async ({ ctx, quotaStatus }) => openSettings(ctx, quotaStatus)
2116
+ run: async ({ ctx, quotaStatus }) => {
2117
+ if (quotaStatus) registerQuotaSettings(quotaStatus);
2118
+ await openSettings(ctx);
2119
+ }
2035
2120
  },
2036
2121
  {
2037
2122
  name: "websearch",
@@ -2182,6 +2267,7 @@ async function runAntigravitySubcommand(args, ctx, quotaStatus, catalog) {
2182
2267
  var PROVIDER_NAME = "Antigravity";
2183
2268
  function index_default(pi) {
2184
2269
  const quotaStatus = new QuotaStatusCoordinator();
2270
+ registerQuotaSettings(quotaStatus);
2185
2271
  const catalog = createModelCatalog();
2186
2272
  pi.registerProvider(PROVIDER_ID, {
2187
2273
  name: PROVIDER_NAME,