Skip to content

WIP: LSP integration #3118

Open
terion-name wants to merge 47 commits intocoder:mainfrom
terion-name:lsp-integration
Open

WIP: LSP integration #3118
terion-name wants to merge 47 commits intocoder:mainfrom
terion-name:lsp-integration

Conversation

@terion-name
Copy link
Copy Markdown

@terion-name terion-name commented Apr 3, 2026

In short

This merge request adds a built-in LSP integration to mux. Implementation is heavily inspired by OpenCode LSP architecture. Currently it auto-provisions TypeScript, Python, Go and Rust lsp's. Feature is gated by experiment flag, auto-provisioning is settings-controlled. Multiple runtimes supported.

I did test what I was able to. It would be great if you also take som time to tackle around with it

In details

It introduces:

  • a backend LSP subsystem managed by mux itself
  • an agent-facing lsp_query tool for semantic code navigation
  • automatic post-edit diagnostics after supported structured code changes
  • a workspace diagnostics API and a minimal Stats → Diagnostics UI
  • trust-aware, policy-driven provisioning for built-in LSP servers
  • polling-based diagnostics refresh for already-known files edited outside mux tools
  • CLI support for the same LSP feature set

The result is that LSP becomes useful in three places at once:

  1. Agent reasoning and code investigations via lsp_query
  2. Agent repair loops via post-edit diagnostics
  3. User inspection via the Diagnostics panel

What this adds

1. Built-in backend LSP subsystem

Mux now owns LSP clients in the backend instead of relying on editor/plugin integration.

2. Agent-facing semantic queries

Adds the experiment-gated lsp_query tool with support for:

  • hover
  • definition
  • references
  • implementation
  • document_symbols
  • workspace_symbols

This gives the model semantic code navigation instead of grep-only exploration.

3. Automatic post-edit diagnostics

After supported structured edits, mux now refreshes LSP state and returns fresh diagnostics in the tool result.

Currently wired into:

  • file_edit_replace_string
  • file_edit_replace_lines
  • file_edit_insert
  • task_apply_git_patch

This creates a practical edit → diagnose → repair loop for the agent.

4. Workspace diagnostics snapshot + live UI

Adds workspace-scoped diagnostics APIs and a minimal UI surface:

  • backend:
    • workspace.lsp.listDiagnostics
    • workspace.lsp.subscribeDiagnostics
  • frontend:
    • Right Sidebar → Stats → Diagnostics

The panel shows:

  • loading state
  • retry/error state
  • empty state
  • grouped file diagnostics
  • severity summary
  • source/code/location metadata

5. Provisioning and trust policy

Adds policy-driven launch/provisioning for built-in servers with config:

  • lspProvisioningMode: "manual" | "auto"

Built-in policies now cover:

  • TypeScript
  • Python / Pyright
  • Go / gopls
  • Rust / rust-analyzer

Provisioning is trust-aware:

  • trusted workspaces can use trusted repo-local execution and auto provisioning
  • untrusted workspaces cannot use repo-local execution or automatic provisioning

6. Diagnostics refresh after ordinary editor saves

Adds polling-based refresh for already tracked LSP files, so the diagnostics panel can update after out-of-band edits made in an editor or terminal.

This is intentionally scoped:

  • no whole-repo indexing
  • no full filesystem watch abstraction
  • only files already known to the LSP subsystem
  • polling only runs while diagnostics listeners are active

7. CLI parity

The CLI participates in the same LSP model:

  • request-scoped lsp-query experiment wiring
  • effective provisioning mode handling
  • trust-aware temp-config setup for mux run

Architecture

Backend responsibilities

LspManager

Owns orchestration:

  • descriptor selection
  • root resolution
  • client caching/reuse
  • launch-plan caching
  • diagnostics caching
  • publish receipt freshness tracking
  • idle cleanup
  • tracked-file polling
  • query normalization

LspClient

Owns protocol interaction:

  • process startup from a resolved launch plan
  • initialize / shutdown
  • didOpen / didChange / didClose
  • JSON-RPC request/response handling
  • forwarding publishDiagnostics

LspStdioTransport

Owns framed stdio JSON-RPC transport and serialized writes.

Launch policy layer

Launch/provisioning is separated from protocol logic:

  • lspServerRegistry.ts defines built-in server policies
  • lspLaunchResolver.ts chooses the final launch plan
  • lspLaunchProvisioning.ts handles provisioning/probing mechanics

This keeps:

  • transport
  • protocol
  • launch policy
  • workspace lifecycle

cleanly separated.


Integration points

Service graph / lifecycle

The LSP manager is wired into the backend service graph and workspace lifecycle, so clients are owned and cleaned up by mux.

Relevant files include:

  • src/node/services/coreServices.ts
  • src/node/services/serviceContainer.ts
  • src/node/services/aiService.ts
  • src/node/orpc/context.ts
  • src/node/orpc/router.ts

Tool system

lsp_query is added to tool definitions and availability wiring:

  • src/common/utils/tools/toolDefinitions.ts
  • src/common/utils/tools/tools.ts
  • src/common/types/tools.ts
  • src/node/services/tools/lsp_query.ts

File mutation hooks

Structured file-mutating tools now call a shared onFilesMutated(...) hook, which AIService maps to LspManager.collectPostMutationDiagnostics(...).

Frontend store

Diagnostics snapshots flow through:

  • src/browser/stores/WorkspaceStore.ts

This store subscribes lazily and exposes diagnostics state to the UI.

Settings / config

Provisioning mode is wired through:

  • config schema
  • backend config loading/saving
  • ORPC
  • settings UI

CLI

CLI support is wired through:

  • src/cli/run.ts
  • src/cli/runOptions.ts
  • src/cli/runTrust.ts

How it works end-to-end

Explicit agent query

  1. Model calls lsp_query
  2. mux resolves the correct built-in server and root
  3. mux starts or reuses an LSP client
  4. mux syncs file contents if needed
  5. JSON-RPC query runs
  6. mux returns normalized locations/symbols/hover back to the model

Post-edit repair loop

  1. Agent edits code using a supported structured tool
  2. Tool invokes onFilesMutated(...)
  3. AIService asks LspManager for fresh diagnostics
  4. mux re-syncs changed files into the LSP client
  5. mux waits for fresh publishDiagnostics
  6. diagnostics are appended to the tool result

User diagnostics UI

  1. Backend caches diagnostics from publishDiagnostics
  2. ORPC exposes workspace snapshot/subscription
  3. WorkspaceStore subscribes lazily
  4. Stats → Diagnostics renders the current workspace diagnostics state

Built-in server behavior

TypeScript

Supports:

  • trusted workspace-local typescript-language-server
  • PATH typescript-language-server
  • package-manager exec fallback
  • ancestor TypeScript discovery via tsserver.path
  • nested package / monorepo handling
  • fallback provisioning of typescript when needed

Python

Supports:

  • PATH pyright-langserver
  • package-manager exec fallback for pyright

Go

Supports:

  • PATH gopls
  • managed go install fallback into mux-managed tools dir

Rust

Supports:

  • PATH rust-analyzer
  • explicit unsupported error when unavailable

Trust and runtime handling

This MR is intentionally conservative about execution safety.

Trust

LSP launch behavior is driven by:

  • provisioning mode
  • trusted vs untrusted workspace execution

Runtime behavior

Launch behavior is runtime-aware:

  • local/worktree runtimes can safely use sanitized PATH overrides for untrusted pathCommand
  • remote/container-style runtimes do not get host-local PATH injected
  • MultiProjectRuntime is unwrapped so trust/path decisions follow its primary runtime behavior

Path mapping

LspPathMapper keeps host/runtime paths aligned so LSP can run inside the runtime environment while mux still reports useful paths to agents and UI.

Known limitations

This MR intentionally does not add:

  • editor hover / go-to-definition UI
  • explorer context LSP actions
  • whole-repo diagnostics indexing
  • rich diagnostics navigation UI
  • auto-install support for every ecosystem/runtime combination

The UI surface is intentionally small:

  • Stats → Diagnostics

The polling refresh is also intentionally narrow:

  • tracked files only
  • listener-driven

@terion-name
Copy link
Copy Markdown
Author

@ibetitsmike ?

Copy link
Copy Markdown
Contributor

@ibetitsmike ibetitsmike left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mux working on behalf of Mike.

I found two blocking concurrency issues in the new LSP manager and transport paths. I left both inline below.

Comment thread src/node/services/lsp/lspManager.ts Outdated
Comment on lines +194 to +200
const client = await this.clientFactory({
descriptor,
runtime,
rootPath,
rootUri,
});
workspaceEntry.clients.set(clientKey, client);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mux working on behalf of Mike.

If two lsp_query calls hit the same workspace and root concurrently, both callers can observe no existing client here, both await clientFactory(), and the later one overwrites the earlier client in workspaceEntry.clients. That leaks an orphaned LSP process outside manager tracking. Please dedupe in-flight creation per (workspaceId, clientKey) or otherwise serialize this path, then add a concurrent query test that asserts the factory only runs once.

Comment on lines +52 to +55
const body = this.encoder.encode(JSON.stringify(message));
const header = this.encoder.encode(`Content-Length: ${body.byteLength}\r\n\r\n`);
await this.stdinWriter.write(header);
await this.stdinWriter.write(body);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mux working on behalf of Mike.

This method writes one LSP frame as two separate awaited writes, header then body. If two requests or notifications share this transport concurrently, the byte stream can interleave as headerA, headerB, bodyA, bodyB, which breaks Content-Length framing. Please serialize writes, or write each framed message in one critical section, and add a concurrent send test for it.

@ibetitsmike
Copy link
Copy Markdown
Contributor

hey @terion-name chatted with the team and I have a good/bad news type of situation.
this work in this work will not land in main. we've noticed the same thing you did - quite a lof of complexity in our code and we're in the process of architecting and implementing a breakdown into a core system + extensions (and moving some of the existing functionalities into those). this would be a great candidate for such. please hop onto our discord and we can have a more direct comms.

in the meantime - you can definitely finish this implementation in your fork and play around with it.

Add shared workspace LSP diagnostics schemas, backend list/subscribe plumbing, browser WorkspaceStore caching/hooks, and focused coverage for manager/store/server paths.
Reconnect active workspace diagnostics subscriptions after client swaps, retry diagnostics streams when they end unexpectedly, and settle pending LSP diagnostic waits during workspace disposal.
Add a Diagnostics sub-tab to the Stats sidebar and render grouped LSP diagnostics
from the workspace store without subscribing while the tab is inactive.

Add focused tests for the new diagnostics panel states and the Stats sub-tab list.
Separate LSP server metadata from resolved launch plans, add manager-side launch resolution/provisioning helpers, and cover the new seams with focused tests.
Update the policy-context cache-key test to prove trusted workspaces can still use the repo-local TypeScript server while untrusted workspaces fall back to an external PATH entry after workspace-local PATH entries are sanitized.
Poll tracked LSP client files so workspace diagnostics refresh after out-of-band saves.

Add a tracked-file accessor to LspClient, start a manager-owned polling loop, and reuse the existing ensureFile/publishDiagnostics path so Stats → Diagnostics updates without UI or ORPC changes. Add focused coverage for tracked-file exposure, empty-cache polling refreshes, and failure tolerance when a tracked file disappears.
Keep poll-driven diagnostics refreshes from looking idle and reset per-workspace
poll/serialization bookkeeping on disposal so recreated workspaces do not inherit
stale in-flight state.
Search ancestor directories up to the workspace root for TypeScript and package-manager metadata, and provision a fallback TypeScript package when package-manager exec has no usable workspace tsserver.
Ensure untrusted PATH-based LSP resolution always probes and launches with an explicit sanitized PATH.
Separate persisted config reads from the effective runtime config so MUX_LSP_PROVISIONING_MODE stays read-time only, and copy the effective provisioning mode into mux run's ephemeral config.
Keep the sanitized PATH for untrusted pathCommand probing, but stop copying
that synthetic env onto the final launch plan unless the launch policy
explicitly requested env overrides.

Add a regression test covering untrusted pathCommand launches without an
explicit env block.
Use the full workspace path when sanitizing untrusted pathCommand PATH
entries so nested package roots cannot inherit repo-level node_modules/.bin
entries. Also unwrap MultiProjectRuntime to its primary runtime so host-local
wrappers still sanitize while Docker/devcontainer-style wrappers keep their
remote PATH untouched.
Handle workspace_symbols queries that target a directory like ./ by inferring the matching built-in server from language-specific root markers, skipping file opens when no concrete file is needed, and failing clearly when the directory matches multiple server types.
Prune package-only TypeScript descendants from directory workspace_symbols discovery, warm TypeScript roots with a representative source file before workspace/symbol requests, and summarize repeated root failures so mixed-language monorepos return more useful results.
Prefer exact symbol-name matches when selecting a TypeScript representative file for directory workspace_symbols warm-up, and rank exact path token matches ahead of loose substring matches.

Add a repo-root regression test covering the Teleport ResourceService vs ResourcesService case.
Return one workspace_symbols success item per usable directory root, preserve per-root metadata, and surface clearer no-root/no-usable-root failures. Update tool schemas and focused tests to cover the new contract.
Add structured skipped root metadata, richer symbol fields, and disambiguation hints for directory workspace_symbols queries.
Postprocess Go workspace_symbols results so exact symbol-name matches win by default behind EXPERIMENT_LSP_GO_EXACT_MATCH_SYMBOLS, while preserving the original fuzzy output when the env var is explicitly set to false.

Add focused coverage for both single-root and directory workspace_symbols queries.
When any directory workspace_symbols root returns an exact match, drop Go-only
fuzzy groups unless that Go root also has an exact match. This keeps nested
TypeScript exact hits from getting buried under unrelated gopls noise while
preserving the existing env-var escape hatch.
@terion-name
Copy link
Copy Markdown
Author

hey @terion-name chatted with the team and I have a good/bad news type of situation. this work in this work will not land in main. we've noticed the same thing you did - quite a lof of complexity in our code and we're in the process of architecting and implementing a breakdown into a core system + extensions (and moving some of the existing functionalities into those). this would be a great candidate for such. please hop onto our discord and we can have a more direct comms.

in the meantime - you can definitely finish this implementation in your fork and play around with it.

@ibetitsmike I have pushed implementation to usable state. Tested and tuned on complex multi-language monorepo (like Teleport), fine-tuned behaviors and output shapes for proper agentic understanding.

In general this stuff has a lot of caveats and needs a lot of case-testing. But if we need to wait architectural changes, at this point it doesn't make sense to continue.

It would be great if you in your team also play around with it for some feedback. And when designing extension system to take into consideration this code. Ping me when new arch will be ready, I'll port this

PS
I'll also duplicate this message in discord channel, didn't find you there

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants