Braide supports the Language Server Protocol (LSP) on a per-project basis. When a file in the embedded editor matches a configured server's globs, Braide spawns the server as a subprocess, brokers Monaco's provider calls (hover, go-to-definition, references, document symbols, workspace symbols) over stdio, and surfaces server-pushed diagnostics as Monaco markers (squiggles).
LSPs are configured per project — different projects can use different servers, different commands, even different versions of the same server, and they don't interfere with each other.
Each project can have any number of LSP entries. Each entry is a tuple of { name, command, args, fileGlobs, workspaceRoot, enabled } describing one server process. When an entry is enabled and a file in the editor matches one of its globs, the server starts (if it isn't already running) and Monaco's providers route through it.
The chip bar above the session view shows one chip per configured LSP, colour-coded by current status. Click a chip to open its action popover — Disable / Enable / Restart, and (when in error) Show error detail which surfaces the last 50 lines of the server's stderr — useful for diagnosing servers that fail to start or crash on initialize.
| Status | Dot colour | Animation | What it means |
|---|---|---|---|
off (disabled) | grey | static | The entry exists but is not running — either you toggled Enabled off in settings, or the project's LSPs have not been started yet. |
starting… | amber | pulsing | The WebSocket bridge is open and the client is awaiting the server's initialize response. |
ready | green | static | initialize round-trip complete; Monaco providers are registered and diagnostics are wired. |
indexing… | blue | pulsing | The server has signalled (via $/progress) that it is doing first-time work — parsing tsconfig.json, walking node_modules, building the project graph, etc. Hovers and go-to-definition may return incomplete results — including any for TypeScript types — until the chip turns green. For real-world TypeScript projects this can take several seconds on first launch. |
reconnecting… | amber | pulsing | The WebSocket dropped after ready (network loss, sleep/wake, bridge restart) and the lifecycle is opening a fresh socket. The chip returns to ready if the reconnect succeeds, or moves to error if attempts are exhausted. |
error | red | static | Failed before/while reaching ready, or reconnect attempts were exhausted. The popover gains a Show error detail item with the renderer-side failure message merged with the server's stderr tail. |
The workspace symbol search modal (Cmd+Shift+F) queries every ready LSP for the project in parallel and merges results; an empty configured set surfaces the "No language servers configured" empty state.
LSP servers advertise their feature set in their initialize response. Braide silently skips registering Monaco providers for capabilities the server doesn't claim — there's no error, and other capabilities the server does claim still work. For example, pyright doesn't implement textDocument/typeDefinition; Braide notices that, doesn't register a TypeDefinitionProvider, and lets Monaco fall back to its default behaviour.
Diagnostics (squiggles) are server-pushed via textDocument/publishDiagnostics. There's no corresponding capability flag — Braide always subscribes; servers that don't publish diagnostics simply never wake the listener.
Open Settings → Language Servers for the active project. The empty state offers a New Language Server button; once you have one entry the same button moves into the section header.
The edit form has these fields:
TypeScript, Rust, pyright).--stdio (or omit args entirely if the server defaults to stdio).apps/frontend for a TypeScript server scoped to that app).The glob matcher in v1 supports **, *, ?, and {a,b} brace alternation. Negation (!pattern) and character classes ([abc] / [a-z]) are not handled — they are treated as literal characters, so a glob like **/*.[ch] will silently fail to match any C / header file. If you need richer matching, list each extension explicitly: **/*.c and **/*.h on separate lines.
The recipes below show one working configuration per language. Substitute paths to your own toolchain locations as needed.
Install:
npm install -g typescript-language-server typescript
Entry:
| Field | Value |
|---|---|
| Name | TypeScript |
| Command | typescript-language-server |
| Arguments | --stdio |
| File globs | **/*.ts<br>**/*.tsx<br>**/*.js<br>**/*.jsx |
When a TypeScript LSP attaches to a .ts / .js model, Braide automatically suppresses Monaco's bundled TypeScript worker for hovers, definitions, references, document symbols, signature help, rename, and completions to avoid duplicate UI from the two providers.
Install:
rustup component add rust-analyzer
Entry:
| Field | Value |
|---|---|
| Name | Rust |
| Command | rust-analyzer |
| Arguments | (none) |
| File globs | **/*.rs |
Install:
go install golang.org/x/tools/gopls@latest
Entry:
| Field | Value |
|---|---|
| Name | Go |
| Command | gopls |
| Arguments | (none) |
| File globs | **/*.go |
Install:
npm install -g pyright
Entry:
| Field | Value |
|---|---|
| Name | Python |
| Command | pyright-langserver |
| Arguments | --stdio |
| File globs | **/*.py |
Install (macOS):
brew install lua-language-server
Entry:
| Field | Value |
|---|---|
| Name | Lua |
| Command | lua-language-server |
| Arguments | (none) |
| File globs | **/*.lua |
| Shortcut | Action |
|---|---|
Cmd+Shift+F / Ctrl+Shift+F | Open the workspace symbol search modal |
Cmd+Shift+O / Ctrl+Shift+O | Open the document (current-file) symbol picker — built into Monaco |
Cmd+Shift+F follows the conventional "find across project" binding used by VS Code and JetBrains. Plain Cmd+F is left untouched so it remains the in-page find shortcut inside Monaco and the rest of the app — stealing it would surprise users who reach for find-in-file every minute. The Electron menu's Cmd+T is reserved for Toggle Terminal, so the renderer never sees a plain Cmd+T keydown anyway.
The two pickers are complementary: Cmd+Shift+O is fast and offline (Monaco walks the document symbols already cached for the current file); Cmd+Shift+F queries every ready LSP in parallel and surfaces matches across the workspace.
The workspace symbol search modal is also reachable from the session header toolbar — the magnifying-glass icon next to the file-browser and GitHub buttons opens the same modal as Cmd+Shift+F, so the action is discoverable without users having to know the keybinding.
Both activation paths require a session to be selected. Language servers are tied to the active session, so workspace symbol search is only meaningful while a session is open. Triggering the shortcut from the home/sessions list view surfaces an info-level toast that explains the dependency rather than opening an empty modal.
spawn ENOENT on the macOS desktop appGUI applications launched on macOS do not inherit your shell's PATH. A server that resolves correctly in a terminal (because PATH includes Homebrew, asdf, nvm, fnm, etc.) will fail to spawn from the Braide desktop app with an ENOENT error.
Symptom: the chip flips to error immediately after enabling the entry. Clicking the chip and choosing Show error detail surfaces the line:
Command not found on PATH: '<command>'. Try using an absolute path (e.g.
/usr/local/bin/<command>) — the desktop app does not inherit your shell PATH on macOS.
Fix: replace the bare command with its absolute path. Run which <command> in a terminal to find it (which typescript-language-server → /Users/you/.nvm/versions/node/v25.0.0/bin/typescript-language-server), then paste that into the Command field.
readyThe chip stays in starting for more than a few seconds. This almost always means the server is failing the initialize handshake — usually because it can't find its workspace root or a runtime dependency.
Click the chip, choose Show error detail (visible once the chip transitions to error), and read the last few stderr lines. Common causes:
pyright reports it can't find the active virtualenv — set the Workspace root to the directory containing pyproject.toml / setup.py.rust-analyzer reports Cargo.toml not found — same fix, but for the Rust workspace.typescript-language-server reports it can't find tsserver — install typescript alongside it (npm install -g typescript).any right after the server startsYou hover a typed value and the popup shows any (or the symbol's type is missing entirely), but the chip is green. Check the chip colour: if it's blue (indexing), the server is still loading the project graph and hover responses are incomplete by design. Wait for the chip to turn green before relying on type information.
If the chip is green and hovers still come back as any, the most likely causes are:
tsconfig.json include set). Check the Workspace root for the entry.pyright without a pyproject.toml will run in single-file mode). Restart the chip after fixing the config.The file is open, you've intentionally introduced an error, but no squiggle appears. Check, in order:
ready (not off, starting, indexing, reconnecting, or error).[ch] / [a-z] character classes don't work.pyright won't process .pyi unless explicitly globbed).LSPs can be memory-hungry — rust-analyzer and tsserver regularly use 1–2 GB on real-world workspaces. With three project tabs open, each running a TypeScript and a Rust server, Braide's resident memory can reach 6–10 GB. This is the LSPs' usage, not the editor's; the desktop app and the renderer themselves stay in the low hundreds of megabytes.
If you hit memory pressure, close project tabs you're not actively using. Stopping a project tab tears down every LSP attached to it, freeing the subprocess memory immediately.
**, *, ?, and {a,b}. Negation (!pattern) and character classes ([abc] / [a-z]) are not handled and silently mis-match.Cmd+T cannot be used as the workspace-symbol shortcut on the desktop app — Electron's menu accelerator (Toggle Terminal) preempts it. Braide uses Cmd+Shift+F instead, which also avoids stealing Monaco's plain Cmd+F in-page find.targetUri even when it points outside the project worktree, but no special handling is provided for those out-of-tree paths beyond what Monaco's editor opener does by default.