AIDE Architecture — Phase 3: Editor (Monaco)
SkillSearchPhase 3 SOP for the AIDE offline-first IDE rebuild, the Monaco editor core: locally bundled monaco-editor with Vite worker wiring (NO CDN), model/view separation (URI-keyed models), open/save round-trip via daemon contract, native undo/redo, find/replace + workspace search, large-file handling (Monaco virtualization replaces the custom windowing), dirty state, split editor groups, view-state restore, and LSP wiring points (didOpen/didChange versioning, markers). Use whenever wiring editor open/save/tabs/find/split/LSP, debugging "editor blank", "undo lost", "model mismatch", or worker loading failures. Research-grounded (github.com/microsoft/monaco-editor, VS Code engineering docs).
Use AIDE Architecture — Phase 3: Editor (Monaco) in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add AIDE Architecture — Phase 3: Editor (Monaco) and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the AIDE Architecture skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; Ahel provides instructions and does not run this skill.
No other account needed.
Add Ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
What this skill tells your AI
The instructions your AI receives, as published by anonymousnomad/covert-coder in skills/packs/aide-arch-editor/SKILL.md and read by Ahel’s review.
Doctrine
- Monaco, offline. The exact engine VS Code uses. Bundled locally via npm — NO CDN, no loader from jsdelivr. All workers local. Fonts local.
- Model/View separation (the core Monaco law):
editor.createModel(content, language, uri)creates the MODEL (content + language + edit history, URI-keyed, process-wide).monaco.editor.create(dom, { model })creates a VIEW of that model. One model, many views (split editors). View state is serializable and restorable. - The daemon owns the filesystem, Monaco owns the text. The frontend never writes files directly (renderer rule); save = push model content to the daemon file route.
- Native undo/redo. Monaco has a built-in, correct undo/redo stack per model. The custom undo-stack.mjs is superseded for the editor (its verified cap logic survives conceptually in session/ops hygiene, but Monaco's stack is the source of truth). Never reimplement undo on top of Monaco.
- Verify before claiming: open/save/find/split round-trips verified live in the real browser at 127.0.0.1:4173 before a phase is called done.
What
Phase 3 replaces the hand-rolled editor (textarea + renderEditorText + custom windowing + custom undo) with Monaco:
- Monaco bootstrap: local import, CSS, worker wiring for Vite (see How #1).
- File lifecycle: open (daemon GET /api/file → tooLarge gate → model create or "file too large" fallback path), save (dirty → daemon POST), close (dirty guard, LSP didClose, diagnostics clear — ported verified behavior).
- Tabs + splits: tab bar = open models (uri-keyed); split = second editor VIEW on the same model; view states saved per tab in the session.
- Find/replace: Monaco's built-in find widget (options: case, regex, whole word, selection) + workspace search/replace via daemon contract (the verified /api/search/replace semantics: approved flag, dotfile/node_modules skip, 20k cap, ≤512KiB per file).
- Large files: Monaco renders virtualized lines natively — the custom WINDOW_LINES=40 windowing is deleted. The 1MiB too-large gate stays as the load guard (verified:
{too_large:true, size}→ user prompt to open raw/read-only or skip). - Dirty state + hot-exit: dirty flag per model; save prompts on close; session persists open models + view states + caret (shared zod schema).
- LSP wiring points (protocols in Phase 5): didOpen on model create, didChange debounced with version counter, didSave on save, didClose on close, publishDiagnostics → monaco markers, completions/hover/definition commands bound to Monaco APIs.
- Theme/fonts: dark theme (match current styles), monospace font bundled locally, correct font metrics so cursor/line height are stable.
How
1. Monaco + Vite worker wiring (the classic failure point)
- Install
monaco-editoras a local dep. NOmonaco-editor/esm/vs/loaderCDN usage. - Workers: Monaco needs web workers for its language services (typescript/json/css/html basics). With Vite, import workers explicitly:
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; self.MonacoEnvironment = { getWorker(_, label) { if (label === 'typescript' || label === 'javascript') return new tsWorker(); return new editorWorker(); } }; - Import the ESM entry + css:
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import 'monaco-editor/min/vs/editor/editor.main.css';(or the esm css path matching the version). - Vite
build.assetsInlineLimit/ worker format: workers are emitted as separate chunks — verify they exist indist/after build and are served LOCALLY (offline rule: no worker from remote origin). - Version pinning: lock the exact monaco version in package.json (upgrades change worker/API surface; deliberate upgrades only).
2. Model registry (the editor's source of truth)
// browser/src/editor/models.ts
const models = new Map<string, monaco.editor.ITextModel>(); // key: workspace-relative path
export function getOrCreateModel(relPath: string, content: string, language: string): ITextModel
export function disposeModel(relPath: string): void
export function dirty(relPath: string): boolean
- Language mapping: extension → languageId (js/ts → typescript, py → python, html/css/json/md/gitignore…). Registered via
monaco.languages.registerif a language is missing. - One model per file, process-wide — split views share it; LSP didOpen fires once per model, not per view.
- URI convention:
inmemory://model/<relPath>or a plainfile://-style scheme — pick ONE and keep it in the contract/session schema (restore must reproduce the same URI).
3. Open/save round-trip
- Open:
api.file.read(relPath)→ envelope → iftooLarge→ show the gate UX (no model created; offer read-only raw view via the same route with raw=true if the daemon supports it, else skip). Else create model, set value, mark clean, notify LSP didOpen. - Save:
model.getValue()→api.file.write(relPath, content)→ on ok mark clean + LSP didSave; on error envelope → keep dirty + toast the code. - Debounce: save on Ctrl+S + on tab close + on window/pagehide flush (port the verified pagehide flush pattern).
- Auto-save option (if the old app had it) stays opt-in.
4. Find/replace
- In-file: Monaco find widget (
findaction), configurefind.addExtraSpaceOnTop, case/regex/whole-word/selection toggles; results scroll the view automatically (Monaco handles it — no manual scrollIntoView hacks). - Workspace:
api.search.replacecontract ported verbatim (approved flag — UI shows a confirm dialog with occurrence count BEFORE sending approved:true; dotfiles/node_modules/target/.git/dist/build skipped; per-file ≤512KiB; 20k cap). Result: per-file replacement counts; then reload the AFFECTED models' content (model.setValue or a dedicated reload for dirty files — NEVER overwrite dirty unsaved buffers; skip files that are dirty with a clear report).
5. Splits, view state, hot-exit
- Split = second
monaco.editor.createon the same model; each view's state viaeditor.saveViewState()/restoreViewState()(scroll, caret, folding — all serializable). - Session (shared zod schema, daemon-persisted): tabs [{relPath, language, viewStates: [per group], dirty: bool}], active tab, split layout (vertical/horizontal, sizes).
- Restore order: create all models → create views → restore view states → THEN paint shell (prevents blank flash — verified lesson).
6. LSP integration points (interface only; Phase 5 implements)
editor/lsp-bridge.tsexposes:onOpen(model, relPath),onChange(model, version),onSave(model),onClose(model),setMarkers(relPath, monaco.MarkerData[]),requestCompletion(position)→CompletionList, hover, definition. Phase 5 implements the bridge against the LSP manager; Phase 3 just defines and stubs it with zero external effects.- Version counter:
model.getVersionId()is the Monaco-native version — use it for didChange version tracking (no custom counters).
Why (research grounding)
- Monaco docs (github.com/microsoft/monaco-editor): models are the state, editors are views, view states serializable — this is why VS Code tabs/splits/restore "just work"; we inherit it.
- VS Code uses Monaco for files of any size via virtualization — the custom windowing was a workaround for a hand-rolled textarea; it is deleted, not ported.
- VS Code engineering docs: renderer owns the editor, privileged host owns fs — our save path (model → contract → daemon) is that split.
- Verified project lessons: the 1MiB too-large gate + pagehide flush + dirty-close confirm + LSP didClose/diagnostics-clear on tab close are proven behavior — ported as contracts, not re-invented.
Dependencies
monaco-editor (pinned), Vite worker support (Phase 0), common/contracts file.ts (Phase 1), api client (Phase 2), LSP bridge stub (Phase 5 fills). Node-side: nothing new — the daemon already has file routes + search/replace.
Known issues / bugs (watch these)
- Worker 404s: if dist/ is missing worker chunks or paths are wrong, editor loads but language features (TS) silently die — verify workers exist in dist and are requested from the same origin. Check the Network tab after
vite build. - Monaco CSS: forgetting editor.main.css import = broken layout/overlap between editor and overlays (LEARN/MAP/EXP/RUN must keep absolute positioning over the editor column — verified Phase 2 rule).
- EOL/CRLF:
model.getValue(monaco.editor.EndOfLinePreference.CRLF)vs LF — files written must preserve the ORIGINAL EOL; detect on read (model.detectIndentation / EOL sniff) and write back the same. CRLF round-trip bugs are silent data corruption. - BOM: strip on read, restore on write if the original had one (UTF-8 BOM); forgetting = diff noise in git.
- model.setValue resets undo history: when reloading a file after workspace replace, ONLY setValue if the model is clean; dirty files must NOT be clobbered (report + keep buffer). setValue also resets markers — re-request diagnostics after.
- Undo across save: Monaco undo survives saves natively; do NOT clear undo stack on save.
- Memory: dispose models on tab close (unused models leak memory — the process-wide registry must be the only holder).
- IME/unicode: Monaco handles IME; but
getValuewith astral chars is fine — never do manual offset math on the string; use Monaco positions/offsets APIs. - Find widget overlap: the find widget is inside the editor DOM; the custom find-bar DOM element from the old app is removed (no dual find UIs).
- Font: non-bundled font = FOUT + wrong metrics (cursor misalignment). Bundle a monospace font (e.g. JetBrains Mono or Fira Code woff2, local file) and set it in the editor options AND the CSS, matching family/size.
- Glob masks (FOUND 2026-08-18, FIXED):
escapeRegExp(pattern).replace(/\*/g,'.*')is WRONG — escape leaves\*in the string, then the star replacement yields\..*(a literal dot + anything) instead of.*. Real symptom:mask=*.jsonmatched NOTHING while exact masks worked (search "accepts a file mask" test was vacuous — it never passed a mask). Correct order: escape regex specials EXCEPT*and?first (/[.+^${}()|[\]\\]/g), then*→.*,?→.(node/src/routes/fs.ts matchMask). Regression test must pass a REAL glob (e.g.*.txtand*.t?).
Phase 3 audit checklist (applied to the existing editor code)
- monaco-editor local; workers emitted to dist/ and served same-origin; zero CDN references anywhere (grep for http(s):// in browser code).
- Custom textarea editor, renderEditorText windowing, custom undo-stack wiring, custom find-bar DOM all REMOVED; Monaco owns editing/find/undo/virtualization.
- Model registry: one model per file; split views share models; dispose on close.
- Round-trip verified live: open → edit → save → reopen shows saved content; CRLF + BOM preserved on a test file.
- Dirty guard: closing a dirty tab prompts; session restore reopens tabs + view states without blank flash.
- Workspace replace reloads clean models, reports/skips dirty ones; approved-flag confirm flow intact.
- LSP bridge stub compiles (Phase 5 implements);
npm run checkgreen.
Signals
- GitHub stars
- 43
- Forks
- 14
- Last commit
- Oct 2026
Advanced
- Item type
- skill
- Key
aide-arch-editor- Source
- github.com/anonymousnomad/covert-coder
github.com/anonymousnomad/covert-coder
Related picks
Skill · thedaviddias
The pick for JavaScriptmodern-javascript-patterns
Skill · wshobson
The pick for JavaScriptpython-performance-optimization
Skill · wshobson
The pick for Pythonpython-pro
Skill · jeffallan
The pick for Pythonsetup-ts-deep-modules
Skill · mattpocock
The pick for TypeScripttypescript-pro
Skill · jeffallan
The pick for TypeScript