Custom interfaces
Cabbage JS API
The Cabbage JavaScript API provides the communication layer between webview UIs and the Cabbage backend. This API enables bidirectional communication for plugin development.
Architecture Overview
Section titled “Architecture Overview”UI → Backend (Outgoing Communication)
Section titled “UI → Backend (Outgoing Communication)”Three functions cover everything a UI sends to the backend. Use Cabbage.sendControlData() to send widget value changes; the backend automatically determines routing based on channel automatable status. Use Cabbage.sendWidgetUpdate() to send widget property updates (position, visibility, colours, and so on) as extended JSON packets. Use Cabbage.sendChannelData() to write a channel directly, bypassing the widget layer.
Backend → UI (Incoming Communication)
Section titled “Backend → UI (Incoming Communication)”Use Cabbage.addMessageListener(callback) to receive messages from the backend. This works in all environments — VS Code, native DAW plugins, and custom UI frameworks — without any environment-specific setup.
API Reference
Section titled “API Reference”Core Functions
Section titled “Core Functions”Cabbage.addMessageListener(callback)
Section titled “Cabbage.addMessageListener(callback)”Register a listener for incoming messages from the Cabbage backend. This is the recommended way to handle all backend → UI communication. It abstracts the two runtime environments so that the same UI code works everywhere:
- VS Code (custom UI / iframe relay): messages arrive as
windowmessage events - Native plugin (DAW): messages arrive via
window.hostMessageCallback
Parameters:
callback(function): Called with each incoming message object
Returns: A cleanup function — call it when the UI is torn down to avoid memory leaks.
Incoming message shapes:
command |
Fields | Sent by |
|---|---|---|
parameterChange |
paramIdx, value (may be top-level or nested in data) |
DAW automation / backend |
widgetUpdate |
id, value |
cabbageSetValue opcode |
widgetUpdate |
id, widgetJson |
cabbageSet opcode |
channelDataUpdate |
channel, value |
Csound channel data |
resizeResponse |
accepted, width, height |
Response to requestResize() |
Example — Vanilla HTML:
import { Cabbage } from './cabbage/cabbage.js';
Cabbage.addMessageListener((msg) => { if (msg.command === 'parameterChange') { // paramIdx/value may be top-level or nested in msg.data (DAW host dependent) const data = msg.data ?? msg; updateSlider(data.paramIdx, data.value); } else if (msg.command === 'widgetUpdate') { // id and value are always top-level if (msg.value !== undefined) { document.getElementById(msg.id).value = msg.value; } else if (msg.widgetJson !== undefined) { const widget = JSON.parse(msg.widgetJson); // apply full widget update... } }});Example — Svelte:
import { Cabbage } from 'cabbage';import { onMount, onDestroy } from 'svelte';
let removeListener;
onMount(() => { removeListener = Cabbage.addMessageListener((msg) => { if (msg.command === 'widgetUpdate' && msg.id === 'myChannel') { myValue = msg.value; } });});
onDestroy(() => removeListener?.());Cabbage.sendControlData(data, vscode)
Section titled “Cabbage.sendControlData(data, vscode)”Send widget value changes to the Cabbage backend. This is the primary API for user interactions.
Parameters:
data(Object): Control data objectchannel(string): The channel namevalue(number|string): Value in natural range (e.g., 20-20000 Hz for frequency)gesture(string, optional): Gesture type -"begin","value","end","complete"(default:"complete")
vscode(Object|null): VS Code API instance (passnullfor plugin mode)
Behavior:
- Automatically routes based on channel automatable status
- Thread-safe: Asynchronous, doesn’t block audio thread
- Values sent in natural ranges, backend handles normalization
- String values: on a non-automatable channel, a string value is stored as the channel’s
stringValue(and into the matching Csound string channel). String state is included in session/preset saves and re-pushed in the launch dumps — see Persisting Custom UI State. The channel must be declared with"type": "string", e.g."channels": [{ "id": "myState", "type": "string" }].
Examples:
// Basic usageCabbage.sendControlData({ channel: "frequency", value: 1000 }, null);
// String state (persists across UI reloads via session save)Cabbage.sendControlData({ channel: "myState", value: JSON.stringify(myState) }, null);
// With gesture for DAW automation recordingCabbage.sendControlData({ channel: "volume", value: 0.8, gesture: "begin" }, null);Cabbage.sendControlData({ channel: "volume", value: 0.9, gesture: "value" }, null);Cabbage.sendControlData({ channel: "volume", value: 0.9, gesture: "end" }, null);Cabbage.sendChannelData(channel, data, vscode)
Section titled “Cabbage.sendChannelData(channel, data, vscode)”Send channel data directly to Csound without DAW automation — bypasses all widget routing and validation.
Parameters:
channel(string): Csound channel namedata(number|string): Data to sendvscode(Object|null): VS Code API instance
Notes:
- Use
sendControlData()instead for most cases as it determines the best route automatically - Bypasses DAW automation system
- Automatically determines string vs numeric data
Cabbage.sendWidgetUpdate(widget, vscode)
Section titled “Cabbage.sendWidgetUpdate(widget, vscode)”Send extended JSON widget property packets to the Cabbage backend. The packet is merge-patched into the stored widget object, so this is the recommended way for a custom frontend to update widget properties such as bounds, visible, or colours at runtime. It is the counterpart to sendControlData(): values travel through sendControlData(), everything else travels through here.
Parameters:
widget(Object): Widget packet. Must identify the widget with anid, either a top-levelidor anychannels[].id, plus the properties to changevscode(Object|null): VS Code API instance (passnullfor plugin mode)
Behavior:
- Merge-patch semantics: only the supplied properties are merged, so partial packets are fine, but a
nullvalue deletes its key, and arrays replace wholesale, so always send complete arrays (e.g. a fullchannelsarray, never a fragment of one) - Values cannot travel this way:
valueandrange.valueare stripped before sending, along with binding and runtime keys (parameterIndex,channel,samples, file paths), value changes must go throughsendControlData()(orsendChannelData()). Stripping happens twice,sendWidgetUpdate()sanitizes the packet viaCabbageUtils.sanitizeForEditor(), and the backend strips the same keys again before merging, so the guarantee holds even for hand-built packets. Note stored widgets may already show a backend-stamped top-levelvalue: that is live runtime state, and a packet’svaluenever overwrites it - Fire-and-forget: the backend applies the patch without echoing it back, so apply the change locally in the UI as well; packets with an unknown id fail silently, so double-check the spelling. The silence is deliberate: re-broadcasting full widget packets, which can carry large
samplesarrays, risks echo loops that flood the strings-only message queue, so property updates are applied without notifying other views
Example:
// Move a widget and hide another, from a custom frontendCabbage.sendWidgetUpdate({ id: "mainGain", bounds: { left: 40, top: 20 } }, null);Cabbage.sendWidgetUpdate({ id: "debugPanel", visible: 0 }, null);Cabbage.sendMidiMessageFromUI(statusByte, dataByte1, dataByte2, vscode)
Section titled “Cabbage.sendMidiMessageFromUI(statusByte, dataByte1, dataByte2, vscode)”Send MIDI messages from the UI to the backend.
Parameters:
statusByte(number): MIDI status bytedataByte1(number): First data bytedataByte2(number): Second data bytevscode(Object|null): VS Code API instance
Example:
Cabbage.sendMidiMessageFromUI(144, 60, 100, null); // Note onCabbage.MidiMessageFromHost(statusByte, dataByte1, dataByte2)
Section titled “Cabbage.MidiMessageFromHost(statusByte, dataByte1, dataByte2)”Handle incoming MIDI messages from the backend (callback function).
Parameters:
statusByte(number): MIDI status bytedataByte1(number): First data bytedataByte2(number): Second data byte
Cabbage.isReadyToLoad(vscode)
Section titled “Cabbage.isReadyToLoad(vscode)”Signal that the UI is ready to load and initialize. This must be called in order to notify the backend that the front has completed its initial load.
Parameters:
vscode(Object|null): VS Code API instance
Utility Functions
Section titled “Utility Functions”Cabbage.triggerFileOpenDialog(vscode, channel, options)
Section titled “Cabbage.triggerFileOpenDialog(vscode, channel, options)”Trigger a file open dialog for file selection widgets.
Parameters:
vscode(Object|null): VS Code API instancechannel(string): Associated channel nameoptions(Object, optional): Dialog optionsdirectory(string): Starting directoryfilters(string): File filtersopenAtLastKnownLocation(boolean): Use last known location
Example:
Cabbage.triggerFileOpenDialog(null, "audioFile", { filters: "*.wav;*.aiff", openAtLastKnownLocation: true});Cabbage.openUrl(vscode, url, file)
Section titled “Cabbage.openUrl(vscode, url, file)”Open a URL or file in the system default application.
Parameters:
vscode(Object|null): VS Code API instanceurl(string): URL to openfile(string): File path to open
Cabbage.consumeKeypresses(consume) (Windows plugin mode only)
Section titled “Cabbage.consumeKeypresses(consume) (Windows plugin mode only)”Control whether keyboard events are captured by the webview or forwarded to the host DAW.
By default (false), all key events are passed through to the DAW so keyboard shortcuts keep working while the plugin UI has focus. This is handled automatically by Cabbage’s internal key listener.
Parameters:
consume(boolean):trueto capture keys in the webview,falseto pass them through to the DAW (default)
Notes:
- Native
<input>and<textarea>elements are handled automatically — no need to call this - Only required for custom text-entry widgets built from non-native elements (e.g. a
div-based code editor) - Always restore to
falsewhen the custom widget loses focus
Example:
myCustomEditor.addEventListener('focus', () => Cabbage.consumeKeypresses(true));myCustomEditor.addEventListener('blur', () => Cabbage.consumeKeypresses(false));Cabbage.requestResize(width, height, vscode)
Section titled “Cabbage.requestResize(width, height, vscode)”Request a resize of the plugin GUI window (plugin mode only).
Parameters:
width(number): Requested width in pixelsheight(number): Requested height in pixelsvscode(Object|null): VS Code API instance
Notes:
- Only works in plugin mode (CLAP/VST3/AUv2)
- Host may accept or reject the request
- Backend sends a
resizeResponsemessage indicating acceptance and final dimensions
Cabbage.sendCustomCommand(command, vscode, data)
Section titled “Cabbage.sendCustomCommand(command, vscode, data)”Send a custom command to the backend for specialized operations.
Parameters:
command(string): Command namevscode(Object|null): VS Code API instancedata(Object, optional): Additional data to send
Example:
Cabbage.sendCustomCommand('cabbageIsReadyToLoad', null);Incoming Messages (Backend → UI)
Section titled “Incoming Messages (Backend → UI)”Use Cabbage.addMessageListener() to handle all incoming messages. See the full API entry above for details and examples.
Message Types
Section titled “Message Types”parameterChange
Section titled “parameterChange”- Purpose: Parameter updates from DAW automation or backend
- Fields:
paramIdx,value— may be top-level or nested insidedatadepending on the host - Action: Update visual display only — never call
sendControlData()in response (causes feedback loops)
widgetUpdate
Section titled “widgetUpdate”- Purpose: Widget value or property update from Csound opcodes (
cabbageSetValue,cabbageSet) - Fields:
id(string), and eithervalue(number) for simple updates orwidgetJson(string) for full property updates - Action: Update the corresponding widget’s value or properties
channelDataUpdate
Section titled “channelDataUpdate”- Purpose: Channel data from Csound
- Fields:
channel,value
csoundOutputUpdate
Section titled “csoundOutputUpdate”- Purpose: Csound console output for debugging
- Fields:
text - Action: Display or log Csound output
resizeResponse
Section titled “resizeResponse”- Purpose: Response to GUI resize requests
- Fields:
accepted(boolean),width(number),height(number) - Action: Handle resize acceptance/rejection and update UI dimensions
Gesture Types
Section titled “Gesture Types”Use appropriate gestures for proper DAW automation recording:
"begin": Start of user interaction (e.g., mouse down on slider)"value": Continuous updates during interaction (e.g., mouse drag)"end": End of continuous interaction (e.g., mouse up)"complete": Discrete actions (e.g., button clicks, default)
Best Practices
Section titled “Best Practices”Avoiding Feedback Loops
Section titled “Avoiding Feedback Loops”When receiving parameterChange messages, only update the visual display. Never call sendControlData() in response, as this creates feedback loops that interfere with DAW automation.
isDragging Pattern
Section titled “isDragging Pattern”When a user is actively dragging a slider, ignore incoming parameterChange updates to prevent the UI fighting with the user’s input:
let isDragging = false;
slider.addEventListener('pointerdown', () => { isDragging = true; });slider.addEventListener('pointerup', () => { isDragging = false; });
Cabbage.addMessageListener((msg) => { if (msg.command === 'parameterChange' && !isDragging) { const data = msg.data ?? msg; slider.value = data.value; }});Thread Safety
Section titled “Thread Safety”All outgoing functions are asynchronous and thread-safe. They queue messages without blocking the audio thread.
Value Ranges
Section titled “Value Ranges”Send values in their full/natural ranges (e.g., 20-20000 Hz for frequency). The backend handles all DAW normalization automatically.
Environment Detection
Section titled “Environment Detection”Pass null for the vscode parameter in all API calls — the library detects the environment automatically. The vscode parameter is a legacy option retained for backwards compatibility with older custom UIs that acquired the VS Code API directly.