---
title: "What's new in Vel: hot reload, an SDK, and a real editor"
date: "2026-06-06"
description: "Hot reload, a Flutter-style SDK, a VS Code extension, and a live theme configurator — what shipped for Vel since the introduction post."
slug: "whats-new-in-vel"
tldr: "Since 'Building Vel' a month ago, four shipping items: a Flutter-style SDK installer (no more 30–60 minute Skia builds), hot reload via dynamic plugins, a VS Code extension with LSP diagnostics, and a live theme configurator that re-tints the whole tree from a button click."
tags: ["uilang", "cpp", "dsl", "tooling"]
cover: "/images/blog/whats-new-in-vel.svg"
coverAlt: "Four cards labelled SDK install, hot reload, VS Code, theme configurator, with smaller pills for EditableText refactor, CI, and multi-page showcase"
author: "Sai Chandan Kadarla"
devto_id: 3836839
---

When I posted [Building Vel](/blog/building-vel) a month ago, the language and runtime worked end-to-end — but everything around them was painful. You had to clone the repo, wait 30–60 minutes for vcpkg to build Skia, restart the binary on every change, and write `.vel` files in a plain text editor with no help.

Here's what shipped to fix that. None of it changed what Vel *is*. All of it changed what using Vel *feels like*.

## A Flutter-style SDK

The install path is now one line:

```bash
curl -fsSL https://raw.githubusercontent.com/chan27-2/vel/main/scripts/install-vel.sh | bash
echo 'eval "$(vel shell-init bash)"' >> ~/.zshrc
vel doctor
```

The script downloads a prebuilt SDK archive for your OS and arch — `vel-sdk-<ver>-darwin-arm64.tar.gz`, `linux-x64.tar.gz`, `windows-x64.zip` — drops it at `~/.vel/sdk`, and adds `vel` + `velc` to your `PATH`. No Skia compile. No vcpkg bootstrap. First run to working compiler: seconds, not hours.

Windows users get the equivalent PowerShell line:

```powershell
irm https://raw.githubusercontent.com/chan27-2/vel/main/scripts/install-vel.ps1 | iex
```

The model is Flutter's, deliberately:

| Flutter | Vel |
|---------|-----|
| `~/flutter` | `~/.vel/sdk` |
| `stable` / `beta` channels | `vel channel stable\|beta` |
| `flutter upgrade` | `vel upgrade` |
| `flutter doctor` | `vel doctor` |

The piece Vel doesn't ship: your final binary still links Skia/GLFW via vcpkg, same as Flutter's platform embedders. The SDK gets you the compiler and the framework — your CMake handles the application link.

## Hot reload via dynamic plugins

The biggest architectural change since the introduction post. `libvel` is now a shared library, and the app shell can `dlopen` a plugin — your widget tree compiled into its own dylib.

The plugin ABI is one symbol:

```cpp
extern "C" vel::Widget* vel_create_root();
```

The host watches the dylib's mtime every 50ms. When you save a `.vel` file, a watch loop runs `cmake --build build --target showcase_plugin` (typically 1–2s for a small edit). The dylib mtime ticks. The host destroys the current root, `dlclose`s the old handle, `dlopen`s the new one, and calls `vel_create_root()` again. The window updates in place.

One script wires it all together:

```bash
./scripts/dev-hot.sh
```

That's the dev loop now: edit, save, see the UI change in under two seconds without restarting.

**What it costs.** Signal state is reset on reload. If you had a form half-filled, those values are gone after the dylib swap — the new root is a fresh tree. This is a deliberate tradeoff: serializing state across an ABI boundary is the kind of feature that ends up dictating every widget's API. For a UI dev loop, "edit structure → see new structure" is what matters; the state comes back when you fill the form once.

The other cost: `RTLD_LOCAL` matters. Plugins are loaded with `dlopen(path, RTLD_NOW | RTLD_LOCAL)` so symbol tables don't leak across reloads. Without it, the second load picks up stale vtables from the first plugin and crashes on the first virtual call. The teardown order also matters — destroy the old root *before* `dlclose`, because the destructors run vtables that live in the plugin's text segment.

## A VS Code extension with real diagnostics

Vel has [a VS Code extension](https://github.com/chan27-2/vel/tree/main/editors/vscode) now — syntax highlighting, completion, hover, live diagnostics.

The interesting part is where the data comes from. Two things were already true about `velc`:

1. `velc --diagnostics-json file.vel` emits structured parse + typecheck errors.
2. `velc --docs-json` emits the complete widget registry — every widget, every prop, every event, every example — as JSON.

The extension is mostly a thin TypeScript shim over those two outputs. The language server runs `velc --diagnostics-json` on save and surfaces the errors. Completion and hover read from `widgets.json` — the *same* file the docs site is built from. There is one source of truth for "what widgets and props exist," and the compiler owns it.

This is the part I'm proudest of. The compiler doing double duty as the registry source means the editor experience never drifts from the language. Add a widget; the editor knows about it on the next sync. Rename a prop; the diagnostics flag every old call site.

## A live theme configurator

The framework now ships a `ThemeConfig` global — accent, radius scale, dark/light — that any widget can flip:

```vel
Btn "Rose"   variant=outline -> click => vel::useAccentRose()
Btn "Lg"     variant=outline -> click => vel::useRadiusLg()
Btn "Light"  variant=outline -> click => vel::useLightTheme()
```

There are 7 accents (Indigo, Blue, Violet, Rose, Emerald, Amber, Slate), 6 radius scales (None → Full), and dark/light. A single setter call rebuilds the `Theme` from the config and re-applies it across the tree. The whole UI re-tints in one frame.

The detail that took thought: this used to crash. The old path called `root->onThemeChanged()` synchronously from inside the click dispatch, which rebuilt the entire widget tree — freeing the button that was still executing its own `onClick` handler. Use-after-free, every time.

The fix is deferred rebuild: set a `pendingThemeChange_` flag and apply the theme at the start of the *next* frame, after the current event dispatch has unwound. It's now the pattern for any widget action that mutates global state — locale switching will use it next.

## EditableText: one widget, two surfaces

`Input` and `Textarea` shared ~95% of their code — UTF-8 cursor math, blink, key handling, focus sync, char input. They were drifting. Both have been refactored onto a shared `EditableText` base:

```cpp
class EditableText : public Interactable {
public:
    std::string text;
    std::string placeholder;
    std::function<void(const std::string&)> onChange;
    std::function<void(const std::string&)> onSubmit;

    bool onMouseDown(Point, MouseButton) override;     // final
    bool onKey(int, int, bool) override;               // final
    bool onChar(unsigned int) override;                // final
    void tick(double now) override;                    // final

protected:
    virtual int  byteIndexAt(Point pos) const;         // override per layout
    virtual void onEnterPressed();                     // Input submits, Textarea inserts \n
};
```

Subclasses now only own their rendering and the click-to-cursor mapping. One place to fix backspace-at-position-zero. One place to keep the cursor on a UTF-8 boundary. Adding a `PasswordInput` or a syntax-highlighted code editor is much smaller now — the base class owns the hard parts.

## CI on Linux, macOS, and Windows

Less glamorous, but it matters: GitHub Actions builds Vel on Ubuntu, macOS, and Windows on every push and PR. Each platform builds `velc`, builds `libvel`, runs CTest, and runs the showcase smoke test where a display exists.

Two things were painful to get right:

- **vcpkg caching.** Earlier iterations cached only `installed/`, which left the cache unable to bootstrap on restore — the `.git` directory and `scripts/` were missing. Now the entire `vcpkg-root` is cached, keyed on the manifest hash. CI cold start dropped from a full Skia build to a tar extract.
- **Headless runners.** macOS CI has no display; Windows Server runners ship without a usable OpenGL driver, so `glfwCreateWindow` aborts the showcase. The `vel run` script now detects `MINGW*/MSYS*/CYGWIN*` and macOS CI and skips the GUI launch — compile + CTest still cover the toolchain.

Multi-platform CI is the kind of thing you don't notice until it breaks. It took three commits to get right, and I hope to never touch it again.

## The showcase, reorganized

A small one but worth flagging: `examples/showcase.vel` used to be one long scrolling page exercising every primitive. It's now a multi-page demo with a sidebar:

- Form Controls
- Buttons & Cards
- Data
- Overlays
- Theme

Build, then `./scripts/vel run ./build/showcase` — that's the first thing you see. It's also the integration test that exercises every widget, every prop, and every event.

## What's next

- **Per-widget damage rectangles.** Frame-level damage tracking is great for idle apps; per-widget would let animations not repaint the whole screen.
- **`vel.json` package manifest**, so third-party registries become installable instead of vendored.
- **A `vel new` template generator.** The boilerplate to start a Vel project is small but not zero.
- **More registry primitives** — Video, SVG, a devtools overlay, a syntax-highlighted code editor (which the `EditableText` refactor now makes practical).

If you tried Vel a month ago and bounced off the install, give it another go. `curl -fsSL .../install-vel.sh | bash`, run `vel doctor`, open the showcase. The first ten minutes look very different now.
