Dev Inspector

The dev inspector is a set of dev-only client scripts injected into every rendered page when the server runs with --dev (or --hot). It lets you edit translations and CSS classes directly in the browser, jump to the source file in your editor, and file GitHub issues with annotated screenshots - without leaving the page you are looking at.

The inspector is never present in production: the template engine skips source stamping when caching is on (production), and the injection functions are no-ops. There is nothing to disable or configure for a deployed site.

How It Works: Source Stamping

During .ree template compilation in dev mode, the template engine stamps every rendered HTML element with its source location. Two stamp types are written:

  • data-ree="<file>:<line>" on block-level HTML tags (div, section, form, a, button, img, input, select, textarea, etc.). Pure inline tags (span, strong, em, br) are not stamped - a click on one resolves up to its nearest stamped ancestor.
  • data-ree-i18n="<dotted.path>" on a wrapper <span> around every {_ path} / {- path} / {@ path} translation lookup, so the client can target the exact rendered string. A data-ree-i18n-raw="1" flag marks markup strings ({- } / {@ }) that need a dialog editor rather than inline text editing. A data-ree-i18n-file="<file>" attribute records the source template file, used to disambiguate the translation namespace when the same key path exists under multiple namespaces.

The stamps use project-root-relative paths (e.g. apps/main/home/index.ree:42), which the server validates against the project root before any file read or write.

The Two Chords

The inspector uses two distinct modifier-chord combinations, derived per-event from the mouse event's modifier flags (not tracked via keydown/keyup, so a missed keyup can never leave it stuck on):

ChordPlatformTierWhat it does
Cmd+Shift (macOS) / Win+Shift (Windows/Linux)allContentEdit a {_ } string in place, edit a {- }/{@ } markup string in a dialog, or open a block/component in the editor
Alt+Shift (no Ctrl/Cmd)allClassEdit the nearest stamped tag's class attribute (patched into the .ree source)

Hover with a chord held to highlight the target element with a colored overlay and a label showing what a click would do. Click (chord still held) to act. Release any modifier to clear the highlight.

The overlay color indicates the action:

ColorAction
GreenEdit text in place (plain {_ } string)
PurpleEdit markup in a dialog ({- } / {@ } string)
GreyOpen source in editor (a stamped block/component)
AmberEdit class attribute (the nearest .ree-stamped tag)

Inline i18n Editing

When you Cmd+Shift+Click (or Win+Shift+Click) a translation string:

  • Plain {_ } text (green overlay): the rendered <span> becomes contenteditable. Type to edit, Enter to save, Esc or click-away to cancel. A save only happens on an explicit Enter. The new value is written to the locale's {locale}.json file via a WebSocket message, and the page hot-reloads with the updated string.

  • {- } / {@ } markup (purple overlay): a dialog opens with a textarea pre-filled with the raw source value (fetched via i18n_get). Edit the markup as text, Ctrl+Enter or click Save to write it back (i18n_update), Esc or Cancel to discard. The page hot-reloads after save.

The WebSocket rides the same /__reload endpoint as live reload. The server resolves the translation key to a namespace (using the stamped data-ree-i18n-file attribute when the same key path exists under multiple namespaces, falling back to the page URL), reads or writes the {locale}.json file, and the translation system hot-reloads. See Translations for the file layout and how create_ctx() merges namespaces.

Class-Attribute Editing

When you Alt+Shift+Click an element, the inspector finds the nearest .ree-stamped ancestor (the block tag whose source line is known). A dialog opens with a single-line input pre-filled with the current literal class attribute value (fetched from the .ree source via class_get). Edit the value, Enter or Save to patch the .ree source file, Esc or Cancel to discard.

The patch is a string transform on the .ree source at the stamped line: it locates the tag by name among same-line siblings and replaces its class="..." attribute (or adds one if absent). The file is written to disk and Bun's --hot file watcher reloads the page with the updated styling.

The hover label for the class tier shows the tag name (<div>) and the current class value, so you can see what you are about to edit before clicking.

Open in Editor

When you Cmd+Shift+Click (or Win+Shift+Click) a stamped block/component that is not a translation string (grey overlay), the inspector POSTs to /__ree_open?file=<path>&line=<n>. The server:

  1. Validates the path is project-root-relative (no absolute paths, no traversal outside the project root).
  2. Launches the configured editor at the file and line.

The editor is set by the OPEN_IDE env var in .env:

OPEN_IDE=vscode

Supported values:

KeyCommand
vscodecode --goto {file}:{line}
zedzed {file}:{line}
nvimnvim +{line} {file}
sublimesubl {file}:{line}
ideaidea --line {line} {file}

When OPEN_IDE is not set, the open-in-editor tier is disabled (a toast says so), but the rest of the inspector still works. The path guard mirrors the template engine's include resolver: resolve, then assert the target stays under the project root.

Disabled form controls suppress click events, so the inspector uses a pointerdown capture listener as a fallback for disabled elements - the same open-in-editor action fires.

The GitHub Issue Reporter

Ctrl+Shift+I opens the issue reporter dialog. It files a GitHub issue with title, description, labels, and annotated screenshots directly from the browser.

Prerequisites

  • ree.issue_repo in package.json - the owner/repo to file against (set with bun reeman set-repo <owner/repo>). This is separate from the npm repository/bugs fields so a fork or renamed remote cannot send issues to the wrong place.
  • gh CLI authenticated (gh auth login). The server shells out to gh and never handles a GitHub token itself.

Screenshots and Annotation

The dialog offers three screenshot paths:

  • Capture before/after state - uses getDisplayMedia to capture the screen, then opens a full-viewport annotation editor. The editor supports arrow, rectangle, ellipse, pen, blur/redact, and text tools, with color and stroke-width pickers. The previous screenshot is shown side-by-side as a reference while annotating. Tool, color, and width settings are persisted in localStorage so you do not re-pick them on every capture.
  • Add another - same capture + annotation flow for an additional screenshot.
  • Paste from clipboard - paste an image (Ctrl+V in the dialog or click the button) for when screen capture is unavailable.

Screen capture (getDisplayMedia) requires a secure context (HTTPS or localhost). Over plain HTTP the capture buttons are disabled with a hint to use a tunnel or paste instead.

What Gets Filed

The issue body includes:

  • Annotated screenshot images, uploaded to an orphan screenshots branch in the issue repo via the GitHub contents API (no third-party image host). Each image is a clickable markdown thumbnail linking to its blob URL.
  • The description text.
  • The page URL the dialog was opened from (appended as "Reported from: ...").
  • The filing user's email (or username), bolded to avoid GitHub @mentions.

Every issue gets the agent ready label (created lazily if it does not exist), marking it as ready for an LLM agent to pick up. Screenshot upload degrades gracefully: if an upload fails, the issue is still filed with a note listing what did not make it, so the reporter can attach the image by hand later.

Submission

The form POSTs to /__issue as multipart/form-data. The server creates the GitHub issue via gh issue create --body-file -, uploading each screenshot to the screenshots branch first. The issue URL is returned and opened in a new tab.

How Scripts Are Injected

Three client scripts are injected by lib/livereload.ts into the </body> of every rendered page in dev mode:

  1. livereload_client.js - opens the WebSocket for hot reload.
  2. inspector_client.js - the two-chord inspector (i18n editing, class editing, open-in-editor).
  3. issue_reporter_client.js - the Ctrl+Shift+I issue reporter dialog.

Each is read once, cached, and injected by rewriting the closing </body> tag. In production, the injection functions are no-ops and the scripts are absent. The inspector's WebSocket rides the same /__reload endpoint as live reload, so there is no separate connection to manage.

The inspector client reads the data-ree and data-ree-i18n stamps written by lib/inspector_stamp.ts during template compilation. It walks the DOM upward from the click target to find the nearest stamped ancestor, classifies the target (text, markup, code, or class), and dispatches the appropriate action.

Configuration

SettingWherePurpose
OPEN_IDE.envWhich editor to launch for the open-in-editor tier. One of: vscode, zed, nvim, sublime, idea. Unset = tier disabled.
ree.issue_repopackage.jsonThe owner/repo the issue reporter files against. Set with bun reeman set-repo <owner/repo>.
gh auth loginsystemGitHub CLI authentication (required for the issue reporter to create issues and upload screenshots).

See Configuration for the full environment variable reference. The inspector and issue reporter work on any dev server (bun dev, bun dev:all) and on the ReeQA app - they are injected wherever render() runs in dev mode.