VS_CODE_MONACO_FIRST_PASS_PLAN

VS Code → gxwf-ui Monaco Integration Plan

Date: 2026-04-12 Predecessor: VS_CODE_WEB_INTEGRATION_PLAN.md (inverted direction — that plan pushed the web app into VS Code; this one pulls the IDE into the web app). 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.


Design Decisions (Locked)

QuestionDecision
Editor library@codingame/monaco-vscode-api (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)
Operations panelKeep standalone (gxwf-client driven). LSP diagnostics are a bonus, not a replacement.
LS package deliveryOption D: load galaxy-workflows-vscode as a .vsix into the extension host worker
Dev delivery indirectionVITE_GXWF_EXT_SOURCE env var — one of folder: / vsix: / openvsx:
CSS auditRequired phase, not optional
Keybinding testsRequired phase, not optional

Upstream Projects

ProjectPathRole
galaxy-tool-util (this repo)/Users/jxc755/projects/worktrees/galaxy-tool-util/branch/vs_code_integrationgxwf-ui host, IndexedDBCacheStorage, gxwf-client
galaxy-workflows-vscode/Users/jxc755/projects/worktrees/galaxy-workflows-vscode/branch/wf_tool_stateVS Code extension producing the .vsix we embed
Open VSX listingdavelopez/galaxy-workflowsStable prod source after feature branch merges

Architecture

┌─────────────────── gxwf-ui (Vue 3 SPA) ────────────────────┐
│                                                            │
│   Router tab:  /files/:path  →  FileView.vue               │
│                                                            │
│   ┌─────────────────────────┐  ┌───────────────────────┐   │
│   │  EditorTab (new)        │  │ OperationPanel        │   │
│   │  ┌────────────────────┐ │  │ (unchanged, uses      │   │
│   │  │ Monaco editor DOM  │ │  │  gxwf-client)         │   │
│   │  │ (monaco-vscode-api)│ │  │                       │   │
│   │  └────────────────────┘ │  │ Validate / Lint /     │   │
│   │                         │  │ Clean / Convert       │   │
│   └───────────┬─────────────┘  └───────────────────────┘   │
│               │                                            │
│               │ LSP messages (in-process postMessage)      │
│               ▼                                            │
│   ┌───────────────────────────────────────────────────┐    │
│   │ Extension Host Worker (monaco-vscode-api)         │    │
│   │   • loads galaxy-workflows-vscode .vsix           │    │
│   │   • client/src/browser/extension.ts activates     │    │
│   │   • spawns two LSP web workers                    │    │
│   │                                                   │    │
│   │   ┌──────────────┐   ┌──────────────────────┐     │    │
│   │   │ ls-native WW │   │ ls-format2 WW        │     │    │
│   │   │  (hover,     │   │  (hover, complete,   │     │    │
│   │   │   complete,  │   │   validate)          │     │    │
│   │   │   validate)  │   │                      │     │    │
│   │   └──────┬───────┘   └──────────┬───────────┘     │    │
│   │          │                      │                 │    │
│   │          └──────┬───────────────┘                 │    │
│   │                 ▼                                 │    │
│   │   ToolRegistryService                             │    │
│   │     → ToolInfoService                             │    │
│   │        → IndexedDBCacheStorage (this repo)        │    │
│   │        → fetcher: ToolShed (direct) or proxy      │    │
│   └───────────────────────────────────────────────────┘    │
└────────────────────────────────────────────────────────────┘

LSP traffic stays inside the browser. No server round-trips for editor features. OperationPanel continues to call gxwf-web over HTTP (unchanged).


Phase 0: De-Risk Spike (Half-Day)

Phase 0 is expected to surface upstream blockers in galaxy-workflows-vscode (Node-builtin imports, inversify browser binding gaps). Those fixes land in Phase 0.5 before Phase 1 begins. Do not install Phase 1 deps until the spike plus upstream cleanup are green.

Before committing to the rest of the plan, prove the load path. Nothing here is plan-shaped code — it’s a throwaway verification.

0.1 — ✅ Done (2026-04-12). Built the extension’s browser bundles on the web_fixes branch of galaxy-workflows-vscode. Both dist/web/nativeServer.js (2.85 MB) and dist/web/gxFormat2Server.js (2.66 MB) compile cleanly, and after the 0.5.1–0.5.4 changes below the browser bundles contain zero require("os"|"fs"|"path"|"crypto"|"fs/promises"|"node:*") calls. Verified with grep -oE 'require\("(os|fs|path|crypto|fs/promises|node:[a-z/]+)"\)'.

Update (2026-04-13): After galaxy-tool-util-ts#52 merged (core split into browser-safe root + /node subpath with a browser export condition), the shim scaffolding introduced in the first pass was entirely unwound — see 0.5.1 update below. Browser bundles are now clean by virtue of the upstream export map, not local plugins.

0.2 — ✅ Done (2026-04-13). Scratch app at ~/projects/repositories/monaco-spike (Vite 8 + Vue 3 + TS). All @codingame/monaco-vscode-* packages installed at 30.0.1 with --save-exact. Deps aliased: monaco-editor@codingame/monaco-vscode-editor-api@30.0.1, vscode@codingame/monaco-vscode-extension-api@30.0.1. Service overrides wired: extensions (with enableWorkerExtensionHost: true), languages, textmate, theme, configuration (with initUserConfiguration before initialize), files, keybindings, notifications, quickaccess. Monaco editor mounts cleanly into a <div> with a gxformat2 model.

0.3 — ✅ Done (2026-04-13, second pass). registerExtension(manifest, ExtensionHostKind.LocalWebWorker, { path: "/galaxy-workflows" }) accepted the manifest. registerFileUrl(relPath, fsUrl) wired up the browser entry, both LSP workers, the 2 language-configuration JSONs, and the 2 TextMate grammars, served via Vite’s /@fs/<absolute> route with server.fs.allow extended to the extension worktree. Both LSP web workers boot end-to-end; hover at class: in a gxformat2 buffer returns Format2-schema content (The 'outputs' field is required. / class - Must be 'GalaxyWorkflow'.), confirming LSP round-trip. Two spike-side fixes were needed beyond the original wiring: (a) MonacoEnvironment.getWorker must dispatch the TextMateWorker label to the textmate override’s own worker bundle (@codingame/monaco-vscode-textmate-service-override/worker), not the editor worker — the wrong bundle yields Missing method $init on worker thread channel default; (b) URLs passed to registerFileUrl must be absolute (with origin) — root-relative /@fs/... paths get URI.parsed by registerExtensionFileUrl (extensions.js:32) to scheme file:, and the extension-host worker’s _loadCommonJSModule then fails on a file:// URL with bare “Failed to fetch”. Diagnosed via a custom worker entry that wraps self.fetch with a logger (kept in the spike at src/editor/extensionHostWorker.ts).

0.4 — ✅ Done (2026-04-13). Findings captured at ~/projects/repositories/monaco-spike/FINDINGS.md. Concrete drift from the plan:

  1. Package list drift. @codingame/monaco-vscode-quickinput-service-override does not exist at v30 — the correct name is @codingame/monaco-vscode-quickaccess-service-override. @codingame/monaco-vscode-language-detection-worker-service-override does not exist either (drop from Phase 1.1). monaco-editor should be installed as an alias (npm:@codingame/monaco-vscode-editor-api), not as a direct dep — the two Monacos cannot coexist.
  2. Service-override options. Keybindings override’s prop is shouldUseGlobalKeybindings, not shouldUseGlobalStorage. Configuration override takes no args; call initUserConfiguration(jsonBlob) before initialize(...). Extensions override accepts { enableWorkerExtensionHost, iframeAlternateDomain }.
  3. registerExtension semantics. path becomes the URI path of the extension-file URI (extension-file://<publisher>.<name>/<path>). registerFileUrl(filePath, url) joins filePath onto that location — file paths must be RELATIVE (no leading /<path>/).
  4. MonacoEnvironment needs BOTH getWorker and getWorkerUrl. Known labels: editorWorkerService, TextMateWorker (background tokenizer), extensionHostWorkerMain / extensionHost / extensionHostWorker (main thread spawn + iframe URL), webWorkerExtensionHostIframe (URL pointing at the override package’s iframe HTML).
  5. Iframe HTML can’t be deep-imported. The override package’s exports only matches *.js/*.css/*.d.ts, so import ... from ".../webWorkerExtensionHostIframe.html?url" is rejected. Workaround: copy the file into a public/ folder (or a tiny Vite plugin). Needs a dedicated step in Phase 1.
  6. Extension-host worker format — resolved in-spike. The iframe HTML’s createWorker already branches on workerOptions.type: a 'module' value makes the blob use await import(url) instead of importScripts(url). MonacoEnvironment.getWorkerOptions(moduleId, label) → { type: "module" } for the extensionHost* labels lets Vite’s ESM ?worker&url output work as-is. Verified in spike: extension host boots, activate() runs. See Phase 0.5.7.
  7. Vite configuration. optimizeDeps.exclude all @codingame/monaco-vscode-* packages — the dep optimizer moves JS to /.vite/deps/ but leaves sibling asset files behind, so new URL('./x', import.meta.url) 404s. optimizeDeps.esbuildOptions prints a Vite 8 deprecation warning (still functional in 8.0.x); use optimizeDeps.rolldownOptions for new code. server.fs.allow needs the extension worktree root for folder: delivery to work during dev.
  8. CSP. Add frame-src 'self' blob: to the Phase 4.5 header set — the extension host iframe is spawned via a blob URL.
  9. Tool-cache wiring (Phase 4) is not yet exercised. The IndexedDBCacheStorage code path cannot be verified until item 6 is fixed and activate() can actually reach the language-client initialization.

Exit criteria — MET (2026-04-13, second pass): prototype loads the extension manifest + assets, boots the extension host worker, completes activate(), both LSP web workers spawn (Galaxy Workflows (galaxyworkflow) server is ready. / Galaxy Workflows (gxformat2,gxwftests) server is ready.), and an LSP hover at class: returns Format2-schema content. Phase 1 unblocked.


Phase 0.5: Upstream Browser-Readiness PRs (galaxy-workflows-vscode)

Items discovered during Phase 0 that must land upstream before gxwf-ui work proceeds.

0.5.1 — Purge Node builtins from LS runtime.Done (2026-04-12), redone simpler (2026-04-13).

First pass (2026-04-12):

Second pass (2026-04-13) after galaxy-tool-util-ts#52 merged:

0.5.2 — Storage injection in ToolRegistryServiceImpl.Done (2026-04-12), refactored (2026-04-13).

After the #52 bump ToolCache.storage is required (no filesystem default). Final shape:

Dependency shift. server/packages/server-common/package.json dep switched from the .tgz file-url to file:../../../../../../galaxy-tool-util/branch/vs_code_integration/packages/core (directory symlink — live edits, no repack). Publishing a real version bump to npm/Open VSX is still the eventual follow-up.

0.5.3 — Browser-only extension bundle variant. Not addressed in this pass. The existing dual build (client/src/extension.ts + client/src/browser/extension.ts) already covers desktop vs. browser activation; decision on packaging a separate .vsix can wait until Phase 2 actually needs it.

0.5.4 — reflect-metadata + inversify in browser worker.Done (2026-04-12). Added explicit import "reflect-metadata" as the first line of both browser entries (server/gx-workflow-ls-native/src/browser/server.ts, server/gx-workflow-ls-format2/src/browser/server.ts). It was previously only transitive via @gxwf/server-common/src/languageTypes; the explicit import removes the accidental reliance on import ordering.

0.5.5 — build:watch target.Already present. server/package.json ships watch-native-server, watch-format2-server, and watch (concurrent).

0.5.6 — Browser-mode configuration surface. Not addressed in this pass. IndexedDBCacheStorage currently uses its built-in galaxy-tool-cache-v1 default; exposing galaxyWorkflows.cacheDbName / galaxyWorkflows.toolCacheProxy.url is a small follow-up once Phase 2 wiring exercises the settings surface end-to-end.

0.5.7 — ESM extension-host worker via getWorkerOptions.Resolved (2026-04-13). The iframe HTML (webWorkerExtensionHostIframe.html:105) already branches on workerOptions.type: (workerOptions?.type === 'module') ? await import('${workerUrl}') : importScripts('${workerUrl}'). MonacoEnvironment.getWorkerOptions(_moduleId, label) flows through StandaloneWebWorkerService.getWorkerOptions into the iframe postMessage payload. Fix: return { type: "module" } from getWorkerOptions for the extensionHost* labels. No IIFE side-build, no HTML fork, extension-host worker stays isolated. Confirmed in the spike: after this change the extension host boots and activate() runs (next error is an unrelated extension-file://new Worker(...) URL issue inside the extension’s own code, see Phase 0.5.8).

Companion fix (still applies): public/monaco/webWorkerExtensionHostIframe.html needs to be populated from the override package at install time — postinstall script is the simpler of the two options considered, drops in a Vite plugin later if we want one.

0.5.8 — DISSOLVED (2026-04-13). Originally framed as: extension’s new Worker(serverUri.toString()) at client/src/browser/extension.ts:31 fails because browsers can’t spawn workers from extension-file:// URIs, requiring an upstream fix to convert to HTTP first. This is incorrect. monaco-vscode-api’s extension-host worker already patches self.Worker (extensionHostWorker.js:54, patchWorker(asBrowserUri, getAllStaticBrowserUris)) — new Worker(extension-file://...) inside extension code is intercepted, the URI is resolved to its registered browser URL via FileAccess.asBrowserUri, and a blob bootstrap calls importScripts(<resolvedUrl>). The “Failed to fetch” originally attributed to this code path was actually the previous fetch — _loadCommonJSModule loading the extension’s own browser entry — failing because spike-side registerFileUrl calls were storing scheme-less paths that URI.parse defaulted to file: (see Phase 0.3 update). Once registrations were rewritten as absolute http:// URLs, both LSP workers spawn cleanly with no extension-side change. No upstream PR is required for this item.

Exit criteria: extension loads via Phase 0 spike with LSP workers functional and tool hover sourced from IndexedDB. Upstream PRs merged or at least tagged on a dev branch referenced by EXT_COMMIT.md.

Test regressions from this pass (2026-04-12). 7 of 377 server tests fail in packages/server-common/tests/unit/toolRegistry.test.ts. All failures are API-drift from the @galaxy-tool-util/core bump (cache-key input format changed; ToolCache.hasCached is now async; populateCache failure path changed). The tests pre-date the core rewrite and were written against the 0.2.0-npm cache layout.

Resolved (2026-04-13). Rewrote toolRegistry.test.ts against the new API:

Test status (2026-04-13). Server 379/379, client jest 43/43, E2E 12/12 (npm run test:e2e — 12 mocha specs across ga + gxformat2, incl. tool-aware clean, conversions, tool-state validation empty/populated caches).


Phase 1: gxwf-ui Dependencies + Editor Shell

1.1 — Add deps to packages/gxwf-ui/package.json. Final list post-Phase-0 (2026-04-13):

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

Corrections vs. previous draft: quickaccess replaces quickinput (which does not exist at v30); language-detection-worker-service-override dropped (no such package); monaco-editor and vscode are aliases to @codingame/monaco-vscode-editor-api and @codingame/monaco-vscode-extension-api respectively — installing real monaco-editor breaks everything by bringing in a second Monaco runtime. All @codingame/* packages pinned to the same exact version.

Also required (not strictly “service overrides”):

Explicit anti-goal: do NOT pull @codingame/monaco-vscode-workbench or the views/layout service overrides unless Phase 7 expands scope. Those trigger the full-screen workbench shell we rejected.

FileSystemProvider for single-file workspace. The extension activation expects a workspace. Register a minimal virtual FileSystemProvider backed by the single in-editor buffer under a scheme like gxwf-ui:///current-file.ga. This is also where files-service-override hooks in — not just as a dep, but as the seam for our FS provider. Covered here, not in Phase 2.

Initial configuration bootstrap. configuration-service-override requires an initial JSON blob. Assemble one from gxwf-ui’s reactive settings at mount time — anything the extension reads via workspace.getConfiguration("galaxyWorkflows") comes from there.

1.2 — Configure Vite worker handling (revised post-Phase-0, 2026-04-13):

1.3 — Create packages/gxwf-ui/src/components/MonacoEditor.vue. Replace EditorShell.vue’s textarea with this new component at call sites. Contract:

<script setup lang="ts">
defineProps<{
  content: string;
  fileName: string;       // used to resolve language by extension
  readonly?: boolean;
}>();
const emit = defineEmits<{
  "update:content": [value: string];
}>();
</script>

EditorShell.vue stays as a thin wrapper for now — keeps the existing diagnostics-list fallback path alive until the LSP wiring lands. Delete once LSP is in.

1.4 — Lifecycle: mount on tab activate, dispose model + editor on unmount. Use a singleton getMonacoServices() initializer — VS Code services are global and should init exactly once per page load. Guard with let servicesReady: Promise<void> | null = null.

1.5 — Language detection: extension → language-id map. Three languages ship in the extension (package.json contributes): galaxyworkflow (.ga), gxformat2 (.gxwf.yml/.gxwf.yaml), and gxwftests (-test(s).yml/-test(s).yaml). Map all three; missing the third silently mis-identifies workflow-test YAML. The loaded extension registers the languages — do not hardcode Monaco registerLanguage() calls here; let the extension own that.

Test targets for this phase:


Phase 2: Extension Source Indirection

2.1 — Create packages/gxwf-ui/src/editor/extensionSource.ts exporting one resolver:

export type ExtensionSource =
  | { kind: "folder"; path: string }       // dev
  | { kind: "vsix"; url: string }          // CI / preview
  | { kind: "openvsx"; id: string; version?: string }; // prod

export function parseExtensionSource(spec: string): ExtensionSource;
export async function loadExtensionSource(src: ExtensionSource): Promise<RegisteredExtension>;

spec format: folder:/abs/or/relative/path | vsix:/public/ext/foo.vsix | openvsx:davelopez/galaxy-workflows@0.x.

2.2 — Read import.meta.env.VITE_GXWF_EXT_SOURCE in App.vue (or editor mount point). Default to openvsx:davelopez/galaxy-workflows@latest so a no-config build still works post-merge. In dev we expect the env var to be set.

2.3 — Single “virtual-FS register” loader, three fetch strategies. monaco-vscode-api’s extensions service does not natively read dev-server directories as extensions — the idiomatic path is registerExtension({ manifest, location }) plus a virtual FileSystemProvider (or in-memory file map) exposing the bundle files. All three sources end at the same registerExtension() call.

Implementation: one buildInMemoryFS(files: Map<string, Uint8Array>) helper; the three strategies differ only in how they populate files.

2.4 — Wire gxwf-ui settings plumbing so the loaded extension sees:

Configuration is surfaced through the configuration-service-override. The extension reads it via workspace.getConfiguration() the same way it does on desktop.


Phase 3: Pinning + Dev Scripts

3.1 — Create packages/gxwf-ui/EXT_COMMIT.md (or a top-level repo constant file, TBD) declaring:

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

All dev environments, CI, and contributor docs reference this file. Bumps are deliberate, reviewed commits. Prevents “works on my machine” drift while the feature branch moves.

3.2 — Add pnpm dev:with-ext script in packages/gxwf-ui/package.json:

"dev:with-ext": "concurrently -n ext,ui -c blue,green \"pnpm -C $GXWF_EXT_PATH build:watch\" \"VITE_GXWF_EXT_SOURCE=folder:$GXWF_EXT_PATH/client/dist pnpm dev\""

Requires GXWF_EXT_PATH env var. Script fails loudly if unset.

3.3 — The VS Code extension repo must expose a build:watch target producing the browser bundle on change. If it doesn’t exist, add it upstream (separate PR on that repo). The tsup.config.ts there already does dual builds — add a --watch invocation.

3.4 — Vite HMR will not reload the extension host worker automatically. Add a small file watcher to the MonacoEditor component’s mount path (dev-only) that listens for changes under the extension output dir via Vite’s dev server WS and triggers a page reload. Alternative: document that dev users hit refresh after extension rebuilds — acceptable for now.

3.5 — Developer README additions in packages/gxwf-ui/README.md:


Phase 4: Tool Cache Wiring

Fold the existing IndexedDBCacheStorage (commit ac820d3) into the extension host runtime so ToolRegistryServiceToolInfoServiceIndexedDBCacheStorage operates entirely in-browser.

4.1 — Prerequisite landed in Phase 0.5.2 (ToolRegistryServiceImpl accepts CacheStorage via inversify factory). Here we wire it:

IndexedDBCacheStorage’s constructor takes only dbName (defaults to galaxy-tool-cache-v1). If a user wants cache isolation per origin/workflow, surface a galaxyWorkflows.cacheDbName setting (added in Phase 0.5.6) and pass it through the factory.

4.2 — The DB name needs to be deterministic per origin. Default "galaxy-tool-cache-v1" (as shipped) is fine — one cache per browser, shared across workflows.

4.3 — Fetcher configuration: the LSP server in browser mode calls ToolShed directly (fetchFromToolShed uses fetch). ToolShed CORS behavior needs verification — it may or may not allow browser-origin GET. If it blocks, users configure a tool-cache-proxy URL and the extension routes fetches through it. Add a galaxyWorkflows.toolCacheProxy.url read in the browser entry.

4.4 — Pre-seeding: optional future enhancement. A “pre-warm cache” action could fetch top-N tools from a bundled dataset and saveAll() them on first run. Not v1.

4.5 — Cache inspection: add a dev-only panel in gxwf-ui that lists IndexedDB contents (cache.list()) with size / age columns. Helpful for debugging during Phase 0–6.

Tests:


Phase 4.5: CSP Headers on gxwf-web

gxwf-web serves the built UI in production. The extension host worker, LSP web workers, and language-detection worker require permissive worker-src and script-src. Update gxwf-web’s response headers (or static-serving middleware) to include:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'wasm-unsafe-eval';
  worker-src 'self' blob:;
  frame-src 'self' blob:;
  connect-src 'self' https://open-vsx.org https://toolshed.g2.bx.psu.edu <configured proxies>;
  style-src 'self' 'unsafe-inline';
  font-src 'self' data:;
  img-src 'self' data:;

frame-src 'self' blob: added post-Phase-0 (2026-04-13): the extensions service spawns its extension-host iframe by URL.createObjectURL(...) on the sandbox HTML. Without frame-src the iframe is blocked.

unsafe-inline for styles is currently required by monaco-vscode-api’s inline theme injection. Audit whether nonce-based CSP is feasible later. wasm-unsafe-eval is needed by some textmate grammar engines.

Tests: add a smoke test that loads the Monaco tab against a gxwf-web instance with CSP enabled and checks the browser console for CSP violations. Fail the test if any fire.


Phase 5: CSS Audit Pass

monaco-vscode-api ships styles that assume VS Code’s theme variable namespace on a global scope and that Monaco’s container is the primary layout. PrimeVue and the gx-gold styling must coexist.

5.1 — Inventory conflicts by diff-comparing gxwf-ui rendering before/after Monaco load. Capture screenshots of Dashboard, WorkflowView, FileView — confirm no color shifts, no font substitutions, no button-radius changes.

5.2 — Scope monaco-vscode-api styles to the editor container. Options ranked:

5.3 — Theme integration: monaco-vscode-api’s theme service supports loading a .json theme. Pick a dark theme that matches gxwf-ui’s gold/dark identity (there’s brand identity work in commits d1af987 / eb6f518). Author a minimal custom theme in packages/gxwf-ui/src/editor/theme.ts — semantic tokens from gx palette.

5.4 — Font handling: gxwf-ui uses Atkinson Hyperlegible for body text. Keep Monaco on a monospace default ("Menlo, Consolas, monospace") — do not pull body font into the editor. Verify the loaded extension doesn’t try to register fonts.

5.5 — Validation: visual regression run. Recommend Playwright screenshot tests against Dashboard, WorkflowView, FileView (editor closed), FileView (editor open). Added to make test pipeline.


Phase 6: Keybindings

The keybindings-service-override installs a global keybinding registry. This can collide with gxwf-ui’s router shortcuts, browser shortcuts, or host app Ctrl+S semantics.

6.1 — Scope keybindings to the editor element via VS Code’s when context clauses. The editor has an implicit focus context (editorFocus, editorTextFocus). Audit the loaded extension’s package.json contributes.keybindings — each should have a when that restricts to editor focus.

6.2 — Block Ctrl+S (Cmd+S) from reaching the extension’s default save handler. gxwf-ui already has its own save flow through OperationPanel and gxwf-client. Either:

Preference: override and route to gxwf-ui save. Users expect Ctrl+S to work.

6.3 — Browser shortcuts to preserve: Ctrl+T (new tab), Ctrl+W (close tab), Ctrl+Shift+I (devtools), F5 (reload), Ctrl+F (in-editor find is fine — monaco-vscode-api provides native find widget, but browser Ctrl+F outside the editor should also work).

6.4 — Keybinding tests. Create packages/gxwf-ui/test/editor/keybindings.test.ts. Use Playwright component testing or Vitest + @vue/test-utils with a real DOM. Scenarios:

6.5 — Document the keybinding contract in packages/gxwf-ui/README.md. Any future extension contribution that adds a keybinding must declare a when clause scoped to editor focus. Enforce in review.


Phase 7: Feature Surface (v1)

What the editor delivers on day one:

Explicitly NOT in v1:

Validation: the existing OperationPanel continues to display full validate / lint / clean / roundtrip reports driven by gxwf-client. LSP diagnostics and operation reports will show some overlap — that’s accepted. Users who rely on the operation reports are not disrupted; users who prefer inline editor feedback get it too.


Phase 8: Ship Path — Switching to Open VSX

Once galaxy-workflows-vscode’s wf_tool_state branch (or its successor) merges and publishes a release to Open VSX:

8.1 — Update EXT_COMMIT.md to point at the merged commit. 8.2 — Change the default VITE_GXWF_EXT_SOURCE to openvsx:davelopez/galaxy-workflows@<version>. 8.3 — Remove the folder: fallback from prod builds (dev keeps it). 8.4 — Pin the Open VSX version in gxwf-ui’s build. Bumping the version is a deliberate PR. 8.5 — Add a CI check that verifies the pinned Open VSX version is still resolvable (guards against the extension being unpublished).


Phase 9: Later Iterations (Intentionally Loose)

Items we expect to tackle after v1 ships, shaped roughly. Details will sharpen with experience.

9A — Preview publishing pipeline. CI job that builds the VS Code extension at the pinned commit, runs gxwf-ui’s full test suite against it as a .vsix, and publishes both a gxwf-ui preview deploy and a GitHub Releases .vsix artifact. Lets reviewers try both together without local setup.

9B — Tighter LSP ↔ OperationPanel integration. Currently diagnostics come from two paths (LSP in editor, gxwf-client in panel). Consider: panel shows a “sourced from editor” badge on diagnostics that also exist in LSP output, or the panel subscribes to LSP diagnostic streams when available. TBD based on what feels right in use.

9C — Pre-warm cache from bundle. Ship a JSON dump of top-N tools with the gxwf-ui build, saveAll() on first mount. Cuts first-workflow latency.

9D — Shadow DOM finalization. If Phase 5’s shadow-DOM approach worked in the Phase 0 spike, great. If not, revisit once we know which monaco-vscode-api features we actually ship.

9E — Desktop VS Code extension reuse for editor commands. Right now gxwf-ui’s OperationPanel and the extension’s commands are independent implementations of clean / convert / validate. Long-term: the panel can dispatch to the loaded extension’s commands via commands.executeCommand(...) instead of calling gxwf-web. Consolidates logic, but only makes sense once extension-side commands are feature-complete.

9F — Multi-file / Contents API integration. If gxwf-ui grows multi-file editing, revisit: does the extension host get a workspace with multiple Monaco models, or do we stay one-editor-per-tab and open separate tabs? Defer.

9G — Read-only embed mode. A gxwf-ui route that renders a workflow read-only with LSP hover-only for documentation / demos / IWC listings. Small delta from the editable version.

9H — Upstream contributions as they shake out. Each phase will likely spawn small PRs against galaxy-workflows-vscode (browser bundle hardening, Node-builtin elimination, configuration surface additions, extension repackaging script). Track them in a running list rather than pre-specifying.


Test Strategy Summary

PhaseTest TypeWhere
1Unit — language resolution, component mountpackages/gxwf-ui/test/editor/
2Unit — source spec parser, loader dispatchsame
3Manual — dev loop smokedocumented in README
4Integration — hover uses IndexedDB cachesame
5Visual regression — Playwright screenshotspackages/gxwf-ui/test/visual/
6E2E — keybinding scopespackages/gxwf-ui/test/editor/keybindings.test.ts
7E2E — feature smoke per bulletsame
8CI — Open VSX resolution check.github/workflows/

Red-to-green for every phase: write a failing test expressing the phase’s acceptance, land implementation that flips it green.


Dependency Summary

New depPackagePhase
monaco-editorgxwf-ui1
@codingame/monaco-vscode-api + service overridesgxwf-ui1
fflate (or similar) for .vsix unpackgxwf-ui2
concurrently (dev)gxwf-ui3
Playwright (if not already present)gxwf-ui (dev)5, 6

No new production deps on the galaxy-tool-util side beyond gxwf-ui. IndexedDBCacheStorage is already shipped in @galaxy-tool-util/core.

Upstream changes required on galaxy-workflows-vscode:


Migration & Compatibility


Risks

RiskMitigation
Phase 0 reveals monaco-vscode-api can’t load the extension cleanlyResolved (2026-04-13): full Phase 0 spike loads manifest, grammars, configs, both LSP web workers, and returns LSP hover content for gxformat2. Two minor spike-side wiring fixes were needed (TextMate worker dispatch, absolute-URL registerFileUrl calls) — neither requires upstream changes. Option-A fallback (npm + monaco-languageclient) shelved unless gxwf-ui Phase 1 surfaces something new.
Bundle size blows past toleranceAudit service overrides and drop any not strictly needed (theme, quickinput, language-detection are likely candidates for removal). Lazy-load the editor tab so the dashboard isn’t penalized.
Extension host extension has Node-only activation codeGate behind isBrowser check upstream. Small PRs to the VS Code repo.
CSS bleed breaks PrimeVueShadow DOM (Phase 5.2 preferred path). If shadow DOM fails, scope via CSS layers — messier but workable.
Ctrl+S / other keybinding collisions ship unnoticedPhase 6.4 tests are the gate; do not ship without them passing.
Open VSX publishing of the target extension stallsPhase 8 is deferred until it ships. Phase 3’s .vsix: delivery mode is production-viable indefinitely if needed.
IndexedDB quota pressure on low-end devicesAdd cache-size inspection UI (Phase 4.5) and a “clear tool cache” button.
node:os / node:fs / node:path imports in server-common (concrete: toolRegistry.ts:1 imports node:os) block browser bundlingResolved: upstream galaxy-tool-util-ts#52 split core into browser-safe root + /node subpath; local shims + esbuild plugin removed (Phase 0.5.1 second pass, 2026-04-13).
Inversify @injectable() needs reflect-metadata in browser workerPhase 0.5.4 confirms import "reflect-metadata" is first line of each LS browser entry.
monaco-vscode-api + monaco-editor version drift breaks everything silentlyPin exact versions (no ^); pnpm up is a deliberate, reviewed PR. Lock versions in EXT_COMMIT.md or a sibling file.
@galaxy-tool-util/schema pulls Effect into LS worker bundle (size)Measure in Phase 0. If >2 MB per worker, evaluate Effect tree-shaking or lazy grammar loading.

Open Questions

  1. Ctrl+S target — editor command dispatch or gxwf-ui save handler?
  2. Where does EXT_COMMIT live — gxwf-ui README.md, a top-level constants file, or renovate-style metadata?
  3. Who owns the custom theme authoring? Visual design pass needed.
  4. Version pinning granularity on Open VSX — exact version, caret range, or latest?
  5. Pre-warm cache bundle — yes/no for v1, or defer to 9C?
  6. Visual regression infra — add Playwright, or reuse existing tooling?
  7. Single universal .vsix (desktop + browser) vs. separate galaxy-workflows-browser.vsix — decide in Phase 0.5.3.
  8. Inversify browser container wiring — upstream in galaxy-workflows-vscode, or a small gxwf-ui-side shim? Preference is upstream so desktop/web share binding config.
  9. packageBundle.json manifest format for folder: loader — define shape in Phase 0.5.5.
  10. Classic-worker strategy for the extension host — A/B/C in Phase 0.5.7? Resolved: Option D (getWorkerOptions{ type: "module" }) confirmed in spike.
  11. Iframe-HTML delivery — postinstall copy script vs. dedicated Vite plugin? Either works; picking the simpler of the two.
  12. Where does the extension-file:// → HTTP rewrite belong for extension-spawned LSP workers (Phase 0.5.8)? Resolved: nowhere — monaco-vscode-api’s patchWorker already handles this. See 0.5.8 dissolution note.
  13. Service-override set likely needs environment / host / log / storage additions before activation completes Resolved: the spike’s set (extensions, languages, textmate, theme, configuration, files, keybindings, notifications, quickaccess) is sufficient to reach a working LSP. Add more only as concrete needs emerge.

Previously listed, now answered in the plan body: