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

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_MARKER right 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 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

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

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 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 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:

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

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

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

  1. Sops secret editor - Protected inline editing

    • Overlay for secret selection
    • Inline decryption/editing
    • Re-encrypt on save
    • Never show in main editor
  2. Niri window grid - Visual window picker

    • ASCII art grid of workspaces
    • Window thumbnails (if terminal supports images)
    • Keyboard navigation
    • Launch window in context
  3. Git checkpoint visualizer - Tree view overlay

    • Show checkpoint stash refs
    • Visual diff preview
    • One-key restore
    • Fork visualization
  4. 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

  1. Skill extraction wizard - Piception pattern

    • Detect debugging sessions
    • Offer extraction at session end
    • Interactive editor for skill content
    • Auto-populate metadata
  2. Usage quota widget - Above-editor status

    • Anthropic 5h/week countdown
    • OpenAI rate limits
    • Gemini quota
    • Color-coded warnings
  3. Rainbow ultrathink - Fun effect

    • Shimmer animation for thinking states
    • Configurable trigger words
    • Gradient colors
  4. 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:

// 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