VZCode offers a multiplayer code editing environment that caters to a real-time collaborative development experience. It's the code editor component of VizHub, and can also be used independently from VizHub.
- Getting started: Usage and Development below. The
package.jsonscripts providenpm run dev,npm run test-interactive,npm test,npm run typecheck, andnpm run lint. - Using the editor in other applications: Embedding the Visual Editor.
- Contributing: CONTRIBUTING.md, AGENTS.md and the issue tracker.
- Status: Browser development and desktop packaging have different requirements. The
vzcodecommand does not automatically install the desktop application; consult Usage.
- Documentation map
- Usage
- Development
- Features
- Keyboard Shortcuts
- Use Cases
- Optional integrations
- Desktop application
- Embedding the Visual Editor in a host application
- Visual Editor
- Stack
- Goals
- Prior Work
- Spaces
- Embedded editor styling
The vzcode command is currently a launcher for a built Tauri desktop
application; installing the npm package alone does not include that application.
The --browser option is not a browser-server launcher yet. To run the browser
editor from a checkout, see Development.
VZCode loads and auto-saves files in the server's working directory. It does not watch for external filesystem changes; edits made by another program while VZCode is running can be overwritten by an auto-save.
To collaborate, make the server reachable to the other editors on your network.
For public access, configure a tunnel or reverse proxy yourself; VZCode does not
start an ngrok tunnel or use NGROK_TOKEN. Only share access with people you
trust.
-
Backlog & Issues: Use the repository issue tracker and pull requests. Historical organization project-board links are not the maintained onboarding entry point.
-
Local Setup:
cd vzcode
npm install
npm run test-interactiveThis starts the editor server with the sample playground workspace on port
3030 and opens it in a browser. For local client development with hot reloading,
run the development command instead:
npm run devnpm run dev starts both the backend and Vite. Vite serves the client at
http://localhost:5173 and proxies requests to the backend on port 3030; do not
start it alongside a separate npm run test-interactive server. On Windows,
install the platform-specific Rollup package if npm reports that it is missing:
npm install @rollup/rollup-win32-x64-msvcYou can use npm link to test the package from another project.
- Browser-based editor with syntax highlighting, completion, diagnostics, formatting, and common VS Code key bindings
- Real-time collaborative editing with participant cursors and presence
- File and nested-directory browsing, tabs, file search, and create, rename, and delete operations
- Text editing and image preview
- Code execution in a preview pane; run actions format the active file and refresh the preview
- Interactive widgets for editing numbers (Alt+drag) and colors (Alt+click)
- Debounced auto-save, with throttled saves while interacting with widgets
- AI chat for code editing, streaming responses, and change diffs; AI requires a configured provider
- A Visual Editor panel for configurable widgets backed by
config.json - Editor settings for theme, font, font size, and collaboration username; the editor includes ten themes
- Optional LiveKit voice chat and speech-to-text input (see Optional integrations)
- A Tauri desktop application when built with the desktop toolchain
Ctrl+SorShift+Enter: format the active file and run the codeCtrl+Shift+E: open FilesCtrl+Shift+F: open SearchCtrl+Shift+K: open Keyboard ShortcutsCtrl+Shift+SorCtrl+,: open Editor SettingsCtrl+Shift+N: open the create-file dialogCtrl+Shift+D: open the create-directory dialogCtrl+Shift+A: toggle automatic following of another participantAlt+N: open the create-file dialogAlt+W: close the active tabAlt+PageUp/Alt+PageDown: switch to the previous/next tabCtrl+clickan identifier: jump to its closest definition
The Keyboard Shortcuts panel in the editor lists additional shortcuts.
-
Local Editor: Use VZCode like VSCode or Vim:
npm install -g vzcode cd myProject vzcode -
Project-specific Editor: Embed VZCode within your project for developers who might not have a preferred IDE, or to provide an editing experience that seamlessly integrates with hot reloading.
{ "name": "example-project", "scripts": { "edit": "vzcode" }, "dependencies": { "vzcode": "^3.2.0" } }Run using
npm run edit.For example, as the editor of Vite D3 Template, which showcases the throttled auto-save behavior of VZCode while using the interactive widgets in the context of editing files served by the Vite dev server which supports hot reloading.
-
Staging Site Editor (Experimental): Use VZCode on a persistent server, making code changes with multiplayer mode remotely, reflecting instantly on a staging site.
To configure AI chat, copy .env.example to .env and set
VZCODE_EDIT_WITH_AI_API_KEY. The optional
VZCODE_EDIT_WITH_AI_MODEL_NAME selects a model, and
VZCODE_EDIT_WITH_AI_BASE_URL can point to an OpenAI-compatible endpoint.
Prompts and project context are sent to the configured provider.
Set these variables in the client build environment to enable optional voice features:
VITE_ENABLE_LIVEKIT=trueenables LiveKit voice chat. ConfigureVITE_LIVEKIT_URL,LIVEKIT_API_KEY, andLIVEKIT_API_SECRETfor your LiveKit deployment.VITE_ENABLE_VOICE_INPUT=trueenables speech-to-text in AI chat. Availability depends on browser speech-recognition support and microphone permissions.
The desktop wrapper uses Tauri. With Rust and the platform's Tauri prerequisites
installed, run npm run tauri:dev for development or npm run tauri:build to
build the desktop application. The vzcode launcher can open a built desktop
binary; see Usage for its current limitations.
Embedded clients can pass initialSidebarView="visual" to VZCodeProvider to open the Visual panel (standalone VZCode keeps its existing AI chat default). Pass renderVisualEditorEmptyState={(reason) => ...} to replace built-in empty states with your own UI. The callback receives missing-config, invalid-config, missing-widgets, invalid-widgets, or empty-widgets; return null to use the default message. A nonempty, valid visualEditorWidgets array activates the normal widgets.
When embedding VZCode beside a host-owned visualization iframe, pass the mutable iframeRef to VZCodeProvider. VZCode sends top-level config patches via postMessage after config.json changes; the host must make sure the iframe implements the message listener and that iframe recreations receive the current config. Host applications own any AI setup action: VZCode never triggers paid generation itself.
VZCode includes a visual editor feature that allows you to create interactive widgets (like sliders and color pickers) for tweaking configuration parameters in real-time. This is particularly useful for data visualizations where you want to adjust visual properties dynamically without editing code.
The visual editor works by:
- Loading a
config.jsonfile that defines configuration parameters - Defining interactive widgets in the
visualEditorWidgetsarray - Listening for configuration updates via
postMessage - Re-rendering the visualization when configuration changes
Your project should include a config.json file with the following structure:
{
"xValue": "sepal_length",
"yValue": "sepal_width",
"margin": {
"top": 20,
"right": 67,
"bottom": 60,
"left": 60
},
"fontSize": "14px",
"fontFamily": "sans-serif",
"pointRadius": 17.2675034867503,
"pointFill": "black",
"pointOpacity": 0.7,
"loadingFontSize": "24px",
"loadingFontFamily": "sans-serif",
"loadingMessage": "Loading...",
"dataUrl": "iris.csv",
"colorScale": {
"setosa": "#1f77b4",
"versicolor": "#ff7f0e",
"virginica": "#2ca02c"
},
"visualEditorWidgets": [
{
"type": "slider",
"label": "Point Radius",
"property": "pointRadius",
"min": 1,
"max": 30
},
{
"type": "slider",
"label": "Left Margin",
"property": "margin.left",
"min": 0,
"max": 200
}
]
}The visualEditorWidgets array defines the interactive controls that will be available in the visual editor. Each widget has the following properties:
- type: The type of widget. Supported types are:
"slider"- A slider control for numeric values"checkbox"- A checkbox for boolean values"textInput"- A text input field for string values"dropdown"- A dropdown selector with predefined options"color"- A color picker with HCL (Hue, Chroma, Lightness) sliders
- label: Human-readable label displayed in the UI
- property: The configuration property to modify (supports nested properties using dot notation like
"margin.left") - min: Minimum value (required for
sliderwidgets) - max: Maximum value (required for
sliderwidgets) - step: Step increment for
sliderwidgets (optional, defaults to 1 if not specified) - options: Array of string options (required for
dropdownwidgets)
Every widget's property must resolve to an existing value in config.json (dot-separated paths such as margin.left are supported, but arrays and prototype keys are not editable). Match the value to the widget: finite number for slider, boolean for checkbox, string for textInput, one of options for dropdown, and a valid, opaque CSS color for color. Invalid widget values or paths show the host-configurable invalid-widgets empty state rather than rendering broken controls.
To make your visualization work with the visual editor, you need to:
Use d3-rosetta's state management to load the configuration file:
import { createStateField } from 'd3-rosetta';
import { json } from 'd3';
export const viz = (container, state, setState) => {
const stateField = createStateField(state, setState);
const [config, setConfig] = stateField('config');
// Load config first if not already loaded
if (!config) {
json('config.json')
.then((loadedConfig) => {
setConfig(loadedConfig);
})
.catch((error) => {
console.error('Failed to load config:', error);
});
return;
}
// ... rest of your visualization code
};Add an event listener to receive configuration updates from the visual editor:
export const viz = (container, state, setState) => {
// ... other code ...
// Set up postMessage event listener if not already set
if (!state.eventListenerAttached) {
window.addEventListener('message', (event) => {
// Verify the message contains config data
if (event.data && typeof event.data === 'object') {
// Update the config with the received data
setState((state) => ({
...state,
config: {
...state.config,
...event.data,
},
}));
}
});
// Mark that we've attached the event listener to avoid duplicates
setState((prevState) => ({
...prevState,
eventListenerAttached: true,
}));
}
// ... rest of your visualization code
};Use the configuration values when rendering your visualization:
// Example: Rendering data points with configurable properties
export const renderMarks = (
svg,
{
data,
xScale,
yScale,
xValue,
yValue,
pointRadius,
colorScale,
pointOpacity,
},
) =>
svg
.selectAll('circle.data-point')
.data(data)
.join('circle')
.attr('class', 'data-point')
.attr('cx', (d) => xScale(xValue(d)))
.attr('cy', (d) => yScale(yValue(d)))
.attr('r', pointRadius) // Uses config value
.attr('fill', (d) => colorScale[d.species])
.attr('opacity', pointOpacity); // Uses config valueDisplay loading states while data is being fetched:
export const renderLoadingState = (
svg,
{ x, y, text, shouldShow, fontSize, fontFamily },
) => {
svg
.selectAll('text.loading-text')
.data(shouldShow ? [null] : [])
.join('text')
.attr('class', 'loading-text')
.attr('x', x)
.attr('y', y)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-size', fontSize) // Uses config value
.attr('font-family', fontFamily) // Uses config value
.text(text);
};Handle asynchronous data requests with proper state management:
export const asyncRequest = (
setDataRequest,
loadAndParseData,
) => {
setDataRequest({ status: 'Loading' });
loadAndParseData()
.then((data) => {
setDataRequest({ status: 'Succeeded', data });
})
.catch((error) => {
setDataRequest({ status: 'Failed', error });
});
};Here's how everything fits together in your main visualization file:
import { createStateField } from 'd3-rosetta';
import { setupSVG } from './setupSVG.js';
import { renderLoadingState } from './renderLoadingState.js';
import { asyncRequest } from './asyncRequest.js';
import { loadAndParseData } from './loadAndParseData.js';
import { scatterPlot } from './scatterPlot.js';
import { measureDimensions } from './measureDimensions.js';
import { json } from 'd3';
export const viz = (container, state, setState) => {
const stateField = createStateField(state, setState);
const [dataRequest, setDataRequest] =
stateField('dataRequest');
const [config, setConfig] = stateField('config');
// Set up postMessage event listener if not already set
if (!state.eventListenerAttached) {
window.addEventListener('message', (event) => {
if (event.data && typeof event.data === 'object') {
setState((state) => ({
...state,
config: {
...state.config,
...event.data,
},
}));
}
});
setState((prevState) => ({
...prevState,
eventListenerAttached: true,
}));
}
// Load config first if not already loaded
if (!config) {
json('config.json')
.then((loadedConfig) => {
setConfig(loadedConfig);
})
.catch((error) => {
console.error('Failed to load config:', error);
});
return;
}
// After config is loaded, load the data
if (!dataRequest) {
return asyncRequest(setDataRequest, () =>
loadAndParseData(config.dataUrl),
);
}
const { data, error } = dataRequest;
const dimensions = measureDimensions(container);
const svg = setupSVG(container, dimensions);
renderLoadingState(svg, {
shouldShow: !data,
text: error
? `Error: ${error.message}`
: config.loadingMessage,
x: dimensions.width / 2,
y: dimensions.height / 2,
fontSize: config.loadingFontSize,
fontFamily: config.loadingFontFamily,
});
if (data) {
// Transform string properties in config to accessor functions
const configWithAccessors = {
...config,
xValue: (d) => d[config.xValue],
yValue: (d) => d[config.yValue],
};
scatterPlot(svg, {
...configWithAccessors,
data,
dimensions,
});
}
};Your index.html should include a container element and proper script imports:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Visual Editor Example</title>
<link rel="stylesheet" href="styles.css" />
<script type="importmap">
{
"imports": {
"d3": "https://cdn.jsdelivr.net/npm/d3@7.9.0/+esm",
"d3-rosetta": "https://cdn.jsdelivr.net/npm/d3-rosetta@3.0.0/+esm"
}
}
</script>
</head>
<body>
<div id="viz-container"></div>
<script type="module" src="index.js"></script>
</body>
</html>Use CSS to ensure your visualization container fills the viewport:
#viz-container {
position: fixed;
inset: 0;
}- Use d3-rosetta: The visual editor is designed to work with d3-rosetta's unidirectional data flow pattern
- Modular Code: Separate your rendering logic into small, focused functions
- Configuration-Driven: Make visual properties configurable through
config.json - Nested Properties: Use dot notation (e.g.,
"margin.left") to modify nested configuration values - Hot Reloading: The visual editor works seamlessly with VZCode's throttled auto-save for hot reloading environments
For a complete working example, see the visualEditor sample directory in this repository.
Built using technologies such as:
The project aims to:
- Offer a feasible alternative to VSCode + Live Share for frontend development
- Facilitate easy project-specific IDE embedding
- Enhance user experience with advanced features
- Support instant feedback through hot reloading
- Keep improving based on feedback
- Serve as a core for VizHub's next-gen editor
VZCode is inspired by VizHub v2. VizHub V2's code editor supports real-time collaboration using older versions of libraries such as CodeMirror 5 and JSON0 OT. For VZCode, the aim is to leverage the latest technologies to deliver a more streamlined experience.
VZCode uses native React controls and editor-scoped SCSS. Importing the editor
does not inject Bootstrap, Bootstrap Icons, or VizHub UI CSS. The editor's
colors are exposed as overridable --vz-color-* variables on the .app
container. src/client/App/style.scss does not reset html, body,
or :root; the standalone app loads src/client/standalone.scss to size
its own document.
Hosts like VizHub AI Studio can continue importing
vzcode/src/client/App/style.scss in their client editor chunk.
The controls and dialogs are styled without requiring a host framework.
The Bootstrap-free editor keeps the original VizHub appearance. The scoped
palette in src/client/ui.scss is derived from
vizhub-ui/scss/_variables.scss
and the font roles from
vizhub-ui/scss/_overrides.scss:
Inter for UI text, Poppins for headings, and Roboto Mono for code.
It retains the original neutral navy scale (#1f2022, #202e46,
#576786, #bdc7dc) and action blue (#3866e9).
All tokens are VZCode-local; hosts must not load the retired vizhub-ui
stylesheet. Native modal controls use the original light VizHub palette;
file-search controls retain the dark editor theme. The
test/editorStyleIsolation.test.ts checks token values and forbids
Bootstrap-era utility classes in production code.

