# Result Relay

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

Also known as Async result widget.

## Intent

Replace pending feedback with a persistent non-modal result widget.

## Motivation

Dot314's btw command starts with pending feedback and leaves a result above the editor.

## Applicability

- Use this when background work should leave useful output near the editor.

## Structure

```text
pending -> status
ready -> result widget
dismiss -> clear
```

## Participants

- [`ctx.ui.setStatus`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user): Writes or clears one named footer slot.
- [`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.
- `Result owner`: Replaces pending feedback and clears the dismissed result.

Pi screen APIs: [`ctx.ui.setStatus`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user), [`ctx.ui.setWidget`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#interact-with-the-user), `setStatus`, `setWidget`

## Consequences

- Background output can remain useful without a modal.
- The result persists until its owning dismissal or cleanup path clears it.

## Implementation

- Use a stable key for replacement.
- Clear the result when dismissed or cancelled.

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

### Work pending

`pending`: Show a compact pending status for synthetic work.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Result Relay, Work pending, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/result-relay/pending-40-dark.6d0c9f32ec93.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/pending-40-dark.anim.8c6cd875c3e1.webp) ![Result Relay, Work pending, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/result-relay/pending-40-light.57d7a70ba38c.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/pending-40-light.anim.5f37d1195fee.webp)
- 60 columns: ![Result Relay, Work pending, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/result-relay/pending-60-dark.c9d813a5b48a.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/pending-60-dark.anim.5679b0b59fef.webp) ![Result Relay, Work pending, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/result-relay/pending-60-light.0b6f7e7b8c9f.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/pending-60-light.anim.0065763008c5.webp)
- 80 columns: ![Result Relay, Work pending, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/result-relay/pending-80-dark.e9bbe1ee5e1b.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/pending-80-dark.anim.43f93e4d4d08.webp) ![Result Relay, Work pending, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/result-relay/pending-80-light.55edfc635423.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/pending-80-light.anim.977d2e36be14.webp)
- 120 columns: ![Result Relay, Work pending, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/result-relay/pending-120-dark.422ec8c4f461.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/pending-120-dark.anim.07cb22eb0e57.webp) ![Result Relay, Work pending, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/result-relay/pending-120-light.f89ad0c52d62.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/pending-120-light.anim.2df43c626599.webp)

### Result ready

`result`: Replace pending feedback with the result widget.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Result Relay, Result ready, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/result-relay/result-40-dark.562dc4c4b829.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/result-40-dark.anim.7caeab3fca4c.webp) ![Result Relay, Result ready, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/result-relay/result-40-light.1a704b918d24.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/result-40-light.anim.664443c64591.webp)
- 60 columns: ![Result Relay, Result ready, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/result-relay/result-60-dark.c9b39621a19d.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/result-60-dark.anim.06d3d2d8009a.webp) ![Result Relay, Result ready, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/result-relay/result-60-light.63e740e5d5b8.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/result-60-light.anim.b5a5fcfc7f3c.webp)
- 80 columns: ![Result Relay, Result ready, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/result-relay/result-80-dark.93275166aa1f.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/result-80-dark.anim.83b1bf741fd6.webp) ![Result Relay, Result ready, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/result-relay/result-80-light.5979ccdf49ad.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/result-80-light.anim.bfc7a34a7d86.webp)
- 120 columns: ![Result Relay, Result ready, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/result-relay/result-120-dark.95f39b0617f8.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/result-120-dark.anim.4825ac173ae0.webp) ![Result Relay, Result ready, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/result-relay/result-120-light.7f56469bd36c.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/result-120-light.anim.c47b1ad9a43e.webp)

### Dismissed

`dismissed`: Remove the result on dismissal.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Result Relay, Dismissed, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-40-dark.5c036fa97d5a.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-40-dark.anim.a7d7493d3f69.webp) ![Result Relay, Dismissed, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-40-light.4c44fc7ae154.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-40-light.anim.a620e7c572ab.webp)
- 60 columns: ![Result Relay, Dismissed, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-60-dark.54c190cd56f7.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-60-dark.anim.fb042a6ed4c0.webp) ![Result Relay, Dismissed, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-60-light.833bad35b777.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-60-light.anim.7be15454b2d1.webp)
- 80 columns: ![Result Relay, Dismissed, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-80-dark.bec0c03d59b6.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-80-dark.anim.01aec86c4411.webp) ![Result Relay, Dismissed, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-80-light.bb363ad78b1b.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-80-light.anim.eb16a3d3e922.webp)
- 120 columns: ![Result Relay, Dismissed, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-120-dark.666d9f10a6e1.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-120-dark.anim.f78659128aba.webp) ![Result Relay, Dismissed, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-120-light.81c4de20ba60.webp) [animated](https://pi-tui.ratstack.sh/frames/result-relay/dismissed-120-light.anim.28956265eb69.webp)

## Sample code

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

```ts
// Result Relay: replace pending status with a persistent non-modal result.
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "result-relay", title: "Result Relay", kind: "screen",
  apis: ["setStatus", "setWidget"],
  states: [
    { id: "pending", label: "Work pending", animated: true, steps: [{ type: "tick", ms: 400 }] },
    { id: "result", label: "Result ready", animated: true, steps: [{ type: "tick", ms: 600 }] },
    { id: "dismissed", label: "Dismissed", animated: true, steps: [
      { type: "action", name: "dismiss" }, { type: "tick", ms: 300 },
    ] },
  ],
  setup({ ui, theme, action }) {
    // Lifecycle: pending -> ready -> dismissed. No network or agent is run.
    let state: "pending" | "ready" | "dismissed" = "pending";
    ui.setEditorText("Continue reviewing the patch.");
    ui.setStatus("relay", theme.fg("muted", "checking imports…"));
    ui.setWidget("hint", [theme.fg("muted", "Background import check · keep typing")]);
    const resultTimer = setTimeout(() => {
      if (state !== "pending") return;
      state = "ready";
      ui.setStatus("relay", undefined);
      ui.setWidget("hint", undefined);
      ui.setWidget("relay", [theme.fg("success", "Import check · result ready"),
        theme.fg("text", "No unused imports in src/parser.ts."),
        theme.fg("muted", "Stays above editor until dismissed.")]);
    }, 700);
    action("dismiss", () => {
      state = "dismissed";
      clearTimeout(resultTimer);
      ui.setStatus("relay", undefined);
      ui.setWidget("relay", undefined);
      ui.setWidget("hint", [theme.fg("muted", "Import result dismissed · draft preserved")]);
    });
    return () => { state = "dismissed"; clearTimeout(resultTimer); };
  },
};
```

## Known uses: seen in Nico's repos

- [**dot314**](https://github.com/nicobailon/dot314): Keep asynchronous btw results in a replaceable widget
  - [extensions/btw/index.ts:536-598](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/btw/index.ts#L536-L598) @17cce138

## For agents: choose and check

Choose Result Relay when your job matches its intent and applicability above. Its neighbours in Status and widgets are listed below. Read the one whose intent fits your job more closely before you commit.

- [Keyed Slot](https://pi-tui.ratstack.sh/patterns/keyed-slot.md): Publish and clear compact state under one stable footer key.
- [Signal Pair](https://pi-tui.ratstack.sh/patterns/signal-pair.md): Pair a compact footer signal with a structured near-editor widget.
- [Widget Dock](https://pi-tui.ratstack.sh/patterns/widget-dock.md): Mount and replace auxiliary content under a stable widget key.
- [Notice Fuse](https://pi-tui.ratstack.sh/patterns/notice-fuse.md): Clear a one-off keyed notice after its display interval.
- [Work Caption](https://pi-tui.ratstack.sh/patterns/work-caption.md): Set task-specific text in Pi's active working indicator.

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

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

- [Signal Pair](https://pi-tui.ratstack.sh/patterns/signal-pair.md): pairs compact and detailed signals

## Linked from

- [Widget Dock](https://pi-tui.ratstack.sh/patterns/widget-dock.md): replaces pending content with a result
