# Branch Fold

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

Also known as Foldable tree.

## Intent

Retain validated fold identifiers while navigating a session tree.

## Motivation

Dot314's anycopy selector reopens a session tree with saved fold identifiers.

## Applicability

- Use this when reopening a tree should preserve its compact view.

## Structure

```text
tree + valid fold IDs
      -> visible branches
```

## 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.
- [`pi.appendEntry`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#state-management): Persists typed custom data outside model context.
- [`ctx.sessionManager.getEntries`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#state-management): Reads session-file entries for deliberate reconstruction.
- `Fold identifiers`: Name hidden branches after validation against current nodes.

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), [`pi.appendEntry`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#state-management), [`ctx.sessionManager.getEntries`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#state-management), `TreeSelectorComponent`, `TreeSelectorComponent.getTreeList`, `setKeybindings`, `truncateToWidth`

## Consequences

- A reopened tree retains its compact view.
- Saved identifiers must be checked against the current nodes.

## Implementation

- Discard fold identifiers that no longer name a tree node.
- Check navigation capability before changing the session.

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

### Expanded tree

`expanded`: Show several synthetic branches with the selected node.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Branch Fold, Expanded tree, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/branch-fold/expanded-40-dark.c18c948248e5.webp) ![Branch Fold, Expanded tree, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/branch-fold/expanded-40-light.1b7be750bd07.webp)
- 60 columns: ![Branch Fold, Expanded tree, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/branch-fold/expanded-60-dark.d78772677d7d.webp) ![Branch Fold, Expanded tree, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/branch-fold/expanded-60-light.219456327f48.webp)
- 80 columns: ![Branch Fold, Expanded tree, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/branch-fold/expanded-80-dark.0335e705c004.webp) ![Branch Fold, Expanded tree, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/branch-fold/expanded-80-light.e0ebb123be73.webp)
- 120 columns: ![Branch Fold, Expanded tree, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/branch-fold/expanded-120-dark.428dcd286d7c.webp) ![Branch Fold, Expanded tree, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/branch-fold/expanded-120-light.70755ac606d3.webp)

### Folded branch

`folded`: Hide descendants while leaving the branch visible.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Branch Fold, Folded branch, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/branch-fold/folded-40-dark.1d60b1b8cf81.webp) ![Branch Fold, Folded branch, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/branch-fold/folded-40-light.7f148fca7455.webp)
- 60 columns: ![Branch Fold, Folded branch, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/branch-fold/folded-60-dark.1de7ce8bf2a5.webp) ![Branch Fold, Folded branch, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/branch-fold/folded-60-light.8e6da0e2421f.webp)
- 80 columns: ![Branch Fold, Folded branch, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/branch-fold/folded-80-dark.e109273a1520.webp) ![Branch Fold, Folded branch, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/branch-fold/folded-80-light.8c2f581f9bc1.webp)
- 120 columns: ![Branch Fold, Folded branch, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/branch-fold/folded-120-dark.4503759637c0.webp) ![Branch Fold, Folded branch, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/branch-fold/folded-120-light.e070910f4938.webp)

### Restored folds

`reopened`: Reopen from saved fold IDs and omit an invalid saved ID.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Branch Fold, Restored folds, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/branch-fold/reopened-40-dark.f2fdb3167ef7.webp) ![Branch Fold, Restored folds, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/branch-fold/reopened-40-light.08ea3e740b85.webp)
- 60 columns: ![Branch Fold, Restored folds, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/branch-fold/reopened-60-dark.c7fde4e9bd01.webp) ![Branch Fold, Restored folds, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/branch-fold/reopened-60-light.36082cdb5ed7.webp)
- 80 columns: ![Branch Fold, Restored folds, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/branch-fold/reopened-80-dark.d0a4a0c8d865.webp) ![Branch Fold, Restored folds, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/branch-fold/reopened-80-light.06fc62dad143.webp)
- 120 columns: ![Branch Fold, Restored folds, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/branch-fold/reopened-120-dark.dc063a97e9f6.webp) ![Branch Fold, Restored folds, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/branch-fold/reopened-120-light.6bff65366ce8.webp)

## Sample code

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

```ts
// Branch Fold: restore validated fold identifiers in a synthetic session tree.
import { TreeSelectorComponent, type SessionTreeNode } from "@earendil-works/pi-coding-agent";
import { getKeybindings, setKeybindings, truncateToWidth, type Component } from "@earendil-works/pi-tui";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "branch-fold", title: "Branch Fold", kind: "component",
  apis: ["TreeSelectorComponent", "TreeSelectorComponent.getTreeList", "setKeybindings", "truncateToWidth"],
  rows: 24,
  states: [
    { id: "expanded", label: "Expanded tree" },
    { id: "folded", label: "Folded branch", steps: [{ type: "action", name: "fold" }] },
    { id: "reopened", label: "Restored folds", steps: [{ type: "action", name: "reopen" }] },
  ],
  setup({ theme, tui, action, keybindings }) {
    // TreeSelector reads Pi\'s shared key registry, not constructor bindings.
    const previousBindings = getKeybindings();
    setKeybindings(keybindings);
    const node = (id: string, parentId: string | null, text: string, children: SessionTreeNode[] = []): SessionTreeNode => ({
      entry: { type: "message", id, parentId, timestamp: "2026-01-01T00:00:00Z",
        message: { role: "user", content: text, timestamp: 1767225600000 } },
      children,
    });
    const tree = [node("draft", null, "Sketch the task list", [
      node("preview", "draft", "Render a preview", [
        node("dark", "preview", "Review the dark theme"),
        node("light", "preview", "Review the light theme"),
      ]),
      node("checks", "draft", "Check the sample code"),
    ])];
    const index = new Set<string>();
    const visit = (nodes: SessionTreeNode[]) => nodes.forEach(n => { index.add(n.entry.id); visit(n.children); });
    visit(tree);
    // In a real extension these IDs come from appendEntry/getEntries.
    // This UI-only harness has no session-file APIs; saved data is a fixture.
    let savedFoldIds: readonly string[] = [];
    let caption = "Expanded · selected preview branch";
    const makeSelector = (selectedId: string) => new TreeSelectorComponent(tree, "checks", 12,
      id => { caption = "Selected " + id + " · navigation not available in this fixture"; tui.requestRender(); },
      () => {}, undefined, selectedId, "all");
    let selector = makeSelector("preview");
    action("fold", () => {
      const selected = selector.getTreeList().getSelectedNode();
      if (!selected) return;
      selector.handleInput("\x1b[1;5D"); // Pi's default fold-or-up binding.
      savedFoldIds = [selected.entry.id];
      caption = "Saved fold: " + selected.entry.id;
      tui.requestRender();
    });
    action("reopen", () => {
      const restoredIds = [...savedFoldIds, "removed-node"].filter(id => index.has(id));
      selector = makeSelector("preview");
      for (const id of restoredIds) {
        selector = makeSelector(id);
        selector.handleInput("\x1b[1;5D");
      }
      caption = "Restored preview · 1 stale ID dropped";
      tui.requestRender();
    });
    const component = {
      dispose() { setKeybindings(previousBindings); },
      invalidate() { selector.invalidate(); },
      handleInput(data: string) { selector.handleInput(data); tui.requestRender(); },
      render(width: number) {
        return [...selector.render(width), theme.fg("muted", truncateToWidth(caption, width))];
      },
    } satisfies Component & { dispose(): void };
    return component;
  },
};
```

## Known uses: seen in Nico's repos

- [**dot314**](https://github.com/nicobailon/dot314): Persist fold state while reusing a session-tree selector
  - [extensions/anycopy/index.ts:971-1035](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/anycopy/index.ts#L971-L1035) @17cce138

## For agents: choose and check

Choose Branch Fold 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.
- [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), [`pi.appendEntry`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#state-management), [`ctx.sessionManager.getEntries`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#state-management), `TreeSelectorComponent`, `TreeSelectorComponent.getTreeList`, `setKeybindings`, `truncateToWidth` before you use them.
- Every reference frame gets the verdict its state expects.

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

- [Session Memento](https://pi-tui.ratstack.sh/patterns/session-memento.md): persists the fold identifiers

## Linked from

- [Session Memento](https://pi-tui.ratstack.sh/patterns/session-memento.md): uses saved fold identifiers
