# Abort Lantern

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

Also known as Cancellable loader.

## Intent

Settle foreground loading before opening a separate result view.

## Motivation

Dot314's session-ask operation must settle its foreground loader before showing analysis results.

## Applicability

- Use this when a long operation needs a visible cancel control.

## Structure

```text
work -> bordered loader
escape -> cancel -> done(null)
success -> done -> result view
```

## 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.
- [`BorderedLoader`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#compose-built-in-components): Presents cancellable foreground loading inside a border.
- `Owning operation`: Settles work before constructing a separate result view.

Pi screen 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), [`BorderedLoader`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#compose-built-in-components), `Container`, `ctx.ui.setWidget`

## Consequences

- Users can cancel long foreground work.
- Success, failure and null cancellation need separate settlement paths.

## Implementation

- Resolve success, failure and cancellation.
- Check the cancellation result before building the result view.

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

### Loading

`loading`: Show the animated loader and its cancel hint.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Abort Lantern, Loading, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-40-dark.5bc57715d511.webp) [animated](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-40-dark.anim.26645163d26a.webp) ![Abort Lantern, Loading, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-40-light.9b2e353746e6.webp) [animated](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-40-light.anim.1d8a6a198736.webp)
- 60 columns: ![Abort Lantern, Loading, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-60-dark.9835bb0aa58c.webp) [animated](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-60-dark.anim.ed216dabe03f.webp) ![Abort Lantern, Loading, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-60-light.e1fcd0a0c39e.webp) [animated](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-60-light.anim.29034ca19285.webp)
- 80 columns: ![Abort Lantern, Loading, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-80-dark.883aee2d2719.webp) [animated](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-80-dark.anim.5c343dad0f1c.webp) ![Abort Lantern, Loading, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-80-light.e3d8cce3e0a8.webp) [animated](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-80-light.anim.557e7ef556d0.webp)
- 120 columns: ![Abort Lantern, Loading, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-120-dark.c694c1791a16.webp) [animated](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-120-dark.anim.81a10f581f3c.webp) ![Abort Lantern, Loading, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-120-light.947093436a4e.webp) [animated](https://pi-tui.ratstack.sh/frames/abort-lantern/loading-120-light.anim.37b4b6d4d193.webp)

### Result view

`success`: Replace the completed loader with synthetic results.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Abort Lantern, Result view, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/success-40-dark.369beeca0c1d.webp) ![Abort Lantern, Result view, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/success-40-light.b990b6bc3403.webp)
- 60 columns: ![Abort Lantern, Result view, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/success-60-dark.558755f39703.webp) ![Abort Lantern, Result view, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/success-60-light.2ce973b2b6ed.webp)
- 80 columns: ![Abort Lantern, Result view, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/success-80-dark.a80213de2972.webp) ![Abort Lantern, Result view, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/success-80-light.cc7d6920f968.webp)
- 120 columns: ![Abort Lantern, Result view, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/success-120-dark.03cff3ec6b95.webp) ![Abort Lantern, Result view, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/success-120-light.a9a59334354e.webp)

### Cancelled

`cancelled`: Return to the host without opening results.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Abort Lantern, Cancelled, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/cancelled-40-dark.87d5739c1765.webp) ![Abort Lantern, Cancelled, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/cancelled-40-light.6300c67550d2.webp)
- 60 columns: ![Abort Lantern, Cancelled, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/cancelled-60-dark.3e6b4fcfdd3d.webp) ![Abort Lantern, Cancelled, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/cancelled-60-light.b9eedc71d2f2.webp)
- 80 columns: ![Abort Lantern, Cancelled, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/cancelled-80-dark.0c65cdbaa854.webp) ![Abort Lantern, Cancelled, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/cancelled-80-light.59d19c7f0292.webp)
- 120 columns: ![Abort Lantern, Cancelled, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/cancelled-120-dark.00ae1908fbca.webp) ![Abort Lantern, Cancelled, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/cancelled-120-light.6ba2896e8cdf.webp)

### Failure

`failed`: Settle loading and show a concise error.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Abort Lantern, Failure, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/failed-40-dark.2ce8feb1dea0.webp) ![Abort Lantern, Failure, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/failed-40-light.029050e1ea49.webp)
- 60 columns: ![Abort Lantern, Failure, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/failed-60-dark.f65ecdb3cd08.webp) ![Abort Lantern, Failure, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/failed-60-light.e0cdd2640e4b.webp)
- 80 columns: ![Abort Lantern, Failure, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/failed-80-dark.32b705a6ee9a.webp) ![Abort Lantern, Failure, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/failed-80-light.5a18ff7d7e72.webp)
- 120 columns: ![Abort Lantern, Failure, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/abort-lantern/failed-120-dark.be4491937201.webp) ![Abort Lantern, Failure, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/abort-lantern/failed-120-light.beacfe5501a8.webp)

## Sample code

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

```ts
// Abort Lantern: settle cancellable loading before constructing a result view.
import { BorderedLoader } from "@earendil-works/pi-coding-agent";
import { Container, Text } from "@earendil-works/pi-tui";
import type { PatternStory } from "../../../src/pattern.ts";

type WorkResult = { status: "success"; summary: string } | { status: "failed"; message: string } | null;

export const story: PatternStory = {
  id: "abort-lantern", title: "Abort Lantern", kind: "screen",
  apis: ["ctx.ui.custom", "BorderedLoader", "Container", "ctx.ui.setWidget"],
  states: [
    { id: "loading", label: "Loading", animated: true, steps: [{ type: "tick", ms: 400 }] },
    { id: "success", label: "Result view", steps: [{ type: "action", name: "succeed" }] },
    { id: "cancelled", label: "Cancelled", steps: [
      { type: "keys", data: "\x1b" }, { type: "action", name: "retry" },
      { type: "tick", ms: 200 }, { type: "keys", data: "\x1b" },
    ] },
    { id: "failed", label: "Failure", steps: [
      { type: "action", name: "retry" }, { type: "tick", ms: 200 },
      { type: "action", name: "fail" },
    ] },
  ],
  setup({ ui, action, theme }) {
    ui.setEditorText("Review the task summary");
    let finishWork: (result: WorkResult) => void = () => {};
    action("succeed", () => finishWork({ status: "success", summary: "3 tasks · 2 ready · 1 needs review" }));
    action("fail", () => finishWork({ status: "failed", message: "Analysis failed · fixture unavailable" }));
    function load() {
      ui.setWidget("analysis", [theme.fg("muted", "Analysis pending · escape cancels")]);
      void ui.custom<WorkResult>((tui, theme, _keys, done) => {
        const loader = new BorderedLoader(tui, theme, "Analyzing 3 synthetic tasks…");
        let settlement: "pending" | "settled" = "pending";
        function settle(result: WorkResult) {
          if (settlement === "settled") return;
          settlement = "settled";
          done(result);
        }
        loader.onAbort = () => settle(null);
        // Synthetic driver: no model, files, network, or real work.
        finishWork = result => { if (!loader.signal.aborted) settle(result); };
        return loader;
      }).then(result => {
        if (result === null) {
          ui.setWidget("analysis", [theme.fg("muted", "Cancelled · no result view opened")]);
          return;
        }
        if (result.status === "failed") {
          ui.setWidget("analysis", [theme.fg("error", result.message)]);
          return;
        }
        ui.setWidget("analysis", [theme.fg("success", "Loader settled · results ready")]);
        // Only a successful, settled operation may open this second interaction.
        void ui.custom<void>((_tui, theme, keys, done) => {
          const results = new Container();
          results.addChild(new Text(theme.fg("accent", "Task analysis"), 0, 0));
          results.addChild(new Text(result.summary, 0, 0));
          results.addChild(new Text(theme.fg("muted", "Review task: document the retry path"), 0, 0));
          results.addChild(new Text(theme.fg("dim", "escape returns to the draft"), 0, 0));
          return {
            invalidate() { results.invalidate(); },
            render: width => results.render(width),
            handleInput(data) { if (keys.matches(data, "tui.select.cancel")) done(); },
          };
        });
      });
    }
    action("retry", load);
    load();
  },
};
```

## Known uses: seen in Nico's repos

- [**dot314**](https://github.com/nicobailon/dot314): Separate cancellable loading from results
  - [extensions/session-ask/index.ts:1645-1665](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/session-ask/index.ts#L1645-L1665) @17cce138
  - [extensions/extension-stats.ts:1110-1152](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/extension-stats.ts#L1110-L1152) @17cce138

## For agents: choose and check

Choose Abort Lantern when your job matches its intent and applicability above. Its neighbours in Overlays and dialogs are listed below. Read the one whose intent fits your job more closely before you commit.

- [Shared Shell](https://pi-tui.ratstack.sh/patterns/shared-shell.md): Wrap specialized dialog content in a shared themed frame.
- [Warning Gate](https://pi-tui.ratstack.sh/patterns/warning-gate.md): Follow a consequential selection with a separate warning confirmation.
- [Dialog Fuse](https://pi-tui.ratstack.sh/patterns/dialog-fuse.md): Show remaining time before a transient dialog automatically dismisses.
- [Abort Tether](https://pi-tui.ratstack.sh/patterns/abort-tether.md): Tie a built-in dialog's lifetime to an operation's abort signal.
- [Idle Fuse](https://pi-tui.ratstack.sh/patterns/idle-fuse.md): Reset a component-owned inactivity timer after every input.
- [Settings Bench](https://pi-tui.ratstack.sh/patterns/settings-bench.md): Keep configuration interaction separate from the live activity view.
- [Action Sieve](https://pi-tui.ratstack.sh/patterns/action-sieve.md): Derive dialog choices from current control and completion state.
- [Done Contract](https://pi-tui.ratstack.sh/patterns/done-contract.md): Resolve each custom interaction with a typed selection or cancellation.

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.
- This pattern animates. Check every frame of the animation, not only the first.
- 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), [`BorderedLoader`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#compose-built-in-components), `Container`, `ctx.ui.setWidget` before you use them.
- Every reference frame gets the verdict its state expects.

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

- [Abort Tether](https://pi-tui.ratstack.sh/patterns/abort-tether.md): cancels a prompt instead of a loader

## Linked from

- [Abort Tether](https://pi-tui.ratstack.sh/patterns/abort-tether.md): owns cancellation during work
