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
850 lines
22 KiB
Markdown
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)
|