# Control Baton

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

Also known as Human takeover.

## Intent

Pause automatic output updates while the user controls an interactive job.

## Motivation

Interactive Shell must stop automatic output updates when a human takes over its live terminal.

## Applicability

- Use this when long-running terminal work alternates between agent and human control.

## Structure

```text
agent updates -> flush
flush -> human control
return -> agent updates
```

## 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.
- [`TUI.requestRender`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model): Requests a coalesced redraw after state changes.
- `Control state`: Separates automatic updates from human terminal control.

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

## Consequences

- Agent and human control can alternate without closing the view.
- Takeover must flush pending output and respect update budgets.

## Implementation

- Flush pending output before takeover.
- Bound per-update and total output budgets.

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

### Automatic updates

`automatic`: Show a synthetic job with bounded agent progress updates.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Control Baton, Automatic updates, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/control-baton/automatic-40-dark.ce381821bfe8.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/automatic-40-dark.anim.a720783e5016.webp) ![Control Baton, Automatic updates, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/control-baton/automatic-40-light.78b74e21c901.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/automatic-40-light.anim.25a3a5c5a619.webp)
- 60 columns: ![Control Baton, Automatic updates, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/control-baton/automatic-60-dark.3064098fffce.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/automatic-60-dark.anim.278aea753a84.webp) ![Control Baton, Automatic updates, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/control-baton/automatic-60-light.2c33bdad37f6.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/automatic-60-light.anim.251129869752.webp)
- 80 columns: ![Control Baton, Automatic updates, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/control-baton/automatic-80-dark.42c048d94fb2.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/automatic-80-dark.anim.2863f18ffcd2.webp) ![Control Baton, Automatic updates, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/control-baton/automatic-80-light.8231551afcc7.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/automatic-80-light.anim.5593ba75e009.webp)
- 120 columns: ![Control Baton, Automatic updates, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/control-baton/automatic-120-dark.eb44e6e47483.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/automatic-120-dark.anim.03c4b92ae9de.webp) ![Control Baton, Automatic updates, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/control-baton/automatic-120-light.325aa09b85c2.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/automatic-120-light.anim.519759d19cdb.webp)

### Human control

`takeover`: Flush pending output and show that automatic updates have stopped.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Control Baton, Human control, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/control-baton/takeover-40-dark.77fbc071ca21.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/takeover-40-dark.anim.89a7a6e5f450.webp) ![Control Baton, Human control, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/control-baton/takeover-40-light.466e42254254.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/takeover-40-light.anim.ebbccf4b45e0.webp)
- 60 columns: ![Control Baton, Human control, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/control-baton/takeover-60-dark.be43e7a688bd.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/takeover-60-dark.anim.fe2d11873dba.webp) ![Control Baton, Human control, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/control-baton/takeover-60-light.1cab1c3dd338.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/takeover-60-light.anim.1c5532cf1ceb.webp)
- 80 columns: ![Control Baton, Human control, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/control-baton/takeover-80-dark.c965fb9e7839.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/takeover-80-dark.anim.2dc8669bb66e.webp) ![Control Baton, Human control, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/control-baton/takeover-80-light.2d6260cbe18d.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/takeover-80-light.anim.bfa48aa18714.webp)
- 120 columns: ![Control Baton, Human control, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/control-baton/takeover-120-dark.15ce09a0929a.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/takeover-120-dark.anim.201d7a934be6.webp) ![Control Baton, Human control, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/control-baton/takeover-120-light.05b9ca18dfa6.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/takeover-120-light.anim.4369c548a019.webp)

### Agent control

`returned`: Return control and resume bounded automatic updates.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Control Baton, Agent control, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/control-baton/returned-40-dark.47412eabb1e0.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/returned-40-dark.anim.4a6a60656e8f.webp) ![Control Baton, Agent control, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/control-baton/returned-40-light.ff72be03adb4.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/returned-40-light.anim.38a09ed05d57.webp)
- 60 columns: ![Control Baton, Agent control, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/control-baton/returned-60-dark.962e7606beb2.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/returned-60-dark.anim.19b84d938905.webp) ![Control Baton, Agent control, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/control-baton/returned-60-light.461a3340f314.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/returned-60-light.anim.770700ec3322.webp)
- 80 columns: ![Control Baton, Agent control, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/control-baton/returned-80-dark.05bc7938aa5a.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/returned-80-dark.anim.08a6cade3789.webp) ![Control Baton, Agent control, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/control-baton/returned-80-light.ca96f7e077c6.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/returned-80-light.anim.44e3a81be1fd.webp)
- 120 columns: ![Control Baton, Agent control, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/control-baton/returned-120-dark.74c211742a05.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/returned-120-dark.anim.ce7b41d1b8f5.webp) ![Control Baton, Agent control, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/control-baton/returned-120-light.a6b4e9bce85a.webp) [animated](https://pi-tui.ratstack.sh/frames/control-baton/returned-120-light.anim.db1d2c28a81c.webp)

## Sample code

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

```ts
// Control Baton: flush pending output before handing a synthetic job to a human.
// Machine: automatic --takeover/flush--> human --return--> automatic.
import { Box, Text, matchesKey, type Component } from "@earendil-works/pi-tui";
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "control-baton",
  title: "Control Baton",
  kind: "screen",
  apis: ["custom", "Component", "TUI.requestRender", "Box", "DynamicBorder", "matchesKey"],
  states: [
    { id: "automatic", label: "Automatic updates", animated: true, steps: [{ type: "tick", ms: 700 }] },
    { id: "takeover", label: "Human control", animated: true, steps: [
      { type: "keys", data: "h" }, { type: "tick", ms: 500 }, { type: "keys", data: "n" },
    ] },
    { id: "returned", label: "Agent control", animated: true, steps: [
      { type: "keys", data: "a" }, { type: "tick", ms: 700 },
    ] },
  ],
  setup({ ui, theme }) {
    ui.setHeader(() => new Text(theme.fg("accent", "Pi · Control Baton"), 0, 0));
    ui.setEditorText("Explain the parser test results");
    ui.setWidget("job-context", [theme.fg("muted", "Job: token boundary test suite")]);
    void ui.custom((tui, panelTheme, _keys, done) => {
      let owner: "automatic" | "human" = "automatic";
      let produced = 0;
      let pending: string[] = [];
      let visible: string[] = [];
      let lastHandoff = "No handoff yet";
      const panel = new Box(1, 0, line => panelTheme.bg("customMessageBg", line));
      const content = new Text("", 0, 0);
      panel.addChild(new DynamicBorder(line => panelTheme.fg("borderAccent", line)));
      panel.addChild(content);
      panel.addChild(new DynamicBorder(line => panelTheme.fg("borderAccent", line)));
      function flush() {
        // At most two lines per flush and three lines retained in the view.
        visible = [...visible, ...pending.splice(0, 2)].slice(-3);
      }
      const timer = setInterval(() => {
        if (owner !== "automatic") return;
        pending.push("check " + (++produced) + " · token case passed");
        if (pending.length >= 2) flush();
        tui.requestRender();
      }, 200);
      const job: Component & { dispose(): void } = {
        invalidate() { panel.invalidate(); },
        dispose() { clearInterval(timer); },
        handleInput(data) {
          if (matchesKey(data, "h") && owner === "automatic") {
            const count = pending.length;
            flush();
            owner = "human";
            lastHandoff = "Flushed " + count + " pending line";
          } else if (matchesKey(data, "a")) owner = "automatic";
          else if (matchesKey(data, "n") && owner === "human") {
            visible = [...visible, "human > inspect next case"].slice(-3);
          } else if (matchesKey(data, "escape")) done(undefined);
          tui.requestRender();
        },
        render(width) {
          content.setText([
            panelTheme.fg("accent", "Token tests · " + (owner === "human" ? "HUMAN" : "AGENT")),
            panelTheme.fg(owner === "human" ? "warning" : "success",
              owner === "human" ? "Auto updates PAUSED" : "Auto updates ACTIVE"),
            ...visible.map(line => panelTheme.fg("toolOutput", line)),
            panelTheme.fg("muted", "Produced " + produced + " · pending " + pending.length),
            panelTheme.fg("dim", lastHandoff),
            panelTheme.fg("muted", owner === "human" ? "n inspect · a return · Esc close" : "h take over · Esc close"),
          ].join("\n"));
          return panel.render(width);
        },
      };
      return job;
    }, { overlay: true, overlayOptions: { width: "85%", anchor: "top-center", row: 1, margin: 1 } });
    return () => ui.setWidget("job-context", undefined);
  },
};
```

## Known uses: seen in Nico's repos

- [**pi-interactive-shell**](https://github.com/nicobailon/pi-interactive-shell): Separate hands-free updates from user takeover
  - [overlay-component.ts:300-445](https://github.com/nicobailon/pi-interactive-shell/blob/77df9a8142a2f731635a4c5a01d68feecb5cced4/overlay-component.ts#L300-L445) @77df9a81

## For agents: choose and check

Choose Control Baton when your job matches its intent and applicability above. Its neighbours in Keys and focus are listed below. Read the one whose intent fits your job more closely before you commit.

- [Action Compass](https://pi-tui.ratstack.sh/patterns/action-compass.md): Resolve configurable actions through injected keybindings.
- [Input Switch](https://pi-tui.ratstack.sh/patterns/input-switch.md): Continue, transform or handle submitted input before normal processing.
- [Focus Baton](https://pi-tui.ratstack.sh/patterns/focus-baton.md): Move keyboard ownership without closing a persistent overlay.
- [Ghost Overlay](https://pi-tui.ratstack.sh/patterns/ghost-overlay.md): Keep an overlay visible without automatically taking keyboard focus.
- [Field Baton](https://pi-tui.ratstack.sh/patterns/field-baton.md): Switch keyboard focus between a choice list and an editable task field.
- [Pause Latch](https://pi-tui.ratstack.sh/patterns/pause-latch.md): Keep pause, resume and quit controls inside the component input contract.
- [Input Lease](https://pi-tui.ratstack.sh/patterns/input-lease.md): Intercept raw terminal controls only while their owning operation is active.

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

Next actions: `related({ id: "control-baton" })` lists what to read next, and `states({ id: "control-baton", 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 Sieve](https://pi-tui.ratstack.sh/patterns/action-sieve.md): offers only valid handoff actions

## Linked from

- [Action Sieve](https://pi-tui.ratstack.sh/patterns/action-sieve.md): changes control ownership
