Skip to content
vizhub-studioPublic

About

Mob Programming Code Editor

Resources

Contributing

Stars

92 stars

Watchers

5 watching

Forks

Repository files navigation

VZCode: Mob Programming Code Editor

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.

VZCode Interface

Documentation map

  • Getting started: Usage and Development below. The package.json scripts provide npm run dev, npm run test-interactive, npm test, npm run typecheck, and npm 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 vzcode command does not automatically install the desktop application; consult Usage.

Table of Contents

Usage

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.

Development

cd vzcode
npm install
npm run test-interactive

This 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 dev

npm 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-msvc

You can use npm link to test the package from another project.

Features

  • 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

Keyboard Shortcuts

  • Ctrl+S or Shift+Enter: format the active file and run the code
  • Ctrl+Shift+E: open Files
  • Ctrl+Shift+F: open Search
  • Ctrl+Shift+K: open Keyboard Shortcuts
  • Ctrl+Shift+S or Ctrl+,: open Editor Settings
  • Ctrl+Shift+N: open the create-file dialog
  • Ctrl+Shift+D: open the create-directory dialog
  • Ctrl+Shift+A: toggle automatic following of another participant
  • Alt+N: open the create-file dialog
  • Alt+W: close the active tab
  • Alt+PageUp / Alt+PageDown: switch to the previous/next tab
  • Ctrl+click an identifier: jump to its closest definition

The Keyboard Shortcuts panel in the editor lists additional shortcuts.

Use Cases

  • 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.

Optional integrations

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=true enables LiveKit voice chat. Configure VITE_LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET for your LiveKit deployment.
  • VITE_ENABLE_VOICE_INPUT=true enables speech-to-text in AI chat. Availability depends on browser speech-recognition support and microphone permissions.

Desktop application

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.

Embedding the Visual Editor in a host application

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.

Visual Editor

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.

Overview

The visual editor works by:

  1. Loading a config.json file that defines configuration parameters
  2. Defining interactive widgets in the visualEditorWidgets array
  3. Listening for configuration updates via postMessage
  4. Re-rendering the visualization when configuration changes

Configuration Structure

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
    }
  ]
}

Widget Configuration

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 slider widgets)
  • max: Maximum value (required for slider widgets)
  • step: Step increment for slider widgets (optional, defaults to 1 if not specified)
  • options: Array of string options (required for dropdown widgets)

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.

Setting Up Your Visualization

To make your visualization work with the visual editor, you need to:

1. Load the Configuration

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
};

2. Set Up postMessage Event Listener

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
};

3. Use Configuration in Your Rendering

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 value

4. Handle Loading States

Display 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);
};

5. Manage Asynchronous Data Loading

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 });
    });
};

Complete Example

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,
    });
  }
};

Required HTML Structure

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>

Styling

Use CSS to ensure your visualization container fills the viewport:

#viz-container {
  position: fixed;
  inset: 0;
}

Best Practices

  1. Use d3-rosetta: The visual editor is designed to work with d3-rosetta's unidirectional data flow pattern
  2. Modular Code: Separate your rendering logic into small, focused functions
  3. Configuration-Driven: Make visual properties configurable through config.json
  4. Nested Properties: Use dot notation (e.g., "margin.left") to modify nested configuration values
  5. 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.

Stack

Built using technologies such as:

Goals

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

Prior Work

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.

Prior Work Image

Spaces

Embedded editor styling

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.

Legacy appearance compatibility

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.

About

Mob Programming Code Editor

Resources

Contributing

Stars

92 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages