Commit c8c42236c058

Vincent Demeester <vincent@sbr.pm>
2026-02-13 13:40:24
feat(pi): added custom header extension with green-to-blue gradient
Added custom pi header extension that: - Displays π symbol in Braille ASCII art with green-to-blue gradient - Shows version information - Displays extension issues in header if present - Adds /resources command for inline display of loaded resources (extensions, skills, prompts, themes, context files) - Works with quietStartup setting to replace default header The /resources command displays information inline like /todos, showing all loaded extensions with descriptions, skills from Claude, prompt templates, available themes (with current theme marked), and detected AGENTS.md context files.
1 parent 61eae77
Changed files (1)
dots
pi
agent
dots/pi/agent/extensions/custom-header.ts
@@ -0,0 +1,216 @@
+/**
+ * Custom Header Extension
+ *
+ * Displays a custom header with:
+ * - Pi (π) mathematical symbol in ASCII art
+ * - Version information
+ * - Extension issues (if any) displayed in header
+ * - /resources command to show loaded extensions/skills/prompts inline
+ */
+
+import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
+import { VERSION } from "@mariozechner/pi-coding-agent";
+
+// Pi symbol ASCII art (using Braille patterns) with green-to-blue gradient
+function getPiArt(theme: Theme): string[] {
+	// Green to blue gradient colors - from bright green at top to blue at bottom
+	const color1 = "\x1b[38;2;0;255;100m";    // Bright green
+	const color2 = "\x1b[38;2;0;240;110m";    // Green
+	const color3 = "\x1b[38;2;0;220;125m";    // Green-cyan
+	const color4 = "\x1b[38;2;0;195;145m";    // Cyan-green
+	const color5 = "\x1b[38;2;0;165;170m";    // Cyan
+	const color6 = "\x1b[38;2;0;130;195m";    // Cyan-blue
+	const color7 = "\x1b[38;2;0;95;220m";     // Blue-cyan
+	const color8 = "\x1b[38;2;0;60;240m";     // Blue
+	
+	return [
+		"",
+		color1 + "⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⡀⠀",
+		color2 + "⠀⠀⠀⠀⠀⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄",
+		color2 + "⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠀",
+		color3 + "⠀⠀⣴⣿⣿⣿⣿⣿⡟⠉⠀⠀⠈⢻⣿⣿⣿⣿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
+		color3 + "⢀⣾⣿⣿⣿⣿⡿⣻⡇⠀⠀⠄⠕⢹⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
+		color4 + "⠈⢿⣿⣿⡿⠋⡃⢸⣿⠓⠄⠄⠀⢸⣿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
+		color4 + "⠀⠀⠈⠁⠀⠀⠌⠀⣿⡇⠀⠐⠀⣾⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
+		color5 + "⠀⠀⠀⠀⠀⠀⠘⠀⡇⡇⠀⠈⠀⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
+		color5 + "⠀⠀⠀⠀⠀⠀⡎⠐⣷⣷⠀⠀⠀⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
+		color6 + "⠀⠀⠀⠀⠀⠀⡏⠀⣷⡇⠈⠀⠀⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀",
+		color6 + "⠀⠀⠀⠀⢈⠀⡎⠸⣿⡇⠀⠀⠀⣿⣿⣿⣿⣿⡠⠀⠀⠀⣰⣿⣿⣿⣦⠀⠀⠀",
+		color7 + "⠀⠀⠀⠀⠁⠀⢹⢰⣿⡇⠀⠀⠀⣿⣿⣿⣿⣿⣄⡀⠀⣰⣿⣿⣿⣿⡿⠀⠀⠀",
+		color7 + "⠀⠀⠀⠀⠀⠀⠼⢿⣿⡇⠀⠀⠀⠘⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀",
+		color8 + "⠀⠀⠀⠀⠀⠀⠀⠀⠃⠀⠀⠀⠀⠀⠈⠙⢿⣿⣿⣿⣿⣿⣿⡿⠟⠁⠀⠀⠀⠀",
+		color8 + "⠀⠀⠀⠀⠀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀",
+		"",
+	];
+}
+
+// Track extension issues from pi's startup
+let extensionIssues: string[] = [];
+
+export default function (pi: ExtensionAPI) {
+	// Capture extension issues from the console/logs if available
+	// This is a placeholder - we'd need pi to expose these via an event or API
+	pi.on("session_start", async (_event, ctx) => {
+		// TODO: Check if pi exposes extension errors/warnings
+		// For now, we'll show issues in the header if they exist
+	});
+
+	// Set custom header on load
+	pi.on("session_start", async (_event, ctx) => {
+		if (ctx.hasUI) {
+			ctx.ui.setHeader((_tui, theme) => {
+				return {
+					render(_width: number): string[] {
+						const artLines = getPiArt(theme);
+						const versionLine = `    ${theme.fg("muted", "shitty coding agent")} ${theme.fg("dim", `v${VERSION}`)}`;
+						const hintLine = `    ${theme.fg("dim", "Type /resources to view loaded extensions")}`;
+						
+						const lines = [...artLines, versionLine, hintLine];
+						
+						// Add extension issues if any
+						if (extensionIssues.length > 0) {
+							lines.push("");
+							lines.push(`    ${theme.fg("warning", "⚠️  Extension Issues:")}`);
+							for (const issue of extensionIssues) {
+								lines.push(`    ${theme.fg("warning", "  • " + issue)}`);
+							}
+						}
+						
+						return lines;
+					},
+					invalidate() {},
+				};
+			});
+		}
+	});
+
+	// Command to show resources inline (like /todos)
+	pi.registerCommand("resources", {
+		description: "Show loaded extensions, skills, prompts, themes, and context files",
+		handler: async (_args, ctx) => {
+			// Get extension info from commands
+			const commands = pi.getCommands();
+			
+			const extensions = commands
+				.filter(cmd => cmd.source === "extension")
+				.map(cmd => ({
+					name: cmd.name,
+					description: cmd.description,
+					path: cmd.path,
+				}));
+			
+			const skills = commands
+				.filter(cmd => cmd.source === "skill")
+				.map(cmd => ({
+					name: cmd.name,
+					location: cmd.location,
+					path: cmd.path,
+				}));
+			
+			const prompts = commands
+				.filter(cmd => cmd.source === "prompt")
+				.map(cmd => ({
+					name: cmd.name,
+					location: cmd.location,
+					path: cmd.path,
+				}));
+
+			// Get themes
+			const themes = ctx.ui.getAllThemes();
+			const currentTheme = ctx.ui.theme;
+
+			// Build markdown content
+			const lines: string[] = [];
+			
+			lines.push("## 🔧 Loaded Resources");
+			lines.push("");
+			
+			// Context files section
+			lines.push("### 📄 Context Files");
+			lines.push("");
+			const systemPrompt = ctx.getSystemPrompt();
+			// Try to extract AGENTS.md file references from the system prompt
+			// This is a heuristic - check if prompt contains common patterns
+			if (systemPrompt.includes("AGENTS.md") || systemPrompt.includes("Project-specific instructions")) {
+				// Common locations for context files
+				const contextFiles = [
+					"~/.pi/agent/AGENTS.md",
+					`${ctx.cwd}/AGENTS.md`,
+				];
+				for (const file of contextFiles) {
+					lines.push(`- \`${file}\``);
+				}
+			} else {
+				lines.push("*No AGENTS.md files detected*");
+			}
+			lines.push("");
+			
+			if (extensions.length > 0) {
+				lines.push(`### 🔌 Extensions (${extensions.length})`);
+				lines.push("");
+				for (const ext of extensions) {
+					const desc = ext.description ? ` — ${ext.description}` : "";
+					const path = ext.path ? ` \`${ext.path}\`` : "";
+					lines.push(`- **/${ext.name}**${desc}${path}`);
+				}
+				lines.push("");
+			}
+			
+			if (skills.length > 0) {
+				lines.push(`### 🎯 Skills (${skills.length})`);
+				lines.push("");
+				for (const skill of skills) {
+					const loc = skill.location ? ` *(${skill.location})*` : "";
+					const path = skill.path ? ` \`${skill.path}\`` : "";
+					lines.push(`- **/skill:${skill.name}**${loc}${path}`);
+				}
+				lines.push("");
+			}
+			
+			if (prompts.length > 0) {
+				lines.push(`### 📝 Prompt Templates (${prompts.length})`);
+				lines.push("");
+				for (const prompt of prompts) {
+					const loc = prompt.location ? ` *(${prompt.location})*` : "";
+					const path = prompt.path ? ` \`${prompt.path}\`` : "";
+					lines.push(`- **/${prompt.name}**${loc}${path}`);
+				}
+				lines.push("");
+			}
+			
+			if (themes.length > 0) {
+				lines.push(`### 🎨 Themes (${themes.length})`);
+				lines.push("");
+				for (const theme of themes) {
+					const isCurrent = currentTheme && theme.name === currentTheme.name;
+					const marker = isCurrent ? "**→** " : "  ";
+					const path = theme.path ? ` \`${theme.path}\`` : " *(built-in)*";
+					lines.push(`${marker}**${theme.name}**${path}`);
+				}
+				lines.push("");
+			}
+			
+			// Empty state
+			if (extensions.length === 0 && skills.length === 0 && prompts.length === 0) {
+				lines.push("*No extensions, skills, or prompts loaded.*");
+				lines.push("");
+			}
+			
+			// Send as inline message
+			pi.sendMessage({
+				customType: "resources",
+				content: lines.join("\n"),
+				display: true,
+			});
+		},
+	});
+
+	// Command to restore built-in header
+	pi.registerCommand("builtin-header", {
+		description: "Restore built-in header",
+		handler: async (_args, ctx) => {
+			ctx.ui.setHeader(undefined);
+			ctx.ui.notify("Built-in header restored. Use /reload to re-enable custom header.", "info");
+		},
+	});
+}