# Action Compass

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

Also known as Semantic key controls.

## Intent

Resolve configurable actions through injected keybindings.

## Motivation

The Subagents fleet view receives keybindings so selection actions need not rely on literal arrow keys.

## Applicability

- Use this when picker controls should follow the user's bindings.

## Structure

```text
input -> keybinding action
action -> state + matching hints
```

## Participants

- [`KeybindingsManager.matches`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus): Resolves input against configurable action bindings.
- [`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.
- [`pi.registerShortcut`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#choose-an-integration-point): Registers a dedicated extension shortcut.
- `Action hints`: Describe configured controls rather than stale literal keys.

Pi component APIs: [`KeybindingsManager.matches`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), [`matchesKey`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), [`pi.registerShortcut`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#choose-an-integration-point), `KeybindingsManager.getKeys`, `SelectList`

## Consequences

- Configurable actions follow the user's keybindings.
- Dedicated extension shortcuts still need conflict checks.

## Implementation

- Check registered shortcut conflicts.
- Keep explicit extension shortcuts distinct from configurable selection actions.

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

### Default bindings

`default`: Show navigation and action hints using ordinary bindings.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Action Compass, Default bindings, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/action-compass/default-40-dark.90065f6be2a3.webp) ![Action Compass, Default bindings, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/action-compass/default-40-light.72a66763c3da.webp)
- 60 columns: ![Action Compass, Default bindings, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/action-compass/default-60-dark.a940ba2f9031.webp) ![Action Compass, Default bindings, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/action-compass/default-60-light.88b5e7b854b5.webp)
- 80 columns: ![Action Compass, Default bindings, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/action-compass/default-80-dark.6ed35dd7c3ef.webp) ![Action Compass, Default bindings, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/action-compass/default-80-light.ea30941f8340.webp)
- 120 columns: ![Action Compass, Default bindings, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/action-compass/default-120-dark.976426519cab.webp) ![Action Compass, Default bindings, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/action-compass/default-120-light.6c59caad3fb5.webp)

### Remapped action

`remapped`: Use a synthetic remapping for selection and show matching hints.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Action Compass, Remapped action, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/action-compass/remapped-40-dark.4aabef03ad67.webp) ![Action Compass, Remapped action, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/action-compass/remapped-40-light.f72a18ac8910.webp)
- 60 columns: ![Action Compass, Remapped action, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/action-compass/remapped-60-dark.e968cd849a56.webp) ![Action Compass, Remapped action, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/action-compass/remapped-60-light.684e6d7a66ca.webp)
- 80 columns: ![Action Compass, Remapped action, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/action-compass/remapped-80-dark.583f330a7ae7.webp) ![Action Compass, Remapped action, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/action-compass/remapped-80-light.9184ba1b3e7d.webp)
- 120 columns: ![Action Compass, Remapped action, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/action-compass/remapped-120-dark.a30af79c8090.webp) ![Action Compass, Remapped action, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/action-compass/remapped-120-light.a744f7f05d07.webp)

### Dedicated action

`shortcut`: Toggle a small mode with an explicitly registered shortcut.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Action Compass, Dedicated action, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/action-compass/shortcut-40-dark.7f1f7efe9805.webp) ![Action Compass, Dedicated action, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/action-compass/shortcut-40-light.817f65ba1da4.webp)
- 60 columns: ![Action Compass, Dedicated action, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/action-compass/shortcut-60-dark.dd5f9f69114e.webp) ![Action Compass, Dedicated action, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/action-compass/shortcut-60-light.8e5acbb5db74.webp)
- 80 columns: ![Action Compass, Dedicated action, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/action-compass/shortcut-80-dark.6278032d450e.webp) ![Action Compass, Dedicated action, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/action-compass/shortcut-80-light.296a0bdeafce.webp)
- 120 columns: ![Action Compass, Dedicated action, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/action-compass/shortcut-120-dark.5504e378b5cf.webp) ![Action Compass, Dedicated action, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/action-compass/shortcut-120-light.a3c0f66ec174.webp)

## Sample code

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

```ts
// Action Compass: resolve navigation through injected bindings and keep hints in sync.
import { SelectList, matchesKey, truncateToWidth, type Component } from "@earendil-works/pi-tui";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "action-compass",
  title: "Action Compass",
  kind: "component",
  apis: ["KeybindingsManager.matches", "KeybindingsManager.getKeys", "SelectList", "matchesKey"],
  rows: 9,
  states: [
    { id: "default", label: "Default bindings" },
    { id: "remapped", label: "Remapped action", steps: [
      { type: "action", name: "remap" }, { type: "keys", data: "j" },
    ] },
    { id: "shortcut", label: "Dedicated action", steps: [{ type: "keys", data: "\x18" }] },
  ],
  setup({ theme, keybindings, tui, action }) {
    const targets = new SelectList([
      { value: "map", label: "Map the parser" },
      { value: "test", label: "Test the token boundary" },
      { value: "review", label: "Review the patch" },
    ], 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),
    });
    let selected = 0;
    let detail = false;
    action("remap", () => {
      keybindings.setUserBindings({ "tui.select.up": "k", "tui.select.down": "j" });
      tui.requestRender();
    });
    const compass: Component = {
      invalidate() { targets.invalidate(); },
      handleInput(data) {
        if (keybindings.matches(data, "tui.select.down")) selected = Math.min(2, selected + 1);
        if (keybindings.matches(data, "tui.select.up")) selected = Math.max(0, selected - 1);
        // Explicit extension control, separate from remappable list actions.
        // This component owns a local key. Pi registerShortcut runs through
        // the host editor, where ctrl+x is reserved for app.message.copy.
        if (matchesKey(data, "ctrl+x")) detail = !detail;
        targets.setSelectedIndex(selected);
        tui.requestRender();
      },
      render(width) {
        const up = keybindings.getKeys("tui.select.up").join("/");
        const down = keybindings.getKeys("tui.select.down").join("/");
        return [
          theme.fg("accent", "Action Compass · task picker"),
          ...targets.render(width),
          theme.fg("muted", truncateToWidth(up + "/" + down + " navigate · ctrl+x detail", width)),
          theme.fg(detail ? "success" : "dim", truncateToWidth(
            detail ? "Detail ON · inspect tests before review" : "Detail OFF · compact task labels", width)),
        ];
      },
    };
    return compass;
  },
};
```

## Known uses: seen in Nico's repos

- [**dot314**](https://github.com/nicobailon/dot314): Bind a mode action to a dedicated shortcut
  - [extensions/reverse-thinking.ts:1-19](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/reverse-thinking.ts#L1-L19) @17cce138
- [**pi-subagents**](https://github.com/nicobailon/pi-subagents): Keep fleet selection stable while details are derived
  - [src/tui/fleet.ts:1-120](https://github.com/nicobailon/pi-subagents/blob/6826b0545216077195ae8ffe468a6434715814b7/src/tui/fleet.ts#L1-L120) @6826b054
  - [src/tui/fleet.ts:1360-1463](https://github.com/nicobailon/pi-subagents/blob/6826b0545216077195ae8ffe468a6434715814b7/src/tui/fleet.ts#L1360-L1463) @6826b054
- [**pi-intercom**](https://github.com/nicobailon/pi-intercom): Compose with explicit send state and inline failure recovery
  - [ui/compose.ts:18-91](https://github.com/nicobailon/pi-intercom/blob/a5fad4df2a9fe4909bf4d9b06263c8316976b57d/ui/compose.ts#L18-L91) @a5fad4df

## For agents: choose and check

Choose Action Compass 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.

- [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.
- [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 [`KeybindingsManager.matches`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), [`matchesKey`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), [`pi.registerShortcut`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#choose-an-integration-point), `KeybindingsManager.getKeys`, `SelectList` before you use them.
- Every reference frame gets the verdict its state expects.

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

- [Input Lease](https://pi-tui.ratstack.sh/patterns/input-lease.md): intercepts raw input instead

## Linked from

- [Tab Deck](https://pi-tui.ratstack.sh/patterns/tab-deck.md): resolves navigation actions
- [Editor Steward](https://pi-tui.ratstack.sh/patterns/editor-steward.md): keeps base application controls
- [Input Lease](https://pi-tui.ratstack.sh/patterns/input-lease.md): resolves configurable component actions
