Skip to content

Commit 96ea169

Browse files
committed
feat: add lsp runtime mode setting
1 parent 242f27c commit 96ea169

22 files changed

Lines changed: 2025 additions & 8 deletions

docs/superpowers/plans/2026-05-21-lsp-runtime-mode.md

Lines changed: 857 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
# LSP Runtime Mode Design
2+
3+
> Status: Draft
4+
> Date: 2026-05-21
5+
> Scope: `packages/core/src/domain/lsp.ts`, `packages/server/src/lsp/*`, `packages/server/src/commands/{lsp,settings}.ts`, `packages/server/src/server.ts`, `packages/web/src/features/code-editor/*`, `packages/web/src/features/settings/components/settings-page.tsx`
6+
7+
## Goal
8+
9+
Add a user-facing LSP runtime mode setting so users can explicitly turn off LSP and reclaim memory immediately.
10+
11+
The product should:
12+
13+
- preserve the current lazy-start plus idle-reclaim behavior as the default `auto` mode
14+
- let users switch LSP to `off` from Settings
15+
- reclaim LSP memory immediately when switching to `off`
16+
- keep plain editing usable when LSP is disabled
17+
- restore normal on-demand LSP behavior when switching back to `auto`
18+
19+
## Problem
20+
21+
The current LSP lifecycle is service-managed and already avoids eager startup:
22+
23+
- sessions are created only when the editor requests `lsp.ensureSession`
24+
- sessions are reused per `workspace + server kind`
25+
- idle sessions are disposed after `60_000` ms
26+
- workspace teardown disposes all related LSP sessions
27+
28+
This is close to an implicit auto mode, but it is not a user-controlled product feature.
29+
30+
The current design has three gaps:
31+
32+
1. users cannot explicitly disable LSP when they want to reduce memory usage
33+
2. current idle reclaim waits for inactivity instead of reclaiming memory immediately on demand
34+
3. the frontend has no explicit disabled state and cannot distinguish "user turned this off" from unsupported language or missing tools
35+
36+
## Decision
37+
38+
Introduce a global LSP runtime mode with two values:
39+
40+
- `auto`
41+
- `off`
42+
43+
`auto` preserves the current behavior:
44+
45+
- LSP starts on demand
46+
- idle sessions are reclaimed automatically
47+
- supported editor intelligence features remain available
48+
49+
`off` is an explicit hard-disable mode:
50+
51+
- all active LSP sessions are disposed immediately
52+
- all current editor LSP attachments are detached immediately
53+
- diagnostics and LSP notices are cleared from current editor surfaces
54+
- future `lsp.*` requests do not create or restart sessions
55+
56+
This is a global application-level setting, not a per-workspace or per-language setting.
57+
58+
## Product Behavior
59+
60+
### Settings Surface
61+
62+
Add a new control in `Settings > General`:
63+
64+
- title: `LSP Runtime Mode`
65+
- description: `Control code intelligence memory usage.`
66+
- options:
67+
- `Auto``Start on demand and reclaim when idle`
68+
- `Off``Disable LSP and reclaim memory immediately`
69+
70+
This control should use the existing pill-style segmented setting pattern rather than a boolean switch because the design already has a natural multi-mode direction.
71+
72+
### `auto` Mode
73+
74+
`auto` keeps the current system behavior:
75+
76+
- opening a supported file may trigger `lsp.ensureSession`
77+
- session startup remains lazy
78+
- diagnostics, hover, definition, references, declaration, type definition, and document symbols behave as they do today
79+
- idle sessions are reclaimed by the existing TTL logic
80+
81+
### `off` Mode
82+
83+
When the user switches to `off`:
84+
85+
- existing LSP sessions are disposed immediately on the server
86+
- current editor models detach from the LSP bridge immediately on the client
87+
- pending LSP install polling and change-document timers are canceled
88+
- diagnostics markers for attached files are cleared
89+
- future editor activity does not reattach LSP or recreate sessions
90+
91+
Plain editing remains available:
92+
93+
- file loading
94+
- text editing
95+
- saving
96+
- Monaco syntax highlighting
97+
- non-LSP navigation already owned by the workspace UI
98+
99+
LSP-powered features are unavailable:
100+
101+
- hover
102+
- go to definition
103+
- go to declaration
104+
- go to type definition
105+
- references
106+
- document symbols
107+
- diagnostics
108+
109+
### Re-enabling `auto`
110+
111+
When the user switches back to `auto`:
112+
113+
- no LSP sessions are prewarmed
114+
- future supported editor activity resumes the current on-demand startup path
115+
116+
This avoids a memory spike when the user turns LSP back on.
117+
118+
## State Model
119+
120+
### Persistent Setting
121+
122+
Persist the new setting through the existing settings store:
123+
124+
- key: `lsp.mode`
125+
- values: `"auto" | "off"`
126+
127+
The default is `auto` when no stored value exists.
128+
129+
### Shared Domain Type
130+
131+
Add a domain type in `packages/core/src/domain/lsp.ts`:
132+
133+
- `LspRuntimeMode = "auto" | "off"`
134+
135+
Add an explicit disabled readiness result:
136+
137+
- `{ kind: "disabled"; mode: "off"; message: string }`
138+
139+
This prevents the frontend from misclassifying disabled LSP as unsupported language or missing tool.
140+
141+
### Frontend Runtime State
142+
143+
Add a frontend runtime atom for the hydrated mode:
144+
145+
- `lspRuntimeModeAtom`
146+
147+
This atom is driven by server-backed settings hydration and immediate settings changes, not by local-only storage.
148+
149+
## Backend Design
150+
151+
### `LspManager`
152+
153+
Extend `LspManager` to own the current runtime mode.
154+
155+
New responsibilities:
156+
157+
- track `runtimeMode`
158+
- expose `setRuntimeMode(mode)`
159+
- expose `getRuntimeMode()`
160+
- immediately dispose all sessions when switching to `off`
161+
- short-circuit all `lsp.*` behavior when runtime mode is `off`
162+
163+
Behavioral rules:
164+
165+
1. `ensureSession()` returns `{ kind: "disabled", mode: "off", ... }` when mode is `off`
166+
2. `openDocument`, `changeDocument`, `definition`, `references`, `declaration`, `typeDefinition`, `hover`, and `documentSymbols` return `null` without creating sessions when mode is `off`
167+
3. `closeDocument()` remains safe and no-op if no session exists
168+
169+
This makes the backend the final authority for LSP disabled state and protects against frontend race conditions.
170+
171+
### Runtime Mode Command
172+
173+
Add a new LSP runtime command:
174+
175+
- `lsp.setMode`
176+
177+
Input:
178+
179+
- `{ mode: "auto" | "off" }`
180+
181+
Behavior:
182+
183+
- delegates to `ctx.lspMgr.setRuntimeMode(mode)`
184+
- returns `{ mode }`
185+
186+
This command applies runtime state immediately. It does not replace `settings.update`, which remains responsible for persistence.
187+
188+
### Startup Hydration
189+
190+
During server initialization, read `lsp.mode` from `settingsRepo` and apply it to `LspManager`.
191+
192+
This ensures a server restart preserves the user's disabled choice.
193+
194+
## Frontend Design
195+
196+
### Settings Page
197+
198+
Hydrate `lsp.mode` from `settings.get` and render the new General setting.
199+
200+
When the user changes mode:
201+
202+
1. persist with `settings.update({ lsp: { mode } })`
203+
2. apply immediately with `lsp.setMode({ mode })`
204+
3. update frontend runtime state only after both steps succeed
205+
206+
If runtime application fails after persistence succeeds, the UI must not present a false success state. It should either:
207+
208+
- roll the selection back to the last applied runtime mode
209+
- or surface a clear error and reload server-backed settings before allowing further interaction
210+
211+
The simpler and safer implementation is to keep the UI selection unchanged until both requests succeed.
212+
213+
### Monaco Host
214+
215+
`MonacoHost` should subscribe to `lspRuntimeModeAtom`.
216+
217+
When mode is `auto`:
218+
219+
- preserve the current attach flow
220+
221+
When mode is `off`:
222+
223+
- do not call `globalLspBridge.attachModel()`
224+
- if a handle is already attached, run its cleanup immediately through the effect cleanup path
225+
- reset local LSP state so install/retry affordances are not shown
226+
227+
This guarantees the editor surface reflects the disabled state immediately after the setting changes.
228+
229+
### LSP Bridge
230+
231+
The current bridge detach path already does most required cleanup:
232+
233+
- clears change timers
234+
- clears install poll timers
235+
- removes tracked models
236+
- clears diagnostics for the file
237+
- sends `lsp.closeDocument`
238+
239+
The bridge must additionally handle the new disabled readiness result without:
240+
241+
- attempting install flow
242+
- scheduling polls
243+
- attempting follow-up `lsp.openDocument`
244+
245+
### Editor Notice
246+
247+
If the editor shows a notice for disabled LSP, it should be distinct from missing-tool and failed-start notices:
248+
249+
- title: `Language server disabled`
250+
- message: `LSP is turned off in Settings to reduce memory usage.`
251+
252+
No install or retry actions should be shown in this state.
253+
254+
Hiding the notice entirely is also acceptable if the surrounding product already makes the disabled state obvious and there is no empty or confusing UI gap.
255+
256+
## Runtime Transition Flow
257+
258+
### `auto -> off`
259+
260+
1. user selects `Off` in Settings
261+
2. frontend persists `lsp.mode=off` with `settings.update`
262+
3. frontend calls `lsp.setMode({ mode: "off" })`
263+
4. backend updates `LspManager.runtimeMode`
264+
5. backend immediately calls `disposeAll()`
265+
6. frontend runtime atom updates to `off`
266+
7. mounted editors rerun their LSP attachment effect cleanup
267+
8. editor diagnostics and timers are cleared locally
268+
269+
### `off -> auto`
270+
271+
1. user selects `Auto` in Settings
272+
2. frontend persists `lsp.mode=auto`
273+
3. frontend calls `lsp.setMode({ mode: "auto" })`
274+
4. backend updates `LspManager.runtimeMode`
275+
5. frontend runtime atom updates to `auto`
276+
6. later supported editor activity reuses the existing on-demand startup path
277+
278+
## File-Level Impact
279+
280+
### Shared Types
281+
282+
- `packages/core/src/domain/lsp.ts`
283+
- add `LspRuntimeMode`
284+
- add disabled ensure-session result
285+
286+
### Backend
287+
288+
- `packages/server/src/commands/settings.ts`
289+
- accept and return `lsp.mode`
290+
- `packages/server/src/commands/lsp.ts`
291+
- add `lsp.setMode`
292+
- `packages/server/src/lsp/manager.ts`
293+
- track runtime mode
294+
- dispose sessions on `off`
295+
- short-circuit all LSP entry points on `off`
296+
- `packages/server/src/server.ts`
297+
- hydrate stored mode into `LspManager` during startup
298+
299+
### Frontend
300+
301+
- new atom or feature state file for `lspRuntimeModeAtom`
302+
- `packages/web/src/features/settings/components/settings-page.tsx`
303+
- hydrate and save `lsp.mode`
304+
- render General setting UI
305+
- `packages/web/src/features/code-editor/components/monaco-host.tsx`
306+
- attach only in `auto`
307+
- detach immediately on `off`
308+
- `packages/web/src/features/code-editor/lsp/bridge.ts`
309+
- handle disabled readiness cleanly
310+
- `packages/web/src/features/code-editor/components/lsp-status-notice.tsx`
311+
- optionally render disabled-state messaging
312+
313+
## Testing Strategy
314+
315+
### Backend Tests
316+
317+
- `settings.get/settings.update` read and persist `lsp.mode`
318+
- `LspManager.setRuntimeMode("off")` disposes active sessions immediately
319+
- `ensureSession()` returns disabled while off
320+
- `definition/openDocument/...` do not create sessions while off
321+
- `lsp.setMode` command applies runtime state
322+
- server startup hydrates persisted `lsp.mode`
323+
324+
### Frontend Tests
325+
326+
- settings page hydrates `lsp.mode`
327+
- settings page only commits UI state after persistence plus runtime apply succeed
328+
- `MonacoHost` does not attach in `off`
329+
- `MonacoHost` detaches immediately on `auto -> off`
330+
- bridge clears timers and diagnostics during detach
331+
- disabled result does not expose install or retry actions
332+
333+
### End-to-End Coverage
334+
335+
Add a focused workflow test:
336+
337+
1. open a supported file with LSP active
338+
2. switch runtime mode to `off`
339+
3. verify editor stays usable
340+
4. verify LSP interactions no longer activate
341+
5. verify returning to `auto` allows later on-demand LSP recovery
342+
343+
## Risks
344+
345+
### Persisted State But Failed Runtime Apply
346+
347+
If `settings.update` succeeds but `lsp.setMode` fails, persistence and runtime behavior can diverge.
348+
349+
The UI should avoid optimistic success and keep the last known applied mode until both operations succeed.
350+
351+
### Race Conditions During Mode Switch
352+
353+
An editor may still issue `lsp.*` requests while the setting is switching.
354+
355+
The backend must remain authoritative. Frontend cleanup improves UX, but backend short-circuiting is what prevents unwanted session recreation.
356+
357+
### Diagnostic Residue
358+
359+
Without explicit marker clearing, users may still see stale diagnostics after disabling LSP.
360+
361+
Detach cleanup must remain mandatory for all mounted models.
362+
363+
## Out of Scope
364+
365+
This design does not include:
366+
367+
- per-workspace LSP mode
368+
- per-language LSP mode
369+
- a third visible product mode beyond `auto` and `off`
370+
- disabling Monaco's built-in non-LSP language workers
371+
- changing existing idle TTL values
372+
373+
## Recommendation
374+
375+
Ship the setting as:
376+
377+
- `LSP Runtime Mode`
378+
- `Auto` by default
379+
- `Off` as the explicit immediate-memory-reclaim option
380+
381+
This matches the current architecture, gives users control, and solves the concrete memory-reclaim requirement without introducing broader LSP policy complexity.

0 commit comments

Comments
 (0)