# Row Window

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

Also known as Windowed list.

## Intent

Keep the selected item inside a bounded moving list window.

## Motivation

Intercom's session picker shows multi-line entries inside a bounded selection window.

## Applicability

- Use this when a picker has more rows than its terminal view can show.

## Structure

```text
items + selection -> window
window -> fitted rows + hint
```

## Participants

- [`SelectList`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#compose-built-in-components): Owns a bounded selection list and selection callbacks.
- [`truncateToWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model): Clips text to its allotted columns and can pad the result.
- [`visibleWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model): Measures rendered terminal columns rather than string length.
- `Selection window`: Keeps the highlight inside its physical-row budget.

Pi component APIs: [`SelectList`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#compose-built-in-components), [`truncateToWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), [`visibleWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), `matchesKey`

## Consequences

- The selection stays visible without drawing the entire list.
- Physical rows, descriptions and hints share a limited budget.

## Implementation

- Budget physical rows rather than counting multi-line items.
- Keep clipping and position hints inside the width budget.

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

`start`: Show the first selected item and a clipped-list position hint.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Row Window, First window, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/start-40-dark.656ab7cdfd4b.webp) ![Row Window, First window, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/start-40-light.ea5d38feb9a9.webp)
- 60 columns: ![Row Window, First window, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/start-60-dark.fac680469116.webp) ![Row Window, First window, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/start-60-light.903102e6e5c7.webp)
- 80 columns: ![Row Window, First window, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/start-80-dark.caca5b30c57f.webp) ![Row Window, First window, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/start-80-light.616d4e732000.webp)
- 120 columns: ![Row Window, First window, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/start-120-dark.789d957e4b74.webp) ![Row Window, First window, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/start-120-light.38f9abfbfa99.webp)

### Moving window

`middle`: Move selection into the middle of a longer list.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Row Window, Moving window, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/middle-40-dark.28c9cc34e74f.webp) ![Row Window, Moving window, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/middle-40-light.d31664223567.webp)
- 60 columns: ![Row Window, Moving window, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/middle-60-dark.e8512692a390.webp) ![Row Window, Moving window, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/middle-60-light.4ce0673484ae.webp)
- 80 columns: ![Row Window, Moving window, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/middle-80-dark.278beee11332.webp) ![Row Window, Moving window, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/middle-80-light.26d61469393d.webp)
- 120 columns: ![Row Window, Moving window, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/middle-120-dark.b331457d78b3.webp) ![Row Window, Moving window, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/middle-120-light.3a95435bc029.webp)

### Multi-line items

`multiline`: Fit descriptions and hints into a fixed row budget.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Row Window, Multi-line items, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/multiline-40-dark.43d787bae3da.webp) ![Row Window, Multi-line items, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/multiline-40-light.1c74ab8d1366.webp)
- 60 columns: ![Row Window, Multi-line items, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/multiline-60-dark.aa6815c9f0b0.webp) ![Row Window, Multi-line items, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/multiline-60-light.07dceb78a378.webp)
- 80 columns: ![Row Window, Multi-line items, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/multiline-80-dark.c54514d660c3.webp) ![Row Window, Multi-line items, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/multiline-80-light.07705985ba56.webp)
- 120 columns: ![Row Window, Multi-line items, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/multiline-120-dark.52ef65136c41.webp) ![Row Window, Multi-line items, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/multiline-120-light.6e62b3bbe93a.webp)

### Don't: count entries as rows

`dont-count-items`

This state is a counter-example. It fails height on purpose.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ pass
- height: ✓ fails, as intended

Evidence from the checker:

- height at 40 dark: line 9, column 1: frame 0: rendered rows=14, limit=8
- height at 40 light: line 9, column 1: frame 0: rendered rows=14, limit=8
- height at 60 dark: line 9, column 1: frame 0: rendered rows=14, limit=8
- height at 60 light: line 9, column 1: frame 0: rendered rows=14, limit=8
- height at 80 dark: line 9, column 1: frame 0: rendered rows=14, limit=8
- height at 80 light: line 9, column 1: frame 0: rendered rows=14, limit=8
- height at 120 dark: line 9, column 1: frame 0: rendered rows=14, limit=8
- height at 120 light: line 9, column 1: frame 0: rendered rows=14, limit=8

Frames:

- 40 columns: ![Row Window, Don't: count entries as rows, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/dont-count-items-40-dark.51216f6c4492.webp) ![Row Window, Don't: count entries as rows, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/dont-count-items-40-light.48daf4185fcb.webp)
- 60 columns: ![Row Window, Don't: count entries as rows, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/dont-count-items-60-dark.36196fa90d28.webp) ![Row Window, Don't: count entries as rows, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/dont-count-items-60-light.9d10234995e7.webp)
- 80 columns: ![Row Window, Don't: count entries as rows, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/dont-count-items-80-dark.b82a05431d2a.webp) ![Row Window, Don't: count entries as rows, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/dont-count-items-80-light.8a6974f9672d.webp)
- 120 columns: ![Row Window, Don't: count entries as rows, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/row-window/dont-count-items-120-dark.8d258d703ead.webp) ![Row Window, Don't: count entries as rows, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/row-window/dont-count-items-120-light.e65969bbd68b.webp)

## Sample code

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

```ts
import { matchesKey, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
import type { PatternStory } from "../../../src/pattern.ts";

// Original example: each entry consumes a title row and a detail row.
export const story: PatternStory = {
  id: "row-window",
  title: "Row Window",
  kind: "component",
  apis: ["matchesKey", "truncateToWidth", "visibleWidth"],
  rows: 8,
  states: [
    { id: "start", label: "First window" },
    { id: "middle", label: "Moving window", steps: [
      { type: "keys", data: "\x1b[B" }, { type: "keys", data: "\x1b[B" }, { type: "keys", data: "\x1b[B" },
    ] },
    { id: "multiline", label: "Multi-line items", steps: [{ type: "action", name: "long-details" }] },
    { id: "dont-count-items", label: "Don't: count entries as rows", steps: [{ type: "action", name: "count-items" }], expectFail: ["height"] },
  ],
  setup(ctx) {
    const entries = [
      ["🐀 Explore", "Read the synthetic brief and map the available pieces."],
      ["界 Sketch", "A CJK title still reserves two terminal columns."],
      ["Build", "Use a deliberately long description to prove truncation."],
      ["Test", "Move selection without growing the terminal window."],
      ["Review", "Keep the selected entry visible near the bottom."],
      ["Ship", "Six items do not mean six physical rows."],
    ];
    let selected = 0, countItems = false;
    ctx.action("long-details", () => { entries.forEach(entry => { entry[1] = "Detailed task notes share the same physical-row budget and visible-width clipping."; }); });
    ctx.action("count-items", () => { countItems = true; });
    const component: Component = {
      invalidate() {},
      handleInput(data) {
        if (matchesKey(data, "down")) selected = Math.min(entries.length - 1, selected + 1);
        if (matchesKey(data, "up")) selected = Math.max(0, selected - 1);
        ctx.tui.requestRender();
      },
      render(width) {
        const available = ctx.tui.terminal.rows - 2;
        const count = countItems ? available : Math.max(1, Math.floor(available / 2));
        const start = Math.max(0, Math.min(selected - count + 1, entries.length - count));
        const lines = [ctx.theme.fg("accent", "Tasks · " + (selected + 1) + "/" + entries.length)];
        entries.slice(start, start + count).forEach(([title, detail], i) => {
          const marker = start + i === selected ? "> " : "  ";
          const row = truncateToWidth(marker + title!, width);
          if (visibleWidth(row) > width) throw new Error("Truncation invariant failed");
          lines.push(ctx.theme.fg(start + i === selected ? "accent" : "text", row));
          lines.push(ctx.theme.fg("muted", truncateToWidth("  " + detail!, width)));
        });
        lines.push(ctx.theme.fg(countItems ? "warning" : "dim", truncateToWidth(
          countItems ? "Don't: six entries consume twelve rows" : "↑↓ select · two rows per entry", width)));
        return lines;
      },
    };
    return component;
  },
};
```

## Known uses: seen in Nico's repos

- [**pi-intercom**](https://github.com/nicobailon/pi-intercom): Window session rows and truncate paths by visible width
  - [ui/session-list.ts:6-29](https://github.com/nicobailon/pi-intercom/blob/a5fad4df2a9fe4909bf4d9b06263c8316976b57d/ui/session-list.ts#L6-L29) @a5fad4df
  - [ui/session-list.ts:112-179](https://github.com/nicobailon/pi-intercom/blob/a5fad4df2a9fe4909bf4d9b06263c8316976b57d/ui/session-list.ts#L112-L179) @a5fad4df
- [**pi-skill-palette**](https://github.com/nicobailon/pi-skill-palette): Narrow palette results as the query grows
  - [index.ts:622-741](https://github.com/nicobailon/pi-skill-palette/blob/a5c4429b8c2e33ab903d07856497014f3d5ad34e/index.ts#L622-L741) @a5c4429b
- [**pi-subagents**](https://github.com/nicobailon/pi-subagents): Rank selector choices by best matching field
  - [src/tui/render-helpers.ts:1-29](https://github.com/nicobailon/pi-subagents/blob/6826b0545216077195ae8ffe468a6434715814b7/src/tui/render-helpers.ts#L1-L29) @6826b054
  - [src/slash/selector.ts:85-117](https://github.com/nicobailon/pi-subagents/blob/6826b0545216077195ae8ffe468a6434715814b7/src/slash/selector.ts#L85-L117) @6826b054

## For agents: choose and check

Choose Row Window 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.

- [Detail Lens](https://pi-tui.ratstack.sh/patterns/detail-lens.md): Derive a selected item's detail sections from a read-only snapshot.
- [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.
- "Don't: count entries as rows" is a counter-example. Its frames fail height on purpose. Your version should not look like it.
- Read the Pi 1.0.3 docs for [`SelectList`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#compose-built-in-components), [`truncateToWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), [`visibleWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), `matchesKey` before you use them.
- Every reference frame gets the verdict its state expects.

Next actions: `related({ id: "row-window" })` lists what to read next, and `states({ id: "row-window", 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): preserves identity when items change

## Linked from

No other pattern links here.
