Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ jobs:
- name: Lint check
run: pnpm lint

- name: Build workspace packages
run: pnpm --filter @runtimed/components build

- name: Type check
run: pnpm type-check

Expand Down
8 changes: 6 additions & 2 deletions Dockerfile.iframe-outputs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ WORKDIR /app
# Copy workspace files
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./

# Copy schema package.json (only workspace dependency needed for iframe)
# Copy workspace package.json files (needed for pnpm workspace resolution)
COPY packages/schema/package.json packages/schema/tsconfig.json ./packages/schema/
COPY packages/components/package.json packages/components/tsconfig.json ./packages/components/

# Install dependencies with cache mount
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
Expand All @@ -19,10 +20,13 @@ COPY packages/schema/src ./packages/schema/src
COPY src/components/ui ./src/components/ui
COPY src/lib ./src/lib
COPY src/util/iframe.ts ./src/util/iframe.ts
COPY src/components/outputs/shared-with-iframe ./src/components/outputs/shared-with-iframe
COPY packages/components ./packages/components
COPY iframe-outputs ./iframe-outputs
COPY vite.config.ts tsconfig.json tsconfig.node.json ./

# Build the components package first (required for iframe-outputs)
RUN pnpm --filter @runtimed/components build

# Build the iframe outputs
RUN pnpm build:iframe

Expand Down
4 changes: 4 additions & 0 deletions Dockerfile.sync
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ COPY packages/ai-core/package.json ./packages/ai-core/
COPY packages/pyodide-runtime/package.json ./packages/pyodide-runtime/
COPY packages/schema/package.json ./packages/schema/

# Remove @runtimed/components from dependencies (sync service doesn't need it)
# This allows pnpm install to succeed without the components package
RUN node -e "const pkg = require('./package.json'); delete pkg.dependencies['@runtimed/components']; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2));"

# Install dependencies
RUN pnpm install

Expand Down
1 change: 1 addition & 0 deletions Dockerfile.web
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ COPY packages/agent-core/package.json ./packages/agent-core/
COPY packages/ai-core/package.json ./packages/ai-core/
COPY packages/pyodide-runtime/package.json ./packages/pyodide-runtime/
COPY packages/schema/package.json ./packages/schema/
COPY packages/components/package.json ./packages/components/

# Install dependencies with cache mount
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
Expand Down
8 changes: 8 additions & 0 deletions ecosystem.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@
"env": {
"NODE_ENV": "development"
}
},
{
"name": "components",
"script": "pnpm",
"args": "--filter @runtimed/components dev",
"env": {
"NODE_ENV": "development"
}
}
]
}
7 changes: 3 additions & 4 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -354,11 +354,10 @@ export default [
},
{
ignores: [
"dist/**",
"node_modules/**",
"*.d.ts",
"**/dist/**",
"**/node_modules/**",
"**/*.d.ts",
"scripts/**",
"iframe-outputs/worker/dist/**",
"iframe-outputs/worker/.wrangler/**",
],
},
Expand Down
3 changes: 1 addition & 2 deletions iframe-outputs/src/react-main.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createRoot } from "react-dom/client";
import { IframeReactApp } from "./components/IframeReactApp";
import { IframeReactApp, sendFromIframe } from "@runtimed/components";
import "./style.css";
import { sendFromIframe } from "@/components/outputs/shared-with-iframe/comms";

// Main React initialization for iframe outputs
function initializeReactIframe() {
Expand Down
2 changes: 1 addition & 1 deletion iframe-outputs/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Docs: https://tailwindcss.com/docs/preflight#overview

@import "tw-animate-css";
@plugin "@tailwindcss/typography";
@source "../../src/components/outputs/shared-with-iframe";
@source "../../packages/components/src";
@source "../../src/components/ui";

:root {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"@react-spring/web": "^10.0.1",
"@runtimed/agent-core": "workspace:*",
"@runtimed/ai-core": "workspace:*",
"@runtimed/components": "workspace:*",
"@runtimed/pyodide-runtime": "workspace:*",
"@runtimed/schema": "workspace:*",
"@tanstack/react-query": "^5.85.5",
Expand Down
177 changes: 177 additions & 0 deletions packages/components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# @runtimed/components

React components for rendering notebook cell outputs. This package provides a comprehensive set of output renderers for displaying code execution results, including rich multimedia formats, terminal output, AI tool interactions, and geographic data.

## Installation

```bash
pnpm add @runtimed/components
# or
npm install @runtimed/components
```

**Peer Dependencies**: React 19+

## Quick Start

```tsx
import { SingleOutput, OutputsContainer } from "@runtimed/components";
import "@runtimed/components/styles.css";

function NotebookOutputs({ outputs }) {
return (
<OutputsContainer>
{outputs.map((output) => (
<SingleOutput key={output.id} output={output} />
))}
</OutputsContainer>
);
}
```

## Components

### Output Renderers

| Component | Description |
| ------------------- | ----------------------------------------------------------------------- |
| `SingleOutput` | Smart router that selects the appropriate renderer based on output type |
| `OutputsContainer` | Wrapper for consistent output styling |
| `RichOutputContent` | Renders multimedia content by MIME type |

### Specific Renderers

| Component | MIME Types |
| ------------------ | ------------------------------------------------------ |
| `PlainTextOutput` | `text/plain` |
| `MarkdownRenderer` | `text/markdown` - GFM, KaTeX math, syntax highlighting |
| `HtmlOutput` | `text/html` |
| `JsonOutput` | `application/json` - interactive tree view |
| `ImageOutput` | `image/png`, `image/jpeg`, `image/gif`, `image/webp` |
| `SvgOutput` | `image/svg+xml` |
| `AnsiOutput` | Terminal output with ANSI color codes |
| `GeoJsonMapOutput` | `application/geo+json` - MapLibre-powered maps |

### AI Tool Components

| Component | Purpose |
| ---------------------- | ------------------------------------------- |
| `AiToolCallOutput` | Displays AI tool invocation details |
| `AiToolResultOutput` | Renders tool execution results |
| `AiToolApprovalOutput` | UI for human-in-the-loop approval workflows |

### Iframe Integration

For sandboxed output rendering:

```tsx
import { IframeReactApp, IframeOutput } from "@runtimed/components";

// Parent: embed outputs in an iframe
<IframeOutput outputs={cellOutputs} />

// Child iframe: render the app
<IframeReactApp />
```

Communication utilities:

```tsx
import {
useIframeCommsParent,
useIframeCommsChild,
sendToIframe,
sendFromIframe,
} from "@runtimed/components";
```

### UI Components

Basic UI building blocks:

```tsx
import { Button, Card, Spinner } from "@runtimed/components";

<Button variant="outline" size="sm">Click me</Button>
<Spinner size="md" />
```

## Utilities

```tsx
import { cn, groupConsecutiveStreamOutputs } from "@runtimed/components";

// Merge Tailwind classes
cn("px-2 py-1", condition && "bg-blue-500");

// Group stdout/stderr streams for cleaner display
const grouped = groupConsecutiveStreamOutputs(outputs);
```

## Styling

Import the CSS for proper styling:

```tsx
import "@runtimed/components/styles.css";
```

The package uses Tailwind CSS v4. Components are designed to work in both light and dark themes.

## Features

- **Lazy loading**: Heavy components like `MarkdownRenderer` are dynamically imported
- **Error boundaries**: Outputs gracefully handle rendering failures
- **Artifact support**: Handles both inline data and artifact URLs for large outputs
- **Suspense-ready**: Built-in loading states with `SuspenseSpinner`

## Output Data Format

Components expect outputs conforming to `@runtimed/schema` types:

```typescript
import type { OutputData, OutputType } from "@runtimed/components";

interface OutputData {
id: string;
outputType:
| "multimedia_display"
| "multimedia_result"
| "terminal"
| "markdown"
| "error";
data?: string | null;
representations?: Record<string, MediaContainer>;
streamName?: "stdout" | "stderr";
}
```

## Development

```bash
# Build
pnpm build

# Watch mode
pnpm dev

# Type check
pnpm type-check

# Lint
pnpm lint
```

## Demo

The package includes a demo page for testing all output types:

```tsx
import { OutputTypesDemoPage } from "@runtimed/components";

<OutputTypesDemoPage iframeUri="localhost:8000" />;
```

## License

MIT
6 changes: 6 additions & 0 deletions packages/components/jsr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@runtimed/components",
"version": "0.3.0-beta.1",
"exports": "./src/index.ts",
"license": "BSD-3-Clause"
}
96 changes: 96 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{
"name": "@runtimed/components",
"version": "0.3.0-beta.1",
"description": "React components for rendering notebook cell outputs",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"source": "src/index.ts",
"files": [
"dist",
"src"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"default": "./dist/index.mjs"
},
"./styles.css": "./dist/styles.css"
},
"scripts": {
"type-check": "tsc --noEmit --allowImportingTsExtensions",
"lint": "eslint src/",
"lint:check": "eslint src/ --max-warnings 0",
"format": "prettier --write .",
"format:check": "prettier --check .",
"build": "tsdown; pnpm build:css",
"build:css": "tailwindcss -i src/styles.css -o dist/styles.css --minify",
"dev": "tsdown --watch & tailwindcss -i src/styles.css -o dist/styles.css --watch",
"prepublishOnly": "pnpm build"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"dependencies": {
"@uiw/react-json-view": "2.0.0-alpha.40",
"react-error-boundary": "^6.0.0",
"@radix-ui/react-slot": "^1.2.3",
"@runtimed/schema": "workspace:*",
"ansi-to-react": "^6.1.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"geojson-map-fit-mercator": "^1.1.0",
"katex": "^0.16.22",
"lucide-react": "^0.545.0",
"maplibre-gl": "^5.7.1",
"maplibre-react-components": "^0.2.6",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^15.6.1",
"react-use": "^17.6.0",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@tailwindcss/cli": "^4.1.10",
"@types/node": "^24.0.10",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-syntax-highlighter": "^15.5.13",
"@typescript-eslint/eslint-plugin": "^8.35.1",
"@typescript-eslint/parser": "^8.35.1",
"eslint": "^9.30.1",
"prettier": "^3.6.0",
"react": "19.2.1",
"react-dom": "19.2.1",
"tsdown": "0.20.0-beta.1",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=23.0.0",
"pnpm": ">=10.9.0"
},
"keywords": [
"react",
"components",
"notebook",
"outputs",
"runt",
"anode"
],
"repository": {
"type": "git",
"url": "git+https://github.com/runtimed/anode.git",
"directory": "packages/components"
},
"publishConfig": {
"access": "public"
},
"author": "Runt Team",
"license": "MIT"
}
6 changes: 6 additions & 0 deletions packages/components/src/Incrementor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useState } from "react";

export function Incrementor() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count is {count}</button>;
}
Loading
Loading