# Message Fold

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

Presentation / Tool and message output · `message-fold` · [HTML](https://pi-tui.ratstack.sh/patterns/message-fold/) · [JSON](https://pi-tui.ratstack.sh/patterns/message-fold.json) · [all patterns](https://pi-tui.ratstack.sh/patterns.md)

Also known as Collapsible message.

## Intent

Render custom message content as a preview with expanded detail.

## Motivation

Skill Palette injects context that needs an identifiable preview without filling the transcript.

## Applicability

- Use this when injected context or attachments should stay identifiable but compact.

## Structure

```text
custom message -> text blocks
text -> preview + count / full view
```

## Participants

- [`pi.registerMessageRenderer`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#state-management): Renders a named custom-message type.
- [`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.
- [`wrapTextWithAnsi`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model): Wraps text while preserving styling across lines.
- `Text preview`: Retains an identity label and omitted-line count when collapsed.

Pi component APIs: [`pi.registerMessageRenderer`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#state-management), [`Component`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), [`wrapTextWithAnsi`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), `CustomMessageComponent`, `MessageRenderer`

## Consequences

- Named message content can stay compact until expanded.
- Collapsed output omits lines and the extractor ignores non-text blocks.

## Implementation

- Keep a remaining-line count when the preview is clipped.
- The skill-context extractor omits non-text blocks.

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

### Preview

`collapsed`: Show a named synthetic context block and a remaining-line count.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Message Fold, Preview, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/message-fold/collapsed-40-dark.32a6f5ac9a36.webp) ![Message Fold, Preview, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/message-fold/collapsed-40-light.791fe8ccc3d2.webp)
- 60 columns: ![Message Fold, Preview, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/message-fold/collapsed-60-dark.2bf0e97bc816.webp) ![Message Fold, Preview, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/message-fold/collapsed-60-light.70064c0da1e0.webp)
- 80 columns: ![Message Fold, Preview, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/message-fold/collapsed-80-dark.de8bac3398f6.webp) ![Message Fold, Preview, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/message-fold/collapsed-80-light.83ca1c7aa095.webp)
- 120 columns: ![Message Fold, Preview, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/message-fold/collapsed-120-dark.2bc5f07bec9f.webp) ![Message Fold, Preview, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/message-fold/collapsed-120-light.39105cdb3f81.webp)

### Full message

`expanded`: Show its full text and synthetic attachment metadata.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Message Fold, Full message, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/message-fold/expanded-40-dark.bef084e028d5.webp) ![Message Fold, Full message, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/message-fold/expanded-40-light.dba4dd70f76c.webp)
- 60 columns: ![Message Fold, Full message, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/message-fold/expanded-60-dark.fe8015526f5c.webp) ![Message Fold, Full message, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/message-fold/expanded-60-light.ebfb26dd186a.webp)
- 80 columns: ![Message Fold, Full message, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/message-fold/expanded-80-dark.0a5a731911c5.webp) ![Message Fold, Full message, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/message-fold/expanded-80-light.8a68a4b09a44.webp)
- 120 columns: ![Message Fold, Full message, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/message-fold/expanded-120-dark.8726abd586f2.webp) ![Message Fold, Full message, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/message-fold/expanded-120-light.91d0a23fd160.webp)

### Non-text content

`non-text`: Show only supported text without fabricating content for a non-text block.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Message Fold, Non-text content, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/message-fold/non-text-40-dark.cd5c064af112.webp) ![Message Fold, Non-text content, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/message-fold/non-text-40-light.02e622442eaa.webp)
- 60 columns: ![Message Fold, Non-text content, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/message-fold/non-text-60-dark.d9b9064cc531.webp) ![Message Fold, Non-text content, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/message-fold/non-text-60-light.8113fa809861.webp)
- 80 columns: ![Message Fold, Non-text content, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/message-fold/non-text-80-dark.bc6174036990.webp) ![Message Fold, Non-text content, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/message-fold/non-text-80-light.b9c10008652a.webp)
- 120 columns: ![Message Fold, Non-text content, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/message-fold/non-text-120-dark.46d97cb16796.webp) ![Message Fold, Non-text content, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/message-fold/non-text-120-light.27d8f0477b2f.webp)

## Sample code

`stories/patterns/presentation/message-fold.ts`, the story the frames above were rendered from.

```ts
// Message Fold: keep injected context identifiable while hiding its longer body.
import { Container, wrapTextWithAnsi } from "@earendil-works/pi-tui";
import { CustomMessageComponent, type MessageRenderer } from "@earendil-works/pi-coding-agent";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "message-fold", title: "Message Fold", kind: "component",
  apis: ["CustomMessageComponent", "MessageRenderer", "wrapTextWithAnsi"], rows: 16,
  states: [
    { id: "collapsed", label: "Preview" },
    { id: "expanded", label: "Full message", steps: [{ type: "action", name: "expand" }] },
    { id: "non-text", label: "Non-text content", steps: [{ type: "action", name: "non-text" }] },
  ],
  setup({ theme, tui, action, clock }) {
    const renderer: MessageRenderer = (message, options) => {
      const details = message.details;
      const attachment = typeof details === "object" && details !== null &&
        "attachment" in details && typeof details.attachment === "string" ? details.attachment : undefined;
      const content = typeof message.content === "string" ? message.content :
        message.content.filter(block => block.type === "text").map(block => block.text).join("\n");
      return {
        invalidate() {},
        render(width) {
          const body = wrapTextWithAnsi(content, width);
          const preview = options.expanded ? body : body.slice(0, 2);
          const omitted = body.length - preview.length;
          return [
            theme.fg("customMessageLabel", "[review-context]"),
            ...preview.map(row => theme.fg("customMessageText", row)),
            ...(omitted ? [theme.fg("muted", "+" + omitted + " lines · expand to read")] : []),
            ...(options.expanded && attachment
              ? wrapTextWithAnsi("Attachment: " + attachment, width)
                .map(row => theme.fg("dim", row)) : []),
          ];
        },
      };
    };
    // This is the same callback pi.registerMessageRenderer accepts.
    const message: Parameters<typeof renderer>[0] = {
      role: "custom", customType: "review-context", display: true, timestamp: clock.now(),
      content: "Review the parser boundary.\nKeep the narrow preview readable.\nCheck both themes.\nRetain the completion summary.\nNo network calls in this fixture.",
      details: { attachment: "review-notes.txt · synthetic metadata" },
    };
    const slot = new Container();
    let view = new CustomMessageComponent(message, renderer);
    slot.addChild(view);
    action("expand", () => { view.setExpanded(true); tui.requestRender(); });
    action("non-text", () => {
      view = new CustomMessageComponent({ ...message, details: undefined, content: [
        { type: "text", text: "Supported text: inspect the preview." },
        { type: "image", data: "", mimeType: "image/png" },
      ] }, renderer);
      slot.children = [view]; tui.requestRender();
    });
    return slot;
  },
};
```

## Known uses: seen in Nico's repos

- [**pi-skill-palette**](https://github.com/nicobailon/pi-skill-palette): Render skill context as a collapsible message preview
  - [index.ts:850-887](https://github.com/nicobailon/pi-skill-palette/blob/a5c4429b8c2e33ab903d07856497014f3d5ad34e/index.ts#L850-L887) @a5c4429b
- [**pi-intercom**](https://github.com/nicobailon/pi-intercom): Cache message layout separately from live theme styling
  - [ui/inline-message.ts:10-125](https://github.com/nicobailon/pi-intercom/blob/a5fad4df2a9fe4909bf4d9b06263c8316976b57d/ui/inline-message.ts#L10-L125) @a5fad4df

## For agents: choose and check

Choose Message Fold when your job matches its intent and applicability above. Its neighbours in Tool and message output are listed below. Read the one whose intent fits your job more closely before you commit.

- [Detail Fold](https://pi-tui.ratstack.sh/patterns/detail-fold.md): Show compact progress and summaries with bounded expanded tool detail.
- [Call Capsule](https://pi-tui.ratstack.sh/patterns/call-capsule.md): Retain a display shell through call and result renders of one tool execution.
- [Hinge Diff](https://pi-tui.ratstack.sh/patterns/hinge-diff.md): Choose compact, unified or split diff presentation from available width.
- [Word Spotlight](https://pi-tui.ratstack.sh/patterns/word-spotlight.md): Emphasize changed words inside parsed patch lines.
- [Error Digest](https://pi-tui.ratstack.sh/patterns/error-digest.md): Project structured errors into compact and expanded diagnostic output.
- [Renderer Chain](https://pi-tui.ratstack.sh/patterns/renderer-chain.md): Add tool rendering while preserving an existing renderer or fallback.
- [Image Parachute](https://pi-tui.ratstack.sh/patterns/image-parachute.md): Render terminal images where supported and text placeholders otherwise.

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 [`pi.registerMessageRenderer`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#state-management), [`Component`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), [`wrapTextWithAnsi`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), `CustomMessageComponent`, `MessageRenderer` before you use them.
- Every reference frame gets the verdict its state expects.

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

- [Late Paint](https://pi-tui.ratstack.sh/patterns/late-paint.md): reuses wrapped message layout

## Linked from

No other pattern links here.
