Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi
This commit is contained in:
124
extensions/footer.ts
Normal file
124
extensions/footer.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
// ABOUTME: Footer widget displaying model name, context percentage + window size, and working directory.
|
||||
// ABOUTME: Shows context usage warnings; core pi framework handles actual auto-compaction.
|
||||
/**
|
||||
* Footer — Dark status bar with model · context % / window · directory.
|
||||
*
|
||||
* Context compaction is handled by pi's core _runAutoCompaction which properly
|
||||
* emits auto_compaction_start/end events. The interactive-mode handles these
|
||||
* events by calling rebuildChatFromMessages() to clear and re-render the UI.
|
||||
*
|
||||
* Previously, this extension called ctx.compact() directly which bypassed
|
||||
* the auto_compaction events, leaving stale UI components that caused
|
||||
* doubled/artifact rendering after compaction.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
||||
import { basename, dirname } from "node:path";
|
||||
import { applyExtensionDefaults } from "./lib/themeMap.ts";
|
||||
import { shouldWarnForCompaction, getProactiveCompactionPhase } from "./lib/context-gate.ts";
|
||||
|
||||
/** Turn a model name like "Claude 4 Opus" into "opus 4" */
|
||||
function shortModelName(name: string | undefined): string {
|
||||
if (!name) return "no model";
|
||||
const cleaned = name.replace(/^claude\s*/i, "").trim();
|
||||
const tokens = cleaned.split(/\s+/);
|
||||
const versions: string[] = [];
|
||||
const words: string[] = [];
|
||||
for (const token of tokens) {
|
||||
if (/^[\d.]+$/.test(token)) versions.push(token);
|
||||
else words.push(token.toLowerCase());
|
||||
}
|
||||
const parts = [...words, ...versions];
|
||||
return parts.join(" ") || name.toLowerCase();
|
||||
}
|
||||
|
||||
/** Format a token count into compact K/M notation: 200K, 1.2M */
|
||||
export function formatTokens(n: number): string {
|
||||
if (n < 1000) return String(Math.round(n));
|
||||
if (n < 1_000_000) {
|
||||
const k = n / 1000;
|
||||
return k % 1 === 0 ? `${k}K` : `${parseFloat(k.toFixed(1))}K`;
|
||||
}
|
||||
const m = n / 1_000_000;
|
||||
return m % 1 === 0 ? `${m}M` : `${parseFloat(m.toFixed(1))}M`;
|
||||
}
|
||||
|
||||
/** Thinking level → labeled indicator */
|
||||
function thinkingIndicator(level: string | undefined, theme: any): string {
|
||||
const label = level || "off";
|
||||
const color = label === "off" ? "dim" : label === "high" || label === "xhigh" ? "warning" : "accent";
|
||||
return theme.fg("dim", "thinking: ") + theme.fg(color, theme.bold(label));
|
||||
}
|
||||
|
||||
/** Last two path components: "Github-Work/pi-vs-claude-code" */
|
||||
function shortDir(cwd: string): string {
|
||||
const child = basename(cwd);
|
||||
const parent = basename(dirname(cwd));
|
||||
return parent ? `${parent}/${child}` : child;
|
||||
}
|
||||
|
||||
function setupFooter(pi: ExtensionAPI, ctx: any, onUnsub: (unsub: () => void) => void) {
|
||||
ctx.ui.setFooter((tui: any, theme: any, footerData: any) => {
|
||||
const unsub = footerData.onBranchChange(() => tui.requestRender());
|
||||
onUnsub(unsub);
|
||||
return {
|
||||
dispose: unsub,
|
||||
invalidate() {},
|
||||
render(width: number): string[] {
|
||||
const model = shortModelName(ctx.model?.name);
|
||||
const usage = ctx.getContextUsage();
|
||||
const contextWindow = ctx.model?.contextWindow || 0;
|
||||
|
||||
let usageStr = "–";
|
||||
if (usage?.percent != null) {
|
||||
const pct = `${Math.round(usage.percent)}%`;
|
||||
if (contextWindow > 0) {
|
||||
usageStr = `${pct} / ${formatTokens(contextWindow)}`;
|
||||
} else {
|
||||
usageStr = pct;
|
||||
}
|
||||
}
|
||||
|
||||
const dir = shortDir(ctx.cwd);
|
||||
const thinking = thinkingIndicator(pi.getThinkingLevel?.(), theme);
|
||||
const sep = theme.fg("dim", " | ");
|
||||
const modelStr = theme.fg("accent", theme.bold(model));
|
||||
const leftContent = ` ` + modelStr + sep + theme.fg("dim", usageStr) + sep + theme.fg("dim", dir);
|
||||
const rightContent = thinking + ` `;
|
||||
|
||||
const leftWidth = visibleWidth(leftContent);
|
||||
const rightWidth = visibleWidth(rightContent);
|
||||
const gap = Math.max(1, width - leftWidth - rightWidth);
|
||||
const line = leftContent + " ".repeat(gap) + rightContent;
|
||||
|
||||
return [truncateToWidth(line, width, "")];
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
let branchUnsub: (() => void) | null = null;
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
applyExtensionDefaults(import.meta.url, ctx);
|
||||
setupFooter(pi, ctx, (unsub) => {
|
||||
branchUnsub = unsub;
|
||||
});
|
||||
});
|
||||
|
||||
// No tool_call blocking — core auto-compaction handles compaction properly
|
||||
// via auto_compaction_start/end events which trigger UI rebuild.
|
||||
|
||||
// Footer no longer shows context warnings — memory-cycle.ts handles
|
||||
// proactive compaction with two-phase inject (70% prep, 80% hard stop).
|
||||
// The footer just renders the percentage in the status bar.
|
||||
|
||||
pi.on("session_shutdown", async () => {
|
||||
if (branchUnsub) {
|
||||
branchUnsub();
|
||||
branchUnsub = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user