# Draft Fence

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

Also known as Staged draft.

## Intent

Keep inline draft edits separate from the committed queue snapshot.

## Motivation

Dot314's queue-steer timeline must show committed queue data while inline edits remain drafts.

## Applicability

- Use this when a timeline includes editable work that has not been committed.

## Structure

```text
queue snapshot -> timeline
draft -> editor
commit -> new snapshot
```

## Participants

- [`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.
- [`ctx.ui.setEditorComponent`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user): Installs the single custom editor factory.
- [`CustomEditor`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus): Preserves application controls for editor replacements.
- `Queue snapshot`: Remains committed state while local drafts are edited.

Pi screen APIs: [`ctx.ui.setWidget`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user), [`ctx.ui.setEditorComponent`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user), [`CustomEditor`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), `setWidget`, `setEditorComponent`, `matchesKey`

## Consequences

- Uncommitted edits do not impersonate persisted queue values.
- Draft state and queue state require separate commit and cleanup paths.

## Implementation

- Do not render an unsaved draft as persisted queue state.
- Clear the queue widget when its queue becomes empty.

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

### Queue snapshot

`committed`: Show a synthetic queued item in the timeline.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Draft Fence, Queue snapshot, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-40-dark.521625eb4835.webp) ![Draft Fence, Queue snapshot, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-40-light.afca8b5d9e2d.webp)
- 60 columns: ![Draft Fence, Queue snapshot, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-60-dark.0dc60c3f880c.webp) ![Draft Fence, Queue snapshot, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-60-light.608ea73dc861.webp)
- 80 columns: ![Draft Fence, Queue snapshot, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-80-dark.7c712de14158.webp) ![Draft Fence, Queue snapshot, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-80-light.f6d55049a6e5.webp)
- 120 columns: ![Draft Fence, Queue snapshot, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-120-dark.a242f6429692.webp) ![Draft Fence, Queue snapshot, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-120-light.ded7cb8c6c87.webp)

### Uncommitted edit

`draft`: Edit its draft while the timeline still shows the committed value.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Draft Fence, Uncommitted edit, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/draft-fence/draft-40-dark.3d1d84693e6e.webp) ![Draft Fence, Uncommitted edit, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/draft-fence/draft-40-light.d3f24d84a61c.webp)
- 60 columns: ![Draft Fence, Uncommitted edit, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/draft-fence/draft-60-dark.9b3939ebbe66.webp) ![Draft Fence, Uncommitted edit, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/draft-fence/draft-60-light.46ca702eb7bc.webp)
- 80 columns: ![Draft Fence, Uncommitted edit, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/draft-fence/draft-80-dark.d6f5fe7f7f8e.webp) ![Draft Fence, Uncommitted edit, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/draft-fence/draft-80-light.a7dd5e5bfd7b.webp)
- 120 columns: ![Draft Fence, Uncommitted edit, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/draft-fence/draft-120-dark.a3f03a789751.webp) ![Draft Fence, Uncommitted edit, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/draft-fence/draft-120-light.e3e61736d34c.webp)

### Edit committed

`committed-edit`: Apply the draft and update the timeline snapshot.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Draft Fence, Edit committed, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-edit-40-dark.e2f1be786883.webp) ![Draft Fence, Edit committed, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-edit-40-light.88e06e9ccfb3.webp)
- 60 columns: ![Draft Fence, Edit committed, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-edit-60-dark.b8c0c467aab3.webp) ![Draft Fence, Edit committed, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-edit-60-light.7bee798a68b7.webp)
- 80 columns: ![Draft Fence, Edit committed, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-edit-80-dark.8aee8ad8091c.webp) ![Draft Fence, Edit committed, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-edit-80-light.1dab85b9503d.webp)
- 120 columns: ![Draft Fence, Edit committed, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-edit-120-dark.67303d2d6b29.webp) ![Draft Fence, Edit committed, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/draft-fence/committed-edit-120-light.ae7f17bceb3f.webp)

## Sample code

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

```ts
// Draft Fence: only an explicit commit moves editor text into the queue snapshot.
import { Text, matchesKey } from "@earendil-works/pi-tui";
import { CustomEditor } from "@earendil-works/pi-coding-agent";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "draft-fence",
  title: "Draft Fence",
  kind: "screen",
  apis: ["setWidget", "setEditorComponent", "CustomEditor", "matchesKey"],
  states: [
    { id: "committed", label: "Queue snapshot" },
    { id: "draft", label: "Uncommitted edit", steps: [
      { type: "keys", data: "\x15" }, { type: "text", text: "Review parser edge cases" },
    ] },
    { id: "committed-edit", label: "Edit committed", steps: [{ type: "keys", data: "\r" }] },
  ],
  setup({ ui, theme }) {
    let queueSnapshot = "Review parser tests";
    ui.setHeader(() => new Text(theme.fg("accent", "Pi · Draft Fence"), 0, 0));
    function publishSnapshot() {
      ui.setWidget("queue", queueSnapshot ? [
        theme.fg("accent", "Queue · COMMITTED"),
        theme.fg("text", "1 → " + queueSnapshot),
      ] : undefined);
    }
    publishSnapshot();
    ui.setEditorComponent((tui, editorTheme, keys) => {
      class StagedEditor extends CustomEditor {
        handleInput(data: string) {
          if (matchesKey(data, "escape")) {
            this.setText(queueSnapshot);
            ui.setStatus("draft", theme.fg("muted", "edit discarded"));
          } else super.handleInput(data); // Keep Pi's normal app controls.
          tui.requestRender();
        }
      }
      const editor = new StagedEditor(tui, editorTheme, keys);
      editor.onChange = draft => ui.setStatus("draft",
        theme.fg(draft === queueSnapshot ? "success" : "warning", draft === queueSnapshot ? "matches queue" : "not committed"));
      editor.onSubmit = draft => {
        queueSnapshot = draft;
        publishSnapshot();
        editor.setText(queueSnapshot);
        ui.setStatus("draft", theme.fg("success", "committed"));
      };
      return editor;
    });
    ui.setEditorText(queueSnapshot);
    ui.setWidget("draft-hint", [
      theme.fg("muted", "Editor = local draft · queue = saved"),
      theme.fg("dim", "Enter commit · Esc discard edit"),
    ], { placement: "belowEditor" });
    return () => {
      ui.setStatus("draft", undefined);
      ui.setWidget("queue", undefined);
      ui.setWidget("draft-hint", undefined);
      ui.setEditorComponent(undefined);
    };
  },
};
```

## Known uses: seen in Nico's repos

- [**dot314**](https://github.com/nicobailon/dot314): Compose queue drafts and timeline widgets
  - [extensions/pi-queue-steer/index.ts:268-304](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/pi-queue-steer/index.ts#L268-L304) @17cce138
  - [extensions/pi-queue-steer/index.ts:525-545](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/pi-queue-steer/index.ts#L525-L545) @17cce138

## For agents: choose and check

Choose Draft Fence when your job matches its intent and applicability above. Its neighbours in Editors and drafts are listed below. Read the one whose intent fits your job more closely before you commit.

- [Editor Steward](https://pi-tui.ratstack.sh/patterns/editor-steward.md): Compose editor enhancements behind one replacement factory.
- [Retry Buffer](https://pi-tui.ratstack.sh/patterns/retry-buffer.md): Keep the local draft editable after a failed send.
- [Prompt Trail](https://pi-tui.ratstack.sh/patterns/prompt-trail.md): Browse submitted prompts while retaining the current editor draft.
- [Draft Return](https://pi-tui.ratstack.sh/patterns/draft-return.md): Restore the captured editor draft after a temporary submission.

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

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

- [Retry Buffer](https://pi-tui.ratstack.sh/patterns/retry-buffer.md): keeps unsubmitted text local

## Linked from

- [Draft Return](https://pi-tui.ratstack.sh/patterns/draft-return.md): separates draft from committed state
