# Detail Lens

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

Also known as List-detail view.

## Intent

Derive a selected item's detail sections from a read-only snapshot.

## Motivation

The Subagents fleet browser derives detail sections for a selected heterogeneous run.

## Applicability

- Use this when heterogeneous jobs or records need a browseable detail view.

## Structure

```text
snapshot -> selected item
selected item -> detail sections
```

## 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.
- [`Component`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model): Renders width-bounded lines and invalidates cached output.
- [`KeybindingsManager.matches`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus): Resolves input against configurable action bindings.
- `Snapshot projection`: Derives detail for the currently selected item.

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

## Consequences

- One selected snapshot drives consistent detail output.
- Projection must remain separate from job mutations and action callbacks.

## Implementation

- Do not mutate the underlying jobs during display projection.
- Pass actions and configurable keys into the mounted view.

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

### Selected detail

`default`: Show a list and details for its highlighted synthetic run.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Detail Lens, Selected detail, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/detail-lens/default-40-dark.0fc376b72feb.webp) ![Detail Lens, Selected detail, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/detail-lens/default-40-light.c8d2da4b6f44.webp)
- 60 columns: ![Detail Lens, Selected detail, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/detail-lens/default-60-dark.f92fb41c2864.webp) ![Detail Lens, Selected detail, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/detail-lens/default-60-light.36740118327f.webp)
- 80 columns: ![Detail Lens, Selected detail, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/detail-lens/default-80-dark.14778e7bb203.webp) ![Detail Lens, Selected detail, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/detail-lens/default-80-light.18ddc60d6d6e.webp)
- 120 columns: ![Detail Lens, Selected detail, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/detail-lens/default-120-dark.36748a35d43a.webp) ![Detail Lens, Selected detail, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/detail-lens/default-120-light.1891015790b8.webp)

### Different selection

`changed`: Select another run and derive its own detail sections.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Detail Lens, Different selection, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/detail-lens/changed-40-dark.1d0eebb38b8a.webp) ![Detail Lens, Different selection, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/detail-lens/changed-40-light.7c64c3ab4124.webp)
- 60 columns: ![Detail Lens, Different selection, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/detail-lens/changed-60-dark.408518ed1dc7.webp) ![Detail Lens, Different selection, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/detail-lens/changed-60-light.de946656efdf.webp)
- 80 columns: ![Detail Lens, Different selection, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/detail-lens/changed-80-dark.1d4c5e0ce7a0.webp) ![Detail Lens, Different selection, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/detail-lens/changed-80-light.2a350e869e1f.webp)
- 120 columns: ![Detail Lens, Different selection, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/detail-lens/changed-120-dark.8c130010f608.webp) ![Detail Lens, Different selection, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/detail-lens/changed-120-light.2d7040c91f7b.webp)

### Empty snapshot

`empty`: Show a no-runs view without stale details.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Detail Lens, Empty snapshot, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/detail-lens/empty-40-dark.14afccbe7246.webp) ![Detail Lens, Empty snapshot, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/detail-lens/empty-40-light.fa86468c56c4.webp)
- 60 columns: ![Detail Lens, Empty snapshot, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/detail-lens/empty-60-dark.f1e9a93173c5.webp) ![Detail Lens, Empty snapshot, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/detail-lens/empty-60-light.70408f15cbf5.webp)
- 80 columns: ![Detail Lens, Empty snapshot, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/detail-lens/empty-80-dark.13e9a79f2e09.webp) ![Detail Lens, Empty snapshot, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/detail-lens/empty-80-light.b6e665294dbc.webp)
- 120 columns: ![Detail Lens, Empty snapshot, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/detail-lens/empty-120-dark.004f00ac89df.webp) ![Detail Lens, Empty snapshot, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/detail-lens/empty-120-light.2703224478c9.webp)

## Sample code

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

```ts
// Detail Lens: project selected run details from a read-only snapshot.
import { 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: "detail-lens", title: "Detail Lens", kind: "component",
  apis: ["Component", "SelectList", "KeybindingsManager.matches", "truncateToWidth"],
  states: [
    { id: "default", label: "Selected detail" },
    { id: "changed", label: "Different selection", steps: [{ type: "keys", data: "\x1b[B" }] },
    { id: "empty", label: "Empty snapshot", steps: [{ type: "action", name: "empty" }] },
  ],
  setup({ theme, keybindings, tui, action }) {
    const runs = [
      { id: "preview", title: "Render preview", state: "ready", file: "src/preview.ts",
        detail: "Both themes rendered; review the narrow frame." },
      { id: "check", title: "Check task list", state: "waiting", file: "src/tasks.ts",
        detail: "Waiting for the sample-code review." },
    ] as const;
    let snapshot: readonly (typeof runs)[number][] = runs;
    const makeList = () => new SelectList(snapshot.map(run => ({
      value: run.id, label: run.title, description: run.state,
    })), 4, getSelectListTheme());
    let list = makeList();
    action("empty", () => { snapshot = []; list = makeList(); tui.requestRender(); });
    const component: Component = {
      invalidate() { list.invalidate(); },
      handleInput(data) {
        const index = snapshot.findIndex(run => run.id === list.getSelectedItem()?.value);
        if (keybindings.matches(data, "tui.select.down")) list.setSelectedIndex(Math.min(snapshot.length - 1, index + 1));
        else if (keybindings.matches(data, "tui.select.up")) list.setSelectedIndex(Math.max(0, index - 1));
        tui.requestRender();
      },
      render(width) {
        const selected = snapshot.find(run => run.id === list.getSelectedItem()?.value);
        if (!selected) return [
          theme.fg("accent", "Runs · 0"),
          theme.fg("muted", "No runs in this snapshot."),
          theme.fg("dim", "Start a task to see its details."),
        ];
        const detail = [selected.title, "State: " + selected.state, "File: " + selected.file, selected.detail];
        return [
          theme.fg("accent", "Runs · " + snapshot.length),
          ...list.render(width), "",
          theme.fg("accent", "SELECTED RUN"),
          ...detail.map(line => theme.fg("text", truncateToWidth(line, width))),
          theme.fg("muted", "↑↓ select · snapshot stays unchanged"),
        ];
      },
    };
    return component;
  },
};
```

## Known uses: seen in Nico's repos

- [**pi-subagents**](https://github.com/nicobailon/pi-subagents): Keep fleet selection stable while details are derived
  - [src/tui/fleet.ts:1-120](https://github.com/nicobailon/pi-subagents/blob/6826b0545216077195ae8ffe468a6434715814b7/src/tui/fleet.ts#L1-L120) @6826b054
  - [src/tui/fleet.ts:1360-1463](https://github.com/nicobailon/pi-subagents/blob/6826b0545216077195ae8ffe468a6434715814b7/src/tui/fleet.ts#L1360-L1463) @6826b054
- [**pi-mcp-adapter**](https://github.com/nicobailon/pi-mcp-adapter): Responsive list/detail TUI panels
  - [mcp-setup-panel.ts:12-37](https://github.com/nicobailon/pi-mcp-adapter/blob/85db03d87cd0f7461b55eab8d25c10bce473b801/mcp-setup-panel.ts#L12-L37) @85db03d8
  - [mcp-setup-panel.ts:45-84](https://github.com/nicobailon/pi-mcp-adapter/blob/85db03d87cd0f7461b55eab8d25c10bce473b801/mcp-setup-panel.ts#L45-L84) @85db03d8

## For agents: choose and check

Choose Detail Lens 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.
- [Tab Deck](https://pi-tui.ratstack.sh/patterns/tab-deck.md): Keep separate keyboard-navigable data views behind a tab strip.
- [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), [`Component`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), [`KeybindingsManager.matches`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), `SelectList`, `truncateToWidth` before you use them.
- Every reference frame gets the verdict its state expects.

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

- [Identity Anchor](https://pi-tui.ratstack.sh/patterns/identity-anchor.md): keeps the selected identity stable

## Linked from

No other pattern links here.
