pi-provider-antigravity 0.4.0 → 0.6.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/README.md CHANGED
@@ -95,7 +95,7 @@ Remaining percentages are automatically colorized to keep depletion visible with
95
95
  - **Low** (≤ 30%): Yellow
96
96
  - **Critical** (≤ 10%): Red
97
97
 
98
- In `both` mode, each window carries its own color based on its remaining percentage.
98
+ In `all` mode, each window carries its own color based on its remaining percentage.
99
99
 
100
100
  ### Display Modes
101
101
 
@@ -104,13 +104,27 @@ Switch between display modes at any time using `/antigravity settings`:
104
104
  | Mode | Example | Behavior |
105
105
  |---|---|---|
106
106
  | `off` *(default)* | *(hidden)* | Slot stays empty; zero background quota requests |
107
- | `single` | `Wk 6% (2d 14h)` | Shows only the primary bottleneck window |
108
- | `both` | `5h 90% (3h 54m) · Wk 6% (2d 14h)` | Shows both 5-hour and weekly windows side-by-side |
107
+ | `smart` | `Wk 6% (2d 14h)` | Shows the window that runs out first, 5h or weekly |
108
+ | `all` | `5h 90% (3h 54m) · Wk 6% (2d 14h)` | Shows every window of the pool backing the current model |
109
+
110
+ ### Footer placement
111
+
112
+ The extension never calls `setFooter` — it only publishes the slot text under the key `pi-provider-antigravity-footer-usage` via `setStatus`. Where that text renders is up to your footer; Pi's built-in one gives it its own line, below the default two.
113
+
114
+ To move it elsewhere (the right side of the first line, for example), ask Pi in a session and name the key:
115
+
116
+ > Move the `pi-provider-antigravity-footer-usage` slot to the right side of the first footer line, keeping the rest of the footer as it is.
117
+
118
+ Pi edits your own footer extension, creating one if you have none — based on
119
+ Pi's [complete custom-footer
120
+ example](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/examples/extensions/custom-footer.ts),
121
+ which Pi maintains. A custom footer replaces the built-in one, so Pi has to
122
+ redraw whichever lines you want to keep.
109
123
 
110
124
  ### Under the Hood
111
125
 
112
126
  - **Model-aware pool routing**: Automatically aligns with your active model. Selecting a Gemini model displays the Gemini quota pool; switching to Claude or GPT switches to the third-party pool. When using a non-Antigravity model or no model at all, the slot cleanly disappears.
113
- - **Intelligent bottleneck detection**: In `single` mode, the extension determines urgency using `min(r5h, rWk × R)` rather than a naive percentage comparison, accounting for total volume differences between the 5-hour and weekly pools. The volume multiplier `R` starts at 6.0 and automatically self-calibrates between 1.0 and 20.0 based on real usage deltas, persisting across restarts.
127
+ - **Bottleneck detection**: In `smart` mode, the extension determines urgency using `min(r5h, rWk × R)` rather than a naive percentage comparison, accounting for total volume differences between the 5-hour and weekly pools. The volume multiplier `R` starts at 6.0 and automatically self-calibrates between 1.0 and 20.0 based on real usage deltas, persisting across restarts.
114
128
  - **Battery- and network-friendly caching**: Idle sessions perform zero network requests. The slot repaints instantly from memory on turn completion, session launch, and model changes. Upstream quota queries are throttled to at most once every 5 minutes. Running `/antigravity usage` refreshes upstream data immediately.
115
129
 
116
130
  ## Configuration
@@ -136,7 +150,7 @@ Configuration is stored in `pi-provider-antigravity.json` alongside Pi's `settin
136
150
  ```json
137
151
  {
138
152
  "settings": {
139
- "quotaFooter": "single"
153
+ "quotaFooter": "smart"
140
154
  }
141
155
  }
142
156
  ```
@@ -147,7 +161,7 @@ Configuration is stored in `pi-provider-antigravity.json` alongside Pi's `settin
147
161
 
148
162
  | Key | Values | Default | Description |
149
163
  |---|---|---|---|
150
- | `quotaFooter` | `off`, `single`, `both` | `off` | Display remaining quota in Pi's status footer. See [Quota footer](#quota-footer). |
164
+ | `quotaFooter` | `off`, `smart`, `all` | `off` | Display remaining quota in Pi's status footer. See [Quota footer](#quota-footer). |
151
165
 
152
166
  ## Disclaimer
153
167
 
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { createHash, randomBytes } from "node:crypto";
3
3
 
4
4
  // src/protocol.ts
5
5
  var DEFAULT_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
6
- var DEFAULT_USER_AGENT = "antigravity/cli/1.1.27 (aidev_client; os_type=linux; arch=amd64; cl=976543523; auth_method=consumer)";
6
+ var DEFAULT_USER_AGENT = "antigravity/cli/1.1.28 (aidev_client; os_type=linux; arch=amd64; cl=978129418; auth_method=consumer)";
7
7
  var PROVIDER_ID = "antigravity";
8
8
  async function postAntigravity({
9
9
  token,
@@ -275,6 +275,19 @@ function updateCatalogStore(enums, runtimeIds, thinking = {}, deprecated = {}) {
275
275
  version: activeStore.version + 1
276
276
  };
277
277
  }
278
+ var lastCatalog;
279
+ function ingestCatalog(catalog) {
280
+ updateCatalogStore(
281
+ catalog.modelEnums,
282
+ catalog.models.map((m) => m.id),
283
+ buildThinkingMap(catalog.models),
284
+ catalog.deprecated || {}
285
+ );
286
+ lastCatalog = catalog;
287
+ }
288
+ function getStoredCatalog() {
289
+ return lastCatalog;
290
+ }
278
291
  function resolveRuntimeModelId(modelId, effort, availableRuntimeIds = activeStore.runtimeIds) {
279
292
  if (modelId.endsWith("-high") || modelId.endsWith("-medium") || modelId.endsWith("-low") || modelId.endsWith("-thinking") || modelId.endsWith("-agent") || modelId.endsWith("-extra-low") || modelId.endsWith("-tiered")) {
280
293
  return modelId;
@@ -490,20 +503,19 @@ async function refreshCatalog(context) {
490
503
  }
491
504
  const { token, projectId } = parseStoredCredentials(apiKey);
492
505
  const catalog = await fetchAvailableModelsCatalog(token, projectId, context.signal);
493
- const thinking = buildThinkingMap(catalog.models);
494
- const deprecated = catalog.deprecated || {};
495
- updateCatalogStore(catalog.modelEnums, catalog.models.map((m) => m.id), thinking, deprecated);
506
+ ingestCatalog(catalog);
496
507
  const dynamicModels = buildDynamicPublicModels(catalog);
497
508
  if (context.publish) {
509
+ const snap = getCatalogSnapshot();
498
510
  await context.publish({
499
511
  persist: {
500
512
  models: dynamicModels,
501
513
  checkedAt: Date.now(),
502
514
  "pi-provider-antigravity": {
503
- modelEnums: catalog.modelEnums,
504
- runtimeIds: catalog.models.map((m) => m.id),
505
- thinking,
506
- deprecated
515
+ modelEnums: { ...snap.enums },
516
+ runtimeIds: [...snap.runtimeIds],
517
+ thinking: { ...snap.thinking ?? {} },
518
+ deprecated: { ...snap.deprecated ?? {} }
507
519
  }
508
520
  }
509
521
  });
@@ -513,6 +525,14 @@ async function refreshCatalog(context) {
513
525
  return context.stored?.models || [];
514
526
  }
515
527
  }
528
+ async function refreshCatalogGeneration(doRefresh) {
529
+ const before = getCatalogSnapshot().version;
530
+ await doRefresh();
531
+ const catalog = getStoredCatalog();
532
+ if (!catalog) return { status: "failed" };
533
+ if (getCatalogSnapshot().version === before) return { status: "stale", catalog };
534
+ return { status: "fresh", catalog };
535
+ }
516
536
  function parseAvailableModels(data) {
517
537
  const models = [];
518
538
  const modelEnums = {};
@@ -758,7 +778,7 @@ function colorizeQuotaFooterBoth(footer) {
758
778
  if (!footer) return void 0;
759
779
  return footer.split(" \xB7 ").map((part) => colorizeQuotaFooter(part) ?? part).join(" \xB7 ");
760
780
  }
761
- var QUOTA_STATUS_KEY = "antigravity_quota";
781
+ var QUOTA_STATUS_KEY = "pi-provider-antigravity-footer-usage";
762
782
  var QUOTA_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
763
783
  var MAX_OBSERVATION_AGE_MS = 60 * 60 * 1e3;
764
784
  function isFiniteNumber(value) {
@@ -787,7 +807,7 @@ function paintQuotaStatus(coord, ctx) {
787
807
  const plain = coord.footerFor(ctx.model?.id, mode);
788
808
  ctx.ui.setStatus(
789
809
  QUOTA_STATUS_KEY,
790
- mode === "both" ? colorizeQuotaFooterBoth(plain) : colorizeQuotaFooter(plain)
810
+ mode === "all" ? colorizeQuotaFooterBoth(plain) : colorizeQuotaFooter(plain)
791
811
  );
792
812
  }
793
813
  var QuotaStatusCoordinator = class {
@@ -809,9 +829,9 @@ var QuotaStatusCoordinator = class {
809
829
  mode() {
810
830
  return this.store.loadMode();
811
831
  }
812
- footerFor(modelId, mode = "single") {
832
+ footerFor(modelId, mode = "smart") {
813
833
  if (!this.summary || mode === "off") return void 0;
814
- return mode === "both" ? buildQuotaFooterBoth(this.summary, modelId) : buildQuotaFooter(this.summary, modelId, this.weeklyTo5hRatio);
834
+ return mode === "all" ? buildQuotaFooterBoth(this.summary, modelId) : buildQuotaFooter(this.summary, modelId, this.weeklyTo5hRatio);
815
835
  }
816
836
  get isFresh() {
817
837
  return !!this.summary && Date.now() - this.fetchedAt < QUOTA_REFRESH_INTERVAL_MS;
@@ -832,12 +852,13 @@ var QuotaStatusCoordinator = class {
832
852
  await this.refresh(ctx);
833
853
  paintQuotaStatus(this, ctx);
834
854
  }
835
- // One fetch for preview purposes even when the footer slot is off: opening
836
- // quota settings is an explicit look at quota.
855
+ // One fetch for preview purposes even when the footer slot is off or the
856
+ // current model belongs to another provider: opening quota settings is an
857
+ // explicit look at quota (like /antigravity usage), and the preview is the
858
+ // only place that quota is visible while a foreign model is selected.
837
859
  ensurePreview(ctx) {
838
860
  if (this.summary) return Promise.resolve(this.summary);
839
861
  if (this.inflight) return this.inflight;
840
- if (!isAntigravityModel(ctx.model)) return Promise.resolve(void 0);
841
862
  return this.fetchOnce(ctx);
842
863
  }
843
864
  fetchOnce(ctx, signal) {
@@ -928,7 +949,7 @@ function saveProviderConfig(file, data) {
928
949
  }
929
950
  function normalizeFooterMode(value) {
930
951
  const v = typeof value === "string" ? value.trim().toLowerCase() : "";
931
- return v === "off" || v === "single" || v === "both" ? v : void 0;
952
+ return v === "off" || v === "smart" || v === "all" ? v : void 0;
932
953
  }
933
954
  function resolveFooterMode(config) {
934
955
  return normalizeFooterMode(config?.settings?.quotaFooter) ?? "off";
@@ -968,13 +989,16 @@ var SETTINGS_FIELDS = Object.freeze([
968
989
  key: "quotaFooter",
969
990
  label: "Quota footer",
970
991
  description: "Show remaining quota in the footer",
971
- options: ["off", "single", "both"],
992
+ options: ["off", "smart", "all"],
972
993
  defaultValue: "off",
994
+ optionNotes: {
995
+ smart: "Picks whichever window runs out first (5h or weekly), weighting the weekly pool by a ratio learned from your usage",
996
+ all: "Lists every window of the pool backing the current model (5h or weekly)"
997
+ },
973
998
  onChange: async (ctx, quotaStatus) => {
974
- if (quotaStatus) {
975
- await quotaStatus.refresh(ctx);
976
- paintQuotaStatus(quotaStatus, ctx);
977
- }
999
+ if (!quotaStatus) return;
1000
+ if (quotaStatus.mode() !== "off") await quotaStatus.refresh(ctx, { ignoreMode: true });
1001
+ paintQuotaStatus(quotaStatus, ctx);
978
1002
  }
979
1003
  }
980
1004
  ]);
@@ -997,9 +1021,10 @@ async function applySettingValue(field, value, file, ctx, quotaStatus) {
997
1021
  }
998
1022
  function previewQuotaFooterText(coord, modelId, mode) {
999
1023
  if (!coord || mode === "off") return void 0;
1000
- const plain = coord.footerFor(modelId, mode === "both" ? "both" : "single");
1024
+ const plain = coord.footerFor(modelId, mode === "all" ? "all" : "smart");
1001
1025
  if (!plain) return void 0;
1002
- return mode === "both" ? colorizeQuotaFooterBoth(plain) : colorizeQuotaFooter(plain);
1026
+ const colored = mode === "all" ? colorizeQuotaFooterBoth(plain) : colorizeQuotaFooter(plain);
1027
+ return ANSI_FG_RESET + colored;
1003
1028
  }
1004
1029
  function buildSettingsItems(config, fields = SETTINGS_FIELDS) {
1005
1030
  return fields.map((f) => ({
@@ -1081,7 +1106,10 @@ async function openSettingsDialog(ctx, quotaStatus, file) {
1081
1106
  if (!item) return;
1082
1107
  const base = quotaField?.description ?? "";
1083
1108
  const sample = mode === "off" ? "hidden" : previewQuotaFooterText(quotaStatus, ctx.model?.id, mode);
1084
- item.description = sample && base ? `${base}: ${sample}` : sample ?? base;
1109
+ const head = sample && base ? `${base}: ${sample}` : sample ?? base;
1110
+ const note = quotaField?.optionNotes?.[mode];
1111
+ item.description = note ? `${head}
1112
+ ${note}` : head;
1085
1113
  };
1086
1114
  paintPreview(resolveFooterMode(config));
1087
1115
  const list = new SettingsList(
@@ -1153,12 +1181,12 @@ function createSseFeed() {
1153
1181
  };
1154
1182
  const closeOpenBlock = (events) => {
1155
1183
  if (openType === null) return;
1156
- const index = content.length - 1;
1157
- const block = content[index];
1184
+ const contentIndex = content.length - 1;
1185
+ const block = content[contentIndex];
1158
1186
  if (block?.type === "thinking") {
1159
- events.push({ kind: "thinking_end", index, content: block.thinking });
1187
+ events.push({ type: "thinking_end", contentIndex, content: block.thinking });
1160
1188
  } else if (block?.type === "text") {
1161
- events.push({ kind: "text_end", index, content: block.text });
1189
+ events.push({ type: "text_end", contentIndex, content: block.text });
1162
1190
  }
1163
1191
  openType = null;
1164
1192
  };
@@ -1201,7 +1229,7 @@ function createSseFeed() {
1201
1229
  if (openType !== "thinking") {
1202
1230
  closeOpenBlock(events);
1203
1231
  content.push({ type: "thinking", thinking: "" });
1204
- events.push({ kind: "thinking_start", index: content.length - 1 });
1232
+ events.push({ type: "thinking_start", contentIndex: content.length - 1 });
1205
1233
  openType = "thinking";
1206
1234
  }
1207
1235
  const block = content[content.length - 1];
@@ -1212,10 +1240,9 @@ function createSseFeed() {
1212
1240
  block.thinkingSignature = part.thoughtSignature;
1213
1241
  }
1214
1242
  events.push({
1215
- kind: "thinking_delta",
1216
- index: content.length - 1,
1217
- delta,
1218
- ...part.thoughtSignature ? { thinkingSignature: part.thoughtSignature } : {}
1243
+ type: "thinking_delta",
1244
+ contentIndex: content.length - 1,
1245
+ delta
1219
1246
  });
1220
1247
  } else if (part.functionCall) {
1221
1248
  closeOpenBlock(events);
@@ -1228,18 +1255,25 @@ function createSseFeed() {
1228
1255
  thoughtSignature: part.thoughtSignature || lastThoughtSignature
1229
1256
  };
1230
1257
  content.push(block);
1231
- events.push({ kind: "toolCall", index: content.length - 1, block });
1258
+ const contentIndex = content.length - 1;
1259
+ events.push({ type: "toolcall_start", contentIndex });
1260
+ events.push({
1261
+ type: "toolcall_delta",
1262
+ contentIndex,
1263
+ delta: JSON.stringify(block.arguments)
1264
+ });
1265
+ events.push({ type: "toolcall_end", contentIndex, toolCall: block });
1232
1266
  } else if (part.text !== void 0) {
1233
1267
  if (openType !== "text") {
1234
1268
  closeOpenBlock(events);
1235
1269
  content.push({ type: "text", text: "" });
1236
- events.push({ kind: "text_start", index: content.length - 1 });
1270
+ events.push({ type: "text_start", contentIndex: content.length - 1 });
1237
1271
  openType = "text";
1238
1272
  }
1239
1273
  const block = content[content.length - 1];
1240
1274
  if (block?.type !== "text") continue;
1241
1275
  block.text += part.text;
1242
- events.push({ kind: "text_delta", index: content.length - 1, delta: part.text });
1276
+ events.push({ type: "text_delta", contentIndex: content.length - 1, delta: part.text });
1243
1277
  }
1244
1278
  }
1245
1279
  };
@@ -1266,6 +1300,7 @@ function createSseFeed() {
1266
1300
  // src/builder.ts
1267
1301
  import { createHash as createHash2, randomUUID } from "node:crypto";
1268
1302
  var BASE64_SIGNATURE_PATTERN = /^[A-Za-z0-9+/_-]+={0,2}$/;
1303
+ var SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator";
1269
1304
  function isValidThoughtSignature(sig) {
1270
1305
  if (typeof sig !== "string" || sig.length === 0) return false;
1271
1306
  if (sig.includes("=") && sig.length % 4 !== 0) return false;
@@ -1297,12 +1332,12 @@ function resolveSessionTrajectory(sessionId) {
1297
1332
  const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
1298
1333
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1299
1334
  }
1335
+ var AGY_SESSION_ID = "-3750763034362895579";
1300
1336
  function resolveNumericSessionId(sessionId) {
1301
1337
  if (/^-?\d+$/.test(sessionId)) {
1302
1338
  return sessionId;
1303
1339
  }
1304
- const hash = createHash2("sha256").update(`antigravity:session:${sessionId}`).digest();
1305
- return hash.readBigInt64BE(0).toString();
1340
+ return AGY_SESSION_ID;
1306
1341
  }
1307
1342
  function normalizeToolCallId(id) {
1308
1343
  if (!id || typeof id !== "string") return id;
@@ -1375,6 +1410,7 @@ function convertTools(tools) {
1375
1410
  function translateTurnTrace(context, runtimeModelId) {
1376
1411
  const contents = [];
1377
1412
  const messages = context.messages || [];
1413
+ const isGeminiRequest = classifyModelFamily(runtimeModelId) === "gemini";
1378
1414
  for (const msg of messages) {
1379
1415
  if (msg.role === "user") {
1380
1416
  const parts = [];
@@ -1468,6 +1504,8 @@ function translateTurnTrace(context, runtimeModelId) {
1468
1504
  if (sig) {
1469
1505
  part.thoughtSignature = sig;
1470
1506
  pendingThinkingSig = void 0;
1507
+ } else if (isGeminiRequest && !isSameProviderAndModel) {
1508
+ part.thoughtSignature = SKIP_THOUGHT_SIGNATURE_VALIDATOR;
1471
1509
  }
1472
1510
  parts.push(part);
1473
1511
  }
@@ -1673,11 +1711,12 @@ function streamAntigravity(model, context, options) {
1673
1711
  const reader = res.body.getReader();
1674
1712
  const decoder = new TextDecoder();
1675
1713
  const feed = createSseFeed();
1676
- const applyUsage = (u) => {
1677
- output.usage.input = u.input;
1678
- output.usage.cacheRead = u.cacheRead;
1679
- output.usage.output = u.output;
1680
- output.usage.totalTokens = u.total;
1714
+ const applyFeedOutput = (fed) => {
1715
+ output.content = fed.content;
1716
+ output.usage.input = fed.usage.input;
1717
+ output.usage.cacheRead = fed.usage.cacheRead;
1718
+ output.usage.output = fed.usage.output;
1719
+ output.usage.totalTokens = fed.usage.total;
1681
1720
  if (model.cost) {
1682
1721
  const c = model.cost;
1683
1722
  const inputCost = output.usage.input * (c.input || 0) / 1e6;
@@ -1691,51 +1730,10 @@ function streamAntigravity(model, context, options) {
1691
1730
  total: inputCost + outputCost + cacheCost
1692
1731
  };
1693
1732
  }
1694
- };
1695
- const translate = (events) => {
1696
- for (const ev of events) {
1697
- if (ev.kind === "text_start" || ev.kind === "thinking_start") {
1698
- const isThinking = ev.kind === "thinking_start";
1699
- stream.push(
1700
- isThinking ? { type: "thinking_start", contentIndex: ev.index, partial: output } : { type: "text_start", contentIndex: ev.index, partial: output }
1701
- );
1702
- } else if (ev.kind === "text_delta" || ev.kind === "thinking_delta") {
1703
- stream.push(
1704
- ev.kind === "text_delta" ? { type: "text_delta", contentIndex: ev.index, delta: ev.delta, partial: output } : { type: "thinking_delta", contentIndex: ev.index, delta: ev.delta, partial: output }
1705
- );
1706
- } else if (ev.kind === "text_end" || ev.kind === "thinking_end") {
1707
- stream.push(
1708
- ev.kind === "text_end" ? { type: "text_end", contentIndex: ev.index, content: ev.content, partial: output } : { type: "thinking_end", contentIndex: ev.index, content: ev.content, partial: output }
1709
- );
1710
- } else if (ev.kind === "toolCall") {
1711
- const toolCall = {
1712
- type: "toolCall",
1713
- id: ev.block.id,
1714
- name: ev.block.name,
1715
- arguments: ev.block.arguments || {},
1716
- thoughtSignature: ev.block.thoughtSignature
1717
- };
1718
- stream.push({ type: "toolcall_start", contentIndex: ev.index, partial: output });
1719
- stream.push({
1720
- type: "toolcall_delta",
1721
- contentIndex: ev.index,
1722
- delta: JSON.stringify(toolCall.arguments),
1723
- partial: output
1724
- });
1725
- stream.push({
1726
- type: "toolcall_end",
1727
- contentIndex: ev.index,
1728
- toolCall,
1729
- partial: output
1730
- });
1731
- }
1732
- }
1733
- };
1734
- const applyFeedOutput = (fed) => {
1735
- output.content = fed.content;
1736
- applyUsage(fed.usage);
1737
1733
  output.stopReason = fed.stopReason;
1738
- translate(fed.events);
1734
+ for (const ev of fed.events) {
1735
+ stream.push({ ...ev, partial: output });
1736
+ }
1739
1737
  };
1740
1738
  while (true) {
1741
1739
  const { done, value } = await reader.read();
@@ -1762,29 +1760,32 @@ function streamAntigravity(model, context, options) {
1762
1760
 
1763
1761
  // src/commands.ts
1764
1762
  var USAGE_TEXT = "Usage: /antigravity <command>\n\nCommands:\n usage Show 5h and weekly quota pool limits\n models List recommended models with context window and remaining quota (alias: model)\n refresh Force refresh model catalog\n settings Pick provider settings: Enter cycles values (alias: setting)\n login Run /login antigravity";
1765
- var FETCH_SUBCOMMANDS = {
1766
- models: {
1767
- progress: "Fetching available models\u2026",
1768
- errLabel: "models",
1769
- run: (token, projectId, signal) => fetchAvailableModelsCatalog(token, projectId, signal).then(formatModelsList)
1770
- }
1771
- };
1772
1763
  async function resolveToken(ctx) {
1773
1764
  const apiKey = await ctx.modelRegistry?.getApiKeyForProvider?.(PROVIDER_ID);
1774
1765
  if (!apiKey) return null;
1775
1766
  return parseStoredCredentials(apiKey);
1776
1767
  }
1777
- async function runWithToken(ctx, cmd) {
1778
- const creds = await resolveToken(ctx);
1779
- if (!creds) {
1768
+ async function runModelsSubcommand(ctx) {
1769
+ if (await resolveToken(ctx) === null) {
1780
1770
  emitOutput(ctx, "Not logged in. Run /login antigravity first.", "warning");
1781
1771
  return;
1782
1772
  }
1783
1773
  try {
1784
- if (ctx.hasUI) ctx.ui.notify(cmd.progress, "info");
1785
- emitOutput(ctx, await cmd.run(creds.token, creds.projectId, ctx.signal));
1774
+ if (ctx.hasUI) ctx.ui.notify("Fetching available models\u2026", "info");
1775
+ const { status, catalog } = await refreshCatalogGeneration(
1776
+ () => ctx.modelRegistry?.refresh?.({ force: true, providers: [PROVIDER_ID], signal: ctx.signal })
1777
+ );
1778
+ if (status === "failed" || !catalog) {
1779
+ throw new Error("no new generation received");
1780
+ }
1781
+ if (status === "stale") {
1782
+ emitOutput(ctx, "Failed to refresh models, showing last known list.", "warning");
1783
+ emitOutput(ctx, formatModelsList(catalog));
1784
+ return;
1785
+ }
1786
+ emitOutput(ctx, formatModelsList(catalog));
1786
1787
  } catch (err) {
1787
- emitOutput(ctx, `Failed to fetch ${cmd.errLabel}: ${err.message}`, "error");
1788
+ emitOutput(ctx, `Failed to fetch models: ${err.message}`, "error");
1788
1789
  }
1789
1790
  }
1790
1791
  function parseAntigravitySubcommand(args) {
@@ -1822,9 +1823,8 @@ async function runAntigravitySubcommand(args, ctx, quotaStatus) {
1822
1823
  await runUsageSubcommand(ctx, quotaStatus);
1823
1824
  return;
1824
1825
  }
1825
- const fetchCmd = FETCH_SUBCOMMANDS[sub];
1826
- if (fetchCmd) {
1827
- await runWithToken(ctx, fetchCmd);
1826
+ if (sub === "models") {
1827
+ await runModelsSubcommand(ctx);
1828
1828
  return;
1829
1829
  }
1830
1830
  if (sub === "settings") {
@@ -1834,8 +1834,16 @@ async function runAntigravitySubcommand(args, ctx, quotaStatus) {
1834
1834
  if (sub === "refresh") {
1835
1835
  try {
1836
1836
  if (ctx.hasUI) ctx.ui.notify("Refreshing models\u2026", "info");
1837
- await ctx.modelRegistry?.refresh?.({ force: true, providers: [PROVIDER_ID], signal: ctx.signal });
1838
- emitOutput(ctx, "Antigravity models refreshed successfully.");
1837
+ const { status } = await refreshCatalogGeneration(
1838
+ () => ctx.modelRegistry?.refresh?.({ force: true, providers: [PROVIDER_ID], signal: ctx.signal })
1839
+ );
1840
+ if (status === "fresh") {
1841
+ emitOutput(ctx, "Antigravity models refreshed successfully.");
1842
+ } else if (status === "stale") {
1843
+ emitOutput(ctx, "Failed to refresh models, keeping last known list.", "warning");
1844
+ } else {
1845
+ emitOutput(ctx, "Failed to refresh models: no new generation received.", "error");
1846
+ }
1839
1847
  } catch (err) {
1840
1848
  emitOutput(ctx, `Failed to refresh: ${err.message}`, "error");
1841
1849
  }