# Word Spotlight

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

Also known as Intraline diff.

## Intent

Emphasize changed words inside parsed patch lines.

## Motivation

Dot314's RepoPrompt CLI parser highlights small word changes inside patch lines.

## Applicability

- Use this when line-level changes hide a small textual edit.

## Structure

```text
patch lines -> word comparison
comparison -> styled changed spans
```

## Participants

- [`ToolDefinition.renderResult`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#tool-rendering): Renders partial or final tool output.
- [`theme.style`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#apply-themes-correctly): Styles text through semantic or concrete colours.
- [`truncateToWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model): Clips text to its allotted columns and can pad the result.
- `Word comparison`: Identifies changed spans before terminal styling.

Pi component APIs: [`ToolDefinition.renderResult`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#tool-rendering), [`theme.style`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#apply-themes-correctly), [`truncateToWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), `ToolExecutionComponent`

## Consequences

- Small edits remain identifiable within changed lines.
- Incomplete or non-diff blocks need a plain fallback.

## Implementation

- Keep parsing separate from terminal styling.
- Provide a fallback for incomplete or non-diff 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.

### Changed words

`changed`: Show a synthetic patch with changed-word emphasis.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Word Spotlight, Changed words, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/word-spotlight/changed-40-dark.1968e898c3c4.webp) ![Word Spotlight, Changed words, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/word-spotlight/changed-40-light.0cf534befb49.webp)
- 60 columns: ![Word Spotlight, Changed words, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/word-spotlight/changed-60-dark.498f87c53197.webp) ![Word Spotlight, Changed words, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/word-spotlight/changed-60-light.b61a89243001.webp)
- 80 columns: ![Word Spotlight, Changed words, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/word-spotlight/changed-80-dark.b322a2c16949.webp) ![Word Spotlight, Changed words, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/word-spotlight/changed-80-light.abb8b93d3374.webp)
- 120 columns: ![Word Spotlight, Changed words, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/word-spotlight/changed-120-dark.56516615c0ae.webp) ![Word Spotlight, Changed words, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/word-spotlight/changed-120-light.41c4bf49a056.webp)

### Incomplete patch

`incomplete`: Show a plain fallback while the patch is incomplete.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Word Spotlight, Incomplete patch, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/word-spotlight/incomplete-40-dark.8b4fd20a17d4.webp) ![Word Spotlight, Incomplete patch, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/word-spotlight/incomplete-40-light.d973ab3ac113.webp)
- 60 columns: ![Word Spotlight, Incomplete patch, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/word-spotlight/incomplete-60-dark.dfccaa4be1bc.webp) ![Word Spotlight, Incomplete patch, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/word-spotlight/incomplete-60-light.63b18ec1124d.webp)
- 80 columns: ![Word Spotlight, Incomplete patch, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/word-spotlight/incomplete-80-dark.508c282fbca6.webp) ![Word Spotlight, Incomplete patch, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/word-spotlight/incomplete-80-light.907121d001c4.webp)
- 120 columns: ![Word Spotlight, Incomplete patch, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/word-spotlight/incomplete-120-dark.897087b7e920.webp) ![Word Spotlight, Incomplete patch, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/word-spotlight/incomplete-120-light.adedfe9c6484.webp)

### Non-diff output

`plain`: Render a non-diff block without pretending it is a patch.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Word Spotlight, Non-diff output, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/word-spotlight/plain-40-dark.992a673bb2a7.webp) ![Word Spotlight, Non-diff output, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/word-spotlight/plain-40-light.3977979404dd.webp)
- 60 columns: ![Word Spotlight, Non-diff output, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/word-spotlight/plain-60-dark.badf189423da.webp) ![Word Spotlight, Non-diff output, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/word-spotlight/plain-60-light.9c8d76186813.webp)
- 80 columns: ![Word Spotlight, Non-diff output, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/word-spotlight/plain-80-dark.b4be8857fcbc.webp) ![Word Spotlight, Non-diff output, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/word-spotlight/plain-80-light.a6824ff3a68b.webp)
- 120 columns: ![Word Spotlight, Non-diff output, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/word-spotlight/plain-120-dark.a0855cc589d0.webp) ![Word Spotlight, Non-diff output, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/word-spotlight/plain-120-light.bf9d2c022b04.webp)

## Sample code

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

```ts
// Word Spotlight: parse changed spans first, then emphasize only edited words.
import { Text, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
import { ToolExecutionComponent, type ToolRenderers } from "@earendil-works/pi-coding-agent";
import type { PatternStory } from "../../../src/pattern.ts";

function compareWords(before: string, after: string) {
  const left = before.split(" "), right = after.split(" ");
  let prefix = 0, suffix = 0;
  while (prefix < left.length && prefix < right.length && left[prefix] === right[prefix]) prefix++;
  while (suffix < left.length - prefix && suffix < right.length - prefix &&
    left[left.length - 1 - suffix] === right[right.length - 1 - suffix]) suffix++;
  return {
    before: { words: left, start: prefix, end: left.length - suffix },
    after: { words: right, start: prefix, end: right.length - suffix },
  };
}
export const story: PatternStory = {
  id: "word-spotlight", title: "Word Spotlight", kind: "component",
  apis: ["ToolExecutionComponent", "ToolDefinition.renderResult", "theme.style", "truncateToWidth"], rows: 12,
  states: [
    { id: "changed", label: "Changed words" },
    { id: "incomplete", label: "Incomplete patch", steps: [{ type: "action", name: "incomplete" }] },
    { id: "plain", label: "Non-diff output", steps: [{ type: "action", name: "plain" }] },
  ],
  setup({ theme, tui, action }) {
    // The fixture parser deliberately accepts only a complete two-line patch.
    let output = "- keep four preview rows\n+ keep eight preview rows";
    const renderers: ToolRenderers = {
      renderCall: () => new Text(theme.fg("toolTitle", "patch_preview · preview limit"), 0, 0),
      renderResult() {
        const [before, after, extra] = output.split("\n");
        const comparison = before?.startsWith("- ") && after?.startsWith("+ ") && extra === undefined
          ? compareWords(before.slice(2), after.slice(2)) : undefined;
        return {
          invalidate() {},
          render(width) {
            if (!comparison) return wrapTextWithAnsi(output, width).map(row => theme.fg("toolOutput", row));
            const highlight = (side: typeof comparison.before, sign: string) => {
              const content = side.words.map((word, index) => index >= side.start && index < side.end
                ? theme.style(word, { fg: sign === "-" ? "toolDiffRemoved" : "toolDiffAdded",
                    bold: true, underline: true })
                : theme.fg("muted", word)).join(" ");
              return truncateToWidth(theme.fg(sign === "-" ? "toolDiffRemoved" : "toolDiffAdded", sign + " ") +
                content, width);
            };
            return [highlight(comparison.before, "-"), highlight(comparison.after, "+"),
              theme.fg("dim", "Only changed words are emphasized")];
          },
        };
      },
    };
    const tool = new ToolExecutionComponent("patch_preview", "example-patch-1", {},
      undefined, renderers, tui, "/workspace/example");
    tool.updateResult({ content: [], isError: false }, false);
    action("incomplete", () => {
      output = "- keep four preview rows\nPatch still arriving…";
      tool.updateResult({ content: [], isError: false }, true); tui.requestRender();
    });
    action("plain", () => {
      output = "Preview build complete. No patch supplied.";
      tool.updateResult({ content: [], isError: false }, false); tui.requestRender();
    });
    return tool;
  },
};
```

## Known uses: seen in Nico's repos

- [**dot314**](https://github.com/nicobailon/dot314): Render patch output with compact and intra-line diff treatments
  - [extensions/repoprompt-cli/index.ts:1020-1085](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/repoprompt-cli/index.ts#L1020-L1085) @17cce138

## For agents: choose and check

Choose Word Spotlight 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.
- [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.
- [Message Fold](https://pi-tui.ratstack.sh/patterns/message-fold.md): Render custom message content as a preview with expanded detail.
- [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 [`ToolDefinition.renderResult`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#tool-rendering), [`theme.style`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#apply-themes-correctly), [`truncateToWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), `ToolExecutionComponent` before you use them.
- Every reference frame gets the verdict its state expects.

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

- [Hinge Diff](https://pi-tui.ratstack.sh/patterns/hinge-diff.md): chooses the enclosing diff layout

## Linked from

No other pattern links here.
