# Idle Fuse

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

Also known as Idle dismissal.

## Intent

Reset a component-owned inactivity timer after every input.

## Motivation

Skill Palette resets a 60-second timer on input and cancels the picker when the user stops interacting.

## Applicability

- Use this when an unattended picker should eventually close.

## Structure

```text
input -> reset idle timer
idle deadline -> done(cancel)
```

## 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.
- `Idle timer`: Resets on input and cancels the abandoned interaction.

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), `SelectList`, `Box`

## Consequences

- An abandoned picker does not remain open indefinitely.
- Every input and completion path must manage the owned timer.

## Implementation

- Clear the timer on completion and disposal.
- Do not confuse idle timeout with a fixed countdown.

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

### Idle timer

`waiting`: Show a synthetic palette and remaining idle time.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Idle Fuse, Idle timer, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-40-dark.dcaa7ecc48db.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-40-dark.anim.d89ea4ef96e9.webp) ![Idle Fuse, Idle timer, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-40-light.8d23a2cf628a.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-40-light.anim.e54783dcef56.webp)
- 60 columns: ![Idle Fuse, Idle timer, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-60-dark.f5d4b4087889.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-60-dark.anim.ef7cfd73c336.webp) ![Idle Fuse, Idle timer, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-60-light.3c5e4d797c63.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-60-light.anim.e48c8f2a9e93.webp)
- 80 columns: ![Idle Fuse, Idle timer, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-80-dark.adbc9723acd8.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-80-dark.anim.d23f69574037.webp) ![Idle Fuse, Idle timer, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-80-light.4fea8802fe53.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-80-light.anim.d20e8a46cf24.webp)
- 120 columns: ![Idle Fuse, Idle timer, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-120-dark.598da2559a7c.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-120-dark.anim.ffee1c2d59c6.webp) ![Idle Fuse, Idle timer, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-120-light.0981dcc2033f.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/waiting-120-light.anim.53b9f6cd0766.webp)

### Timer reset

`input`: Navigate once and reset the visible idle timer.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Idle Fuse, Timer reset, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/idle-fuse/input-40-dark.a33eea136ef2.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/input-40-dark.anim.e49ae627ea10.webp) ![Idle Fuse, Timer reset, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/idle-fuse/input-40-light.72a1189b0a6e.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/input-40-light.anim.7578bce44364.webp)
- 60 columns: ![Idle Fuse, Timer reset, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/idle-fuse/input-60-dark.ded5e53a3217.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/input-60-dark.anim.029534580059.webp) ![Idle Fuse, Timer reset, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/idle-fuse/input-60-light.7b9a17add26e.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/input-60-light.anim.4744fe4c128c.webp)
- 80 columns: ![Idle Fuse, Timer reset, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/idle-fuse/input-80-dark.e5e87b966417.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/input-80-dark.anim.915b4d49da1c.webp) ![Idle Fuse, Timer reset, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/idle-fuse/input-80-light.5c98770eb2c7.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/input-80-light.anim.dfb643732e9b.webp)
- 120 columns: ![Idle Fuse, Timer reset, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/idle-fuse/input-120-dark.2f38242438de.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/input-120-dark.anim.e497a09d7cb7.webp) ![Idle Fuse, Timer reset, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/idle-fuse/input-120-light.37beaab05eb3.webp) [animated](https://pi-tui.ratstack.sh/frames/idle-fuse/input-120-light.anim.c1c6583e7c40.webp)

### Idle cancellation

`expired`: Close the palette after the next uninterrupted idle interval.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Idle Fuse, Idle cancellation, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/idle-fuse/expired-40-dark.93f7a12cfb05.webp) ![Idle Fuse, Idle cancellation, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/idle-fuse/expired-40-light.79a7b13317e9.webp)
- 60 columns: ![Idle Fuse, Idle cancellation, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/idle-fuse/expired-60-dark.b4b7effd93df.webp) ![Idle Fuse, Idle cancellation, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/idle-fuse/expired-60-light.2adc8dd66d6f.webp)
- 80 columns: ![Idle Fuse, Idle cancellation, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/idle-fuse/expired-80-dark.28e88e0497d6.webp) ![Idle Fuse, Idle cancellation, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/idle-fuse/expired-80-light.7fc62a3bf988.webp)
- 120 columns: ![Idle Fuse, Idle cancellation, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/idle-fuse/expired-120-dark.66947330c148.webp) ![Idle Fuse, Idle cancellation, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/idle-fuse/expired-120-light.6c4132e0da2b.webp)

## Sample code

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

```ts
// Idle Fuse: every input resets a component-owned inactivity deadline.
import { getSelectListTheme } from "@earendil-works/pi-coding-agent";
import { Box, SelectList, Text } from "@earendil-works/pi-tui";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "idle-fuse", title: "Idle Fuse", kind: "screen",
  apis: ["ctx.ui.custom", "SelectList", "Box", "TUI.requestRender"],
  states: [
    { id: "waiting", label: "Idle timer", animated: true, steps: [{ type: "tick", ms: 1100 }] },
    { id: "input", label: "Timer reset", animated: true, steps: [
      { type: "keys", data: "\x1b[B" }, { type: "tick", ms: 100 },
    ] },
    { id: "expired", label: "Idle cancellation", steps: [
      { type: "tick", ms: 1900 }, { type: "resize", rows: 12 },
    ] },
  ],
  setup({ ui, theme }) {
    ui.setEditorText("Keep drafting here");
    ui.setWidget("palette", [theme.fg("muted", "Palette open · idle resets on input")]);
    void ui.custom<string | undefined>((tui, theme, keys, done) => {
      const idleMs = 2000; // A short synthetic interval for the story, not source policy.
      const choices = new SelectList(["Build", "Review", "Test"].map(label => ({
        value: label, label,
      })), 3, getSelectListTheme());
      let lifecycle: "open" | "closed" = "open";
      let deadline = Date.now() + idleMs;
      let timeout: ReturnType<typeof setTimeout>;
      const pulse = setInterval(() => tui.requestRender(), 100);
      function dispose() {
        lifecycle = "closed";
        clearTimeout(timeout);
        clearInterval(pulse);
      }
      function finish(value: string | undefined) {
        if (lifecycle === "closed") return;
        dispose();
        done(value);
      }
      function resetIdle() {
        clearTimeout(timeout);
        deadline = Date.now() + idleMs;
        timeout = setTimeout(() => finish(undefined), idleMs);
      }
      resetIdle();
      choices.onSelect = item => finish(item.value);
      choices.onCancel = () => finish(undefined);
      return {
        dispose,
        invalidate() { choices.invalidate(); },
        handleInput(data) {
          resetIdle(); // All input counts, including keys the chooser does not own.
          if (keys.matches(data, "tui.select.cancel")) finish(undefined);
          else choices.handleInput(data);
          tui.requestRender();
        },
        render(width) {
          const panel = new Box(1, 1, text => theme.bg("customMessageBg", text));
          panel.addChild(new Text(theme.fg("accent", "Idle palette · " + Math.ceil((deadline - Date.now()) / 1000) + "s left"), 0, 0));
          panel.addChild(choices);
          panel.addChild(new Text(theme.fg("dim", "↑↓ select · input resets 2s"), 0, 0));
          return panel.render(width);
        },
      };
    }).then(selection => {
      ui.setWidget("palette", [
        theme.fg("success", selection === undefined ? "Idle cancellation · palette closed" : "Selected: " + selection),
        theme.fg("muted", "Owned timers cleared · draft kept"),
      ]);
    });
  },
};
```

## Known uses: seen in Nico's repos

- [**pi-skill-palette**](https://github.com/nicobailon/pi-skill-palette): Dismiss an idle palette and clean its timer
  - [index.ts:625-660](https://github.com/nicobailon/pi-skill-palette/blob/a5c4429b8c2e33ab903d07856497014f3d5ad34e/index.ts#L625-L660) @a5c4429b
  - [index.ts:829-842](https://github.com/nicobailon/pi-skill-palette/blob/a5c4429b8c2e33ab903d07856497014f3d5ad34e/index.ts#L829-L842) @a5c4429b

## For agents: choose and check

Choose Idle Fuse when your job matches its intent and applicability above. Its neighbours in Overlays and dialogs are listed below. Read the one whose intent fits your job more closely before you commit.

- [Shared Shell](https://pi-tui.ratstack.sh/patterns/shared-shell.md): Wrap specialized dialog content in a shared themed frame.
- [Warning Gate](https://pi-tui.ratstack.sh/patterns/warning-gate.md): Follow a consequential selection with a separate warning confirmation.
- [Abort Lantern](https://pi-tui.ratstack.sh/patterns/abort-lantern.md): Settle foreground loading before opening a separate result view.
- [Dialog Fuse](https://pi-tui.ratstack.sh/patterns/dialog-fuse.md): Show remaining time before a transient dialog automatically dismisses.
- [Abort Tether](https://pi-tui.ratstack.sh/patterns/abort-tether.md): Tie a built-in dialog's lifetime to an operation's abort signal.
- [Settings Bench](https://pi-tui.ratstack.sh/patterns/settings-bench.md): Keep configuration interaction separate from the live activity view.
- [Action Sieve](https://pi-tui.ratstack.sh/patterns/action-sieve.md): Derive dialog choices from current control and completion state.
- [Done Contract](https://pi-tui.ratstack.sh/patterns/done-contract.md): Resolve each custom interaction with a typed selection or cancellation.

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), `SelectList`, `Box` before you use them.
- Every reference frame gets the verdict its state expects.

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

- [Dialog Fuse](https://pi-tui.ratstack.sh/patterns/dialog-fuse.md): uses a fixed expiry deadline

## Linked from

- [Dialog Fuse](https://pi-tui.ratstack.sh/patterns/dialog-fuse.md): expires from inactivity instead
