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

Also known as Overlay focus handoff.

## Intent

Move keyboard ownership without closing a persistent overlay.

## Motivation

Side Chat keeps a secondary overlay mounted while its shortcut changes keyboard ownership.

## Applicability

- Use this when a secondary view and the host editor need alternating input.

## Structure

```text
visible overlay -> focus()
editor <- unfocus(target)
interaction end -> done
```

## 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.
- [`OverlayHandle.focus`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays): Gives the overlay keyboard ownership.
- [`OverlayHandle.unfocus`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays): Releases input to fallback or a specified target.
- [`OverlayHandle.isFocused`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays): Reports the overlay's current input ownership.
- [`OverlayHandle.setHidden`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays): Temporarily changes visibility without completion.
- `Focus target`: Receives keyboard input while the overlay remains mounted.

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), [`OverlayHandle.focus`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays), [`OverlayHandle.unfocus`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays), [`OverlayHandle.isFocused`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays), [`OverlayHandle.setHidden`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays), `custom`, `Input`, `Focusable`, `Box`, `DynamicBorder`

## Consequences

- The secondary view stays visible during focus handoff.
- Visibility and input ownership need separate handle operations.

## Implementation

- Visibility does not imply keyboard ownership.
- Finish custom UI through done rather than permanent handle removal.

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

### Overlay focused

`overlay`: Show synthetic secondary content with its focus indicator.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Focus Baton, Overlay focused, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/overlay-40-dark.2a32a3a6d9d7.webp) ![Focus Baton, Overlay focused, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/overlay-40-light.3d06723a5437.webp)
- 60 columns: ![Focus Baton, Overlay focused, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/overlay-60-dark.74b974847ce8.webp) ![Focus Baton, Overlay focused, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/overlay-60-light.15b5cae615b7.webp)
- 80 columns: ![Focus Baton, Overlay focused, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/overlay-80-dark.fe0a15b25696.webp) ![Focus Baton, Overlay focused, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/overlay-80-light.7125567dc5ce.webp)
- 120 columns: ![Focus Baton, Overlay focused, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/overlay-120-dark.db3b90c2020f.webp) ![Focus Baton, Overlay focused, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/overlay-120-light.7d19ffc36174.webp)

### Editor focused

`editor`: Release the overlay to the editor while keeping it visible.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Focus Baton, Editor focused, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/editor-40-dark.13b55983c7db.webp) ![Focus Baton, Editor focused, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/editor-40-light.1c3cbbf6c5b1.webp)
- 60 columns: ![Focus Baton, Editor focused, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/editor-60-dark.3beb3df94eda.webp) ![Focus Baton, Editor focused, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/editor-60-light.3bed1272a609.webp)
- 80 columns: ![Focus Baton, Editor focused, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/editor-80-dark.bdd0e8403ef5.webp) ![Focus Baton, Editor focused, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/editor-80-light.2d3bf242a58d.webp)
- 120 columns: ![Focus Baton, Editor focused, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/editor-120-dark.cd998e3f9c5a.webp) ![Focus Baton, Editor focused, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/editor-120-light.8f2f7b297705.webp)

### Focus restored

`returned`: Take focus back after a temporary prompt.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Focus Baton, Focus restored, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/returned-40-dark.e4ac3eda2d61.webp) ![Focus Baton, Focus restored, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/returned-40-light.048604ad52f1.webp)
- 60 columns: ![Focus Baton, Focus restored, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/returned-60-dark.49465d8e2b04.webp) ![Focus Baton, Focus restored, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/returned-60-light.7aacf05c1f9d.webp)
- 80 columns: ![Focus Baton, Focus restored, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/returned-80-dark.40c8aac2d40d.webp) ![Focus Baton, Focus restored, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/returned-80-light.776e7a1dc515.webp)
- 120 columns: ![Focus Baton, Focus restored, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/returned-120-dark.f6d86f108d23.webp) ![Focus Baton, Focus restored, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/returned-120-light.34f70d56945b.webp)

### Temporarily hidden

`hidden`: Hide and restore through setHidden without settling the interaction.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Focus Baton, Temporarily hidden, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/hidden-40-dark.80bcde7624df.webp) ![Focus Baton, Temporarily hidden, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/hidden-40-light.80c09a0210a2.webp)
- 60 columns: ![Focus Baton, Temporarily hidden, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/hidden-60-dark.65b3edbc83fc.webp) ![Focus Baton, Temporarily hidden, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/hidden-60-light.6520b1a35722.webp)
- 80 columns: ![Focus Baton, Temporarily hidden, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/hidden-80-dark.289d7272b1ad.webp) ![Focus Baton, Temporarily hidden, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/hidden-80-light.d96e3cba047c.webp)
- 120 columns: ![Focus Baton, Temporarily hidden, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/focus-baton/hidden-120-dark.de0ef0840bea.webp) ![Focus Baton, Temporarily hidden, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/focus-baton/hidden-120-light.ab359ce83605.webp)

## Sample code

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

```ts
// Focus Baton: keep a framed side view mounted while keyboard ownership changes.
import { Box, Input, Text, type Component, type Focusable, type OverlayHandle } 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: "focus-baton",
  title: "Focus Baton",
  kind: "screen",
  apis: ["custom", "Input", "Focusable", "OverlayHandle.focus", "OverlayHandle.unfocus",
    "OverlayHandle.isFocused", "OverlayHandle.setHidden", "Box", "DynamicBorder"],
  states: [
    { id: "overlay", label: "Overlay focused", steps: [
      { type: "action", name: "open" }, { type: "text", text: "Trace the parser" },
    ] },
    { id: "editor", label: "Editor focused", steps: [
      { type: "action", name: "editor-focus" }, { type: "text", text: "Review token tests" },
    ] },
    { id: "returned", label: "Focus restored", steps: [
      { type: "action", name: "prompt" }, { type: "text", text: "boundary" }, { type: "keys", data: "\r" },
    ] },
    { id: "hidden", label: "Hidden then restored", steps: [
      { type: "action", name: "hide" }, { type: "text", text: " next" },
      { type: "action", name: "restore" },
    ] },
  ],
  setup({ ui, screen, theme, action }) {
    let handle: OverlayHandle | undefined;
    let topic = "Topic: parser";
    let restored = false;
    ui.setHeader(() => new Text(theme.fg("accent", "Pi · Focus Baton"), 0, 0));
    ui.setWidget("host-context", [
      theme.fg("muted", "Main task: review the token boundary"),
      theme.fg("dim", "Side notes remain mounted"),
      theme.fg("dim", "Main draft has its own keyboard owner"),
    ]);
    action("open", () => { void ui.custom((_tui, panelTheme) => {
      const draft = new Input({ prompt: "> " });
      const heading = new Text("", 0, 0);
      const detail = new Text("", 0, 0);
      const panel = new Box(1, 0, line => panelTheme.bg("customMessageBg", line));
      panel.addChild(new DynamicBorder(line => panelTheme.fg("borderAccent", line)));
      panel.addChild(heading);
      panel.addChild(detail);
      panel.addChild(draft);
      panel.addChild(new DynamicBorder(line => panelTheme.fg("borderAccent", line)));
      class SideNotes implements Component, Focusable {
        get focused() { return draft.focused; }
        set focused(value: boolean) { draft.focused = value; }
        invalidate() { panel.invalidate(); }
        handleInput(data: string) { draft.handleInput(data); _tui.requestRender(); }
        render(width: number) {
          heading.setText(panelTheme.fg(handle?.isFocused() ? "success" : "muted",
            handle?.isFocused() ? "Side notes · FOCUSED" : "Side notes · passive"));
          detail.setText(panelTheme.fg("accent", restored ? "Restored · same draft" : topic));
          return panel.render(width);
        }
      }
      return new SideNotes();
    }, {
      overlay: true,
      overlayOptions: { width: "70%", anchor: "top-right", row: 1, margin: 1 },
      onHandle: value => { handle = value; },
    }); });
    action("editor-focus", () => {
      handle?.unfocus({ target: screen.editor });
      ui.setStatus("focus", theme.fg("success", "main editor"));
    });
    action("prompt", () => {
      void ui.input("Choose a note topic", "topic").then(answer => {
        topic = "Topic: " + (answer ?? "parser");
        handle?.focus();
        ui.setStatus("focus", theme.fg("success", "side notes"));
      });
    });
    action("hide", () => {
      handle?.setHidden(true);
      ui.setStatus("focus", theme.fg("muted", "hidden · draft retained"));
    });
    action("restore", () => {
      restored = true;
      handle?.setHidden(false);
      handle?.focus();
      ui.setStatus("focus", theme.fg("success", "side notes restored"));
    });
    return () => { ui.setWidget("host-context", undefined); ui.setStatus("focus", undefined); };
  },
};
```

## Known uses: seen in Nico's repos

- [**pi-side-chat**](https://github.com/nicobailon/pi-side-chat): Focus handoff for a persistent side-chat overlay
  - [index.ts:37-76](https://github.com/nicobailon/pi-side-chat/blob/9d59cc042634111c1ed633b7bacb9f74611aebbd/index.ts#L37-L76) @9d59cc04
  - [index.ts:138-218](https://github.com/nicobailon/pi-side-chat/blob/9d59cc042634111c1ed633b7bacb9f74611aebbd/index.ts#L138-L218) @9d59cc04
- [**earendil-works/pi**](https://github.com/earendil-works/pi): Harden overlay focus restoration after temporary UI changes
  - [packages/tui/src/tui.ts:1-100](https://github.com/earendil-works/pi/blob/91a2f8660099f41e7c8c71dbd3ae52e2ab3e81f5/packages/tui/src/tui.ts#L1-L100) @91a2f866

## For agents: choose and check

Choose Focus 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.
- [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.
- [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 [`ctx.ui.custom`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays), [`OverlayHandle.focus`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays), [`OverlayHandle.unfocus`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays), [`OverlayHandle.isFocused`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays), [`OverlayHandle.setHidden`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays), `custom`, `Input`, `Focusable`, `Box`, `DynamicBorder` before you use them.
- Every reference frame gets the verdict its state expects.

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

- [Ghost Overlay](https://pi-tui.ratstack.sh/patterns/ghost-overlay.md): starts without taking focus

## Linked from

- [Ghost Overlay](https://pi-tui.ratstack.sh/patterns/ghost-overlay.md): transfers input after mounting
- [Done Contract](https://pi-tui.ratstack.sh/patterns/done-contract.md): moves focus without completion
