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

Presentation / Lifecycle and mounting · `snapshot-lens` · [HTML](https://pi-tui.ratstack.sh/patterns/snapshot-lens/) · [JSON](https://pi-tui.ratstack.sh/patterns/snapshot-lens.json) · [all patterns](https://pi-tui.ratstack.sh/patterns.md)

Also known as Snapshot status projection.

## Intent

Derive compact status and bounded detail from lifecycle snapshots.

## Motivation

Subagents derives workflow widgets from job snapshots, while Dot314 projects lifecycle data into external sidebar slots.

## Applicability

- Use this when several job or workflow states need a coherent display.

## Structure

```text
lifecycle / jobs -> snapshot
snapshot -> status + widget
```

## Participants

- [`ctx.ui.setStatus`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user): Writes or clears one named footer slot.
- [`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.
- [`pi.on`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#events): Registers ordered extension event handlers.
- `Job snapshot`: Supplies lifecycle facts without being mutated by rendering.

Pi screen APIs: [`ctx.ui.setStatus`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user), [`ctx.ui.setWidget`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user), [`pi.on`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#events), `wrapTextWithAnsi`

## Consequences

- Status and detail can reflect one coherent snapshot.
- Dimension changes and external status effects remain separate concerns.

## Implementation

- Separate projection from mutations and external status effects.
- Invalidate dimension-dependent layout when identity or coverage changes.
- The cmux example is an external sidebar effect rather than a native Pi mount.

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

### Idle snapshot

`idle`: Project synthetic idle jobs into a compact signal.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Snapshot Lens, Idle snapshot, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/idle-40-dark.94fd9ed9e996.webp) ![Snapshot Lens, Idle snapshot, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/idle-40-light.c35fb8e7696f.webp)
- 60 columns: ![Snapshot Lens, Idle snapshot, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/idle-60-dark.cc6a27c2a7eb.webp) ![Snapshot Lens, Idle snapshot, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/idle-60-light.feb2588a5761.webp)
- 80 columns: ![Snapshot Lens, Idle snapshot, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/idle-80-dark.1da076e89c5a.webp) ![Snapshot Lens, Idle snapshot, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/idle-80-light.09cc489e5898.webp)
- 120 columns: ![Snapshot Lens, Idle snapshot, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/idle-120-dark.8ab8e80c5958.webp) ![Snapshot Lens, Idle snapshot, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/idle-120-light.10e7059784e6.webp)

### Workflow snapshot

`running`: Show a phase, nested jobs and bounded widget rows.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Snapshot Lens, Workflow snapshot, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/running-40-dark.568fbb9d78af.webp) ![Snapshot Lens, Workflow snapshot, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/running-40-light.5dda742f0e29.webp)
- 60 columns: ![Snapshot Lens, Workflow snapshot, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/running-60-dark.6f27200b4cc9.webp) ![Snapshot Lens, Workflow snapshot, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/running-60-light.933a0a1b215f.webp)
- 80 columns: ![Snapshot Lens, Workflow snapshot, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/running-80-dark.7c53886f7bd8.webp) ![Snapshot Lens, Workflow snapshot, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/running-80-light.c578db9c7bc7.webp)
- 120 columns: ![Snapshot Lens, Workflow snapshot, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/running-120-dark.1d6453f341af.webp) ![Snapshot Lens, Workflow snapshot, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/running-120-light.60860a836b60.webp)

### Lifecycle ends

`ended`: Update the completed snapshot and clear owned lifecycle status.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Snapshot Lens, Lifecycle ends, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/ended-40-dark.bcfdd2cbc40c.webp) ![Snapshot Lens, Lifecycle ends, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/ended-40-light.4cf81ccc0b60.webp)
- 60 columns: ![Snapshot Lens, Lifecycle ends, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/ended-60-dark.45e324904539.webp) ![Snapshot Lens, Lifecycle ends, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/ended-60-light.b982149b9148.webp)
- 80 columns: ![Snapshot Lens, Lifecycle ends, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/ended-80-dark.04e0c25b18b3.webp) ![Snapshot Lens, Lifecycle ends, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/ended-80-light.c0d0b86ffa52.webp)
- 120 columns: ![Snapshot Lens, Lifecycle ends, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/ended-120-dark.82b6e0d0f764.webp) ![Snapshot Lens, Lifecycle ends, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/snapshot-lens/ended-120-light.2469d3df72d8.webp)

## Sample code

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

```ts
// Snapshot Lens: project lifecycle facts into a compact signal and bounded detail.
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
import type { PatternStory } from "../../../src/pattern.ts";

type Job = { title: string; status: "running" | "queued" };
type Snapshot =
  | { kind: "idle"; queued: number }
  | { kind: "running"; phase: string; jobs: readonly Job[] }
  | { kind: "ended"; completed: number };
function project(snapshot: Snapshot) {
  switch (snapshot.kind) {
    case "idle": return { signal: snapshot.queued + " queued", lines: ["Workflow queue", snapshot.queued + " jobs queued · no active work"] };
    case "running": return {
      signal: snapshot.jobs.filter(job => job.status === "running").length + " running",
      lines: ["Task workflow · " + snapshot.phase, ...snapshot.jobs.map(job =>
        "  - " + job.title + " / " + job.status)],
    };
    case "ended": return { signal: undefined, lines: ["Workflow complete", snapshot.completed + " jobs checked · lifecycle slot cleared"] };
  }
}
export const story: PatternStory = {
  id: "snapshot-lens", title: "Snapshot Lens", kind: "screen",
  apis: ["ctx.ui.setStatus", "ctx.ui.setWidget", "wrapTextWithAnsi"],
  states: [
    { id: "idle", label: "Idle snapshot" },
    { id: "running", label: "Workflow snapshot", steps: [{ type: "action", name: "running" }] },
    { id: "ended", label: "Lifecycle ends", steps: [{ type: "action", name: "ended" }] },
  ],
  setup({ ui, theme, action, tui }) {
    // Synthetic lifecycle events replace snapshots; projection never mutates them.
    function publish(snapshot: Snapshot) {
      const projection = project(snapshot);
      ui.setStatus("jobs", projection.signal === undefined ? undefined : theme.fg("accent", projection.signal));
      ui.setWidget("workflow", () => ({
        invalidate() {},
        render(width) {
          const wrapped = projection.lines.flatMap(line => wrapTextWithAnsi(line, width));
          const budget = 5, omitted = Math.max(0, wrapped.length - budget);
          const visible = omitted ? [...wrapped.slice(0, budget - 1),
            "+" + (omitted + 1) + " detail rows · open workflow"] : wrapped;
          return visible.map((line, index) => theme.fg(index === 0 ? "accent" : "muted", line));
        },
      }), { placement: "aboveEditor" });
      tui.requestRender();
    }
    ui.setEditorText("Review the task list");
    publish({ kind: "idle", queued: 5 });
    action("running", () => publish({ kind: "running", phase: "validate", jobs: [
      { title: "Parser boundary", status: "running" },
      { title: "Theme preview", status: "running" },
      { title: "Keyboard focus", status: "queued" },
      { title: "Tool summary", status: "queued" },
      { title: "Release notes", status: "queued" },
    ] }));
    action("ended", () => publish({ kind: "ended", completed: 5 }));
    return () => { ui.setStatus("jobs", undefined); ui.setWidget("workflow", undefined); };
  },
};
```

## Known uses: seen in Nico's repos

- [**pi-subagents**](https://github.com/nicobailon/pi-subagents): Project workflow state before rendering status widgets
  - [src/tui/render.ts:2520-2585](https://github.com/nicobailon/pi-subagents/blob/6826b0545216077195ae8ffe468a6434715814b7/src/tui/render.ts#L2520-L2585) @6826b054
  - [src/tui/render.ts:3060-3110](https://github.com/nicobailon/pi-subagents/blob/6826b0545216077195ae8ffe468a6434715814b7/src/tui/render.ts#L3060-L3110) @6826b054
- [**dot314**](https://github.com/nicobailon/dot314): Project agent lifecycle into cmux status slots
  - [extensions/cmux/index.ts:60-89](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/cmux/index.ts#L60-L89) @17cce138

## For agents: choose and check

Choose Snapshot Lens when your job matches its intent and applicability above. Its neighbours in Lifecycle and mounting are listed below. Read the one whose intent fits your job more closely before you commit.

- [Elastic Overlay](https://pi-tui.ratstack.sh/patterns/elastic-overlay.md): Resolve overlay size and placement from current terminal dimensions.
- [Event Relay](https://pi-tui.ratstack.sh/patterns/event-relay.md): Update visible UI from named extension event channels.
- [Process Shell](https://pi-tui.ratstack.sh/patterns/process-shell.md): Bind a temporary terminal process view to one custom interaction.
- [Output Vault](https://pi-tui.ratstack.sh/patterns/output-vault.md): Keep completed job output available after its foreground view closes.
- [Deferred Crest](https://pi-tui.ratstack.sh/patterns/deferred-crest.md): Mount optional startup content only after deferred discovery remains eligible.
- [Session Memento](https://pi-tui.ratstack.sh/patterns/session-memento.md): Reconstruct deliberate view state from typed custom session entries.
- [Mode Fence](https://pi-tui.ratstack.sh/patterns/mode-fence.md): Keep terminal components separate from dialog-capable and no-UI modes.

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

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

- [Signal Pair](https://pi-tui.ratstack.sh/patterns/signal-pair.md): provides the two display surfaces

## Linked from

No other pattern links here.
