Skip to content

Ship the Monaco editor as a builtin plugin - #2127

Merged
andrewkchan merged 9 commits into
get-bb:mainfrom
andrewkchan:feat/monaco-builtin-plugin
Aug 25, 2026
Merged

Ship the Monaco editor as a builtin plugin#2127
andrewkchan merged 9 commits into
get-bb:mainfrom
andrewkchan:feat/monaco-builtin-plugin

Conversation

@andrewkchan

@andrewkchan andrewkchan commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Human comments

Screenshot 2026-08-25 at 4 03 49 PM

This basically upstreams https://github.com/andrewkchan/bb-plugin-monaco as a built-in plugin.

  • Optimized so that it gets bundled instead of lazy loaded with AMD loader, now it should add only ~6MB (the typescript language services are stripped)
  • One functional difference from external monaco is that hovering symbols in typescript files no longer shows diagnostics and CMD+click does not work to gotodef in the same file. This is because we're stripping the builtin monaco typescript language service. The plan is to make the language diagnostics optional (first, add back in the monaco builtins, then add an interface for custom LSPs as plugins, see Language intelligence should be plugin-contributed, not built into the editor #2438)

What was wrong

BB's file panel is read-only. Opening a file from chat, the file search, or bb thread open gives 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. A fileOpener that replaces the preview with Monaco for ~86 text and code extensions:

  • Editing with ⌘S saves guarded by expectedSha256, so a save that would clobber a concurrent write (usually the agent's) stops and offers Reload or Overwrite.
  • Find (⌘F), line numbers, syntax highlighting, and Monaco's usual editing affordances.
  • A file tree with path filtering, expand/collapse, reveal-the-open-file, and a right-click menu to copy absolute path / relative path / filename.
  • Quick-palette rows for folding, sorting selected lines, and copying the current file's path, via the commandPaletteAction slot from Let plugins add rows to the quick palette #2270.
  • Type and theme matched to BB's own preview (font-mono text-xs leading-5, --font-mono, light/dark).

Registered defaultEnabled: true under Interface. 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 the Original prop.

The plugin id is monaco-editor, not monaco. The id derives from the package name, and monaco is 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.js and not copied from its prebuilt tree. scripts/stage-assets.mjs runs esbuild over monaco-bundle/editor.js and emits dist/monaco/{editor.js,editor.css,editor.worker.js}; lib/monaco-loader.ts loads those from a files.createPreview URL the first time a file tab opens.

Bundling into app.js is not an option: bb plugin build emits 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.js is 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's dist/ and skills/, and builds plugins itself without running per-plugin scripts — so a plugin needing runtime files on disk has nowhere to put them. copyBuiltinPlugin now runs <pluginRoot>/scripts/stage-assets.mjs when 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.ts and plugin-registration.ts are unchanged from main.

How you verified

pnpm exec turbo run typecheck test --filter=bb-plugin-monaco-editor    # 11 tests
pnpm exec turbo run test --filter=@bb/server -- builtin-plugins official-plugins   # 33 tests
pnpm exec turbo run test --filter=@bb/app -- useThreadFileTabs         # 20 tests

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 json was absent from the trimmed bundle and package.json would 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.api alone) 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 than actions.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 contains dist/monaco/{editor.js,editor.css,editor.worker.js} beside dist/server.js and dist/app.js, totalling 6.3 MB, with Monaco absent from app.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 the experimental_hostId field on PluginFileOpenerSource — 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:

Fixes #

AGENT GENERATED: by Claude Opus 5

andrewkchan and others added 3 commits August 25, 2026 14:16
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
andrewkchan force-pushed the feat/monaco-builtin-plugin branch from 77791c9 to 65a4363 Compare August 25, 2026 21:19
andrewkchan and others added 6 commits August 25, 2026 14:35
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
andrewkchan merged commit 052a508 into get-bb:main Aug 25, 2026
13 checks passed
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>
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.

1 participant