# Tab Deck

> 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 / Lists and pickers · `tab-deck` · [HTML](https://pi-tui.ratstack.sh/patterns/tab-deck/) · [JSON](https://pi-tui.ratstack.sh/patterns/tab-deck.json) · [all patterns](https://pi-tui.ratstack.sh/patterns.md)

Also known as Tabbed view.

## Intent

Keep separate keyboard-navigable data views behind a tab strip.

## Motivation

The usage extension puts several data views in one keyboard-driven dashboard.

## Applicability

- Use this when one compact dashboard has several related datasets.

## Structure

```text
keys -> active tab
active tab -> rows + selection
```

## 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.
- [`matchesKey`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus): Recognizes explicit terminal key combinations.
- [`TUI.requestRender`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model): Requests a coalesced redraw after state changes.
- `Active tab`: Selects which dataset and row highlight are visible.

Pi component 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), [`matchesKey`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), [`TUI.requestRender`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), `SelectList`, `truncateToWidth`

## Consequences

- Tabs keep related datasets in one compact view.
- Only the active tab is visible at a time.

## Implementation

- Request a render after tab or row changes.
- Resolve the custom interaction on dismissal.

## 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.

### First tab

`first`: Show summary rows under the active tab.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Tab Deck, First tab, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/tab-deck/first-40-dark.9aaa2d7ca516.webp) ![Tab Deck, First tab, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/tab-deck/first-40-light.b0312fdc2e56.webp)
- 60 columns: ![Tab Deck, First tab, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/tab-deck/first-60-dark.e01b183d2c13.webp) ![Tab Deck, First tab, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/tab-deck/first-60-light.af8173ff1f19.webp)
- 80 columns: ![Tab Deck, First tab, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/tab-deck/first-80-dark.af5115350f14.webp) ![Tab Deck, First tab, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/tab-deck/first-80-light.c4360638b525.webp)
- 120 columns: ![Tab Deck, First tab, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/tab-deck/first-120-dark.2fd0c5d51bf7.webp) ![Tab Deck, First tab, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/tab-deck/first-120-light.a6dbe7f8e5f5.webp)

### Second tab

`second`: Change tab and show its independent rows and highlight.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Tab Deck, Second tab, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/tab-deck/second-40-dark.94d36cb22136.webp) ![Tab Deck, Second tab, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/tab-deck/second-40-light.77b7a20eaf9c.webp)
- 60 columns: ![Tab Deck, Second tab, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/tab-deck/second-60-dark.838d8ffe852a.webp) ![Tab Deck, Second tab, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/tab-deck/second-60-light.6c43c623dd81.webp)
- 80 columns: ![Tab Deck, Second tab, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/tab-deck/second-80-dark.e7397945c8de.webp) ![Tab Deck, Second tab, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/tab-deck/second-80-light.85621b887a42.webp)
- 120 columns: ![Tab Deck, Second tab, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/tab-deck/second-120-dark.8f5f8e891c02.webp) ![Tab Deck, Second tab, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/tab-deck/second-120-light.2eafc9f0a87e.webp)

## Sample code

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

```ts
// Tab Deck: preserve each dataset and its selection behind a keyboard-driven tab strip.
import { matchesKey, SelectList, truncateToWidth, type Component } from "@earendil-works/pi-tui";
import { getSelectListTheme } from "@earendil-works/pi-coding-agent";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "tab-deck", title: "Tab Deck", kind: "component",
  apis: ["matchesKey", "SelectList", "TUI.requestRender", "truncateToWidth"],
  states: [
    { id: "first", label: "First tab" },
    { id: "second", label: "Second tab", steps: [
      { type: "keys", data: "\x1b[B" }, { type: "keys", data: "\t" },
      { type: "keys", data: "\x1b[B" },
    ] },
  ],
  setup({ theme, tui }) {
    const tabs = [
      { title: "Tasks", rows: ["Render preview · ready", "Review sample · waiting", "Publish · queued"] },
      { title: "Files", rows: ["src/preview.ts · 2 edits", "src/tasks.ts · 1 edit", "test/preview.ts · unchanged"] },
    ].map(tab => ({ ...tab, list: new SelectList(tab.rows.map((label, i) => ({
      value: String(i), label,
    })), 4, getSelectListTheme()) }));
    let activeTab = 0;
    const component: Component = {
      invalidate() { tabs.forEach(tab => tab.list.invalidate()); },
      handleInput(data) {
        if (matchesKey(data, "tab") || matchesKey(data, "right")) activeTab = (activeTab + 1) % tabs.length;
        else if (matchesKey(data, "left")) activeTab = (activeTab + tabs.length - 1) % tabs.length;
        else tabs[activeTab]!.list.handleInput(data);
        tui.requestRender();
      },
      render(width) {
        const strip = tabs.map((tab, i) => theme.fg(i === activeTab ? "accent" : "muted",
          i === activeTab ? "[" + tab.title + "]" : " " + tab.title + " ")).join("  ");
        const current = tabs[activeTab]!;
        return [
          theme.fg("accent", "Task workspace"),
          truncateToWidth(strip, width), theme.fg("border", "─".repeat(width)),
          ...current.list.render(width),
          theme.fg("dim", truncateToWidth("Row " + (Number(current.list.getSelectedItem()?.value ?? 0) + 1) +
            " of " + current.rows.length + " · " + current.title, width)),
          theme.fg("muted", "tab / ←→ view · ↑↓ row"),
        ];
      },
    };
    return component;
  },
};
```

## Known uses: seen in Nico's repos

- [**pi-extensions**](https://github.com/nicobailon/pi-extensions): Usage extension: navigable tabular dashboard
  - [usage-extension/index.ts:347-400](https://github.com/nicobailon/pi-extensions/blob/bca5070b541ffa7d273e74036dcd7d5f8b63eed3/usage-extension/index.ts#L347-L400) @bca5070b
  - [usage-extension/index.ts:526-558](https://github.com/nicobailon/pi-extensions/blob/bca5070b541ffa7d273e74036dcd7d5f8b63eed3/usage-extension/index.ts#L526-L558) @bca5070b

## For agents: choose and check

Choose Tab Deck when your job matches its intent and applicability above. Its neighbours in Lists and pickers are listed below. Read the one whose intent fits your job more closely before you commit.

- [Row Window](https://pi-tui.ratstack.sh/patterns/row-window.md): Keep the selected item inside a bounded moving list window.
- [Detail Lens](https://pi-tui.ratstack.sh/patterns/detail-lens.md): Derive a selected item's detail sections from a read-only snapshot.
- [Branch Fold](https://pi-tui.ratstack.sh/patterns/branch-fold.md): Retain validated fold identifiers while navigating a session tree.
- [Match Ladder](https://pi-tui.ratstack.sh/patterns/match-ladder.md): Rank matching options across weighted label and description fields.
- [Shrinking Sieve](https://pi-tui.ratstack.sh/patterns/shrinking-sieve.md): Narrow existing candidates while a search query grows.
- [Identity Anchor](https://pi-tui.ratstack.sh/patterns/identity-anchor.md): Retain the highlighted item's identity as asynchronous results arrive.
- [Preview Basket](https://pi-tui.ratstack.sh/patterns/preview-basket.md): Keep selection separate from thumbnail loading and zoom inspection.
- [Lazy Peek](https://pi-tui.ratstack.sh/patterns/lazy-peek.md): Load and cache preview detail only for items the user inspects.

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), [`matchesKey`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), [`TUI.requestRender`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), `SelectList`, `truncateToWidth` before you use them.
- Every reference frame gets the verdict its state expects.

Next actions: `related({ id: "tab-deck" })` lists what to read next, and `states({ id: "tab-deck", 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

- [Action Compass](https://pi-tui.ratstack.sh/patterns/action-compass.md): resolves navigation actions

## Linked from

No other pattern links here.
