DialogramTalk to your diagrams. Dialogram is a general-purpose diagram platform for VS Code extensions, built on Eclipse GLSP, with an integrated agent chat driven by opencode over the Agent Client Protocol (ACP). A consumer extension contributes a diagram profile — its model source, edit strategy, command ids, chat configuration — and gets the full platform in return:
The platform core is strictly product-neutral: no consumer vocabulary appears in the core packages (enforced in CI, see Neutrality gates). Everything product-specific arrives through the profile. Multiple consumers can be installed simultaneously — every command id, view type, and settings namespace is consumer-owned and disjoint by contract. ArchitectureDialogram ships as a VS Code extension (
The GLSP server runs inside the extension host (not a separate process).
Everything a profile registers is bound to the consumer's
Packages
Only Consuming DialogramThere are three ways in, ordered from simplest to most invasive. 1. Sidecar-backed diagrams (the wfpy / calpy path)For a consumer whose source files are rewritten by an external sidecar
CLI (see The sidecar contract). Declare Dialogram
in
2. Build-time library (custom views and model source)For a consumer with fully custom views and its own model source. Instead
of calling across the extension boundary, the consumer bundles
One realm rule governs this mode: one esbuild bundle = one inversify/GLSP
realm. The consumer's bundler must force a single instance of 3. Chat onlyA consumer that keeps its own diagram stack can still adopt the chat runtime — ACP/opencode sessions, revert, file/graph context injection, per-turn selection, in-process MCP tools — while owning the webview UI and transport:
The
|
| Group | Fields |
|---|---|
| Identity | key, displayName, settingsNamespace, customEditorViewType, glspClientId, glspClientName |
| Commands | commands (21 consumer-owned command ids), operationKinds (port create/delete kind strings) |
| Model & edits | modelSource factory, edits ('read-only' or a strategy with operation modules), serverModules, serverDiagramModule (library mode only), storageOptions |
| Client | clientBehavior (neutral capability flags injected into the webview), clientAssets (custom webview bundle — data only), onWebviewMessage (inbound message hook) |
| Features | watch (file globs), navigation (cross-file drill-down), canOpenSource (openability predicate), editBackend (chat mutation seam), chat (chat carry-overs), runDriver (factory receiving a DiagramRunHost), newSourceFile |
Everything optional degrades gracefully: no chat → no chat backend, no
runDriver → no run/stop commands or live glow, no clientAssets → the
stock webview bundle.
The returned DiagramProfileHandle exposes chat diagnostics plus two
webview channels: dispatchToWebview(uri, action) (host→client GLSP action
over the overlay bridge) and postToWebview(uri, message) (raw
webview.postMessage for consumer-owned protocols such as cursor sync).
API versioning
Pre-1.0, major.minor must match exactly — a minor bump may break.
Consumers must call isApiVersionCompatible(api.apiVersion) at activation
and fail with an actionable message. 0.2.0 is the DiagramProfile v2
contract described here.
The bundle-boundary law
The one rule that shapes the whole API, learned the hard way:
DI-decorated classes, inversify
ContainerModules, andSymbol-keyed bindings must never cross an esbuild bundle boundary. Only data, closures, and structurally-typed plain objects may.
Each bundle carries its own inversify realm and Symbol table; a class
constructed in a consumer's bundle has no injection metadata in the
platform's realm and fails at resolution time (or worse, at GLSP's
DI-less new ctor() probes). Consequences baked into the API:
- Sidecar profiles are assembled platform-side by
createSidecarDiagramProfile; the assembler stampsSymbol.for('dialogram.platformAssembledProfile')on the result so the platform can tell its own DI objects round-tripping through the consumer from foreign ones. assertProfileCrossesPlatformApiSafelyrejects unbrandedserverModules/edits.operationModulesat the API boundary, and rejectsserverDiagramModuleunconditionally — that field is build-time-library only.DiagramProfile.clientAssetsis path strings, never code objects.- Library-mode consumers must force singleton resolution of inversify/GLSP in their bundler so their extension bundle is one realm.
The sidecar contract
Sidecar-backed consumers never touch source text from the extension. Every
diagram edit becomes an operation sent to an external sidecar CLI
(e.g. wfpy-sidecar, calpy-sidecar — libcst-based) that rewrites the file
while preserving formatting. The wire protocol:
- newline-delimited JSON over stdin/stdout,
- requests
{ file, op: "<prefix>.<name>", args }, - responses carry a
revisioncontent hash; mutations passexpectedRevisionfor optimistic concurrency, - capabilities discovered through
getCapabilities, - the source file is the single source of truth — no state-file overlays.
The toolkit's characterization suites run against fake sidecar/CLI fixtures
in packages/sidecar-toolkit/test/fixtures/, so the full protocol is
exercised without any product installed.
Execution overlays and run drivers
A profile's runDriver factory receives a DiagramRunHost:
overlay: ExecutionOverlaySink— push neutral execution events; the platform routes them to the webview asdialogram.executionOverlayactions (node glow, running-agents bar, SSE token streaming),requestRefresh(sourceUri, kind)— ask for a model rebuild without holding the GLSP connector,useLiveOverlaySignatureSource(...)— drive live-execution glow for runs started outside the IDE.
Overlay events are replayed (bounded, anchor-preserving) when a webview is
closed and reopened mid-run, so the bar and glow survive tab churn. The
toolkit ships CliRunDriver — a complete implementation over a spawned CLI
with SSE event streaming, per-entity agent-tool overrides, and process
control — which sidecar profiles get for free.
Development
Requires Node 20 (volta-pinned). TypeScript strict, four-space indentation,
single quotes, all packages ESM ("type": "module"; host bundles emitted as
.cjs).
npm install
npm run build # tsc -b + esbuild → out/, dist/
npm run watch # incremental rebuild
npm test # vitest suites: diagram-server, diagram-client, extension-core, sidecar-toolkit
npm run check:neutrality # the three product-neutrality gates (CI gate)
npm run package # vsce package → dialogram-<version>.vsix at repo root
Single test file: from the package directory,
npx vitest run test/<file>.test.ts. Tests alias the vscode module to a
local mock and run in a node environment; the diagram-client suite also
proves the library consumer path by running the documented esbuild recipe
headlessly.
Neutrality gates
npm run check:neutrality enforces that the platform stays product-free:
- Core content — no
sidecar|wfpy|calpy|pythontoken anywhere indiagram-server,diagram-client, orextension-coresources (single sanctioned exception:legacy-settings-compat.ts, which names legacy settings keys). - Toolkit content — no
wfpy|calpy|calLangtoken insidecar-toolkitsources; the toolkit is sidecar-generic, products inject their strings. - File names — no product-derived file names in the core packages.
If a change trips a gate, the fix is to move the product value into the consumer's profile input — never to extend the allow-list.
Debugging aids
[dialogram build] <bundle> <ISO timestamp> <git sha>— stamped into each bundle at build time; check the extension host console to confirm the running bundles are fresh.[dialogram perf] load <file>#<n>: acquire=… layout=… loadTotal=…— per-load phase attribution emitted by the diagram server, including which enrichment phases were deferred off the first paint. Loads after the first should be low-millisecond (memoized); expensive scans (caller references, skills/agents enrichment) run bounded in the background and redeliver viarefresh-*model requests.
Documentation
packages/diagram-client/README.md— the complete client-library consumer contract.
Status
The DiagramProfile v2 program (fully neutral core, sidecar toolkit, client
library, first library-mode consumer) is complete at API 0.2.0. Known open items: a
DiagramProfile.diagramType field, publishing the @dialogram/api npm
package and the Marketplace publisher, and passing opencodePath into the
ACP client explicitly instead of via the environment.