# Elastic Overlay

> For agents: start with the [agent guide](https://pi-tui.ratstack.sh/llms.txt). Every page here has a Markdown twin, and every pattern has a JSON record.

Structural / Lifecycle and mounting · `elastic-overlay` · [HTML](https://pi-tui.ratstack.sh/patterns/elastic-overlay/) · [JSON](https://pi-tui.ratstack.sh/patterns/elastic-overlay.json) · [all patterns](https://pi-tui.ratstack.sh/patterns.md)

Also known as Responsive overlay.

## Intent

Resolve overlay size and placement from current terminal dimensions.

## Motivation

Nico's overlay positioning and recentering work prevents dialogs retaining stale mount-time coordinates after resize.

## Applicability

- Use this when a temporary view must remain positioned through resize.

## Structure

```text
terminal size + options -> bounds
bounds + component -> overlay
```

## Participants

- [`ctx.ui.custom`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays): Mounts one interaction and resolves through done.
- [`OverlayOptions`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays): Defines overlay dimensions, placement and focus behavior.
- `Current terminal bounds`: Drive placement again after dimension changes.

Pi screen APIs: [`ctx.ui.custom`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays), [`OverlayOptions`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays), `OverlayHandle.getBounds`, `Box`, `DynamicBorder`

## Consequences

- Anchors, margins and percentages adapt a view to the current terminal.
- The component must still fit each line inside its supplied width.

## Implementation

- Do not cache mount-time coordinates.
- Overlay sizing does not excuse lines exceeding render(width).

## States

Each state was rendered at 40, 60, 80 and 120 columns in Pi's dark and light themes. Each frame links one WebP. An animated frame links its first image, then the animation.

### Centered view

`centered`: Show a synthetic centered dialog above host content.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ pass
- height: ✓ pass

Frames:

- 40 columns: ![Elastic Overlay, Centered view, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/centered-40-dark.15b295554d22.webp) ![Elastic Overlay, Centered view, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/centered-40-light.34fdbd9badca.webp)
- 60 columns: ![Elastic Overlay, Centered view, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/centered-60-dark.e41026a4e5b5.webp) ![Elastic Overlay, Centered view, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/centered-60-light.0663d9fa5943.webp)
- 80 columns: ![Elastic Overlay, Centered view, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/centered-80-dark.ad90f62d8cf2.webp) ![Elastic Overlay, Centered view, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/centered-80-light.07e0760b372a.webp)
- 120 columns: ![Elastic Overlay, Centered view, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/centered-120-dark.7fbbb32f4b4d.webp) ![Elastic Overlay, Centered view, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/centered-120-light.d7cd4f34968d.webp)

### Recomputed bounds

`resized`: Resize the terminal and keep its bounds correctly centered.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ pass
- height: ✓ pass

Frames:

- 40 columns: ![Elastic Overlay, Recomputed bounds, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/resized-40-dark.a3009d3ed0cb.webp) ![Elastic Overlay, Recomputed bounds, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/resized-40-light.008e564cdde5.webp)
- 60 columns: ![Elastic Overlay, Recomputed bounds, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/resized-60-dark.bdfd83b64d70.webp) ![Elastic Overlay, Recomputed bounds, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/resized-60-light.52cd0bdc9b61.webp)
- 80 columns: ![Elastic Overlay, Recomputed bounds, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/resized-80-dark.0873d5136d3e.webp) ![Elastic Overlay, Recomputed bounds, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/resized-80-light.30486c0048cf.webp)
- 120 columns: ![Elastic Overlay, Recomputed bounds, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/resized-120-dark.4769e988f76c.webp) ![Elastic Overlay, Recomputed bounds, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/resized-120-light.f61f52c8b35a.webp)

### Anchored view

`anchored`: Show an edge-anchored panel with margins and responsive visibility.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ pass
- height: ✓ pass

Frames:

- 40 columns: ![Elastic Overlay, Anchored view, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/anchored-40-dark.eb03a6f90490.webp) ![Elastic Overlay, Anchored view, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/anchored-40-light.2132277eb70a.webp)
- 60 columns: ![Elastic Overlay, Anchored view, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/anchored-60-dark.9a54cf9f446a.webp) ![Elastic Overlay, Anchored view, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/anchored-60-light.cd11f1362895.webp)
- 80 columns: ![Elastic Overlay, Anchored view, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/anchored-80-dark.476b5cbc111f.webp) ![Elastic Overlay, Anchored view, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/anchored-80-light.d54dc43c56e4.webp)
- 120 columns: ![Elastic Overlay, Anchored view, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/anchored-120-dark.ee1d853cd101.webp) ![Elastic Overlay, Anchored view, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/elastic-overlay/anchored-120-light.83c7d36d25ec.webp)

## Sample code

`stories/patterns/structural/elastic-overlay.ts`, the story the frames above were rendered from.

```ts
// Elastic Overlay: let Pi resolve bounds again from current terminal dimensions.
import { Box, Text, matchesKey, type OverlayHandle, type OverlayOptions } from "@earendil-works/pi-tui";
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "elastic-overlay", title: "Elastic Overlay", kind: "screen",
  apis: ["ctx.ui.custom", "OverlayOptions", "OverlayHandle.getBounds", "Box", "DynamicBorder"],
  rows: 24, // Full viewport is the point: prove centering before and after resize.
  states: [
    { id: "centered", label: "Centered view", steps: [
      { type: "wait", ms: 0 }, { type: "action", name: "assert-center" },
    ] },
    { id: "resized", label: "Recomputed bounds", steps: [
      { type: "resize", rows: 18 }, { type: "action", name: "assert-center" },
    ] },
    { id: "anchored", label: "Anchored view", steps: [{ type: "action", name: "anchor" }] },
  ],
  setup({ ui, theme, action, tui }) {
    let handle: OverlayHandle | undefined;
    let complete: (() => void) | undefined;
    const openPanel = (title: string, options: OverlayOptions) => {
      void ui.custom<void>((_tui, panelTheme, _keys, done) => {
        complete = () => done();
        const panel = new Box(1, 0, line => panelTheme.bg("customMessageBg", line));
        const border = () => new DynamicBorder(line => panelTheme.fg("borderAccent", line));
        panel.addChild(border());
        panel.addChild(new Text(panelTheme.fg("accent", title), 0, 0));
        panel.addChild(new Text(panelTheme.fg("text", "Preview ready\n3 tasks · 2 checks passed"), 0, 0));
        panel.addChild(new Text(panelTheme.fg("muted", "esc close"), 0, 0));
        panel.addChild(border());
        return {
          render: (width: number) => panel.render(width),
          invalidate: () => panel.invalidate(),
          handleInput: (data: string) => { if (matchesKey(data, "escape")) done(); },
        };
      }, { overlay: true, overlayOptions: options, onHandle: value => { handle = value; } });
    };
    ui.setEditorText("Review the task preview");
    ui.setWidget("bounds", (_tui, widgetTheme) => ({
      invalidate() {},
      render(width) { return [widgetTheme.fg("muted", "Terminal: " + width + " × " + tui.terminal.rows)]; },
    }));
    openPanel("Task preview", { width: "75%", minWidth: 28, anchor: "center", margin: 1 });
    action("assert-center", () => {
      tui.renderNow();
      const bounds = handle?.getBounds();
      if (!bounds || bounds.col !== Math.floor((tui.terminal.columns - bounds.width) / 2) ||
          bounds.row !== Math.floor((tui.terminal.rows - bounds.height) / 2)) {
        throw new Error("Overlay bounds mismatch: " + JSON.stringify(bounds));
      }
    });
    action("anchor", () => {
      complete?.();
      openPanel("Task preview · docked", {
        width: "55%", minWidth: 28, anchor: "top-right", margin: { top: 1, right: 2, left: 1 },
        visible: (columns, rows) => columns >= 44 && rows >= 16,
      });
      ui.setWidget("visibility", [theme.fg("muted",
        tui.terminal.columns < 44 ? "Docked preview hidden below 44 columns" : "Docked preview · right margin 2")]);
    });
    return () => { complete?.(); ui.setWidget("bounds", undefined); ui.setWidget("visibility", undefined); };
  },
};
```

## Known uses: seen in Nico's repos

- [**earendil-works/pi**](https://github.com/earendil-works/pi): Configurable overlay sizing and placement
  - [packages/tui/src/tui.ts:71-104](https://github.com/earendil-works/pi/blob/0c0aac65990decf95ad5f49886ff5fccf1c09540/packages/tui/src/tui.ts#L71-L104) @0c0aac65
  - [packages/tui/src/tui.ts:87-120](https://github.com/earendil-works/pi/blob/a4ccff382c465fd789a318a981526b0b883630da/packages/tui/src/tui.ts#L87-L120) @a4ccff38
- [**earendil-works/pi**](https://github.com/earendil-works/pi): Recenter overlays after terminal resize
  - [packages/tui/src/tui.ts:262-280](https://github.com/earendil-works/pi/blob/c565fa9af8876b9f3db07d45ad99493fc4eb9d0b/packages/tui/src/tui.ts#L262-L280) @c565fa9a
- [**pi-interactive-shell**](https://github.com/nicobailon/pi-interactive-shell): Interactive shell overlay lifecycle and terminal sizing
  - [overlay-component.ts:20-205](https://github.com/nicobailon/pi-interactive-shell/blob/77df9a8142a2f731635a4c5a01d68feecb5cced4/overlay-component.ts#L20-L205) @77df9a81
- [**dot314**](https://github.com/nicobailon/dot314): Configurable overlay sizing and placement
- [**pi-autoresearch**](https://github.com/nicobailon/pi-autoresearch): Configurable overlay sizing and placement
- [**pi-interactive-shell**](https://github.com/nicobailon/pi-interactive-shell): Configurable overlay sizing and placement
- [**pi-intercom**](https://github.com/nicobailon/pi-intercom): Configurable overlay sizing and placement
- [**pi-mcp-adapter**](https://github.com/nicobailon/pi-mcp-adapter): Configurable overlay sizing and placement
- [**pi-memory-workbench**](https://github.com/nicobailon/pi-memory-workbench): Configurable overlay sizing and placement
- [**pi-powerline-footer**](https://github.com/nicobailon/pi-powerline-footer): Configurable overlay sizing and placement
- [**pi-side-chat**](https://github.com/nicobailon/pi-side-chat): Configurable overlay sizing and placement
- [**pi-skill-palette**](https://github.com/nicobailon/pi-skill-palette): Configurable overlay sizing and placement
- [**pi-subagents**](https://github.com/nicobailon/pi-subagents): Configurable overlay sizing and placement
- [**pi-tool-display**](https://github.com/nicobailon/pi-tool-display): Configurable overlay sizing and placement

## For agents: choose and check

Choose Elastic Overlay when your job matches its intent and applicability above. Its neighbours in Lifecycle and mounting are listed below. Read the one whose intent fits your job more closely before you commit.

- [Event Relay](https://pi-tui.ratstack.sh/patterns/event-relay.md): Update visible UI from named extension event channels.
- [Process Shell](https://pi-tui.ratstack.sh/patterns/process-shell.md): Bind a temporary terminal process view to one custom interaction.
- [Output Vault](https://pi-tui.ratstack.sh/patterns/output-vault.md): Keep completed job output available after its foreground view closes.
- [Deferred Crest](https://pi-tui.ratstack.sh/patterns/deferred-crest.md): Mount optional startup content only after deferred discovery remains eligible.
- [Session Memento](https://pi-tui.ratstack.sh/patterns/session-memento.md): Reconstruct deliberate view state from typed custom session entries.
- [Mode Fence](https://pi-tui.ratstack.sh/patterns/mode-fence.md): Keep terminal components separate from dialog-capable and no-UI modes.
- [Snapshot Lens](https://pi-tui.ratstack.sh/patterns/snapshot-lens.md): Derive compact status and bounded detail from lifecycle snapshots.

Check your version:

- Render your version at 40, 60, 80 and 120 columns in the dark and light themes.
- Check width: no rendered line is wider than the terminal.
- Check style-leak: no line ends with colour, bold or a link still switched on.
- Check hard-coded-colour: every colour on screen comes from the active theme.
- Check height: the output fits in the rows the terminal has.
- Read the Pi 1.0.3 docs for [`ctx.ui.custom`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays), [`OverlayOptions`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays), `OverlayHandle.getBounds`, `Box`, `DynamicBorder` before you use them.
- Every reference frame gets the verdict its state expects.

Next actions: `related({ id: "elastic-overlay" })` lists what to read next, and `states({ id: "elastic-overlay", width: 40 })` returns every frame and verdict at one width. The [build-pi-tui-component skill](https://pi-tui.ratstack.sh/skills/build-pi-tui-component.md) walks through the whole loop.

## Related

- [Hinge Panel](https://pi-tui.ratstack.sh/patterns/hinge-panel.md): adapts content within those bounds

## Linked from

No other pattern links here.
