# How Streaming Output Works in Supported Terminals in the Summarize CLI

> Learn how the Summarize CLI streams output to supported terminals. Discover live markdown rendering and plain-text options for an enhanced user experience.

- Repository: [Peter Steinberger/summarize](https://github.com/steipete/summarize)
- Tags: internals
- Published: 2026-02-19

---

**The Summarize CLI detects rich TTY capabilities to enable incremental markdown-to-ANSI rendering with live slide parsing, falling back to plain-text streaming with configurable line or delta modes in non-interactive environments.**

The `steipete/summarize` repository provides a command-line interface that streams AI-generated summaries incrementally rather than waiting for complete responses. When running in a supported terminal, the tool leverages rich TTY features to render formatted markdown with colors and hyperlinks in real-time. Understanding how streaming output works in supported terminals helps developers customize the experience for both interactive and CI/CD environments.

## Detecting Rich TTY Capabilities

The foundation of streaming behavior rests in [`src/run/terminal.ts`](https://github.com/steipete/summarize/blob/main/src/run/terminal.ts), specifically the `isRichTty` function. This utility checks the `isTTY` property of the output stream to determine if the terminal supports interactive features like cursor positioning and color codes.

```typescript
// src/run/terminal.ts
export function isRichTty(stream: NodeJS.WritableStream): boolean {
  // Returns true only if the stream reports `isTTY`.
  return Boolean((stream as unknown as { isTTY?: boolean }).isTTY);
}

```

All downstream streaming logic checks `isRichTty(stdout)` to decide whether to render markdown as ANSI or fall back to plain-text streaming. This boolean also determines whether the CLI displays inline images and OSC-8 hyperlinks.

## Plain-Text Streaming for Non-TTY Environments

When running in pipes or CI environments where `isRichTty` returns false, or when the user specifies `--plain`, the CLI uses the stream-output gate implemented in [`src/run/stream-output.ts`](https://github.com/steipete/summarize/blob/main/src/run/stream-output.ts). The `createStreamOutputGate` function normalizes output through two distinct strategies:

- **Line mode**: Buffers content and flushes only when complete lines (ending in newlines) are received, preventing broken words in log files.
- **Delta mode**: Writes incremental fragments immediately as they arrive, creating a "live-typing" effect ideal for watching LLM generation in real-time.

```typescript
// src/run/stream-output.ts
export function createStreamOutputGate({
  stdout, clearProgressForStdout, restoreProgressAfterStdout, outputMode, richTty,
}) {
  // – `handleChunk(streamed, prevStreamed)` is called for every new chunk.
  // – In line mode it looks for the last newline and flushes up to that point.
  // – In delta mode it writes any appended text if the new chunk is a strict
  //   continuation of the previous one, otherwise it rewrites the whole buffer.
}

```

The gate tracks previously flushed content via `plainFlushedLen` and coordinates with progress spinners through `clearProgressForStdout` and `restoreProgressAfterStdout` callbacks to prevent UI corruption during concurrent updates.

## Markdown-to-ANSI Streaming in Rich Terminals

For rich TTY environments, the CLI creates a markdown streamer via `createMarkdownStreamer` in [`src/run/flows/url/slides-output.ts`](https://github.com/steipete/summarize/blob/main/src/run/flows/url/slides-output.ts). This component converts raw markdown into terminal-ready ANSI sequences using the **markdansi** library.

```typescript
// src/run/flows/url/slides-output.ts
const streamer = shouldRenderMarkdown
  ? createMarkdownStreamer({
      render: (markdown) =>
        renderMarkdownAnsi(
          prepareMarkdownForTerminalStreaming(markdown),
          {
            width: markdownRenderWidth(stdout, env),
            wrap: true,
            color: supportsColor(stdout, envForRun),
            hyperlinks: true,
          },
        ),
      spacing: "single",
    })
  : null;

```

The pipeline first calls `prepareMarkdownForTerminalStreaming` to trim whitespace and prevent "phantom blanks" when content reaches the exact terminal width. The `renderMarkdownAnsi` function then produces colored output respecting the terminal's actual width, color support level, and hyperlink capabilities.

## Parsing Slides During Stream Execution

When processing slide-based content (activated via `--slides`), the `createSlidesSummaryStreamHandler` manages the stream by parsing special markers in the incoming text. The handler buffers incoming chunks and scans for slide references using `slideTagRegex`, `slideLabelRegex`, and fallback patterns to detect markers like `[slide 1]` or `slide 1 – title`.

Upon detecting a marker, the handler invokes `renderSlide(index, title)`, which may:
- Display an inline image if `inlineEnabled` and the terminal supports image protocols (e.g., iTerm2 inline images).
- Write a formatted header line with timestamp links.
- Update progress counters via `onProgressText` callbacks.

This architecture ensures that slide boundaries are rendered immediately while preserving the incremental nature of the underlying LLM stream.

## Wiring the Pipeline Together

The orchestration happens in [`src/run/flows/url/summary.ts`](https://github.com/steipete/summarize/blob/main/src/run/flows/url/summary.ts), where `createSlidesTerminalOutput` assembles the appropriate streaming components based on flags and terminal capabilities.

```typescript
// src/run/flows/url/summary.ts (excerpt)
const slidesOutput = createSlidesTerminalOutput({
  io,
  flags,
  extracted,
  slides,
  enabled: flags.slides,
  outputMode: flags.outputMode,        // "line" | "delta"
  clearProgressForStdout,
  restoreProgressAfterStdout,
  onProgressText,
});

if (slidesOutput) {
  daemon.on("slideChunk", slidesOutput.onSlideChunk);
  daemon.on("slidesExtracted", slidesOutput.onSlidesExtracted);
  daemon.on("slidesDone", slidesOutput.onSlidesDone);

  await runSummaryEngine({
    ...,
    streamHandler: slidesOutput.streamHandler,
  });
}

```

The factory registers callbacks with the daemon to receive `slideChunk`, `slidesExtracted`, and `slidesDone` events, routing them through the streaming handler. If `flags.plain` is set or the terminal lacks TTY support, the factory automatically substitutes the plain-text output gate for the markdown streamer.

## Summary

- **Rich TTY detection** via `isRichTty` in [`src/run/terminal.ts`](https://github.com/steipete/summarize/blob/main/src/run/terminal.ts) determines whether to use markdown-to-ANSI rendering or plain-text streaming.
- **Plain-text streaming** employs `createStreamOutputGate` from [`src/run/stream-output.ts`](https://github.com/steipete/summarize/blob/main/src/run/stream-output.ts), supporting both "line" and "delta" output modes for different use cases.
- **Markdown streaming** uses the `markdansi` library through `createMarkdownStreamer`, respecting terminal width, color support, and hyperlink capabilities.
- **Slide parsing** handles `[slide …]` markers via regex patterns in the stream handler, enabling inline image rendering and progress updates.
- **Pipeline integration** in [`src/run/flows/url/summary.ts`](https://github.com/steipete/summarize/blob/main/src/run/flows/url/summary.ts) wires daemon events to the appropriate stream handler based on runtime flags and terminal detection.

## Frequently Asked Questions

### How does Summarize detect if my terminal supports streaming markdown?

The CLI calls `isRichTty(stdout)` from [`src/run/terminal.ts`](https://github.com/steipete/summarize/blob/main/src/run/terminal.ts), which checks the `isTTY` property of the output stream. When this returns true, the terminal supports rich features and the CLI enables markdown-to-ANSI conversion; otherwise, it falls back to plain-text streaming through the output gate.

### What is the difference between line mode and delta mode?

**Line mode** buffers output until complete newline-terminated lines are received, making it ideal for log files and CI pipelines where atomic lines prevent fragmented output. **Delta mode** writes incremental fragments immediately as they arrive from the LLM, creating a "live-typing" effect. Both modes are implemented in `createStreamOutputGate` within [`src/run/stream-output.ts`](https://github.com/steipete/summarize/blob/main/src/run/stream-output.ts).

### Can I disable ANSI formatting even in a supported terminal?

Yes. Passing the `--plain` flag forces the CLI to bypass markdown rendering and use the plain-text output gate, effectively disabling colors, hyperlinks, and inline images regardless of whether `isRichTty` returns true.

### How are slide markers parsed during streaming?

The `createSlidesSummaryStreamHandler` in [`src/run/flows/url/slides-output.ts`](https://github.com/steipete/summarize/blob/main/src/run/flows/url/slides-output.ts) uses regular expressions (`slideTagRegex`, `slideLabelRegex`) to detect markers like `[slide 1]` in the incoming character stream. When detected, the handler immediately renders the slide header and triggers inline image display if the terminal protocol supports it.