mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 19:05:11 +00:00
Extensions can now replace the built-in footer with a custom component: - setFooter(factory) replaces with custom component - setFooter(undefined) restores built-in footer Includes example extension demonstrating context usage display. Closes #481
86 lines
2.9 KiB
TypeScript
86 lines
2.9 KiB
TypeScript
/**
|
|
* Custom Footer Extension
|
|
*
|
|
* Demonstrates ctx.ui.setFooter() for replacing the built-in footer
|
|
* with a custom component showing session context usage.
|
|
*/
|
|
|
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
|
|
export default function (pi: ExtensionAPI) {
|
|
let isCustomFooter = false;
|
|
|
|
// Toggle custom footer with /footer command
|
|
pi.registerCommand("footer", {
|
|
description: "Toggle custom footer showing context usage",
|
|
handler: async (_args, ctx) => {
|
|
isCustomFooter = !isCustomFooter;
|
|
|
|
if (isCustomFooter) {
|
|
ctx.ui.setFooter((_tui, theme) => {
|
|
return {
|
|
render(width: number): string[] {
|
|
// Calculate usage from branch entries
|
|
let totalInput = 0;
|
|
let totalOutput = 0;
|
|
let totalCost = 0;
|
|
let lastAssistant: AssistantMessage | undefined;
|
|
|
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
const msg = entry.message as AssistantMessage;
|
|
totalInput += msg.usage.input;
|
|
totalOutput += msg.usage.output;
|
|
totalCost += msg.usage.cost.total;
|
|
lastAssistant = msg;
|
|
}
|
|
}
|
|
|
|
// Context percentage from last assistant message
|
|
const contextTokens = lastAssistant
|
|
? lastAssistant.usage.input +
|
|
lastAssistant.usage.output +
|
|
lastAssistant.usage.cacheRead +
|
|
lastAssistant.usage.cacheWrite
|
|
: 0;
|
|
const contextWindow = ctx.model?.contextWindow || 0;
|
|
const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
|
|
|
// Format tokens
|
|
const fmt = (n: number) => (n < 1000 ? `${n}` : `${(n / 1000).toFixed(1)}k`);
|
|
|
|
// Build footer line
|
|
const left = [
|
|
theme.fg("dim", `↑${fmt(totalInput)}`),
|
|
theme.fg("dim", `↓${fmt(totalOutput)}`),
|
|
theme.fg("dim", `$${totalCost.toFixed(3)}`),
|
|
].join(" ");
|
|
|
|
// Color context percentage based on usage
|
|
let contextStr = `${contextPercent.toFixed(1)}%`;
|
|
if (contextPercent > 90) {
|
|
contextStr = theme.fg("error", contextStr);
|
|
} else if (contextPercent > 70) {
|
|
contextStr = theme.fg("warning", contextStr);
|
|
} else {
|
|
contextStr = theme.fg("success", contextStr);
|
|
}
|
|
|
|
const right = `${contextStr} ${theme.fg("dim", ctx.model?.id || "no model")}`;
|
|
const padding = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(right)));
|
|
|
|
return [truncateToWidth(left + padding + right, width)];
|
|
},
|
|
invalidate() {},
|
|
};
|
|
});
|
|
ctx.ui.notify("Custom footer enabled", "info");
|
|
} else {
|
|
ctx.ui.setFooter(undefined);
|
|
ctx.ui.notify("Built-in footer restored", "info");
|
|
}
|
|
},
|
|
});
|
|
}
|