Add a proper public-facing documentation site using Mintlify with 19 MDX pages covering getting started, auto mode, commands, configuration, and all user-facing features. Move internal/SDK documentation (Pi SDK, TUI, context & hooks, research notes, ADRs) to docs-internal/ since they should not be part of the public documentation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5.4 KiB
5.4 KiB
Custom UI — Visual Components
Pi's extension UI has multiple layers, from simple notifications to full custom components.
12.1 Dialogs (Blocking)
// Selection
const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);
// Confirmation
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
// Text input
const name = await ctx.ui.input("Name:", "placeholder");
// Multi-line editor
const text = await ctx.ui.editor("Edit:", "prefilled text");
Timed Dialogs
// Auto-dismiss after 5s with countdown: "Title (5s)" → "Title (4s)" → ...
const ok = await ctx.ui.confirm("Auto-confirm?", "Proceeds in 5s", { timeout: 5000 });
// Returns false on timeout
12.2 Persistent UI Elements
// Footer status (persistent until cleared)
ctx.ui.setStatus("my-ext", "● Active");
ctx.ui.setStatus("my-ext", undefined); // Clear
// Widget above editor (default placement)
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
// Widget below editor
ctx.ui.setWidget("my-widget", ["Below!"], { placement: "belowEditor" });
// Widget with theme callback
ctx.ui.setWidget("my-widget", (_tui, theme) => ({
render: () => [theme.fg("accent", "Styled widget")],
invalidate: () => {},
}));
// Working message during streaming
ctx.ui.setWorkingMessage("Analyzing code...");
ctx.ui.setWorkingMessage(); // Restore default
// Custom footer (replaces built-in entirely)
ctx.ui.setFooter((tui, theme, footerData) => ({
render(width) { return [theme.fg("dim", `branch: ${footerData.getGitBranch()}`)]; },
invalidate() {},
dispose: footerData.onBranchChange(() => tui.requestRender()),
}));
// Editor control
ctx.ui.setEditorText("Prefill");
const current = ctx.ui.getEditorText();
ctx.ui.pasteToEditor("pasted content");
// Tool expansion
ctx.ui.setToolsExpanded(true);
ctx.ui.setToolsExpanded(false);
// Theme management
const themes = ctx.ui.getAllThemes();
ctx.ui.setTheme("light");
12.3 Custom Components (ctx.ui.custom)
For complex UI, ctx.ui.custom() temporarily replaces the editor with your component:
const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) => {
// Return a component object
return {
render(width: number): string[] {
return ["Press Enter to confirm, Escape to cancel"];
},
handleInput(data: string) {
if (matchesKey(data, Key.enter)) done("confirmed");
if (matchesKey(data, Key.escape)) done(null);
},
invalidate() {},
};
});
12.4 Overlays (Floating Modals)
const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyDialog({ onClose: done }),
{
overlay: true,
overlayOptions: {
anchor: "center", // 9 positions: center, top-left, top-right, etc.
width: "50%",
maxHeight: "80%",
margin: 2,
visible: (w, h) => w >= 80, // Hide on narrow terminals
},
onHandle: (handle) => {
// handle.setHidden(true/false)
},
}
);
12.5 Custom Editor (Replace Main Input)
import { CustomEditor } from "@mariozechner/pi-coding-agent";
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
class VimEditor extends CustomEditor {
private mode: "normal" | "insert" = "insert";
handleInput(data: string): void {
if (matchesKey(data, "escape") && this.mode === "insert") {
this.mode = "normal";
return;
}
if (this.mode === "insert") {
super.handleInput(data); // Normal text editing + app keybindings
return;
}
// Vim normal mode keys...
if (data === "i") { this.mode = "insert"; return; }
super.handleInput(data); // Pass unhandled to parent
}
}
// Register:
ctx.ui.setEditorComponent((_tui, theme, keybindings) => new VimEditor(theme, keybindings));
ctx.ui.setEditorComponent(undefined); // Restore default
Key point: Extend
CustomEditor(notEditor) to get app keybindings (escape to abort, ctrl+d, model switching).
12.6 Built-in TUI Components
Import from @mariozechner/pi-tui:
| Component | Purpose |
|---|---|
Text |
Multi-line text with word wrapping |
Box |
Container with padding and background |
Container |
Groups children vertically |
Spacer |
Empty vertical space |
Markdown |
Rendered markdown with syntax highlighting |
Image |
Image rendering (Kitty, iTerm2, etc.) |
SelectList |
Interactive selection from list |
SettingsList |
Toggle settings UI |
Input |
Text input field |
Import from @mariozechner/pi-coding-agent:
| Component | Purpose |
|---|---|
DynamicBorder |
Border line with theming |
BorderedLoader |
Spinner with cancel support |
12.7 Keyboard Input
import { matchesKey, Key } from "@mariozechner/pi-tui";
handleInput(data: string) {
if (matchesKey(data, Key.up)) { /* arrow up */ }
if (matchesKey(data, Key.enter)) { /* enter */ }
if (matchesKey(data, Key.escape)) { /* escape */ }
if (matchesKey(data, Key.ctrl("c"))) { /* ctrl+c */ }
if (matchesKey(data, Key.shift("tab"))) { /* shift+tab */ }
}
12.8 Line Width Rules
Critical: Each line from render() must not exceed the width parameter.
import { visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
render(width: number): string[] {
return [truncateToWidth(this.text, width)];
}