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

Also known as Field focus.

## Intent

Switch keyboard focus between a choice list and an editable task field.

## Motivation

Intercom's handover picker combines target navigation with an optional editable task field.

## Applicability

- Use this when a picker combines a target and an optional text prompt.

## Structure

```text
Tab -> list <-> task Input
focused Input -> CURSOR_MARKER
```

## Participants

- [`Focusable`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus): Carries focused state to the cursor-owning component.
- [`CURSOR_MARKER`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus): Marks the hardware cursor position for IME placement.
- [`Input`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#compose-built-in-components): Owns single-line editing and cursor state.
- [`matchesKey`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus): Recognizes explicit terminal key combinations.
- `Focus selector`: Routes list keys and task typing to their respective owners.

Pi component APIs: [`Focusable`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), [`CURSOR_MARKER`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), [`Input`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#compose-built-in-components), [`matchesKey`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), `SelectList`

## Consequences

- List movement and task typing remain separate interactions.
- The wrapper must propagate child focus for correct cursor positioning.

## Implementation

- Forward focused state to the child text input for cursor positioning.
- Keep list navigation separate from task text editing.

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

### List owns keys

`list`: Show the target list highlighted and the task field inactive.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Field Baton, List owns keys, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/field-baton/list-40-dark.8bba9771077a.webp) ![Field Baton, List owns keys, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/field-baton/list-40-light.ffad1778be82.webp)
- 60 columns: ![Field Baton, List owns keys, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/field-baton/list-60-dark.774992368fef.webp) ![Field Baton, List owns keys, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/field-baton/list-60-light.162455280d65.webp)
- 80 columns: ![Field Baton, List owns keys, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/field-baton/list-80-dark.2e6f85fe166a.webp) ![Field Baton, List owns keys, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/field-baton/list-80-light.a3c1dace2895.webp)
- 120 columns: ![Field Baton, List owns keys, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/field-baton/list-120-dark.77b6a27fd626.webp) ![Field Baton, List owns keys, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/field-baton/list-120-light.a59255ed54f6.webp)

### Task owns keys

`task`: Switch with Tab and show the text cursor in the task field.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Field Baton, Task owns keys, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/field-baton/task-40-dark.2d9b09cb8387.webp) ![Field Baton, Task owns keys, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/field-baton/task-40-light.31c41f117fd0.webp)
- 60 columns: ![Field Baton, Task owns keys, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/field-baton/task-60-dark.62d6c9d94f68.webp) ![Field Baton, Task owns keys, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/field-baton/task-60-light.c40a525c0deb.webp)
- 80 columns: ![Field Baton, Task owns keys, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/field-baton/task-80-dark.0ebef77565e9.webp) ![Field Baton, Task owns keys, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/field-baton/task-80-light.1125dfbd0555.webp)
- 120 columns: ![Field Baton, Task owns keys, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/field-baton/task-120-dark.df73f6bfbe7a.webp) ![Field Baton, Task owns keys, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/field-baton/task-120-light.248b05e32119.webp)

### List focus returns

`returned`: Switch back without losing the typed draft.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Field Baton, List focus returns, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/field-baton/returned-40-dark.5215883727c5.webp) ![Field Baton, List focus returns, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/field-baton/returned-40-light.8862b2013bb4.webp)
- 60 columns: ![Field Baton, List focus returns, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/field-baton/returned-60-dark.5f39044d7de1.webp) ![Field Baton, List focus returns, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/field-baton/returned-60-light.70e8a01a38e4.webp)
- 80 columns: ![Field Baton, List focus returns, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/field-baton/returned-80-dark.c9194bccf81f.webp) ![Field Baton, List focus returns, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/field-baton/returned-80-light.7ebba70eff68.webp)
- 120 columns: ![Field Baton, List focus returns, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/field-baton/returned-120-dark.ec2be533d3a2.webp) ![Field Baton, List focus returns, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/field-baton/returned-120-light.6900fed19f2b.webp)

## Sample code

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

```ts
// Field Baton: pass focus between a target list and the draft-owning Input.
import { Input, SelectList, matchesKey, truncateToWidth, type Component, type Focusable } from "@earendil-works/pi-tui";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "field-baton",
  title: "Field Baton",
  kind: "component",
  apis: ["Focusable", "Input", "SelectList", "matchesKey", "CURSOR_MARKER"],
  rows: 10,
  states: [
    { id: "list", label: "List owns keys" },
    { id: "task", label: "Task owns keys", steps: [
      { type: "keys", data: "\t" }, { type: "text", text: "Review the parser tests" },
    ] },
    { id: "returned", label: "List focus returns", steps: [
      { type: "keys", data: "\t" }, { type: "keys", data: "\x1b[B" },
    ] },
  ],
  setup({ theme, tui }) {
    const task = new Input({ prompt: "> ", placeholder: "Optional handover task" });
    const targets = new SelectList([
      { value: "parser", label: "Parser session" },
      { value: "tests", label: "Test session" },
      { value: "review", label: "Review session" },
    ], 3, {
      selectedPrefix: text => theme.fg("accent", text),
      selectedText: text => theme.fg("accent", text),
      description: text => theme.fg("muted", text),
      scrollInfo: text => theme.fg("dim", text),
      noMatch: text => theme.fg("warning", text),
    });
    class FocusSelector implements Component, Focusable {
      private active: "list" | "task" = "list";
      private hasFocus = false;
      get focused() { return this.hasFocus; }
      set focused(value: boolean) {
        this.hasFocus = value;
        task.focused = value && this.active === "task";
      }
      invalidate() { targets.invalidate(); task.invalidate(); }
      handleInput(data: string) {
        if (matchesKey(data, "tab")) {
          this.active = this.active === "list" ? "task" : "list";
          this.focused = this.hasFocus;
        } else if (this.active === "task") task.handleInput(data);
        else targets.handleInput(data);
        tui.requestRender();
      }
      render(width: number) {
        return [
          theme.fg("accent", "Field Baton · handover"),
          theme.fg(this.active === "list" ? "success" : "muted", "Targets" + (this.active === "list" ? " · FOCUSED" : "")),
          ...targets.render(width),
          theme.fg(this.active === "task" ? "success" : "muted", "Task draft" + (this.active === "task" ? " · FOCUSED" : "")),
          ...task.render(width), // Input emits CURSOR_MARKER when focused.
          theme.fg("muted", truncateToWidth("Tab switch · arrows select · draft stays", width)),
        ];
      }
    }
    return new FocusSelector();
  },
};
```

## Known uses: seen in Nico's repos

- [**pi-intercom**](https://github.com/nicobailon/pi-intercom): Separate task-field focus and retain remote selection through async refresh
  - [ui/handover-picker.ts:61-180](https://github.com/nicobailon/pi-intercom/blob/a5fad4df2a9fe4909bf4d9b06263c8316976b57d/ui/handover-picker.ts#L61-L180) @a5fad4df

## For agents: choose and check

Choose Field 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.
- [Control Baton](https://pi-tui.ratstack.sh/patterns/control-baton.md): Pause automatic output updates while the user controls an interactive job.
- [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.
- Read the Pi 1.0.3 docs for [`Focusable`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), [`CURSOR_MARKER`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), [`Input`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#compose-built-in-components), [`matchesKey`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), `SelectList` before you use them.
- Every reference frame gets the verdict its state expects.

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

- [Identity Anchor](https://pi-tui.ratstack.sh/patterns/identity-anchor.md): keeps target identity through refresh

## Linked from

- [Identity Anchor](https://pi-tui.ratstack.sh/patterns/identity-anchor.md): separates target and task input
