Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/tps/index.ts

Raw
import 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,
};