Ship the Monaco editor as a builtin plugin - #2127
Merged
Merged
Conversation
Replaces BB's read-only file preview with Monaco for ~86 text and code extensions: editing with compare-and-swap saves, find, syntax highlighting, and a file tree with filtering and copy actions. Enabled by default under the Interface category, so the panel becomes an editor rather than a viewer; per-extension opt-out stays in Settings → File openers, and binaries still fall through to BB's preview via experimental_Original. Monaco loads its prebuilt AMD bundle from disk rather than being bundled: the plugin build has no loader for its font asset, cannot code split, and inlining it would add ~4.4 MB to every app boot. Packaging copies only a builtin's dist/, so scripts/stage-assets.mjs stages monaco-editor/min/vs into dist/vs and the server serves it over a files.createPreview lease. copy-builtin-plugins.ts gains a generic hook that runs a plugin's stage-assets.mjs when it has one — the mechanism any plugin needing runtime files on disk would use. This adds ~25 MB to the packaged app. Ported from the standalone plugin: imports @bb/shared-ui instead of vendored components, resolves project-backed workspace sources (which carry no environment and may name a host), and accepts the experimental_hostId field main added to PluginFileOpenerSource. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A 400 KB PNG in the repo for a README heading is weight the tree does not need to carry; the standalone plugin keeps it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ports the folding, sorting, and copy-path rows from the standalone plugin. The palette's `run` gets a thread id and a project id but no route to a file tab, so editors publish themselves to a module-level last-focused slot that the commands read back — the palette and every mounted fileOpener are the same bundle in one browser context. Sort rows require a real multi-line selection: Monaco treats an empty or single-line selection as "sort the whole document", which is a lot of damage from a row reached by typing "sort". `read` now returns the file's absolute and root-relative paths so the copy rows need no second round trip. Drops the standalone version's guard for a missing commandPaletteAction slot: a builtin ships with the BB that defines it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
andrewkchan
force-pushed
the
feat/monaco-builtin-plugin
branch
from
August 25, 2026 21:19
77791c9 to
65a4363
Compare
The packaged builtin carried Monaco's whole AMD distribution because
that was the only browser-loadable form on hand. Most of it was never
used: ~14 MB is the TypeScript language service and its worker, which
this plugin disables outright, and another ~2.6 MB is the CSS, HTML, and
JSON services it never asks for.
`scripts/stage-assets.mjs` now runs esbuild over a narrow entry — the
editor plus `basic-languages`, which is what syntax highlighting
actually is — and emits `dist/monaco/{editor.js,editor.css,
editor.worker.js}`. Reachability is proven by the bundler rather than
decided by deleting directories, so nothing pruned can be requested
later at runtime; the alternative was instrumenting every fetch across
all 86 claimed languages and hoping the matrix was complete.
The loader moves from AMD to a plain `import()` of the built module,
which also retires the deprecated AMD path Monaco's README warns about.
The diagnostics-disabling code goes with it: the trimmed bundle has no
TypeScript language service to switch off.
JSON now highlights as JavaScript. Monaco has no basic-language for it —
JSON support is a full language service costing ~1.6 MB — and JSON is a
subset of JavaScript's object syntax, so tokenization is correct.
A new test asserts every claimed extension maps to a language the built
bundle actually registers. It caught exactly this JSON gap on its first
run, which is the failure mode the whole change risks.
Shipped builtin: 25 MB to 4.8 MB (Monaco 3.3 MB; the rest is the plugin
bundles and the server sourcemap). app.js is unchanged at 24 KB.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only packaging runs stage-assets.mjs: the dev server loads builtins straight from plugins/<name> and rebuilds nothing but dist/app.js. So `pnpm dev` on a fresh clone would have loaded this plugin into an error state — it worked in my checkout only because I had run the build by hand. The plugin now builds its own bundle when it is absent, lazily on the first `assets` call rather than at load, so a dev server's startup is not delayed for users who never open a file. Packaged builds are unaffected: the bundle is always staged there, and the fallback never runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit only checked whether the bundle existed, so editing monaco-bundle/ or bumping monaco-editor left a stale bundle in place and the change silently did not appear — the worst kind of dev loop, since everything else in the plugin does reload on save. Compares the artifact's mtime against the entries, the build script, and the manifest. Scoped to a source checkout by construction: a packaged plugin has no monaco-bundle/ beside it, so there is nothing that can be newer than the artifact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The plugin id derives from the package name, so `bb-plugin-monaco` resolved to `monaco` — the same id as the external plugin this upstreams. Reconcile refuses to displace a chosen install, so anyone holding that plugin would have had the builtin silently skipped, with only a server log saying why, until they removed theirs and restarted. Renaming sidesteps it entirely: the two coexist, and removing the external one needs no special handling. `monaco-editor` also matches the package and the product. The display name is "Monaco editor" so the two are tellable apart in Settings while the external one is deprecated. The alternative was teaching `remove` to adopt a bundled plugin whose id had just been freed. That works, but it changes remove semantics for every plugin to solve a transitional problem for one, and shared plugin machinery is the wrong place to carry that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The trimmed bundle entry imported `editor.api`, which is the API surface without Monaco's 59 contribution modules. The editor still opened, still highlighted, and still typed — so it looked right — while cmd+F, the option+arrow word motions, and every folding command silently did not exist. The palette's fold and sort rows were dead for the same reason. Uses `editor.main` instead, Monaco's own standalone-editor entry. It carries the contributions and the grammars but not the CSS, HTML, JSON, and TypeScript language services, which are the parts this plugin has no use for: 4.6 MB against 3.3 MB trimmed, and 24 MB for the prebuilt tree. The build script now fails when the bundle lacks the find widget, folding, word navigation, line sorting, grammars, or contributions at all. Verified against the broken entry, which it reports precisely. Checking for `actions.find` would not have worked — that string is in the API with or without the widget; the widget's own class name is not. JSON goes back to being highlighted as JSON: this entry registers the language, so the JavaScript stand-in is no longer needed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Monaco's word-highlight contribution ships in the bundle but renders nothing on its own: it draws whatever a DocumentHighlightProvider reports, and the only thing that registers one is a language service. The external plugin got this from Monaco's TypeScript service; dropping that service took the highlighting with it. Registers a textual provider through the public API instead — whole-word and case-sensitive, with no notion of whether two spellings mean the same symbol, which is what the external plugin's behaviour looked like in practice and what is useful without a language server. Costs nothing to ship. Monaco has its own textual provider but never wires it into the standalone editor, and it takes internal services a plugin cannot reach. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
andrewkchan
added a commit
to andrewkchan/bb-plugin-monaco
that referenced
this pull request
Aug 25, 2026
Monaco is now a BB builtin (get-bb/bb#2127). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SawyerHood
added a commit
that referenced
this pull request
Aug 25, 2026
) ## Human comments ## What was wrong #2127 shipped the Monaco builtin with `defaultEnabled: true`. On a fresh install it replaces the read-only file preview for ~86 text extensions without the user opting in. Its user-facing name also described the engine (Monaco) rather than what it does in BB. ## What changed - `apps/server/src/services/plugins/builtin-registry.ts`: `monaco-editor` now has `defaultEnabled: false`. A fresh database registers it disabled. Existing registrations keep their stored `enabled` state because reconciliation uses `existing?.enabled ?? bundled.defaultEnabled`, so this does not turn it off for users who already have it. - `plugins/monaco-editor/package.json`: manifest `bb.name` is now `File Editor` (Settings > Plugins, plugin store). - `plugins/monaco-editor/app.tsx`: the `fileOpener` `title` is now `File Editor` (Settings > File openers). - The plugin id `monaco-editor`, the directory, the package name, and the opener id `monaco` do not change, so existing registration rows and per-extension opt-outs still match. - `packages/bb-app/scripts/smoke-tarball.mjs`: `monaco-editor` leaves `EXPECTED_RUNNING_BUILTIN_PLUGINS`. That list is the default-enabled set the package smoke waits on, and a disabled plugin never reaches `running`. No wire changes. No CLI or doc surfaces name the plugin. ## How you verified - Added `ships the File Editor (monaco-editor) disabled on a fresh database` to `apps/server/test/services/plugins/builtin-plugins.test.ts`. It fails on `main` (`defaultEnabled` is `true`) and passes here. - `pnpm exec turbo run test --filter=@bb/server -- test/services/plugins/builtin-plugins.test.ts test/services/plugins/official-plugins.test.ts`: 34/34 pass. - `pnpm exec turbo run test typecheck --filter=bb-plugin-monaco-editor`: typecheck clean, 11/11 pass. - EAP codename scan of the working tree, `HEAD`, and `origin/main..HEAD`: clean. > AGENT GENERATED --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Human comments
This basically upstreams https://github.com/andrewkchan/bb-plugin-monaco as a built-in plugin.
What was wrong
BB's file panel is read-only. Opening a file from chat, the file search, or
bb thread opengives a preview, so any edit means leaving BB for an editor — even for a one-line change to a file the agent just wrote. This upstreams the Monaco plugin I built out-of-tree (andrewkchan/bb-plugin-monaco) so the panel can edit as well as show.What changed
New builtin:
plugins/monaco-editor. AfileOpenerthat replaces the preview with Monaco for ~86 text and code extensions:expectedSha256, so a save that would clobber a concurrent write (usually the agent's) stops and offers Reload or Overwrite.commandPaletteActionslot from Let plugins add rows to the quick palette #2270.font-mono text-xs leading-5,--font-mono, light/dark).Registered
defaultEnabled: trueunderInterface. That is the significant product change: the file panel becomes an editor for nearly every text file by default. Per-extension opt-out lives in Settings → File openers, and binaries fall through to BB's preview via theOriginalprop.The plugin id is
monaco-editor, notmonaco. The id derives from the package name, andmonacois what the external plugin this upstreams already uses. Reconcile refuses to displace a user's own install, so sharing the id would have meant anyone holding that plugin got the builtin silently skipped — nothing but a server log — until they removed theirs and restarted. Renaming sidesteps it: the two coexist, and removing the external one needs no special handling. The display name is "Monaco editor" so they are tellable apart in Settings during the transition.Monaco is built here, not bundled into
app.jsand not copied from its prebuilt tree.scripts/stage-assets.mjsruns esbuild overmonaco-bundle/editor.jsand emitsdist/monaco/{editor.js,editor.css,editor.worker.js};lib/monaco-loader.tsloads those from afiles.createPreviewURL the first time a file tab opens.Bundling into
app.jsis not an option:bb plugin buildemits one file with no code splitting, so Monaco would parse at app boot for every user including those who never open a file, and its worker could not be emitted at all. Building it separately keeps it lazy —app.jsis 24 KB — while letting esbuild prove reachability, which is what makes the trimming safe.Size: the packaged builtin is 6.3 MB, of which Monaco is 4.6 MB. The entry is Monaco's own
editor.main— the API, its contribution modules, and every Monarch grammar — minus the CSS, HTML, JSON, and TypeScript language services, which this plugin has no use for (its checker sees only the open file, so its "cannot find module" errors would be wrong). An earlier revision of this branch shipped Monaco's whole prebuilt AMD tree at 25 MB.A generic staging hook (
apps/server/scripts/copy-builtin-plugins.ts). Packaging copies only a builtin'sdist/andskills/, and builds plugins itself without running per-plugin scripts — so a plugin needing runtime files on disk has nowhere to put them.copyBuiltinPluginnow runs<pluginRoot>/scripts/stage-assets.mjswhen present. Optional, and this plugin is currently the only user. A source checkout never runs that path, so the plugin also builds its own bundle when it is missing or older than its sources.Also updated:
smoke-tarball.mjs, and the two "declare metadata for every builtin" tables in the server tests (both failed before I added the entries, which is them working as intended). No other core code is touched —plugin-service.tsandplugin-registration.tsare unchanged frommain.How you verified
Unit tests cover the tree logic that is easy to get subtly wrong — nesting flat paths, synthesising directories a truncated listing omitted, case-insensitive sorting, and filtering, which must expand every ancestor of a match or the match stays hidden behind a collapsed row. Another asserts every claimed extension maps to a language the built bundle actually registers; it failed on its first run, catching that
jsonwas absent from the trimmed bundle andpackage.jsonwould have rendered as plain text.The build script asserts the bundle is complete, because trimming it is how this went wrong twice. It fails when the output lacks the find widget, folding, word navigation, line sorting, grammars, or contributions. An earlier entry (
editor.apialone) shipped without Monaco's contribution modules: the editor opened, highlighted, and typed, so it looked correct, while ⌘F, the option+arrow word motions, and every folding command silently did not exist. The guard reports that entry precisely. Note it checks for the find widget's own class name rather thanactions.find, which is present with or without the widget and would have passed.Ran the real packaging path (
tsx apps/server/scripts/copy-builtin-plugins.ts) and confirmed the shipped builtin containsdist/monaco/{editor.js,editor.css,editor.worker.js}besidedist/server.jsanddist/app.js, totalling 6.3 MB, with Monaco absent fromapp.js.Manual testing is in progress in a dev desktop build. Two paths are new in the port and have never run: project-backed workspace sources (no environment, resolved through the project's
sources[]) and theexperimental_hostIdfield onPluginFileOpenerSource— the plugin's schema was.strict()and would have rejected those files outright before this branch.Known gaps
Documented in the plugin README, with upstream issues where the fix is not ours:
node_modules(Path listing hides every dotfile and node_modules unconditionally, so file search cannot find .github/workflows/ci.yml #2093), and is read-only — no rename/create/delete.Fixes #