Files
calvana/extensions/themeMap.ts
Omair Saleh 2fb612b1df feat: calvana application microsite + ship-log extension
- Static site: /manifesto, /live, /hire pages
- Ship-log Pi extension: calvana_ship, calvana_oops, calvana_deploy tools
- Docker + nginx deploy to calvana.quikcue.com
- Terminal-ish dark aesthetic, mobile responsive
- Auto-updating /live page from extension state
2026-03-02 18:03:22 +08:00

146 lines
6.2 KiB
TypeScript

/**
* themeMap.ts — Per-extension default theme assignments
*
* Themes live in .pi/themes/ and are mapped by extension filename (no extension).
* Each extension calls applyExtensionTheme(import.meta.url, ctx) in its session_start
* hook to automatically load its designated theme on boot.
*
* Available themes (.pi/themes/):
* catppuccin-mocha · cyberpunk · dracula · everforest · gruvbox
* midnight-ocean · nord · ocean-breeze · rose-pine
* synthwave · tokyo-night
*/
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
import { basename } from "path";
import { fileURLToPath } from "url";
// ── Theme assignments ──────────────────────────────────────────────────────
//
// Key = extension filename without extension (matches extensions/<key>.ts)
// Value = theme name from .pi/themes/<value>.json
//
export const THEME_MAP: Record<string, string> = {
"agent-chain": "midnight-ocean", // deep sequential pipeline
"agent-dashboard": "tokyo-night", // unified monitoring hub
"agent-team": "dracula", // rich orchestration palette
"cross-agent": "ocean-breeze", // cross-boundary, connecting
"damage-control": "gruvbox", // grounded, earthy safety
"minimal": "synthwave", // synthwave by default now!
"observatory": "cyberpunk", // futuristic observation deck
"pi-pi": "rose-pine", // warm creative meta-agent
"pure-focus": "everforest", // calm, distraction-free
"purpose-gate": "tokyo-night", // intentional, sharp focus
"session-replay": "catppuccin-mocha", // soft, reflective history
"subagent-widget": "cyberpunk", // multi-agent futuristic
"system-select": "catppuccin-mocha", // soft selection UI
"theme-cycler": "synthwave", // neon, it's a theme tool
"tilldone": "everforest", // task-focused calm
"tool-counter": "synthwave", // techy metrics
"tool-counter-widget":"synthwave", // same family
};
// ── Helpers ───────────────────────────────────────────────────────────────
/** Derive the extension name (e.g. "minimal") from its import.meta.url. */
function extensionName(fileUrl: string): string {
const filePath = fileUrl.startsWith("file://") ? fileURLToPath(fileUrl) : fileUrl;
return basename(filePath).replace(/\.[^.]+$/, "");
}
// ── Theme ──────────────────────────────────────────────────────────────────
/**
* Apply the mapped theme for an extension on session boot.
*
* @param fileUrl Pass `import.meta.url` from the calling extension file.
* @param ctx The ExtensionContext from the session_start handler.
* @returns true if the theme was applied successfully, false otherwise.
*/
export function applyExtensionTheme(fileUrl: string, ctx: ExtensionContext): boolean {
if (!ctx.hasUI) return false;
const name = extensionName(fileUrl);
// If there are multiple extensions stacked in 'ipi', they each fire session_start
// and try to apply their own mapped theme. The LAST one to fire wins.
// Since system-select is last in the ipi alias array, it was setting 'catppuccin-mocha'.
// We want to skip theme application for all secondary extensions if they are stacked,
// so the primary extension (first in the array) dictates the theme.
const primaryExt = primaryExtensionName();
if (primaryExt && primaryExt !== name) {
return true; // Pretend we succeeded, but don't overwrite the primary theme
}
let themeName = THEME_MAP[name];
if (!themeName) {
themeName = "synthwave";
}
const result = ctx.ui.setTheme(themeName);
if (!result.success && themeName !== "synthwave") {
return ctx.ui.setTheme("synthwave").success;
}
return result.success;
}
// ── Title ──────────────────────────────────────────────────────────────────
/**
* Read process.argv to find the first -e / --extension flag value.
*
* When Pi is launched as:
* pi -e extensions/subagent-widget.ts -e extensions/pure-focus.ts
*
* process.argv contains those paths verbatim. Every stacked extension calls
* this and gets the same answer ("subagent-widget"), so all setTitle calls
* are idempotent — no shared state or deduplication needed.
*
* Returns null if no -e flag is present (e.g. plain `pi` with no extensions).
*/
function primaryExtensionName(): string | null {
const argv = process.argv;
for (let i = 0; i < argv.length - 1; i++) {
if (argv[i] === "-e" || argv[i] === "--extension") {
return basename(argv[i + 1]).replace(/\.[^.]+$/, "");
}
}
return null;
}
/**
* Set the terminal title to "π - <first-extension-name>" on session boot.
* Reads the title from process.argv so all stacked extensions agree on the
* same value — no coordination or shared state required.
*
* Deferred 150 ms to fire after Pi's own startup title-set.
*/
function applyExtensionTitle(ctx: ExtensionContext): void {
if (!ctx.hasUI) return;
const name = primaryExtensionName();
if (!name) return;
setTimeout(() => ctx.ui.setTitle(`π - ${name}`), 150);
}
// ── Combined default ───────────────────────────────────────────────────────
/**
* Apply both the mapped theme AND the terminal title for an extension.
* Drop-in replacement for applyExtensionTheme — call this in every session_start.
*
* Usage:
* import { applyExtensionDefaults } from "./themeMap.ts";
*
* pi.on("session_start", async (_event, ctx) => {
* applyExtensionDefaults(import.meta.url, ctx);
* // ... rest of handler
* });
*/
export function applyExtensionDefaults(fileUrl: string, ctx: ExtensionContext): void {
applyExtensionTheme(fileUrl, ctx);
applyExtensionTitle(ctx);
}