# ResponseStream

> Reveals text progressively, either typed out character by character or faded in word by word, from a string or an async stream of chunks.

## Facts

- **Product**: Crew OS
- **Family**: AI
- **Global**: `window.DigitalCrew.ResponseStream`
- **Source**: `digitalcrew-orchestrator: components/ui/response-stream.tsx`
- **Import in the app**: `import { ResponseStream } from "@/components/ui/response-stream";`
- **Live preview**: /design/components/response-stream/preview.html
- **Page**: /design/components/response-stream

## Guidelines

**Use it for** short AI answers and summaries that should feel live: an agent's reply summary, a generated subject line. For long markdown replies render [`Markdown`](/design/components/markdown.md) directly as the stream arrives.

**What you provide**: `textStream` (a string, or an `AsyncIterable<string>` whose chunks are appended as they arrive), `mode` (`typewriter` default, `fade`), `speed` 1–100 (default 20), `className`, `as` (element, default `div`), `onComplete`, and the overrides `characterChunkSize`, `segmentDelay`, `fadeDuration` (ms). `useTextStream` exposes the same engine with `pause`, `resume`, `reset`.

**Behaviour**: a string is revealed on animation frames. Typewriter adds 1 character per step below speed 25, otherwise `round((speed − 25) / 10)`; steps are at least `round(100 / √speed)` ms apart (22ms at the default). Fade splits the text into words with `Intl.Segmenter`; each word fades from 0 to 1 opacity over `round(1000 / √speed)` ms, ease-out (224ms at the default), staggered by the step delay. Changing `textStream` restarts from empty. There is no reduced-motion branch: when the user asks for less motion, pass a high `speed` or render the text directly.

**Don't** stream text the user must act on immediately (buttons, numbers in forms), or restart the stream on every parent render (keep the string stable).

## API

```ts
export type Mode = "typewriter" | "fade";
export type UseTextStreamOptions = {
    textStream: string | AsyncIterable<string>;
    speed?: number;
    mode?: Mode;
    onComplete?: () => void;
    fadeDuration?: number;
    segmentDelay?: number;
    characterChunkSize?: number;
    onError?: (error: unknown) => void;
};
export type UseTextStreamResult = {
    displayedText: string;
    isComplete: boolean;
    segments: {
        text: string;
        index: number;
    }[];
    getFadeDuration: () => number;
    getSegmentDelay: () => number;
    reset: () => void;
    startStreaming: () => void;
    pause: () => void;
    resume: () => void;
};
declare function useTextStream({ textStream, speed, mode, onComplete, fadeDuration, segmentDelay, characterChunkSize, onError, }: UseTextStreamOptions): UseTextStreamResult;
export type ResponseStreamProps = {
    textStream: string | AsyncIterable<string>;
    mode?: Mode;
    speed?: number;
    className?: string;
    onComplete?: () => void;
    as?: keyof React.JSX.IntrinsicElements;
    fadeDuration?: number;
    segmentDelay?: number;
    characterChunkSize?: number;
};
declare function ResponseStream({ textStream, mode, speed, className, onComplete, as, fadeDuration, segmentDelay, characterChunkSize, }: ResponseStreamProps): any;
export { useTextStream, ResponseStream };
```

## Example

```html
<div id="root" class="p-6"></div>
<script>
(function () {
  var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
  var text = "Claire Dubois replied to step 2 of Q3 CFO outreach. She is interested but wants to revisit after Northwind's budget review on 14 October, so I drafted a follow-up for the 15th and moved her to Nurture.";
  function Panel(p) {
    return h("div", { className: "grid content-start gap-2 rounded-xl border bg-card p-4" },
      h("div", { className: "flex items-center justify-between" },
        h("span", { className: "text-muted-foreground text-xs font-medium" }, p.label),
        h(DC.Button, { size: "sm", variant: "ghost", onClick: p.onReplay }, h(I.History), "Replay")),
      h("div", { className: "min-h-[120px] text-sm leading-relaxed" },
        h(DC.ResponseStream, { key: p.run, textStream: text, mode: p.mode, speed: 60, characterChunkSize: p.chunk })));
  }
  function App() {
    var a = React.useState(0), b = React.useState(0);
    return h("div", { className: "grid gap-4 sm:grid-cols-2" },
      h(Panel, { label: "mode=\"typewriter\"", mode: "typewriter", run: a[0], onReplay: function () { a[1](a[0] + 1); } }),
      h(Panel, { label: "mode=\"fade\"", mode: "fade", chunk: 4, run: b[0], onReplay: function () { b[1](b[0] + 1); } }));
  }
  ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>
```

## More in AI

- [Markdown](/design/components/markdown.md): Renders a markdown string (GitHub-flavoured: tables, task lists, strikethrough) and stays cheap while an AI reply streams in.
- [Reasoning](/design/components/reasoning.md): A collapsible block that shows how an agent reached its answer, with the reasoning streamed in as markdown.
