Two new dashboard controls, Kotlin and Swift, and three Viewer fixes — one of which could freeze the whole MCP server.
One indexed file could freeze the entire server
The per-file stats in buildTree used three LEFT JOINs with COUNT(DISTINCT) in a single GROUP BY. The joins multiply per file before the DISTINCT collapses them again: occurrences × methods × types. One generated header in an indexed project — 191.134 occurrences × 3.138 methods × 786 types — came to 471 billion intermediate rows for that one file.
better-sqlite3 runs synchronously in native code, so the first tree request from a connecting browser killed the process for good: event loop dead, 98 % CPU for hours, 1,5 GB RSS, MCP calls hanging, dashboard empty. The ports kept listening throughout, which made it look "on but broken" rather than crashed.
It is now three correlated subqueries per file, each walking one table by its file_id index — same numbers, milliseconds instead of hours. (COUNT(DISTINCT id) on a primary key is just COUNT(*).) Both the code and all branches carried the pattern.
Worth recording, because it cost a lot of time: the usual profiling tools are useless here. --cpu-prof never writes its file when the event loop is dead, and the inspector cannot interrupt native frames — V8 --prof just reports ~98 % "unaccounted" ticks, which is the finding. What worked was bisection against a standalone repro: hub alone 1 % CPU, viewer alone 96 %, and the spin always started right after a client connected.
Switches and buttons on the Live dashboard
The Live tab could only ever set a value — a slider or a number box. Two control types join them.
toggle is state: a two-position switch, value 0/1, with unit doubling as the "ON|OFF" captions.
button is an event, and that needs a different shape. A source polls GET /control at its own pace, so a boolean "pressed" would be reset again before it ever looked. A button's value is therefore a monotonically rising press counter: the source compares against the last count it saw and learns both that and how often it was pressed. Verified — 50 concurrent presses count to exactly 50, and a demo polling every 500 ms picks up all five clicks that land between two polls.
Presses arrive via the new POST /control/press, which takes only an id. The hub owns the counter deliberately: if the browser computed the next value itself, two open dashboards would send the same number and one press would vanish.
A backwards jump in the counter (wrap at 1e6, POST /panel/clear, hub restart) means "restart, adopt the value" — not a million presses. Re-announcing a widget, as a device does after rebooting, no longer clobbers a value the user dialed in, and the press count survives it.
Kotlin and Swift
.kt, .kts and .swift are indexed via tree-sitter and classified as code across every extension list, bringing the language count to 14. A small share of files using very recent syntax falls back gracefully when the community grammar can't parse them.
The Viewer stopped hoarding OS handles
chokidar removed glob support in v4, but the watcher still passed v3 globs ('**/node_modules/**' and friends). In v5 a string matcher is an exact comparison, so the ignore list silently never matched — every directory below the project root got watched, one non-recursive fs.watch and therefore one OS handle each.
Measured on this repo (2.520 directories):
| Handles | RSS | |
|---|---|---|
| no Viewer (control) | 191 | — |
| before | 19.619 | 224 MB |
| after | 2.375 | 84 MB |
ignored is now a predicate that returns true for the directory itself — matching only files inside it still lets chokidar descend and open the handle — and it reuses the exclusion list the indexer already had, since a directory not worth indexing is not worth watching. Verified by inspecting what chokidar actually watches afterwards: 352 directories, none that should have been excluded.
This also fixes the spurious tree rebuilds: every node_modules event used to trigger a full rebuild and broadcast to all connected browsers, for data that is not even in the database. That was the breeding ground for the memory leak that v2.1.0 could only treat at the symptom end.
managed_components excluded too. The predicate fix was necessary but not sufficient on ESP-IDF projects, where that directory is the embedded world's node_modules. The arithmetic on one such repo is the real finding: 14.858 handles = 1.865 watched directories + 12.713 watched files + base. On Windows chokidar holds a handle per watched file, not merely per directory — and 12.407 of those files sat in managed_components. That single entry brings the process back to a few hundred handles.
Also
- The Viewer's
Debugtab is now calledLive—Logsis history that scrolls past,Liveis the current state in fixed slots. Nothing about the API changed; existing senders and deep links keep working. - The
▷ Demobutton was missing from an empty dashboard — exactly when it was needed, since it is the way to get your first widgets.
Full changelog: v2.2.2...v2.3.0