> ## Documentation Index
> Fetch the complete documentation index at: https://herm.liftaris.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Herm plugin examples: slots, tabs, events, and rasterizers

> Examples for bundled Herm plugins, including bottom-gutter UI, custom tabs, command-palette entries, gateway events, KV state, and Eikon rasterizers.

These examples are for bundled plugins under `src/plugins/bundled/`. Herm does not load external plugin packages yet.

## Example 1: Bottom-gutter status

```tsx theme={null}
import type { HermPlugin } from "../types"

const plugin: HermPlugin = {
  id: "acme.status",
  enabled: false,
  tui(api) {
    api.slots.register({
      order: 50,
      slots: {
        app_bottom: (_ctx, props) => (
          <text fg={api.theme.current.textMuted} wrapMode="none">
            {props.streaming ? "agent running" : "agent idle"}
          </text>
        ),
      },
    })
  },
}

export default plugin
```

`app_bottom` uses `single_winner` composition. Use an `order` value that does not conflict with another enabled plugin that should win the same row.

## Example 2: Custom route and command

```tsx theme={null}
import type { HermPlugin } from "../types"

const plugin: HermPlugin = {
  id: "acme.notes",
  enabled: false,
  tui(api) {
    api.route.register([{
      name: "Notes",
      description: "Scratch notes",
      render: () => <text fg={api.theme.current.text}>notes</text>,
    }])

    api.command.register([{
      title: "Open Notes",
      value: "acme.notes.open",
      category: "Plugin",
      onSelect: () => api.route.navigate("Notes"),
    }])
  },
}

export default plugin
```

Routes appear after Herm's built-in tabs. Commands appear in the `Ctrl+K` palette.

## Example 3: Gateway event listener

```tsx theme={null}
import type { HermPlugin } from "../types"

const plugin: HermPlugin = {
  id: "acme.events",
  enabled: false,
  tui(api) {
    api.event.on((event) => {
      if (event.type === "session.ready") {
        api.ui.toast({ variant: "success", message: "session ready" })
      }
    })
  },
}

export default plugin
```

Herm scope-tracks the disposer returned by `event.on()`. You do not need to remove the listener manually on plugin deactivation.

## Example 4: Persistent plugin setting

```tsx theme={null}
import type { HermPlugin } from "../types"

const plugin: HermPlugin = {
  id: "acme.counter",
  enabled: false,
  tui(api) {
    api.command.register([{
      title: "Increment Counter",
      value: "acme.counter.increment",
      category: "Plugin",
      onSelect: () => {
        const next = api.kv.get("count", 0) + 1
        api.kv.set("count", next)
        api.ui.toast({ message: `count: ${next}` })
      },
    }])
  },
}

export default plugin
```

KV keys are scoped by plugin id at runtime.

## Example 5: Eikon rasterizer

Bundled plugins can register Studio rasterizers. In current bundled code, rasterizer examples may import internal utility types because there is no external plugin package boundary yet.

```tsx theme={null}
import type { HermPlugin } from "../types"
import type { Rasterizer } from "../../utils/eikon-render"

const rasterizer: Rasterizer = {
  name: "blocks",
  knobs: [],
  async available() {
    return true
  },
  async render(win) {
    const frame = Array.from({ length: win.h }, () => "█".repeat(win.w))
    return { frames: [frame] }
  },
}

const plugin: HermPlugin = {
  id: "acme.blocks",
  enabled: false,
  tui(api) {
    api.eikon.rasterizer.register(rasterizer)
  },
}

export default plugin
```

## Related docs

* [Plugin overview](/plugins/overview)
* [Plugin API reference](/plugins/api-reference)
* [Config tab](/features/config)
