Files
wow-pi/packages/wow-statusline/src/state.ts
kmou424 9f95a34c18 feat(statusline): added wow-statusline package for editor status bar
- Created wow-statusline package with configurable editor footer showing CWD, git branch, model/provider info, thinking level, token usage, and cost.
- Registered `/wow:statusline:show` command to inspect the module's enabled state.
- Added `statusline?` config option to WowConfig, supporting boolean or `{ enabled?: boolean }` shape.
- Wired statusline into wow-pi extension entry point as a workspace dependency and side-effect import.
2026-06-17 00:27:45 +08:00

105 lines
2.8 KiB
TypeScript

import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
import type {
FooterDataSnapshot,
SessionSnapshot,
UsageSummary,
} from "./types";
export interface StatuslineState {
currentCtx: ExtensionContext | null;
thinkingLevel: string;
statusEntries: Array<[string, string]>;
branch: string;
requestRender: (() => void) | null;
footerData: FooterDataSnapshot | null;
originalSetFooter: ((...args: unknown[]) => unknown) | null;
footerHijacked: boolean;
lastRenderSignature: string;
}
export function createState(): StatuslineState {
return {
currentCtx: null,
thinkingLevel: "off",
statusEntries: [],
branch: "",
requestRender: null,
footerData: null,
originalSetFooter: null,
footerHijacked: false,
lastRenderSignature: "",
};
}
export function snapshotFromContext(
state: StatuslineState,
ctx: ExtensionContext,
): SessionSnapshot {
return {
cwd: ctx.cwd,
branch: state.branch,
modelName: ctx.model?.name || ctx.model?.id || "no-model",
provider: ctx.model?.provider,
thinkingLevel: state.thinkingLevel,
usage: collectUsage(ctx),
statuses: [...state.statusEntries],
};
}
export function syncFooterSnapshot(state: StatuslineState): void {
if (!state.footerData) return;
state.branch = state.footerData.getGitBranch() || "";
const statuses = state.footerData.getExtensionStatuses();
if (statuses.size === 0) return;
state.statusEntries = Array.from(statuses.entries()).sort(([left], [right]) =>
left.localeCompare(right),
);
}
function collectUsage(ctx: ExtensionContext): UsageSummary {
let input = 0;
let output = 0;
let cost = 0;
try {
for (const entry of ctx.sessionManager.getBranch()) {
if (entry.type !== "message" || entry.message.role !== "assistant")
continue;
const usage = readUsage(entry.message);
if (!usage) continue;
input += usage.input;
output += usage.output;
cost += usage.cost;
}
} catch {
// Session access can fail during teardown or special modes.
}
return { input, output, cost };
}
function readUsage(message: unknown): UsageSummary | null {
if (!message || typeof message !== "object") return null;
const usage = (message as { usage?: unknown }).usage;
if (!usage || typeof usage !== "object") return null;
const input = readNumber((usage as { input?: unknown }).input);
const output = readNumber((usage as { output?: unknown }).output);
const costValue =
usage && typeof usage === "object"
? readNumber(
(
(usage as { cost?: unknown }).cost as
| { total?: unknown }
| undefined
)?.total,
)
: 0;
return { input, output, cost: costValue };
}
function readNumber(value: unknown): number {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}