Christian Battaglia
March 1, 2026
50 min read
Pick two:
VSCode and Cursor give you #1. Zed gives you #2. Nobody gives you all three.
That's the gap I'm building into.
But honestly, if this were just about performance, I wouldn't bother. Editors are fast enough. What this is really about is something bigger: who owns the developer's most intimate tool?
Your editor knows everything. Every character you type, every file you open, every search query, every undo. It sees your thought process in real-time. And right now, the trend in developer tools is to send all of that to someone else's cloud and hope they're good stewards.
I think there's a better path.
Open VSCode. Install 30 extensions. Notice the lag.
Here's what's actually happening: every single extension — Prettier, ESLint, GitLens, your theme, your icon pack's activation hook — runs in a single Node.js process called the Extension Host. One process. One thread for JavaScript. One V8 heap shared across everything.
When GitLens diffs your blame annotations while ESLint lints your file while TypeScript rebuilds its project graph, they're all fighting for the same event loop. One badly-written extension can stall everything. One memory leak slowly degrades your entire editing experience.
Microsoft engineered this isolation brilliantly — the extension host is a separate process that communicates with the editor UI via a well-defined binary protocol over IPC. Extensions can't crash the editor. But they can sure as hell make it feel slow.
Cursor inherited this architecture wholesale. They added incredible AI features on top — and they're genuinely great at it — but the foundation is still Node.js running your extensions single-threaded. Same bottleneck. Same heap pressure. They changed the ceiling; they didn't change the floor.
Zed took the opposite approach: rewrite everything in Rust, use WASM for extensions. It's blazing fast. But the ecosystem is tiny. No Prettier. No ESLint. No GitLens. You're starting from zero. And after years of funded development with a great team, they still don't have extension parity. Rebuilding an ecosystem is the hardest problem in software.
Lapce tried too — another Rust editor, WASI-based extensions. Same problem. Beautiful architecture. Empty extension marketplace.
The pattern is clear: you can have native performance or you can have the ecosystem. Nobody has cracked both.
I don't like arguing from theory. Let me show you what's actually happening right now on my MacBook while I write this post in Cursor.
Running ps aux | grep Cursor reveals 65 separate processes consuming 9.5 GB of resident memory. For a text editor. Here's where it all goes:
| Process Type | Count | Total RSS |
|---|---|---|
| Renderer (Chromium) | 5 windows | ~4.3 GB |
| Extension Hosts | 10 processes | 2.5 GB |
| Language Servers | 29 processes | 1.8 GB |
| GPU process | 1 | 136 MB |
| Shared process | 1 | 90 MB |
| File watchers | 2 | ~150 MB |
| Terminal PTY host | 1 | 68 MB |
The Chromium overhead is what it is — that's the cost of Electron. The interesting story is the extension hosts: 10 processes eating 2.5 GB, plus the 29 language server child processes they spawn eating another 1.8 GB. That's 4.3 GB just for extensions.
Vanilla VSCode runs one extension host per window. Cursor doesn't. It runs three distinct types per window, each in its own process:
extension-host (user) — Your Normal ExtensionsPID 91634 [window 1] 160 MB
PID 91635 [window 2] 312 MB
PID 89683 [window 3] 82 MB
PID 90724 [window 4] 434 MB
These are the traditional extension hosts running ESLint, Prettier, Tailwind CSS, YAML, JSON, Markdown, spell checker, GitHub Actions, Docker — everything from the marketplace. Each open window gets its own. Window 4 is the one with this project open, and it's at 434 MB because of all the language servers it has spawned.
Each user extension host spawns 7-10 language server child processes via --node-ipc:
eslintServer.js --clientProcessId=90724
tailwindServer.js --clientProcessId=91635
code-spell-checker/main.cjs --clientProcessId=90724
yaml/languageserver.js --clientProcessId=91635
json/jsonServerMain --clientProcessId=91634
markdown/serverWorkerMain --clientProcessId=90724
html/htmlServerMain --clientProcessId=91635
github-actions/server-node.js --clientProcessId=90724
dockerfile-language-server --clientProcessId=90724
compose-language-service --clientProcessId=90724
Every single one of these is a separate Node.js process. 29 language servers across 4 windows. Each one has its own V8 heap, its own event loop, its own memory overhead. The --clientProcessId flag tells you which extension host spawned it.
extension-host (retrieval-always-local) — Cursor's Intelligence EnginePID 91636 [window 1] 186 MB
PID 91637 [window 2] 830 MB ← the big one
PID 90751 [window 4] 222 MB
This is where it gets interesting. These hosts run two Cursor-proprietary extensions: cursor-retrieval (codebase indexing and search) and cursor-always-local (local features, git workers). Window 2's retrieval host is eating 830 MB by itself — that's a single Node.js process with a Rust native addon doing file indexing.
Each of these hosts also spawns a gitWorker.js subprocess:
PID 90829 parent=90751 gitWorker.js 51 MB
PID 91841 parent=91636 gitWorker.js 49 MB
PID 91889 parent=91637 gitWorker.js 48 MB
extension-host (agent-exec) — The AI SandboxPID 91639 [window 1] 84 MB
PID 91638 [window 2] 138 MB
PID 90752 [window 4] 129 MB
This is where agent conversations execute. The cursor-agent-exec extension lives here — the one with shell access, file editing, tool use. Every agent turn I take in this conversation runs in one of these processes. The sandboxed shell commands, the file reads, the grep searches — all mediated through this extension host.
Using lsof -p on extension host PID 91634 reveals the pipe topology:
FD 1 PIPE → stdout to main process
FD 2 PIPE → stderr to main process
FD 13 PIPE ↔ bidirectional pair with FD 14 (main IPC channel)
FD 20 PIPE ↔ bidirectional pair with FD 21 (secondary channel)
FD 27 PIPE ↔ bidirectional pair with FD 28 (tertiary channel)
FD 54 UNIX socket → /tmp/vscode-git-*.sock (git extension)
FD 95 UNIX socket → /tmp/ssh-askpass-*.sock (SSH auth)
Three bidirectional pipe pairs plus Unix domain sockets. The main channel carries the binary RPC protocol. The secondary and tertiary channels likely handle bulk data transfer and diagnostic streams. This is the exact protocol I've been implementing in Anneal's anneal-host crate — 13-byte header framing over these pipes.
The retrieval host (PID 91637) has a similar layout but with 118 open file descriptors total — the extra FDs are file handles for the indexing engine reading your codebase.
Cursor Helper (Plugin)Every extension host is the same binary:
/Applications/Cursor.app/Contents/Frameworks/
Cursor Helper (Plugin).app/Contents/MacOS/Cursor Helper (Plugin)
Mach-O 64-bit executable arm64
222 KB
It's Electron's utility process binary — a stripped Node.js runtime. It boots by loading bootstrap-fork.js, which reads VSCODE_ESM_ENTRYPOINT from the environment and dynamically imports:
vs/workbench/api/node/extensionHostProcess.js (4.6 MB bundled)
That single 4.6 MB JavaScript file is the entire extension host runtime — the vscode.* API implementation, the extension activation lifecycle, the provider registry, everything. It's what Anneal replaces.
Here's the finding that made me sit up straight. Inside cursor-retrieval, there's a native Node addon:
@anysphere/file-service/file_service.darwin-universal.node
23 MB — Mach-O universal binary (x86_64 + arm64)
The otool -L output shows it was compiled from:
/Users/runner/work/everysphere/everysphere/_work/1/s/target/
aarch64-apple-darwin/release/deps/libfile_service.dylib
This is compiled Rust. Built in Anysphere's (Cursor's parent company) CI from a repo called everysphere. Running strings on the binary reveals exactly what Rust crates are compiled inside:
| Crate | Purpose |
|---|---|
| libgit2-sys 0.17 + gix 0.74 & 0.77 | Git operations (dual implementation — C bindings AND pure Rust) |
| grep-searcher, grep-matcher, grep-regex | Ripgrep's core search engine |
| aho-corasick | Multi-pattern string matching |
| globset | Fast glob/pattern matching |
| walkdir | Recursive directory traversal |
| regex / regex-syntax / regex-automata | Full regex engine |
| tokio 1.47 | Async runtime |
| memmap2 | Memory-mapped file I/O |
| bstr | Byte string handling |
| jiff | Date/time library |
| tracing-subscriber | Structured logging/observability |
Cursor already ships a Rust binary for their most performance-critical path. They took the ripgrep search engine, the gix git library, and a file walker, compiled them into a 23 MB native addon, and loaded it into a Node.js process via NAPI. The cursor-retrieval extension calls into this Rust code for file indexing, code search, and git operations.
This is the engine behind the 830 MB retrieval host on my machine.
Cursor ships 16 proprietary extensions alongside the standard VSCode ones:
| Extension | Bundle Size | What It Does |
|---|---|---|
| cursor-agent | 38 MB | The AI agent — composer, chat, inline edits. The biggest JS bundle. |
| cursor-agent-exec | 4.1 MB | Agent execution sandbox — shell, file ops, tool use |
| cursor-always-local | 3.8 MB | Local features, experimentation, git workers |
| cursor-retrieval | 3.5 MB JS + 23 MB native Rust | Codebase indexing and search |
| cursor-mcp | 1.5 MB | Model Context Protocol server |
| cursor-shadow-workspace | 1.1 MB | Background apply workspace |
| cursor-browser-automation | 276 KB | Browser testing MCP |
| cursor-file-service | — | File operations |
| cursor-commits | — | Commit intelligence |
| cursor-deeplink | — | URL scheme handler |
| cursor-worktree-textmate | — | Worktree grammars |
| cursor-polyfills-remote | — | Remote development polyfills |
| cursor-ndjson-ingest | — | Streaming JSON ingestion |
| cursor-ios-simulator-connect | — | iOS development |
| cursor-android-emulator-connect | — | Android development |
| theme-cursor | — | The Cursor theme |
The cursor-agent bundle is 38 MB of JavaScript. That's the AI brain — and it runs in a Node.js process. Every agent turn serializes context through the V8 heap, through JSON-RPC, through the IPC pipes, to the renderer, then back.
The picture is clear: Cursor is already doing half of what Anneal proposes, but from the wrong direction.
Cursor's architecture:
Electron main process
└─ spawns Node.js extension host (Cursor Helper Plugin binary)
└─ loads JavaScript extensions
└─ loads Rust native addon via NAPI (@anysphere/file-service)
└─ ripgrep + gix + walkdir + tokio compiled to .node
Anneal's architecture:
Electron main process
└─ spawns Rust extension host (anneal-host binary)
└─ embeds V8 for JavaScript extensions (rusty_v8)
└─ loads native Rust extensions directly (dylib/trait impls)
└─ runs intelligence stack in-process (sqlite-vec, Kreuzberg)
Same insight: Rust is needed for performance-critical editor operations. Opposite execution: Cursor bolts Rust onto Node.js from the outside as a native addon. Anneal makes Rust the foundation and embeds V8 inside it.
The consequences are significant:
exec() a binary and open pipes. Plus V8 bytecode caching for the JS extensions on top.cursor-agent is 38 MB of JavaScript that V8 has to parse, compile, and execute on every process start. With bytecode pre-compilation, that 38 MB could load in under 3ms instead of the hundreds of milliseconds it takes to cold-compile.And there's one more thing Anneal gets for free that Node.js has been unable to ship for years.
In February 2026, Matteo Collina and the Platformatic team published benchmark results for V8 pointer compression in Node.js — a feature that reduces each heap pointer from 64 bits to 32 bits, cutting JavaScript memory usage by roughly 50% with only a 2-4% latency increase. Chrome has had this enabled since 2020. Node.js still doesn't turn it on by default.
Why? Two historical reasons. First, pointer compression originally forced all V8 isolates in a process to share a single 4 GB memory "cage." That was fine for Chrome (one tab = one process), but fatal for Node.js where worker threads share a process. Cloudflare and Igalia eventually solved this with IsolateGroups — per-isolate cages instead of a process-wide one — but the Node.js integration only landed in late 2025 and still requires a compile-time flag. Second, legacy native addons built with NAN (Native Abstractions for Node.js) break under pointer compression because NAN exposes V8's internal pointer layout directly. Any addon that hasn't migrated to Node-API is incompatible.
So Node.js sits on a 50% memory reduction that it can't enable by default because of backwards compatibility constraints. The result: every VSCode extension host, every language server, every Cursor retrieval process runs with uncompressed 64-bit pointers, using twice the heap they need to.
Anneal doesn't have this problem.
When you embed V8 through rusty_v8, you control the build flags. You enable pointer compression from day one. You use IsolateGroups — each JS extension gets its own V8 isolate with its own 4 GB cage, which is more than any extension will ever need. There are no NAN addons to break because Anneal doesn't use NAN — Rust extensions use the trait system, and JS extensions use the standard vscode.* API shimmed through V8's C++ embedding API.
The math is straightforward. The 10 extension host processes on my machine use 2.5 GB of RSS. A significant portion of that is V8 heap. With pointer compression, the V8 heap portion drops by ~50%. That's potentially over a gigabyte of savings just by flipping a build flag that Node.js can't flip.
And it compounds. Pointer compression doesn't just save memory — it makes the garbage collector faster. Smaller objects fit in fewer cache lines. The young generation fills more slowly, triggering fewer minor GCs. Major GC has less to scan. Compaction moves fewer bytes. The Platformatic benchmarks showed p99 latency actually dropped by 7% with pointer compression enabled, because GC pauses were shorter and less frequent.
For an editor where GC pauses directly translate to UI jank — a completion menu that hangs, a cursor that stutters, a syntax highlight that lags — this matters. Anneal's embedded V8 isolates run with compressed pointers and shorter GC pauses from the first release. No compile-time flag. No Docker image swap. No waiting for the Node.js project to solve backwards compatibility. It's just... on.
This isn't theoretical. The numbers are on my machine. 65 processes. 9.5 GB. 29 language servers. Three extension host types per window. The most performance-critical piece is already compiled Rust, just bolted onto Node.js from the outside. And the V8 heaps powering every one of those processes are twice as large as they need to be because Node.js can't turn on a feature that Chrome has had for six years.
VSCode's extension host talks to the editor through a well-defined protocol — 13-byte header framing with binary RPC messages. Not JSON-RPC. A custom binary protocol designed for speed. The editor UI doesn't know or care what language the extension host is written in. It only sees protocol messages.
You can replace Node.js with Rust and the editor doesn't notice.
I've been deep in the VSCode source. The protocol has three layers:
$$ref$$ markers, proxy identifiers for service routingThe initialization handshake is elegant: the Rust host sends Ready (one byte: 0x02), receives JSON init data (workspace info, extension list, environment), sends Initialized (one byte: 0x01), and then both sides enter the RPC loop. From that point on, it's just typed binary messages: requests, acknowledgements, replies.
I've implemented this in Rust. It works. The handshake completes. RPC messages parse. The hard part isn't the protocol — it's the API surface behind it.
Anneal is a Rust-native VSCode extension host.
It replaces the Node.js extension host process with a Rust binary that:
rusty_v8 bindings (v146+, stable, Chrome release cadence). Each extension gets its own V8 isolate. Pre-compiled bytecode means extensions load faster than they would in Node.js.CompletionProvider interface works in both languages.sqlite-vec for vector storage and Kreuzberg for document extraction. Your entire project — code, docs, everything — indexed locally. No cloud dependency.The name comes from metallurgy: annealing is the process of heating metal and cooling it slowly to make it stronger and less brittle. The VSCode extension ecosystem is brittle — single-threaded, crash-prone, memory-hungry. Anneal makes it resilient.
Here's the thing that got me started. I was experimenting with V8's bytecode compilation in Rust — building a pipeline that takes JavaScript, compiles it to V8 bytecode using EagerCompile, extracts the code cache via UnboundScript::create_code_cache(), and measures the difference. The speedup is dramatic:
| Bundle Size | Cold Compile | With Bytecode Cache | Time Saved |
|---|---|---|---|
| 50KB | 115μs | 19μs | 83% |
| 100KB | 102μs | 12μs | 88% |
| 250KB | 353μs | 32μs | 91% |
| 500KB | 597μs | 43μs | 93% |
| 1MB | 1,514μs | 86μs | 94% |
For a typical VSCode extension (100-500KB bundled), pre-compiled bytecode eliminates ~90% of the compilation overhead. When you have 30 extensions loading at startup, those microseconds compound into the difference between "instant" and "why is my editor still loading?"
Now think about Cursor's cursor-agent extension at 38 MB. Cold-compiling 38 MB of JavaScript takes real time — likely in the range of 50-100ms on a fast machine. With bytecode pre-compilation, that drops to under 5ms. Multiply that by the 16 Cursor extensions plus your marketplace extensions, across all three extension host types, across every window. Startup time goes from "wait for it" to imperceptible.
The code cache is version-locked to the exact V8 build — which is fine when you control the runtime. We pin the V8 version in Cargo.toml and recompile caches on upgrades. A solved problem.
This is the most interesting architectural decision and the one I think has the most long-term implications: Rust and JavaScript extensions share the same API.
// A Rust extension implements the trait directly
impl CompletionProvider for MyRustExtension {
fn provide_completions(&self, uri: &Uri, position: &Position) -> Vec<CompletionItem> {
// Native speed, zero serialization
vec![CompletionItem { label: "hello".into(), .. }]
}
}// A JS extension implements the same interface
class MyJSExtension implements CompletionProvider {
provideCompletions(uri: Uri, position: Position): CompletionItem[] {
// Runs on embedded V8, bridged via Fast API
return [{ label: "hello", ... }];
}
}The Rust traits are the source of truth. TypeScript interfaces are auto-generated. When the extension host needs completions, it calls the trait method — if the extension is Rust, it's a direct function call with zero serialization. If it's JavaScript, it crosses the V8 Fast API bridge with minimal overhead.
Existing VSCode extensions work through a compatibility shim that maps vscode.* API calls to the trait system. You don't rewrite your extensions. They just run in a better runtime.
Why this matters: it creates a migration path. You start with your existing JS extensions running on embedded V8. Performance-critical extensions can be incrementally rewritten in Rust against the same interface. No big bang migration. No ecosystem split. Just a gradient from interpreted to native, at whatever pace makes sense.
Consider Cursor's own architecture: they have a 38 MB JavaScript agent and a 23 MB Rust native addon in the same process, communicating through NAPI bindings. With Anneal's dual interface, that boundary disappears. The agent logic could be partly Rust (for performance-critical paths like file diffing, AST parsing, context assembly) and partly JavaScript (for the flexible, rapidly-iterating UI integration) — and both sides use the same type system, the same provider pattern, the same extension lifecycle.
Let me be direct about this: an IDE must be open source.
Your editor is the most intimate tool you use as a developer. It sees every keystroke, every file, every thought process. It's more personal than your browser. And the trend right now is toward closed-source AI IDEs that process your code on their servers — Cursor, Windsurf, GitHub Copilot — each asking you to trust a company with your entire codebase in exchange for productivity features.
That's a deal you shouldn't have to make.
Here's what happens with closed-source developer tools:
Open source breaks this cycle. The code is inspectable. The data flow is auditable. If the maintainer goes sideways, you fork. If the company raises prices, you self-host. The community owns the foundation.
Look at the precedents:
The best developer tools have always been open source because developers demand visibility into their tools. We literally read code for a living. We're not going to trust a black box with our most productive hours.
And here's the thing I found while dissecting Cursor's process tree: that 23 MB Rust native addon? @anysphere/file-service? Closed source. Compiled from a private repo called everysphere. The extension host code, the agent logic, the retrieval engine — none of it is available for inspection. You can't audit what it indexes. You can't verify what it sends to the network. You can't see what cursor-ndjson-ingest does with your data. You're trusting a compiled binary with read access to every file in your workspace.
Anneal will be open source. Not "open core." Not "source available." Open source. The intelligence stack, the extension host, the protocol implementation — all of it. If you want to build on it, fork it, sell it, go ahead.
The business model, if there ever is one, is the same as every successful open source company: sell the hosted version, the support, the enterprise features. Not the tool itself. The tool belongs to everyone.
The second hard question: why not build a new extension format? Why carry the baggage of VSCode's VSIX packages, package.json contribution points, activation events?
Because the VSIX ecosystem represents millions of person-hours of development that you cannot replicate.
Prettier has been in continuous development since 2017. ESLint since 2013. The TypeScript language server is maintained by Microsoft with dozens of engineers. GitLens is a single developer's life's work. The Python extension has integration with debugpy, Jupyter, virtual environments, conda — years of edge cases handled.
You're not going to rewrite these. Nobody is. Zed has been trying for years with a well-funded team and still doesn't have full parity on any of them. Lapce same story.
The VSIX format itself is actually well-designed:
package.json contribution points — declarative. Themes, snippets, keybindings, grammars, languages. These don't need any API at all. An extension that only declares contributions works with zero code changes. That's roughly 40% of the marketplace.onLanguage:python means the Python extension doesn't touch memory until you open a .py file. The onStartupFinished event lets critical extensions like Cursor's retrieval engine start after the UI is interactive.registerCompletionItemProvider, registerHoverProvider, etc. Uniform, composable, well-typed. This is the same pattern I'm using for the Rust trait system.The problem was never the contract. It was the runtime. The vscode.d.ts API surface is large (~823 symbols across 15 namespaces), but the core that 80% of extensions use is only ~130 symbols. Commands, workspace configuration, language providers, diagnostics, text document model. Implement that, and the overwhelming majority of extensions just work.
Look at what I found in Cursor's extension manifests: cursor-retrieval uses extensionKind: ['workspace'] and activationEvents: ['onStartupFinished']. cursor-always-local uses extensionKind: ['ui'] and activates on onStartupFinished plus onResolveRemoteAuthority:background-composer. cursor-agent activates on * (everything). These are all standard VSIX patterns. Even Cursor's most proprietary extensions use the same package.json contract as every marketplace extension.
The remaining 20% — webviews, terminals, debug adapters, notebooks — is the long tail. You get there eventually. But you ship with 80% on day one.
Most "AI code search" tools only index code files. Your .ts, .py, .rs files. Maybe .md if you're lucky. And that's... fine? If all you care about is code.
But real projects aren't just code. Real projects have:
When you ask your AI assistant "how does the authentication flow work?", the answer might be in a code file. Or it might be in the architecture doc from 2023. Or the RFC that proposed the redesign. Or the incident postmortem that explains why the token refresh logic is weird.
If your index only sees code, you're searching with one eye closed.
Kreuzberg opens both eyes. It's a polyglot document intelligence framework with a Rust core. 75+ file formats. PDFs, Office docs, images (with OCR via Tesseract/PaddleOCR), HTML, XML, emails, archives, academic formats. SIMD-optimized. Streaming parsers for multi-GB files. No GPU required.
And because it has a Rust core, it runs natively inside the Anneal extension host. No subprocess. No Python dependency. No Docker container. Just a library call.
Compare this to what Cursor ships: their cursor-retrieval extension bundles a 23 MB Rust addon that handles code search and git operations. Impressive. But it only indexes code files. Kreuzberg would let the same indexing pipeline understand every document in your project — the PDF spec your API implements, the Confluence doc your PM wrote, the architecture diagram someone whiteboarded and photographed.
The indexing pipeline becomes: file watcher detects changes, Kreuzberg extracts text from any file in the project, a code-aware chunker splits it into meaningful pieces (respecting function boundaries for code, section boundaries for docs), a local embedding model generates vectors, sqlite-vec stores them.
Your IDE doesn't just understand your code. It understands your project. Every document. Every format. Every language. Offline.
Let me be precise about what "local-first" means and why I believe it's the correct architectural decision, not just a feature checkbox.
Your code is your most sensitive intellectual property. Every variable name encodes a business concept. Every architecture decision reveals competitive strategy. Every TODO comment exposes technical debt you haven't told investors about. Every git commit message is a record of your team's thinking.
When you send this to a cloud service for indexing, you're trusting that company with:
"We don't train on your code" is a policy decision, not an architectural guarantee. Policies change when companies get acquired, when they raise their next round, when they pivot their business model. The only guarantee is architecture.
Local-first is an architectural guarantee. Your index is a SQLite file on your disk. The embedding model runs on your hardware. The vector search happens in-process. There is no network call. There is no server. There is no policy to change because there is no third party.
This isn't hypothetical paranoia. This is a real constraint for:
But it's also just... better UX. Local-first means:
The tools that index locally are better tools, full stop. The only reason more tools don't do it is because it's harder to build. You can't just throw everything at an API and call it a day. You have to ship the embedding model. You have to optimize the vector search. You have to handle incremental re-indexing when files change. You have to do it all within the memory and CPU constraints of a developer's laptop.
And you can't just bolt it on. Look at Cursor's retrieval host — 830 MB for a single workspace. That's what happens when you run a Rust search engine inside a Node.js process that's also running JavaScript extensions. Two allocators. Two memory models. Two garbage collectors (Rust doesn't have one, but V8 does, and they step on each other). The Rust addon allocates memory that V8 doesn't know about, so V8's heap heuristics are wrong, so it triggers GC at the wrong times, so the whole process gets bloated.
When Rust owns the process, the memory story is clean. One allocator. One memory space. V8 isolates get their own heaps (as they should), but the host controls the ceiling. The intelligence stack runs in the same address space as the extension host. No IPC. No serialization boundary. No duplicated data structures. An indexed code chunk can be handed directly from the vector database to the completion provider without a single copy.
It's harder. But it's right.
There's a hot debate in the AI coding tools space right now: GUI editors (Cursor, Windsurf) vs TUI/CLI tools (Claude Code, aider, goose, OpenCode). The argument goes something like:
Team TUI says: The terminal is composable. It follows the Unix philosophy. You can pipe, redirect, script. It works over SSH. It's accessible. It doesn't lock you into a specific editor. Power users prefer it.
Team GUI says: Coding is inherently visual. Syntax highlighting, indentation, bracket matching, inline diagnostics, git blame decorations — you need to see your code. Discoverability matters. Not everyone wants to memorize commands. The context window of a visual editor is richer than a terminal scroll buffer.
Here's my take: both sides are right, and framing it as a choice is the mistake.
The terminal is great for autonomous, multi-file operations — "refactor the authentication module across 15 files." You describe intent, the AI executes, you review the diff. The terminal's simplicity is a feature here: less UI means less distraction from the task of understanding what the AI did.
The editor is great for interactive, context-rich editing — you see the code, you see the lints, you see the type errors, you see the blame annotations, and you make surgical changes with full visual context. The AI assists inline. You accept or reject in real-time.
These aren't competing paradigms. They're different tools for different parts of the same workflow. The developer who uses Claude Code in the terminal for big refactors and Cursor in the editor for feature work isn't confused — they're sophisticated.
OpenCode is the project that has gone furthest toward solving the multi-interface problem — and the place where it gets stuck is exactly where Anneal picks up.
With 100,000+ GitHub stars and 2.5 million monthly developers, OpenCode is the most successful open-source AI coding agent. And its architecture is genuinely clever: a Bun/JavaScript backend that exposes an HTTP server (using Hono), with multiple clients connecting to it:
opencode web starts the backend server and makes it accessible in a browser. You can even share it over your local network with mDNS discovery and authentication.This is the right idea. One intelligence backend, many interfaces. The server does the work — running tools, calling LLMs, managing sessions, executing bash commands, editing files — and the clients are just views into that work. You can even run opencode attach to connect a TUI to an already-running web server, giving you both interfaces simultaneously on the same session.
OpenCode is also provider-agnostic (75+ LLM providers via the AI SDK), has LSP integration for feeding diagnostics back to the model, supports multi-session parallel agents, and lets you define custom agents with their own system prompts, tool sets, and model choices. It's a remarkably well-designed system.
But it doesn't have an editor.
This is the crucial gap. OpenCode has a TUI, a web UI, and a desktop app — but none of them are a code editor. They're all agent interfaces. You describe what you want, the agent reads files, makes edits, runs commands, and shows you the results. But you never directly edit code in OpenCode. There's no syntax highlighting, no bracket matching, no inline diagnostics, no go-to-definition, no refactoring tools. The /editor command exists but it just opens your system's $EDITOR (vim, nano, whatever) in a temp file — and it's buggy, often opening the wrong file entirely.
This means OpenCode always lives alongside an editor. You run OpenCode in one terminal, and you have Cursor or VSCode or Neovim open in another window to actually look at and interact with the code. The ACP integration helps — it lets Zed or JetBrains host OpenCode as a subprocess — but then OpenCode becomes subordinate to the editor's UI, losing its own TUI and web interfaces.
The architecture tells the story:
OpenCode (the agent):
Bun/JS backend (HTTP server + Hono)
├─ Go TUI client (Bubble Tea)
├─ Web client (browser)
├─ Tauri desktop client
└─ ACP client (editor subprocess)
├─ Zed
├─ JetBrains
└─ Neovim
Tools: bash, read, write, edit, webfetch, LSP diagnostics
Intelligence: LLM providers (cloud APIs)
Indexing: none (relies on LLM context window + file reads)
Compare to what Anneal proposes:
Anneal (the editor + the agent):
VSCode fork (Electron/Chromium renderer)
└─ Rust extension host (anneal-host binary)
├─ V8 isolates (JS extensions: ESLint, Prettier, GitLens...)
├─ Native Rust extensions (dylib trait impls)
├─ Intelligence stack (sqlite-vec, Kreuzberg, embeddings)
└─ Agent runtime (native, in-process)
anneal CLI (TUI client):
└─ Shares sqlite-vec index with the editor
└─ Same intelligence, different interface
OpenCode has the multi-interface vision right. But by not owning the editor, it's permanently a sidecar — a very good sidecar, but a sidecar. It can't provide inline completions as you type. It can't show diagnostics decorations in the gutter. It can't offer a "fix this" code action when you hover over an error. It can't do any of the things that make Cursor feel magical, because those features require being inside the editor, not beside it.
And because OpenCode doesn't have a local index, every query goes to a cloud LLM. The LLM has to read files into its context window to understand your codebase. That works — models are good at it — but it means every question costs tokens, takes network latency, and requires sending your code to someone else's servers. There's no persistent, local understanding of your project.
The lesson from OpenCode: the multi-interface architecture is correct. The client-server split is correct. Being editor-agnostic is wrong.
You can have multiple interfaces — TUI, web, desktop — but one of them needs to be a real editor. Not a sidecar. Not a subprocess. The editor itself. Because the editor is where the developer lives, and the agent needs to live there too, with shared state, shared index, and zero context switching.
Anneal takes OpenCode's insight (separate the intelligence from the interface) and combines it with Cursor's insight (the AI needs to be inside the editor) — while keeping it open source and local-first.
What's missing across all of these tools is a shared intelligence layer. Right now, your terminal AI tool and your editor AI tool have separate indexes, separate context, separate understanding of your codebase. You have to re-explain context when you switch between them.
Look at how Cursor handles this internally: the cursor-agent extension (38 MB, runs in the agent-exec host) and the cursor-retrieval extension (runs in the retrieval-always-local host) are in separate processes. They communicate through IPC. The agent asks the retrieval engine for context, and the results get serialized across a process boundary. That's overhead that exists purely because of the Node.js process-per-host architecture.
OpenCode's architecture is cleaner — the Bun backend is a single process — but it has no persistent local index. Every session starts from scratch. The LLM has to re-read files it already read in a previous session. There's no accumulated understanding.
Anneal's local intelligence stack — the sqlite-vec index, the Kreuzberg-powered extraction, the code-aware chunking — is a process-local library, not a service. It can serve the GUI (the VSCode fork's inline AI features) AND a TUI (an anneal CLI) from the same index. Switch between terminal and editor freely. The intelligence follows you. Same vectors. Same understanding. Zero re-indexing. And unlike OpenCode, it persists between sessions — the index is a SQLite file on disk, incrementally updated as your project changes.
The right answer to "GUI or TUI?" is "yes, and they share a brain."
| VSCode | Cursor | OpenCode | Claude Code | Zed | Lapce | Anneal | |
|---|---|---|---|---|---|---|---|
| Has an editor | Yes | Yes | No | No | Yes | Yes | Yes |
| Extension ecosystem | Massive | Massive | N/A | N/A | Tiny (WASM) | Tiny (WASI) | Massive + native |
| Extension performance | Node.js | Node.js + Rust addon | N/A | N/A | WASM sandbox | WASI sandbox | Rust native + V8 |
| TUI interface | No | No | Yes (Go/Bubble Tea) | Yes | No | No | Yes (planned) |
| Web UI | No | No | Yes | No | No | No | Possible |
| Multi-interface | No | No | Yes (TUI+Web+Desktop+ACP) | No | No | No | Yes (Editor+TUI) |
| AI features | Copilot (cloud) | Deep (cloud) | Deep (cloud, 75+ providers) | Deep (cloud) | Basic | None | Local-first |
| Model agnostic | No (Copilot) | Partial | Yes (75+ providers) | No (Claude only) | Partial | N/A | Yes |
| Local index | No | Partial (Rust addon) | No | No | No | No | Yes (sqlite-vec) |
| Offline AI | No | No | No | No | No | No | Yes |
| Open source | Yes | No | Yes | No | Yes | Yes | Yes |
| Non-code file indexing | No | No | No | No | No | No | Yes (Kreuzberg) |
| Shared index across interfaces | No | No | No | No | No | No | Yes |
There's a question I keep coming back to: is there a world where we don't have to speak the IPC language at all?
Right now, the 13-byte header framing, the binary RPC, the handshake sequence — all of it exists because VSCode's architecture draws a hard line between "the thing that renders pixels" (Chromium/Electron) and "the thing that understands code" (the extension host). Two processes. A pipe between them. A protocol over the pipe.
The IPC protocol is a compatibility tax. It's the price you pay for reusing Electron's renderer. Every completion request takes a round trip: keystroke in the renderer → serialize into binary RPC → write to pipe → context switch to extension host process → deserialize → call the provider → serialize the response → write to pipe → context switch back to renderer → deserialize → render the completion menu. Two context switches, two serialization boundaries, two pipe writes, for every single interaction.
That tax is worth paying — today. Because it gives you the entire VSCode UI for free. Monaco editor, sidebar, terminal, command palette, themes, keybindings. Years of Microsoft's design and engineering. You don't rewrite that on day one.
But what if you didn't have to pay it forever?
Replace the extension host. Keep Electron. Speak the binary IPC protocol fluently. The renderer doesn't know or care that Rust is on the other end of the pipe. You get ecosystem compatibility on day one — every VSCode extension, every theme, every keybinding. This is the pragmatic move.
The cost: two process boundaries, serialization overhead, pipe latency. But the benefit is enormous: you ship something real that works with the existing ecosystem.
Once you control the extension host and have forked the renderer, you own both sides of the pipe. The protocol becomes something you can evolve.
Instead of the full binary RPC format with its 12 message types and $$ref$$ buffer encoding, you could:
The protocol gets thinner as you own more of the stack. Each piece you move from the renderer to the host is one less thing that needs to cross the wire.
Here's the question that changes everything: what does the Electron renderer actually do?
It's Chromium. Drawing text in a <textarea>-like construct (Monaco). Rendering HTML/CSS for the sidebar, the terminal, the command palette. It's a web browser rendering a text editor. That's a staggering amount of machinery for the task of putting colored characters on a screen.
Zed proved the alternative works. Their GPUI framework renders the entire editor UI in Rust using GPU-accelerated paths — no Electron, no Chromium, no HTML, no CSS, no DOM. And the result is faster, uses less memory, and gives the developers more control over every pixel. The text rendering is sharper. The animations are smoother. The input latency is lower. Because there's no browser engine between the keystroke and the screen.
If the Anneal extension host already has:
...then the renderer is reduced to a single job: draw what the host tells it to draw. Colored text. Cursor blink. Gutter decorations. Completion popups. Scrollbars. It's a presentation layer, not a computation layer.
At that point, the architecture collapses into a single process:
Single Rust process:
├─ GPU renderer (wgpu / GPUI-style)
│ └─ Text shaping (cosmic-text or swash)
│ └─ Input handling (winit)
│ └─ UI layout (Taffy or custom)
├─ Editor core
│ └─ Text buffer (rope data structure)
│ └─ Selections, cursors, undo
│ └─ Tree-sitter incremental parsing
├─ Extension host
│ └─ V8 isolates (JS extensions)
│ └─ Native Rust extensions (dylib)
│ └─ Provider registry
├─ Intelligence stack
│ └─ sqlite-vec (vector search)
│ └─ Kreuzberg (document extraction)
│ └─ Local embedding model
└─ Agent runtime
└─ Tool execution (bash, file ops)
└─ LLM interface (local + cloud)
└─ Session management
No IPC. No pipe. No serialization. No context switches between processes. A completion request goes from keystroke → event handler → provider registry → Rust trait call (or V8 Fast API bridge) → response → GPU render. All function calls within one address space.
The latency floor drops from "two context switches and a pipe round-trip" to "a function call and a GPU draw command." We're talking microseconds instead of milliseconds.
And the memory story finally makes sense. One process. One allocator. V8 isolates get their own heaps (as they should), but the host controls the ceiling. No duplicated data structures across process boundaries. No serialized copies of completion items sitting in two different heaps. The 9.5 GB that Cursor uses for 4 windows could become 1-2 GB.
This phased approach isn't novel. The most ambitious project in adjacent territory — Ladybird, the from-scratch web browser — is walking the same path right now, and their story is instructive.
Ladybird is building a completely independent browser engine. No Chromium. No WebKit. No forked code from any existing browser. Written from scratch, funded by donations and sponsorships from Shopify, Cloudflare, 37signals, and GitHub co-founder Chris Wanstrath. The philosophical position is sharp: "The web is too essential to have one primary source of funding, and too important to have that source of funding be advertising."
Sound familiar? Replace "web" with "developer tools" and "advertising" with "cloud AI subscriptions" and you have Anneal's thesis.
But here's the part that really resonates: Ladybird just adopted Rust. In February 2026, Andreas Kling announced the project was transitioning from C++ to Rust after a failed year-long detour through Swift (C++ interop never matured, platform support outside Apple's ecosystem was limited). The first target was LibJS, Ladybird's JavaScript engine. Kling used Claude Code and Codex for human-directed translation — hundreds of small prompts steering the process — and produced 25,000 lines of Rust in two weeks. Zero regressions across 52,898 test262 tests and 12,461 regression tests. Byte-for-byte identical output from both the C++ and Rust pipelines.
The strategy is deliberate: C++ development continues as the main focus. Rust porting runs as a parallel sidetrack with well-defined interop boundaries. The C++ code isn't thrown away — it's gradually replaced, subsystem by subsystem, with verified equivalence at each step.
This is exactly the phasing strategy Anneal uses:
| Ladybird (Browser) | Anneal (IDE) | |
|---|---|---|
| Phase 1 | Build the engine in C++, pass web platform tests | Build the extension host in Rust, speak VSCode's IPC protocol |
| Phase 2 | Port subsystems to Rust with interop boundaries | Thin the protocol, move subsystems from renderer to host |
| Phase 3 | Full Rust engine, independent of all existing browsers | Full Rust process, independent of Electron |
| Ecosystem compat | Must pass web platform tests (the spec IS the ecosystem) | Must run VSCode extensions (the VSIX format IS the ecosystem) |
| Language migration | C++ → Rust (gradual, verified, AI-assisted) | Node.js → Rust (V8 embedded for compat, native for performance) |
Both projects start by being compatible with the dominant ecosystem. Both migrate gradually toward a native Rust foundation. Both use well-defined interop boundaries to keep the old and new worlds coexisting. And both are driven by the same conviction: the thing the entire industry depends on shouldn't be controlled by one company's business model.
Ladybird is doing it for the browser. Anneal is doing it for the editor.
And there's a deeper connection: if Ladybird succeeds, Anneal's Phase 3 gets easier. Right now, dropping Electron means dropping the web rendering engine — which matters for VSCode's webview API, the settings UI, the extension marketplace, and other HTML-based features. But if Ladybird ships a lightweight, embeddable, independent web engine by 2028, Anneal could use it for the HTML rendering bits without carrying all of Chromium. A Rust IDE with a Rust browser engine embedded for the parts that need HTML. The dependencies align.
This is what Zed does — single Rust process, GPU rendering, no Electron. But Zed started from scratch. No VSCode extensions. No ecosystem. They're building a beautiful empty room.
Anneal starts from the opposite end. Phase 1 gives you the ecosystem (VSCode extensions running on embedded V8). Phase 2 proves the Rust extension host in production. Phase 3, when it comes, brings the renderer in-house — but by then, the most important extensions have native Rust ports (through the dual interface system), the V8 compatibility layer is battle-tested, and the intelligence stack is mature.
You end up in the same place as Zed — a single Rust process with GPU rendering — but you got there while carrying the ecosystem with you. Every step of the way, the thing works. Every step, you ship something useful. The protocol is scaffolding. You build with it. Then you remove it.
And if the Ladybird comparison holds, the timeline isn't crazy. Ladybird went from hobby project to funded nonprofit to passing thousands of web platform tests in two years. Kling ported 25,000 lines of his JS engine from C++ to Rust in two weeks with AI assistance. The tools for this kind of migration exist now in a way they didn't even a year ago.
The IPC protocol is not the architecture. It's the migration path.
If I'm being honest about what this could become:
In the near term (Phase 1), it's a faster VSCode. Your extensions load quicker. Your editor uses less memory. One extension host process per window instead of three. No language server child process explosion — the most common language servers could be compiled directly into the host. You get semantic search that works offline. That alone is worth the effort.
In the medium term (Phase 2), it's a platform shift. When you can write editor extensions in Rust with zero-cost abstractions and direct access to system APIs — no WASM sandbox, no Node.js overhead — you unlock a class of extensions that aren't possible today. Real-time audio visualization in the editor. GPU-accelerated rendering of custom views. Native integration with hardware debugging tools. Extensions that are as fast as the editor itself, because they're compiled with the same toolchain. The protocol thins. Shared memory replaces serialization. The two processes start to feel like one.
In the long term (Phase 3), it's a new kind of IDE. A single Rust process. GPU-rendered. Local-first AI. The full VSCode extension ecosystem running on embedded V8 alongside native Rust extensions. No Electron. No Chromium. No browser engine between the developer and the code. The protocol is gone because there's nothing left to separate.
And it's open source. Every layer. Every phase. The scaffolding and the building.
The AI coding tools market is at an inflection point. Cursor showed that developers will pay for AI-powered editing. OpenCode showed that a multi-interface, open-source agent can reach 100k stars. Zed showed that native Rust rendering is the future. But nobody has combined all three: ecosystem compatibility, open-source multi-interface intelligence, and native performance.
That's Anneal. Not today — today it's four Rust crates, a blog post, and a plan. But the architecture is right. The phasing is right. The tools exist. And the need is real.
The foundation exists:
JsRuntime abstraction handles compilation, caching, and execution.$activateByEvent, $startExtensionHost, and routes all 12 RPC message types. Phase 1 scaffolding — built to be removed.Position, Range, Uri, TextEdit, CompletionItem, Diagnostic, etc.) and 10 provider traits (completion, hover, definition, formatting, code actions, code lenses, symbols, diagnostics, references, rename). Plus the Extension trait with manifest parsing and activation lifecycle. This survives all three phases — the traits are the stable API.vscode.* API coverage until Prettier, ESLint, and GitLens work.anneal CLI for terminal-based AI interactions sharing the same index.If you want to follow along or contribute, the code is at repos/fluid-compute and will move to its own org when the time is right. This is going to be a ride.
Anneal: making the brittle VSCode extension ecosystem resilient through controlled heat.
In metallurgy, annealing heats metal past its critical temperature and cools it slowly — relieving internal stresses, increasing ductility, making it workable without being fragile. That's what we're doing to the IDE.
vscode.* API implementation.vscode.d.ts) — The complete TypeScript definition file for the VSCode extension API surface.ipc.net.ts) — The binary wire protocol with header framing, socket layer, and WebSocket support used between the renderer and extension host.extHost.protocol.ts) — The RPC interface contract between the main process and extension host.progrium/vscode-protocol — Unofficial VSCode protocol decoder and documentation.rusty_v8 bindings — Production-stable Rust bindings to V8's C++ API, synced to Chrome's release cadence. 3,800+ stars.rusty_v8 stabilization at v129.0.0 after 150 releases and 3.1 million downloads.IsolateGroup API reference — The V8 C++ API for creating independent pointer compression cages.