Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,13 @@ jobs:
run: |
python -m pip install --pre jupyterlite-core jupyterlite-pyodide-kernel jupyterlab_hybrid_kernels*.whl
- name: Build the JupyterLite site
working-directory: docs
run: |
jupyter lite build --output-dir dist
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./dist
path: ./docs/dist

deploy_lite:
needs: build_lite
Expand Down
4 changes: 2 additions & 2 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ build:
commands:
- mamba env update --name base --file docs/environment.yml
- python -m pip install .
- jupyter lite build --output-dir dist
- cd docs && jupyter lite build --output-dir dist
- mkdir -p $READTHEDOCS_OUTPUT/html
- cp -r dist/* $READTHEDOCS_OUTPUT/html/
- cp -r docs/dist/* $READTHEDOCS_OUTPUT/html/
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,46 @@ This extension lets you use in-browser kernels (like Pyodide) and regular Jupyte
> [!NOTE]
> While regular Jupyter kernels can be used across tabs and persist after reloading the page, in-browser kernels are only available on the page or browser tab where they were started, and destroyed on page reload.

### Operating Modes

This extension supports two operating modes, configured via the `hybridKernelsMode` PageConfig option:

#### Hybrid Mode (default)

In hybrid mode (`hybridKernelsMode: 'hybrid'`), the extension shows:

- Kernels from the local Jupyter server (e.g., Python, R)
- In-browser lite kernels (e.g., Pyodide)

This is the default mode when running JupyterLab with a local Jupyter server. No additional configuration is needed.

#### Remote Mode

In remote mode (`hybridKernelsMode: 'remote'`), the extension shows:

- In-browser lite kernels
- Optionally, kernels from a remote Jupyter server (configured via the "Configure Remote Jupyter Server" command)

This mode is designed for JupyterLite or similar environments where there's no local Jupyter server. To enable remote mode, set the PageConfig option:

```html
<script id="jupyter-config-data" type="application/json">
{
"hybridKernelsMode": "remote"
}
</script>
```

Or in `jupyter-lite.json`:

```json
{
"jupyter-config-data": {
"hybridKernelsMode": "remote"
}
}
```

### File system access from in-browser kernels

In-browser kernels like Pyodide (via `jupyterlite-pyodide-kernel`) can access the files shown in the JupyterLab file browser.
Expand Down
12 changes: 12 additions & 0 deletions docs/jupyter-lite.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"jupyter-lite-schema-version": 0,
"jupyter-config-data": {
"appName": "Hybrid Kernels Demo",
"hybridKernelsMode": "remote",
"disabledExtensions": [
"@jupyterlite/services-extension:kernel-manager",
"@jupyterlite/services-extension:kernel-spec-manager",
"@jupyterlite/services-extension:session-manager"
]
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@jupyterlab/coreutils": "^6.4.2",
"@jupyterlab/services": "^7.4.2",
"@jupyterlab/settingregistry": "^4.4.2",
"@jupyterlab/translation": "^4.4.2",
"@jupyterlite/services": "^0.7.0",
"@lumino/signaling": "^2.1.5",
"mock-socket": "^9.3.1"
Expand Down Expand Up @@ -206,7 +207,7 @@
"rules": {
"csstree/validator": true,
"property-no-vendor-prefix": null,
"selector-class-pattern": "^()(-[A-z\\d]+)*$",
"selector-class-pattern": "^(jp-[A-Z][A-Za-z\\d]*(-[A-Za-z\\d]+)*)$",
"selector-no-vendor-prefix": null,
"value-no-vendor-prefix": null
}
Expand Down
15 changes: 15 additions & 0 deletions schema/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"jupyter.lab.shortcuts": [],
"jupyter.lab.toolbars": {
"TopBar": [
{
"name": "remote-server-status",
"rank": 40
}
]
},
"title": "Hybrid Kernels",
"description": "Settings for hybrid kernels extension",
"type": "object",
"properties": {}
}
8 changes: 0 additions & 8 deletions schema/plugin.json

This file was deleted.

165 changes: 165 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { ServerConnection } from '@jupyterlab/services';
import { PageConfig } from '@jupyterlab/coreutils';
import { Signal } from '@lumino/signaling';
import type { ISignal } from '@lumino/signaling';

import type { IRemoteServerConfig, HybridKernelsMode } from './tokens';

/**
* PageConfig keys for hybrid kernels configuration
*/
const PAGE_CONFIG_BASE_URL_KEY = 'hybridKernelsBaseUrl';
const PAGE_CONFIG_TOKEN_KEY = 'hybridKernelsToken';
const PAGE_CONFIG_MODE_KEY = 'hybridKernelsMode';

/**
* Get the current hybrid kernels mode from PageConfig.
* Defaults to 'hybrid' if not configured.
*/
export function getHybridKernelsMode(): HybridKernelsMode {
const mode = PageConfig.getOption(PAGE_CONFIG_MODE_KEY);
if (mode === 'remote') {
return 'remote';
}
return 'hybrid';
}

/**
* Implementation of remote server configuration.
* Always reads from and writes to PageConfig, acting as a proxy.
*/
export class RemoteServerConfig implements IRemoteServerConfig {
/**
* Get the base URL from PageConfig
*/
get baseUrl(): string {
return PageConfig.getOption(PAGE_CONFIG_BASE_URL_KEY);
}

/**
* Get the token from PageConfig
*/
get token(): string {
return PageConfig.getOption(PAGE_CONFIG_TOKEN_KEY);
}

/**
* Whether we are currently connected to the remote server
*/
get isConnected(): boolean {
return this._isConnected;
}

/**
* A signal emitted when the configuration changes.
*/
get changed(): ISignal<this, void> {
return this._changed;
}

/**
* Set the connection state
*/
setConnected(connected: boolean): void {
if (this._isConnected !== connected) {
this._isConnected = connected;
this._changed.emit();
}
}

/**
* Update the configuration by writing to PageConfig.
* The new values will be immediately available via the getters.
*/
update(config: { baseUrl?: string; token?: string }): void {
let hasChanged = false;
const currentBaseUrl = this.baseUrl;
const currentToken = this.token;

if (config.baseUrl !== undefined && config.baseUrl !== currentBaseUrl) {
PageConfig.setOption(PAGE_CONFIG_BASE_URL_KEY, config.baseUrl);
hasChanged = true;
}

if (config.token !== undefined && config.token !== currentToken) {
PageConfig.setOption(PAGE_CONFIG_TOKEN_KEY, config.token);
hasChanged = true;
}

if (hasChanged) {
this._changed.emit();
}
}

private _changed = new Signal<this, void>(this);
private _isConnected = false;
}

/**
* Create dynamic server settings that read from PageConfig on every access.
* This ensures that when the user updates the configuration via the dialog,
* subsequent API calls will use the new values without needing to recreate managers.
*
* The returned object implements ServerConnection.ISettings with dynamic getters
* for baseUrl, wsUrl, and token that always read the current values from PageConfig.
*/
export function createServerSettings(): ServerConnection.ISettings {
const defaultSettings = ServerConnection.makeSettings();

const dynamicSettings: ServerConnection.ISettings = {
get baseUrl(): string {
const baseUrl = PageConfig.getOption(PAGE_CONFIG_BASE_URL_KEY);
if (!baseUrl) {
return defaultSettings.baseUrl;
}
return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
},

get appUrl(): string {
return defaultSettings.appUrl;
},

get wsUrl(): string {
const baseUrl = PageConfig.getOption(PAGE_CONFIG_BASE_URL_KEY);
if (!baseUrl) {
return defaultSettings.wsUrl;
}
const wsUrl = baseUrl.replace(/^http/, 'ws');
return wsUrl.endsWith('/') ? wsUrl : `${wsUrl}/`;
},

get token(): string {
return PageConfig.getOption(PAGE_CONFIG_TOKEN_KEY);
},

get init(): RequestInit {
return defaultSettings.init;
},

get Headers(): typeof Headers {
return defaultSettings.Headers;
},

get Request(): typeof Request {
return defaultSettings.Request;
},

get fetch(): ServerConnection.ISettings['fetch'] {
return defaultSettings.fetch;
},

get WebSocket(): typeof WebSocket {
return defaultSettings.WebSocket;
},

get appendToken(): boolean {
return true;
},

get serializer(): ServerConnection.ISettings['serializer'] {
return defaultSettings.serializer;
}
};

return dynamicSettings;
}
Loading
Loading