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}