Skip to content

REPL & Sandbox: structured “display” channel for rich outputs #30470

@rgbkrk

Description

@rgbkrk

Motivation

Right now the REPL and Sandbox only return strings. Two things are missing for notebook use cases:

  1. execute_result: the rich form of the final eval result (currently coerced to string).
  2. display: a way for sandboxed code to proactively send structured outputs back to the host, not just one final JS value.

Borrowing directly from the Deno Jupyter implementation, IPython, and the Jupyter protocol, I'd love a display() function injected into the sandbox. When user code calls display(obj), the host receives a message with { data: { "text/plain": "...", "image/png": "...", ... }, metadata }. The host decides how to render the text, image, plot, html, etc.

In practice, this is so that we can show:

  • Rich tables (text/html, application/vnd.dataresource+json)
  • Plots (image/png, application/vnd.vegalite.v5+json)
  • Interactive outputs

This lets notebooks and dashboards work with the same primitives, instead of bolting on custom stdout hacks.

The Deno Jupyter kernel has many of the building blocks with display(), format() / media bundle, and the$display symbol for rich outputs and library opt-in formatting. The difference here is transport method.

Proposal

JSON REPL

For the JSON REPL:

  • Update RunSuccess to optionally carry a MIME bundle (an ExecuteResult).
  • Add a new side-channel for Display and DisplayUpdate messages, emitted when user code calls display()
// Host <- REPL
type ReplMessage =
  | { type: "Run" | "RunSuccess" | "RunFailure" | "Error"; ...existing }
  | {
      type: "Display";
      id?: string;          // display_id for updates
      data: Record<string, unknown>;  // MIME bundle / Multimedia bundle
      metadata?: Record<string, unknown>;
      transient?: Record<string, unknown>; // lightweight hints
      buffers?: Uint8Array[]; // optional binary blobs (avoid base64 when possible)
    }
  | {
      type: "DisplayUpdate";
      id: string;
      data: Record<string, unknown>;
      metadata?: Record<string, unknown>;
      buffers?: Uint8Array[];
    };

Execution semantics

  • User code in the REPL can call display(value, opts?). This is separate from the normal return value.
  • The normal return still yields an ExecuteResult (rich MIME bundle form of the last expression).
  • display() posts a Display message immediately. If opts.display_id is provided, later calls with the same id emit DisplayUpdate.

Ideally, we'd prefer side-band binary for images (buffers) with media types like image/png, arrow, etc. Textual media stays JSON.

Host API

TBD for Sandbox, maybe something like this:

repl.on('display', callback)

Library ecosystem hook

To enable rich rendering without bespoke adapters, standardize on:

$display symbol (already used by Deno Jupyter) for objects to return their own media bundle.

A canonical format(obj, raw?) util that:

  • Checks $display
  • Handles common cases (HTML elements, SVG, canvas, images, Vega/Vega-Lite, DataFrames → application/vnd.dataresource+json + text/html)
  • Falls back to "text/plain" (stringifed inspect) or "application/json" if serializable

This mirrors Python’s _repr_html_ success (pandas popularized it) and lets libs opt-in to richer views without coupling to any front-end. Deno already has much of this code in cli/js/40_jupyter.js.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions