Global Editor → full editor experience (LSP-wired, pluggable languages)
Why
The Global Editor overlay (⌘⇧E) is currently a file viewer with a half-finished editing layer bolted on. A full audit (2026-07-09) found that most of the missing "editor experience" is not missing backend — src/main/ipc/editorFs.ts already has read/write-with-mtime-guard/create/rename/delete, and src/main/lspManager.ts already runs a real typescript-language-server — it's missing wiring and UX. This issue is the umbrella for turning the overlay into a real editor.
Scope level (decided): between "solid lightweight editor" and "full IDE" — the TS/JS LSP experience gets fully wired into the real editor (semantic tokens, diagnostics, hover, go-to-definition, completions), and the LSP layer is refactored into a pluggable server registry so additional languages (pyright, rust-analyzer, gopls, …) become "detect on PATH + config entry", without bundling any server binaries (third_party policy: binaries are never committed; bundling via manifest is a possible follow-up per the #119/#120 pattern).
Confirmed bugs (audit findings, with root causes)
1. Closing file tabs does not work
useGlobalEditorStore.closeFile() refuses dirty buffers and returns false expecting the caller to confirm — but GlobalEditorShell.tsx (onCloseFile={path => closeFileAction(activeCwd, path)}) drops the return value and no confirm dialog exists anywhere. One keystroke in a file ⇒ its tab's × silently no-ops forever.
src/renderer/src/features/global-editor/store.ts:339-369
src/renderer/src/features/global-editor/ui/GlobalEditorShell.tsx:298
- Compounding: a failed save (mtime conflict) sets
buffer.error, and MonacoFileEditor.tsx:154-160 renders the error instead of the editor — the user loses sight of their code, and the tab is now both dirty and uncloseable.
- Same dropped-return pattern re-implemented in
AiWorkspaceEditor.tsx (closeFile local variant).
2. Syntax highlighting is intermittently completely blank
Three stacked causes, worst first:
.tsx/.jsx files get a language ID Monaco doesn't have. normalizeCodeLanguage maps tsx → typescriptreact, jsx → javascriptreact (src/shared/code/language.ts). Those are VS Code IDs; Monaco only ships typescript/javascript and nothing ever calls monaco.languages.register for the react variants. Result: the plaintext tokenizer, i.e. zero highlighting for every .tsx/.jsx file — which is "half the files" in a TS/React repo and exactly matches the "works half the time" symptom. (MonacoFileEditor.tsx:52 creates the model with file.language verbatim.)
- Semantic highlighting is globally dead.
ensureSemanticProvider registers LSP semantic-token providers (src/renderer/src/lib/code/monacoRuntime.ts:169-215), but no editor instance ever sets 'editor.semanticHighlighting.enabled': true. Monaco's default is configuredByTheme, and all our custom themes leave it off — so LSP semantic tokens are computed (IPC + tsserver work) and never painted.
- Worker wiring is racy and CSP-fragile.
MonacoEnvironment.getWorker is installed only after the lazy import('monaco-editor') resolves (monacoRuntime.ts:146), and src/renderer/index.html CSP omits worker-src (falls back to script-src 'self'; any blob: worker path dies silently). A worker that fails to start = monarch-only or no tokenization with no visible error.
- Bonus theme bug: every
getMonaco() call re-runs defineThemes() → monaco.editor.setTheme(currentThemeName()) (monacoRuntime.ts:163). Monaco themes are global, so any transcript CodeBlock mounting while the Global Editor is open yanks the editor back onto the dark slab theme, fighting activateEditorTheme/deactivateEditorTheme (features/editor/lib/monacoEditorTheme.ts).
3. LSP exists but the real editor never uses it
src/main/lspManager.ts spawns typescript-language-server --stdio per workspace root, with document sync, semantic tokens, and push diagnostics; IPC in src/main/ipc/lsp.ts, preload in src/preload/api/lsp.ts.
- Only transcript code slabs consume it (
CodeBlock.tsx calls ensureSemanticProvider + openLspDocument + onLspDiagnostics). MonacoFileEditor.tsx — the actual file editor — calls none of it. No diagnostics, no hover, no go-to-def, no completions in the editor.
LspManager today only implements legend/semantic-tokens/diagnostics; hover/definition/completion/references/documentSymbol requests don't exist yet.
4. Editing-experience gaps (backend exists, UX doesn't)
- No file watching: an agent/git write to a clean open buffer is never noticed (buffer goes silently stale; only the next ⌘S fails).
editorFsCache invalidates only on the editor's own writes.
- No create/rename/delete UI —
editor-fs:create-file/create-directory/rename/delete IPC handlers exist (editorFs.ts:256-340) with zero callers in the renderer.
- No save feedback, no dirty-close confirm, no conflict-resolution UX (see bug 1).
- Undo history dies on every tab switch:
MonacoFileEditor disposes the model per switch (MonacoFileEditor.tsx:96-113); buffers live as plain strings in zustand.
- No persistence: open tabs, active file, splitter ratio, tree width all reset on app restart (deliberate-for-now per
store.ts:27-35, revisit here — persist paths and geometry, never unsaved contents).
5. No file search of any kind
- No quick-open (fuzzy filename), no project content search.
features/spotlight is an unrelated agent-view toggle. No ripgrep binding exists anywhere.
- Reusable pieces: the command palette fuzzy scorer (
features/command-palette/lib/rankCommands.ts) and the junk-filtered, root-contained editor-fs:list-directory (single-level; editorFs.ts:117 — its header comment already anticipates a quick-open surface). Needs a recursive-walk IPC and a content-search IPC.
6. No fullscreen editor
- The overlay splitter is hard-clamped to 0.2–0.8 (
store.ts:142-150); the editor can never take the full window. No command, no keybind, no state.
7. Isolation gaps (structure)
features/editor/ is already a clean pure-view layer (good). But:
- Editor commands live in
features/workspace/commands/layoutCommands.ts — there is no editor-owned command module registered in command-palette/registry.ts; new editor commands have no natural home.
CommandPalette.tsx reaches into useGlobalEditorStore directly instead of the ui bridge; editor-store access is scattered across three features.
AiWorkspaceEditor.tsx:19-163 re-implements the entire buffer lifecycle (open/dirty/save/close) in component-local state — parallel to global-editor/store.ts, differing only in the IO adapter (editorFs vs aiWorkspace* IPC).
- Two theme modules (
monacoRuntime.ts + monacoEditorTheme.ts) fight over Monaco's global theme (see bug 2 bonus).
- Stale comments reference a deleted
features/editor/store.ts (global-editor/store.ts:17-25 etc.).
Work plan (one branch/PR, fat implementation plan to follow)
A. Correctness first
- Fix
.tsx/.jsx language mapping for Monaco (model language typescript/javascript + ts-worker JSX compiler options; keep the react IDs only where the LSP protocol needs them).
- Enable semantic highlighting on editor instances; single-owner Monaco theme module (kill the
getMonaco() setTheme-on-every-call fight).
- Install
MonacoEnvironment before/independently of the lazy import; add worker-src 'self' blob: to CSP.
- Dirty-close confirm dialog (shared component, used by Global Editor and AI Workspace); save-conflict banner that keeps the editor visible (reload-from-disk / keep-mine actions) instead of replacing the pane.
B. LSP into the real editor + pluggable registry
5. Refactor LspManager → server registry: per-language server spec (command, args, init options), PATH detection for user-installed servers (pyright-langserver, rust-analyzer, gopls, …), tsserver stays the built-in default. No bundled binaries.
6. Add LSP request methods: hover, definition, completion (+resolve), references, documentSymbol; IPC + preload plumbing.
7. Wire MonacoFileEditor to LSP: document open/change/close sync, diagnostics markers, semantic tokens, hover provider, go-to-definition (cross-file: opens target in a new editor tab at position), completion provider.
8. Model/document registry with ref-counting in the renderer so undo history survives tab switches and models are shared with future split views.
C. Editor experience
9. File watcher on open buffers (clean buffers auto-refresh; dirty buffers get a "changed on disk" state feeding the conflict banner).
10. Explorer context menu: new file / new folder / rename / delete (IPC already exists).
11. Quick Open (⌘P): recursive file-walk IPC (junk-filtered, bounded) + rankCommands fuzzy scoring, editor-owned palette surface.
12. Project content search: main-process search IPC (streamed results, bounded); rg-via-third_party-manifest as a follow-up if JS walk proves too slow.
13. Fullscreen editor command + keybind: editor takes 100% of the workspace area (workspace pane unmount-hidden, not destroyed), Esc/toggle restores previous ratio.
14. Persist per-cwd open-file paths, active file, splitter/tree geometry (never file contents) across restarts.
D. Isolation
15. Editor-owned command module (features/global-editor/commands/) registered in the palette registry; move the 5 editor commands out of layoutCommands.ts; palette uses the bridge, not direct store reach-ins.
16. Extract shared buffer-lifecycle (store factory or hook parameterized by an IO adapter) so AI Workspace stops duplicating it.
17. Comment hygiene: fix stale references to the deleted features/editor/store.ts.
Out of scope (explicit): bundling language-server binaries (follow-up via third_party manifest pattern), git gutter/blame decorations, breadcrumbs, split editor views, multi-root workspaces, refactoring providers (rename-symbol etc.), settings UI for LSP servers.
Verification
- Raw
tsc on both projects (build/vitest don't type-check), plus manual end-to-end: open .tsx file → colored + semantic highlights; introduce type error → squiggle + hover; ⌘-click → cross-file jump; dirty tab close → confirm; external write → refresh/banner; ⌘P → fuzzy open; content search; fullscreen toggle.
Global Editor → full editor experience (LSP-wired, pluggable languages)
Why
The Global Editor overlay (⌘⇧E) is currently a file viewer with a half-finished editing layer bolted on. A full audit (2026-07-09) found that most of the missing "editor experience" is not missing backend —
src/main/ipc/editorFs.tsalready has read/write-with-mtime-guard/create/rename/delete, andsrc/main/lspManager.tsalready runs a realtypescript-language-server— it's missing wiring and UX. This issue is the umbrella for turning the overlay into a real editor.Scope level (decided): between "solid lightweight editor" and "full IDE" — the TS/JS LSP experience gets fully wired into the real editor (semantic tokens, diagnostics, hover, go-to-definition, completions), and the LSP layer is refactored into a pluggable server registry so additional languages (pyright, rust-analyzer, gopls, …) become "detect on PATH + config entry", without bundling any server binaries (third_party policy: binaries are never committed; bundling via manifest is a possible follow-up per the #119/#120 pattern).
Confirmed bugs (audit findings, with root causes)
1. Closing file tabs does not work
useGlobalEditorStore.closeFile()refuses dirty buffers and returnsfalseexpecting the caller to confirm — butGlobalEditorShell.tsx(onCloseFile={path => closeFileAction(activeCwd, path)}) drops the return value and no confirm dialog exists anywhere. One keystroke in a file ⇒ its tab's × silently no-ops forever.src/renderer/src/features/global-editor/store.ts:339-369src/renderer/src/features/global-editor/ui/GlobalEditorShell.tsx:298buffer.error, andMonacoFileEditor.tsx:154-160renders the error instead of the editor — the user loses sight of their code, and the tab is now both dirty and uncloseable.AiWorkspaceEditor.tsx(closeFilelocal variant).2. Syntax highlighting is intermittently completely blank
Three stacked causes, worst first:
.tsx/.jsxfiles get a language ID Monaco doesn't have.normalizeCodeLanguagemapstsx → typescriptreact,jsx → javascriptreact(src/shared/code/language.ts). Those are VS Code IDs; Monaco only shipstypescript/javascriptand nothing ever callsmonaco.languages.registerfor the react variants. Result: the plaintext tokenizer, i.e. zero highlighting for every .tsx/.jsx file — which is "half the files" in a TS/React repo and exactly matches the "works half the time" symptom. (MonacoFileEditor.tsx:52creates the model withfile.languageverbatim.)ensureSemanticProviderregisters LSP semantic-token providers (src/renderer/src/lib/code/monacoRuntime.ts:169-215), but no editor instance ever sets'editor.semanticHighlighting.enabled': true. Monaco's default isconfiguredByTheme, and all our custom themes leave it off — so LSP semantic tokens are computed (IPC + tsserver work) and never painted.MonacoEnvironment.getWorkeris installed only after the lazyimport('monaco-editor')resolves (monacoRuntime.ts:146), andsrc/renderer/index.htmlCSP omitsworker-src(falls back toscript-src 'self'; any blob: worker path dies silently). A worker that fails to start = monarch-only or no tokenization with no visible error.getMonaco()call re-runsdefineThemes()→monaco.editor.setTheme(currentThemeName())(monacoRuntime.ts:163). Monaco themes are global, so any transcript CodeBlock mounting while the Global Editor is open yanks the editor back onto the dark slab theme, fightingactivateEditorTheme/deactivateEditorTheme(features/editor/lib/monacoEditorTheme.ts).3. LSP exists but the real editor never uses it
src/main/lspManager.tsspawnstypescript-language-server --stdioper workspace root, with document sync, semantic tokens, and push diagnostics; IPC insrc/main/ipc/lsp.ts, preload insrc/preload/api/lsp.ts.CodeBlock.tsxcallsensureSemanticProvider+openLspDocument+onLspDiagnostics).MonacoFileEditor.tsx— the actual file editor — calls none of it. No diagnostics, no hover, no go-to-def, no completions in the editor.LspManagertoday only implements legend/semantic-tokens/diagnostics; hover/definition/completion/references/documentSymbol requests don't exist yet.4. Editing-experience gaps (backend exists, UX doesn't)
editorFsCacheinvalidates only on the editor's own writes.editor-fs:create-file/create-directory/rename/deleteIPC handlers exist (editorFs.ts:256-340) with zero callers in the renderer.MonacoFileEditordisposes the model per switch (MonacoFileEditor.tsx:96-113); buffers live as plain strings in zustand.store.ts:27-35, revisit here — persist paths and geometry, never unsaved contents).5. No file search of any kind
features/spotlightis an unrelated agent-view toggle. No ripgrep binding exists anywhere.features/command-palette/lib/rankCommands.ts) and the junk-filtered, root-containededitor-fs:list-directory(single-level;editorFs.ts:117— its header comment already anticipates a quick-open surface). Needs a recursive-walk IPC and a content-search IPC.6. No fullscreen editor
store.ts:142-150); the editor can never take the full window. No command, no keybind, no state.7. Isolation gaps (structure)
features/editor/is already a clean pure-view layer (good). But:features/workspace/commands/layoutCommands.ts— there is no editor-owned command module registered incommand-palette/registry.ts; new editor commands have no natural home.CommandPalette.tsxreaches intouseGlobalEditorStoredirectly instead of theuibridge; editor-store access is scattered across three features.AiWorkspaceEditor.tsx:19-163re-implements the entire buffer lifecycle (open/dirty/save/close) in component-local state — parallel toglobal-editor/store.ts, differing only in the IO adapter (editorFsvsaiWorkspace*IPC).monacoRuntime.ts+monacoEditorTheme.ts) fight over Monaco's global theme (see bug 2 bonus).features/editor/store.ts(global-editor/store.ts:17-25etc.).Work plan (one branch/PR, fat implementation plan to follow)
A. Correctness first
.tsx/.jsxlanguage mapping for Monaco (model languagetypescript/javascript+ ts-worker JSX compiler options; keep the react IDs only where the LSP protocol needs them).getMonaco()setTheme-on-every-call fight).MonacoEnvironmentbefore/independently of the lazy import; addworker-src 'self' blob:to CSP.B. LSP into the real editor + pluggable registry
5. Refactor
LspManager→ server registry: per-language server spec (command, args, init options), PATH detection for user-installed servers (pyright-langserver, rust-analyzer, gopls, …), tsserver stays the built-in default. No bundled binaries.6. Add LSP request methods: hover, definition, completion (+resolve), references, documentSymbol; IPC + preload plumbing.
7. Wire
MonacoFileEditorto LSP: document open/change/close sync, diagnostics markers, semantic tokens, hover provider, go-to-definition (cross-file: opens target in a new editor tab at position), completion provider.8. Model/document registry with ref-counting in the renderer so undo history survives tab switches and models are shared with future split views.
C. Editor experience
9. File watcher on open buffers (clean buffers auto-refresh; dirty buffers get a "changed on disk" state feeding the conflict banner).
10. Explorer context menu: new file / new folder / rename / delete (IPC already exists).
11. Quick Open (⌘P): recursive file-walk IPC (junk-filtered, bounded) +
rankCommandsfuzzy scoring, editor-owned palette surface.12. Project content search: main-process search IPC (streamed results, bounded); rg-via-third_party-manifest as a follow-up if JS walk proves too slow.
13. Fullscreen editor command + keybind: editor takes 100% of the workspace area (workspace pane unmount-hidden, not destroyed), Esc/toggle restores previous ratio.
14. Persist per-cwd open-file paths, active file, splitter/tree geometry (never file contents) across restarts.
D. Isolation
15. Editor-owned command module (
features/global-editor/commands/) registered in the palette registry; move the 5 editor commands out oflayoutCommands.ts; palette uses the bridge, not direct store reach-ins.16. Extract shared buffer-lifecycle (store factory or hook parameterized by an IO adapter) so AI Workspace stops duplicating it.
17. Comment hygiene: fix stale references to the deleted
features/editor/store.ts.Out of scope (explicit): bundling language-server binaries (follow-up via third_party manifest pattern), git gutter/blame decorations, breadcrumbs, split editor views, multi-root workspaces, refactoring providers (rename-symbol etc.), settings UI for LSP servers.
Verification
tscon both projects (build/vitest don't type-check), plus manual end-to-end: open .tsx file → colored + semantic highlights; introduce type error → squiggle + hover; ⌘-click → cross-file jump; dirty tab close → confirm; external write → refresh/banner; ⌘P → fuzzy open; content search; fullscreen toggle.