# Custom UI — Visual Components


Pi's extension UI has multiple layers, from simple notifications to full custom components.

### 12.1 Dialogs (Blocking)

```typescript
// Selection
const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);

// Confirmation
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");

// Text input
const name = await ctx.ui.input("Name:", "placeholder");

// Multi-line editor
const text = await ctx.ui.editor("Edit:", "prefilled text");
```

#### Timed Dialogs

```typescript
// Auto-dismiss after 5s with countdown: "Title (5s)" → "Title (4s)" → ...
const ok = await ctx.ui.confirm("Auto-confirm?", "Proceeds in 5s", { timeout: 5000 });
// Returns false on timeout
```

### 12.2 Persistent UI Elements

```typescript
// Footer status (persistent until cleared)
ctx.ui.setStatus("my-ext", "● Active");
ctx.ui.setStatus("my-ext", undefined);   // Clear

// Widget above editor (default placement)
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);

// Widget below editor
ctx.ui.setWidget("my-widget", ["Below!"], { placement: "belowEditor" });

// Widget with theme callback
ctx.ui.setWidget("my-widget", (_tui, theme) => ({
  render: () => [theme.fg("accent", "Styled widget")],
  invalidate: () => {},
}));

// Working message during streaming
ctx.ui.setWorkingMessage("Analyzing code...");
ctx.ui.setWorkingMessage();  // Restore default

// Custom footer (replaces built-in entirely)
ctx.ui.setFooter((tui, theme, footerData) => ({
  render(width) { return [theme.fg("dim", `branch: ${footerData.getGitBranch()}`)]; },
  invalidate() {},
  dispose: footerData.onBranchChange(() => tui.requestRender()),
}));

// Editor control
ctx.ui.setEditorText("Prefill");
const current = ctx.ui.getEditorText();
ctx.ui.pasteToEditor("pasted content");

// Tool expansion
ctx.ui.setToolsExpanded(true);
ctx.ui.setToolsExpanded(false);

// Theme management
const themes = ctx.ui.getAllThemes();
ctx.ui.setTheme("light");
```

### 12.3 Custom Components (ctx.ui.custom)

For complex UI, `ctx.ui.custom()` temporarily replaces the editor with your component:

```typescript
const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) => {
  // Return a component object
  return {
    render(width: number): string[] {
      return ["Press Enter to confirm, Escape to cancel"];
    },
    handleInput(data: string) {
      if (matchesKey(data, Key.enter)) done("confirmed");
      if (matchesKey(data, Key.escape)) done(null);
    },
    invalidate() {},
  };
});
```

### 12.4 Overlays (Floating Modals)

```typescript
const result = await ctx.ui.custom<string | null>(
  (tui, theme, keybindings, done) => new MyDialog({ onClose: done }),
  {
    overlay: true,
    overlayOptions: {
      anchor: "center",         // 9 positions: center, top-left, top-right, etc.
      width: "50%",
      maxHeight: "80%",
      margin: 2,
      visible: (w, h) => w >= 80,  // Hide on narrow terminals
    },
    onHandle: (handle) => {
      // handle.setHidden(true/false)
    },
  }
);
```

### 12.5 Custom Editor (Replace Main Input)

```typescript
import { CustomEditor } from "@mariozechner/pi-coding-agent";
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";

class VimEditor extends CustomEditor {
  private mode: "normal" | "insert" = "insert";

  handleInput(data: string): void {
    if (matchesKey(data, "escape") && this.mode === "insert") {
      this.mode = "normal";
      return;
    }
    if (this.mode === "insert") {
      super.handleInput(data);  // Normal text editing + app keybindings
      return;
    }
    // Vim normal mode keys...
    if (data === "i") { this.mode = "insert"; return; }
    super.handleInput(data);  // Pass unhandled to parent
  }
}

// Register:
ctx.ui.setEditorComponent((_tui, theme, keybindings) => new VimEditor(theme, keybindings));
ctx.ui.setEditorComponent(undefined);  // Restore default
```

> **Key point:** Extend `CustomEditor` (not `Editor`) to get app keybindings (escape to abort, ctrl+d, model switching).

### 12.6 Built-in TUI Components

Import from `@mariozechner/pi-tui`:

| Component | Purpose |
|-----------|---------|
| `Text` | Multi-line text with word wrapping |
| `Box` | Container with padding and background |
| `Container` | Groups children vertically |
| `Spacer` | Empty vertical space |
| `Markdown` | Rendered markdown with syntax highlighting |
| `Image` | Image rendering (Kitty, iTerm2, etc.) |
| `SelectList` | Interactive selection from list |
| `SettingsList` | Toggle settings UI |
| `Input` | Text input field |

Import from `@mariozechner/pi-coding-agent`:

| Component | Purpose |
|-----------|---------|
| `DynamicBorder` | Border line with theming |
| `BorderedLoader` | Spinner with cancel support |

### 12.7 Keyboard Input

```typescript
import { matchesKey, Key } from "@mariozechner/pi-tui";

handleInput(data: string) {
  if (matchesKey(data, Key.up)) { /* arrow up */ }
  if (matchesKey(data, Key.enter)) { /* enter */ }
  if (matchesKey(data, Key.escape)) { /* escape */ }
  if (matchesKey(data, Key.ctrl("c"))) { /* ctrl+c */ }
  if (matchesKey(data, Key.shift("tab"))) { /* shift+tab */ }
}
```

### 12.8 Line Width Rules

**Critical:** Each line from `render()` must not exceed the `width` parameter.

```typescript
import { visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";

render(width: number): string[] {
  return [truncateToWidth(this.text, width)];
}
```

---
