tau-coding-agent 0.1.3 → 0.1.4

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.
@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import {
5
+ type AppKeybinding,
5
6
  CustomEditor,
6
7
  createBashToolDefinition,
7
8
  createFindToolDefinition,
@@ -16,8 +17,15 @@ import {
16
17
  type ToolDefinition,
17
18
  type ToolInfo,
18
19
  type ToolRenderResultOptions,
19
- } from "@mariozechner/pi-coding-agent";
20
- import { Container, Text, type Component, type EditorTheme, type TUI } from "@mariozechner/pi-tui";
20
+ } from "@earendil-works/pi-coding-agent";
21
+ import {
22
+ Container,
23
+ Text,
24
+ type AutocompleteProvider,
25
+ type Component,
26
+ type EditorComponent,
27
+ type Focusable,
28
+ } from "@earendil-works/pi-tui";
21
29
 
22
30
  // --- Constants ---
23
31
 
@@ -46,11 +54,22 @@ type ToolName = keyof typeof TOOL_FACTORIES;
46
54
  type AnyToolDefinition = ToolDefinition<any, any, any>;
47
55
  type AnyToolRenderContext = Parameters<NonNullable<AnyToolDefinition["renderResult"]>>[3];
48
56
  type JsonObject = Record<string, unknown>;
57
+ type EditorFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorComponent"]>>;
49
58
 
50
59
  type ToolDisplayModeConfig = {
51
60
  mode: Mode;
52
61
  };
53
62
 
63
+ type CustomEditorLike = EditorComponent &
64
+ Partial<Focusable> & {
65
+ actionHandlers?: Map<AppKeybinding, () => void>;
66
+ onEscape?: () => void;
67
+ onCtrlD?: () => void;
68
+ onPasteImage?: () => void;
69
+ onExtensionShortcut?: (data: string) => boolean;
70
+ isShowingAutocomplete?: () => boolean;
71
+ };
72
+
54
73
  // --- Config ---
55
74
 
56
75
  function emptyConfig(): ToolDisplayModeConfig {
@@ -242,11 +261,11 @@ function renderMinimalResult(
242
261
  break;
243
262
  }
244
263
 
245
- return renderMinimalText(theme, "muted", `→ ${summary}`);
264
+ return renderMinimalText(theme, "muted", `↳ ${summary}`);
246
265
  }
247
266
 
248
267
  function renderMinimalText(theme: Theme, color: "error" | "muted", text: string): Text {
249
- return new Text(theme.fg(color, `\n${text}`), 0, 0);
268
+ return new Text(theme.fg(color, text), 0, 0);
250
269
  }
251
270
 
252
271
  function bashSummary(result: AgentToolResult<any>): string {
@@ -328,29 +347,152 @@ function emptyComponent(): Container {
328
347
 
329
348
  // --- Editor ---
330
349
 
331
- class ToolDisplayEditor extends CustomEditor {
350
+ class ToolDisplayEditor implements EditorComponent, Focusable {
351
+ readonly actionHandlers: Map<AppKeybinding, () => void>;
352
+ private readonly customBase: CustomEditorLike;
353
+ private fallbackFocused = false;
354
+ private fallbackOnEscape?: () => void;
355
+ private fallbackOnCtrlD?: () => void;
356
+ private fallbackOnPasteImage?: () => void;
357
+ private fallbackOnExtensionShortcut?: (data: string) => boolean;
358
+
332
359
  constructor(
333
- tui: TUI,
334
- theme: EditorTheme,
360
+ private readonly base: EditorComponent,
335
361
  private readonly appKeybindings: KeybindingsManager,
336
362
  private readonly cycleMode: () => void,
337
363
  ) {
338
- super(tui, theme, appKeybindings);
364
+ this.customBase = base as CustomEditorLike;
365
+ this.actionHandlers = this.customBase.actionHandlers ?? new Map();
366
+ }
367
+
368
+ get focused(): boolean {
369
+ return this.customBase.focused ?? this.fallbackFocused;
370
+ }
371
+
372
+ set focused(value: boolean) {
373
+ this.fallbackFocused = value;
374
+ if ("focused" in this.customBase) this.customBase.focused = value;
375
+ }
376
+
377
+ get onSubmit(): ((text: string) => void) | undefined {
378
+ return this.base.onSubmit;
379
+ }
380
+
381
+ set onSubmit(handler: ((text: string) => void) | undefined) {
382
+ this.base.onSubmit = handler;
383
+ }
384
+
385
+ get onChange(): ((text: string) => void) | undefined {
386
+ return this.base.onChange;
387
+ }
388
+
389
+ set onChange(handler: ((text: string) => void) | undefined) {
390
+ this.base.onChange = handler;
391
+ }
392
+
393
+ get wantsKeyRelease(): boolean | undefined {
394
+ return this.base.wantsKeyRelease;
395
+ }
396
+
397
+ get borderColor(): ((str: string) => string) | undefined {
398
+ return this.base.borderColor;
399
+ }
400
+
401
+ set borderColor(handler: ((str: string) => string) | undefined) {
402
+ this.base.borderColor = handler;
403
+ }
404
+
405
+ get onEscape(): (() => void) | undefined {
406
+ return this.customBase.onEscape ?? this.fallbackOnEscape;
407
+ }
408
+
409
+ set onEscape(handler: (() => void) | undefined) {
410
+ this.fallbackOnEscape = handler;
411
+ if ("onEscape" in this.customBase) this.customBase.onEscape = handler;
412
+ }
413
+
414
+ get onCtrlD(): (() => void) | undefined {
415
+ return this.customBase.onCtrlD ?? this.fallbackOnCtrlD;
416
+ }
417
+
418
+ set onCtrlD(handler: (() => void) | undefined) {
419
+ this.fallbackOnCtrlD = handler;
420
+ if ("onCtrlD" in this.customBase) this.customBase.onCtrlD = handler;
421
+ }
422
+
423
+ get onPasteImage(): (() => void) | undefined {
424
+ return this.customBase.onPasteImage ?? this.fallbackOnPasteImage;
339
425
  }
340
426
 
341
- override handleInput(data: string): void {
427
+ set onPasteImage(handler: (() => void) | undefined) {
428
+ this.fallbackOnPasteImage = handler;
429
+ if ("onPasteImage" in this.customBase) this.customBase.onPasteImage = handler;
430
+ }
431
+
432
+ get onExtensionShortcut(): ((data: string) => boolean) | undefined {
433
+ return this.customBase.onExtensionShortcut ?? this.fallbackOnExtensionShortcut;
434
+ }
435
+
436
+ set onExtensionShortcut(handler: ((data: string) => boolean) | undefined) {
437
+ this.fallbackOnExtensionShortcut = handler;
438
+ if ("onExtensionShortcut" in this.customBase) this.customBase.onExtensionShortcut = handler;
439
+ }
440
+
441
+ getText(): string {
442
+ return this.base.getText();
443
+ }
444
+
445
+ setText(text: string): void {
446
+ this.base.setText(text);
447
+ }
448
+
449
+ addToHistory(text: string): void {
450
+ this.base.addToHistory?.(text);
451
+ }
452
+
453
+ insertTextAtCursor(text: string): void {
454
+ this.base.insertTextAtCursor?.(text);
455
+ }
456
+
457
+ getExpandedText(): string {
458
+ return this.base.getExpandedText?.() ?? this.base.getText();
459
+ }
460
+
461
+ setAutocompleteProvider(provider: AutocompleteProvider): void {
462
+ this.base.setAutocompleteProvider?.(provider);
463
+ }
464
+
465
+ setPaddingX(padding: number): void {
466
+ this.base.setPaddingX?.(padding);
467
+ }
468
+
469
+ setAutocompleteMaxVisible(maxVisible: number): void {
470
+ this.base.setAutocompleteMaxVisible?.(maxVisible);
471
+ }
472
+
473
+ render(width: number): string[] {
474
+ return this.base.render(width);
475
+ }
476
+
477
+ invalidate(): void {
478
+ this.base.invalidate();
479
+ }
480
+
481
+ handleInput(data: string): void {
342
482
  if (this.appKeybindings.matches(data, "app.tools.expand")) {
343
483
  this.cycleMode();
344
484
  return;
345
485
  }
346
486
 
347
- super.handleInput(data);
487
+ this.base.handleInput(data);
348
488
  }
349
489
  }
350
490
 
351
491
  export default function toolDisplayModeExtension(pi: ExtensionAPI): void {
352
492
  let mode: Mode = DEFAULT_MODE;
353
493
  let registeredToolRenderers = false;
494
+ let installedEditorFactory: EditorFactory | undefined;
495
+ let previousEditorFactory: EditorFactory | undefined;
354
496
  const toolCache = new Map<string, Partial<Record<ToolName, AnyToolDefinition>>>();
355
497
 
356
498
  const setMode = (ctx: ExtensionContext, next: Mode): void => {
@@ -379,9 +521,24 @@ export default function toolDisplayModeExtension(pi: ExtensionAPI): void {
379
521
  applyMode(ctx, mode);
380
522
  if (!ctx.hasUI) return;
381
523
 
382
- ctx.ui.setEditorComponent(
383
- (tui, theme, keybindings) =>
384
- new ToolDisplayEditor(tui, theme, keybindings, () => setMode(ctx, nextMode(mode))),
385
- );
524
+ if (ctx.ui.getEditorComponent() === installedEditorFactory) return;
525
+
526
+ previousEditorFactory = ctx.ui.getEditorComponent();
527
+ installedEditorFactory = (tui, theme, keybindings) => {
528
+ const baseEditor =
529
+ previousEditorFactory?.(tui, theme, keybindings) ??
530
+ new CustomEditor(tui, theme, keybindings);
531
+ return new ToolDisplayEditor(baseEditor, keybindings, () => setMode(ctx, nextMode(mode)));
532
+ };
533
+ ctx.ui.setEditorComponent(installedEditorFactory);
534
+ });
535
+
536
+ pi.on("session_shutdown", async (_event, ctx) => {
537
+ if (ctx.hasUI && ctx.ui.getEditorComponent() === installedEditorFactory) {
538
+ ctx.ui.setEditorComponent(previousEditorFactory);
539
+ }
540
+
541
+ installedEditorFactory = undefined;
542
+ previousEditorFactory = undefined;
386
543
  });
387
544
  }
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  clampPercent,
4
4
  formatCurrency,
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  clampPercent,
4
4
  formatCount,
@@ -1,5 +1,5 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import { BorderedLoader } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { BorderedLoader } from "@earendil-works/pi-coding-agent";
3
3
  import {
4
4
  Key,
5
5
  matchesKey,
@@ -7,7 +7,7 @@ import {
7
7
  type TUI,
8
8
  truncateToWidth,
9
9
  visibleWidth,
10
- } from "@mariozechner/pi-tui";
10
+ } from "@earendil-works/pi-tui";
11
11
  import { createReadStream, type Dirent } from "node:fs";
12
12
  import fs from "node:fs/promises";
13
13
  import os from "node:os";
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  clampPercent,
4
4
  formatCount,
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  clampPercent,
4
4
  formatCurrency,
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  formatCurrency,
4
4
  HttpError,
@@ -1,6 +1,5 @@
1
1
  import anthropicProvider from "./anthropic.js";
2
2
  import githubCopilotProvider from "./github-copilot.js";
3
- import googleGeminiCliProvider from "./google-gemini-cli.js";
4
3
  import minimaxProvider from "./minimax.js";
5
4
  import openAICodexProvider from "./openai-codex.js";
6
5
  import openRouterProvider from "./openrouter.js";
@@ -11,7 +10,6 @@ export const SUPPORTED_PROVIDERS: readonly UsageProviderDefinition[] = [
11
10
  openAICodexProvider,
12
11
  anthropicProvider,
13
12
  githubCopilotProvider,
14
- googleGeminiCliProvider,
15
13
  openRouterProvider,
16
14
  zaiProvider,
17
15
  minimaxProvider,
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  export interface RGB {
4
4
  r: number;
@@ -10,7 +10,6 @@ export type SupportedProviderId =
10
10
  | "openai-codex"
11
11
  | "anthropic"
12
12
  | "github-copilot"
13
- | "google-gemini-cli"
14
13
  | "openrouter"
15
14
  | "zai"
16
15
  | "minimax";
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  clampPercent,
4
4
  formatCount,
@@ -1,12 +1,12 @@
1
- import type { Api, Model } from "@mariozechner/pi-ai";
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
2
  import {
3
3
  defineTool,
4
4
  getMarkdownTheme,
5
5
  keyHint,
6
6
  type ExtensionAPI,
7
7
  type ExtensionContext,
8
- } from "@mariozechner/pi-coding-agent";
9
- import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
8
+ } from "@earendil-works/pi-coding-agent";
9
+ import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
10
10
  import { Type } from "typebox";
11
11
 
12
12
  import { createBrowserSession, discoverProfiles } from "./browser/discovery.js";
@@ -1,4 +1,4 @@
1
- import type { Api, Model } from "@mariozechner/pi-ai";
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
2
 
3
3
  import { dedupeSources, extractMarkdownSources } from "../normalize.js";
4
4
  import type { WebsearchResult } from "../types.js";
@@ -1,4 +1,4 @@
1
- import type { Api, Model } from "@mariozechner/pi-ai";
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
2
 
3
3
  import { dedupeSources, extractMarkdownSources, normalizeSourceTitle } from "../normalize.js";
4
4
  import type { WebsearchResult, WebsearchSource } from "../types.js";
@@ -1,4 +1,4 @@
1
- import type { Api, Model } from "@mariozechner/pi-ai";
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
2
 
3
3
  import type { WebsearchResult } from "../types.js";
4
4
  import { decodeJwtAccountId, runOpenAICodexSearch } from "./openai-codex.shared.js";
@@ -1,5 +1,5 @@
1
- import type { Api, Model } from "@mariozechner/pi-ai";
2
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
 
4
4
  export interface PiModelSelection {
5
5
  model: Model<Api>;
@@ -18,14 +18,14 @@
18
18
  * Lists worktrees and their status.
19
19
  */
20
20
 
21
- import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent";
21
+ import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
22
22
  import {
23
23
  CURRENT_SESSION_VERSION,
24
24
  DynamicBorder,
25
25
  SessionManager,
26
26
  highlightCode,
27
27
  type SessionHeader,
28
- } from "@mariozechner/pi-coding-agent";
28
+ } from "@earendil-works/pi-coding-agent";
29
29
  import {
30
30
  Box,
31
31
  Container,
@@ -33,7 +33,7 @@ import {
33
33
  SelectList,
34
34
  Text,
35
35
  matchesKey,
36
- } from "@mariozechner/pi-tui";
36
+ } from "@earendil-works/pi-tui";
37
37
  import { spawnSync } from "node:child_process";
38
38
  import { randomUUID } from "node:crypto";
39
39
  import * as fs from "node:fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tau-coding-agent",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Opinionated Pi package with web search, review workflows, sandboxing, memory, usage reporting, skills, and a theme",
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -35,10 +35,10 @@
35
35
  "turndown-plugin-gfm": "^1.0.2"
36
36
  },
37
37
  "peerDependencies": {
38
- "@mariozechner/pi-agent-core": "*",
39
- "@mariozechner/pi-ai": "*",
40
- "@mariozechner/pi-coding-agent": "*",
41
- "@mariozechner/pi-tui": "*",
38
+ "@earendil-works/pi-agent-core": "*",
39
+ "@earendil-works/pi-ai": "*",
40
+ "@earendil-works/pi-coding-agent": "*",
41
+ "@earendil-works/pi-tui": "*",
42
42
  "typebox": "*"
43
43
  },
44
44
  "engines": {
@@ -161,7 +161,6 @@ function providerScore(provider, family) {
161
161
  }
162
162
 
163
163
  if (family === "google") {
164
- if (normalized === "google-gemini-cli") return 700;
165
164
  if (normalized === "google") return 600;
166
165
  if (normalized === "github-copilot") return 500;
167
166
  if (normalized.includes("vertex")) return 450;
@@ -1,232 +0,0 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import {
3
- clampPercent,
4
- LiveUsageUnavailableError,
5
- makeMeterItem,
6
- parseISODate,
7
- readArray,
8
- readString,
9
- } from "./shared.js";
10
- import type { LiveUsageItem, LiveUsageSnapshot, UsageProviderDefinition } from "./types.js";
11
-
12
- const PROVIDER_ID = "google-gemini-cli";
13
- const QUOTA_URL = "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota";
14
- const LOAD_CODE_ASSIST_URL = "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist";
15
-
16
- type GeminiTier = "free-tier" | "legacy-tier" | "standard-tier";
17
-
18
- interface GeminiAuthPayload {
19
- token?: string;
20
- projectId?: string;
21
- }
22
-
23
- interface GeminiQuota {
24
- modelId: string;
25
- percentLeft: number;
26
- resetTime?: Date;
27
- }
28
-
29
- function readAuthPayload(value: string): GeminiAuthPayload {
30
- try {
31
- return JSON.parse(value) as GeminiAuthPayload;
32
- } catch {
33
- throw new LiveUsageUnavailableError("Gemini CLI OAuth login is required for live quota data");
34
- }
35
- }
36
-
37
- function parsePlan(tier: GeminiTier | undefined): string | undefined {
38
- switch (tier) {
39
- case "standard-tier":
40
- return "Paid";
41
- case "free-tier":
42
- return "Free";
43
- case "legacy-tier":
44
- return "Legacy";
45
- default:
46
- return undefined;
47
- }
48
- }
49
-
50
- function parseModelLabel(modelId: string): string {
51
- const value = modelId.trim();
52
- const slash = value.lastIndexOf("/");
53
- return slash === -1 ? value : value.slice(slash + 1);
54
- }
55
-
56
- function isFlashLiteModel(id: string): boolean {
57
- return id.includes("flash-lite");
58
- }
59
-
60
- function isFlashModel(id: string): boolean {
61
- return id.includes("flash") && !isFlashLiteModel(id);
62
- }
63
-
64
- function isProModel(id: string): boolean {
65
- return id.includes("pro");
66
- }
67
-
68
- function makeMeter(key: string, label: string, quota: GeminiQuota): LiveUsageItem {
69
- return makeMeterItem({
70
- key,
71
- label,
72
- usedPercent: clampPercent(100 - quota.percentLeft),
73
- windowLabel: "24h",
74
- resetsAt: quota.resetTime,
75
- });
76
- }
77
-
78
- async function fetchJson(
79
- url: string,
80
- token: string,
81
- body: Record<string, unknown>,
82
- signal?: AbortSignal,
83
- ): Promise<Record<string, unknown>> {
84
- const response = await fetch(url, {
85
- method: "POST",
86
- headers: {
87
- Authorization: `Bearer ${token}`,
88
- "Content-Type": "application/json",
89
- },
90
- body: JSON.stringify(body),
91
- signal,
92
- });
93
-
94
- if (response.status === 401 || response.status === 403) {
95
- throw new Error("Gemini CLI login expired. Re-run /login google-gemini-cli.");
96
- }
97
-
98
- if (!response.ok) {
99
- const bodyText = await response.text().catch(() => "");
100
- throw new Error(
101
- bodyText.trim()
102
- ? `Gemini usage request failed (${response.status}): ${bodyText.trim()}`
103
- : `Gemini usage request failed (${response.status}).`,
104
- );
105
- }
106
-
107
- return (await response.json()) as Record<string, unknown>;
108
- }
109
-
110
- async function loadCodeAssistStatus(
111
- token: string,
112
- signal?: AbortSignal,
113
- ): Promise<{ tier?: GeminiTier }> {
114
- try {
115
- const payload = await fetchJson(
116
- LOAD_CODE_ASSIST_URL,
117
- token,
118
- { metadata: { ideType: "GEMINI_CLI", pluginType: "GEMINI" } },
119
- signal,
120
- );
121
-
122
- const currentTier = payload.currentTier as Record<string, unknown> | undefined;
123
- const tierId = readString(currentTier?.id);
124
-
125
- return {
126
- tier:
127
- tierId === "free-tier" || tierId === "legacy-tier" || tierId === "standard-tier"
128
- ? tierId
129
- : undefined,
130
- };
131
- } catch {
132
- return {};
133
- }
134
- }
135
-
136
- function parseQuotas(payload: Record<string, unknown>): GeminiQuota[] {
137
- const buckets = readArray<Record<string, unknown>>(payload.buckets);
138
- const quotaByModel = new Map<string, GeminiQuota>();
139
-
140
- for (const bucket of buckets) {
141
- const input = bucket as Record<string, unknown> | null | undefined;
142
- const modelId = readString(input?.modelId);
143
- const remainingFraction =
144
- typeof input?.remainingFraction === "number" ? input.remainingFraction : undefined;
145
- if (!modelId || remainingFraction === undefined) continue;
146
-
147
- const percentLeft = clampPercent(remainingFraction * 100);
148
- const resetTime = parseISODate(readString(input?.resetTime));
149
- const quota: GeminiQuota = {
150
- modelId,
151
- percentLeft,
152
- resetTime,
153
- };
154
-
155
- const existing = quotaByModel.get(modelId);
156
- if (!existing || quota.percentLeft < existing.percentLeft) {
157
- quotaByModel.set(modelId, quota);
158
- }
159
- }
160
-
161
- return [...quotaByModel.values()].sort((left, right) =>
162
- left.modelId.localeCompare(right.modelId),
163
- );
164
- }
165
-
166
- function buildItems(quotas: GeminiQuota[]): LiveUsageItem[] {
167
- const lower = quotas.map((quota) => ({ quota, modelId: quota.modelId.toLowerCase() }));
168
- const pro = lower.filter((entry) => isProModel(entry.modelId)).map((entry) => entry.quota);
169
- const flash = lower.filter((entry) => isFlashModel(entry.modelId)).map((entry) => entry.quota);
170
- const flashLite = lower
171
- .filter((entry) => isFlashLiteModel(entry.modelId))
172
- .map((entry) => entry.quota);
173
-
174
- const items: LiveUsageItem[] = [];
175
- const proMin = pro.sort((left, right) => left.percentLeft - right.percentLeft)[0];
176
- const flashMin = flash.sort((left, right) => left.percentLeft - right.percentLeft)[0];
177
- const flashLiteMin = flashLite.sort((left, right) => left.percentLeft - right.percentLeft)[0];
178
-
179
- if (proMin) items.push(makeMeter("pro", "Pro", proMin));
180
- if (flashMin) items.push(makeMeter("flash", "Flash", flashMin));
181
- if (flashLiteMin) items.push(makeMeter("flash-lite", "Flash Lite", flashLiteMin));
182
-
183
- if (items.length > 0) return items;
184
-
185
- for (const quota of quotas.slice(0, 3)) {
186
- items.push(makeMeter(quota.modelId, parseModelLabel(quota.modelId), quota));
187
- }
188
- return items;
189
- }
190
-
191
- async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<LiveUsageSnapshot> {
192
- const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
193
- if (!apiKey) throw new LiveUsageUnavailableError("no auth configured");
194
-
195
- const auth = readAuthPayload(apiKey);
196
- const token = typeof auth.token === "string" ? auth.token.trim() : "";
197
- if (!token) {
198
- throw new LiveUsageUnavailableError("Gemini CLI OAuth login is required for live quota data");
199
- }
200
-
201
- const authProjectId = typeof auth.projectId === "string" ? auth.projectId.trim() : undefined;
202
- const statusPromise = loadCodeAssistStatus(token, signal);
203
- const quotaPromise = fetchJson(
204
- QUOTA_URL,
205
- token,
206
- authProjectId ? { project: authProjectId } : {},
207
- signal,
208
- );
209
-
210
- const [status, quotaPayload] = await Promise.all([statusPromise, quotaPromise]);
211
- const quotas = parseQuotas(quotaPayload);
212
-
213
- if (quotas.length === 0) {
214
- throw new Error("Gemini did not return any live quota data.");
215
- }
216
-
217
- return {
218
- plan: parsePlan(status.tier),
219
- fetchedAt: new Date(),
220
- items: buildItems(quotas),
221
- };
222
- }
223
-
224
- const googleGeminiCliProvider: UsageProviderDefinition = {
225
- id: PROVIDER_ID,
226
- label: "Gemini CLI",
227
- shortLabel: "gemini",
228
- color: { r: 171, g: 135, b: 234 },
229
- fetchLiveUsage: fetchUsage,
230
- };
231
-
232
- export default googleGeminiCliProvider;