# Preview Basket

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

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

Also known as Multi-select preview.

## Intent

Keep selection separate from thumbnail loading and zoom inspection.

## Motivation

Dot314's screenshot picker needs staged selections alongside thumbnail and zoom inspection.

## Applicability

- Use this when a picker stages several images before confirmation.

## Structure

```text
selected set -> staged widget
current item -> thumbnail / zoom
```

## 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.
- [`Image`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/tui/README.md#image): Renders an inline image or unsupported-terminal placeholder.
- [`ctx.ui.setWidget`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user): Mounts, replaces or clears one near-editor widget.
- `Staged set`: Retains selected items independently of zoom inspection.

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), [`Image`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/tui/README.md#image), [`ctx.ui.setWidget`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user), `Component`, `Text`

## Consequences

- Inspection can change without changing the selected set.
- Loading, missing sources and staged-widget cleanup remain separate work.

## Implementation

- Handle unavailable images without changing selection.
- Clear the staged preview widget when dismissed.

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

### Staged images

`selected`: Mark two synthetic images and show their staged previews.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Preview Basket, Staged images, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/preview-basket/selected-40-dark.ba793af42fec.webp) ![Preview Basket, Staged images, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/preview-basket/selected-40-light.0c229652a015.webp)
- 60 columns: ![Preview Basket, Staged images, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/preview-basket/selected-60-dark.3cd55c92f79e.webp) ![Preview Basket, Staged images, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/preview-basket/selected-60-light.c87e32c9c5de.webp)
- 80 columns: ![Preview Basket, Staged images, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/preview-basket/selected-80-dark.4489f710839d.webp) ![Preview Basket, Staged images, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/preview-basket/selected-80-light.954d4ac4b278.webp)
- 120 columns: ![Preview Basket, Staged images, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/preview-basket/selected-120-dark.c0e2ad058cfb.webp) ![Preview Basket, Staged images, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/preview-basket/selected-120-light.a789bc51fbaa.webp)

### Zoom inspection

`zoom`: Inspect one image without changing the staged set.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Preview Basket, Zoom inspection, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/preview-basket/zoom-40-dark.30cb7fede234.webp) ![Preview Basket, Zoom inspection, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/preview-basket/zoom-40-light.f4161d20d0c6.webp)
- 60 columns: ![Preview Basket, Zoom inspection, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/preview-basket/zoom-60-dark.56e398f4b6ad.webp) ![Preview Basket, Zoom inspection, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/preview-basket/zoom-60-light.130c33afab24.webp)
- 80 columns: ![Preview Basket, Zoom inspection, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/preview-basket/zoom-80-dark.05986c65b771.webp) ![Preview Basket, Zoom inspection, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/preview-basket/zoom-80-light.42df02506241.webp)
- 120 columns: ![Preview Basket, Zoom inspection, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/preview-basket/zoom-120-dark.becf3bc11e14.webp) ![Preview Basket, Zoom inspection, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/preview-basket/zoom-120-light.af4a60a4d4d3.webp)

### Unavailable image

`missing`: Show a text placeholder for an unavailable source.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Preview Basket, Unavailable image, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/preview-basket/missing-40-dark.dee663b227a5.webp) ![Preview Basket, Unavailable image, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/preview-basket/missing-40-light.3618377df719.webp)
- 60 columns: ![Preview Basket, Unavailable image, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/preview-basket/missing-60-dark.37a705e35f4f.webp) ![Preview Basket, Unavailable image, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/preview-basket/missing-60-light.e2ac9ca5eee5.webp)
- 80 columns: ![Preview Basket, Unavailable image, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/preview-basket/missing-80-dark.aa273da2fa94.webp) ![Preview Basket, Unavailable image, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/preview-basket/missing-80-light.0399cfae9bf8.webp)
- 120 columns: ![Preview Basket, Unavailable image, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/preview-basket/missing-120-dark.ed88367e51c9.webp) ![Preview Basket, Unavailable image, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/preview-basket/missing-120-light.ec2b377ad6ba.webp)

## Sample code

`stories/patterns/behavioral/preview-basket.ts`, the story the frames above were rendered from.

```ts
// Preview Basket: keep staging independent of zoom and source availability.
import { Image, Text, getCapabilities, setCapabilities } from "@earendil-works/pi-tui";
import type { PatternStory } from "../../../src/pattern.ts";

import { syntheticPreview } from "../../fixtures/synthetic-images.ts";
const images = ["task-list.png", "patch-preview.png", "missing.png"];
type Inspection = { mode: "thumbnails" } | { mode: "zoom"; name: string } | { mode: "missing"; name: string };

export const story: PatternStory = {
  id: "preview-basket", title: "Preview Basket", kind: "component",
  apis: ["Image", "Component", "Text"], rows: 16,
  states: [
    { id: "selected", label: "Staged images" },
    { id: "zoom", label: "Zoom inspection", steps: [{ type: "action", name: "zoom" }] },
    { id: "missing", label: "Unavailable image", steps: [{ type: "action", name: "missing" }] },
  ],
  setup({ tui, theme, action }) {
    const previous = getCapabilities();
    setCapabilities({ ...previous, images: "kitty" });
    const staged = new Set(images.slice(0, 2));
    let inspection: Inspection = { mode: "thumbnails" };
    const previews = new Map(images.slice(0, 2).map(name => [name, new Image(
      syntheticPreview(name === images[0] ? "tasks" : "patch"), "image/png", { fallbackColor: text => theme.fg("muted", text) },
      { filename: name, maxWidthCells: 24, maxHeightCells: 3 },
    )]));
    const zoom = new Image(syntheticPreview("tasks"), "image/png", { fallbackColor: text => theme.fg("muted", text) },
      { filename: images[0], maxWidthCells: 48, maxHeightCells: 7 });
    function inspect(next: Inspection) { inspection = next; tui.requestRender(); }
    action("zoom", () => inspect({ mode: "zoom", name: images[0]! }));
    action("missing", () => inspect({ mode: "missing", name: images[2]! }));
    function toggle(name: string) {
      if (staged.has(name)) staged.delete(name); else staged.add(name);
      tui.requestRender();
    }
    return {
      invalidate() { for (const image of previews.values()) image.invalidate(); zoom.invalidate(); },
      dispose() { staged.clear(); previews.clear(); setCapabilities(previous); },
      handleInput(data) { if (data === " ") toggle(images[0]!); },
      render(width) {
        const lines = new Text(theme.fg("accent", "Stage screenshots · " + staged.size + " selected"), 0, 0).render(width);
        for (const name of images) lines.push(...new Text(theme.fg(
          staged.has(name) ? "success" : "muted", (staged.has(name) ? "[x] " : "[ ] ") + name,
        ), 0, 0).render(width));
        lines.push("", ...new Text(theme.fg("accent",
          inspection.mode === "thumbnails" ? "Staged previews" : "Inspect · " + inspection.name,
        ), 0, 0).render(width));
        if (inspection.mode === "thumbnails") {
          for (const name of staged) lines.push(...(previews.get(name)?.render(width) ?? []));
        } else if (inspection.mode === "zoom") {
          lines.push(...zoom.render(width));
          lines.push(...new Text(theme.fg("muted", "Zoom leaves both staged items selected"), 0, 0).render(width));
        } else {
          lines.push(...new Text(theme.fg("warning", "Source unavailable · selection unchanged"), 0, 0).render(width));
        }
        lines.push("", ...new Text(theme.fg("dim", "PNG previews · staging preserved"), 0, 0).render(width));
        return lines;
      },
    };
  },
};
```

## Known uses: seen in Nico's repos

- [**dot314**](https://github.com/nicobailon/dot314): Select and preview multiple screenshots in a custom picker
  - [extensions/screenshots-picker/index.ts:800-830](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/screenshots-picker/index.ts#L800-L830) @17cce138

## For agents: choose and check

Choose Preview Basket 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.
- [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), [`Image`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/tui/README.md#image), [`ctx.ui.setWidget`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user), `Component`, `Text` before you use them.
- Every reference frame gets the verdict its state expects.

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

- [Image Parachute](https://pi-tui.ratstack.sh/patterns/image-parachute.md): handles preview capability limits

## Linked from

- [Image Parachute](https://pi-tui.ratstack.sh/patterns/image-parachute.md): uses image-backed staged inspection
