feat: add configurable pi statusbar extension
This commit is contained in:
106
modules/basic.ts
Normal file
106
modules/basic.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { homedir } from "node:os";
|
||||
import type { ModuleContext, RenderedModule } from "../types";
|
||||
|
||||
function formatTokens(count: number): string {
|
||||
if (count < 1000) return count.toString();
|
||||
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
||||
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
||||
if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
|
||||
return `${Math.round(count / 1000000)}M`;
|
||||
}
|
||||
|
||||
function sessionEntries(ctx: any): any[] {
|
||||
try {
|
||||
return ctx.sessionManager?.getEntries?.() ?? [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function totalCost(ctx: any): number {
|
||||
return sessionEntries(ctx).reduce((total, entry) => {
|
||||
const cost =
|
||||
entry?.type === "message" && entry.message?.role === "assistant"
|
||||
? entry.message.usage?.cost?.total
|
||||
: undefined;
|
||||
return (
|
||||
total + (typeof cost === "number" && Number.isFinite(cost) ? cost : 0)
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function isUsingSubscription(ctx: any): boolean {
|
||||
const provider = ctx.model?.provider;
|
||||
if (!provider) return false;
|
||||
try {
|
||||
return Boolean(ctx.modelRegistry?.isUsingOAuth?.(ctx.model));
|
||||
} catch {
|
||||
return provider === "anthropic" || provider === "openai-codex";
|
||||
}
|
||||
}
|
||||
|
||||
export function directoryModule(moduleCtx: ModuleContext): RenderedModule {
|
||||
const cwd = moduleCtx.ctx.sessionManager?.getCwd?.() ?? process.cwd();
|
||||
const home = homedir();
|
||||
let text = home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd;
|
||||
|
||||
// Git And Session Name
|
||||
const options = moduleCtx.config.modules.directory;
|
||||
if (options?.showGitBranch) {
|
||||
const branch = moduleCtx.footerData?.getGitBranch?.();
|
||||
if (branch) text = `${text} (${branch})`;
|
||||
}
|
||||
if (options?.showSessionName) {
|
||||
const sessionName = moduleCtx.ctx.sessionManager?.getSessionName?.();
|
||||
if (sessionName) text = `${text} • ${sessionName}`;
|
||||
}
|
||||
|
||||
return { text };
|
||||
}
|
||||
|
||||
export function contextModule(moduleCtx: ModuleContext): RenderedModule {
|
||||
const usage = moduleCtx.ctx.getContextUsage?.();
|
||||
const contextWindow =
|
||||
usage?.contextWindow ?? moduleCtx.ctx.model?.contextWindow ?? 0;
|
||||
const percent = usage?.percent;
|
||||
const showWindow = moduleCtx.config.modules.context?.showWindow ?? false;
|
||||
const value =
|
||||
percent === null || percent === undefined
|
||||
? "?"
|
||||
: `${Number(percent).toFixed(1)}%`;
|
||||
return {
|
||||
text: showWindow ? `${value} / ${formatTokens(contextWindow)}` : `${value}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function modelModule(moduleCtx: ModuleContext): RenderedModule {
|
||||
const model = moduleCtx.ctx.model;
|
||||
if (!model) return { text: "no-model" };
|
||||
|
||||
// Model Label
|
||||
const options = moduleCtx.config.modules.model;
|
||||
const provider = options?.showProvider ? `(${model.provider}) ` : "";
|
||||
let text = `${provider}${model.id}`;
|
||||
if (options?.showThinking && model.reasoning) {
|
||||
const thinking = moduleCtx.state.thinkingLevel ?? "off";
|
||||
text = thinking === "off" ? `${text} thinking off` : `${text} ${thinking}`;
|
||||
}
|
||||
return { text };
|
||||
}
|
||||
|
||||
export function thinkingModule(moduleCtx: ModuleContext): RenderedModule {
|
||||
if (!moduleCtx.ctx.model?.reasoning) return { text: "" };
|
||||
const level = moduleCtx.state.thinkingLevel ?? "off";
|
||||
if (level === "off" && moduleCtx.config.modules.thinking?.hideWhenOff)
|
||||
return { text: "" };
|
||||
return { text: `think ${level}` };
|
||||
}
|
||||
|
||||
export function costModule(moduleCtx: ModuleContext): RenderedModule {
|
||||
const cost = totalCost(moduleCtx.ctx);
|
||||
const subscription = isUsingSubscription(moduleCtx.ctx);
|
||||
if (!cost && !subscription) return { text: "" };
|
||||
if (subscription && moduleCtx.config.modules.cost?.showSubscription !== false)
|
||||
return { text: cost ? `$${cost.toFixed(3)} sub` : "sub" };
|
||||
return { text: `$${cost.toFixed(3)}` };
|
||||
}
|
||||
152
modules/usage.ts
Normal file
152
modules/usage.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { ModuleContext, ModuleSpec, RenderedModule, UsageTextPart, UsageWindows } from "../types";
|
||||
import type { UsageLimit, UsageReport } from "../usage";
|
||||
|
||||
function usageFraction(limit: UsageLimit): number | undefined {
|
||||
if (limit.amount.usedFraction !== undefined) return Math.min(Math.max(limit.amount.usedFraction, 0), 1);
|
||||
if (limit.amount.used !== undefined && limit.amount.limit !== undefined && limit.amount.limit > 0) {
|
||||
return Math.min(Math.max(limit.amount.used / limit.amount.limit, 0), 1);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function duration(valueMs: number | undefined): string | undefined {
|
||||
if (valueMs === undefined || !Number.isFinite(valueMs)) return undefined;
|
||||
const ms = Math.max(0, valueMs);
|
||||
const days = Math.floor(ms / 86_400_000);
|
||||
const hours = Math.floor((ms % 86_400_000) / 3_600_000);
|
||||
const minutes = Math.round((ms % 3_600_000) / 60_000);
|
||||
if (days > 0) return `${days}d${hours}h`;
|
||||
if (hours > 0) return `${hours}h${minutes}m`;
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
function compactNumber(value: number | undefined): string | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
|
||||
if (value >= 1_000) return `${(value / 1_000).toFixed(1)}k`;
|
||||
return `${Math.round(value)}`;
|
||||
}
|
||||
|
||||
function lerp(start: number, end: number, amount: number): number {
|
||||
return Math.round(start + (end - start) * amount);
|
||||
}
|
||||
|
||||
function fgRgb(text: string, r: number, g: number, b: number): string {
|
||||
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
||||
}
|
||||
|
||||
function usageColor(fraction: number): { r: number; g: number; b: number } {
|
||||
const green = { r: 34, g: 197, b: 94 };
|
||||
const yellow = { r: 234, g: 179, b: 8 };
|
||||
const red = { r: 239, g: 68, b: 68 };
|
||||
|
||||
// Continuous Green → Yellow → Red Gradient
|
||||
if (fraction <= 0.5) {
|
||||
const amount = fraction / 0.5;
|
||||
return {
|
||||
r: lerp(green.r, yellow.r, amount),
|
||||
g: lerp(green.g, yellow.g, amount),
|
||||
b: lerp(green.b, yellow.b, amount),
|
||||
};
|
||||
}
|
||||
|
||||
const amount = (fraction - 0.5) / 0.5;
|
||||
return {
|
||||
r: lerp(yellow.r, red.r, amount),
|
||||
g: lerp(yellow.g, red.g, amount),
|
||||
b: lerp(yellow.b, red.b, amount),
|
||||
};
|
||||
}
|
||||
|
||||
function findWindowLimits(report: UsageReport): UsageWindows {
|
||||
const windowId = (limit: UsageLimit) => (limit.window?.id ?? limit.scope.windowId ?? "").toLowerCase();
|
||||
const current = report.limits.find(limit => ["5h", "primary"].includes(windowId(limit))) ?? report.limits[0];
|
||||
const week = report.limits.find(limit => ["7d", "secondary"].includes(windowId(limit))) ?? report.limits.find(limit => limit !== current);
|
||||
return { current, week };
|
||||
}
|
||||
|
||||
function lineBar(limit: UsageLimit | undefined, width: number, theme?: any, color = true): string {
|
||||
if (width <= 0) return "";
|
||||
const fraction = limit ? usageFraction(limit) ?? 0 : 0;
|
||||
|
||||
const filled = Math.round(fraction * width);
|
||||
const empty = width - filled;
|
||||
const filledText = "━".repeat(filled);
|
||||
const emptyText = "─".repeat(empty);
|
||||
if (!color) return `${filledText}${emptyText}`;
|
||||
|
||||
const { r, g, b } = usageColor(fraction);
|
||||
const coloredFilled = filledText ? fgRgb(filledText, r, g, b) : "";
|
||||
const dimEmpty = emptyText && theme?.fg ? theme.fg("dim", emptyText) : emptyText;
|
||||
return `${coloredFilled}${dimEmpty}`;
|
||||
}
|
||||
|
||||
function selectedLimit(moduleCtx: ModuleContext, spec: ModuleSpec): UsageLimit | undefined {
|
||||
if (!moduleCtx.state.report || moduleCtx.state.error) return undefined;
|
||||
const windows = findWindowLimits(moduleCtx.state.report);
|
||||
const window = typeof spec === "string" ? "both" : spec.window ?? "both";
|
||||
if (window === "week") return windows.week;
|
||||
return windows.current ?? windows.week;
|
||||
}
|
||||
|
||||
function textPart(limit: UsageLimit, part: UsageTextPart): string | undefined {
|
||||
const fraction = usageFraction(limit);
|
||||
switch (part) {
|
||||
case "percent":
|
||||
return fraction === undefined ? undefined : `${Math.round(fraction * 100)}%`;
|
||||
case "time":
|
||||
return duration(limit.window?.resetsAt !== undefined ? limit.window.resetsAt - Date.now() : undefined);
|
||||
case "used":
|
||||
return compactNumber(limit.amount.used);
|
||||
case "limit":
|
||||
return compactNumber(limit.amount.limit);
|
||||
case "remaining":
|
||||
return compactNumber(limit.amount.remaining);
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackTextPart(part: UsageTextPart): string | undefined {
|
||||
switch (part) {
|
||||
case "percent":
|
||||
return "0%";
|
||||
case "time":
|
||||
return "∞";
|
||||
case "used":
|
||||
return "0";
|
||||
case "remaining":
|
||||
return "∞";
|
||||
case "limit":
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function textUsage(moduleCtx: ModuleContext, spec: ModuleSpec): string {
|
||||
// Usage Text Parts
|
||||
const parts = typeof spec === "string"
|
||||
? moduleCtx.config.modules.usage?.parts ?? ["percent", "time"]
|
||||
: spec.parts ?? moduleCtx.config.modules.usage?.parts ?? ["percent", "time"];
|
||||
const separator = typeof spec === "string"
|
||||
? moduleCtx.config.modules.usage?.separator ?? " | "
|
||||
: spec.separator ?? moduleCtx.config.modules.usage?.separator ?? " | ";
|
||||
|
||||
const limit = !moduleCtx.state.report || moduleCtx.state.error ? undefined : selectedLimit(moduleCtx, spec);
|
||||
return parts
|
||||
.map(part => limit ? textPart(limit, part) : fallbackTextPart(part))
|
||||
.filter((part): part is string => Boolean(part))
|
||||
.join(separator);
|
||||
}
|
||||
|
||||
export function usageModule(moduleCtx: ModuleContext, spec: ModuleSpec): RenderedModule {
|
||||
const style = typeof spec === "string" ? "line" : spec.style ?? "line";
|
||||
const grow = typeof spec === "string" ? 1 : spec.grow ?? 1;
|
||||
|
||||
if (style === "text") return { text: textUsage(moduleCtx, spec) };
|
||||
|
||||
return {
|
||||
grow,
|
||||
render(width: number, theme?: any): string {
|
||||
const limit = !moduleCtx.state.report || moduleCtx.state.error ? undefined : selectedLimit(moduleCtx, spec);
|
||||
return lineBar(limit, width, theme, moduleCtx.config.modules.usage?.colorBars !== false);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user