@monotykamary/pi-better-grok 0.1.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/LICENSE +21 -0
- package/README.md +65 -0
- package/THIRD_PARTY_NOTICES.md +19 -0
- package/index.ts +666 -0
- package/package.json +66 -0
- package/src/config.ts +314 -0
- package/src/fast-controller.ts +82 -0
- package/src/format.ts +66 -0
- package/src/grok-auth.ts +242 -0
- package/src/identity.ts +7 -0
- package/src/paths.ts +17 -0
- package/src/usage-controller.ts +210 -0
- package/src/usage.ts +351 -0
package/index.ts
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better Grok for pi.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors pi-better-openai's UX for the xAI/Grok side: fast mode
|
|
5
|
+
* (reasoning_effort override), SuperGrok subscription usage in the footer,
|
|
6
|
+
* footer polish, and a settings picker. Usage rides the revision-pinned
|
|
7
|
+
* cli-chat-proxy.grok.com identity-first billing surface using pi's native
|
|
8
|
+
* xAI OAuth credential.
|
|
9
|
+
*/
|
|
10
|
+
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { Container, SettingsList, type SettingsListTheme } from "@earendil-works/pi-tui";
|
|
12
|
+
import { CONFIG_BASENAME, STATUS_KEY } from "./src/identity.ts";
|
|
13
|
+
import { formatTokens, sanitizeStatusText, truncateToWidth, visibleWidth } from "./src/format.ts";
|
|
14
|
+
import {
|
|
15
|
+
applySettingToRawConfig,
|
|
16
|
+
configPaths,
|
|
17
|
+
DEFAULT_CONFIG,
|
|
18
|
+
DEFAULT_SUPPORTED_MODELS,
|
|
19
|
+
FAST_SETTING_DESCRIPTORS,
|
|
20
|
+
FOOTER_SETTING_DESCRIPTORS,
|
|
21
|
+
normalizeModelKeys,
|
|
22
|
+
parseModelKey,
|
|
23
|
+
parseModels,
|
|
24
|
+
readRawConfig,
|
|
25
|
+
resolveConfig,
|
|
26
|
+
USAGE_SETTING_DESCRIPTORS,
|
|
27
|
+
writeConfig,
|
|
28
|
+
type ResolvedConfig,
|
|
29
|
+
} from "./src/config.ts";
|
|
30
|
+
import {
|
|
31
|
+
formatPercent,
|
|
32
|
+
formatResetCountdown,
|
|
33
|
+
formatUsageDetail,
|
|
34
|
+
formatUsageSnapshot,
|
|
35
|
+
parseUsageSnapshot,
|
|
36
|
+
requestGrokUsage,
|
|
37
|
+
UsageError,
|
|
38
|
+
type UsageSnapshot,
|
|
39
|
+
} from "./src/usage.ts";
|
|
40
|
+
import {
|
|
41
|
+
readGrokCliToken,
|
|
42
|
+
readPiStoredOAuthToken,
|
|
43
|
+
resolveGrokCredential,
|
|
44
|
+
XAI_PROVIDER_IDS,
|
|
45
|
+
} from "./src/grok-auth.ts";
|
|
46
|
+
import { currentModelKey, FastController, modelList, supportsFast } from "./src/fast-controller.ts";
|
|
47
|
+
import { isGrokSubscriptionModel, UsageController } from "./src/usage-controller.ts";
|
|
48
|
+
import { sep } from "node:path";
|
|
49
|
+
|
|
50
|
+
// pi-core's getSettingsListTheme pulls the host module graph into this
|
|
51
|
+
// extension's loader. The settings picker is a command-time surface, so the
|
|
52
|
+
// theme loads when it first opens.
|
|
53
|
+
let loadedSettingsListTheme: SettingsListTheme | undefined;
|
|
54
|
+
const loadSettingsListTheme = async (): Promise<SettingsListTheme> => {
|
|
55
|
+
loadedSettingsListTheme ??= (
|
|
56
|
+
await import("@earendil-works/pi-coding-agent")
|
|
57
|
+
).getSettingsListTheme();
|
|
58
|
+
return loadedSettingsListTheme;
|
|
59
|
+
};
|
|
60
|
+
const requireSettingsListTheme = (): SettingsListTheme => {
|
|
61
|
+
if (!loadedSettingsListTheme) {
|
|
62
|
+
throw new Error("Settings list theme accessed before /grok-settings preload");
|
|
63
|
+
}
|
|
64
|
+
return loadedSettingsListTheme;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
class DynamicBorder {
|
|
68
|
+
readonly #color: (text: string) => string;
|
|
69
|
+
|
|
70
|
+
constructor(color: (text: string) => string = (str) => str) {
|
|
71
|
+
this.#color = color;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
invalidate(): void {}
|
|
75
|
+
|
|
76
|
+
render(width: number): string[] {
|
|
77
|
+
return [this.#color("\u2500".repeat(Math.max(1, width)))];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const COMMAND = "grok-fast";
|
|
82
|
+
const USAGE_COMMAND = "grok-usage";
|
|
83
|
+
const SETTINGS_COMMAND = "grok-settings";
|
|
84
|
+
const FLAG = "grok-fast";
|
|
85
|
+
|
|
86
|
+
type SettingsPickerItem = {
|
|
87
|
+
id: string;
|
|
88
|
+
label: string;
|
|
89
|
+
description?: string;
|
|
90
|
+
currentValue: string;
|
|
91
|
+
values?: string[];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function hasTerminalUI(ctx: ExtensionContext): boolean {
|
|
95
|
+
return ctx.mode === "tui" || (ctx.mode === undefined && ctx.hasUI);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function abbreviateHomePath(
|
|
99
|
+
path: string,
|
|
100
|
+
home = process.env.HOME || process.env.USERPROFILE,
|
|
101
|
+
): string {
|
|
102
|
+
if (!home) return path;
|
|
103
|
+
if (path === home) return "~";
|
|
104
|
+
const homePrefix = home.endsWith(sep) ? home : `${home}${sep}`;
|
|
105
|
+
return path.startsWith(homePrefix) ? `~/${path.slice(homePrefix.length)}` : path;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default function betterGrok(pi: ExtensionAPI): void {
|
|
109
|
+
const fetchUsageSnapshot = async (ctx: ExtensionContext): Promise<UsageSnapshot> => {
|
|
110
|
+
const credential = await resolveGrokCredential(ctx);
|
|
111
|
+
if (!credential) {
|
|
112
|
+
throw new UsageError(
|
|
113
|
+
"auth",
|
|
114
|
+
"No xAI OAuth credential found. Run /login xai and select Use a subscription.",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const { snapshot } = await requestGrokUsage(credential);
|
|
118
|
+
return snapshot;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const fastController = new FastController();
|
|
122
|
+
let cachedConfig: ResolvedConfig | undefined;
|
|
123
|
+
let footerTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
124
|
+
let footerInstalled = false;
|
|
125
|
+
let statusInstalled = false;
|
|
126
|
+
let statusWidgetInstalled = false;
|
|
127
|
+
let contextUsageCached = false;
|
|
128
|
+
let cachedContextUsage: ReturnType<ExtensionContext["getContextUsage"]>;
|
|
129
|
+
let cachedContextLeafId: string | null | undefined;
|
|
130
|
+
let cachedContextModel: ExtensionContext["model"];
|
|
131
|
+
let sessionNameCached = false;
|
|
132
|
+
let cachedSessionNameLeafId: string | null | undefined;
|
|
133
|
+
let cachedSessionName: string | undefined;
|
|
134
|
+
const usageController = new UsageController(config, updateFooter, fetchUsageSnapshot);
|
|
135
|
+
|
|
136
|
+
function refresh(ctx: ExtensionContext): ResolvedConfig {
|
|
137
|
+
cachedConfig = resolveConfig(ctx.cwd || process.cwd());
|
|
138
|
+
return cachedConfig;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function config(ctx: ExtensionContext): ResolvedConfig {
|
|
142
|
+
return cachedConfig ?? refresh(ctx);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function persist(nextConfig: ResolvedConfig): void {
|
|
146
|
+
cachedConfig = {
|
|
147
|
+
...nextConfig,
|
|
148
|
+
active: fastController.active,
|
|
149
|
+
desiredActive: fastController.desiredActive,
|
|
150
|
+
};
|
|
151
|
+
if (!nextConfig.persistState) return;
|
|
152
|
+
writeConfig(nextConfig.configPath, {
|
|
153
|
+
...readRawConfig(nextConfig.configPath),
|
|
154
|
+
active: fastController.active,
|
|
155
|
+
desiredActive: fastController.desiredActive,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function refreshFooterTotals(ctx: ExtensionContext): void {
|
|
160
|
+
footerTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
161
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
162
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
163
|
+
footerTotals.input += entry.message.usage.input;
|
|
164
|
+
footerTotals.output += entry.message.usage.output;
|
|
165
|
+
footerTotals.cacheRead += entry.message.usage.cacheRead;
|
|
166
|
+
footerTotals.cacheWrite += entry.message.usage.cacheWrite;
|
|
167
|
+
footerTotals.cost += entry.message.usage.cost.total;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function invalidateContextUsage(): void {
|
|
172
|
+
contextUsageCached = false;
|
|
173
|
+
cachedContextUsage = undefined;
|
|
174
|
+
cachedContextLeafId = undefined;
|
|
175
|
+
cachedContextModel = undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function contextUsage(ctx: ExtensionContext): ReturnType<ExtensionContext["getContextUsage"]> {
|
|
179
|
+
const leafId = ctx.sessionManager.getLeafId();
|
|
180
|
+
const model = ctx.model;
|
|
181
|
+
if (!contextUsageCached || leafId !== cachedContextLeafId || model !== cachedContextModel) {
|
|
182
|
+
cachedContextUsage = ctx.getContextUsage();
|
|
183
|
+
contextUsageCached = true;
|
|
184
|
+
cachedContextLeafId = leafId;
|
|
185
|
+
cachedContextModel = model;
|
|
186
|
+
}
|
|
187
|
+
return cachedContextUsage;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function sessionName(ctx: ExtensionContext): string | undefined {
|
|
191
|
+
const leafId = ctx.sessionManager.getLeafId();
|
|
192
|
+
if (!sessionNameCached || leafId !== cachedSessionNameLeafId) {
|
|
193
|
+
cachedSessionName = ctx.sessionManager.getSessionName();
|
|
194
|
+
cachedSessionNameLeafId = leafId;
|
|
195
|
+
sessionNameCached = true;
|
|
196
|
+
}
|
|
197
|
+
return cachedSessionName;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function invalidateSessionName(): void {
|
|
201
|
+
sessionNameCached = false;
|
|
202
|
+
cachedSessionNameLeafId = undefined;
|
|
203
|
+
cachedSessionName = undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
pi.registerFlag(FLAG, {
|
|
207
|
+
description: "Start with Grok fast mode enabled (reasoning_effort override)",
|
|
208
|
+
type: "boolean",
|
|
209
|
+
default: false,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
function setActive(ctx: ExtensionContext, next: boolean): void {
|
|
213
|
+
const nextConfig = refresh(ctx);
|
|
214
|
+
fastController.setDesired(ctx, nextConfig, next);
|
|
215
|
+
persist(nextConfig);
|
|
216
|
+
updateFooter(ctx);
|
|
217
|
+
if (next && !fastController.active) {
|
|
218
|
+
ctx.ui.notify(fastController.unsupportedRequestMessage(ctx, nextConfig), "warning");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
ctx.ui.notify(fastController.stateText(nextConfig), "info");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function formatDebugStatus(ctx: ExtensionContext): string {
|
|
225
|
+
const cfg = config(ctx);
|
|
226
|
+
return [
|
|
227
|
+
...fastController.debugLines(ctx, cfg),
|
|
228
|
+
`Footer mode: ${cfg.footer.mode}`,
|
|
229
|
+
"",
|
|
230
|
+
usageController.formatDebug(ctx),
|
|
231
|
+
].join("\n");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
pi.registerCommand(COMMAND, {
|
|
235
|
+
description: "Toggle Grok fast mode",
|
|
236
|
+
handler: async (args, ctx) => {
|
|
237
|
+
const arg = args.trim().toLowerCase();
|
|
238
|
+
if (!arg) return setActive(ctx, !fastController.desiredActive);
|
|
239
|
+
ctx.ui.notify("Usage: /grok-fast", "error");
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
pi.registerCommand(USAGE_COMMAND, {
|
|
244
|
+
description: "Show Grok subscription usage status",
|
|
245
|
+
handler: async (args, ctx) => {
|
|
246
|
+
const arg = args.trim().toLowerCase();
|
|
247
|
+
if (arg === "debug") {
|
|
248
|
+
ctx.ui.notify(formatDebugStatus(ctx), "info");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
await usageController.refresh(ctx, { notify: true, force: true });
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
function settingsItems(
|
|
256
|
+
descriptors:
|
|
257
|
+
| typeof FOOTER_SETTING_DESCRIPTORS
|
|
258
|
+
| typeof USAGE_SETTING_DESCRIPTORS
|
|
259
|
+
| typeof FAST_SETTING_DESCRIPTORS,
|
|
260
|
+
cfg: ResolvedConfig,
|
|
261
|
+
): SettingsPickerItem[] {
|
|
262
|
+
return descriptors.map((descriptor) => ({
|
|
263
|
+
id: descriptor.id,
|
|
264
|
+
label: descriptor.label,
|
|
265
|
+
description: descriptor.description,
|
|
266
|
+
currentValue: descriptor.current(cfg),
|
|
267
|
+
values: descriptor.values ? [...descriptor.values] : undefined,
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function buildSettingsSections(cfg: ResolvedConfig): SettingsPickerItem[] {
|
|
272
|
+
return [
|
|
273
|
+
{ id: "section.footer", label: "Footer", currentValue: "" },
|
|
274
|
+
...settingsItems(FOOTER_SETTING_DESCRIPTORS, cfg),
|
|
275
|
+
{ id: "section.usage", label: "Usage", currentValue: "" },
|
|
276
|
+
...settingsItems(USAGE_SETTING_DESCRIPTORS, cfg),
|
|
277
|
+
{ id: "section.fast", label: "Fast mode", currentValue: "" },
|
|
278
|
+
...settingsItems(FAST_SETTING_DESCRIPTORS, cfg),
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function writeSetting(ctx: ExtensionContext, id: string, rawValue: string): void {
|
|
283
|
+
const cfg = refresh(ctx);
|
|
284
|
+
const current = readRawConfig(cfg.configPath);
|
|
285
|
+
if (id === "fast.enabled") {
|
|
286
|
+
fastController.setDesired(ctx, cfg, rawValue === "true");
|
|
287
|
+
}
|
|
288
|
+
const nextRawConfig = applySettingToRawConfig(current, id, rawValue, {
|
|
289
|
+
persistState: cfg.persistState,
|
|
290
|
+
active: fastController.active,
|
|
291
|
+
desiredActive: fastController.desiredActive,
|
|
292
|
+
});
|
|
293
|
+
writeConfig(cfg.configPath, nextRawConfig);
|
|
294
|
+
if (id === "usage.refreshIntervalMs") usageController.restart(ctx);
|
|
295
|
+
updateFooter(ctx);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function showSettingsPicker(ctx: ExtensionContext): Promise<void> {
|
|
299
|
+
if (!hasTerminalUI(ctx)) {
|
|
300
|
+
ctx.ui.notify("Better Grok settings require interactive TUI mode.", "warning");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
await loadSettingsListTheme();
|
|
304
|
+
try {
|
|
305
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
306
|
+
const container = new Container();
|
|
307
|
+
container.addChild(new DynamicBorder((text) => theme.fg("border", text)));
|
|
308
|
+
container.addChild(
|
|
309
|
+
new (class {
|
|
310
|
+
render(_width: number) {
|
|
311
|
+
const cfg = config(ctx);
|
|
312
|
+
return [
|
|
313
|
+
theme.bold(theme.fg("accent", "Better Grok Settings")),
|
|
314
|
+
theme.fg("muted", cfg.configPath),
|
|
315
|
+
"",
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
invalidate() {}
|
|
319
|
+
})(),
|
|
320
|
+
);
|
|
321
|
+
const settingsList = new SettingsList(
|
|
322
|
+
buildSettingsSections(refresh(ctx)),
|
|
323
|
+
8,
|
|
324
|
+
requireSettingsListTheme(),
|
|
325
|
+
(id, newValue) => {
|
|
326
|
+
if (!id.startsWith("section.")) writeSetting(ctx, id, newValue);
|
|
327
|
+
settingsList.updateValue(
|
|
328
|
+
id,
|
|
329
|
+
buildSettingsSections(config(ctx)).find((item) => item.id === id)?.currentValue ??
|
|
330
|
+
newValue,
|
|
331
|
+
);
|
|
332
|
+
tui.requestRender();
|
|
333
|
+
},
|
|
334
|
+
() => done(undefined),
|
|
335
|
+
{ enableSearch: true },
|
|
336
|
+
);
|
|
337
|
+
container.addChild(settingsList);
|
|
338
|
+
container.addChild(new DynamicBorder((text) => theme.fg("border", text)));
|
|
339
|
+
return {
|
|
340
|
+
render(width: number) {
|
|
341
|
+
return container.render(width);
|
|
342
|
+
},
|
|
343
|
+
invalidate() {
|
|
344
|
+
container.invalidate();
|
|
345
|
+
},
|
|
346
|
+
handleInput(data: string) {
|
|
347
|
+
settingsList.handleInput(data);
|
|
348
|
+
tui.requestRender();
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
} finally {
|
|
353
|
+
updateFooter(ctx);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
pi.registerCommand(SETTINGS_COMMAND, {
|
|
358
|
+
description: "Open Better Grok settings picker",
|
|
359
|
+
handler: async (_args, ctx) => {
|
|
360
|
+
await showSettingsPicker(ctx);
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
function installFooter(ctx: ExtensionContext): void {
|
|
365
|
+
if (footerInstalled) return;
|
|
366
|
+
footerInstalled = true;
|
|
367
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
368
|
+
const unsubscribe = footerData.onBranchChange?.(() => tui.requestRender());
|
|
369
|
+
return {
|
|
370
|
+
dispose: () => {
|
|
371
|
+
unsubscribe?.();
|
|
372
|
+
footerInstalled = false;
|
|
373
|
+
},
|
|
374
|
+
invalidate() {},
|
|
375
|
+
render(width: number): string[] {
|
|
376
|
+
const totalInput = footerTotals.input;
|
|
377
|
+
const totalOutput = footerTotals.output;
|
|
378
|
+
const totalCacheRead = footerTotals.cacheRead;
|
|
379
|
+
const totalCacheWrite = footerTotals.cacheWrite;
|
|
380
|
+
const totalCost = footerTotals.cost;
|
|
381
|
+
|
|
382
|
+
let pwd = abbreviateHomePath(ctx.sessionManager.getCwd());
|
|
383
|
+
const branch = footerData.getGitBranch?.();
|
|
384
|
+
if (branch) pwd = `${pwd} (${branch})`;
|
|
385
|
+
const currentSessionName = sessionName(ctx);
|
|
386
|
+
if (currentSessionName) pwd = `${pwd} • ${currentSessionName}`;
|
|
387
|
+
|
|
388
|
+
const parts: string[] = [];
|
|
389
|
+
if (totalInput) parts.push(`↑${formatTokens(totalInput)}`);
|
|
390
|
+
if (totalOutput) parts.push(`↓${formatTokens(totalOutput)}`);
|
|
391
|
+
if (totalCacheRead) parts.push(`R${formatTokens(totalCacheRead)}`);
|
|
392
|
+
if (totalCacheWrite) parts.push(`W${formatTokens(totalCacheWrite)}`);
|
|
393
|
+
|
|
394
|
+
const usingSubscription = ctx.model
|
|
395
|
+
? typeof ctx.modelRegistry?.isUsingOAuth === "function"
|
|
396
|
+
? ctx.modelRegistry.isUsingOAuth(ctx.model)
|
|
397
|
+
: false
|
|
398
|
+
: false;
|
|
399
|
+
if (totalCost || usingSubscription) {
|
|
400
|
+
parts.push(`$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const currentContextUsage = contextUsage(ctx);
|
|
404
|
+
const contextWindow = currentContextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
405
|
+
const contextPercentValue = currentContextUsage?.percent ?? 0;
|
|
406
|
+
const contextPercent =
|
|
407
|
+
currentContextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
|
|
408
|
+
const contextDisplay =
|
|
409
|
+
contextPercent === "?"
|
|
410
|
+
? `?/${formatTokens(contextWindow)} (auto)`
|
|
411
|
+
: `${contextPercent}%/${formatTokens(contextWindow)} (auto)`;
|
|
412
|
+
const contextText =
|
|
413
|
+
contextPercentValue > 90
|
|
414
|
+
? theme.fg("error", contextDisplay)
|
|
415
|
+
: contextPercentValue > 70
|
|
416
|
+
? theme.fg("warning", contextDisplay)
|
|
417
|
+
: contextDisplay;
|
|
418
|
+
parts.push(contextText);
|
|
419
|
+
|
|
420
|
+
const cfg = config(ctx);
|
|
421
|
+
const usageStatusLine = usageController.statusLine(ctx, cfg, usingSubscription);
|
|
422
|
+
const usageLine = usageStatusLine ? theme.fg("dim", usageStatusLine) : undefined;
|
|
423
|
+
|
|
424
|
+
let statsLeft = parts.join(" ");
|
|
425
|
+
let statsLeftWidth = visibleWidth(statsLeft);
|
|
426
|
+
if (statsLeftWidth > width) {
|
|
427
|
+
statsLeft = truncateToWidth(statsLeft, width, "...");
|
|
428
|
+
statsLeftWidth = visibleWidth(statsLeft);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const modelName = ctx.model?.id || "no-model";
|
|
432
|
+
const thinkingLevel = pi.getThinkingLevel();
|
|
433
|
+
const fastSuffix =
|
|
434
|
+
fastController.active && supportsFast(ctx, cfg.supportedModels) ? " fast" : "";
|
|
435
|
+
let rightWithoutProvider = modelName;
|
|
436
|
+
if (ctx.model?.reasoning) {
|
|
437
|
+
rightWithoutProvider =
|
|
438
|
+
thinkingLevel === "off"
|
|
439
|
+
? `${modelName}${fastSuffix} • thinking off`
|
|
440
|
+
: `${modelName}${fastSuffix} • ${thinkingLevel}`;
|
|
441
|
+
} else if (fastSuffix) {
|
|
442
|
+
rightWithoutProvider = `${modelName}${fastSuffix}`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let rightSide = rightWithoutProvider;
|
|
446
|
+
if ((footerData.getAvailableProviderCount?.() ?? 0) > 1 && ctx.model) {
|
|
447
|
+
const withProvider = `(${ctx.model.provider}) ${rightWithoutProvider}`;
|
|
448
|
+
if (statsLeftWidth + 2 + visibleWidth(withProvider) <= width) rightSide = withProvider;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const rightWidth = visibleWidth(rightSide);
|
|
452
|
+
const totalNeeded = statsLeftWidth + 2 + rightWidth;
|
|
453
|
+
let statsLine: string;
|
|
454
|
+
if (totalNeeded <= width) {
|
|
455
|
+
statsLine = statsLeft + " ".repeat(width - statsLeftWidth - rightWidth) + rightSide;
|
|
456
|
+
} else {
|
|
457
|
+
const availableForRight = width - statsLeftWidth - 2;
|
|
458
|
+
if (availableForRight > 0) {
|
|
459
|
+
const truncatedRight = truncateToWidth(rightSide, availableForRight, "");
|
|
460
|
+
statsLine =
|
|
461
|
+
statsLeft +
|
|
462
|
+
" ".repeat(Math.max(0, width - statsLeftWidth - visibleWidth(truncatedRight))) +
|
|
463
|
+
truncatedRight;
|
|
464
|
+
} else {
|
|
465
|
+
statsLine = statsLeft;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const textLines: string[] = [
|
|
470
|
+
truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "...")),
|
|
471
|
+
theme.fg("dim", statsLeft) + theme.fg("dim", statsLine.slice(statsLeft.length)),
|
|
472
|
+
];
|
|
473
|
+
if (usageLine) {
|
|
474
|
+
textLines.push(truncateToWidth(usageLine, width, theme.fg("dim", "...")));
|
|
475
|
+
}
|
|
476
|
+
const extensionStatuses = footerData.getExtensionStatuses?.();
|
|
477
|
+
if (extensionStatuses?.size) {
|
|
478
|
+
const statusLines = Array.from(extensionStatuses.entries())
|
|
479
|
+
.sort(([a], [b]) => String(a).localeCompare(String(b)))
|
|
480
|
+
.map(([, text]) => theme.fg("dim", sanitizeStatusText(String(text))));
|
|
481
|
+
textLines.push(
|
|
482
|
+
...statusLines.map((line) => truncateToWidth(line, width, theme.fg("dim", "..."))),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
return textLines;
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function clearFooter(ctx: ExtensionContext): void {
|
|
492
|
+
if (!footerInstalled) return;
|
|
493
|
+
ctx.ui.setFooter(undefined);
|
|
494
|
+
footerInstalled = false;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function setStatus(ctx: ExtensionContext, text: string | undefined): void {
|
|
498
|
+
if (!text && !statusInstalled) return;
|
|
499
|
+
ctx.ui.setStatus(STATUS_KEY, text);
|
|
500
|
+
statusInstalled = text !== undefined;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function setStatusWidget(ctx: ExtensionContext, text: string | undefined): void {
|
|
504
|
+
if (!text && !statusWidgetInstalled) return;
|
|
505
|
+
ctx.ui.setWidget(
|
|
506
|
+
STATUS_KEY,
|
|
507
|
+
text
|
|
508
|
+
? (_tui, theme) => ({
|
|
509
|
+
invalidate() {},
|
|
510
|
+
render(width: number): string[] {
|
|
511
|
+
const line = theme.fg("dim", sanitizeStatusText(text));
|
|
512
|
+
return [truncateToWidth(line, width, theme.fg("dim", "..."))];
|
|
513
|
+
},
|
|
514
|
+
})
|
|
515
|
+
: undefined,
|
|
516
|
+
{ placement: "belowEditor" },
|
|
517
|
+
);
|
|
518
|
+
statusWidgetInstalled = text !== undefined;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function updateFooter(ctx: ExtensionContext): void {
|
|
522
|
+
const cfg = config(ctx);
|
|
523
|
+
|
|
524
|
+
if (!hasTerminalUI(ctx)) {
|
|
525
|
+
if (cfg.footer.mode === "off") {
|
|
526
|
+
setStatus(ctx, undefined);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const fast = fastController.statusSegment(ctx, cfg);
|
|
530
|
+
const usage = usageController.statusLine(ctx, cfg);
|
|
531
|
+
setStatus(ctx, [fast, usage].filter(Boolean).join(" | ") || undefined);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (cfg.footer.mode === "replace") {
|
|
536
|
+
setStatus(ctx, undefined);
|
|
537
|
+
setStatusWidget(ctx, undefined);
|
|
538
|
+
installFooter(ctx);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
clearFooter(ctx);
|
|
543
|
+
setStatus(ctx, undefined);
|
|
544
|
+
|
|
545
|
+
if (cfg.footer.mode === "off") {
|
|
546
|
+
setStatusWidget(ctx, undefined);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const fast = fastController.statusSegment(ctx, cfg);
|
|
551
|
+
const usage = usageController.statusLine(ctx, cfg);
|
|
552
|
+
setStatusWidget(ctx, [fast, usage].filter(Boolean).join(" | ") || undefined);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
pi.on("session_start", (_event, ctx) => {
|
|
556
|
+
invalidateContextUsage();
|
|
557
|
+
invalidateSessionName();
|
|
558
|
+
const nextConfig = refresh(ctx);
|
|
559
|
+
fastController.initializeForSession(ctx, nextConfig, pi.getFlag(FLAG) === true);
|
|
560
|
+
if (
|
|
561
|
+
fastController.desiredActive !== nextConfig.desiredActive ||
|
|
562
|
+
fastController.active !== nextConfig.active
|
|
563
|
+
) {
|
|
564
|
+
persist(nextConfig);
|
|
565
|
+
}
|
|
566
|
+
if (fastController.desiredActive && !fastController.active) {
|
|
567
|
+
ctx.ui.notify(fastController.unsupportedRequestMessage(ctx, nextConfig), "warning");
|
|
568
|
+
}
|
|
569
|
+
refreshFooterTotals(ctx);
|
|
570
|
+
updateFooter(ctx);
|
|
571
|
+
usageController.start(ctx);
|
|
572
|
+
if (fastController.active) ctx.ui.notify(fastController.stateText(nextConfig), "info");
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
576
|
+
invalidateContextUsage();
|
|
577
|
+
updateFooter(ctx);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
pi.on("turn_end", (event, ctx) => {
|
|
581
|
+
invalidateContextUsage();
|
|
582
|
+
if (event.message?.role === "assistant") {
|
|
583
|
+
footerTotals.input += event.message.usage.input;
|
|
584
|
+
footerTotals.output += event.message.usage.output;
|
|
585
|
+
footerTotals.cacheRead += event.message.usage.cacheRead;
|
|
586
|
+
footerTotals.cacheWrite += event.message.usage.cacheWrite;
|
|
587
|
+
footerTotals.cost += event.message.usage.cost.total;
|
|
588
|
+
} else {
|
|
589
|
+
refreshFooterTotals(ctx);
|
|
590
|
+
}
|
|
591
|
+
updateFooter(ctx);
|
|
592
|
+
void usageController.refresh(ctx);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
596
|
+
invalidateContextUsage();
|
|
597
|
+
refreshFooterTotals(ctx);
|
|
598
|
+
updateFooter(ctx);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
602
|
+
invalidateContextUsage();
|
|
603
|
+
refreshFooterTotals(ctx);
|
|
604
|
+
updateFooter(ctx);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
pi.on("model_select", (_event, ctx) => {
|
|
608
|
+
invalidateContextUsage();
|
|
609
|
+
const cfg = config(ctx);
|
|
610
|
+
const wasActive = fastController.active;
|
|
611
|
+
fastController.applyDesiredState(ctx, cfg);
|
|
612
|
+
if (fastController.active !== wasActive) {
|
|
613
|
+
persist(cfg);
|
|
614
|
+
ctx.ui.notify(
|
|
615
|
+
fastController.active
|
|
616
|
+
? fastController.stateText(cfg)
|
|
617
|
+
: fastController.inactiveForModelMessage(ctx),
|
|
618
|
+
fastController.active ? "info" : "warning",
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
updateFooter(ctx);
|
|
622
|
+
void usageController.refresh(ctx, { force: true });
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
pi.on("session_shutdown", () => {
|
|
626
|
+
invalidateContextUsage();
|
|
627
|
+
invalidateSessionName();
|
|
628
|
+
usageController.shutdown();
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
632
|
+
return fastController.injectProviderPayload(event, ctx, config(ctx));
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
pi.on("message_start", invalidateContextUsage);
|
|
636
|
+
pi.on("message_update", invalidateContextUsage);
|
|
637
|
+
pi.on("message_end", invalidateContextUsage);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export const _test = {
|
|
641
|
+
CONFIG_BASENAME,
|
|
642
|
+
STATUS_KEY,
|
|
643
|
+
DEFAULT_SUPPORTED_MODELS,
|
|
644
|
+
DEFAULT_CONFIG,
|
|
645
|
+
configPaths,
|
|
646
|
+
abbreviateHomePath,
|
|
647
|
+
parseModelKey,
|
|
648
|
+
parseModels,
|
|
649
|
+
normalizeModelKeys,
|
|
650
|
+
resolveConfig,
|
|
651
|
+
readRawConfig,
|
|
652
|
+
supportsFast,
|
|
653
|
+
modelList,
|
|
654
|
+
currentModelKey,
|
|
655
|
+
isGrokSubscriptionModel,
|
|
656
|
+
parseUsageSnapshot,
|
|
657
|
+
formatPercent,
|
|
658
|
+
formatResetCountdown,
|
|
659
|
+
formatUsageSnapshot,
|
|
660
|
+
formatUsageDetail,
|
|
661
|
+
readPiStoredOAuthToken,
|
|
662
|
+
readGrokCliToken,
|
|
663
|
+
XAI_PROVIDER_IDS,
|
|
664
|
+
UsageController,
|
|
665
|
+
FastController,
|
|
666
|
+
};
|