Tooling reference¶
Three tools ship alongside lumenc: an in-window devtools overlay, a language
server for editors, and an MCP server that lets an agent inspect and drive a
running app.
Devtools¶
A panel docked to the right edge of the window, showing the live element tree, signal values, and captured network requests. While it is open the app reflows into the remaining width, like a browser's docked devtools; it never covers the app.
Availability¶
Every default lumenc build carries the overlay; a
--no-default-features compiler build drops it with the rest of the
runtime. It mounts only for a run from source. An app started from a
precompiled .lmna artifact has no overlay, and packaged apps never
contain it.
Opening it¶
| Trigger | Effect |
|---|---|
F12 |
Toggles the overlay. |
LUMEN_DEVTOOLS_OPEN |
Any value other than empty or 0 opens the overlay at startup. |
There is no config key and no CLI flag.
Panels¶
Switch panels by clicking a tab. There is no keyboard shortcut for switching.
| Tab | Contents |
|---|---|
| Elements | Live element tree, one row per entity, syntax-colored the way a browser colors markup: tag, #id.class, [WxH], and :hover/:focus/:press state each in their own color. Hovering a row overlays that element in the app with a tinted box and a <tag>#id WxH chip; clicking selects it and opens the inspect pane. The panel's own entities are excluded. Capped at 400 rows. |
| Signals | Frame number, tick time, entity count, then one name = value (kind) row per global signal. |
| Network | HTTP exchanges captured from script fetch() calls, oldest first: status, method, URL, and tag. Holds the last 128. |
The Pick button arms hover-to-inspect on the app itself: the element under the pointer is overlaid, and clicking it selects it in the tree. The click that picks still reaches the app.
Editing the running app¶
The inspect pane under the tree shows the selected element's box, layout style, fill, and text facts, and edits the element in place:
| Control | Effect |
|---|---|
| text field + Apply | Replaces the element's text content and re-lays it out. Only elements that already show text take the edit. |
| Hide | Toggles the element's visibility. |
| Delete | Despawns the element and its subtree. |
Edits change the live world only - source files are untouched, and a hot-reload or a script that rewrites the same element overwrites them.
The Elements panel needs the snapshot pipeline, which the introspection server provides. With that server off the panel says so instead of showing a tree.
Language server¶
lumen-lsp provides completion, diagnostics, navigation, and formatting for
Lumen projects.
Running it¶
The binary is lumen-lsp. It speaks LSP over stdio, takes no arguments, and
logs to stderr. Point any LSP-capable editor at it as a stdio server.
It treats files by extension: .lmn as markup, .css as stylesheets, and
.rhai as script. It discovers the rest of a project from the sibling files in
the same directory, an app's src/, preferring main.lmn, main.css, and
main.rhai, which is what lets it resolve an id in a script back to the
element that defines it.
What it provides¶
| Feature | Markup (.lmn) |
CSS (.css) |
Script (.rhai) |
|---|---|---|---|
| Diagnostics | Parse errors and every lint finding, with severity | Parse errors and cascade warnings, ranged at the property | Script compile errors |
| Completion | Tag names, attribute names, and constrained values for the attributes that take a fixed set | none | Builtin functions with signature, documentation, and snippet; element ids inside an id argument |
| Signature help | none | none | Builtin signatures |
| Hover | Documentation for the tag or attribute under the cursor | none | Documentation for the builtin under the cursor |
| Go to definition | A tag resolves to its <template name="..."> in the same file |
none | An id string resolves to the element that defines it, across files |
| Find references | An id resolves to every use across markup, CSS, and script | #id selectors participate |
id arguments participate |
| Rename | Rewrites the id in all three file kinds | same | same |
| Document symbols | Elements | none | Functions |
| Formatting | Whole-document reformat | none | none |
Completion triggers on <, space, ", and .; signature help on ( and
,.
The markup, CSS, and cross-file id features do not depend on a script language.
The .rhai column is the lang-rhai build feature, which is on by default; a
server built without it keeps every other column and answers script requests
with nothing. candela (.cdl) and Lua (.lua) buffers have no intelligence
yet.
VS Code extension¶
The extension lives in tools/vscode-lumen. Build it with npm install &&
npm run compile, package it with vsce package, and install the resulting
.vsix.
It activates on a .lmn, .rhai, or Lumen CSS file, and on any workspace
containing a .lmn file or a lumen.toml. It contributes syntax highlighting
for markup (with embedded script and CSS), for Lumen CSS, and for Rhai. A
.css file is treated as Lumen CSS when it sits in an app's src/, beside the
markup.
Commands, all under the Lumen category:
| Command | Action |
|---|---|
| Run App | lumenc run on the workspace app |
| Check (parse gate) | lumenc check |
| Format Markup File | Formats the active .lmn |
| Build AOT Artifact (.lmna) | lumenc build |
| New App from Template | Scaffolds with lumenc new |
| Open Live Preview | Headless run plus screenshot, shown in a panel |
| Restart Language Server | Restarts lumen-lsp |
Open Live Preview is bound to Ctrl+Shift+V (Cmd+Shift+V on macOS) in a
markup file. It drives the app through the introspection server, so the app
needs [mcp] simulate = true in lumen.toml; without it the preview has
nothing to capture.
Settings:
| Setting | Default | Effect |
|---|---|---|
lumen.serverPath |
unset | Explicit path to lumen-lsp. |
lumen.lumencPath |
unset | Explicit path to lumenc. |
lumen.serverAutoDiscover |
true |
Searches for a built server before falling back to PATH. |
lumen.run.flags |
[] |
Extra flags passed to lumenc run. |
lumen.run.headless |
false |
Runs the app headless. |
lumen.preview.size |
"960x720" |
Preview viewport. |
lumen.preview.dpr |
1 |
Preview device pixel ratio. |
lumen.trace.server |
"off" |
LSP trace verbosity: off, messages, or verbose. |
With auto-discovery on, the extension looks for the server in
$CARGO_TARGET_DIR, then in each workspace folder's target/ directory
(release before debug), then for lumen-lsp on PATH.
JetBrains plugin¶
The plugin is on the
JetBrains Marketplace.
It also lives in tools/jetbrains-lumen: build it with ./gradlew
buildPlugin, then install the zip from build/distributions/ through
Settings | Plugins | Install Plugin from Disk. It works in every IntelliJ-based
IDE from 2024.2 on, Community editions included.
It needs LSP4IJ, the LSP
client it talks to lumen-lsp through. Install that from the Marketplace
first. On IntelliJ 2026.2 and later, LSP4IJ 0.21.0 or newer is required;
earlier LSP4IJ builds do not load there.
The plugin highlights .lmn (with embedded script and CSS) and .rhai using
the same TextMate grammars as the VS Code extension. Every other feature is the
server's: diagnostics, completion, hover, signature help, go to definition,
find usages, rename, structure view, and reformatting a .lmn file. A .css
file reaches the server when it sits in an app's src/. Server status and
the LSP traffic are in View | Tool Windows | Language Servers.
Settings, under Settings | Languages & Frameworks | Lumen:
| Setting | Default | Effect |
|---|---|---|
Path to lumen-lsp |
unset | Explicit path to the server binary. |
| Look for a locally built server | on | Searches $CARGO_TARGET_DIR and the project's target/ directories (release before debug) before falling back to PATH. |
Changing either setting restarts the server.
The plugin has no lumenc commands and no live preview.
Neovim, Helix, and Zed¶
These three editors highlight .lmn from a tree-sitter grammar, which lives
in tools/tree-sitter-lumen with its queries. Highlighting covers tags,
attributes, {interpolation} placeholders and their $signal and row.field
reference forms, comments, and entity references; <for>, <if>,
<template>, <use>, <slot>, and <include> read as keywords, and
bind-* and on-* are distinguished from plain attributes. An inline
<script> body is injected as Rhai, so install a Rhai grammar to highlight
it. A <script src="../..."> file is opened as its own language, which is where
Lua and candela scripts belong.
The generated parser is committed, so no editor needs the tree-sitter CLI. Every setup below pins a revision of the grammar directory; update that pin after the grammar changes.
Neovim (0.11 or newer, with nvim-treesitter): copy
tools/tree-sitter-lumen/editors/nvim/lumen.lua to
~/.config/nvim/lua/lumen.lua and call require('lumen').setup(). It
registers .lmn as the lumen filetype, registers the grammar so
:TSInstall lumen builds it, and enables lumen-lsp. The server is found the
way the VS Code extension finds it: $CARGO_TARGET_DIR, then the project's
target/ directory, then PATH, with the server_path option overriding all
of it. Pass grammar_path to build the grammar from a local Lumen checkout.
The same directory also holds lsp/lumen_lsp.lua, the server definition on
its own in the layout nvim-lspconfig uses.
Helix: append tools/tree-sitter-lumen/editors/helix/languages.toml to
~/.config/helix/languages.toml, copy the queries into
~/.config/helix/runtime/queries/lumen/, then run hx --grammar fetch and
hx --grammar build. hx --health lumen reports what Helix found.
lumen-lsp has to be on PATH, or named by an absolute path in the
[language-server.lumen-lsp] section.
Zed: the extension is in tools/zed-lumen. Install it from the Extensions
view with "Install Dev Extension", pointed at that directory; Zed builds the
grammar and the extension itself. lumen-lsp is taken from PATH, or from
lsp.lumen-lsp.binary.path in your Zed settings.
MCP server¶
A running Lumen app exposes its UI over a local JSON-RPC socket. An agent
reaches it through lumen-mcp-server, a bridge that speaks MCP on stdin and
stdout and forwards to the app.
Enabling it¶
The server listens on 127.0.0.1 at [mcp] port, defaulting to 7878. It runs
by default for a windowed app. A headless app turns it off unless
[mcp] simulate = true or [runtime] mcp = true. Setting [mcp] port = 0
disables it outright.
simulate gates input injection only. Reading the UI works either way.
issues gates lumen_framework_status's GitHub issue lookup, off by
default: the port has no authentication, so a shipped app leaves the git
and gh subprocesses that tool runs off that surface unless a developer
opts in.
On startup the app prints its port and a ready-to-paste agent configuration
fragment, or lumenc: MCP server disabled.
Connecting an agent¶
LUMEN_MCP_HOST and LUMEN_MCP_PORT set the same two values. In an MCP
client configuration:
{
"mcpServers": {
"lumen": {
"command": "lumen-mcp-server",
"args": ["--host", "127.0.0.1", "--port", "7878"]
}
}
}
The bridge connects lazily, so it registers cleanly with no app running; the first call then reports that nothing is listening. Start the app, and subsequent calls work without restarting the agent.
For one-off queries from a shell, lumenc covers part of the surface with no
bridge: snapshot, find, element-at, click, type, key, scroll,
lint, diff, and screenshot. Every other tool in the table below is
reachable through the bridge only. See Testing.
Talking to the port yourself¶
The port speaks newline-delimited JSON-RPC 2.0 on a raw TCP socket: send one
request object on one line, read one reply on one line, and keep the
connection open for as many requests as you like. It is not HTTP, so an
HTTP request line comes back as a -32700 parse error.
Tools¶
| Tool | Parameters | Returns |
|---|---|---|
lumen_tick |
none | Current frame number, and last_tick_micros: the last tick's wall-clock duration, covering the whole main schedule plus extract and scene encode on ticks that rendered. |
lumen_snapshot_text |
max_lines (default 200, capped at 2000), cursor, omit_invisible (default true) |
Indented one-line-per-entity text tree, with a resume cursor when truncated. |
lumen_snapshot_tree |
max_nodes (default 2000, capped at 10000), omit_invisible (default false) |
The same tree as nested JSON nodes carrying tag, id, classes, role, label, text, rect, and flags. |
lumen_find |
by_text, by_role, by_id, limit (default 50, capped at 500) |
Matching entities with id, role, label, bounds, and state. |
lumen_element_at |
x, y (required) |
The smallest entity containing the point, scroll-corrected, or a miss. |
lumen_inspect_entity |
id (required) |
Every component on that entity. |
lumen_list_entities |
none | Every entity id with its component type names. |
lumen_list_extracted |
none | The rects and text runs queued for drawing. |
lumen_resources |
none | Viewport, pointer position, modifier state, and focus. |
lumen_signals |
filter, max (default 500, capped at 5000) |
Signal names with value, kind, generation, and last changed frame. |
lumen_set_signal |
name, value |
Writes a signal and waits briefly to confirm the app observed it. |
lumen_simulate |
kind plus that kind's fields, wait_for |
Injects input. Requires [mcp] simulate = true. |
lumen_recent_messages |
type, max (default 32) |
The last N entries of one message ring. |
lumen_diff_since |
tick |
Entity ids added, removed, and changed since that tick. |
lumen_lint |
none | UI findings with category, severity, fix hint, and entity. |
lumen_screenshot |
highlight_ids, highlight_lint, include_bounds_map |
A base64 PNG, its size, and optionally an entity bounds map. |
lumen_framework_status |
none | Open issue count (and the first 10 titles) for the repository the checkout's origin remote points at, plus the last tick duration. Requires [mcp] issues = true. |
Simulate kinds: pointer_move, pointer_down, pointer_up, click, key,
type, and scroll. Point kinds take x and y in logical pixels, button
kinds also take button, key takes key plus optional shift, ctrl,
alt, and super modifiers, type takes text, and scroll takes dx and
dy. One request is drained per tick, in order.
wait_for names a message ring the call blocks on until the app records a
matching event, which is how a driver knows an injected event landed rather
than guessing at a delay.
Message ring names, for wait_for and lumen_recent_messages:
PointerMoved, PointerPressed, PointerReleased, ClickEvent,
KeyPressed, KeyReleased, MouseWheel, FocusedKey. Each holds its last
256 entries.
lumen_framework_status is off unless [mcp] issues = true; with it off the
result carries enabled: false and a hint instead of spawning anything. With
it on, the tool resolves the repository from the checkout's origin git
remote and lists its open issues through the gh CLI, bounded by a timeout.
With no network, no gh on PATH, or no GitHub origin remote, the result
carries issues_error describing why instead of an issue count. A result
past the tool's cap on how many issues it counts exactly sets truncated:
true and reports open_issues as a floor rather than an exact total.
Every result carries a short human-readable summary, a confidence value,
and a next_suggested_tools list, so an agent can chain calls without knowing
the surface in advance.
Snapshots refresh once a second by default. With [mcp] simulate = true, and
in a headless run, they refresh every tick so a driver sees each frame.
Resources and prompts¶
Through the bridge, an agent can also list and read project files: lumen.toml,
src/main.lmn, src/main.css, the example apps, and the docs. The bridge
finds the project root by walking up from its working directory for a
lumen.toml (an app checkout) or a Cargo.toml declaring [workspace] (the
Lumen framework checkout). Reads are restricted to that catalogue.
These come from the bridge, which runs in your checkout. Talking to the app's socket directly gets tools only.
Two prompts ship with the bridge:
debug-layout-issuewalks the snapshot-first workflow for diagnosing a UI bug: lint, then the text tree, then a search, then inspection, with screenshots last.add-new-componentwalks the conventions for adding a new primitive to the framework.