extensions!

This commit is contained in:
2026-02-21 12:56:20 +00:00
parent 52cca2148f
commit fbfc3c33ca
21 changed files with 2334 additions and 19 deletions

View File

@@ -0,0 +1,85 @@
/**
* Modal Editor - vim-like modal editing example
*
* Usage: pi --extension ./examples/extensions/modal-editor.ts
*
* - Escape: insert → normal mode (in normal mode, aborts agent)
* - i: normal → insert mode
* - hjkl: navigation in normal mode
* - ctrl+c, ctrl+d, etc. work in both modes
*/
import { CustomEditor, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
// Normal mode key mappings: key -> escape sequence (or null for mode switch)
const NORMAL_KEYS: Record<string, string | null> = {
h: "\x1b[D", // left
j: "\x1b[B", // down
k: "\x1b[A", // up
l: "\x1b[C", // right
"0": "\x01", // line start
$: "\x05", // line end
x: "\x1b[3~", // delete char
i: null, // insert mode
a: null, // append (insert + right)
};
class ModalEditor extends CustomEditor {
private mode: "normal" | "insert" = "insert";
handleInput(data: string): void {
// Escape toggles to normal mode, or passes through for app handling
if (matchesKey(data, "escape")) {
if (this.mode === "insert") {
this.mode = "normal";
} else {
super.handleInput(data); // abort agent, etc.
}
return;
}
// Insert mode: pass everything through
if (this.mode === "insert") {
super.handleInput(data);
return;
}
// Normal mode: check mapped keys
if (data in NORMAL_KEYS) {
const seq = NORMAL_KEYS[data];
if (data === "i") {
this.mode = "insert";
} else if (data === "a") {
this.mode = "insert";
super.handleInput("\x1b[C"); // move right first
} else if (seq) {
super.handleInput(seq);
}
return;
}
// Pass control sequences (ctrl+c, etc.) to super, ignore printable chars
if (data.length === 1 && data.charCodeAt(0) >= 32) return;
super.handleInput(data);
}
render(width: number): string[] {
const lines = super.render(width);
if (lines.length === 0) return lines;
// Add mode indicator to bottom border
const label = this.mode === "normal" ? " NORMAL " : " INSERT ";
const last = lines.length - 1;
if (visibleWidth(lines[last]!) >= label.length) {
lines[last] = truncateToWidth(lines[last]!, width - label.length, "") + label;
}
return lines;
}
}
export default function (pi: ExtensionAPI) {
pi.on("session_start", (_event, ctx) => {
ctx.ui.setEditorComponent((tui, theme, kb) => new ModalEditor(tui, theme, kb));
});
}