skills/docs/research/pi-ui-ecosystem-research.md
dan bffa966e76 docs: add pi extension ecosystem and synod research
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
2026-01-23 00:31:22 -08:00

850 lines
22 KiB
Markdown

# 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**:
```typescript
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):
```typescript
interface Focusable {
focused: boolean; // Set by TUI when focus changes
}
```
- Emit `CURSOR_MARKER` right before fake cursor
- TUI positions hardware cursor at marker
- Enables IME candidate windows (CJK input)
**Overlay System**:
```typescript
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 children
- `Box` - Container with padding + background
- `Spacer` - Empty lines
**Text**:
- `Text` - Multi-line with word wrap
- `TruncatedText` - Single line with truncation
- `Markdown` - Full markdown rendering with syntax highlight
**Input**:
- `Input` - Single-line text input with scrolling
- `Editor` - Multi-line editor with autocomplete, paste handling, vertical scrolling
**Selection**:
- `SelectList` - Interactive picker with keyboard nav
- `SettingsList` - Settings panel with value cycling + submenus
**Feedback**:
- `Loader` - Animated spinner
- `CancellableLoader` - Loader with Escape + AbortSignal
**Media**:
- `Image` - Inline images (Kitty/iTerm2 protocol, fallback to placeholder)
### Key Detection
```typescript
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
```typescript
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
```typescript
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**:
```typescript
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**:
```typescript
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 from `ctx.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**:
```typescript
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**:
```typescript
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**:
```typescript
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**:
```typescript
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**:
```typescript
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**:
```typescript
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, cost
- `minimal` - Path, git, context
- `compact` - Model, git, cost, context
- `full` - Everything (hostname, time, abbreviated path)
- `nerd` - Maximum detail for Nerd Fonts
- `ascii` - 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 name
- `abbreviated` - Full path with home abbreviated, length limit
- `full` - 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
1. **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
2. **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
3. **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)
4. **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
5. **Sops secret editor** - Protected inline editing
- Overlay for secret selection
- Inline decryption/editing
- Re-encrypt on save
- Never show in main editor
6. **Niri window grid** - Visual window picker
- ASCII art grid of workspaces
- Window thumbnails (if terminal supports images)
- Keyboard navigation
- Launch window in context
7. **Git checkpoint visualizer** - Tree view overlay
- Show checkpoint stash refs
- Visual diff preview
- One-key restore
- Fork visualization
8. **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
9. **Skill extraction wizard** - Piception pattern
- Detect debugging sessions
- Offer extraction at session end
- Interactive editor for skill content
- Auto-populate metadata
10. **Usage quota widget** - Above-editor status
- Anthropic 5h/week countdown
- OpenAI rate limits
- Gemini quota
- Color-coded warnings
11. **Rainbow ultrathink** - Fun effect
- Shimmer animation for thinking states
- Configurable trigger words
- Gradient colors
12. **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 components
- `session_shutdown` - Clean up timers, resources
**UI Customization**:
- `ctx.ui.setHeader(factory)` - Replace header
- `ctx.ui.setFooter(factory)` - Replace footer
- `ctx.ui.setEditorComponent(factory)` - Replace editor
- `ctx.ui.setWidget(id, lines, { placement })` - Add widget
- `ctx.ui.setTheme(name)` - Change theme
**UI Interactions**:
- `ctx.ui.notify(message, level)` - Show notification
- `ctx.ui.select(prompt, options)` - Picker dialog
- `ctx.ui.confirm(prompt)` - Yes/no dialog
- `ctx.ui.custom(factory, { overlay })` - Custom component
**Footer Data** (only in setFooter):
- `footerData.getGitBranch()` - Current branch
- `footerData.getExtensionStatuses()` - Status texts
- `footerData.onBranchChange(callback)` - Subscribe to changes
### Component Best Practices
**Line Width Constraint**:
- Each line MUST NOT exceed `width` parameter
- Use `truncateToWidth()` to ensure compliance
- TUI will error on overflow
**ANSI Handling**:
- `visibleWidth()` ignores ANSI codes
- `truncateToWidth()` preserves ANSI codes
- `wrapTextWithAnsi()` 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 `Focusable` interface
- Set `focused` property
- Emit `CURSOR_MARKER` before fake cursor
- Container components must propagate focus
### Overlay Positioning
**Resolution Order**:
1. `minWidth` floor after width calculation
2. Position: absolute > percentage > anchor
3. `margin` clamps to terminal bounds
4. `visible` callback controls rendering
**Sizing**:
- Numbers = absolute columns/rows
- Strings = percentages ("50%", "80%")
- `maxHeight`, `maxWidth` limits
- `minWidth` floor
**Positioning**:
- `anchor` + `offsetX`/`offsetY` (simple)
- `row`/`col` percentages (responsive)
- Absolute `row`/`col` (precise)
- `margin` for edge padding
### Key Detection Patterns
**Kitty Protocol Support**:
- Use `Key` helper for autocomplete
- String literals also work
- Handles Shift, Ctrl, Alt modifiers
- Gracefully degrades on non-Kitty terminals
**Common Patterns**:
```typescript
// 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
1. Implement NixOS-aware footer extension
2. Create Nix build overlay for long operations
3. Add beads issue selector overlay
4. Prototype multi-model consensus UI
5. Build git checkpoint visualizer
6. Add plan mode visual indicator
---
## References
- [pi-mono TUI package](https://github.com/badlogic/pi-mono/tree/main/packages/tui)
- [pi-mono extension examples](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent/examples/extensions)
- [pi-powerline-footer](https://github.com/nicobailon/pi-powerline-footer)
- [@mariozechner/pi-tui README](https://github.com/badlogic/pi-mono/blob/main/packages/tui/README.md)