flake-update-20260505
  1/**
  2 * Custom Header Extension
  3 *
  4 * Displays a custom header with:
  5 * - Pi (π) mathematical symbol in ASCII art
  6 * - Version information
  7 * - Extension issues (if any) displayed in header
  8 * - /resources command to show loaded extensions/skills/prompts inline
  9 */
 10
 11import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
 12import { VERSION } from "@mariozechner/pi-coding-agent";
 13
 14// Pi symbol ASCII art (using Braille patterns) with green-to-blue gradient
 15function getPiArt(theme: Theme): string[] {
 16	// Green to blue gradient colors - from bright green at top to blue at bottom
 17	const color1 = "\x1b[38;2;0;255;100m";    // Bright green
 18	const color2 = "\x1b[38;2;0;240;110m";    // Green
 19	const color3 = "\x1b[38;2;0;220;125m";    // Green-cyan
 20	const color4 = "\x1b[38;2;0;195;145m";    // Cyan-green
 21	const color5 = "\x1b[38;2;0;165;170m";    // Cyan
 22	const color6 = "\x1b[38;2;0;130;195m";    // Cyan-blue
 23	const color7 = "\x1b[38;2;0;95;220m";     // Blue-cyan
 24	const color8 = "\x1b[38;2;0;60;240m";     // Blue
 25	
 26	return [
 27		"",
 28		color1 + "⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⡀⠀",
 29		color2 + "⠀⠀⠀⠀⠀⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄",
 30		color2 + "⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠀",
 31		color3 + "⠀⠀⣴⣿⣿⣿⣿⣿⡟⠉⠀⠀⠈⢻⣿⣿⣿⣿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
 32		color3 + "⢀⣾⣿⣿⣿⣿⡿⣻⡇⠀⠀⠄⠕⢹⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
 33		color4 + "⠈⢿⣿⣿⡿⠋⡃⢸⣿⠓⠄⠄⠀⢸⣿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
 34		color4 + "⠀⠀⠈⠁⠀⠀⠌⠀⣿⡇⠀⠐⠀⣾⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
 35		color5 + "⠀⠀⠀⠀⠀⠀⠘⠀⡇⡇⠀⠈⠀⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
 36		color5 + "⠀⠀⠀⠀⠀⠀⡎⠐⣷⣷⠀⠀⠀⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
 37		color6 + "⠀⠀⠀⠀⠀⠀⡏⠀⣷⡇⠈⠀⠀⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀",
 38		color6 + "⠀⠀⠀⠀⢈⠀⡎⠸⣿⡇⠀⠀⠀⣿⣿⣿⣿⣿⡠⠀⠀⠀⣰⣿⣿⣿⣦⠀⠀⠀",
 39		color7 + "⠀⠀⠀⠀⠁⠀⢹⢰⣿⡇⠀⠀⠀⣿⣿⣿⣿⣿⣄⡀⠀⣰⣿⣿⣿⣿⡿⠀⠀⠀",
 40		color7 + "⠀⠀⠀⠀⠀⠀⠼⢿⣿⡇⠀⠀⠀⠘⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀",
 41		color8 + "⠀⠀⠀⠀⠀⠀⠀⠀⠃⠀⠀⠀⠀⠀⠈⠙⢿⣿⣿⣿⣿⣿⣿⡿⠟⠁⠀⠀⠀⠀",
 42		color8 + "⠀⠀⠀⠀⠀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀",
 43		"",
 44	];
 45}
 46
 47// Track extension issues from pi's startup
 48let extensionIssues: string[] = [];
 49
 50export default function (pi: ExtensionAPI) {
 51	// Capture extension issues from the console/logs if available
 52	// This is a placeholder - we'd need pi to expose these via an event or API
 53	pi.on("session_start", async (_event, ctx) => {
 54		// TODO: Check if pi exposes extension errors/warnings
 55		// For now, we'll show issues in the header if they exist
 56	});
 57
 58	// Set custom header on load
 59	pi.on("session_start", async (_event, ctx) => {
 60		if (ctx.hasUI) {
 61			ctx.ui.setHeader((_tui, theme) => {
 62				return {
 63					render(_width: number): string[] {
 64						const artLines = getPiArt(theme);
 65						const versionLine = `    ${theme.fg("muted", "shitty coding agent")} ${theme.fg("dim", `v${VERSION}`)}`;
 66						const hintLine = `    ${theme.fg("dim", "Type /resources to view loaded extensions")}`;
 67						
 68						const lines = [...artLines, versionLine, hintLine];
 69						
 70						// Add extension issues if any
 71						if (extensionIssues.length > 0) {
 72							lines.push("");
 73							lines.push(`    ${theme.fg("warning", "⚠️  Extension Issues:")}`);
 74							for (const issue of extensionIssues) {
 75								lines.push(`    ${theme.fg("warning", "  • " + issue)}`);
 76							}
 77						}
 78						
 79						return lines;
 80					},
 81					invalidate() {},
 82				};
 83			});
 84		}
 85	});
 86
 87	// Command to show resources inline (like /todos)
 88	pi.registerCommand("resources", {
 89		description: "Show loaded extensions, skills, prompts, themes, and context files",
 90		handler: async (_args, ctx) => {
 91			// Get extension info from commands
 92			const commands = pi.getCommands();
 93			
 94			const extensions = commands
 95				.filter(cmd => cmd.sourceInfo?.source === "extension")
 96				.map(cmd => ({
 97					name: cmd.name,
 98					description: cmd.description,
 99					path: cmd.sourceInfo?.path,
100				}));
101			
102			const skills = commands
103				.filter(cmd => cmd.sourceInfo?.source === "skill")
104				.map(cmd => ({
105					name: cmd.name,
106					location: cmd.sourceInfo?.path,
107					path: cmd.sourceInfo?.path,
108				}));
109			
110			const prompts = commands
111				.filter(cmd => cmd.sourceInfo?.source === "prompt")
112				.map(cmd => ({
113					name: cmd.name,
114					location: cmd.sourceInfo?.path,
115					path: cmd.sourceInfo?.path,
116				}));
117
118			// Get themes
119			const themes = ctx.ui.getAllThemes();
120			const currentTheme = ctx.ui.theme;
121
122			// Build markdown content
123			const lines: string[] = [];
124			
125			lines.push("## 🔧 Loaded Resources");
126			lines.push("");
127			
128			// Context files section
129			lines.push("### 📄 Context Files");
130			lines.push("");
131			const systemPrompt = ctx.getSystemPrompt();
132			// Try to extract AGENTS.md file references from the system prompt
133			// This is a heuristic - check if prompt contains common patterns
134			if (systemPrompt.includes("AGENTS.md") || systemPrompt.includes("Project-specific instructions")) {
135				// Common locations for context files
136				const contextFiles = [
137					"~/.pi/agent/AGENTS.md",
138					`${ctx.cwd}/AGENTS.md`,
139				];
140				for (const file of contextFiles) {
141					lines.push(`- \`${file}\``);
142				}
143			} else {
144				lines.push("*No AGENTS.md files detected*");
145			}
146			lines.push("");
147			
148			if (extensions.length > 0) {
149				lines.push(`### 🔌 Extensions (${extensions.length})`);
150				lines.push("");
151				for (const ext of extensions) {
152					const desc = ext.description ? `${ext.description}` : "";
153					const path = ext.path ? ` \`${ext.path}\`` : "";
154					lines.push(`- **/${ext.name}**${desc}${path}`);
155				}
156				lines.push("");
157			}
158			
159			if (skills.length > 0) {
160				lines.push(`### 🎯 Skills (${skills.length})`);
161				lines.push("");
162				for (const skill of skills) {
163					const loc = skill.location ? ` *(${skill.location})*` : "";
164					const path = skill.path ? ` \`${skill.path}\`` : "";
165					lines.push(`- **/skill:${skill.name}**${loc}${path}`);
166				}
167				lines.push("");
168			}
169			
170			if (prompts.length > 0) {
171				lines.push(`### 📝 Prompt Templates (${prompts.length})`);
172				lines.push("");
173				for (const prompt of prompts) {
174					const loc = prompt.location ? ` *(${prompt.location})*` : "";
175					const path = prompt.path ? ` \`${prompt.path}\`` : "";
176					lines.push(`- **/${prompt.name}**${loc}${path}`);
177				}
178				lines.push("");
179			}
180			
181			if (themes.length > 0) {
182				lines.push(`### 🎨 Themes (${themes.length})`);
183				lines.push("");
184				for (const theme of themes) {
185					const isCurrent = currentTheme && theme.name === currentTheme.name;
186					const marker = isCurrent ? "**→** " : "  ";
187					const path = theme.path ? ` \`${theme.path}\`` : " *(built-in)*";
188					lines.push(`${marker}**${theme.name}**${path}`);
189				}
190				lines.push("");
191			}
192			
193			// Empty state
194			if (extensions.length === 0 && skills.length === 0 && prompts.length === 0) {
195				lines.push("*No extensions, skills, or prompts loaded.*");
196				lines.push("");
197			}
198			
199			// Send as inline message
200			pi.sendMessage({
201				customType: "resources",
202				content: lines.join("\n"),
203				display: true,
204			});
205		},
206	});
207
208	// Command to restore built-in header
209	pi.registerCommand("builtin-header", {
210		description: "Restore built-in header",
211		handler: async (_args, ctx) => {
212			ctx.ui.setHeader(undefined);
213			ctx.ui.notify("Built-in header restored. Use /reload to re-enable custom header.", "info");
214		},
215	});
216}