The paintbrush for Claude. A visual output surface for AI agents — diagrams, tables, images, and interactive controls.
Project description
lux
A visual output surface for AI agents.
Lux gives AI agents a window they can draw into. It runs an ImGui display server on the local machine, connected by Unix socket IPC. Agents send JSON element trees via MCP tools; the display renders them at 60fps. The protocol is the API surface --- if an agent can describe it as JSON, Lux renders it.
The design follows Smalltalk's Morphic model: every visible element is a composable, nestable object. Windows contain tabs, tabs contain groups, groups contain buttons and plots. The long-term goal is a live environment where the MCP server is the message bus and Lux is the rendering layer, with the agent as the programmer at the keyboard.
Platforms: macOS, Linux
Stage: alpha --- protocol is stable, published on PyPI as punt-lux
Quick Start
curl -fsSL https://raw.githubusercontent.com/punt-labs/lux/1f3b83c/install.sh | sh
Restart Claude Code twice. The Lux display window opens automatically when agents send visual output.
Manual install (if you already have uv)
uv tool install punt-lux
Then install the plugin via the marketplace:
claude plugin marketplace add punt-labs/claude-plugins
claude plugin install lux@punt-labs
Verify before running
curl -fsSL https://raw.githubusercontent.com/punt-labs/lux/1f3b83c/install.sh -o install.sh
shasum -a 256 install.sh
cat install.sh
sh install.sh
Run a demo
lux display &
uv run python demos/dashboard.py
Demos are in demos/ --- each connects as a client and drives the display:
| Demo | What it shows |
|---|---|
interactive.py |
Sliders, checkboxes, combos, text inputs, color pickers |
containers.py |
Windows, tab bars, collapsing headers, groups |
dashboard.py |
Multi-window layout with draw canvases and live controls |
data_viz.py |
Tables, plots, progress bars, spinners, markdown |
menu_bar.py |
Custom menus, event handling, periodic refresh |
Features
- 22 element kinds --- text, buttons, images, sliders, checkboxes, combos, inputs, radios, color pickers, selectables, trees, tables, plots, progress bars, spinners, markdown, draw canvases, groups, tab bars, collapsing headers, windows, separators
- Layout nesting --- windows contain tab bars contain groups contain any element, arbitrarily deep
- Incremental updates ---
updatepatches individual elements by ID without replacing the scene - Menu bar --- built-in Lux/Theme/Window menus, plus agent-extensible custom menus via
set_menu - Interaction events --- button clicks, slider changes, menu selections queue as events the agent reads via
recv - Auto-spawn ---
LuxClientstarts the display server on first connection if it isn't running - Unix socket IPC --- length-prefixed JSON frames, no HTTP overhead, no threads
MCP Tools
Agents interact with Lux through six MCP tools exposed by lux serve:
| Tool | What it does |
|---|---|
show(scene_id, elements) |
Replace the display with a new element tree |
update(scene_id, patches) |
Patch elements by ID (set fields or remove) |
set_menu(menus) |
Add custom menus to the menu bar |
clear() |
Remove all content from the display |
ping() |
Round-trip latency check |
recv(timeout) |
Read the next interaction event (clicks, changes) |
What It Looks Like
Show text and a button
{"tool": "show", "input": {
"scene_id": "hello",
"elements": [
{"kind": "text", "id": "t1", "content": "Hello from the agent"},
{"kind": "button", "id": "b1", "label": "Click me"}
]
}}
Returns "ack:hello". When the user clicks the button:
{"tool": "recv", "input": {"timeout": 5.0}}
Returns "interaction:element=b1,action=click,value=True".
Multi-window dashboard
{"tool": "show", "input": {
"scene_id": "dash",
"elements": [
{"kind": "window", "id": "w1", "title": "Controls", "x": 10, "y": 10,
"children": [
{"kind": "slider", "id": "vol", "label": "Volume", "value": 50}
]},
{"kind": "window", "id": "w2", "title": "Chart", "x": 320, "y": 10,
"children": [
{"kind": "plot", "id": "p1", "title": "Trend",
"series": [{"label": "y", "type": "line",
"x": [1,2,3,4], "y": [10,20,15,25]}]}
]}
]
}}
Update a single element
{"tool": "update", "input": {
"scene_id": "dash",
"patches": [
{"id": "vol", "set": {"value": 75}}
]
}}
Element Kinds
| Category | Kinds |
|---|---|
| Display | text, button, image, separator |
| Interactive | slider, checkbox, combo, input_text, radio, color_picker |
| Lists | selectable, tree |
| Data | table, plot, progress, spinner, markdown |
| Canvas | draw (line, rect, circle, triangle, polyline, text, bezier) |
| Layout | group, tab_bar, collapsing_header, window |
All elements with an id support an optional tooltip field (string shown on hover).
CLI Commands
| Command | What it does |
|---|---|
lux display |
Start the display server (ImGui window) |
lux serve |
Start the MCP server (stdio transport) |
lux status |
Check if the display server is running |
lux version |
Print version |
Architecture
Agent (Claude Code)
│ MCP (stdio)
▼
lux serve (FastMCP)
│ Unix socket (JSON frames)
▼
lux display (ImGui + OpenGL)
│ renders at 60fps
▼
Window on screen
The display server and MCP server are separate processes. The MCP server is a thin adapter that translates MCP tool calls into protocol messages sent over the Unix socket. The display server runs the ImGui render loop, polls the socket each frame via select() with zero timeout, and renders whatever scene the agent last sent.
Client code can also use LuxClient directly as a Python library, bypassing MCP. The demos do this.
Documentation
Design Log | Changelog | Contributing
Development
uv sync --extra dev # Install dependencies
uv run ruff check . # Lint
uv run ruff format --check . # Check formatting
uv run mypy src/ tests/ # Type check (mypy)
uv run pyright # Type check (pyright)
uv run pytest # Test
Acknowledgements
Lux is a thin orchestration layer. The rendering is done by Dear ImGui, Omar Cornut's immediate-mode GUI library. ImGui handles all the hard problems --- text layout, widget state, input handling, GPU rendering --- and does so in a single-pass retained-mode-free architecture that maps naturally to Lux's "send JSON, render this frame" model. The 60fps render loop, the composable widget tree, and the ability to drive a full UI from a socket with no threading are all consequences of ImGui's design.
Python bindings come from imgui-bundle by Pascal Thomet, which packages ImGui, ImPlot, and several other ImGui extensions into a single pip-installable wheel with complete type stubs. imgui-bundle is what makes "install one Python package, get a GPU-accelerated UI" possible.
FastMCP provides the MCP server layer.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file punt_lux-0.5.2.tar.gz.
File metadata
- Download URL: punt_lux-0.5.2.tar.gz
- Upload date:
- Size: 45.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
00cfed22efe48c2434562493e4794c797bc839dcdb69ff4aeb5d2e6de780a349
|
|
| MD5 |
34029253a52a631ae20111b54153e024
|
|
| BLAKE2b-256 |
04dca1d7d901aab87b338756f7984b7632178a40ac55ddf5d10523d452cd07f3
|
Provenance
The following attestation bundles were made for punt_lux-0.5.2.tar.gz:
Publisher:
release.yml on punt-labs/lux
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
punt_lux-0.5.2.tar.gz -
Subject digest:
00cfed22efe48c2434562493e4794c797bc839dcdb69ff4aeb5d2e6de780a349 - Sigstore transparency entry: 1062358263
- Sigstore integration time:
-
Permalink:
punt-labs/lux@4c92b29686c7e37686dc83a6f14e69b37fa4d49b -
Branch / Tag:
refs/tags/v0.5.2 - Owner: https://github.com/punt-labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4c92b29686c7e37686dc83a6f14e69b37fa4d49b -
Trigger Event:
push
-
Statement type:
File details
Details for the file punt_lux-0.5.2-py3-none-any.whl.
File metadata
- Download URL: punt_lux-0.5.2-py3-none-any.whl
- Upload date:
- Size: 48.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b2398051e3940692805c094ab761a47e56f21d674ec92c6c75edfae44799fe35
|
|
| MD5 |
6e22ed77be145c3a86198525e7ebc2c0
|
|
| BLAKE2b-256 |
a2b45d6f3cb9d90e28bbc10c70b31bfc70dd405ae9ca3bcb9070e1d3691157f4
|
Provenance
The following attestation bundles were made for punt_lux-0.5.2-py3-none-any.whl:
Publisher:
release.yml on punt-labs/lux
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
punt_lux-0.5.2-py3-none-any.whl -
Subject digest:
b2398051e3940692805c094ab761a47e56f21d674ec92c6c75edfae44799fe35 - Sigstore transparency entry: 1062358317
- Sigstore integration time:
-
Permalink:
punt-labs/lux@4c92b29686c7e37686dc83a6f14e69b37fa4d49b -
Branch / Tag:
refs/tags/v0.5.2 - Owner: https://github.com/punt-labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4c92b29686c7e37686dc83a6f14e69b37fa4d49b -
Trigger Event:
push
-
Statement type: