# Done Contract

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

Lifecycle / Overlays and dialogs · `done-contract` · [HTML](https://pi-tui.ratstack.sh/patterns/done-contract/) · [JSON](https://pi-tui.ratstack.sh/patterns/done-contract.json) · [all patterns](https://pi-tui.ratstack.sh/patterns.md)

Also known as Custom completion.

## Intent

Resolve each custom interaction with a typed selection or cancellation.

## Motivation

Nico's custom selectors await a result until their component calls done.

## Applicability

- Use this when built-in dialogs cannot express the required view.

## Structure

```text
ctx.ui.custom -> component
component -> done(result)
done -> dispose + return
```

## 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.
- [`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.
- `Completion callback`: Returns one typed selection or cancellation to the caller.

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

## Consequences

- Selection and cancellation settle one typed interaction.
- A missing or duplicate completion can leave the caller pending or settle incorrectly.

## Implementation

- Call done on every exit path.
- Do not permanently hide a custom-owned overlay instead of resolving it.
- Make completion idempotent.

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

### Focused view

`open`: Show a small read-only inspection list with selection and cancel hints.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Done Contract, Focused view, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/done-contract/open-40-dark.6fde93176556.webp) ![Done Contract, Focused view, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/done-contract/open-40-light.3520e1676c47.webp)
- 60 columns: ![Done Contract, Focused view, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/done-contract/open-60-dark.f01a3eac2e03.webp) ![Done Contract, Focused view, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/done-contract/open-60-light.b633250fe4a3.webp)
- 80 columns: ![Done Contract, Focused view, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/done-contract/open-80-dark.583049872d78.webp) ![Done Contract, Focused view, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/done-contract/open-80-light.38bc857d7e08.webp)
- 120 columns: ![Done Contract, Focused view, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/done-contract/open-120-dark.ad93b9dacd75.webp) ![Done Contract, Focused view, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/done-contract/open-120-light.f8e34e4b35cc.webp)

### Selection result

`selected`: Close the view and show its typed result in the host.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Done Contract, Selection result, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/done-contract/selected-40-dark.5f76cc6f8554.webp) ![Done Contract, Selection result, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/done-contract/selected-40-light.dd8a51715254.webp)
- 60 columns: ![Done Contract, Selection result, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/done-contract/selected-60-dark.0c5b76c09095.webp) ![Done Contract, Selection result, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/done-contract/selected-60-light.3b38a23c1950.webp)
- 80 columns: ![Done Contract, Selection result, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/done-contract/selected-80-dark.e7b2b02037e1.webp) ![Done Contract, Selection result, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/done-contract/selected-80-light.d2db00a0fa22.webp)
- 120 columns: ![Done Contract, Selection result, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/done-contract/selected-120-dark.76e4246c6338.webp) ![Done Contract, Selection result, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/done-contract/selected-120-light.777119e2f6f2.webp)

### Cancelled result

`cancelled`: Close through cancellation and show that no action was selected.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Done Contract, Cancelled result, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/done-contract/cancelled-40-dark.f337fa64be06.webp) ![Done Contract, Cancelled result, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/done-contract/cancelled-40-light.c0b87977df1c.webp)
- 60 columns: ![Done Contract, Cancelled result, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/done-contract/cancelled-60-dark.f0fcdff7c274.webp) ![Done Contract, Cancelled result, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/done-contract/cancelled-60-light.42c7712a8652.webp)
- 80 columns: ![Done Contract, Cancelled result, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/done-contract/cancelled-80-dark.213c4afa5cb9.webp) ![Done Contract, Cancelled result, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/done-contract/cancelled-80-light.c1d4cca5ea30.webp)
- 120 columns: ![Done Contract, Cancelled result, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/done-contract/cancelled-120-dark.8b8b4ecd4289.webp) ![Done Contract, Cancelled result, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/done-contract/cancelled-120-light.c124f742c23d.webp)

## Sample code

`stories/patterns/lifecycle/done-contract.ts`, the story the frames above were rendered from.

```ts
// Done Contract: settle each custom interaction with selection or cancellation.
import { ExtensionSelectorComponent } from "@earendil-works/pi-coding-agent";
import type { PatternStory } from "../../../src/pattern.ts";

type InspectionResult = { kind: "selected"; file: string } | { kind: "cancelled" };

export const story: PatternStory = {
  id: "done-contract", title: "Done Contract", kind: "screen",
  apis: ["custom", "ExtensionSelectorComponent"],
  states: [
    { id: "open", label: "Focused view" },
    { id: "selected", label: "Selection result", steps: [{ type: "keys", data: "\r" }] },
    { id: "cancelled", label: "Cancelled result", steps: [
      { type: "action", name: "inspect" }, { type: "keys", data: "\x1b" },
    ] },
  ],
  setup({ ui, theme, action }) {
    ui.setEditorText("Explain the selected change.");
    const inspect = () => {
      ui.setWidget("result", [theme.fg("muted", "Inspection pending · caller awaits done")]);
      void ui.custom<InspectionResult>((tui, _theme, _keys, done) => {
        // Lifecycle: open -> settled. Every exit uses the same guarded callback.
        let settled = false;
        const complete = (result: InspectionResult) => {
          if (settled) return;
          settled = true;
          done(result);
        };
        return new ExtensionSelectorComponent("Inspect changed file", ["src/parser.ts", "test/parser.test.ts"],
          file => complete({ kind: "selected", file }),
          () => complete({ kind: "cancelled" }), { tui });
      }).then(result => {
        ui.setWidget("result", [result.kind === "selected"
          ? theme.fg("success", "Selected: " + result.file)
          : theme.fg("warning", "Cancelled · no file selected")]);
      });
    };
    action("inspect", inspect);
    inspect();
  },
};
```

## Known uses: seen in Nico's repos

- [**dot314**](https://github.com/nicobailon/dot314): Resolve custom selectors through done
  - [extensions/session-switch/picker.ts:439-470](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/session-switch/picker.ts#L439-L470) @17cce138
  - [extensions/tool-horizon/boundary-picker.ts:470-500](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/tool-horizon/boundary-picker.ts#L470-L500) @17cce138
  - [extensions/screenshots-picker/index.ts:807-830](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/screenshots-picker/index.ts#L807-L830) @17cce138
  - [extensions/code-actions/ui.ts:25-75](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/code-actions/ui.ts#L25-L75) @17cce138
  - [extensions/sandbox/index.ts:340-370](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/sandbox/index.ts#L340-L370) @17cce138
  - [extensions/tools/index.ts:260-290](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/tools/index.ts#L260-L290) @17cce138
- [**pi-extensions**](https://github.com/nicobailon/pi-extensions): Resolve custom selectors through done
  - [code-actions/ui.ts:34-96](https://github.com/nicobailon/pi-extensions/blob/bca5070b541ffa7d273e74036dcd7d5f8b63eed3/code-actions/ui.ts#L34-L96) @bca5070b
- [**pi-mcp-adapter**](https://github.com/nicobailon/pi-mcp-adapter): Resolve custom selectors through done
- [**pi-powerline-footer**](https://github.com/nicobailon/pi-powerline-footer): Resolve custom selectors through done
  - [quote-reply.ts:241-267](https://github.com/nicobailon/pi-powerline-footer/blob/859dee671b633fb533b07ceba3e6c1ab1c43360a/quote-reply.ts#L241-L267) @859dee67
- [**dot314**](https://github.com/nicobailon/dot314): Open a custom inspection view for touched files
  - [extensions/files-touched.ts:55-90](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/files-touched.ts#L55-L90) @17cce138
- [**dot314**](https://github.com/nicobailon/dot314): Upstream-derived preset picker and status
  - [extensions/preset.ts:1-18](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/preset.ts#L1-L18) @17cce138

## For agents: choose and check

Choose Done Contract 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.
- [Abort Lantern](https://pi-tui.ratstack.sh/patterns/abort-lantern.md): Settle foreground loading before opening a separate result view.
- [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.

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

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

- [Focus Baton](https://pi-tui.ratstack.sh/patterns/focus-baton.md): moves focus without completion

## Linked from

- [Mode Fence](https://pi-tui.ratstack.sh/patterns/mode-fence.md): settles terminal-only custom interactions
