repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/tps/index.ts
Rawimport type {
ExtensionAPI,
ExtensionContext,
ThemeColor,
} from "@earendil-works/pi-coding-agent";
import { closeDebug, dbg } from "./src/debug.ts";
import {
offerFooterSegment,
removeFooterSegment,
} from "./src/pi-ext-footer-segment.ts";
const STATUS_ID = "tps";
const SEGMENT_ID = "tps";
const ICON = ""; // nf-md-speedometer U+F04C5
const IDLE_TEXT = "—/s";
const LIVE_UPDATE_MS = 250;
const CHARS_PER_TOKEN = 4;
type Usage = {
output?: number;
};
type AssistantLike = {
role?: unknown;
usage?: Usage;
};
type Clock = () => number;
function isAssistantMessage(message: unknown): message is AssistantLike {
return (
!!message &&
typeof message === "object" &&
(message as AssistantLike).role === "assistant"
);
}
function add(value: number | undefined): number {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
function sumAssistantOutput(messages: readonly unknown[]): number {
let output = 0;
for (const message of messages) {
if (!isAssistantMessage(message)) continue;
output += add(message.usage?.output);
}
return output;
}
function finalText(outputTokens: number, assistantElapsedMs: number): string {
if (outputTokens <= 0 || assistantElapsedMs <= 0) return IDLE_TEXT;
return `${(outputTokens / (assistantElapsedMs / 1000)).toFixed(1)}/s`;
}
function liveText(chars: number, elapsedMs: number): string {
const seconds = elapsedMs / 1000;
if (seconds <= 0) return "0.0/s";
return `${(chars / CHARS_PER_TOKEN / seconds).toFixed(1)}/s`;
}
function deltaLength(event: {
assistantMessageEvent: { type: string; delta?: string };
}): number {
const update = event.assistantMessageEvent;
return update.type.endsWith("_delta") && typeof update.delta === "string"
? update.delta.length
: 0;
}
class TpsRun {
private assistantStartMs: number | null = null;
private decodeStartMs: number | null = null;
private assistantElapsedMs = 0;
private measuredElapsedMs = 0;
private streamedChars = 0;
private measuredChars = 0;
private spanDeltaCount = 0;
private lastLiveUpdateMs = 0;
outcome: "measured" | "fallback" | "idle" = "idle";
constructor(private readonly now: Clock = Date.now) {}
startAgent(): void {
this.assistantStartMs = null;
this.decodeStartMs = null;
this.assistantElapsedMs = 0;
this.measuredElapsedMs = 0;
this.streamedChars = 0;
this.measuredChars = 0;
this.spanDeltaCount = 0;
this.lastLiveUpdateMs = 0;
}
startAssistant(): void {
if (this.assistantStartMs !== null) return;
this.assistantStartMs = this.now();
this.decodeStartMs = null;
this.spanDeltaCount = 0;
}
endAssistant(): void {
const now = this.now();
if (this.assistantStartMs !== null)
this.assistantElapsedMs += Math.max(0, now - this.assistantStartMs);
if (this.decodeStartMs !== null && this.spanDeltaCount > 1)
this.measuredElapsedMs += Math.max(0, now - this.decodeStartMs);
this.assistantStartMs = null;
this.decodeStartMs = null;
this.spanDeltaCount = 0;
}
addDelta(chars: number): void {
const safeChars = Math.max(0, chars);
if (safeChars === 0) return;
if (this.assistantStartMs === null) this.startAssistant();
this.streamedChars += safeChars;
this.spanDeltaCount++;
if (this.spanDeltaCount === 1) {
this.decodeStartMs = this.now();
return;
}
this.measuredChars += safeChars;
}
shouldPublishLive(): boolean {
const now = this.now();
if (now - this.lastLiveUpdateMs < LIVE_UPDATE_MS) return false;
this.lastLiveUpdateMs = now;
return true;
}
liveText(): string {
return liveText(this.measuredChars, this.currentMeasuredElapsedMs());
}
finish(messages: readonly unknown[]): string {
this.endAssistant();
const outputTokens = sumAssistantOutput(messages);
if (
this.measuredChars > 0 &&
this.streamedChars > 0 &&
this.measuredElapsedMs > 0
) {
this.outcome = outputTokens > 0 ? "measured" : "idle";
return finalText(
outputTokens * (this.measuredChars / this.streamedChars),
this.measuredElapsedMs,
);
}
this.outcome =
outputTokens > 0 && this.assistantElapsedMs > 0 ? "fallback" : "idle";
return finalText(outputTokens, this.assistantElapsedMs);
}
private currentMeasuredElapsedMs(): number {
if (this.decodeStartMs !== null && this.spanDeltaCount > 1)
return (
this.measuredElapsedMs + Math.max(0, this.now() - this.decodeStartMs)
);
if (this.streamedChars === 0 && this.assistantStartMs !== null)
return (
this.assistantElapsedMs +
Math.max(0, this.now() - this.assistantStartMs)
);
return this.measuredElapsedMs;
}
}
function clearSegment(pi: ExtensionAPI): void {
removeFooterSegment(pi, SEGMENT_ID);
}
function publishPresentation(
pi: ExtensionAPI,
ctx: ExtensionContext,
text: string | undefined,
color: ThemeColor,
): void {
if (!ctx.hasUI) return;
try {
let accepted = false;
if (ctx.mode === "tui") {
accepted =
text === undefined
? removeFooterSegment(pi, SEGMENT_ID)
: offerFooterSegment(pi, {
id: SEGMENT_ID,
icon: ICON,
text,
variants: [text.replace(/\.\d\//, "/")],
color,
zone: "llm",
order: 4,
});
}
if (!accepted) clearSegment(pi);
ctx.ui.setStatus(
STATUS_ID,
accepted || text === undefined ? undefined : `${ICON} ${text}`,
);
} catch {
// Ignore stale ctx during reload/switch/shutdown.
}
}
export default function tpsExtension(pi: ExtensionAPI) {
let run: TpsRun | null = null;
function publish(
ctx: ExtensionContext,
text: string | undefined,
color: ThemeColor = "accent",
): void {
publishPresentation(pi, ctx, text, color);
}
pi.on("session_start", async (_event, ctx) => {
dbg?.("session.start");
publish(ctx, IDLE_TEXT, "dim");
});
pi.on("agent_start", async (_event, ctx) => {
run = new TpsRun();
run.startAgent();
publish(ctx, "0.0/s", "dim");
});
pi.on("message_start", async (event, _ctx) => {
if (!run || !isAssistantMessage(event.message)) return;
run.startAssistant();
});
pi.on("message_update", async (event, ctx) => {
if (!run || !isAssistantMessage(event.message)) return;
run.startAssistant();
run.addDelta(deltaLength(event));
if (!run.shouldPublishLive()) return;
publish(ctx, run.liveText(), "accent");
});
pi.on("message_end", async (event, _ctx) => {
if (!run || !isAssistantMessage(event.message)) return;
run.endAssistant();
});
pi.on("agent_end", async (event, ctx) => {
if (!run) return;
const text = run.finish(event.messages);
dbg?.("rate.finish", { outcome: run.outcome });
run = null;
publish(ctx, text, text === IDLE_TEXT ? "dim" : "success");
});
pi.on("session_shutdown", async (_event, ctx) => {
dbg?.("session.shutdown");
closeDebug();
run = null;
publish(ctx, undefined);
});
}
export const __test = {
TpsRun,
deltaLength,
finalText,
liveText,
sumAssistantOutput,
};