# ashxj-spinners

A pi coding-agent extension that drives the **working-indicator row**
with state-specific spinner pools and per-state kaomoji. The row is
just `${spinner} ${kaomoji}` — no text.

> **Status: active development.** Spinner pool choices and kaomoji are
> being iterated on.

## What it looks like

The row that appears during streaming becomes:

```
[spinner] [kaomoji]
```

With a spinner picked at random from the state's pool on entry, plus
the kaomoji re-rolled every 30 s during long states, so a long thinking
stretch cycles through faces instead of freezing on one. Examples:

```
thinking:    ⢁⠂⠔⠈ (ᵕ—ᴗ—)
searching:   ⠁⠀    (⸝⸝⩌ ⤙ ⩌⸝⸝)
editing:     ⡀⠀⠀  (ᵕ • ᴗ •)
toolRunning: ⣀⣀   (๑´>᎑<)~
error:       ⠀⠶⠀  (,,>﹏<,,)
```

`idle`, `startup`, and `complete` are **truly silent** — no spinner, no
kaomoji, no text. The row is blank during rest.

## The state machine

The extension subscribes to pi's lifecycle events and picks a
`(spinner, kaomoji)` pair for the current state:

| State | Spinner pool | Kaomoji pool | Triggered by | Silent? |
|---|---|---|---|---|
| `idle` | — | — | `session_start`, `agent_end` with no assistant reply | yes |
| `startup` | — | — | `session_start` with `reason: "startup"` | yes |
| `thinking` | shared pool (6 spinners) | shared pool (14 kaomoji) | `turn_start` | no |
| `searching` | `diagswipe` `scan` `cascade` | 2 kaomoji | `tool_execution_start` matching the **search** regex | no |
| `editing` | `columns` `fillsweep` | 3 kaomoji | `tool_execution_start` matching the **edit** regex | no |
| `toolRunning` | shared pool (6 spinners) | shared pool (14 kaomoji) | `tool_execution_start` (default) | no |
| `complete` | — | — | `agent_end` with `stopReason ∈ {stop, length}` | yes |
| `error` | `pulse` | 6 kaomoji | `tool_execution_end` with `isError`, or `agent_end` with `stopReason ∈ {error, aborted}` | no |

**`thinking` and `toolRunning` share one pool.** Both states draw their
spinner and kaomoji from the same combined pool (6 spinners, 14 kaomoji),
so they're visually interchangeable. The shared spinner pool is
`rain` `breathe` `checkerboard` `sparkle` `braille` `snake`. The other
states keep their own pools.

**Kaomoji re-roll within a state.** While an active state is showing, the
kaomoji is re-rolled every `KAOMOJI_REROLL_MS` (30 s) so a long state
cycles through faces instead of freezing on one. The spinner is not
re-rolled on this timer (restarting its animation would stutter); it
only changes on state transitions.

### Minimum state TTL (3 s)

The TTL prevents flicker between **active** states — notably,
`turn_start` firing immediately after an `error` won't clobber the
error before the user can see it. The TTL only applies to
**active→active** transitions:

- **Active → active**: TTL-gated. If within 3 s, the new state is rejected.
- **Active → silent** (`idle` / `startup` / `complete`): TTL bypassed.
  These are end-of-turn markers and must always be reachable, otherwise
  the spinner would get stuck in an active state when the LLM finishes
  a turn within 3 s of starting.
- **Active → error**: TTL bypassed. Error has priority and can always
  be set.
- **Tool execution start**: TTL bypassed via `force: true`. When a tool
  starts, the spinner changes to the tool state immediately so it
  syncs with the tool bubble. Without this, a tool starting within 3 s
  of `turn_start` would leave the spinner stuck on the thinking spinner
  while the tool bubble is already on screen.
- **Silent → active**: TTL bypassed (silent states aren't in
  `STATES_WITH_TTL`).
- **Auto-revert timers** (e.g. `complete` → `idle` after 3 s) bypass
  the TTL by calling `doApplyState` directly.

## Spinners

Spinners are powered by the
[`unicode-animations`](https://www.npmjs.com/package/unicode-animations)
npm package. See [SPINNERS.md](./SPINNERS.md) for the per-state pool
list, plus instructions for adding or swapping spinners. `thinking` and
`toolRunning` share one combined spinner pool (see above).

## Installation

### From a local path (for development)

In `~/.pi/agent/settings.json`:

```json
{
  "extensions": [
    "/path/to/extensions/ashxj-spinners"
  ]
}
```

Then `/reload` inside pi.

### From npm (published)

```bash
pi install npm:ashxj-spinners
```

Or add it to `~/.pi/agent/settings.json`:

```json
{
  "packages": ["npm:ashxj-spinners"]
}
```

See the [pi packages gallery](https://pi.dev/packages) and
[package docs](https://pi.dev/docs/latest/packages) for more.

## Customization

Everything lives in [`index.ts`](./index.ts) as a single file. Common edits:

| Want to… | Edit |
|---|---|
| Add or remove a spinner from a state's pool | the matching entry in `SPINNER_POOL_FOR_STATE` |
| Change the kaomoji pool for a state | the matching entry in `KAOMOJI_FOR_STATE` |
| Change the shared thinking/toolRunning spinner pool | `THINKING_TOOL_SPINNER_POOL` |
| Change the shared thinking/toolRunning kaomoji pool | `THINKING_TOOL_KAOMOJI_POOL` |
| Change how often the kaomoji re-rolls within a state | `KAOMOJI_REROLL_MS` |
| Add a new state | extend the `State` union, add to `SPINNER_POOL_FOR_STATE` and `KAOMOJI_FOR_STATE`, call `applyState(...)` from a new event handler |
| Change the auto-revert delay for `complete` / `startup` | `TRANSIENT_MS` |
| Change the minimum state TTL | `STATE_TTL_MS` |
| Color the spinner or kaomoji differently | the `ctx.ui.theme.fg(...)` calls in `doApplyState` |

## Compatibility

- pi coding-agent `0.80+` (uses `ctx.ui.setWorkingIndicator()` and
  `ctx.ui.setWorkingMessage()`)
- Terminal: any modern terminal with Unicode support. Tested primarily in
  VS Code's integrated terminal.

## License

MIT — see [LICENSE](./LICENSE).
