Research conducted 2026-01-22: - pi-extension-ecosystem-research.md: 56 GitHub projects, 52 official examples - pi-ui-ecosystem-research.md: TUI patterns, components, overlays - multi-model-consensus-analysis.md: gap analysis leading to /synod design
22 KiB
Pi Coding Agent UI/TUI Ecosystem Research
Date: 2026-01-22 Purpose: Survey pi-coding-agent UI/TUI patterns and components for dotfiles integration
Official TUI Package (@mariozechner/pi-tui)
Core Features
Differential Rendering:
- Three-strategy system (first render, width change, normal update)
- Synchronized output with CSI 2026 for flicker-free updates
- Only updates changed lines
Component Architecture:
interface Component {
render(width: number): string[]; // Must not exceed width!
handleInput?(data: string): void; // Keyboard input
invalidate?(): void; // Clear cached state
}
Focusable Interface (IME Support):
interface Focusable {
focused: boolean; // Set by TUI when focus changes
}
- Emit
CURSOR_MARKERright before fake cursor - TUI positions hardware cursor at marker
- Enables IME candidate windows (CJK input)
Overlay System:
const handle = tui.showOverlay(component, {
width: 60 | "80%",
maxHeight: 20 | "50%",
anchor: 'center' | 'top-left' | 'bottom-right',
offsetX: 2, offsetY: -1,
row: 5 | "25%", col: 10 | "50%",
margin: 2 | { top, right, bottom, left },
visible: (termWidth, termHeight) => termWidth >= 100
});
handle.hide();
handle.setHidden(true); // Temporarily hide
handle.isHidden();
Anchor values: center, top-left, top-right, bottom-left, bottom-right, top-center, bottom-center, left-center, right-center
Built-in Components
Layout:
Container- Groups childrenBox- Container with padding + backgroundSpacer- Empty lines
Text:
Text- Multi-line with word wrapTruncatedText- Single line with truncationMarkdown- Full markdown rendering with syntax highlight
Input:
Input- Single-line text input with scrollingEditor- Multi-line editor with autocomplete, paste handling, vertical scrolling
Selection:
SelectList- Interactive picker with keyboard navSettingsList- Settings panel with value cycling + submenus
Feedback:
Loader- Animated spinnerCancellableLoader- Loader with Escape + AbortSignal
Media:
Image- Inline images (Kitty/iTerm2 protocol, fallback to placeholder)
Key Detection
import { matchesKey, Key } from "@mariozechner/pi-tui";
if (matchesKey(data, Key.ctrl("c"))) process.exit(0);
if (matchesKey(data, Key.enter)) submit();
if (matchesKey(data, Key.escape)) cancel();
if (matchesKey(data, Key.up)) moveUp();
if (matchesKey(data, Key.ctrlShift("p"))) command();
Key helpers:
- Basic:
Key.enter,Key.escape,Key.tab,Key.space,Key.backspace,Key.delete,Key.home,Key.end - Arrows:
Key.up,Key.down,Key.left,Key.right - Modifiers:
Key.ctrl("c"),Key.shift("tab"),Key.alt("left"),Key.ctrlShift("p") - String format:
"enter","ctrl+c","shift+tab","ctrl+shift+p"
Utilities
import { visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
// Visible width (ignoring ANSI)
const w = visibleWidth("\x1b[31mHello\x1b[0m"); // 5
// Truncate with ellipsis (preserves ANSI)
const t = truncateToWidth("Hello World", 8); // "Hello..."
const t2 = truncateToWidth("Hello World", 8, ""); // "Hello Wo"
// Wrap text (preserves ANSI across lines)
const lines = wrapTextWithAnsi("Long line...", 20);
Autocomplete
import { CombinedAutocompleteProvider } from "@mariozechner/pi-tui";
const provider = new CombinedAutocompleteProvider(
[
{ name: "help", description: "Show help" },
{ name: "clear", description: "Clear screen" },
],
process.cwd() // base path for file completion
);
editor.setAutocompleteProvider(provider);
// Type "/" for slash commands
// Press Tab for file paths (~/, ./, ../, @)
UI Extension Examples
Header/Footer Customization
custom-header.ts
Replaces built-in header with custom component (pi mascot ASCII art).
Pattern:
pi.on("session_start", async (_event, ctx) => {
ctx.ui.setHeader((_tui, theme) => ({
render(_width: number): string[] {
return [...mascotLines, subtitle];
},
invalidate() {}
}));
});
pi.registerCommand("builtin-header", {
handler: async (_args, ctx) => {
ctx.ui.setHeader(undefined); // Restore built-in
}
});
Steal-worthy:
- ASCII art rendering
- Dynamic theme-aware coloring
- Toggle command to restore defaults
custom-footer.ts
Custom footer with token stats + git branch.
Pattern:
ctx.ui.setFooter((tui, theme, footerData) => {
const unsub = footerData.onBranchChange(() => tui.requestRender());
return {
dispose: unsub,
render(width: number): string[] {
const branch = footerData.getGitBranch(); // Not otherwise accessible!
const left = theme.fg("dim", `↑${input} ↓${output} $${cost}`);
const right = theme.fg("dim", `${model}${branchStr}`);
const pad = " ".repeat(width - visibleWidth(left) - visibleWidth(right));
return [truncateToWidth(left + pad + right, width)];
}
};
});
Key APIs:
footerData.getGitBranch()- Current branch (not in ctx)footerData.getExtensionStatuses()- Status texts fromctx.ui.setStatus()footerData.onBranchChange(callback)- Subscribe to branch changes
Steal-worthy:
- Git integration pattern
- Token/cost tracking
- Left/right alignment with padding
Editor Customization
modal-editor.ts
Vim-like modal editing.
Pattern:
import { CustomEditor, matchesKey } from "@mariozechner/pi-coding-agent";
class ModalEditor extends CustomEditor {
private mode: "normal" | "insert" = "insert";
handleInput(data: string): void {
if (matchesKey(data, "escape")) {
if (this.mode === "insert") {
this.mode = "normal";
} else {
super.handleInput(data); // Abort agent
}
return;
}
if (this.mode === "insert") {
super.handleInput(data);
return;
}
// Normal mode key mappings
const NORMAL_KEYS = {
h: "\x1b[D", j: "\x1b[B", k: "\x1b[A", l: "\x1b[C",
"0": "\x01", $: "\x05", x: "\x1b[3~",
i: null, a: null
};
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);
}
}
}
render(width: number): string[] {
const lines = super.render(width);
const label = this.mode === "normal" ? " NORMAL " : " INSERT ";
// Add mode indicator to bottom border
lines[lines.length - 1] = truncateToWidth(
lines[lines.length - 1],
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)
);
});
}
Steal-worthy:
- Modal editing pattern
- Custom key mapping layer
- Mode indicator in border
- Pass-through to super for unmapped keys
rainbow-editor.ts
Animated rainbow "ultrathink" effect.
Pattern:
class RainbowEditor extends CustomEditor {
private animationTimer?: ReturnType<typeof setInterval>;
private frame = 0;
private startAnimation(): void {
this.animationTimer = setInterval(() => {
this.frame++;
this.tui.requestRender();
}, 60);
}
handleInput(data: string): void {
super.handleInput(data);
if (/ultrathink/i.test(this.getText())) {
this.startAnimation();
} else {
this.stopAnimation();
}
}
render(width: number): string[] {
const cycle = this.frame % 20;
const shinePos = cycle < 10 ? cycle : -1;
return super.render(width).map(line =>
line.replace(/ultrathink/gi, m => colorize(m, shinePos))
);
}
}
function colorize(text: string, shinePos: number): string {
const COLORS = [[233,137,115], [228,186,103], [141,192,122], ...];
return [...text].map((c, i) => {
const baseColor = COLORS[i % COLORS.length];
let factor = 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;
}
Steal-worthy:
- Animation timing with setInterval
- Frame-based shine cycling
- RGB brightening for shimmer effect
- Text replacement in rendered output
Widget Management
widget-placement.ts
Control widget positioning.
Pattern:
const applyWidgets = (ctx: ExtensionContext) => {
if (!ctx.hasUI) return;
ctx.ui.setWidget("widget-above", ["Above editor widget"]);
ctx.ui.setWidget("widget-below",
["Below editor widget"],
{ placement: "belowEditor" }
);
};
export default function (pi: ExtensionAPI) {
pi.on("session_start", (_event, ctx) => applyWidgets(ctx));
pi.on("session_switch", (_event, ctx) => applyWidgets(ctx));
}
API:
ctx.ui.setWidget(id, lines, { placement?: "aboveEditor" | "belowEditor" })- Default: aboveEditor
- Persists across session switches
Steal-worthy:
- Placement control pattern
- Multi-event registration (start + switch)
Overlay Patterns
overlay-test.ts
Comprehensive overlay testing with inline inputs.
Features:
- Inline text inputs within menu items
- Edge case tests (wide chars, styled text, emoji)
- Focusable interface for IME support
- Border rendering with box drawing chars
Pattern:
pi.registerCommand("overlay-test", {
handler: async (_args, ctx) => {
const result = await ctx.ui.custom<Result>(
(tui, theme, kb, done) => new OverlayTestComponent(theme, done),
{ overlay: true }
);
if (result) ctx.ui.notify(result.action, "info");
}
});
class OverlayTestComponent implements Focusable {
readonly width = 70;
focused = false; // Set by TUI
handleInput(data: string): void {
if (matchesKey(data, "escape")) {
this.done(undefined);
return;
}
const current = this.items[this.selected];
if (matchesKey(data, "return")) {
this.done({ action: current.label, query: current.text });
} else if (current.hasInput) {
// Handle text input for inline field
if (matchesKey(data, "backspace")) { /* ... */ }
else if (data.charCodeAt(0) >= 32) {
current.text = current.text.slice(0, current.cursor)
+ data
+ current.text.slice(current.cursor);
current.cursor++;
}
}
}
render(width: number): string[] {
const lines = [];
lines.push(theme.fg("border", `╭${"─".repeat(innerW)}╮`));
lines.push(row(` ${theme.fg("accent", "🧪 Overlay Test")}`));
for (const item of this.items) {
if (item.hasInput) {
let inputDisplay = item.text;
if (isSelected) {
const marker = this.focused ? CURSOR_MARKER : "";
inputDisplay = `${before}${marker}\x1b[7m${cursorChar}\x1b[27m${after}`;
}
lines.push(row(`${prefix}${label} ${inputDisplay}`));
} else {
lines.push(row(prefix + label));
}
}
lines.push(theme.fg("border", `╰${"─".repeat(innerW)}╯`));
return lines;
}
}
Steal-worthy:
- Inline input fields in menus
- IME support with CURSOR_MARKER
- Box drawing character borders
- Edge case testing (wide chars, emoji, styled text)
doom-overlay
Full DOOM game in overlay (35 FPS).
Features:
- WebAssembly game engine
- Half-block character rendering (▀) with 24-bit color
- 90% width, 80% max height, centered
- Maintains 3.2:1 aspect ratio
Pattern:
const handle = tui.showOverlay(doomComponent, {
width: "90%",
maxHeight: "80%",
anchor: "center"
});
// Render loop
setInterval(() => {
// Get frame from WASM
const frame = doomEngine.getFrame();
// Convert to half-blocks with fg/bg colors
const lines = renderHalfBlocks(frame);
component.invalidate();
tui.requestRender();
}, 1000 / 35);
Steal-worthy:
- Percentage-based sizing
- Real-time rendering in overlay
- Half-block technique for pixel rendering
- WebAssembly integration
Theme Management
mac-system-theme.ts
Auto-sync theme with macOS appearance.
Pattern:
async function isDarkMode(): Promise<boolean> {
const { stdout } = await execAsync(
'osascript -e "tell application \\"System Events\\" to tell appearance preferences to return dark mode"'
);
return stdout.trim() === "true";
}
export default function (pi: ExtensionAPI) {
let intervalId: ReturnType<typeof setInterval> | null = null;
pi.on("session_start", async (_event, ctx) => {
let currentTheme = await isDarkMode() ? "dark" : "light";
ctx.ui.setTheme(currentTheme);
intervalId = setInterval(async () => {
const newTheme = await isDarkMode() ? "dark" : "light";
if (newTheme !== currentTheme) {
currentTheme = newTheme;
ctx.ui.setTheme(currentTheme);
}
}, 2000);
});
pi.on("session_shutdown", () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
});
}
Steal-worthy:
- System appearance detection (macOS AppleScript)
- Polling pattern for external state
- Theme switching API
- Cleanup on shutdown
Community UI Extensions
pi-powerline-footer (⭐ 7)
Powerline-style status bar with welcome overlay.
Features:
- Branded splash screen (gradient logo, stats, keybindings)
- Rounded box design in editor border
- Live thinking level indicator (rainbow shimmer for high/xhigh)
- Async git status (1s cache TTL, invalidates on file writes)
- Context warnings (70% yellow, 90% red)
- Token intelligence (1.2k, 45M formatting)
- Nerd Font auto-detection (iTerm, WezTerm, Kitty, Ghostty, Alacritty)
Presets:
default- Model, thinking, path, git, context, tokens, costminimal- Path, git, contextcompact- Model, git, cost, contextfull- Everything (hostname, time, abbreviated path)nerd- Maximum detail for Nerd Fontsascii- Safe for any terminal
Segments: pi, model, thinking, path, git, subagents, token_in, token_out, token_total, cost, context_pct, context_total, time_spent, time, session, hostname, cache_read, cache_write
Separators: powerline, powerline-thin, slash, pipe, dot, chevron, star, block, none, ascii
Path modes:
basename- Just directory nameabbreviated- Full path with home abbreviated, length limitfull- Complete path with home abbreviated
Thinking level display:
- off: gray
- minimal: purple-gray
- low: blue
- medium: teal
- high: 🌈 rainbow
- xhigh: 🌈 rainbow
Steal-worthy:
- Welcome overlay pattern
- Nerd Font detection
- Git caching strategy
- Preset system
- Segment composability
- Thinking level visualization
Patterns Worth Stealing
1. Custom Editor Extensions
Modal Editing:
- Layer vim-like modes on top of editor
- Map keys to escape sequences
- Mode indicator in border
- Pass-through for unmapped keys
Animated Effects:
- setInterval-based animation
- Frame counter for cycling
- Pattern matching in render()
- RGB color manipulation
2. Header/Footer Customization
Custom Header:
- ASCII art rendering
- Theme-aware coloring
- Toggle command for defaults
Custom Footer:
- Git branch integration
- Token/cost tracking
- Left/right alignment
- Dynamic status updates
3. Overlay Patterns
Inline Input Menus:
- Focusable interface for IME
- CURSOR_MARKER for cursor positioning
- Box drawing borders
- Edge case handling
Game/Animation Overlays:
- Percentage-based sizing
- Real-time rendering loops
- Half-block pixel technique
4. Widget Management
Placement Control:
- aboveEditor vs belowEditor
- Multi-event registration
- Persistent across switches
5. Theme Integration
System Sync:
- OS appearance detection
- Polling for external state
- Theme switching API
- Cleanup handlers
6. Powerline Pattern
Segment Composability:
- Modular segment system
- Preset configurations
- Separator styles
- Font detection
Smart Caching:
- TTL-based git status
- Invalidate on file events
- Async fetching
Progressive Enhancement:
- Nerd Font detection
- ASCII fallbacks
- Responsive visibility
Ideas for Dotfiles Integration
High Priority
-
NixOS-aware footer - Extend powerline pattern
- Segments: flake-lock-age, rebuild-needed, generation-count, last-build-status
- Git branch with dirty indicator
- Nix eval cost (tokens used for config generation)
- Auto-compact indicator
-
Nix build overlay - Long-running build visualization
- Show build progress in overlay
- Stream build log with auto-scroll
- Color-coded output (errors red, warnings yellow)
- Escape to background, status in widget
-
Beads issue selector - Overlay with inline filtering
- Show issues with priority/status
- Filter by label, search
- Inline preview of issue description
- Quick actions (update status, add comment)
-
Multi-model consensus UI - Extend oracle pattern
- Model picker with Nix-aware descriptions
- Show model capabilities (nix, general, vision)
- Side-by-side response comparison
- Vote/merge UI
Medium Priority
-
Sops secret editor - Protected inline editing
- Overlay for secret selection
- Inline decryption/editing
- Re-encrypt on save
- Never show in main editor
-
Niri window grid - Visual window picker
- ASCII art grid of workspaces
- Window thumbnails (if terminal supports images)
- Keyboard navigation
- Launch window in context
-
Git checkpoint visualizer - Tree view overlay
- Show checkpoint stash refs
- Visual diff preview
- One-key restore
- Fork visualization
-
Plan mode indicator - Visual read-only state
- Header banner when in plan mode
- Different border color
- Disable write/edit tools
- Clear toggle status
Low Priority
-
Skill extraction wizard - Piception pattern
- Detect debugging sessions
- Offer extraction at session end
- Interactive editor for skill content
- Auto-populate metadata
-
Usage quota widget - Above-editor status
- Anthropic 5h/week countdown
- OpenAI rate limits
- Gemini quota
- Color-coded warnings
-
Rainbow ultrathink - Fun effect
- Shimmer animation for thinking states
- Configurable trigger words
- Gradient colors
-
ASCII art loader - NixOS theme
- Snowflake logo animation
- Nix build status messages
- Progress bar for long operations
Architecture Notes
UI Extension Hooks
Lifecycle:
session_start- Set up UI componentssession_shutdown- Clean up timers, resources
UI Customization:
ctx.ui.setHeader(factory)- Replace headerctx.ui.setFooter(factory)- Replace footerctx.ui.setEditorComponent(factory)- Replace editorctx.ui.setWidget(id, lines, { placement })- Add widgetctx.ui.setTheme(name)- Change theme
UI Interactions:
ctx.ui.notify(message, level)- Show notificationctx.ui.select(prompt, options)- Picker dialogctx.ui.confirm(prompt)- Yes/no dialogctx.ui.custom(factory, { overlay })- Custom component
Footer Data (only in setFooter):
footerData.getGitBranch()- Current branchfooterData.getExtensionStatuses()- Status textsfooterData.onBranchChange(callback)- Subscribe to changes
Component Best Practices
Line Width Constraint:
- Each line MUST NOT exceed
widthparameter - Use
truncateToWidth()to ensure compliance - TUI will error on overflow
ANSI Handling:
visibleWidth()ignores ANSI codestruncateToWidth()preserves ANSI codeswrapTextWithAnsi()maintains styling across wraps- TUI appends SGR reset + OSC 8 reset per line
Caching:
- Cache rendered output when possible
- Invalidate on state changes
- Check cached width matches current width
IME Support:
- Implement
Focusableinterface - Set
focusedproperty - Emit
CURSOR_MARKERbefore fake cursor - Container components must propagate focus
Overlay Positioning
Resolution Order:
minWidthfloor after width calculation- Position: absolute > percentage > anchor
marginclamps to terminal boundsvisiblecallback controls rendering
Sizing:
- Numbers = absolute columns/rows
- Strings = percentages ("50%", "80%")
maxHeight,maxWidthlimitsminWidthfloor
Positioning:
anchor+offsetX/offsetY(simple)row/colpercentages (responsive)- Absolute
row/col(precise) marginfor edge padding
Key Detection Patterns
Kitty Protocol Support:
- Use
Keyhelper for autocomplete - String literals also work
- Handles Shift, Ctrl, Alt modifiers
- Gracefully degrades on non-Kitty terminals
Common Patterns:
// Navigation
if (matchesKey(data, Key.up)) moveUp();
if (matchesKey(data, Key.down)) moveDown();
// Submission
if (matchesKey(data, Key.enter)) submit();
if (matchesKey(data, Key.escape)) cancel();
// Modifiers
if (matchesKey(data, Key.ctrl("c"))) abort();
if (matchesKey(data, Key.shift("tab"))) back();
if (matchesKey(data, Key.ctrlShift("p"))) command();
Next Steps
- Implement NixOS-aware footer extension
- Create Nix build overlay for long operations
- Add beads issue selector overlay
- Prototype multi-model consensus UI
- Build git checkpoint visualizer
- Add plan mode visual indicator