# Lazy Peek

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

Presentation / Lists and pickers · `lazy-peek` · [HTML](https://pi-tui.ratstack.sh/patterns/lazy-peek/) · [JSON](https://pi-tui.ratstack.sh/patterns/lazy-peek.json) · [all patterns](https://pi-tui.ratstack.sh/patterns.md)

Also known as Lazy preview picker.

## Intent

Load and cache preview detail only for items the user inspects.

## Motivation

Dot314's session switcher avoids loading every full transcript before the user inspects an item.

## Applicability

- Use this when eagerly loading every record would make a picker expensive.

## Structure

```text
highlight -> cache lookup
miss -> load -> cache -> preview
```

## 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.
- [`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.
- [`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.
- `Preview cache`: Loads inspected detail and limits retained previews.

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), [`TUI.requestRender`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), [`Component`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), `wrapTextWithAnsi`

## Consequences

- Only inspected items need loaded preview detail.
- The cache needs a bound and dismissal stays distinct from selection.

## Implementation

- Bound the preview cache.
- Keep dismissal distinct from a confirmed selection.

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

### Preview pending

`loading`: Select a synthetic session while its preview is loading.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Lazy Peek, Preview pending, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loading-40-dark.08ece0ae3058.webp) ![Lazy Peek, Preview pending, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loading-40-light.83f418bb43c6.webp)
- 60 columns: ![Lazy Peek, Preview pending, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loading-60-dark.0170ba8ccd1e.webp) ![Lazy Peek, Preview pending, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loading-60-light.ca7c98f7428d.webp)
- 80 columns: ![Lazy Peek, Preview pending, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loading-80-dark.50c48ef22486.webp) ![Lazy Peek, Preview pending, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loading-80-light.02e0b20c6d35.webp)
- 120 columns: ![Lazy Peek, Preview pending, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loading-120-dark.2efdfb3c0ac2.webp) ![Lazy Peek, Preview pending, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loading-120-light.d6a75dd59664.webp)

### Preview loaded

`loaded`: Show the selected item's detail once available.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Lazy Peek, Preview loaded, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loaded-40-dark.e364fe53d6a3.webp) ![Lazy Peek, Preview loaded, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loaded-40-light.985581cc225b.webp)
- 60 columns: ![Lazy Peek, Preview loaded, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loaded-60-dark.933bd3fa13ed.webp) ![Lazy Peek, Preview loaded, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loaded-60-light.5df5de633a4b.webp)
- 80 columns: ![Lazy Peek, Preview loaded, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loaded-80-dark.8b39b4d7b95c.webp) ![Lazy Peek, Preview loaded, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loaded-80-light.63a105eb9147.webp)
- 120 columns: ![Lazy Peek, Preview loaded, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loaded-120-dark.78d60eb4b654.webp) ![Lazy Peek, Preview loaded, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/lazy-peek/loaded-120-light.886be22abc66.webp)

### Cached preview

`revisit`: Revisit the previous item and reuse its cached preview.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Lazy Peek, Cached preview, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/lazy-peek/revisit-40-dark.59244ae379a1.webp) ![Lazy Peek, Cached preview, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/lazy-peek/revisit-40-light.2bd9d78769a3.webp)
- 60 columns: ![Lazy Peek, Cached preview, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/lazy-peek/revisit-60-dark.f86436383dd7.webp) ![Lazy Peek, Cached preview, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/lazy-peek/revisit-60-light.6fe55875183a.webp)
- 80 columns: ![Lazy Peek, Cached preview, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/lazy-peek/revisit-80-dark.dc918a3b6745.webp) ![Lazy Peek, Cached preview, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/lazy-peek/revisit-80-light.500fbd794795.webp)
- 120 columns: ![Lazy Peek, Cached preview, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/lazy-peek/revisit-120-dark.171e29ecdd5a.webp) ![Lazy Peek, Cached preview, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/lazy-peek/revisit-120-light.3e0eb9fd1fd1.webp)

## Sample code

`stories/patterns/presentation/lazy-peek.ts`, the story the frames above were rendered from.

```ts
// Lazy Peek: load and cache detail only for inspected sessions.
import { wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "lazy-peek", title: "Lazy Peek", kind: "component",
  apis: ["Component", "wrapTextWithAnsi", "TUI.requestRender"], rows: 12,
  states: [
    { id: "loading", label: "Preview pending" },
    { id: "loaded", label: "Preview loaded", steps: [{ type: "wait", ms: 300 }] },
    { id: "revisit", label: "Cached preview", steps: [
      { type: "action", name: "inspect-next" }, { type: "wait", ms: 300 },
      { type: "action", name: "revisit" },
    ] },
  ],
  setup({ theme, tui, action }) {
    const sessions = ["Parser tests", "Release notes", "Theme audit"];
    const details = [
      "Fixed the token parser. All six synthetic cases pass.",
      "Drafted the upgrade notes. Two review items remain.",
      "Checked the compact footer in both appearances.",
    ];
    const previewCache = new Map<number, string>();
    let selected = 0, loads = 0, source = "pending";
    let pending: ReturnType<typeof setTimeout> | undefined;
    function inspect(index: number) {
      selected = index;
      if (pending !== undefined) clearTimeout(pending);
      if (previewCache.has(index)) { source = "cache hit"; tui.requestRender(); return; }
      source = "pending";
      loads++;
      pending = setTimeout(() => {
        if (previewCache.size === 2) previewCache.delete(previewCache.keys().next().value!);
        previewCache.set(index, details[index]!);
        source = "loaded"; pending = undefined; tui.requestRender();
      }, 300);
      tui.requestRender();
    }
    inspect(0);
    action("inspect-next", () => inspect(1));
    action("revisit", () => inspect(0));
    const view: Component & { dispose(): void } = {
      invalidate() {},
      render(width) {
        const lines = [theme.fg("accent", "Session previews"),
          ...sessions.map((name, i) => theme.fg(i === selected ? "accent" : "muted",
            (i === selected ? "> " : "  ") + name)), "",
          theme.fg("toolTitle", sessions[selected]!),
          ...wrapTextWithAnsi(previewCache.get(selected) ?? "Loading inspected preview…", width)
            .map(line => theme.fg(source === "pending" ? "warning" : "text", line)), "",
          theme.fg("dim", source + " · reads " + loads + " · cached " + previewCache.size + "/2")];
        return lines;
      },
      dispose() { if (pending !== undefined) clearTimeout(pending); },
    };
    return view;
  },
};
```

## Known uses: seen in Nico's repos

- [**dot314**](https://github.com/nicobailon/dot314): Reuse cached previews in a session-switch selector
  - [extensions/session-switch/picker.ts:1-45](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/session-switch/picker.ts#L1-L45) @17cce138

## For agents: choose and check

Choose Lazy Peek 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.
- [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.

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

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

- [Late Paint](https://pi-tui.ratstack.sh/patterns/late-paint.md): caches layout rather than fetched detail

## Linked from

- [Late Paint](https://pi-tui.ratstack.sh/patterns/late-paint.md): caches loaded data instead of layout
