# How Extension Widgets Render Custom UI Components in π‑Web’s Terminal Style

> Learn how pi-web extension widgets render custom UI components using a four-stage pipeline. Discover the server, React hooks, and CSS variables that create terminal-style interfaces.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: how-to-guide
- Published: 2026-07-31

---

**Extension widgets render as terminal‑style UI components by flowing through a four‑stage pipeline: extensions emit `extension_ui_request` events, the server stores them in `rpc‑manager.ts`, React hooks propagate the state to the client, and [`ChatWindow.tsx`](https://github.com/agegr/pi-web/blob/main/ChatWindow.tsx) paints them as bordered monospaced blocks using CSS variables.**

The `agegr/pi-web` repository implements a chat‑centric interface where extensions can contribute custom UI elements that adopt a terminal aesthetic. These widgets appear as boxed panels inside the chat pane, preserving whitespace and using monospaced fonts to mimic a command‑line environment.

## The Extension Widget Rendering Pipeline

### Step 1: Extensions Emit UI Requests

Extensions initiate rendering by calling the Pi SDK’s `extensionUiRequest` method (or emitting the legacy `extension_ui_request` event). The payload must include three fields:

- **`key`** – A short identifier displayed as the widget’s header.
- **`lines`** – An array of strings representing the terminal content.
- **`placement`** – Either `"aboveEditor"` or `"belowEditor"` to position the widget relative to the chat input.

The SDK forwards this request to the running `AgentSession` on the server.

### Step 2: Server-Side Collection in RPC Manager

Inside [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), the server maintains a `Map<string, ExtensionWidgetItem>` named `extensionWidgets`. When a request arrives, the manager stores the widget via:

```typescript
this.extensionWidgets.set(key, {key, lines, placement});

```

The map is exposed through `getExtensionWidgets()`, which returns `Array.from(this.extensionWidgets.values())` according to lines 52‑55 of `rpc‑manager.ts`.

### Step 3: State Propagation via React Hooks

The client receives updates through [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts). The hook listens for agent events containing `extensionWidgets` and updates React state:

```typescript
const [extensionWidgets, setExtensionWidgets] = useState<ExtensionWidgetItem[]>([]);
// ...
if (liveState.extensionWidgets !== undefined) setExtensionWidgets(liveState.extensionWidgets ?? []);

```

This synchronization occurs around lines 68‑71 and 84‑85, ensuring the UI layer always holds the latest widget list.

### Step 4: Terminal-Style Rendering in ChatWindow

[`components/ChatWindow.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatWindow.tsx) consumes the widget list and splits it by placement:

```typescript
const aboveEditorWidgets = extensionWidgets.filter(w => w.placement !== "belowEditor");
const belowEditorWidgets = extensionWidgets.filter(w => w.placement === "belowEditor");

```

As implemented on lines 79‑80, these arrays pass to the `ExtensionWidgets` helper component (lines 105‑106). The component renders each widget as a bordered terminal panel:

```typescript
function ExtensionWidgets({ widgets }) {
  if (widgets.length === 0) return null;
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 10 }}>
      {widgets.map(widget => (
        <div key={widget.key} style={{
            border: "1px solid var(--border)",
            borderRadius: 7,
            background: "var(--bg-panel)",
            overflow: "hidden",
        }}>
          <div style={{ padding: "5px 9px", borderBottom: "1px solid var(--border)" }}>
            {widget.key}
          </div>
          <pre style={{
            margin: 0,
            padding: "8px 9px",
            color: "var(--text-muted)",
            fontFamily: "var(--font-mono)",
            whiteSpace: "pre-wrap",
            wordBreak: "break-word",
          }}>
            {widget.lines.join("\n")}
          </pre>
        </div>
      ))}
    </div>
  );
}

```

This implementation (lines 53‑75) uses a `<pre>` element to preserve whitespace and applies `var(--font-mono)` for the terminal aesthetic.

## Key Implementation Details

### Placement and Positioning

The `placement` property controls vertical ordering. Widgets with `"aboveEditor"` appear between the chat history and the input field, while `"belowEditor"` renders underneath. The layout uses flexbox with an 8‑pixel gap and 10‑pixel bottom margin to separate multiple widgets.

### Terminal Aesthetic via CSS Variables

The visual style relies on CSS custom properties defined in the π‑Web theme:

- **`var(--bg-panel)`** – Dark background for the widget container.
- **`var(--border)`** – Subtle border color creating the boxed effect.
- **`var(--font-mono)`** – Monospaced typeface for the `<pre>` content.
- **`var(--text-muted)`** – Dimmed text color matching secondary UI elements.

The `whiteSpace: "pre-wrap"` declaration ensures that newline characters in the `lines` array render correctly, while `wordBreak: "break-word"` prevents horizontal overflow.

## Practical Examples

### Extension Side (TypeScript)

Push a terminal widget from an extension entry point:

```typescript
import { AgentSession } from "pi-sdk";

async function showWidget(session: AgentSession) {
  await session.send({
    type: "extension_ui_request",
    id: "system-monitor",
    method: "custom",
    lines: [
      "┌─ System Monitor ───────────┐",
      "│  CPU: 12%                  │",
      "│  Memory: 4.2GB / 16GB      │",
      "└────────────────────────────┘",
    ],
    placement: "aboveEditor",
  });
}

```

### React Consumption

Reuse the built‑in component in custom views:

```typescript
import { ExtensionWidgets } from "@/components/ChatWindow";

const widgets = [
  { key: "logs", lines: ["[INFO] Service started", "[DEBUG] Connected"], placement: "belowEditor" as const },
];

<ExtensionWidgets widgets={widgets} />;

```

## Summary

- Extensions trigger widgets via `extension_ui_request` payloads containing `key`, `lines`, and `placement`.
- [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) stores widgets in a `Map` and exposes them via `getExtensionWidgets()`.
- [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) propagates the widget array to React state on the client.
- [`components/ChatWindow.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatWindow.tsx) filters by placement and renders `ExtensionWidgets` as bordered, monospaced terminal blocks using CSS variables like `--font-mono` and `--bg-panel`.

## Frequently Asked Questions

### What data structure defines an extension widget?

The `ExtensionWidgetItem` type (defined in [`lib/types.ts`](https://github.com/agegr/pi-web/blob/main/lib/types.ts)) requires a string `key`, an array of string `lines`, and a `placement` union type of `"aboveEditor" | "belowEditor"`.

### How do I position a widget above the chat input?

Set the `placement` field to `"aboveEditor"` in your extension request. The [`ChatWindow.tsx`](https://github.com/agegr/pi-web/blob/main/ChatWindow.tsx) component filters the widget list and renders above‑editor items before the text input field.

### Where is the terminal styling defined?

The terminal aesthetic is defined inline within the `ExtensionWidgets` function in [`components/ChatWindow.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatWindow.tsx) (lines 53‑75). It uses CSS variables for theming and a `<pre>` tag to preserve whitespace and enforce monospaced fonts.

### Can extension widgets contain interactive HTML?

No. The current implementation joins the `lines` array with newline characters and renders them inside a `<pre>` tag, which treats content as plain text. Interactive elements would require modifying the `ExtensionWidgets` component to accept JSX children rather than string arrays.