# Retry Buffer

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

Also known as Retryable draft.

## Intent

Keep the local draft editable after a failed send.

## Motivation

Intercom's compose overlay can fail to send after the user has already typed a message.

## Applicability

- Use this when a compact compose view submits asynchronously.

## Structure

```text
draft -> sending
failure -> draft + error
retry -> sending
```

## 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.
- [`Input`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#compose-built-in-components): Owns single-line editing and cursor state.
- [`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.
- [`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.
- `Send state`: Prevents duplicate submission while retaining text on failure.

Pi component 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), [`Input`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#compose-built-in-components), [`KeybindingsManager.matches`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), [`TUI.requestRender`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), `Focusable`

## Consequences

- A failed send leaves the draft available for retry.
- Blank text, duplicate sends and terminal escape sequences need separate guards.

## Implementation

- Reject blank drafts and duplicate sends.
- Ignore raw escape sequences as text input.

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

### Draft

`editing`: Show a synthetic compose draft.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Retry Buffer, Draft, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/retry-buffer/editing-40-dark.dcd2aac68f37.webp) ![Retry Buffer, Draft, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/retry-buffer/editing-40-light.ef2be805034e.webp)
- 60 columns: ![Retry Buffer, Draft, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/retry-buffer/editing-60-dark.a82ee64d832a.webp) ![Retry Buffer, Draft, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/retry-buffer/editing-60-light.d92987f7b758.webp)
- 80 columns: ![Retry Buffer, Draft, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/retry-buffer/editing-80-dark.0344b0be2a3a.webp) ![Retry Buffer, Draft, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/retry-buffer/editing-80-light.da362a3d13bd.webp)
- 120 columns: ![Retry Buffer, Draft, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/retry-buffer/editing-120-dark.e55da5ccbc45.webp) ![Retry Buffer, Draft, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/retry-buffer/editing-120-light.1a04233467cb.webp)

### Send pending

`sending`: Disable duplicate submission while the send is pending.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Retry Buffer, Send pending, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/retry-buffer/sending-40-dark.f1e236dbd2af.webp) ![Retry Buffer, Send pending, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/retry-buffer/sending-40-light.bfc6c2238f49.webp)
- 60 columns: ![Retry Buffer, Send pending, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/retry-buffer/sending-60-dark.883833d8da20.webp) ![Retry Buffer, Send pending, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/retry-buffer/sending-60-light.b5539cb5f5db.webp)
- 80 columns: ![Retry Buffer, Send pending, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/retry-buffer/sending-80-dark.e190610856fe.webp) ![Retry Buffer, Send pending, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/retry-buffer/sending-80-light.93969848124e.webp)
- 120 columns: ![Retry Buffer, Send pending, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/retry-buffer/sending-120-dark.05d79e147c1f.webp) ![Retry Buffer, Send pending, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/retry-buffer/sending-120-light.1d9c698ca7e6.webp)

### Retry available

`failed`: Show an inline error with the unchanged draft ready to retry.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Retry Buffer, Retry available, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/retry-buffer/failed-40-dark.1d1c8ae57fe5.webp) ![Retry Buffer, Retry available, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/retry-buffer/failed-40-light.d69e698d3c08.webp)
- 60 columns: ![Retry Buffer, Retry available, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/retry-buffer/failed-60-dark.4525b469896f.webp) ![Retry Buffer, Retry available, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/retry-buffer/failed-60-light.9d92d47af7cd.webp)
- 80 columns: ![Retry Buffer, Retry available, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/retry-buffer/failed-80-dark.3f7d37783567.webp) ![Retry Buffer, Retry available, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/retry-buffer/failed-80-light.fc08ccc37169.webp)
- 120 columns: ![Retry Buffer, Retry available, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/retry-buffer/failed-120-dark.7ea90d27f9cc.webp) ![Retry Buffer, Retry available, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/retry-buffer/failed-120-light.86028d7c7ea0.webp)

## Sample code

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

```ts
// Retry Buffer: keep an editable draft when an asynchronous send fails.
// Machine: editing/failed --submit--> sending --failure--> failed.
import { Input, truncateToWidth, type Component, type Focusable } from "@earendil-works/pi-tui";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "retry-buffer",
  title: "Retry Buffer",
  kind: "component",
  apis: ["Input", "Focusable", "KeybindingsManager.matches", "TUI.requestRender"],
  rows: 8,
  states: [
    { id: "editing", label: "Draft" },
    { id: "sending", label: "Send pending", steps: [
      { type: "keys", data: "\r" }, { type: "keys", data: "\r" },
    ] },
    { id: "failed", label: "Retry available", steps: [{ type: "wait", ms: 600 }] },
  ],
  setup({ theme, tui, keybindings }) {
    const draft = new Input({ prompt: "> " });
    draft.setValue("Please review the parser patch");
    let sendState: "editing" | "sending" | "failed" = "editing";
    let attempts = 0;
    let pending: ReturnType<typeof setTimeout> | undefined;
    class ComposeBuffer implements Component, Focusable {
      get focused() { return draft.focused; }
      set focused(value: boolean) { draft.focused = value; }
      invalidate() { draft.invalidate(); }
      dispose() { clearTimeout(pending); }
      handleInput(data: string) {
        if (keybindings.matches(data, "tui.input.submit")) {
          if (sendState === "sending" || !draft.getValue().trim()) return;
          sendState = "sending";
          attempts++;
          // Synthetic send driver: no process, network or file access.
          pending = setTimeout(() => { sendState = "failed"; tui.requestRender(); }, 600);
        } else if (sendState !== "sending") draft.handleInput(data);
        tui.requestRender();
      }
      render(width: number) {
        return [
          theme.fg("accent", "Retry Buffer · compose message"),
          theme.fg("muted", "To: Review session"),
          ...draft.render(width),
          theme.fg(sendState === "failed" ? "error" : "muted", truncateToWidth(
            sendState === "failed" ? "Send failed · destination unavailable" :
              sendState === "sending" ? "Sending · input locked until result" : "Local draft · not sent", width)),
          theme.fg("text", "Send attempts: " + attempts),
          theme.fg("muted", truncateToWidth(sendState === "sending" ?
            "Enter ignored · no duplicate send" : "Enter " + (sendState === "failed" ? "retry · edit the retained draft" : "send · blank drafts stay local"), width)),
        ];
      }
    }
    return new ComposeBuffer();
  },
};
```

## Known uses: seen in Nico's repos

- [**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 Retry Buffer when your job matches its intent and applicability above. Its neighbours in Editors and drafts are listed below. Read the one whose intent fits your job more closely before you commit.

- [Editor Steward](https://pi-tui.ratstack.sh/patterns/editor-steward.md): Compose editor enhancements behind one replacement factory.
- [Draft Fence](https://pi-tui.ratstack.sh/patterns/draft-fence.md): Keep inline draft edits separate from the committed queue snapshot.
- [Prompt Trail](https://pi-tui.ratstack.sh/patterns/prompt-trail.md): Browse submitted prompts while retaining the current editor draft.
- [Draft Return](https://pi-tui.ratstack.sh/patterns/draft-return.md): Restore the captured editor draft after a temporary submission.

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 [`ctx.ui.custom`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#use-custom-screens-and-overlays), [`Input`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#compose-built-in-components), [`KeybindingsManager.matches`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#handle-keyboard-input-and-focus), [`TUI.requestRender`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), `Focusable` before you use them.
- Every reference frame gets the verdict its state expects.

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

- [Draft Return](https://pi-tui.ratstack.sh/patterns/draft-return.md): restores text after temporary submission

## Linked from

- [Draft Fence](https://pi-tui.ratstack.sh/patterns/draft-fence.md): keeps unsubmitted text local
