# 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)
