agy-powerline 1.1.0 โ†’ 1.2.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/src/lib/git.js CHANGED
@@ -4,6 +4,14 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
  const { logWarning } = require("./log");
6
6
  const { firstText } = require("./formatters");
7
+ const { writeFileAtomic, PRIVATE_FILE_MODE } = require("./paths");
8
+
9
+ // Ahead/behind counts depend only on the two commits compared, so they are
10
+ // kept per commit pair: git runs again only when the branch or its upstream
11
+ // moves. Starting git costs ~60 ms on Windows, on every refresh otherwise.
12
+ const SYNC_CACHE_NAME = ".git-sync-cache.json";
13
+ const SYNC_CACHE_MAX = 20;
14
+ const COMMIT = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
7
15
 
8
16
  // Only an absolute folder that exists can belong to the user's session. A
9
17
  // relative path (a Windows path read on Linux, for one) resolves against
@@ -217,6 +225,54 @@ function upstreamRef(commonDir, branch) {
217
225
  return remote === "." ? merge : `refs/remotes/${remote}/${tracked}`;
218
226
  }
219
227
 
228
+ // Only next to a real install's config (like the exec cache); CLI previews
229
+ // and other copies don't write it.
230
+ function syncCachePath() {
231
+ try {
232
+ const { getConfigDir } = require("./config");
233
+ const dir = getConfigDir();
234
+ return dir && fs.existsSync(path.join(dir, "statusline.config.json"))
235
+ ? path.join(dir, SYNC_CACHE_NAME)
236
+ : null;
237
+ } catch {
238
+ return null;
239
+ }
240
+ }
241
+
242
+ function readSyncCache(file) {
243
+ try {
244
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
245
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {};
246
+ } catch {
247
+ return {};
248
+ }
249
+ }
250
+
251
+ function cachedSync(file, key) {
252
+ const value = file ? readSyncCache(file)[key] : null;
253
+ const valid =
254
+ Array.isArray(value) &&
255
+ value.length === 2 &&
256
+ value.every((n) => Number.isInteger(n) && n >= 0 && n <= 1e7);
257
+ return valid ? { ahead: value[0], behind: value[1] } : null;
258
+ }
259
+
260
+ function saveSync(file, key, sync) {
261
+ if (!file) {
262
+ return;
263
+ }
264
+ try {
265
+ // Newest last; the oldest pairs go once there are more than SYNC_CACHE_MAX.
266
+ const cache = readSyncCache(file);
267
+ delete cache[key];
268
+ cache[key] = [sync.ahead, sync.behind];
269
+ const kept = Object.fromEntries(Object.entries(cache).slice(-SYNC_CACHE_MAX));
270
+ writeFileAtomic(file, JSON.stringify(kept), { mode: PRIVATE_FILE_MODE });
271
+ } catch {
272
+ // Best effort: the next refresh runs git again.
273
+ }
274
+ }
275
+
220
276
  function getGitSync(gitDir, branch) {
221
277
  if (!gitDir || !isSafeBranchName(branch)) {
222
278
  return { ahead: 0, behind: 0 };
@@ -232,10 +288,19 @@ function getGitSync(gitDir, branch) {
232
288
  return { ahead: 0, behind: 0 };
233
289
  }
234
290
 
235
- if (readRef(commonDir, upstream) === localHash) {
291
+ const upstreamHash = readRef(commonDir, upstream);
292
+ if (upstreamHash === localHash) {
236
293
  return { ahead: 0, behind: 0 };
237
294
  }
238
295
 
296
+ const key =
297
+ COMMIT.test(localHash) && COMMIT.test(upstreamHash) ? `${localHash}..${upstreamHash}` : "";
298
+ const cacheFile = key ? syncCachePath() : null;
299
+ const cached = cachedSync(cacheFile, key);
300
+ if (cached) {
301
+ return cached;
302
+ }
303
+
239
304
  const { execFileSync } = require("child_process");
240
305
  const out = execFileSync(
241
306
  "git",
@@ -253,10 +318,12 @@ function getGitSync(gitDir, branch) {
253
318
 
254
319
  const parts = out.split(/\s+/);
255
320
  if (parts.length >= 2) {
256
- return {
321
+ const sync = {
257
322
  ahead: parseInt(parts[0], 10) || 0,
258
323
  behind: parseInt(parts[1], 10) || 0,
259
324
  };
325
+ saveSync(cacheFile, key, sync);
326
+ return sync;
260
327
  }
261
328
  } catch (err) {
262
329
  // A timeout or missing git is worth knowing about when ahead/behind
@@ -327,4 +394,5 @@ module.exports = {
327
394
  gitHasDirtyMarkers,
328
395
  vcsFromPayload,
329
396
  resolveGit,
397
+ SYNC_CACHE_NAME,
330
398
  };
@@ -292,6 +292,12 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
292
292
  case "thirdPartyWeekly":
293
293
  return session.thirdPartyWeekly ? quotaMeter(session.thirdPartyWeekly) : null;
294
294
 
295
+ case "argonWeekly":
296
+ return session.argonWeekly ? quotaMeter(session.argonWeekly) : null;
297
+
298
+ case "argonReset":
299
+ return quotaReset(session.argonWeekly, true);
300
+
295
301
  case "context": {
296
302
  const { thresh, blinkSeq, color } = contextState();
297
303
  return (
@@ -312,6 +312,48 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
312
312
  );
313
313
  }
314
314
 
315
+ // Argon is a Gemini model, so its quota takes the Gemini 7d color; the
316
+ // ๐Ÿ”ฎ tag tells the two apart.
317
+ case "argonWeekly": {
318
+ if (!session.argonWeekly) {
319
+ return null;
320
+ }
321
+ const thresh = resolveQuotaThresholds(widget, config);
322
+ const rem = session.argonWeekly.remaining;
323
+ const level = rem <= thresh.crit ? "crit" : rem <= thresh.warn ? "warn" : "normal";
324
+ const blink = !ascii && shouldBlink(level, thresh.blink);
325
+ const color = quotaColor(rem, colors.weekly, {
326
+ crit: thresh.crit,
327
+ warn: thresh.warn,
328
+ critColor: colors.warn,
329
+ warnColor: colors.caution,
330
+ });
331
+ return meterSegment(
332
+ color,
333
+ rem,
334
+ meterLabel(),
335
+ `${close} ${rem.toFixed(1)}% rem `,
336
+ true,
337
+ blink,
338
+ pace(session.argonWeekly)
339
+ );
340
+ }
341
+
342
+ case "argonReset": {
343
+ const text = session.argonWeekly
344
+ ? formatResetText(session.argonWeekly, {
345
+ format: widget.format,
346
+ fallback: "countdown",
347
+ weekly: true,
348
+ clock: config.clock,
349
+ })
350
+ : "";
351
+ if (!text) {
352
+ return null;
353
+ }
354
+ return pill(colors.weekly, lbl(text));
355
+ }
356
+
315
357
  case "context": {
316
358
  const thresh = resolveContextThresholds(widget, config);
317
359
  const level =
package/src/lib/log.js CHANGED
@@ -28,6 +28,28 @@ const IN_REPO = path.basename(__dirname) === "lib";
28
28
  let logDir = null;
29
29
  let context = {};
30
30
 
31
+ // The installed statusline.js is the runtime files joined into one (see
32
+ // src/cli/bundle.js). [first line, file name] for each of them, so a stack
33
+ // frame can still name the original file and line.
34
+ let bundleLines = null;
35
+
36
+ function setBundleLines(lines) {
37
+ bundleLines = Array.isArray(lines) ? lines : null;
38
+ }
39
+
40
+ function originalFrame(rel, line) {
41
+ if (!bundleLines || rel !== "statusline.js") {
42
+ return `${rel}:${line}`;
43
+ }
44
+ let match = null;
45
+ for (const entry of bundleLines) {
46
+ if (entry[0] <= line) {
47
+ match = entry;
48
+ }
49
+ }
50
+ return match ? `${match[1]}:${line - match[0] + 1}` : `${rel}:${line}`;
51
+ }
52
+
31
53
  // The copied runtime has no package.json; init records the version in
32
54
  // install.json instead.
33
55
  function agyPowerlineVersion() {
@@ -139,7 +161,7 @@ function ourFrames(stack) {
139
161
  continue;
140
162
  }
141
163
  const fn = match[1] && /^[\w$.<> ]{1,60}$/.test(match[1]) ? `${match[1]} ` : "";
142
- frames.push(`${fn}${rel.split(path.sep).join("/")}:${match[3]}`);
164
+ frames.push(`${fn}${originalFrame(rel.split(path.sep).join("/"), Number(match[3]))}`);
143
165
  if (frames.length >= 8) {
144
166
  break;
145
167
  }
@@ -283,4 +305,5 @@ module.exports = {
283
305
  logError,
284
306
  logWarning,
285
307
  clearLog,
308
+ setBundleLines,
286
309
  };
@@ -26,7 +26,7 @@ function sampleData() {
26
26
  input_tokens: 63382,
27
27
  output_tokens: 346,
28
28
  cache_creation_input_tokens: 0,
29
- cache_read_input_tokens: 20857,
29
+ cache_read_input_tokens: 85935,
30
30
  },
31
31
  },
32
32
  exceeds_200k_tokens: false,
@@ -53,6 +53,11 @@ function sampleData() {
53
53
  reset_time: weeklyReset.toISOString(),
54
54
  reset_in_seconds: 86400 + 17 * 3600,
55
55
  },
56
+ "gemini-argon-wtu-weekly": {
57
+ remaining_fraction: 0.64,
58
+ reset_time: new Date(now + 4 * 86400 * 1000 + 9 * 3600 * 1000).toISOString(),
59
+ reset_in_seconds: 4 * 86400 + 9 * 3600,
60
+ },
56
61
  },
57
62
  agent_state: "thinking",
58
63
  vcs: {
package/src/lib/paths.js CHANGED
@@ -8,7 +8,8 @@ const AGY_SETTINGS = path.join(os.homedir(), ".gemini", "antigravity-cli", "sett
8
8
 
9
9
  const INSTALL_META_NAME = "install.json";
10
10
 
11
- // Library files init copies next to statusline.js (paths relative to src/lib).
11
+ // Library files the runtime needs (paths relative to src/lib). init joins them
12
+ // with src/runtime/statusline.js into the one installed statusline.js.
12
13
  const RUNTIME_FILES = [
13
14
  "shared.js",
14
15
  "colors.js",
@@ -35,13 +36,16 @@ const PRIVATE_FILES = [
35
36
  "statusline.config.json",
36
37
  ".exec-cache.json",
37
38
  "last-payload.json",
39
+ // SYNC_CACHE_NAME in git.js: commit IDs of the repos you work in.
40
+ ".git-sync-cache.json",
38
41
  // LOG_NAME in log.js, spelled out because log.js requires this file.
39
42
  "agy-powerline.log",
40
43
  ];
41
44
  const PRIVATE_FILE_MODE = 0o600;
42
45
 
43
46
  // Everything init or the runtime may create in the install dir. uninstall
44
- // --delete removes only these, never the whole directory tree.
47
+ // --delete removes only these, never the whole directory tree. RUNTIME_FILES
48
+ // are listed for installs from 1.1.0 and earlier, which copied them separately.
45
49
  const INSTALLED_FILES = [
46
50
  "statusline.js",
47
51
  "statusline.cmd",
@@ -6,10 +6,15 @@ const { resolveGit } = require("./git");
6
6
 
7
7
  const FIVE_HOURS = 5 * 3600;
8
8
  const SEVEN_DAYS = 7 * 86400;
9
+ const ARGON_BUCKET = "gemini-argon-wtu-weekly";
9
10
  // The buckets Antigravity sends, with the short names the bar shows.
10
11
  const QUOTA_BUCKETS = {
11
12
  "gemini-5h": { label: "5h", window: FIVE_HOURS },
12
13
  "gemini-weekly": { label: "7d", window: SEVEN_DAYS },
14
+ // Gemini 4 Argon on Google AI Pro: a weekly quota of its own, with no 5-hour
15
+ // window, sent whatever model is in use. On Ultra Argon counts toward the
16
+ // Gemini buckets and this one isn't sent.
17
+ [ARGON_BUCKET]: { label: "Argon 7d", window: SEVEN_DAYS },
13
18
  "3p-5h": { label: "3P 5h", window: FIVE_HOURS },
14
19
  "3p-weekly": { label: "3P 7d", window: SEVEN_DAYS },
15
20
  };
@@ -68,8 +73,21 @@ function readQuota(data, key, windowSeconds) {
68
73
  };
69
74
  }
70
75
 
71
- // The bucket closest to running out for the model in use: gemini-* buckets
72
- // for Gemini models, 3p-* (third-party) buckets for the others. Without a
76
+ // Which quota group a bucket belongs to.
77
+ function bucketGroup(key) {
78
+ if (key.startsWith("gemini-argon-")) {
79
+ return "argon";
80
+ }
81
+ if (key.startsWith("gemini-")) {
82
+ return "gemini";
83
+ }
84
+ return key.startsWith("3p-") ? "3p" : null;
85
+ }
86
+
87
+ // The bucket closest to running out for the model in use: the Argon bucket
88
+ // for Argon models, the other gemini-* buckets for Gemini models, 3p-*
89
+ // (third-party) buckets for the others. Argon falls back to the Gemini
90
+ // buckets, where its usage counts when there is no Argon bucket. Without a
73
91
  // matching bucket every bucket counts. Disabled buckets never do.
74
92
  function tightestQuota(data) {
75
93
  const buckets = data?.quota && typeof data.quota === "object" ? data.quota : {};
@@ -84,9 +102,17 @@ function tightestQuota(data) {
84
102
  }
85
103
 
86
104
  const model = firstText(data?.model?.display_name, data?.model?.id).toLowerCase();
87
- const family = model.startsWith("gemini") ? "gemini-" : "3p-";
88
- const forModel = usable.filter((quota) => quota.key.startsWith(family));
89
- const pool = forModel.length ? forModel : usable;
105
+ const inGroup = (group) => usable.filter((quota) => bucketGroup(quota.key) === group);
106
+ let pool;
107
+ if (!model.startsWith("gemini")) {
108
+ pool = inGroup("3p");
109
+ } else if (model.includes("argon")) {
110
+ pool = inGroup("argon");
111
+ pool = pool.length ? pool : inGroup("gemini");
112
+ } else {
113
+ pool = inGroup("gemini");
114
+ }
115
+ pool = pool.length ? pool : usable;
90
116
 
91
117
  return pool.reduce((low, quota) => (!low || quota.remaining < low.remaining ? quota : low), null);
92
118
  }
@@ -121,9 +147,18 @@ function contextUsedPercent(data) {
121
147
  function extractSession(data, config) {
122
148
  const contextPct = percent(contextUsedPercent(data));
123
149
  const contextSize = count(data.context_window?.context_window_size);
150
+ const current = data.context_window?.current_usage;
151
+ // Context tokens are the used share of the window, so they always agree with
152
+ // the percentage next to them: agy's percentage also counts cached tokens and
153
+ // its own system prompt and tools, which no sum of current_usage reproduces.
154
+ // Without a window size, count what current_usage lists as in the window.
124
155
  const usedTokens =
125
- count(data.context_window?.current_usage?.input_tokens) +
126
- count(data.context_window?.current_usage?.output_tokens);
156
+ contextSize > 0
157
+ ? Math.round((contextPct * contextSize) / 100)
158
+ : count(current?.input_tokens) +
159
+ count(current?.cache_read_input_tokens) +
160
+ count(current?.cache_creation_input_tokens);
161
+ const requestTokens = count(current?.input_tokens) + count(current?.output_tokens);
127
162
 
128
163
  // RAM is measured here; Antigravity doesn't send it.
129
164
  let ramPct = 0;
@@ -139,7 +174,7 @@ function extractSession(data, config) {
139
174
  const inputTokens = count(data.context_window?.total_input_tokens);
140
175
  const outputTokens = count(data.context_window?.total_output_tokens);
141
176
  const sessionTokens = inputTokens + outputTokens;
142
- const totalTokens = sessionTokens > 0 ? sessionTokens : usedTokens;
177
+ const totalTokens = sessionTokens > 0 ? sessionTokens : requestTokens;
143
178
 
144
179
  // Share of the prompt served from cache in the latest request.
145
180
  const cacheReadTokens = count(usage?.cache_read_input_tokens);
@@ -169,6 +204,7 @@ function extractSession(data, config) {
169
204
  weekly: readQuota(data, "gemini-weekly", SEVEN_DAYS),
170
205
  thirdPartyFiveHour: readQuota(data, "3p-5h", FIVE_HOURS),
171
206
  thirdPartyWeekly: readQuota(data, "3p-weekly", SEVEN_DAYS),
207
+ argonWeekly: readQuota(data, ARGON_BUCKET, SEVEN_DAYS),
172
208
  tightestQuota: tightestQuota(data),
173
209
  cwd: firstText(data.cwd, data.workspace?.current_dir),
174
210
  project: firstText(data.workspace?.project_dir),
@@ -93,6 +93,8 @@ const WIDGET_TAGS = {
93
93
  weeklyReset: { icon: "๐Ÿ”„", name: "Reset" },
94
94
  thirdPartyFiveHour: { icon: "๐Ÿงช", name: "3P 5h" },
95
95
  thirdPartyWeekly: { icon: "๐Ÿงช", name: "3P 7d" },
96
+ argonWeekly: { icon: "๐Ÿ”ฎ", name: "Argon 7d" },
97
+ argonReset: { icon: "๐Ÿ”„", name: "Argon reset" },
96
98
  context: { icon: "๐Ÿ’พ", name: "Context", iconOnly: true },
97
99
  contextPct: { icon: "๐Ÿ’พ", name: "Context" },
98
100
  contextTokens: { icon: "๐Ÿ’พ", name: "Context" },