@marcosgilf/pi-usage 0.1.0 → 0.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/README.md +9 -47
- package/package.json +8 -1
- package/src/copilot-usage.ts +60 -220
- package/src/core.ts +0 -74
- package/src/events.ts +2 -0
package/README.md
CHANGED
|
@@ -1,63 +1,25 @@
|
|
|
1
1
|
# 📊 pi-usage
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Show GitHub Copilot plan and usage percentage in Pi's default footer, with a modal for quota details.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## ✨ Features
|
|
8
|
-
|
|
9
|
-
- Show Copilot plan and usage beside Pi's model in the footer.
|
|
10
|
-
- Open a quota modal by clicking the footer usage in fullscreen mode, running `/copilot-usage`, or pressing `super+shift+u`.
|
|
11
|
-
- Refresh modal data with `r` and copy a plain-text summary with `c`.
|
|
12
|
-
- Hide the progress bar for unlimited plans.
|
|
13
|
-
|
|
14
|
-
## 📦 Install
|
|
15
|
-
|
|
16
|
-
Install the published package:
|
|
5
|
+
## Install
|
|
17
6
|
|
|
18
7
|
```sh
|
|
19
8
|
pi install npm:@marcosgilf/pi-usage
|
|
20
9
|
```
|
|
21
10
|
|
|
22
|
-
|
|
11
|
+
- When installed alone, the default footer shows Copilot plan and percentage.
|
|
12
|
+
- `/copilot-usage` opens quota details.
|
|
13
|
+
- `super+shift+u` opens the same modal.
|
|
14
|
+
- In modal: `c` copies full summary; Ctrl/Cmd+C copies selected text; click outside closes.
|
|
15
|
+
- Install `@marcosgilf/pi-footer` to place usage beside the model and coordinate it with session context. It loads this extension as a dependency; do not install both packages separately.
|
|
23
16
|
|
|
24
17
|
Pi extensions run with Pi's operating-system permissions. Review package source before installing.
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Install `gh`, sign in to GitHub, and grant the `user` OAuth scope if needed:
|
|
19
|
+
The quota request uses GitHub's undocumented `copilot_internal/user` endpoint through `gh`. Install and sign in to GitHub CLI. The `user` OAuth scope may be required:
|
|
29
20
|
|
|
30
21
|
```sh
|
|
31
22
|
gh auth refresh -h github.com -s user
|
|
32
23
|
```
|
|
33
24
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
## 💬 Commands
|
|
37
|
-
|
|
38
|
-
- `/copilot-usage` opens the modal and refreshes account quota.
|
|
39
|
-
- `super+shift+u` opens the same modal.
|
|
40
|
-
- In the modal, `r` refreshes data and `c` copies plain text. Esc or Enter closes it.
|
|
41
|
-
|
|
42
|
-
The quota request uses GitHub's undocumented `copilot_internal/user` endpoint through `gh`. GitHub may change this endpoint.
|
|
43
|
-
|
|
44
|
-
## 🚧 Limitations
|
|
45
|
-
|
|
46
|
-
- Footer mouse clicks work in Pi fullscreen mode. Use the command or shortcut in regular mode.
|
|
47
|
-
- Terminal mouse selection copies the rendered modal layout. Press `c` to copy plain text.
|
|
48
|
-
- Clipboard copy uses macOS `pbcopy`.
|
|
49
|
-
|
|
50
|
-
## 🗂️ Package layout
|
|
51
|
-
|
|
52
|
-
- `src/index.ts` is the Pi extension entrypoint.
|
|
53
|
-
- `src/copilot-usage.ts` registers commands, footer, and modal behavior.
|
|
54
|
-
- `src/core.ts` parses and formats Copilot quota and session data.
|
|
55
|
-
- `test/` contains Vitest tests.
|
|
56
|
-
|
|
57
|
-
## 🔎 Keywords
|
|
58
|
-
|
|
59
|
-
`pi-package`, `pi-extension`, `github-copilot`, `usage`, `quota`
|
|
60
|
-
|
|
61
|
-
## 📄 License
|
|
62
|
-
|
|
63
|
-
MIT. See [`LICENSE`](./LICENSE).
|
|
25
|
+
GitHub may change this endpoint.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marcosgilf/pi-usage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Show GitHub Copilot plan and quota usage in Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,11 +19,18 @@
|
|
|
19
19
|
"README.md",
|
|
20
20
|
"LICENSE"
|
|
21
21
|
],
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./src/index.ts",
|
|
24
|
+
"./events": "./src/events.ts"
|
|
25
|
+
},
|
|
22
26
|
"pi": {
|
|
23
27
|
"extensions": [
|
|
24
28
|
"./src/index.ts"
|
|
25
29
|
]
|
|
26
30
|
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@marcosgilf/pi-modal": "^0.1.0"
|
|
33
|
+
},
|
|
27
34
|
"peerDependencies": {
|
|
28
35
|
"@earendil-works/pi-coding-agent": "*",
|
|
29
36
|
"@earendil-works/pi-tui": "*"
|
package/src/copilot-usage.ts
CHANGED
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import type { ExtensionAPI, ExtensionContext
|
|
3
|
-
import { type
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
formatPlanUsage,
|
|
7
|
-
formatProgressBar,
|
|
8
|
-
formatTokens,
|
|
9
|
-
formatUsageSummary,
|
|
10
|
-
getUsageProgress,
|
|
11
|
-
parseCopilotUsage,
|
|
12
|
-
summarizeSession,
|
|
13
|
-
writeClipboard,
|
|
14
|
-
} from "./core.ts";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { type ModalContent, type ModalRow, showModal } from "@marcosgilf/pi-modal";
|
|
4
|
+
import { type CopilotUsage, formatPlanUsage, formatUsageSummary, getUsageProgress, parseCopilotUsage } from "./core.ts";
|
|
5
|
+
import { COPILOT_USAGE_STATUS_KEY, OPEN_COPILOT_USAGE_EVENT } from "./events.ts";
|
|
15
6
|
|
|
16
7
|
const CACHE_MS = 5 * 60 * 1000;
|
|
17
|
-
const FOOTER_STATUS_KEY = "copilot-usage";
|
|
18
8
|
|
|
19
9
|
type UsageSnapshot = CopilotUsage & { fetchedAt: number };
|
|
20
|
-
type FooterRow = { text: string; style?: "accent" | "success" | "warning"; progress?: number };
|
|
21
10
|
|
|
22
11
|
let cachedUsage: UsageSnapshot | undefined;
|
|
23
12
|
let lastError: string | undefined;
|
|
24
13
|
let lastAttempt = 0;
|
|
25
14
|
let pending: Promise<UsageSnapshot> | undefined;
|
|
26
|
-
let requestFooterRender: (() => void) | undefined;
|
|
27
15
|
|
|
28
16
|
function runGh(args: string[]): Promise<string> {
|
|
29
17
|
return new Promise((resolve, reject) => {
|
|
@@ -71,12 +59,10 @@ async function refreshUsage(force = false): Promise<UsageSnapshot> {
|
|
|
71
59
|
.then((usage) => {
|
|
72
60
|
cachedUsage = { ...usage, fetchedAt: Date.now() };
|
|
73
61
|
lastError = undefined;
|
|
74
|
-
requestFooterRender?.();
|
|
75
62
|
return cachedUsage;
|
|
76
63
|
})
|
|
77
64
|
.catch((error: unknown) => {
|
|
78
65
|
lastError = error instanceof Error ? error.message : String(error);
|
|
79
|
-
requestFooterRender?.();
|
|
80
66
|
throw new Error(lastError);
|
|
81
67
|
})
|
|
82
68
|
.finally(() => {
|
|
@@ -85,147 +71,10 @@ async function refreshUsage(force = false): Promise<UsageSnapshot> {
|
|
|
85
71
|
return pending;
|
|
86
72
|
}
|
|
87
73
|
|
|
88
|
-
function
|
|
89
|
-
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
90
|
-
let disposed = false;
|
|
91
|
-
const requestRender = () => {
|
|
92
|
-
if (!disposed) tui.requestRender();
|
|
93
|
-
};
|
|
94
|
-
const unsubscribe = footerData.onBranchChange(requestRender);
|
|
95
|
-
requestFooterRender = requestRender;
|
|
96
|
-
let statsCache: { leafId: string | null; stats: ReturnType<typeof summarizeSession> } | undefined;
|
|
97
|
-
let usageClickBounds: { left: number; right: number } | undefined;
|
|
98
|
-
const getStats = () => {
|
|
99
|
-
const leafId = ctx.sessionManager.getLeafId();
|
|
100
|
-
if (statsCache?.leafId === leafId) return statsCache.stats;
|
|
101
|
-
const stats = summarizeSession(ctx.sessionManager.getEntries());
|
|
102
|
-
statsCache = { leafId, stats };
|
|
103
|
-
return stats;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
dispose() {
|
|
108
|
-
disposed = true;
|
|
109
|
-
unsubscribe();
|
|
110
|
-
if (requestFooterRender === requestRender) requestFooterRender = undefined;
|
|
111
|
-
},
|
|
112
|
-
handleMouse(event: TuiMouseEvent) {
|
|
113
|
-
if (
|
|
114
|
-
event.button !== "left" ||
|
|
115
|
-
event.y !== 1 ||
|
|
116
|
-
!usageClickBounds ||
|
|
117
|
-
event.x < usageClickBounds.left ||
|
|
118
|
-
event.x >= usageClickBounds.right
|
|
119
|
-
)
|
|
120
|
-
return;
|
|
121
|
-
if (event.type === "click") void showUsage(ctx);
|
|
122
|
-
if (event.type === "press" || event.type === "release" || event.type === "click") {
|
|
123
|
-
return { handled: true, render: event.type === "click" };
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
invalidate() {},
|
|
127
|
-
render(width: number) {
|
|
128
|
-
const stats = getStats();
|
|
129
|
-
const context = ctx.getContextUsage();
|
|
130
|
-
const leftParts: string[] = [];
|
|
131
|
-
const input = stats.input + stats.cacheRead + stats.cacheWrite;
|
|
132
|
-
if (input) leftParts.push(`↑${formatTokens(input)}`);
|
|
133
|
-
if (stats.output) leftParts.push(`↓${formatTokens(stats.output)}`);
|
|
134
|
-
if (stats.cacheRead) leftParts.push(`R${formatTokens(stats.cacheRead)}`);
|
|
135
|
-
if (stats.cacheWrite) leftParts.push(`W${formatTokens(stats.cacheWrite)}`);
|
|
136
|
-
if (stats.cost) leftParts.push(`$${stats.cost.toFixed(3)}`);
|
|
137
|
-
if (context) {
|
|
138
|
-
const percent = context.percent === null ? "?" : `${context.percent.toFixed(1)}%`;
|
|
139
|
-
leftParts.push(`${percent}/${formatTokens(context.contextWindow)}`);
|
|
140
|
-
}
|
|
141
|
-
let left = theme.fg("dim", leftParts.join(" "));
|
|
142
|
-
|
|
143
|
-
const model = ctx.model;
|
|
144
|
-
const modelName = model?.name || model?.id || "no-model";
|
|
145
|
-
const thinking = model?.reasoning ? ` • ${ctx.thinkingLevel || "off"}` : "";
|
|
146
|
-
let modelText = `${modelName}${thinking}`;
|
|
147
|
-
if (footerData.getAvailableProviderCount() > 1 && model) modelText = `(${model.provider}) ${modelText}`;
|
|
148
|
-
const usageText = formatPlanUsage(cachedUsage);
|
|
149
|
-
const usageStyle =
|
|
150
|
-
cachedUsage?.percentUsed !== undefined && cachedUsage.percentUsed >= 90 ? "warning" : "accent";
|
|
151
|
-
const usageLabel = usageText ? theme.fg(usageStyle, usageText) : "";
|
|
152
|
-
const right = usageLabel ? `${usageLabel} · ${modelText}` : modelText;
|
|
153
|
-
|
|
154
|
-
const clippedRight = truncateToWidth(right, width, "");
|
|
155
|
-
const clippedRightWidth = visibleWidth(clippedRight);
|
|
156
|
-
const usageLabelWidth = visibleWidth(usageLabel);
|
|
157
|
-
usageClickBounds =
|
|
158
|
-
usageLabelWidth > 0
|
|
159
|
-
? {
|
|
160
|
-
left: width - clippedRightWidth,
|
|
161
|
-
right: width - clippedRightWidth + Math.min(usageLabelWidth, clippedRightWidth),
|
|
162
|
-
}
|
|
163
|
-
: undefined;
|
|
164
|
-
const leftWidth = Math.max(0, width - clippedRightWidth - 2);
|
|
165
|
-
left = leftWidth > 0 ? truncateToWidth(left, leftWidth, "...") : "";
|
|
166
|
-
const padding = " ".repeat(Math.max(0, width - visibleWidth(left) - clippedRightWidth));
|
|
167
|
-
const statsLine = `${left}${padding}${clippedRight}`;
|
|
168
|
-
|
|
169
|
-
const home = process.env.HOME || "";
|
|
170
|
-
let cwd = home && ctx.cwd.startsWith(`${home}/`) ? `~${ctx.cwd.slice(home.length)}` : ctx.cwd;
|
|
171
|
-
const branch = footerData.getGitBranch();
|
|
172
|
-
if (branch) cwd += ` (${branch})`;
|
|
173
|
-
const sessionName = ctx.sessionManager.getSessionName();
|
|
174
|
-
if (sessionName) cwd += ` • ${sessionName}`;
|
|
175
|
-
const lines = [truncateToWidth(theme.fg("dim", cwd), width, theme.fg("dim", "...")), statsLine];
|
|
176
|
-
const statuses = Array.from(footerData.getExtensionStatuses().entries())
|
|
177
|
-
.filter(([key]) => key !== FOOTER_STATUS_KEY)
|
|
178
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
179
|
-
.map(([, text]) => text);
|
|
180
|
-
if (statuses.length) lines.push(truncateToWidth(statuses.join(" "), width, theme.fg("dim", "...")));
|
|
181
|
-
return lines;
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function copyToClipboard(text: string): Promise<void> {
|
|
188
|
-
if (process.platform !== "darwin") return Promise.reject(new Error("Clipboard copy requires macOS."));
|
|
189
|
-
return writeClipboard(text);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function borderedRows(rows: FooterRow[], width: number, theme: Theme): string[] {
|
|
193
|
-
if (width < 6)
|
|
194
|
-
return rows.map(({ text, progress }) =>
|
|
195
|
-
truncateToWidth(progress === undefined ? text : formatProgressBar(progress, width), width, "…"),
|
|
196
|
-
);
|
|
197
|
-
const contentWidth = width - 4;
|
|
198
|
-
const edge = theme.fg("muted", "│");
|
|
199
|
-
const top = theme.fg("muted", `╭${"─".repeat(contentWidth + 2)}╮`);
|
|
200
|
-
const bottom = theme.fg("muted", `╰${"─".repeat(contentWidth + 2)}╯`);
|
|
201
|
-
const content = rows.map(({ text, style, progress }) => {
|
|
202
|
-
const value = progress === undefined ? text : formatProgressBar(progress, contentWidth);
|
|
203
|
-
const styled = style === "accent" ? theme.fg(style, theme.bold(value)) : style ? theme.fg(style, value) : value;
|
|
204
|
-
const clipped = truncateToWidth(styled, contentWidth, "…");
|
|
205
|
-
const padding = " ".repeat(Math.max(0, contentWidth - visibleWidth(clipped)));
|
|
206
|
-
return `${edge}${theme.bg("selectedBg", ` ${clipped}${padding} `)}${edge}`;
|
|
207
|
-
});
|
|
208
|
-
return [top, ...content, bottom];
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function modalOverlayRows(rows: FooterRow[], width: number, height: number, theme: Theme): string[] {
|
|
212
|
-
const panelWidth = Math.min(width, 74);
|
|
213
|
-
const panel = borderedRows(rows, panelWidth, theme);
|
|
214
|
-
const left = Math.floor((width - panelWidth) / 2);
|
|
215
|
-
const top = Math.max(0, Math.floor((height - panel.length) / 2));
|
|
216
|
-
const screen = Array.from({ length: height }, () => " ".repeat(width));
|
|
217
|
-
panel.forEach((line, index) => {
|
|
218
|
-
const row = top + index;
|
|
219
|
-
if (row < height)
|
|
220
|
-
screen[row] = `${" ".repeat(left)}${line}${" ".repeat(Math.max(0, width - left - visibleWidth(line)))}`;
|
|
221
|
-
});
|
|
222
|
-
return screen;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function buildUsageModal(usage: CopilotUsage): { rows: FooterRow[]; clipboardText: string } {
|
|
74
|
+
function buildCopilotUsageModal(usage: CopilotUsage): ModalContent {
|
|
226
75
|
const [title, planText, quotaText, resetText] = formatUsageSummary(usage);
|
|
227
76
|
const progress = getUsageProgress(usage);
|
|
228
|
-
const rows:
|
|
77
|
+
const rows: ModalRow[] = [
|
|
229
78
|
{ text: title, style: "accent" },
|
|
230
79
|
{ text: "" },
|
|
231
80
|
{ text: planText },
|
|
@@ -234,99 +83,90 @@ function buildUsageModal(usage: CopilotUsage): { rows: FooterRow[]; clipboardTex
|
|
|
234
83
|
];
|
|
235
84
|
if (progress === undefined) rows.push({ text: "" });
|
|
236
85
|
else rows.push({ text: "" }, { text: "", progress, style: progress >= 90 ? "warning" : "success" }, { text: "" });
|
|
237
|
-
rows.push({ text: resetText }
|
|
86
|
+
rows.push({ text: resetText });
|
|
238
87
|
return { rows, clipboardText: [title, planText, quotaText, resetText].join("\n") };
|
|
239
88
|
}
|
|
240
89
|
|
|
241
|
-
async function
|
|
90
|
+
async function showCopilotUsage(ctx: ExtensionContext, isCurrent: () => boolean): Promise<void> {
|
|
91
|
+
const unavailableMessage = "/copilot-usage requires Pi TUI mode.";
|
|
242
92
|
if (ctx.mode !== "tui") {
|
|
243
|
-
ctx.ui.notify(
|
|
93
|
+
ctx.ui.notify(unavailableMessage, "warning");
|
|
244
94
|
return;
|
|
245
95
|
}
|
|
246
96
|
|
|
247
97
|
try {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
invalidate() {},
|
|
256
|
-
handleInput(data: string) {
|
|
257
|
-
if (keybindings.matches(data, "app.interrupt") || keybindings.matches(data, "tui.input.submit")) {
|
|
258
|
-
done(undefined);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
const key = data.toLowerCase();
|
|
262
|
-
if (key === "r") {
|
|
263
|
-
content.rows[content.rows.length - 1] = { text: "Refreshing usage…" };
|
|
264
|
-
tui.requestRender();
|
|
265
|
-
void refreshUsage(true)
|
|
266
|
-
.then((usage) => {
|
|
267
|
-
content = buildUsageModal(usage);
|
|
268
|
-
tui.requestRender();
|
|
269
|
-
})
|
|
270
|
-
.catch((error: unknown) => {
|
|
271
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
272
|
-
content.rows[content.rows.length - 1] = { text: `Refresh failed: ${message}`, style: "warning" };
|
|
273
|
-
tui.requestRender();
|
|
274
|
-
});
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
if (key === "c") {
|
|
278
|
-
void copyToClipboard(content.clipboardText)
|
|
279
|
-
.then(() => {
|
|
280
|
-
content.rows[content.rows.length - 1] = { text: "Copied plain text", style: "success" };
|
|
281
|
-
tui.requestRender();
|
|
282
|
-
})
|
|
283
|
-
.catch((error: unknown) => {
|
|
284
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
285
|
-
content.rows[content.rows.length - 1] = { text: `Copy failed: ${message}`, style: "warning" };
|
|
286
|
-
tui.requestRender();
|
|
287
|
-
});
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
tui.requestRender();
|
|
291
|
-
},
|
|
292
|
-
}),
|
|
98
|
+
const usage = await refreshUsage(true);
|
|
99
|
+
if (!isCurrent()) return;
|
|
100
|
+
const content = buildCopilotUsageModal(usage);
|
|
101
|
+
ctx.ui.setStatus(COPILOT_USAGE_STATUS_KEY, formatPlanUsage(usage));
|
|
102
|
+
await showModal(
|
|
103
|
+
ctx,
|
|
104
|
+
content,
|
|
293
105
|
{
|
|
294
|
-
|
|
295
|
-
|
|
106
|
+
refresh: async () => {
|
|
107
|
+
const usage = await refreshUsage(true);
|
|
108
|
+
if (!isCurrent()) throw new Error("Session ended during usage refresh.");
|
|
109
|
+
ctx.ui.setStatus(COPILOT_USAGE_STATUS_KEY, formatPlanUsage(usage));
|
|
110
|
+
return buildCopilotUsageModal(usage);
|
|
111
|
+
},
|
|
296
112
|
},
|
|
113
|
+
unavailableMessage,
|
|
297
114
|
);
|
|
298
115
|
} catch (error) {
|
|
299
|
-
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
116
|
+
if (isCurrent()) ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
300
117
|
}
|
|
301
118
|
}
|
|
302
119
|
|
|
303
120
|
export default function (pi: ExtensionAPI): void {
|
|
121
|
+
let currentContext: ExtensionContext | undefined;
|
|
122
|
+
let sessionGeneration = 0;
|
|
123
|
+
const isCurrent = (generation: number) => () => generation === sessionGeneration;
|
|
124
|
+
|
|
125
|
+
pi.events.on(OPEN_COPILOT_USAGE_EVENT, () => {
|
|
126
|
+
if (currentContext) void showCopilotUsage(currentContext, isCurrent(sessionGeneration));
|
|
127
|
+
});
|
|
128
|
+
|
|
304
129
|
pi.on("session_start", (_event, ctx) => {
|
|
305
|
-
|
|
306
|
-
ctx.
|
|
307
|
-
|
|
308
|
-
|
|
130
|
+
const generation = ++sessionGeneration;
|
|
131
|
+
if (ctx.mode !== "tui") {
|
|
132
|
+
currentContext = undefined;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
currentContext = ctx;
|
|
136
|
+
ctx.ui.setStatus(COPILOT_USAGE_STATUS_KEY, formatPlanUsage(cachedUsage) || undefined);
|
|
137
|
+
void refreshUsage()
|
|
138
|
+
.then((usage) => {
|
|
139
|
+
if (generation === sessionGeneration)
|
|
140
|
+
currentContext?.ui.setStatus(COPILOT_USAGE_STATUS_KEY, formatPlanUsage(usage));
|
|
141
|
+
})
|
|
142
|
+
.catch(() => {});
|
|
309
143
|
});
|
|
310
144
|
|
|
311
145
|
pi.on("agent_end", (_event, ctx) => {
|
|
312
|
-
if (ctx.mode
|
|
146
|
+
if (ctx.mode !== "tui") return;
|
|
147
|
+
currentContext = ctx;
|
|
148
|
+
const generation = sessionGeneration;
|
|
149
|
+
void refreshUsage()
|
|
150
|
+
.then((usage) => {
|
|
151
|
+
if (generation === sessionGeneration)
|
|
152
|
+
currentContext?.ui.setStatus(COPILOT_USAGE_STATUS_KEY, formatPlanUsage(usage));
|
|
153
|
+
})
|
|
154
|
+
.catch(() => {});
|
|
313
155
|
});
|
|
314
156
|
|
|
315
157
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
requestFooterRender = undefined;
|
|
320
|
-
}
|
|
158
|
+
sessionGeneration++;
|
|
159
|
+
if (ctx.mode === "tui") ctx.ui.setStatus(COPILOT_USAGE_STATUS_KEY, undefined);
|
|
160
|
+
currentContext = undefined;
|
|
321
161
|
});
|
|
322
162
|
|
|
323
163
|
pi.registerCommand("copilot-usage", {
|
|
324
164
|
description: "Show GitHub Copilot plan and quota usage",
|
|
325
|
-
handler: (_args, ctx) =>
|
|
165
|
+
handler: (_args, ctx) => showCopilotUsage(ctx, isCurrent(sessionGeneration)),
|
|
326
166
|
});
|
|
327
167
|
|
|
328
168
|
pi.registerShortcut("super+shift+u", {
|
|
329
169
|
description: "Show GitHub Copilot AI-credit usage",
|
|
330
|
-
handler: (ctx) =>
|
|
170
|
+
handler: (ctx) => showCopilotUsage(ctx, isCurrent(sessionGeneration)),
|
|
331
171
|
});
|
|
332
172
|
}
|
package/src/core.ts
CHANGED
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
type Usage = {
|
|
4
|
-
input?: number;
|
|
5
|
-
output?: number;
|
|
6
|
-
cacheRead?: number;
|
|
7
|
-
cacheWrite?: number;
|
|
8
|
-
cost?: { total?: number };
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
type UsageEntry = {
|
|
12
|
-
type?: string;
|
|
13
|
-
provider?: string;
|
|
14
|
-
usage?: Usage;
|
|
15
|
-
message?: { role?: string; provider?: string; usage?: Usage };
|
|
16
|
-
};
|
|
17
|
-
|
|
18
1
|
export type CopilotUsage = {
|
|
19
2
|
plan: string;
|
|
20
3
|
unit: "AI credits" | "premium requests";
|
|
@@ -26,14 +9,6 @@ export type CopilotUsage = {
|
|
|
26
9
|
unlimited: boolean;
|
|
27
10
|
};
|
|
28
11
|
|
|
29
|
-
export type SessionUsage = {
|
|
30
|
-
input: number;
|
|
31
|
-
output: number;
|
|
32
|
-
cacheRead: number;
|
|
33
|
-
cacheWrite: number;
|
|
34
|
-
cost: number;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
12
|
const planNames: Record<string, string> = {
|
|
38
13
|
business: "Copilot Business",
|
|
39
14
|
enterprise: "Copilot Enterprise",
|
|
@@ -108,12 +83,6 @@ export function parseCopilotUsage(data: unknown): CopilotUsage {
|
|
|
108
83
|
};
|
|
109
84
|
}
|
|
110
85
|
|
|
111
|
-
export function formatTokens(tokens: number): string {
|
|
112
|
-
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
113
|
-
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}K`;
|
|
114
|
-
return Math.round(tokens).toLocaleString("en-US");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
86
|
export function formatPlanUsage(usage?: CopilotUsage): string {
|
|
118
87
|
if (!usage) return "";
|
|
119
88
|
return usage.unlimited ? `${usage.plan} ∞` : `${usage.plan} ${usage.percentUsed ?? 0}%`;
|
|
@@ -137,46 +106,3 @@ export function formatUsageSummary(usage: CopilotUsage): [string, string, string
|
|
|
137
106
|
export function getUsageProgress(usage: CopilotUsage): number | undefined {
|
|
138
107
|
return usage.unlimited ? undefined : (usage.percentUsed ?? 0);
|
|
139
108
|
}
|
|
140
|
-
|
|
141
|
-
export function writeClipboard(text: string, command = "pbcopy"): Promise<void> {
|
|
142
|
-
return new Promise((resolve, reject) => {
|
|
143
|
-
const child = execFile(command, (error) => (error ? reject(error) : resolve()));
|
|
144
|
-
child.stdin?.on("error", reject);
|
|
145
|
-
child.stdin?.end(text);
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function formatProgressBar(percent: number, width: number): string {
|
|
150
|
-
const cells = Math.max(0, Math.floor(width));
|
|
151
|
-
const value = Number.isFinite(percent) ? Math.min(100, Math.max(0, percent)) : 0;
|
|
152
|
-
const filled = Math.round((value / 100) * cells);
|
|
153
|
-
return `${"█".repeat(filled)}${"░".repeat(cells - filled)}`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function summarizeSession(entries: readonly unknown[], provider?: string): SessionUsage {
|
|
157
|
-
const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
158
|
-
const add = (usage?: Usage) => {
|
|
159
|
-
if (!usage) return;
|
|
160
|
-
for (const key of ["input", "output", "cacheRead", "cacheWrite"] as const) {
|
|
161
|
-
const value = usage[key];
|
|
162
|
-
if (typeof value === "number" && Number.isFinite(value) && value > 0) total[key] += value;
|
|
163
|
-
}
|
|
164
|
-
const cost = usage.cost?.total;
|
|
165
|
-
if (typeof cost === "number" && Number.isFinite(cost) && cost > 0) total.cost += cost;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
for (const rawEntry of entries) {
|
|
169
|
-
if (!rawEntry || typeof rawEntry !== "object") continue;
|
|
170
|
-
const entry = rawEntry as UsageEntry;
|
|
171
|
-
if (
|
|
172
|
-
entry.type === "message" &&
|
|
173
|
-
entry.message?.role === "assistant" &&
|
|
174
|
-
(!provider || entry.message.provider === provider)
|
|
175
|
-
) {
|
|
176
|
-
add(entry.message.usage);
|
|
177
|
-
} else if (entry.type === "usage" && (!provider || entry.provider === provider)) {
|
|
178
|
-
add(entry.usage);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return total;
|
|
182
|
-
}
|
package/src/events.ts
ADDED