# Refresh Lease

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

Lifecycle / Rendering and performance · `refresh-lease` · [HTML](https://pi-tui.ratstack.sh/patterns/refresh-lease/) · [JSON](https://pi-tui.ratstack.sh/patterns/refresh-lease.json) · [all patterns](https://pi-tui.ratstack.sh/patterns.md)

Also known as Owned refresh.

## Intent

Pair asynchronous refresh triggers with component-owned cleanup.

## Motivation

Messenger, Coordination and Autoresearch pair asynchronous redraw triggers with timer cleanup.

## Applicability

- Use this when a widget or overlay changes independently of user input.

## Structure

```text
timer / subscription -> data
changed data -> requestRender
dispose -> cancel / unsubscribe
```

## Participants

- [`ctx.ui.setWidget`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user): Mounts, replaces or clears one near-editor widget.
- [`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.
- [`pi.on`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#events): Registers ordered extension event handlers.
- `Resource owner`: Cancels timers and subscriptions on disposal or shutdown.

Pi screen APIs: [`ctx.ui.setWidget`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user), [`TUI.requestRender`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), [`pi.on`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#events), `setWidget`, `truncateToWidth`

## Consequences

- A view can react to asynchronous changes without user input.
- Redraw cadence does not prove the underlying data was refreshed.

## Implementation

- Redrawing does not necessarily refresh underlying data.
- Stop timers and unsubscribe listeners on disposal.
- Reject callbacks against replaced session contexts.

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

### Refreshing widget

`mounted`: Show synthetic background durations advancing.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Refresh Lease, Refreshing widget, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-40-dark.98cd7bf5d452.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-40-dark.anim.179edd0cb816.webp) ![Refresh Lease, Refreshing widget, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-40-light.ab79dd0c49d8.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-40-light.anim.e60b14555303.webp)
- 60 columns: ![Refresh Lease, Refreshing widget, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-60-dark.27d283fc0ef5.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-60-dark.anim.67f8e446355a.webp) ![Refresh Lease, Refreshing widget, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-60-light.f2d96baa0620.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-60-light.anim.906605f14a73.webp)
- 80 columns: ![Refresh Lease, Refreshing widget, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-80-dark.fd533578e558.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-80-dark.anim.e125a10bf2fd.webp) ![Refresh Lease, Refreshing widget, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-80-light.9e188ac9d19b.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-80-light.anim.a001acc480ff.webp)
- 120 columns: ![Refresh Lease, Refreshing widget, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-120-dark.206443cb2dfb.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-120-dark.anim.beaab4b72983.webp) ![Refresh Lease, Refreshing widget, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-120-light.b2a4c1991a9a.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/mounted-120-light.anim.016d168455d9.webp)

### New snapshot

`updated`: Show a data refresh separately from its redraw.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Refresh Lease, New snapshot, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-40-dark.bcac4d3f1323.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-40-dark.anim.84455973dd93.webp) ![Refresh Lease, New snapshot, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-40-light.6db66c7461e8.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-40-light.anim.43f678f9ae01.webp)
- 60 columns: ![Refresh Lease, New snapshot, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-60-dark.e1ba88c25141.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-60-dark.anim.cb24bda84f20.webp) ![Refresh Lease, New snapshot, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-60-light.55072dce8fb9.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-60-light.anim.f0bd3839652e.webp)
- 80 columns: ![Refresh Lease, New snapshot, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-80-dark.ae6bf9c89b58.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-80-dark.anim.4d69baf5a7ef.webp) ![Refresh Lease, New snapshot, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-80-light.e7f987a8de59.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-80-light.anim.a66d078d9d09.webp)
- 120 columns: ![Refresh Lease, New snapshot, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-120-dark.0cf3d310b054.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-120-dark.anim.743786f8d0b5.webp) ![Refresh Lease, New snapshot, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-120-light.91679012673f.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/updated-120-light.anim.d8902fea09a9.webp)

### Refresh stopped

`disposed`: Clear the widget and show that its timer and listener no longer update the host.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Refresh Lease, Refresh stopped, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-40-dark.522cf48cb76a.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-40-dark.anim.d254efc89714.webp) ![Refresh Lease, Refresh stopped, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-40-light.d8f61f75ce63.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-40-light.anim.392a8dceaa58.webp)
- 60 columns: ![Refresh Lease, Refresh stopped, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-60-dark.b5c74b2144bb.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-60-dark.anim.8a42ff7ce03f.webp) ![Refresh Lease, Refresh stopped, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-60-light.08b840835d13.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-60-light.anim.dc6b3b6d57be.webp)
- 80 columns: ![Refresh Lease, Refresh stopped, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-80-dark.9d1ee0f71505.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-80-dark.anim.1229ff9f6792.webp) ![Refresh Lease, Refresh stopped, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-80-light.d2ea0eac76f9.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-80-light.anim.f1fb242e27f8.webp)
- 120 columns: ![Refresh Lease, Refresh stopped, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-120-dark.e697f071e6de.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-120-dark.anim.4c8d1415c1da.webp) ![Refresh Lease, Refresh stopped, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-120-light.9ffc557bd14d.webp) [animated](https://pi-tui.ratstack.sh/frames/refresh-lease/disposed-120-light.anim.8e65f28a49e0.webp)

## Sample code

`stories/patterns/lifecycle/refresh-lease.ts`, the story the frames above were rendered from.

```ts
// Refresh Lease: distinguish snapshot refresh from redraw and own both resources.
import { truncateToWidth } from "@earendil-works/pi-tui";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "refresh-lease", title: "Refresh Lease", kind: "screen",
  apis: ["setWidget", "TUI.requestRender", "truncateToWidth"],
  states: [
    { id: "mounted", label: "Refreshing widget", animated: true, steps: [{ type: "tick", ms: 600 }] },
    { id: "updated", label: "New snapshot", animated: true, steps: [
      { type: "action", name: "snapshot" }, { type: "tick", ms: 600 },
    ] },
    { id: "disposed", label: "Refresh stopped", animated: true, steps: [
      { type: "action", name: "dispose" }, { type: "action", name: "late-snapshot" }, { type: "tick", ms: 600 },
    ] },
  ],
  setup({ ui, tui, theme, clock, action }) {
    ui.setEditorText("Review the background checks.");
    // Fixture publisher: the view subscribes; late snapshots have no listener.
    const listeners = new Set<(count: number) => void>();
    const publish = (count: number) => { for (const listener of listeners) listener(count); };
    let draws = 0;
    let proofTimer: ReturnType<typeof setTimeout> | undefined;
    ui.setWidget("refresh", () => {
      let snapshot = 2;
      const started = clock.now();
      const update = (count: number) => { snapshot = count; tui.requestRender(); };
      listeners.add(update);
      const redraw = setInterval(() => { draws++; tui.requestRender(); }, 200);
      return {
        invalidate() {},
        render: width => [
          truncateToWidth(theme.fg("accent", "Parser checks · " + snapshot + "/4 complete"), width),
          truncateToWidth(theme.fg("muted", "Running " + ((clock.now() - started) / 1000).toFixed(1) + "s · redraws " + draws), width),
        ],
        dispose() { clearInterval(redraw); listeners.delete(update); },
      };
    }, { placement: "belowEditor" });
    action("snapshot", () => publish(3));
    action("late-snapshot", () => publish(4));
    action("dispose", () => {
      ui.setWidget("refresh", undefined); // Pi invokes the component's dispose.
      ui.setWidget("receipt", [theme.fg("success", "Refresh stopped · resources released")]);
      ui.setStatus("refresh", theme.fg("muted", "stopped at " + draws + " redraws"));
      const stoppedDraws = draws;
      proofTimer = setTimeout(() => {
        if (draws !== stoppedDraws || listeners.size) throw new Error("Refresh lease outlived its view");
      }, 500);
    });
    return () => { if (proofTimer !== undefined) clearTimeout(proofTimer); };
  },
};
```

## Known uses: seen in Nico's repos

- [**pi-autoresearch**](https://github.com/nicobailon/pi-autoresearch): Pair live refresh with owned timer cleanup
  - [extensions/pi-autoresearch/index.ts:1428-1445](https://github.com/nicobailon/pi-autoresearch/blob/22bd30b19482f2be5936031b2c6b149115779f16/extensions/pi-autoresearch/index.ts#L1428-L1445) @22bd30b1
- [**pi-coordination**](https://github.com/nicobailon/pi-coordination): Pair live refresh with owned timer cleanup
  - [coordinate/dashboard.ts:910-917](https://github.com/nicobailon/pi-coordination/blob/7f32f5d9d597a31fa477a629dfc6dd26bc649214/coordinate/dashboard.ts#L910-L917) @7f32f5d9
  - [coordinate/dashboard.ts:1015-1018](https://github.com/nicobailon/pi-coordination/blob/7f32f5d9d597a31fa477a629dfc6dd26bc649214/coordinate/dashboard.ts#L1015-L1018) @7f32f5d9
- [**pi-messenger**](https://github.com/nicobailon/pi-messenger): Pair live refresh with owned timer cleanup
  - [overlay-coordinator.ts:85-119](https://github.com/nicobailon/pi-messenger/blob/09937ed647a1b07a3b595bf75943feacb80ff123/overlay-coordinator.ts#L85-L119) @09937ed6
- [**pi-interactive-shell**](https://github.com/nicobailon/pi-interactive-shell): Show bounded background-session status below the editor
  - [background-widget.ts:24-106](https://github.com/nicobailon/pi-interactive-shell/blob/77df9a8142a2f731635a4c5a01d68feecb5cced4/background-widget.ts#L24-L106) @77df9a81

## For agents: choose and check

Choose Refresh Lease when your job matches its intent and applicability above. Its neighbours in Rendering and performance are listed below. Read the one whose intent fits your job more closely before you commit.

- [Tail Anchor](https://pi-tui.ratstack.sh/patterns/tail-anchor.md): Follow new output only while the user remains at the bottom.
- [Render Funnel](https://pi-tui.ratstack.sh/patterns/render-funnel.md): Collapse bursty redraw scheduling behind one pending timer.
- [Late Paint](https://pi-tui.ratstack.sh/patterns/late-paint.md): Cache plain layout by width and apply theme styling during rendering.

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.setWidget`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user), [`TUI.requestRender`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), [`pi.on`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#events), `setWidget`, `truncateToWidth` before you use them.
- Every reference frame gets the verdict its state expects.

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

- [Tick Heart](https://pi-tui.ratstack.sh/patterns/tick-heart.md): mutates local state on each tick

## Linked from

- [Event Relay](https://pi-tui.ratstack.sh/patterns/event-relay.md): owns subscriptions and redraws
- [Notice Fuse](https://pi-tui.ratstack.sh/patterns/notice-fuse.md): also ties timers to owners
- [Render Funnel](https://pi-tui.ratstack.sh/patterns/render-funnel.md): owns the redraw trigger's lifetime
