mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 19:05:11 +00:00
- Add setEditorComponent() to ctx.ui for custom editor components - Add CustomEditor base class for extensions (handles app keybindings) - Add keybindings parameter to ctx.ui.custom() factory (breaking change) - Add modal-editor.ts example (vim-like modes) - Add rainbow-editor.ts example (animated text highlighting) - Update docs: extensions.md, tui.md Pattern 7 - Clean up terminal on TUI render errors
95 lines
2.6 KiB
TypeScript
95 lines
2.6 KiB
TypeScript
/**
|
|
* Rainbow Editor - highlights "ultrathink" with animated shine effect
|
|
*
|
|
* Usage: pi --extension ./examples/extensions/rainbow-editor.ts
|
|
*/
|
|
|
|
import { CustomEditor, type ExtensionAPI, type KeybindingsManager } from "@mariozechner/pi-coding-agent";
|
|
import type { EditorTheme, TUI } from "@mariozechner/pi-tui";
|
|
|
|
// Base colors (coral → yellow → green → teal → blue → purple → pink)
|
|
const COLORS: [number, number, number][] = [
|
|
[233, 137, 115], // coral
|
|
[228, 186, 103], // yellow
|
|
[141, 192, 122], // green
|
|
[102, 194, 179], // teal
|
|
[121, 157, 207], // blue
|
|
[157, 134, 195], // purple
|
|
[206, 130, 172], // pink
|
|
];
|
|
const RESET = "\x1b[0m";
|
|
|
|
function brighten(rgb: [number, number, number], factor: number): string {
|
|
const [r, g, b] = rgb.map((c) => Math.round(c + (255 - c) * factor));
|
|
return `\x1b[38;2;${r};${g};${b}m`;
|
|
}
|
|
|
|
function colorize(text: string, shinePos: number): string {
|
|
return (
|
|
[...text]
|
|
.map((c, i) => {
|
|
const baseColor = COLORS[i % COLORS.length]!;
|
|
// 3-letter shine: center bright, adjacent dimmer
|
|
let factor = 0;
|
|
if (shinePos >= 0) {
|
|
const dist = Math.abs(i - shinePos);
|
|
if (dist === 0) factor = 0.7;
|
|
else if (dist === 1) factor = 0.35;
|
|
}
|
|
return `${brighten(baseColor, factor)}${c}`;
|
|
})
|
|
.join("") + RESET
|
|
);
|
|
}
|
|
|
|
class RainbowEditor extends CustomEditor {
|
|
private animationTimer?: ReturnType<typeof setInterval>;
|
|
private tui: TUI;
|
|
private frame = 0;
|
|
|
|
constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) {
|
|
super(theme, keybindings);
|
|
this.tui = tui;
|
|
}
|
|
|
|
private hasUltrathink(): boolean {
|
|
return /ultrathink/i.test(this.getText());
|
|
}
|
|
|
|
private startAnimation(): void {
|
|
if (this.animationTimer) return;
|
|
this.animationTimer = setInterval(() => {
|
|
this.frame++;
|
|
this.tui.requestRender();
|
|
}, 60);
|
|
}
|
|
|
|
private stopAnimation(): void {
|
|
if (this.animationTimer) {
|
|
clearInterval(this.animationTimer);
|
|
this.animationTimer = undefined;
|
|
}
|
|
}
|
|
|
|
handleInput(data: string): void {
|
|
super.handleInput(data);
|
|
if (this.hasUltrathink()) {
|
|
this.startAnimation();
|
|
} else {
|
|
this.stopAnimation();
|
|
}
|
|
}
|
|
|
|
render(width: number): string[] {
|
|
// Cycle: 10 shine positions + 10 pause frames
|
|
const cycle = this.frame % 20;
|
|
const shinePos = cycle < 10 ? cycle : -1; // -1 means no shine (pause)
|
|
return super.render(width).map((line) => line.replace(/ultrathink/gi, (m) => colorize(m, shinePos)));
|
|
}
|
|
}
|
|
|
|
export default function (pi: ExtensionAPI) {
|
|
pi.on("session_start", (_event, ctx) => {
|
|
ctx.ui.setEditorComponent((tui, theme, kb) => new RainbowEditor(tui, theme, kb));
|
|
});
|
|
}
|