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
This commit is contained in:
2026-03-02 18:03:22 +08:00
commit 2fb612b1df
73 changed files with 10938 additions and 0 deletions

181
extensions/theme-cycler.ts Normal file
View File

@@ -0,0 +1,181 @@
/**
* Theme Cycler — Keyboard shortcuts to cycle through available themes
*
* Shortcuts:
* Ctrl+X — Cycle theme forward
* Ctrl+Q — Cycle theme backward
*
* Commands:
* /theme — Open select picker to choose a theme
* /theme <name> — Switch directly by name
*
* Features:
* - Status line shows current theme name with accent color
* - Color swatch widget flashes briefly after each switch
* - Auto-dismisses swatch after 3 seconds
*
* Usage: pi -e extensions/theme-cycler.ts -e extensions/minimal.ts
*/
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import { truncateToWidth } from "@mariozechner/pi-tui";
import { applyExtensionDefaults } from "./themeMap.ts";
export default function (pi: ExtensionAPI) {
let currentCtx: ExtensionContext | undefined;
let swatchTimer: ReturnType<typeof setTimeout> | null = null;
function updateStatus(ctx: ExtensionContext) {
if (!ctx.hasUI) return;
const name = ctx.ui.theme.name;
ctx.ui.setStatus("theme", `🎨 ${name}`);
}
function showSwatch(ctx: ExtensionContext) {
if (!ctx.hasUI) return;
if (swatchTimer) {
clearTimeout(swatchTimer);
swatchTimer = null;
}
ctx.ui.setWidget(
"theme-swatch",
(_tui, theme) => ({
invalidate() {},
render(width: number): string[] {
const block = "\u2588\u2588\u2588";
const swatch =
theme.fg("success", block) +
" " +
theme.fg("accent", block) +
" " +
theme.fg("warning", block) +
" " +
theme.fg("dim", block) +
" " +
theme.fg("muted", block);
const label = theme.fg("accent", " 🎨 ") + theme.fg("muted", ctx.ui.theme.name) + " " + swatch;
const border = theme.fg("borderMuted", "─".repeat(Math.max(0, width)));
return [border, truncateToWidth(" " + label, width), border];
},
}),
{ placement: "belowEditor" },
);
swatchTimer = setTimeout(() => {
ctx.ui.setWidget("theme-swatch", undefined);
swatchTimer = null;
}, 3000);
}
function getThemeList(ctx: ExtensionContext) {
return ctx.ui.getAllThemes();
}
function findCurrentIndex(ctx: ExtensionContext): number {
const themes = getThemeList(ctx);
const current = ctx.ui.theme.name;
return themes.findIndex((t) => t.name === current);
}
function cycleTheme(ctx: ExtensionContext, direction: 1 | -1) {
if (!ctx.hasUI) return;
const themes = getThemeList(ctx);
if (themes.length === 0) {
ctx.ui.notify("No themes available", "warning");
return;
}
let index = findCurrentIndex(ctx);
if (index === -1) index = 0;
index = (index + direction + themes.length) % themes.length;
const theme = themes[index];
const result = ctx.ui.setTheme(theme.name);
if (result.success) {
updateStatus(ctx);
showSwatch(ctx);
ctx.ui.notify(`${theme.name} (${index + 1}/${themes.length})`, "info");
} else {
ctx.ui.notify(`Failed to set theme: ${result.error}`, "error");
}
}
// --- Shortcuts ---
pi.registerShortcut("ctrl+x", {
description: "Cycle theme forward",
handler: async (ctx) => {
currentCtx = ctx;
cycleTheme(ctx, 1);
},
});
pi.registerShortcut("ctrl+q", {
description: "Cycle theme backward",
handler: async (ctx) => {
currentCtx = ctx;
cycleTheme(ctx, -1);
},
});
// --- Command: /theme ---
pi.registerCommand("theme", {
description: "Select a theme: /theme or /theme <name>",
handler: async (args, ctx) => {
currentCtx = ctx;
if (!ctx.hasUI) return;
const themes = getThemeList(ctx);
const arg = args.trim();
if (arg) {
const result = ctx.ui.setTheme(arg);
if (result.success) {
updateStatus(ctx);
showSwatch(ctx);
ctx.ui.notify(`Theme: ${arg}`, "info");
} else {
ctx.ui.notify(`Theme not found: ${arg}. Use /theme to see available themes.`, "error");
}
return;
}
const items = themes.map((t) => {
const desc = t.path ? t.path : "built-in";
const active = t.name === ctx.ui.theme.name ? " (active)" : "";
return `${t.name}${active}${desc}`;
});
const selected = await ctx.ui.select("Select Theme", items);
if (!selected) return;
const selectedName = selected.split(/\s/)[0];
const result = ctx.ui.setTheme(selectedName);
if (result.success) {
updateStatus(ctx);
showSwatch(ctx);
ctx.ui.notify(`Theme: ${selectedName}`, "info");
}
},
});
// --- Session init ---
pi.on("session_start", async (_event, ctx) => {
currentCtx = ctx;
applyExtensionDefaults(import.meta.url, ctx);
updateStatus(ctx);
});
pi.on("session_shutdown", async () => {
if (swatchTimer) {
clearTimeout(swatchTimer);
swatchTimer = null;
}
});
}