pi config update
This commit is contained in:
191
pi/.pi/agent/extensions/footer-display.ts
Normal file
191
pi/.pi/agent/extensions/footer-display.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Footer Display Extension
|
||||
*
|
||||
* Replaces the built-in pi footer with a single clean line that assembles
|
||||
* status from all other extensions:
|
||||
*
|
||||
* \uEF85 | S ⣿⣶⣀⣀⣀ 34% 2h 55m | W ⣿⣿⣷⣀⣀ 68% ⟳ Fri 09:00 | C ⣿⣀⣀⣀⣀ 20% | Sonnet 4.6 | rust-analyzer | MCP: 1/2
|
||||
*
|
||||
* Status sources:
|
||||
* "claude-account" — set by claude-account-switch.ts → just the icon
|
||||
* "usage-bars" — set by usage-bars extension → S/W bars (pass-through)
|
||||
* ctx.getContextUsage() → C bar (rendered here)
|
||||
* ctx.model → model short name
|
||||
* "lsp" — set by lsp-pi extension → strip "LSP " prefix
|
||||
* "mcp" — set by pi-mcp-adapter → strip " servers" suffix
|
||||
*/
|
||||
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { truncateToWidth } from "@mariozechner/pi-tui";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Braille gradient bar — used here only for the context (C) bar
|
||||
// ---------------------------------------------------------------------------
|
||||
const BRAILLE_GRADIENT = "\u28C0\u28C4\u28E4\u28E6\u28F6\u28F7\u28FF";
|
||||
const BRAILLE_EMPTY = "\u28C0";
|
||||
const BAR_WIDTH = 5;
|
||||
|
||||
function renderBrailleBar(theme: any, value: number): string {
|
||||
const v = Math.max(0, Math.min(100, Math.round(value)));
|
||||
const levels = BRAILLE_GRADIENT.length - 1;
|
||||
const totalSteps = BAR_WIDTH * levels;
|
||||
const filledSteps = Math.round((v / 100) * totalSteps);
|
||||
const full = Math.floor(filledSteps / levels);
|
||||
const partial = filledSteps % levels;
|
||||
const empty = BAR_WIDTH - full - (partial ? 1 : 0);
|
||||
const color = v >= 90 ? "error" : v >= 70 ? "warning" : "success";
|
||||
const filled = BRAILLE_GRADIENT[BRAILLE_GRADIENT.length - 1]!.repeat(Math.max(0, full));
|
||||
const partialChar = partial ? BRAILLE_GRADIENT[partial]! : "";
|
||||
const emptyChars = BRAILLE_EMPTY.repeat(Math.max(0, empty));
|
||||
return theme.fg(color, filled + partialChar) + theme.fg("dim", emptyChars);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
function stripAnsi(text: string): string {
|
||||
return text.replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
||||
}
|
||||
|
||||
function getModelShortName(modelId: string): string {
|
||||
// claude-haiku-4-5 → "Haiku 4.5", claude-sonnet-4-6 → "Sonnet 4.6"
|
||||
const m = modelId.match(/^claude-([a-z]+)-([\d]+(?:-[\d]+)*)(?:-\d{8})?$/);
|
||||
if (m) {
|
||||
const family = m[1]!.charAt(0).toUpperCase() + m[1]!.slice(1);
|
||||
return `${family} ${m[2]!.replace(/-/g, ".")}`;
|
||||
}
|
||||
// claude-3-5-sonnet, claude-3-opus, etc.
|
||||
const m2 = modelId.match(/^claude-[\d-]+-([a-z]+)/);
|
||||
if (m2) return m2[1]!.charAt(0).toUpperCase() + m2[1]!.slice(1);
|
||||
return modelId.replace(/^claude-/, "");
|
||||
}
|
||||
|
||||
// Nerd Font codepoints matched to what claude-account-switch.ts emits
|
||||
const ICON_PERSONAL = "\uEF85"; // U+EF85 — home
|
||||
const ICON_WORK = "\uDB80\uDCD6"; // U+F00D6 — briefcase (surrogate pair)
|
||||
const ICON_UNKNOWN = "\uF420"; // U+F420 — claude default
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
let ctx: any = null;
|
||||
let tuiRef: any = null;
|
||||
let footerDataRef: any = null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Footer line builder — called on every render
|
||||
// ---------------------------------------------------------------------------
|
||||
function buildFooterLine(theme: any): string {
|
||||
const sep = theme.fg("dim", " · ");
|
||||
const pipeSep = theme.fg("dim", " | ");
|
||||
const parts: string[] = [];
|
||||
|
||||
const statuses: ReadonlyMap<string, string> =
|
||||
footerDataRef?.getExtensionStatuses?.() ?? new Map();
|
||||
|
||||
// 1. Current working directory
|
||||
const home = os.homedir();
|
||||
const cwd = process.cwd();
|
||||
const dir = cwd.startsWith(home)
|
||||
? "~" + path.sep + path.relative(home, cwd)
|
||||
: cwd;
|
||||
parts.push(theme.fg("dim", dir));
|
||||
|
||||
// 2. Account icon
|
||||
const acctRaw = statuses.get("claude-account");
|
||||
if (acctRaw !== undefined) {
|
||||
const clean = stripAnsi(acctRaw).trim();
|
||||
let icon: string;
|
||||
if (clean.includes("personal")) icon = ICON_PERSONAL;
|
||||
else if (clean.includes("work")) icon = ICON_WORK;
|
||||
else icon = ICON_UNKNOWN;
|
||||
parts.push(theme.fg("dim", icon));
|
||||
}
|
||||
|
||||
// 3. S / W usage bars + C bar — joined as one |-separated block
|
||||
const usageRaw = statuses.get("usage-bars");
|
||||
const contextUsage = ctx?.getContextUsage?.();
|
||||
{
|
||||
let block = usageRaw ?? "";
|
||||
if (contextUsage && contextUsage.percent !== null) {
|
||||
const pct = Math.round(contextUsage.percent);
|
||||
const cBar =
|
||||
theme.fg("muted", "C ") +
|
||||
renderBrailleBar(theme, pct) +
|
||||
" " +
|
||||
theme.fg("dim", `${pct}%`);
|
||||
block = block ? block + pipeSep + cBar : cBar;
|
||||
}
|
||||
if (block) parts.push(block);
|
||||
}
|
||||
|
||||
// 4. Model short name
|
||||
const modelId = ctx?.model?.id;
|
||||
if (modelId) parts.push(theme.fg("dim", getModelShortName(modelId)));
|
||||
|
||||
// 5. LSP — strip "LSP" prefix and activity dot
|
||||
const lspRaw = statuses.get("lsp");
|
||||
if (lspRaw) {
|
||||
const clean = stripAnsi(lspRaw).trim().replace(/^LSP\s*[•·]?\s*/i, "").trim();
|
||||
if (clean) parts.push(theme.fg("dim", clean));
|
||||
}
|
||||
|
||||
// 6. MCP — strip " servers" suffix
|
||||
const mcpRaw = statuses.get("mcp");
|
||||
if (mcpRaw) {
|
||||
const clean = stripAnsi(mcpRaw).trim().replace(/\s*servers?.*$/, "").trim();
|
||||
if (clean) parts.push(theme.fg("dim", clean));
|
||||
}
|
||||
|
||||
return parts.join(sep);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Footer installation
|
||||
// ---------------------------------------------------------------------------
|
||||
function installFooter(_ctx: any) {
|
||||
if (!_ctx?.hasUI) return;
|
||||
_ctx.ui.setFooter((_tui: any, theme: any, footerData: any) => {
|
||||
tuiRef = _tui;
|
||||
footerDataRef = footerData;
|
||||
const unsub = footerData.onBranchChange(() => _tui.requestRender());
|
||||
return {
|
||||
dispose: unsub,
|
||||
invalidate() {},
|
||||
render(width: number): string[] {
|
||||
return [truncateToWidth(buildFooterLine(theme) || "", width)];
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Event handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
pi.on("session_start", async (_event, _ctx) => {
|
||||
ctx = _ctx;
|
||||
installFooter(_ctx);
|
||||
});
|
||||
|
||||
pi.on("session_shutdown", (_event, _ctx) => {
|
||||
if (_ctx?.hasUI) _ctx.ui.setFooter(undefined);
|
||||
});
|
||||
|
||||
// Re-render after turns so context usage stays current
|
||||
pi.on("turn_end", (_event, _ctx) => {
|
||||
ctx = _ctx;
|
||||
if (tuiRef) tuiRef.requestRender();
|
||||
});
|
||||
|
||||
// Re-render when model changes (updates model name in footer)
|
||||
pi.on("model_select", (_event, _ctx) => {
|
||||
ctx = _ctx;
|
||||
if (tuiRef) tuiRef.requestRender();
|
||||
});
|
||||
|
||||
// Re-render when account switches (usage:update comes from usage-bars setStatus which
|
||||
// already triggers a render, but account icon needs a nudge too)
|
||||
pi.events.on("claude-account:switched", () => {
|
||||
if (tuiRef) tuiRef.requestRender();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user