diff --git a/README.md b/README.md
index d1f8eac..d48635b 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,75 @@ Limits of automatic cleanup:
- It will not automatically remove all event listeners or timers.
- It cannot safely revert all stateful third-party module internals.
+## Jupyter Widgets
+
+The kernel provides built-in support for [Jupyter Widgets](https://ipywidgets.readthedocs.io/) (`ipywidgets`-compatible). Widget classes and helpers are available under `Jupyter.widgets`; destructure the ones you need before using them:
+
+```javascript
+const { IntSlider, IntProgress, jslink } = Jupyter.widgets;
+
+const slider = new IntSlider({
+ value: 50,
+ min: 0,
+ max: 100,
+ description: 'My Slider'
+});
+const progress = new IntProgress({
+ value: 50,
+ min: 0,
+ max: 100,
+ description: 'Mirror'
+});
+display(slider);
+display(progress);
+
+slider.observe(({ new: value }) => {
+ console.log('Slider value:', value);
+}, 'value');
+
+jslink([slider, 'value'], [progress, 'value']);
+```
+
+Widgets auto-display when they are the last expression in a cell. Use the global `display()` function to display a widget explicitly, for example when assigning to a variable.
+
+### Available widgets
+
+- **Numeric**: `IntSlider`, `FloatSlider`, `FloatLogSlider`, `IntRangeSlider`, `FloatRangeSlider`, `Play`, `IntProgress`, `FloatProgress`, `IntText`, `FloatText`, `BoundedIntText`, `BoundedFloatText`
+- **Boolean**: `Checkbox`, `ToggleButton`, `Valid`
+- **Selection**: `Dropdown`, `RadioButtons`, `Select`, `SelectMultiple`, `ToggleButtons`, `SelectionSlider`, `SelectionRangeSlider`
+- **String**: `Text`, `Textarea`, `Password`, `Combobox`
+- **Display**: `Label`, `HTML`, `HTMLMath`, `Output`
+- **Button**: `Button` (with `.onClick()` handler)
+- **Color**: `ColorPicker`
+- **Layout / Style**: `Layout`, `DescriptionStyle`, `SliderStyle`, `ProgressStyle`, `ButtonStyle`, `CheckboxStyle`, `ToggleButtonStyle`, `ToggleButtonsStyle`, `TextStyle`, `HTMLStyle`, `HTMLMathStyle`, `LabelStyle`
+- **Containers**: `Box`, `HBox`, `VBox`, `GridBox`, `Accordion`, `Tab`, `Stack`
+- **Helpers**: `jslink`, `jsdlink`
+
+### Ported widget modules
+
+The widget runtime is split into files that roughly follow the upstream `ipywidgets` package structure so it is easier to track what has been ported.
+
+| Upstream `ipywidgets` file | Local file | Status | Notes |
+| ---------------------------------------------------- | --------------------------------------------------------------------- | ------- | -------------------------------------------------------------------- |
+| `packages/base/src/widget.ts` | `packages/javascript-kernel/src/widgets/widget.ts` | Ported | Kernel-side `Widget` and `DOMWidget` equivalents |
+| `packages/base/src/widget_layout.ts` | `packages/javascript-kernel/src/widgets/widget_layout.ts` | Ported | Layout models |
+| `packages/base/src/widget_style.ts` | `packages/javascript-kernel/src/widgets/widget_style.ts` | Ported | Shared style models, plus control-specific styles gathered here |
+| `packages/controls/src/widget_int.ts` | `packages/javascript-kernel/src/widgets/widget_int.ts` | Ported | Integer widgets, play, progress, and text inputs |
+| `packages/controls/src/widget_float.ts` | `packages/javascript-kernel/src/widgets/widget_float.ts` | Ported | Float widgets |
+| `packages/controls/src/widget_bool.ts` | `packages/javascript-kernel/src/widgets/widget_bool.ts` | Ported | Boolean widgets; related styles live in `widget_style.ts` |
+| `packages/controls/src/widget_selection.ts` | `packages/javascript-kernel/src/widgets/widget_selection.ts` | Partial | Selection semantics still differ from `ipywidgets` in some cases |
+| `packages/controls/src/widget_string.ts` | `packages/javascript-kernel/src/widgets/widget_string.ts` | Ported | String and display widgets; related styles live in `widget_style.ts` |
+| `packages/output/src/output.ts` | `packages/javascript-kernel/src/widgets/widget_output.ts` | Partial | Output capture is supported but not feature-complete |
+| `packages/controls/src/widget_button.ts` | `packages/javascript-kernel/src/widgets/widget_button.ts` | Partial | Button widget is present, but callback behavior differs slightly |
+| `packages/controls/src/widget_color.ts` | `packages/javascript-kernel/src/widgets/widget_color.ts` | Ported | Color picker |
+| `packages/controls/src/widget_box.ts` | `packages/javascript-kernel/src/widgets/widget_box.ts` | Ported | Box, HBox, VBox, GridBox |
+| `packages/controls/src/widget_selectioncontainer.ts` | `packages/javascript-kernel/src/widgets/widget_selectioncontainer.ts` | Ported | Accordion, Tab, Stack |
+| `packages/controls/src/widget_link.ts` | `packages/javascript-kernel/src/widgets/widget_link.ts` | Ported | `jslink`, `jsdlink`, `Link`, and `DirectionalLink` |
+
+> **Note:** `jupyterlab-widgets`, `@jupyter-widgets/controls`, and `@jupyter-widgets/output` must be available in the JupyterLite deployment for the full widget set to render.
+
+See the [example notebook](examples/widgets.ipynb) for more usage examples.
+
### Enable or disable specific modes
The two runtime modes are registered by separate plugins:
diff --git a/examples/widgets.ipynb b/examples/widgets.ipynb
new file mode 100644
index 0000000..6e5fe45
--- /dev/null
+++ b/examples/widgets.ipynb
@@ -0,0 +1,413 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": "# Jupyter Widgets\n\nThe JavaScript kernel provides built-in widget classes that mirror the\n[ipywidgets](https://ipywidgets.readthedocs.io/) API. Widget classes are\navailable under `Jupyter.widgets`.\n\nRun the setup cell below first to destructure the widget classes used in this\nnotebook.\n\nWidgets **auto-display** when they are the last expression in a cell.\nYou can also call the global `display()` function explicitly, e.g. when assigning to a variable.\n\n> **Requirements:** `jupyterlab-widgets`, `@jupyter-widgets/controls`, and\n> `@jupyter-widgets/output` must be available in the JupyterLite deployment."
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "const {\n IntSlider,\n FloatSlider,\n FloatLogSlider,\n IntRangeSlider,\n FloatRangeSlider,\n SelectionSlider,\n SelectionRangeSlider,\n IntProgress,\n Play,\n IntText,\n FloatText,\n Text,\n Textarea,\n Password,\n Checkbox,\n ToggleButton,\n Valid,\n Dropdown,\n RadioButtons,\n Select,\n SelectMultiple,\n ToggleButtons,\n Button,\n ButtonStyle,\n HTML,\n Label,\n Output,\n Layout,\n ColorPicker,\n VBox,\n HBox,\n Tab,\n Accordion,\n Stack,\n jslink\n} = Jupyter.widgets;"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Sliders"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "const slider = new IntSlider({\n value: 50,\n min: 0,\n max: 100,\n step: 1,\n description: 'Int Slider'\n});\ndisplay(slider);\n\nslider.observe(({ new: value }) => {\n console.log('Slider value:', value);\n}, 'value');"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new FloatSlider({\n value: 3.14,\n min: 0.0,\n max: 10.0,\n step: 0.01,\n description: 'Float Slider',\n readout_format: '.2f'\n})"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new SelectionSlider({\n options: ['Freezing', 'Cold', 'Warm', 'Hot', 'Blazing'],\n index: 2,\n description: 'Temp'\n})"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new VBox({\n children: [\n new IntRangeSlider({\n value: [20, 80],\n min: 0,\n max: 100,\n description: 'Range'\n }),\n new FloatLogSlider({\n value: 10,\n min: -2,\n max: 2,\n step: 0.1,\n description: 'Log'\n }),\n new FloatRangeSlider({\n value: [0.5, 2.5],\n min: 0,\n max: 5,\n step: 0.1,\n description: 'Float Range'\n })\n ]\n})"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Progress Bars"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "const progress = new IntProgress({\n value: 0,\n min: 0,\n max: 100,\n description: 'Loading',\n bar_style: 'info'\n});\ndisplay(progress);\n\n// Animate the progress bar\nconst interval = setInterval(() => {\n progress.value += 5;\n if (progress.value >= 100) {\n clearInterval(interval);\n progress.bar_style = 'success';\n progress.description = 'Done';\n }\n}, 200);"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Numeric Inputs"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new IntText({ value: 42, description: 'Integer', step: 1 })"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new FloatText({ value: 3.14, description: 'Float', step: 0.1 })"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Text Inputs"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "const name = new Text({\n value: '',\n description: 'Name',\n placeholder: 'Type your name'\n});\ndisplay(name);\n\nname.on('change:value', (v) => console.log('Name:', v));"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new Textarea({\n value: '',\n description: 'Notes',\n placeholder: 'Enter some text...'\n})"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new Password({\n description: 'Secret',\n placeholder: 'Enter password'\n})"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Boolean Widgets"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "const cb = new Checkbox({\n value: false,\n description: 'Enable feature',\n indent: false\n});\ndisplay(cb);\n\ncb.on('change:value', (v) => console.log('Checked:', v));"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new ToggleButton({\n value: false,\n description: 'Toggle me',\n tooltip: 'Click to toggle',\n button_style: 'info'\n})"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new Valid({ value: true, description: 'Status', readout: 'All checks passed' })"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new Valid({ value: false, description: 'Status', readout: 'Something went wrong' })"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Selection Widgets"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "const dropdown = new Dropdown({\n options: ['Apple', 'Banana', 'Cherry', 'Date'],\n index: 0,\n description: 'Fruit'\n});\ndisplay(dropdown);\n\ndropdown.observe(({ new: value }) => {\n console.log('Selected:', value);\n}, 'value');"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new RadioButtons({\n options: ['Small', 'Medium', 'Large'],\n index: 1,\n description: 'Size'\n})"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new Select({\n options: ['Linux', 'macOS', 'Windows'],\n index: 1,\n description: 'OS',\n rows: 3\n})"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new ToggleButtons({\n options: ['Slow', 'Medium', 'Fast'],\n index: 1,\n description: 'Speed',\n tooltips: ['Turtle pace', 'Normal speed', 'Lightning fast']\n})"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new SelectMultiple({\n options: ['Python', 'Julia', 'R', 'JavaScript'],\n index: [0, 3],\n description: 'Languages',\n rows: 4\n})"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new SelectionRangeSlider({\n options: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],\n index: [1, 3],\n description: 'Week'\n})"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Button\n",
+ "\n",
+ "Buttons fire `'click'` events instead of holding a value:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "let clickCount = 0;\n\nconst btn = new Button({\n description: 'Click me!',\n button_style: 'primary',\n tooltip: 'Go ahead, click it'\n});\ndisplay(btn);\n\nbtn.onClick(() => {\n clickCount++;\n console.log(`Clicked ${clickCount} time(s)`);\n});"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Display Widgets"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new HTML({\n value: '
Hello from an HTML widget!
Rendered by ipywidgets.
'\n})"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new Label({ value: 'This is a Label widget.' })"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Color Picker"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "const picker = new ColorPicker({\n value: '#4ecdc4',\n description: 'Color',\n concise: false\n});\ndisplay(picker);\n\npicker.on('change:value', (v) => console.log('Color:', v));"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Programmatic Updates\n",
+ "\n",
+ "Widget properties can be read and written at any time. Changes are\n",
+ "automatically synced with the frontend:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "// Read the current slider value\n",
+ "console.log('Current slider value:', slider.value);\n",
+ "\n",
+ "// Update it programmatically\n",
+ "slider.value = 75;\n",
+ "slider.description = 'Updated!';"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Linking Widgets\n",
+ "\n",
+ "Use the frontend link helpers when you want widgets to stay in sync without manual wiring:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "const play = new Play({ value: 20, min: 0, max: 100, interval: 150 });\nconst source = new IntSlider({ value: 20, min: 0, max: 100, description: 'Value' });\nconst mirror = new IntProgress({ value: 20, min: 0, max: 100, description: 'Mirror' });\nconst label = new Label({ value: 'Value: 20' });\n\njslink([play, 'value'], [source, 'value']);\njslink([source, 'value'], [mirror, 'value']);\nsource.observe(({ new: value }) => {\n label.value = `Value: ${value}`;\n}, 'value');\n\nnew VBox({\n children: [\n new HBox({ children: [play, source] }),\n mirror,\n label\n ]\n})"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Layout, Style, and Output\n",
+ "\n",
+ "Layout models, style models, and output widgets are regular widgets too."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "new Button({\n description: 'Styled button',\n button_style: 'info',\n layout: new Layout({\n width: '220px',\n justify_content: 'center'\n }),\n style: new ButtonStyle({\n button_color: '#ffd166',\n text_color: '#1f2937'\n })\n})"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": "const out = new Output({\n layout: new Layout({\n border: '1px solid #d1d5db',\n padding: '8px',\n max_height: '140px',\n overflow: 'auto'\n })\n});\ndisplay(out);\n\nconst logToOutput = out.capture({ clearOutput: true })(() => {\n console.log('Captured stdout inside Output.');\n out.appendDisplayData({\n 'text/markdown': '**Direct append** from JavaScript',\n 'text/plain': 'Direct append from JavaScript'\n });\n});\nlogToOutput();"
+ },
+ {
+ "cell_type": "markdown",
+ "id": "efiw9mng9a6",
+ "source": "## Container Widgets\n\nContainer widgets group other widgets together and control their layout.\nThey accept a `children` array of widget instances.",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "id": "jdf53ak5lo",
+ "source": "// VBox arranges children vertically\nnew VBox({\n children: [\n new IntSlider({ value: 30, description: 'Slider A' }),\n new IntSlider({ value: 60, description: 'Slider B' }),\n new IntSlider({ value: 90, description: 'Slider C' })\n ]\n})",
+ "metadata": {},
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "id": "8889x2ewlx9",
+ "source": "// HBox arranges children horizontally\nnew HBox({\n children: [\n new Button({ description: 'Left', button_style: 'info' }),\n new Button({ description: 'Center', button_style: 'warning' }),\n new Button({ description: 'Right', button_style: 'danger' })\n ]\n})",
+ "metadata": {},
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "id": "g5mlio5w7jg",
+ "source": "// Tab shows one child at a time with tab headers\nnew Tab({\n children: [\n new IntSlider({ value: 50, description: 'Slider' }),\n new Text({ value: 'Hello', description: 'Name' }),\n new ColorPicker({ value: '#e74c3c', description: 'Color' })\n ],\n titles: ['Slider', 'Text', 'Color']\n})",
+ "metadata": {},
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "id": "pf5mvpnv12",
+ "source": "// Accordion shows one child at a time with collapsible sections\nnew Accordion({\n children: [\n new VBox({\n children: [\n new IntSlider({ description: 'X' }),\n new IntSlider({ description: 'Y' })\n ]\n }),\n new HBox({\n children: [\n new ToggleButton({ description: 'A', button_style: 'success' }),\n new ToggleButton({ description: 'B', button_style: 'info' })\n ]\n })\n ],\n titles: ['Sliders', 'Toggles']\n})",
+ "metadata": {},
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "id": "ug1fid6455",
+ "source": "// Stack displays only the selected child (controlled programmatically)\nconst stack = new Stack({\n children: [\n new HTML({ value: 'Page 1
First page content
' }),\n new HTML({ value: 'Page 2
Second page content
' }),\n new HTML({ value: 'Page 3
Third page content
' })\n ],\n titles: ['Page 1', 'Page 2', 'Page 3'],\n selected_index: 0\n});\ndisplay(stack);\n\n// Switch pages with a dropdown\nconst pagePicker = new Dropdown({\n options: ['Page 1', 'Page 2', 'Page 3'],\n description: 'Show page'\n});\ndisplay(pagePicker);\n\npagePicker.on('change:index', () => {\n stack.selected_index = pagePicker.index;\n});",
+ "metadata": {},
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "---\n",
+ "\n",
+ "## Low-level Comm API\n",
+ "\n",
+ "The widget classes above are built on `Jupyter.comm`, the raw comm protocol\n",
+ "API. You can use it directly for custom frontend↔kernel communication.\n",
+ "\n",
+ "> Custom target names that have no frontend handler will show\n",
+ "> `\"Exception opening new comm\"` in the browser console — this is expected."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "// Open a raw comm channel\n",
+ "const comm = Jupyter.comm.open('my.custom.target', { greeting: 'hello' });\n",
+ "console.log('Comm ID:', comm.commId);\n",
+ "\n",
+ "// Send / receive / close\n",
+ "comm.send({ method: 'update', state: { value: 42 } });\n",
+ "comm.onMsg = (data) => console.log('Received:', data);\n",
+ "comm.close();"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "// Register a target for frontend-initiated comms\n",
+ "Jupyter.comm.registerTarget('my.echo', (comm, data) => {\n",
+ " comm.onMsg = (msg) => comm.send({ echo: msg });\n",
+ "});"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "JavaScript",
+ "language": "javascript",
+ "name": "javascript"
+ },
+ "language_info": {
+ "name": "javascript"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/packages/javascript-kernel/src/comm/index.ts b/packages/javascript-kernel/src/comm/index.ts
new file mode 100644
index 0000000..b176b8c
--- /dev/null
+++ b/packages/javascript-kernel/src/comm/index.ts
@@ -0,0 +1,4 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+export * from './manager';
diff --git a/packages/javascript-kernel/src/comm/manager.ts b/packages/javascript-kernel/src/comm/manager.ts
new file mode 100644
index 0000000..151067b
--- /dev/null
+++ b/packages/javascript-kernel/src/comm/manager.ts
@@ -0,0 +1,245 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import type { RuntimeOutputHandler } from '../runtime_protocol';
+
+/**
+ * Represents an open comm channel.
+ */
+export interface IComm {
+ readonly commId: string;
+ readonly targetName: string;
+ send(
+ data: Record,
+ metadata?: Record,
+ buffers?: ArrayBuffer[]
+ ): void;
+ close(data?: Record): void;
+ display(): void;
+ onMsg:
+ | ((data: Record, buffers?: ArrayBuffer[]) => void)
+ | null;
+ onClose:
+ | ((data: Record, buffers?: ArrayBuffer[]) => void)
+ | null;
+}
+
+/**
+ * Handler invoked when the frontend opens a comm targeting a registered name.
+ */
+export type CommTargetHandler = (
+ comm: IComm,
+ data: Record,
+ buffers?: ArrayBuffer[]
+) => void;
+
+/**
+ * Manages comm lifecycle within the runtime.
+ */
+export class CommManager {
+ constructor(onOutput: RuntimeOutputHandler) {
+ this._onOutput = onOutput;
+ }
+
+ /**
+ * Open a new comm channel.
+ */
+ open(
+ targetName: string,
+ data: Record = {},
+ metadata: Record = {},
+ buffers?: ArrayBuffer[],
+ commId?: string
+ ): IComm {
+ const id = commId ?? crypto.randomUUID();
+ const comm = this._createComm(id, targetName);
+ this._comms.set(id, comm);
+
+ this._onOutput({
+ type: 'comm_open',
+ content: {
+ comm_id: id,
+ target_name: targetName,
+ data
+ },
+ metadata,
+ buffers
+ });
+
+ return comm;
+ }
+
+ /**
+ * Register a handler for frontend-initiated comms targeting the given name.
+ */
+ registerTarget(targetName: string, handler: CommTargetHandler): void {
+ this._targets.set(targetName, handler);
+ }
+
+ /**
+ * Register a widget instance by comm ID.
+ */
+ registerWidget(commId: string, widget: T): void {
+ this._widgets.set(commId, widget);
+ }
+
+ /**
+ * Look up a widget instance by comm ID.
+ */
+ getWidget(commId: string): T | undefined {
+ return this._widgets.get(commId) as T | undefined;
+ }
+
+ /**
+ * Remove a widget registration.
+ */
+ unregisterWidget(commId: string): void {
+ this._widgets.delete(commId);
+ }
+
+ /**
+ * Track the active parent message ID for output capture helpers.
+ */
+ setCurrentMessageId(messageId: string | null): void {
+ this._currentMessageId = messageId;
+ }
+
+ /**
+ * The active parent message ID, if any.
+ */
+ getCurrentMessageId(): string | null {
+ return this._currentMessageId;
+ }
+
+ /**
+ * Handle a comm_open message from the frontend.
+ */
+ handleCommOpen(
+ commId: string,
+ targetName: string,
+ data: Record,
+ buffers?: ArrayBuffer[]
+ ): void {
+ const handler = this._targets.get(targetName);
+ if (!handler) {
+ console.warn(
+ `[javascript-kernel] No handler registered for comm target "${targetName}"`
+ );
+ return;
+ }
+ const comm = this._createComm(commId, targetName);
+ this._comms.set(commId, comm);
+ handler(comm, data, buffers);
+ }
+
+ /**
+ * Handle a comm_msg message from the frontend.
+ */
+ handleCommMsg(
+ commId: string,
+ data: Record,
+ buffers?: ArrayBuffer[]
+ ): void {
+ const comm = this._comms.get(commId);
+ comm?.onMsg?.(data, buffers);
+ }
+
+ /**
+ * Handle a comm_close message from the frontend.
+ */
+ handleCommClose(
+ commId: string,
+ data: Record,
+ buffers?: ArrayBuffer[]
+ ): void {
+ const comm = this._comms.get(commId);
+ if (comm) {
+ comm.onClose?.(data, buffers);
+ this._widgets.delete(commId);
+ this._comms.delete(commId);
+ }
+ }
+
+ /**
+ * Display a widget identified by its comm ID.
+ */
+ displayWidget(commId: string): void {
+ this._onOutput({
+ type: 'display_data',
+ bundle: {
+ data: {
+ 'text/plain': 'Widget',
+ 'application/vnd.jupyter.widget-view+json': {
+ version_major: 2,
+ version_minor: 0,
+ model_id: commId
+ }
+ },
+ metadata: {},
+ transient: {}
+ }
+ });
+ }
+
+ /**
+ * Dispose all comms and clear state.
+ */
+ dispose(): void {
+ this._widgets.clear();
+ this._comms.clear();
+ this._targets.clear();
+ }
+
+ /**
+ * Create an IComm instance bound to this manager.
+ */
+ private _createComm(commId: string, targetName: string): IComm {
+ const comm: IComm = {
+ get commId() {
+ return commId;
+ },
+ get targetName() {
+ return targetName;
+ },
+ send: (
+ data: Record,
+ metadata?: Record,
+ buffers?: ArrayBuffer[]
+ ): void => {
+ this._onOutput({
+ type: 'comm_msg',
+ content: {
+ comm_id: commId,
+ data
+ },
+ metadata,
+ buffers
+ });
+ },
+ close: (data: Record = {}): void => {
+ this._onOutput({
+ type: 'comm_close',
+ content: {
+ comm_id: commId,
+ data
+ }
+ });
+ comm.onClose?.(data);
+ this._widgets.delete(commId);
+ this._comms.delete(commId);
+ },
+ display: (): void => {
+ this.displayWidget(commId);
+ },
+ onMsg: null,
+ onClose: null
+ };
+ return comm;
+ }
+
+ private _onOutput: RuntimeOutputHandler;
+ private _comms = new Map();
+ private _targets = new Map();
+ private _widgets = new Map();
+ private _currentMessageId: string | null = null;
+}
diff --git a/packages/javascript-kernel/src/index.ts b/packages/javascript-kernel/src/index.ts
index c421980..5ee10b4 100644
--- a/packages/javascript-kernel/src/index.ts
+++ b/packages/javascript-kernel/src/index.ts
@@ -7,3 +7,5 @@ export * from './display';
export * from './runtime_protocol';
export * from './runtime_backends';
export * from './runtime_evaluator';
+export * from './comm';
+export * from './widgets';
diff --git a/packages/javascript-kernel/src/kernel.ts b/packages/javascript-kernel/src/kernel.ts
index 543e0cf..e7e0b33 100644
--- a/packages/javascript-kernel/src/kernel.ts
+++ b/packages/javascript-kernel/src/kernel.ts
@@ -38,6 +38,7 @@ export class JavaScriptKernel extends BaseKernel implements IKernel {
return;
}
+ this._comms.clear();
this._backend.dispose();
super.dispose();
}
@@ -99,7 +100,11 @@ export class JavaScriptKernel extends BaseKernel implements IKernel {
): Promise {
try {
await this.ready;
- return await this._backend.execute(content.code, this.executionCount);
+ return await this._backend.execute(
+ content.code,
+ this.executionCount,
+ this.parentHeader?.msg_id
+ );
} catch (error) {
const normalized = this.normalizeError(error);
const traceback = [
@@ -187,9 +192,15 @@ export class JavaScriptKernel extends BaseKernel implements IKernel {
async commInfoRequest(
content: KernelMessage.ICommInfoRequestMsg['content']
): Promise {
+ const comms: Record = {};
+ for (const [commId, info] of this._comms) {
+ if (!content.target_name || info.target_name === content.target_name) {
+ comms[commId] = { target_name: info.target_name };
+ }
+ }
return {
status: 'ok',
- comms: {}
+ comms
};
}
@@ -204,21 +215,45 @@ export class JavaScriptKernel extends BaseKernel implements IKernel {
* Send an `comm_open` message.
*/
async commOpen(msg: KernelMessage.ICommOpenMsg): Promise {
- this._logUnsupportedControlMessage('comm_open', msg.content.target_name);
+ const { comm_id, target_name, data } = msg.content;
+ this._comms.set(comm_id, { target_name });
+ const buffers = this._toArrayBuffers(msg.buffers);
+ await this._backend.handleCommOpen(
+ comm_id,
+ target_name,
+ data as Record,
+ buffers,
+ msg.header.msg_id
+ );
}
/**
* Send an `comm_msg` message.
*/
async commMsg(msg: KernelMessage.ICommMsgMsg): Promise {
- this._logUnsupportedControlMessage('comm_msg');
+ const { comm_id, data } = msg.content;
+ const buffers = this._toArrayBuffers(msg.buffers);
+ await this._backend.handleCommMsg(
+ comm_id,
+ data as Record,
+ buffers,
+ msg.header.msg_id
+ );
}
/**
* Send an `comm_close` message.
*/
async commClose(msg: KernelMessage.ICommCloseMsg): Promise {
- this._logUnsupportedControlMessage('comm_close');
+ const { comm_id, data } = msg.content;
+ this._comms.delete(comm_id);
+ const buffers = this._toArrayBuffers(msg.buffers);
+ await this._backend.handleCommClose(
+ comm_id,
+ data as Record,
+ buffers,
+ msg.header.msg_id
+ );
}
/**
@@ -306,6 +341,37 @@ export class JavaScriptKernel extends BaseKernel implements IKernel {
case 'execute_error':
this.publishExecuteError(message.bundle, parentHeader);
break;
+ case 'comm_open':
+ this._comms.set(message.content.comm_id, {
+ target_name: message.content.target_name
+ });
+ this.handleComm(
+ 'comm_open',
+ message.content as any,
+ (message.metadata ?? {}) as any,
+ message.buffers as any,
+ parentHeader
+ );
+ break;
+ case 'comm_msg':
+ this.handleComm(
+ 'comm_msg',
+ message.content as any,
+ (message.metadata ?? {}) as any,
+ message.buffers as any,
+ parentHeader
+ );
+ break;
+ case 'comm_close':
+ this._comms.delete(message.content.comm_id);
+ this.handleComm(
+ 'comm_close',
+ message.content as any,
+ (message.metadata ?? {}) as any,
+ message.buffers as any,
+ parentHeader
+ );
+ break;
default:
break;
}
@@ -346,11 +412,31 @@ export class JavaScriptKernel extends BaseKernel implements IKernel {
return error;
}
+ /**
+ * Convert incoming message buffers to ArrayBuffer for Comlink transport.
+ */
+ private _toArrayBuffers(
+ buffers?: (ArrayBuffer | ArrayBufferView)[]
+ ): ArrayBuffer[] | undefined {
+ if (!buffers || buffers.length === 0) {
+ return undefined;
+ }
+ return buffers.map(buf => {
+ if (ArrayBuffer.isView(buf)) {
+ return buf.buffer.slice(
+ buf.byteOffset,
+ buf.byteOffset + buf.byteLength
+ );
+ }
+ return buf;
+ });
+ }
+
/**
* Warn once per unsupported control message type to avoid noisy consoles.
*/
private _logUnsupportedControlMessage(
- type: 'input_reply' | 'comm_open' | 'comm_msg' | 'comm_close',
+ type: 'input_reply',
detail?: string
): void {
if (this._unsupportedControlMessages.has(type)) {
@@ -365,9 +451,8 @@ export class JavaScriptKernel extends BaseKernel implements IKernel {
);
}
- private _unsupportedControlMessages = new Set<
- 'input_reply' | 'comm_open' | 'comm_msg' | 'comm_close'
- >();
+ private _unsupportedControlMessages = new Set<'input_reply'>();
+ private _comms = new Map();
private _backend: IRuntimeBackend;
private _executorFactory?: JavaScriptKernel.IExecutorFactory;
private _runtimeMode: RuntimeMode;
diff --git a/packages/javascript-kernel/src/runtime_backends.ts b/packages/javascript-kernel/src/runtime_backends.ts
index 1116010..44d856e 100644
--- a/packages/javascript-kernel/src/runtime_backends.ts
+++ b/packages/javascript-kernel/src/runtime_backends.ts
@@ -34,7 +34,8 @@ export interface IRuntimeBackend {
dispose(): void;
execute(
code: string,
- executionCount: number
+ executionCount: number,
+ parentMessageId?: string
): Promise;
complete(
code: string,
@@ -48,6 +49,25 @@ export interface IRuntimeBackend {
isComplete(
code: string
): Promise;
+ handleCommOpen(
+ commId: string,
+ targetName: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): Promise;
+ handleCommMsg(
+ commId: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): Promise;
+ handleCommClose(
+ commId: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): Promise;
}
/**
@@ -71,10 +91,11 @@ abstract class AbstractRuntimeBackend implements IRuntimeBackend {
*/
async execute(
code: string,
- executionCount: number
+ executionCount: number,
+ parentMessageId?: string
): Promise {
await this.ready;
- return this._getRemote().execute(code, executionCount);
+ return this._getRemote().execute(code, executionCount, parentMessageId);
}
/**
@@ -110,6 +131,63 @@ abstract class AbstractRuntimeBackend implements IRuntimeBackend {
return this._getRemote().isComplete(code);
}
+ /**
+ * Forward comm_open to the remote runtime.
+ */
+ async handleCommOpen(
+ commId: string,
+ targetName: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): Promise {
+ await this.ready;
+ if (this._remote) {
+ await this._remote.handleCommOpen(
+ commId,
+ targetName,
+ data,
+ buffers,
+ parentMessageId
+ );
+ }
+ }
+
+ /**
+ * Forward comm_msg to the remote runtime.
+ */
+ async handleCommMsg(
+ commId: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): Promise {
+ await this.ready;
+ if (this._remote) {
+ await this._remote.handleCommMsg(commId, data, buffers, parentMessageId);
+ }
+ }
+
+ /**
+ * Forward comm_close to the remote runtime.
+ */
+ async handleCommClose(
+ commId: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): Promise {
+ await this.ready;
+ if (this._remote) {
+ await this._remote.handleCommClose(
+ commId,
+ data,
+ buffers,
+ parentMessageId
+ );
+ }
+ }
+
/**
* Return remote runtime API or throw when not initialized.
*/
diff --git a/packages/javascript-kernel/src/runtime_evaluator.ts b/packages/javascript-kernel/src/runtime_evaluator.ts
index d77c917..e332474 100644
--- a/packages/javascript-kernel/src/runtime_evaluator.ts
+++ b/packages/javascript-kernel/src/runtime_evaluator.ts
@@ -5,7 +5,9 @@ import type { KernelMessage } from '@jupyterlab/services';
import { JavaScriptExecutor } from './executor';
import { normalizeError } from './errors';
+import { CommManager } from './comm';
import type { RuntimeOutputHandler } from './runtime_protocol';
+import { Widget, createWidgetClasses } from './widgets';
/**
* Shared execution logic for iframe and worker runtime backends.
@@ -20,6 +22,9 @@ export class JavaScriptRuntimeEvaluator {
this._executor =
options.executor ?? new JavaScriptExecutor(options.globalScope);
+ this._commManager = new CommManager(options.onOutput);
+ this._setupWidgets();
+ this._setupJupyterGlobal();
this._setupDisplay();
this._setupConsoleOverrides();
}
@@ -30,6 +35,9 @@ export class JavaScriptRuntimeEvaluator {
dispose(): void {
this._restoreConsoleOverrides();
this._restoreDisplay();
+ this._restoreWidgets();
+ this._restoreJupyterGlobal();
+ this._commManager.dispose();
}
/**
@@ -51,51 +59,58 @@ export class JavaScriptRuntimeEvaluator {
*/
async execute(
code: string,
- executionCount: number
+ executionCount: number,
+ parentMessageId?: string
): Promise {
- // Parse-time errors are syntax errors, so show only `Name: message`.
- let asyncFunction: () => Promise;
- let withReturn: boolean;
- try {
- const parsed = this._executor.makeAsyncFromCode(code);
- asyncFunction = parsed.asyncFunction;
- withReturn = parsed.withReturn;
- } catch (error) {
- const normalized = normalizeError(error);
- return this._emitError(executionCount, normalized, false);
- }
+ return this._withParentMessageId(parentMessageId, async () => {
+ // Parse-time errors are syntax errors, so show only `Name: message`.
+ let asyncFunction: () => Promise;
+ let withReturn: boolean;
+ try {
+ const parsed = this._executor.makeAsyncFromCode(code);
+ asyncFunction = parsed.asyncFunction;
+ withReturn = parsed.withReturn;
+ } catch (error) {
+ const normalized = normalizeError(error);
+ return this._emitError(executionCount, normalized, false);
+ }
- // Runtime errors may include useful eval frames from user code.
- try {
- const resultPromise = this._evalFunc(asyncFunction);
-
- if (withReturn) {
- const result = await resultPromise;
-
- if (result !== undefined) {
- const data = this._executor.getMimeBundle(result);
- this._onOutput({
- type: 'execute_result',
- bundle: {
- execution_count: executionCount,
- data,
- metadata: {}
+ // Runtime errors may include useful eval frames from user code.
+ try {
+ const resultPromise = this._evalFunc(asyncFunction);
+
+ if (withReturn) {
+ const result = await resultPromise;
+
+ if (result !== undefined) {
+ if (result instanceof Widget) {
+ this._commManager.displayWidget(result.commId);
+ } else {
+ const data = this._executor.getMimeBundle(result);
+ this._onOutput({
+ type: 'execute_result',
+ bundle: {
+ execution_count: executionCount,
+ data,
+ metadata: {}
+ }
+ });
}
- });
+ }
+ } else {
+ await resultPromise;
}
- } else {
- await resultPromise;
- }
- return {
- status: 'ok',
- execution_count: executionCount,
- user_expressions: {}
- };
- } catch (error) {
- const normalized = normalizeError(error);
- return this._emitError(executionCount, normalized, true);
- }
+ return {
+ status: 'ok',
+ execution_count: executionCount,
+ user_expressions: {}
+ };
+ } catch (error) {
+ const normalized = normalizeError(error);
+ return this._emitError(executionCount, normalized, true);
+ }
+ });
}
/**
@@ -134,6 +149,56 @@ export class JavaScriptRuntimeEvaluator {
return this._executor.isComplete(code);
}
+ /**
+ * The comm manager instance.
+ */
+ get commManager(): CommManager {
+ return this._commManager;
+ }
+
+ /**
+ * Handle a comm_open message from the frontend.
+ */
+ handleCommOpen(
+ commId: string,
+ targetName: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): void {
+ void this._withParentMessageId(parentMessageId, async () => {
+ this._commManager.handleCommOpen(commId, targetName, data, buffers);
+ });
+ }
+
+ /**
+ * Handle a comm_msg message from the frontend.
+ */
+ handleCommMsg(
+ commId: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): void {
+ void this._withParentMessageId(parentMessageId, async () => {
+ this._commManager.handleCommMsg(commId, data, buffers);
+ });
+ }
+
+ /**
+ * Handle a comm_close message from the frontend.
+ */
+ handleCommClose(
+ commId: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): void {
+ void this._withParentMessageId(parentMessageId, async () => {
+ this._commManager.handleCommClose(commId, data, buffers);
+ });
+ }
+
/**
* Evaluate an async function within the configured global scope.
*/
@@ -141,6 +206,22 @@ export class JavaScriptRuntimeEvaluator {
return asyncFunc.call(this._globalScope);
}
+ /**
+ * Set the active parent message ID while invoking runtime code.
+ */
+ private async _withParentMessageId(
+ parentMessageId: string | undefined,
+ callback: () => Promise | T
+ ): Promise {
+ const previousMessageId = this._commManager.getCurrentMessageId();
+ this._commManager.setCurrentMessageId(parentMessageId ?? null);
+ try {
+ return await callback();
+ } finally {
+ this._commManager.setCurrentMessageId(previousMessageId);
+ }
+ }
+
/**
* Build and emit an execute error reply.
*/
@@ -283,6 +364,11 @@ export class JavaScriptRuntimeEvaluator {
this._previousDisplay = this._globalScope.display;
this._globalScope.display = (obj: any, metadata?: Record) => {
+ if (obj instanceof Widget) {
+ this._commManager.displayWidget(obj.commId);
+ return;
+ }
+
const data = this._executor.getMimeBundle(obj);
this._onOutput({
@@ -308,9 +394,54 @@ export class JavaScriptRuntimeEvaluator {
this._globalScope.display = this._previousDisplay;
}
+ /**
+ * Create runtime-local widget classes.
+ */
+ private _setupWidgets(): void {
+ this._widgetClasses = createWidgetClasses(this._commManager);
+ }
+
+ /**
+ * Clear runtime-local widget classes.
+ */
+ private _restoreWidgets(): void {
+ this._widgetClasses = null;
+ }
+
+ /**
+ * Install Jupyter.comm in runtime scope.
+ */
+ private _setupJupyterGlobal(): void {
+ this._previousJupyter = this._globalScope.Jupyter;
+ const previousJupyter =
+ typeof this._previousJupyter === 'object' &&
+ this._previousJupyter !== null
+ ? this._previousJupyter
+ : {};
+ this._globalScope.Jupyter = {
+ ...previousJupyter,
+ comm: this._commManager,
+ widgets: this._widgetClasses ?? {}
+ };
+ }
+
+ /**
+ * Restore previous Jupyter binding.
+ */
+ private _restoreJupyterGlobal(): void {
+ if (this._previousJupyter === undefined) {
+ delete this._globalScope.Jupyter;
+ return;
+ }
+ this._globalScope.Jupyter = this._previousJupyter;
+ }
+
private _globalScope: Record;
private _onOutput: RuntimeOutputHandler;
private _executor: JavaScriptExecutor;
+ private _commManager: CommManager;
+ private _widgetClasses: Record | null = null;
+ private _previousJupyter: any;
private _previousDisplay: any;
private _originalOnError: any;
private _originalConsole: {
diff --git a/packages/javascript-kernel/src/runtime_protocol.ts b/packages/javascript-kernel/src/runtime_protocol.ts
index b5f1fee..37db748 100644
--- a/packages/javascript-kernel/src/runtime_protocol.ts
+++ b/packages/javascript-kernel/src/runtime_protocol.ts
@@ -40,6 +40,35 @@ export type RuntimeOutputMessage =
| {
type: 'execute_error';
bundle: KernelMessage.IReplyErrorContent;
+ }
+ | {
+ type: 'comm_open';
+ content: {
+ comm_id: string;
+ target_name: string;
+ data: Record;
+ target_module?: string;
+ };
+ metadata?: Record;
+ buffers?: ArrayBuffer[];
+ }
+ | {
+ type: 'comm_msg';
+ content: {
+ comm_id: string;
+ data: Record;
+ };
+ metadata?: Record;
+ buffers?: ArrayBuffer[];
+ }
+ | {
+ type: 'comm_close';
+ content: {
+ comm_id: string;
+ data: Record;
+ };
+ metadata?: Record;
+ buffers?: ArrayBuffer[];
};
/**
@@ -64,7 +93,8 @@ export interface IRemoteRuntimeApi {
): Promise;
execute(
code: string,
- executionCount: number
+ executionCount: number,
+ parentMessageId?: string
): Promise;
complete(
code: string,
@@ -78,5 +108,24 @@ export interface IRemoteRuntimeApi {
isComplete(
code: string
): Promise;
+ handleCommOpen(
+ commId: string,
+ targetName: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): Promise;
+ handleCommMsg(
+ commId: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): Promise;
+ handleCommClose(
+ commId: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): Promise;
dispose(): Promise;
}
diff --git a/packages/javascript-kernel/src/runtime_remote.ts b/packages/javascript-kernel/src/runtime_remote.ts
index 68aafc1..5c85789 100644
--- a/packages/javascript-kernel/src/runtime_remote.ts
+++ b/packages/javascript-kernel/src/runtime_remote.ts
@@ -51,8 +51,12 @@ export function createRemoteRuntimeApi(
});
},
- async execute(code: string, executionCount: number) {
- return ensureEvaluator().execute(code, executionCount);
+ async execute(
+ code: string,
+ executionCount: number,
+ parentMessageId?: string
+ ) {
+ return ensureEvaluator().execute(code, executionCount, parentMessageId);
},
async complete(code: string, cursorPos: number) {
@@ -71,6 +75,40 @@ export function createRemoteRuntimeApi(
return ensureEvaluator().isComplete(code);
},
+ async handleCommOpen(
+ commId: string,
+ targetName: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): Promise {
+ ensureEvaluator().handleCommOpen(
+ commId,
+ targetName,
+ data,
+ buffers,
+ parentMessageId
+ );
+ },
+
+ async handleCommMsg(
+ commId: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): Promise {
+ ensureEvaluator().handleCommMsg(commId, data, buffers, parentMessageId);
+ },
+
+ async handleCommClose(
+ commId: string,
+ data: Record,
+ buffers?: ArrayBuffer[],
+ parentMessageId?: string
+ ): Promise {
+ ensureEvaluator().handleCommClose(commId, data, buffers, parentMessageId);
+ },
+
async dispose(): Promise {
evaluator?.dispose();
evaluator = null;
@@ -120,6 +158,10 @@ function sanitize(value: any, seen: WeakSet