# Editor Steward

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

Also known as Single-owner editor.

## Intent

Compose editor enhancements behind one replacement factory.

## Motivation

Dot314's editor enhancements warn that competing setEditorComponent replacements cannot safely own the same editor.

## Applicability

- Use this when several features would otherwise compete to replace the main editor.

## Structure

```text
one factory -> CustomEditor
features -> shared input / render
```

## Participants

- [`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.
- `Composed features`: Share one editor factory rather than replacing each other.

Pi screen APIs: [`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), `Editor.setAutocompleteProvider`, `matchesKey`

## Consequences

- Several enhancements can preserve one base editor contract.
- They must cooperate under a single factory rather than install independently.

## Implementation

- Receive EditorTheme rather than the general Theme.
- Forward unowned keys to CustomEditor.
- Preserve autocomplete and restore the default factory when done.

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

### Base editor

`default`: Show an ordinary synthetic draft with autocomplete.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Editor Steward, Base editor, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/editor-steward/default-40-dark.253f512b99c8.webp) ![Editor Steward, Base editor, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/editor-steward/default-40-light.f2540543b6c2.webp)
- 60 columns: ![Editor Steward, Base editor, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/editor-steward/default-60-dark.1a9ef0c81333.webp) ![Editor Steward, Base editor, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/editor-steward/default-60-light.3804e4473ed3.webp)
- 80 columns: ![Editor Steward, Base editor, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/editor-steward/default-80-dark.0adfa7ce4309.webp) ![Editor Steward, Base editor, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/editor-steward/default-80-light.72a89c915e9a.webp)
- 120 columns: ![Editor Steward, Base editor, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/editor-steward/default-120-dark.9192cce8fd12.webp) ![Editor Steward, Base editor, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/editor-steward/default-120-light.a802814ff7fb.webp)

### Composed enhancements

`enhanced`: Show one owned editor with extra chrome and an explicit mode.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Editor Steward, Composed enhancements, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/editor-steward/enhanced-40-dark.398b80b3d947.webp) ![Editor Steward, Composed enhancements, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/editor-steward/enhanced-40-light.a06d159f456c.webp)
- 60 columns: ![Editor Steward, Composed enhancements, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/editor-steward/enhanced-60-dark.44052e8c9ced.webp) ![Editor Steward, Composed enhancements, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/editor-steward/enhanced-60-light.4f5523186b9c.webp)
- 80 columns: ![Editor Steward, Composed enhancements, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/editor-steward/enhanced-80-dark.fed8430bd277.webp) ![Editor Steward, Composed enhancements, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/editor-steward/enhanced-80-light.859e4ffdc518.webp)
- 120 columns: ![Editor Steward, Composed enhancements, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/editor-steward/enhanced-120-dark.e9377bdc3403.webp) ![Editor Steward, Composed enhancements, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/editor-steward/enhanced-120-light.2f842d74ab6a.webp)

### Base controls retained

`forwarded`: Show an unowned key still using the base editor behavior.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Editor Steward, Base controls retained, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/editor-steward/forwarded-40-dark.0faed973d987.webp) ![Editor Steward, Base controls retained, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/editor-steward/forwarded-40-light.493af35a827e.webp)
- 60 columns: ![Editor Steward, Base controls retained, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/editor-steward/forwarded-60-dark.bd91c1801bb3.webp) ![Editor Steward, Base controls retained, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/editor-steward/forwarded-60-light.fff559b35334.webp)
- 80 columns: ![Editor Steward, Base controls retained, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/editor-steward/forwarded-80-dark.22898d981759.webp) ![Editor Steward, Base controls retained, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/editor-steward/forwarded-80-light.e2a934cd0e9f.webp)
- 120 columns: ![Editor Steward, Base controls retained, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/editor-steward/forwarded-120-dark.b8fb0a93d4c3.webp) ![Editor Steward, Base controls retained, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/editor-steward/forwarded-120-light.50f617b03f08.webp)

## Sample code

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

```ts
// Editor Steward: one CustomEditor factory composes chrome, mode and base controls.
import { CustomEditor } from "@earendil-works/pi-coding-agent";
import { matchesKey, truncateToWidth, type AutocompleteProvider } from "@earendil-works/pi-tui";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "editor-steward", title: "Editor Steward", kind: "screen",
  apis: ["ctx.ui.setEditorComponent", "CustomEditor", "Editor.setAutocompleteProvider", "matchesKey"],
  states: [
    { id: "default", label: "Base editor with autocomplete", steps: [
      { type: "text", text: "/rev" }, { type: "keys", data: "\t" }, { type: "wait", ms: 300 },
    ] },
    { id: "enhanced", label: "Composed enhancements", steps: [{ type: "action", name: "enhance" }] },
    { id: "forwarded", label: "Base controls retained", steps: [
      { type: "keys", data: "\x01" }, { type: "text", text: "Also " },
      { type: "action", name: "assert-forwarded" },
    ] },
  ],
  setup({ ui, theme, screen, action }) {
    // Synthetic autocomplete driver: no command execution or file completion.
    const autocomplete: AutocompleteProvider = {
      async getSuggestions(lines, cursorLine, cursorCol, { signal }) {
        const prefix = lines[cursorLine]!.slice(0, cursorCol);
        if (signal.aborted || !prefix.startsWith("/")) return null;
        const items = [
          { value: "/review", label: "/review", description: "Review sample code" },
          { value: "/revise", label: "/revise", description: "Revise the draft" },
        ].filter(item => item.value.startsWith(prefix));
        return items.length ? { items, prefix } : null;
      },
      applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
        const next = [...lines];
        const start = cursorCol - prefix.length;
        next[cursorLine] = lines[cursorLine]!.slice(0, start) + item.value + lines[cursorLine]!.slice(cursorCol);
        return { lines: next, cursorLine, cursorCol: start + item.value.length };
      },
    };
    if (!screen.editor.setAutocompleteProvider) throw new Error("Base editor has no autocomplete seam");
    screen.editor.setAutocompleteProvider(autocomplete);
    ui.setEditorText("");
    action("enhance", () => {
      ui.setEditorComponent((tui, editorTheme, keybindings) => {
        class StewardEditor extends CustomEditor {
          private mode: "draft" | "review" = "review";
          handleInput(data: string) {
            if (matchesKey(data, "f2")) {
              this.mode = this.mode === "draft" ? "review" : "draft";
              tui.requestRender(); return;
            }
            super.handleInput(data); // Editing, autocomplete and app actions remain Pi's.
          }
          render(width: number) {
            const words = this.getText().trim().split(/\s+/).filter(Boolean).length;
            const chrome = this.mode.toUpperCase() + " · " + words + " words · F2 mode";
            return [theme.fg("accent", truncateToWidth(chrome, width)), ...super.render(width)];
          }
        }
        const editor = new StewardEditor(tui, editorTheme, keybindings);
        editor.setAutocompleteProvider(autocomplete);
        return editor;
      });
      ui.setEditorText("Review src/tasks.ts");
    });
    action("assert-forwarded", () => {
      if (ui.getEditorText() !== "Also Review src/tasks.ts") throw new Error("Base cursor-line-start was not retained");
    });
    return () => ui.setEditorComponent(undefined);
  },
};
```

## Known uses: seen in Nico's repos

- [**pi-extensions**](https://github.com/nicobailon/pi-extensions): Session-scoped editor component mount
  - [raw-paste/index.ts:95-104](https://github.com/nicobailon/pi-extensions/blob/bca5070b541ffa7d273e74036dcd7d5f8b63eed3/raw-paste/index.ts#L95-L104) @bca5070b
- [**pi-powerline-footer**](https://github.com/nicobailon/pi-powerline-footer): Editor replacement and chrome
  - [index.ts:3402-3425](https://github.com/nicobailon/pi-powerline-footer/blob/859dee671b633fb533b07ceba3e6c1ab1c43360a/index.ts#L3402-L3425) @859dee67
- [**dot314**](https://github.com/nicobailon/dot314): Compose editor enhancements behind one editor-component owner
  - [extensions/editor-enhancements/index.ts:5-11](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/editor-enhancements/index.ts#L5-L11) @17cce138
  - [extensions/editor-enhancements/index.ts:43-65](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/editor-enhancements/index.ts#L43-L65) @17cce138

## For agents: choose and check

Choose Editor Steward 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.

- [Retry Buffer](https://pi-tui.ratstack.sh/patterns/retry-buffer.md): Keep the local draft editable after a failed send.
- [Draft Fence](https://pi-tui.ratstack.sh/patterns/draft-fence.md): Keep inline draft edits separate from the committed queue snapshot.
- [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.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), `Editor.setAutocompleteProvider`, `matchesKey` before you use them.
- Every reference frame gets the verdict its state expects.

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

- [Action Compass](https://pi-tui.ratstack.sh/patterns/action-compass.md): keeps base application controls

## Linked from

- [Prompt Trail](https://pi-tui.ratstack.sh/patterns/prompt-trail.md): preserves the base editor history
- [Input Switch](https://pi-tui.ratstack.sh/patterns/input-switch.md): changes the editor rather than submission
