VS_CODE_MONACO_FIRST_PLAN_V2

VS Code → gxwf-ui Monaco Integration Plan (V2)

Date: 2026-04-13 Predecessor: VS_CODE_MONACO_FIRST_PASS_PLAN.md (V1) — first-pass plan plus spike journal. This V2 keeps only the forward-looking work; Phase 0 is collapsed to a “verified setup” note. Goal: Embed the Galaxy Workflows VS Code extension’s editing experience inside a tab of the gxwf-ui Vue app using @codingame/monaco-vscode-api. Users get Monaco + hover + completion + diagnostics for a single workflow file, backed by the same LSP servers that power desktop VS Code.


Locked Decisions

QuestionDecision
Editor library@codingame/monaco-vscode-api v30.0.1 (mode 1 — embed, not workbench)
Extension host workerYes — enabled
ScopeSingle-file editor, one tab
Language selectionBy file extension (.ga → native, .gxwf.yml/.gxwf.yaml → format2, -test(s).y(a)ml → tests)
Operations panelStandalone (gxwf-client driven). LSP diagnostics are a bonus, not a replacement.
Extension deliveryfolder: (dev live checkout), vsix: (unpacked-vsix dir served over HTTP — CI / preview / prod). Selected via VITE_GXWF_EXT_SOURCE. No runtime openvsx: loader in the browser — production servers unpack the extension at startup into /ext/galaxy-workflows/ and point vsix: at that URL (2026-04-15 simplification, see Phase 2 / Phase 8).
CSS auditRequired phase, not optional
Keybinding testsRequired phase, not optional
Editor chromePure custom Vue toolbar (Phase 5.5). views-service-override / attachPart(EDITOR_PART) rejected for v1 — monotonic upgrade path if native VS Code breadcrumbs / editor-title actions become necessary.

Architecture

┌─────────────────── gxwf-ui (Vue 3 SPA) ────────────────────┐
│   /files/:path → FileView.vue                              │
│                                                            │
│   ┌─────────────────────────┐  ┌───────────────────────┐   │
│   │ EditorTab               │  │ OperationPanel        │   │
│   │   Monaco editor DOM     │  │ (unchanged, gxwf-     │   │
│   │   (monaco-vscode-api)   │  │  client over HTTP)    │   │
│   └───────────┬─────────────┘  └───────────────────────┘   │
│               │ in-process LSP (postMessage)               │
│               ▼                                            │
│   ┌───────────────────────────────────────────────────┐    │
│   │ Extension Host Worker (monaco-vscode-api)         │    │
│   │  loads galaxy-workflows-vscode browser bundle     │    │
│   │  spawns ls-native + ls-format2 web workers        │    │
│   │      ↓                                            │    │
│   │  ToolRegistryService → ToolInfoService            │    │
│   │      → IndexedDBCacheStorage                      │    │
│   │      → fetcher: ToolShed (direct) or proxy        │    │
│   └───────────────────────────────────────────────────┘    │
└────────────────────────────────────────────────────────────┘

LSP traffic stays in-browser. OperationPanel continues to hit gxwf-web over HTTP.


Phase 0 — Verified (Spike Done)

Spike at ~/projects/repositories/monaco-spike proves the load path. Key findings codified below; they constrain all subsequent phases.

What works in the spike:

Constraints carried into Phase 1+ (all the V1 spike pain, distilled):

  1. Pin every @codingame/monaco-vscode-* to the same exact version. No carets — a version skew between the API package and any service override breaks silently.
  2. monaco-editor and vscode must be aliases, not real deps:
    • monaco-editornpm:@codingame/monaco-vscode-editor-api@<v>
    • vscodenpm:@codingame/monaco-vscode-extension-api@<v> Installing real monaco-editor brings a second Monaco runtime into the bundle and breaks everything.
  3. Service-override gotchas at v30: the package quickaccess-service-override (not quickinput-service-override — does not exist); language-detection-worker-service-override does not exist either. Keybindings option is shouldUseGlobalKeybindings (not …GlobalStorage). Configuration takes no args; call initUserConfiguration(jsonString) BEFORE initialize(...). Extensions takes { enableWorkerExtensionHost: true, iframeAlternateDomain? }.
  4. MonacoEnvironment must expose getWorker, getWorkerUrl, AND getWorkerOptions:
    • Labels to handle in getWorker: editorWorkerService, TextMateWorker, extensionHost* (three aliases).
    • TextMate ships its own worker bundle@codingame/monaco-vscode-textmate-service-override/worker. Using the editor worker for the TextMateWorker label fails with Missing method $init on worker thread channel default. Audit each new override for a sibling worker / workers/* export.
    • Labels to handle in getWorkerUrl: webWorkerExtensionHostIframe (path to the iframe HTML — see #6) and extensionHost* (worker URL).
    • getWorkerOptions(label) must return { type: "module" } for extensionHost* labels. The iframe HTML branches on this and uses await import(url) instead of importScripts(url), allowing Vite’s ESM ?worker&url output to work as-is.
  5. registerFileUrl(path, url) semantics:
    • path is relative to the extension location (no leading /<extensionPath>/).
    • url must be absolute, with origin (e.g. ${self.location.origin}/@fs/...). monaco-vscode-api’s registerExtensionFileUrl runs URI.parse(url); scheme-less paths default to file:, and the extension-host worker’s _loadCommonJSModule then calls fetch("file:///...") which the browser refuses from an http:// origin. Surface error is a bare “Failed to fetch” with no URL.
    • For the extension’s main entry, _loadCommonJSModule does ensureSuffix(path, ".js") itself — register the .js form once.
  6. Iframe HTML can’t be deep-imported. The override package’s exports map only matches *.js/*.css/*.d.ts. Solution: copy @codingame/monaco-vscode-extensions-service-override/vscode/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html into public/monaco/ at install time (postinstall script, or a Vite plugin if we want to be fancier).
  7. Extension-host worker entry cannot use dynamic import() for the monaco-vscode-api worker main — Vite’s worker bundling can’t follow it. Use a static import of the package path @codingame/monaco-vscode-api/vscode/vs/workbench/api/worker/extensionHostWorkerMain (note: NO src/ prefix — the package’s ./vscode/* export rule adds it).
  8. Vite config: optimizeDeps.exclude ALL @codingame/monaco-vscode-* packages (the optimizer strands sibling assets); drop optimizeDeps.esbuildOptions (deprecated under Vite 8 / Rolldown) — rely on build.target for output target; do not pass optimizeDeps.rolldownOptions: { target }, Rolldown rejects it (“Invalid key: Expected never but received ‘target’”); server.fs.allow must include the extension worktree root for folder: delivery.
  9. Standing diagnostic worth keeping: custom extension-host worker entry that wraps self.fetch with a getter/setter to log the URL on failure. Without it, future asset-registration mistakes surface as a bare “Failed to fetch” with no URL. Reference impl: monaco-spike/src/editor/extensionHostWorker.ts.
  10. Phase 0.5.8 was a misdiagnosis (now retired). monaco-vscode-api’s patchWorker (extensionHostWorker.js:54) already intercepts new Worker(extension-file://...) from inside extension code — no upstream extension change is required for that.

Already landed upstream in galaxy-workflows-vscode (wf_tool_state branch):

Not addressed; defer until needed:


Phase 1 — gxwf-ui Dependencies + Editor Shell

Status (2026-04-14): 1.1–1.6, 1.8–1.10 landed on branch vs_code_integration of galaxy-tool-util. 1.7 partially done — the MonacoEditor.vue component ships, but the call-site swap in FileView.vue is deferred until Phase 2 (so the production build can load the extension via vsix:, not just dev-only folder:). make check green across the monorepo. Phase 1 smoke test is deferred to post-Phase 2 — see revised “Tests (Phase 1)” block below.

1.1 — Install deps. Pin all @codingame/* to the same exact version (currently 30.0.1):

pnpm add -F @galaxy-tool-util/gxwf-ui --save-exact \
  monaco-editor@npm:@codingame/monaco-vscode-editor-api@30.0.1 \
  vscode@npm:@codingame/monaco-vscode-extension-api@30.0.1 \
  @codingame/monaco-vscode-api@30.0.1 \
  @codingame/monaco-vscode-editor-api@30.0.1 \
  @codingame/monaco-vscode-extensions-service-override@30.0.1 \
  @codingame/monaco-vscode-languages-service-override@30.0.1 \
  @codingame/monaco-vscode-keybindings-service-override@30.0.1 \
  @codingame/monaco-vscode-notifications-service-override@30.0.1 \
  @codingame/monaco-vscode-quickaccess-service-override@30.0.1 \
  @codingame/monaco-vscode-configuration-service-override@30.0.1 \
  @codingame/monaco-vscode-files-service-override@30.0.1 \
  @codingame/monaco-vscode-textmate-service-override@30.0.1 \
  @codingame/monaco-vscode-theme-service-override@30.0.1 \
  reflect-metadata

Anti-goal: do NOT pull @codingame/monaco-vscode-workbench or layout-related overrides. Those trigger the full-screen workbench shell we rejected.

1.2 — Install postinstall iframe-HTML copy.packages/gxwf-ui/scripts/copy-monaco-iframe.mjs runs as postinstall. Resolves the override via require.resolve("@codingame/monaco-vscode-extensions-service-override") and walks to the sibling vscode/src/... path (the package’s exports map forbids deep package.json resolution). Copies to public/monaco/webWorkerExtensionHostIframe.html.

1.3 — Vite config (packages/gxwf-ui/vite.config.ts). ✅ Per Phase 0 constraint #8:

1.4 — MonacoEnvironment setup.packages/gxwf-ui/src/editor/monacoEnvironment.ts — side-effect module. Imported once at the top of MonacoEditor.vue before any other monaco/vscode touch. Handles labels editorWorkerService, TextMateWorker, extensionHost{,Worker,WorkerMain}, and webWorkerExtensionHostIframe.

1.5 — Extension-host worker entry.packages/gxwf-ui/src/editor/extensionHostWorker.ts. Keeps the self.fetch getter/setter wrapper as a standing diagnostic (Phase 0 constraint #9). Static import of @codingame/monaco-vscode-api/vscode/vs/workbench/api/worker/extensionHostWorkerMain (no src/ prefix).

1.6 — Service init.packages/gxwf-ui/src/editor/services.ts. Singleton initMonacoServices(cfg) with servicesReady: Promise<void> | null. MonacoUserConfig type surfaces toolShedUrl, toolCacheProxyUrl, cacheDbName, validationProfile for Phase 2.4 to populate — currently called with defaults. Config JSON assembled via initUserConfiguration(...) BEFORE initialize(...) per Phase 0 constraint #3.

1.7 — MonacoEditor.vue component. 🟡 Component lands at packages/gxwf-ui/src/components/MonacoEditor.vue; call-site swap deferred to post-Phase 2. Contract:

<script setup lang="ts">
defineProps<{
  content: string;
  fileName: string;       // resolves language by extension
  readonly?: boolean;
  theme?: string;
}>();
const emit = defineEmits<{
  "update:content": [value: string];
  ready: [];
  error: [err: Error];
}>();
</script>

Implementation notes (as landed): side-effect imports monacoEnvironment first; onMounted awaits initMonacoServicesloadGalaxyWorkflowsExtension → creates model via upsertMemoryFile + resolveLanguageId. Watchers sync content / readonly / theme into the live editor. An applyingProp guard prevents update:content echo on external updates. Emits ready when editor is live, error on boot failure (for hosting views to fall back to EditorShell).

EditorShell.vue stays as the default at call sites for now (textarea fallback if Monaco fails to load, and a working UI while folder:-only loader can’t power the production build); the swap happens once Phase 2 ships vsix: + the fixture is bundled into gxwf-web. Delete EditorShell.vue when Phase 7 ships.

1.7b — Folder-mode extension loader (interim).packages/gxwf-ui/src/editor/loadExtension.ts — reads VITE_GXWF_EXT_SOURCE=folder:/abs/path, registers the extension via ${self.location.origin}/@fs/... URLs, awaits whenReady(). Phase 2 replaces this module with the general extensionSource.ts resolver supporting folder: / vsix: (the original openvsx: runtime loader was dropped 2026-04-15 — see Phase 2).

1.8 — Lifecycle. ✅ Mount creates model + editor + content-change subscription; onBeforeUnmount disposes all three and nulls the refs. Verify with monaco.editor.getEditors().length before/after.

1.9 — Language detection.packages/gxwf-ui/src/editor/languageId.ts. resolveLanguageId(fileName) returns gxwftests / gxformat2 / galaxyworkflow / plaintext. Suffix precedence matters: -test(s).y(a)ml must be tested before .gxwf.ya?ml (a test file could conceivably end -tests.gxwf.yml). The loaded extension contributes the three IDs — do not call monaco.languages.register() from gxwf-ui.

1.10 — FileSystemProvider for single-file workspace.packages/gxwf-ui/src/editor/fileSystem.ts — lazily constructs a RegisteredFileSystemProvider (from files-service-override) under scheme gxwf-ui on first use; upsertMemoryFile(fileName, content) calls provider.registerFile(new RegisteredMemoryFile(uri, content)) and returns the URI. Re-registration overwrites silently (no manual disposable tracking yet — fine for single-file scope).

Tests (Phase 1) — restructured 2026-04-14:

The originally-proposed Phase-1 smoke (programmatic hover on class: → assert Format2 Schema) has been deferred until after Phase 2 + the FileView.vue call-site swap. Rationale: the existing gxwf-e2e harness (packages/gxwf-e2e/src/harness.ts) does a production pnpm --filter gxwf-ui build in globalSetup and serves the static dist via gxwf-web. The Phase-1 folder-only loader relies on Vite’s /@fs/ dev middleware, which doesn’t exist in a production build. Writing a Phase-1-only smoke would require a parallel playwright.monaco.config.ts with its own webServer spawning vite dev and a dev-only /monaco-smoke route — throwaway infrastructure that gets deleted the moment Phase 2 lands.


Phase 2 — Extension Source Indirection

Status (2026-04-15): 2.1–2.4 landed; simplified on 2026-04-15 to drop the openvsx: runtime browser loader and the in-browser .vsix unzip in response to the blob-URL cross-context fetch blocker (see VS_CODE_MONACO_FIRST_E2E_DEBRIEF_1.md). Both folder: and vsix: now resolve to a base URL and share one loader; unzip moved to the Node-side staging script. openvsx: is no longer a runtime concern — production servers unpack the extension at startup into the same /ext/galaxy-workflows/ layout and point vsix: at it.

2.1 — Resolver modulepackages/gxwf-ui/src/editor/extensionSource.ts. Current shape:

export type ExtensionSource =
  | { kind: "folder"; path: string }   // dev — Vite /@fs against a live checkout
  | { kind: "vsix"; url: string };     // URL prefix of an unpacked-vsix directory

export function parseExtensionSource(spec: string | undefined): ExtensionSource;
export function loadExtensionSource(src: ExtensionSource): Promise<RegisterLocalExtensionResult>;
export function loadGalaxyWorkflowsExtension(): Promise<RegisterLocalExtensionResult>;

Spec format: folder:/abs/path | vsix:/ext/galaxy-workflows (relative to origin) or vsix:https://host/path/to/dir (absolute). Both forms point at a directory of files reachable over HTTP.

2.2 — Wire import.meta.env.VITE_GXWF_EXT_SOURCE. ✅ Default is vsix:/ext/galaxy-workflows, which resolves against the same origin gxwf-web serves from. A no-config Monaco build (VITE_GXWF_MONACO=1) requires that public/ext/galaxy-workflows/ exists — staged by scripts/stage-extension.mjs from a contributor-supplied fixture, or by the production server at startup. No fixture → stage-extension.mjs no-ops and the Phase 1.7 error path falls back to EditorShell.

2.3 — One loader, two shapes. ✅ Both sources converge on a single loadFromBase(baseUrl) that fetches package.json, walks contributes + browser via collectManifestFiles, and registers each discovered file as ${baseUrl}/${rel}. folder: sets baseUrl = ${origin}/@fs${absPath}; vsix: passes the URL through (prefixing origin if relative). The original in-browser unzipSync / blob-URL path was removed — unzip now happens once in scripts/stage-extension.mjs on prebuild/predev, writing files into public/ext/galaxy-workflows/. Server-side openvsx download at deploy time produces the same layout.

2.4 — Settings plumbing.buildMonacoUserConfigFromEnv() in services.ts reads four env vars and feeds initUserConfiguration(...) before initialize(...):

VariableConfiguration key
VITE_GXWF_TOOLSHED_URLgalaxyWorkflows.toolShed.url
VITE_GXWF_TOOL_CACHE_PROXY_URLgalaxyWorkflows.toolCacheProxy.url
VITE_GXWF_CACHE_DB_NAMEgalaxyWorkflows.cacheDbName
VITE_GXWF_VALIDATION_PROFILEgalaxyWorkflows.validation.profile

Not yet test-covered — E2E assertion that values reach workspace.getConfiguration() deferred to Phase 4+ integration work.


Phase 3 — Dev Loop

Status (2026-04-14): 3.1–3.4 landed. Pinned wf_tool_state @ 5040bd5 in packages/gxwf-ui/EXT_COMMIT.md. pnpm dev:with-ext shipped as a Node driver (scripts/dev-with-ext.mjs) that validates GXWF_EXT_PATH and spawns concurrently. Watch-mode reload option (b) chosen (manual refresh, documented). README at packages/gxwf-ui/README.md covers clone, bump procedure, dev loop, local .vsix packaging, runtime env-var settings. Manual dev:with-ext smoke against the local extension checkout still TBD.

3.1 — EXT_COMMIT.md in packages/gxwf-ui/:

EXTENSION_REPO=https://github.com/davelopez/galaxy-workflows-vscode
EXTENSION_BRANCH=wf_tool_state   # or successor
EXTENSION_COMMIT=<sha>           # pinned

All dev environments, CI, contributor docs reference this. Bumps are deliberate, reviewed PRs.

3.2 — pnpm dev:with-ext script in packages/gxwf-ui/package.json. ✅ Shipped as a Node driver at packages/gxwf-ui/scripts/dev-with-ext.mjs (cleaner error reporting than an inline shell string). The driver:

Plan-drift note: the spec in this document originally said folder:$GXWF_EXT_PATH/client/dist; that was incorrect — the extension root is the repo root (manifest, workflow-languages/, server/ all live there), and the new collectManifestFiles walk resolves paths like client/dist/web/extension.js relative to it.

3.3 — Watch-mode reload. Vite HMR doesn’t reload the extension host worker. Either: (a) tiny dev-only WS listener on the extension output dir that triggers location.reload(), or (b) document that devs hit refresh after extension rebuilds. Pick (b) for v1, revisit if it’s painful.

3.4 — README additions in packages/gxwf-ui/README.md: clone procedure, EXT_COMMIT.md bump procedure, pnpm dev:with-ext usage, how to package a .vsix locally and switch to vsix: mode.


Phase 4 — Tool Cache Wiring

Status (2026-04-14): 4.2 + 4.3 landed on vs_code_integration. Cache-DB-name settings plumbed in Phase 2.4; CORS fallback (VITE_GXWF_TOOL_CACHE_PROXY_URL) documented in packages/gxwf-ui/README.md under a new “Tool cache (browser mode)” section. 4.1 verification + 4.4 cache panel deferred.

The extension’s browser inversify binding is already in place (TYPES.CacheStorageFactory() => new IndexedDBCacheStorage()). Phase 4 just exercises the path end-to-end.

4.1 — Verify cache populates on first hover. ⏸ Deferred to the post-Phase-2 + 1.7-swap E2E (Phase 1 “Tests” block §171–184 already lists the IndexedDB assertion as a piggy-backed check on the hover smoke). No standalone automation — it would need the same FileView swap and .vsix fixture.

4.2 — Per-origin DB name.galaxyWorkflows.cacheDbName is surfaced via Phase 2.4’s VITE_GXWF_CACHE_DB_NAME build-time env var. Default galaxy-tool-cache-v1 stays.

4.3 — Fetcher / CORS fallback. ✅ README now documents direct-ToolShed fetch + the VITE_GXWF_TOOL_CACHE_PROXY_URL workaround when CORS blocks browser-origin GETs. The same section points at gxwf-web’s --csp-connect-src flag so a proxy origin doesn’t get rejected by CSP. Live CORS verification against https://toolshed.g2.bx.psu.edu still pending (requires running stack).

4.4 — Dev-only cache panel. ⏸ Optional per original plan; skipped for now. Revisit if Phases 5–7 debugging makes it painful.

4.5 — Pre-warm: out of scope for v1. Tracked as 9C.


Phase 4.5 — CSP Headers on gxwf-web

Status (2026-04-14): Landed on vs_code_integration. CSP emitted on every static response from serveStatic() in packages/gxwf-web/src/router.ts via buildCspHeader(extraConnectSrc). CreateAppOptions.extraConnectSrc: string[] and a repeatable --csp-connect-src <origin> CLI flag thread per-deployment tool-cache-proxy / ToolShed-mirror origins into connect-src. Test coverage in packages/gxwf-web/test/static.test.ts. No CSP-violation smoke yet — slotted to land with the Phase 1 hover smoke once FileView swaps to MonacoEditor.

Baseline header shape (as implemented):

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'wasm-unsafe-eval';
  worker-src 'self' blob:;
  frame-src 'self' blob:;        # extension host iframe spawned via createObjectURL
  connect-src 'self' https://toolshed.g2.bx.psu.edu <extraConnectSrc…>;
  style-src 'self' 'unsafe-inline';
  font-src 'self' data:;
  img-src 'self' data:;

unsafe-inline for styles required by monaco-vscode-api’s inline theme injection (audit nonce-based later). wasm-unsafe-eval needed by some textmate engines. connect-src does not include blob:/data: — since the 2026-04-15 Phase 2 simplification, extension files are fetched over plain HTTP and no cross-context blob URL is produced. open-vsx.org is no longer allow-listed because the browser never hits it; production servers fetch it Node-side at startup.

Header is sent unconditionally on static responses (HTML, JS, CSS, assets). Confining to text/html only provides no benefit — subresource loads happen in the browsing context of the document whose CSP already applies — and keeps the middleware simple.

Open items on 4.5:


Phase 5 — CSS Audit

Status (2026-04-16): 5.1, 5.2, 5.3, 5.4 done modulo a light-theme follow-up. Decisions and tooling consolidated in commits e85f650 (regression + inventory specs), 092ce9c (architecture/dev docs), and a follow-up commit on vs_code_integration that lands src/editor/theme.ts + the font probe.

5.1 — Inventory conflicts.packages/gxwf-e2e/tests/_inventory-monaco-css.spec.ts is the standing inventory tool (GXWF_E2E_INVENTORY=1 to run). Latest report at packages/gxwf-e2e/.inventory/REPORT.md: zero computed-style drift on Dashboard probes (body, h1, refresh-button, list-frame, directory-path) before vs. after Monaco mount; +4 PrimeVue lazy-load sheets and 7 data-vscode="true" Monaco sheets, all .monaco-* / .codicon-* scoped. Screenshots confirm Dashboard / WorkflowView are unchanged after Monaco loads.

5.2 — Scope monaco-vscode-api styles to the editor container.Decision (2026-04-15): no scoping wrapper required today. The inventory shows monaco-vscode-api’s injected stylesheets are already prefix-scoped at source (no *, html, body, :root, or bare element selectors leak from data-vscode sheets). Wrapping in shadow DOM or @layer monaco would be overhead with no benefit at the current monaco-vscode-api@30.0.1 + pinned-extension state.

The shadow-DOM / @layer paths remain documented as the response if a future bump regresses this — packages/gxwf-e2e/tests/monaco-css-scoping.spec.ts is the standing regression guard (runs in normal CI, fails the build if any newly-added non-PrimeVue stylesheet introduces a globally-reaching selector). Failure message walks the responder through the fix without needing this plan as reference. Phase 9D (“shadow DOM finalization if Phase 5 went the layer route”) is now N/A unless the regression guard fires.

5.3 — Theme. ✅ Superseded by the holistic theme overhaul (see THEME_OVERHAUL_PLAN.md, landed 2026-04-16). Brand identity is now owned by two first-class VS Code color themes — gxwf-dark and gxwf-light — contributed through a synthetic extension (packages/gxwf-ui/src/editor/themesExtension.ts). The decorative workbench.colorCustomizations layering and the MonacoEditor.vue theme prop are gone.

Implementation note: monaco-vscode-api does not support standalone monaco.editor.defineTheme (throws defineTheme is not a function from the workbench theme service shim). Themes must be contributed via an extension manifest’s contributes.themes entry, and selected via the workbench.colorTheme user-config setting. services.ts seeds that setting from the dark class on <html> at boot; themeSync.ts observes the class and pushes updateUserConfiguration(...) when the user flips the app’s dark-mode toggle.

5.4 — Fonts. ✅ Verified by extension to monaco-css-scoping.spec.ts: probes .monaco-editor and .monaco-editor .view-lines computed font-family after Monaco mounts, asserts neither resolves to a family containing Atkinson Hyperlegible (the body brand font, which would destroy column alignment if it leaked in). Premise is also asserted (body still uses Atkinson Hyperlegible) — if the brand font ever moves, the test self-flags rather than silently passing.

The pinned galaxy-workflows-vscode extension contributes no fonts (CSS contributions are only TextMate grammars and language configs — no @font-face and no font-family rules in the staged public/ext/galaxy-workflows/ directory). Standing guard catches any future regression on either side.


Phase 5.5 — Editor Toolbar

Status (2026-04-16): Landed on vs_code_integration. Details in VS_CODE_MONACO_FIRST_PLAN_V2_PHASE_5_5.md.

Pure custom Vue toolbar above the Monaco host inside FileView.vue. Surfaces Save (delegates to FileView.onSave — same handler ⌘S will hit in Phase 6.2), Undo/Redo (polled from model.canUndo() / canRedo() on onDidChangeContent), Format Document (hidden when no formatter provider is registered), Find, Command Palette, and a Problems badge bound to useEditorMarkers (LSP diagnostics; click runs editor.action.marker.next; danger-colored on errors).

MonacoEditor.vue now exposes { editor, model, ready } via defineExpose; FileView.vue forwards those into <EditorToolbar> and renders it only when monacoEnabled && !monacoFailed && editor. The EditorShell textarea fallback path is untouched.

Tests: unit tests for useEditorMarkers in packages/gxwf-ui/test/composables/useEditorMarkers.test.ts (5 cases — count, URI scoping, model re-subscription, jumpToNext, dispose). E2E in packages/gxwf-e2e/tests/monaco-toolbar.spec.ts covering Problems badge on a broken format2 fixture, palette, find, undo, and a save-triggered PUT /api/contents — gated by the GXWF_E2E_MONACO=1 .vsix fixture.

Deferred: Problems popover (list-view of markers) — tracked as 9J below; re-visit if the count-only badge feels insufficient.

Phase 6 — Keybindings

Status (2026-04-20): Landed on vs_code_integration across commits b9600b3 (core) and 3e4c06e (e2e expansion + palette-button fix + lifecycle fix). make check + make test + Monaco e2e suite green (16 unit + 9 e2e).

6.1 — Audit contributes.keybindings. ✅ Extension at pinned commit contributes zero keybindings (only commands, languages, grammars, menus). No upstream PRs needed.

6.2 — Ctrl+S / Cmd+S.packages/gxwf-ui/src/editor/saveCommand.ts stacks a handler on workbench.action.files.save via CommandsRegistry. Registration lives in MonacoEditor.vue’s onMounted, before the ready marker flips — so tests (and fast humans) never race past it. MonacoEditor takes an onSave prop; FileView passes () => onSave(), so toolbar button + keybinding share one code path.

6.3 — Preserve browser shortcuts. ✅ Trivially satisfied given 6.1 — nothing in the extension binds Ctrl+T/W/Shift+I/F5, and the workbench’s defaults don’t reach browser-reserved shortcuts. Left as a review rule in the README (see 6.5).

6.4 — Tests. Converted from the originally-planned vitest keybindings.test.ts to e2e (user call: “swap those planned unit tests out for more E2E tests”). Lives in packages/gxwf-e2e/tests/:

6.5 — Document the contract.packages/gxwf-ui/README.md §“Keybinding contract for the embedded extension”: any future contributes.keybindings must declare when: editorFocus/editorTextFocus.

Side effects / bugs surfaced during Phase 6


Phase 7 — v1 Feature Surface

Ships day one: syntax highlighting, hover, completion, diagnostics (LSP markers), find/replace, format document (if extension registers it), command palette scoped to editor.

Explicitly NOT v1: multi-file/workspace, file explorer, source control, custom views/webviews, task running, debug.

OperationPanel continues displaying full validate/lint/clean/roundtrip reports via gxwf-client. Some overlap with LSP diagnostics is accepted.


Phase 8 — Server-side Open VSX Unpack

Once wf_tool_state (or successor) merges and publishes to Open VSX:

  1. Update EXT_COMMIT.md to point at the merged commit.
  2. Add a server-side step (in gxwf-web startup, or a deploy-time hook) that downloads the pinned .vsix from Open VSX and unpacks it into the served /ext/galaxy-workflows/ directory. Reuses the same logic shape as scripts/stage-extension.mjs — pull + unzip, write to a served directory.
  3. Keep VITE_GXWF_EXT_SOURCE default at vsix:/ext/galaxy-workflows. Prod builds unchanged from preview builds at the browser layer.
  4. Pin the Open VSX version at the server layer (env var or config). Bumps are deliberate PRs that update EXT_COMMIT.md and the server pin together.
  5. CI check: the pinned Open VSX version still resolves at deploy time.

The browser never talks to open-vsx.org. No runtime openvsx: loader, no open-vsx.org entry in CSP.


Phase 9 — Later Iterations (Loose)


Test Strategy

PhaseTypeWhere
1Manual pnpm dev:with-ext eyeball smoke only — automated smoke deferred
1+2LSP hover smoke (post-1.7-swap, via vsix: fixture); doubles as Phase 4.1 cache checkpackages/gxwf-e2e/tests/monaco-hover.spec.ts
2Unit — source spec parser, loader dispatchpackages/gxwf-ui/test/editor/
3Manual — dev loop smokeREADME
4Integration — hover hits IndexedDBsame
4.5CSP smoke — no console violationssame
5Visual regression — Playwright screenshotspackages/gxwf-ui/test/visual/
5.5Unit — useEditorMarkers; E2E — problems badge, palette, find, undo, save PUTpackages/gxwf-ui/test/composables/useEditorMarkers.test.ts, packages/gxwf-e2e/tests/monaco-toolbar.spec.ts
6Unit — saveCommand register/dispose/stack; E2E — workbench save routes to onSave, F1 palette, palette lists GW commands, Ctrl+Space suggestpackages/gxwf-ui/test/editor/saveCommand.test.ts, packages/gxwf-e2e/tests/monaco-toolbar.spec.ts, packages/gxwf-e2e/tests/monaco-keybindings.spec.ts
7E2E — feature smoke per bulletsame
8CI — Open VSX resolution check.github/workflows/

Red-to-green per phase: failing test expressing the acceptance, then implementation.


Dependencies Added

PackagePhaseNotes
@codingame/monaco-vscode-* (12 packages)1Pinned exact, same version
monaco-editor (alias) + vscode (alias)1Aliases, not real packages
reflect-metadata1For inversify in extension host
fflate2.vsix unpack — devDependency since 2026-04-15; used only by scripts/stage-extension.mjs at build time, not shipped in the browser bundle
concurrently3dev script
Playwright5, 6If not already present

No new prod deps on the galaxy-tool-util side. IndexedDBCacheStorage already in @galaxy-tool-util/core.


Migration & Compatibility


Risks (Live)

RiskMitigation
Bundle size blows past toleranceAudit overrides; lazy-load editor tab so dashboard isn’t penalized.
CSS bleed breaks PrimeVuePhase 5 shadow-DOM preferred path. CSS-layer fallback.
Ctrl+S / keybinding collisions ship unnoticedPhase 6.4 tests are the gate. Don’t ship without them green.
Open VSX publishing of target extension stallsPhase 8 deferred; vsix: mode (fixture-driven, or contributor-supplied server-side unpack from any source) is production-viable indefinitely.
IndexedDB quota pressure on low-end devicesCache-size inspection UI (4.4) + “clear cache” button.
@codingame/monaco-vscode-api major version driftPin exact (no caret); pnpm up is a deliberate PR. Lock in EXT_COMMIT.md or sibling.
@galaxy-tool-util/schema pulls Effect into LS bundle (size)Measure in Phase 1. If >2 MB/worker, evaluate Effect tree-shaking or lazy grammar loading.
ToolShed CORS blocks browser fetchDocument galaxyWorkflows.toolCacheProxy.url fallback; surface in Phase 4.3.

Open Questions

  1. Ctrl+S target — editor command dispatch or gxwf-ui save handler? (Lean: gxwf-ui save handler.)
  2. Where does EXT_COMMIT live — gxwf-ui README.md, top-level constants file, or renovate-style metadata?
  3. Custom theme authoring — who owns the visual design pass?
  4. Open VSX version pinning (server-side, per Phase 8) — exact, caret, or latest? (Lean: exact, deliberate bumps.)
  5. Pre-warm cache bundle — yes/no for v1, or defer to 9C? (Lean: defer.)
  6. Visual regression infra — add Playwright, or reuse existing tooling?
  7. Iframe-HTML delivery — postinstall script vs. dedicated Vite plugin? (Either works; pick simpler.)