marimo-jupyter-extension 0.1.0__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/PKG-INFO +13 -5
  2. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/README.md +12 -4
  3. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/docs/index.md +9 -1
  4. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/docs/installation.md +4 -4
  5. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/docs/jupyterhub.md +1 -1
  6. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/docs/troubleshooting.md +2 -2
  7. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/src/icons.ts +9 -0
  8. marimo_jupyter_extension-0.1.1/labextension/src/iframe-widget.ts +377 -0
  9. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/src/index.ts +186 -54
  10. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/src/widget-factory.ts +11 -13
  11. marimo_jupyter_extension-0.1.1/labextension/style/marimo-file-icon.svg +3 -0
  12. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/marimo_jupyter_extension/__init__.py +1 -1
  13. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/marimo_jupyter_extension/executable.py +1 -1
  14. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/marimo_jupyter_extension/handlers.py +1 -1
  15. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/marimo_jupyter_extension/labextension/package.json +1 -1
  16. marimo_jupyter_extension-0.1.1/marimo_jupyter_extension/labextension/static/71.4d6949ab423600535682.js +1 -0
  17. marimo_jupyter_extension-0.1.1/marimo_jupyter_extension/labextension/static/remoteEntry.6ae67aa952d37b1fa69f.js +1 -0
  18. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/pyproject.toml +1 -1
  19. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/scripts/test-environments.sh +87 -0
  20. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/tests/test_executable.py +7 -3
  21. marimo_jupyter_extension-0.1.0/labextension/src/iframe-widget.ts +0 -84
  22. marimo_jupyter_extension-0.1.0/marimo_jupyter_extension/labextension/static/510.5a6289689ce7a0453fc4.js +0 -1
  23. marimo_jupyter_extension-0.1.0/marimo_jupyter_extension/labextension/static/remoteEntry.705371d091b7d314fd9e.js +0 -1
  24. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/.gitignore +0 -0
  25. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/LICENSE +0 -0
  26. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/NOTICE +0 -0
  27. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/biome.jsonc +0 -0
  28. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/docs/configuration.md +0 -0
  29. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/example/opt/README.md +0 -0
  30. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/example/opt/bin/README.md +0 -0
  31. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/example/opt/jupyterhub/README.md +0 -0
  32. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/example/opt/jupyterhub/jupyterhub.service +0 -0
  33. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/example/opt/jupyterhub/jupyterhub_config.py +0 -0
  34. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/example/opt/jupyterhub/nginx.conf +0 -0
  35. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/example/opt/jupyterhub/package.json +0 -0
  36. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/example/opt/jupyterhub/pyproject.toml +0 -0
  37. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/example/opt/notebooks/README.md +0 -0
  38. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/example/opt/notebooks/demo/.gitkeep +0 -0
  39. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/install.json +0 -0
  40. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/.eslintrc.cjs +0 -0
  41. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/.npmrc +0 -0
  42. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/package-lock.json +0 -0
  43. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/package.json +0 -0
  44. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/pnpm-lock.yaml +0 -0
  45. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/schema/plugin.json +0 -0
  46. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/scripts/jupyter +0 -0
  47. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/src/sidebar.ts +0 -0
  48. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/src/svg.d.ts +0 -0
  49. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/style/base.css +0 -0
  50. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/style/marimo.svg +0 -0
  51. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/tsconfig.json +0 -0
  52. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/labextension/yarn.lock +0 -0
  53. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/marimo_jupyter_extension/config.py +0 -0
  54. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/marimo_jupyter_extension/convert.py +0 -0
  55. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/marimo_jupyter_extension/etc/jupyter_server_config.d/marimo_jupyter_extension.json +0 -0
  56. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/marimo_jupyter_extension/exporter.py +0 -0
  57. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/marimo_jupyter_extension/icon.svg +0 -0
  58. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/marimo_jupyter_extension/labextension/schemas/@marimo-team/jupyter-extension/package.json.orig +0 -0
  59. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/marimo_jupyter_extension/labextension/schemas/@marimo-team/jupyter-extension/plugin.json +0 -0
  60. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/marimo_jupyter_extension/labextension/static/style.js +0 -0
  61. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/marimo_jupyter_extension/labextension/static/third-party-licenses.json +0 -0
  62. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/mkdocs.yml +0 -0
  63. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/screenshot.png +0 -0
  64. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/scripts/release.sh +0 -0
  65. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/tests/conftest.py +0 -0
  66. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/tests/test_config.py +0 -0
  67. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/tests/test_convert.py +0 -0
  68. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/tests/test_entry_point.py +0 -0
  69. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/tests/test_exporter.py +0 -0
  70. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/tests/test_handlers.py +0 -0
  71. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/tests/test_integration.py +0 -0
  72. {marimo_jupyter_extension-0.1.0 → marimo_jupyter_extension-0.1.1}/tests/test_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marimo-jupyter-extension
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Jupyter extension to proxy marimo with JupyterLab integration
5
5
  Project-URL: Homepage, https://github.com/marimo-team/marimo-jupyter-extension
6
6
  Project-URL: Documentation, https://marimo-team.github.io/marimo-jupyter-extension/
@@ -59,6 +59,7 @@ Description-Content-Type: text/markdown
59
59
  **Highlights**
60
60
 
61
61
  - 🚀 **Launcher Integration** - marimo appears in the JupyterLab launcher with its own icon
62
+ - 🍃 **First-Class Marimo Notebook Support** - Double-click `_mo.py` files to open directly in marimo
62
63
  - 📊 **Sidebar Panel** - Monitor server status, view running sessions, and quick actions
63
64
  - 🐍 **Venv Selection** - Choose Python environment when creating new notebooks (with PEP 723 metadata)
64
65
  - 📁 **Context Menus** - Right-click `.py` files to edit with marimo, `.ipynb` files to convert
@@ -69,7 +70,7 @@ Description-Content-Type: text/markdown
69
70
  ## Quick Start
70
71
 
71
72
  ```bash
72
- uv pip install 'marimo[sandbox]>=0.19.4' marimo-jupyter-extension
73
+ uv pip install 'marimo[sandbox]>=0.19.8' marimo-jupyter-extension
73
74
  ```
74
75
 
75
76
  Launch JupyterLab and click the marimo icon in the launcher, or use the sidebar panel.
@@ -90,9 +91,16 @@ Create new marimo notebooks from the launcher. The sidebar shows server status,
90
91
 
91
92
  When creating a new notebook, select from available Python environments. The extension discovers Jupyter kernel specs and embeds the venv path using PEP 723 script metadata.
92
93
 
94
+ ### File Type Handling
95
+
96
+ | File Type | Double-click Behavior | "Open With" Menu |
97
+ |-----------|----------------------|------------------|
98
+ | `_mo.py` | Opens in marimo | marimo available |
99
+ | `.py` | Opens in standard editor | marimo available |
100
+
93
101
  ### Context Menu Actions
94
102
 
95
- - **Edit with marimo**: Right-click any `.py` file to open it in the marimo editor
103
+ - **Edit with marimo**: Right-click any `.py` or `_mo.py` file to open it in the marimo editor
96
104
  - **Convert to marimo**: Right-click any `.ipynb` file to convert it to marimo format
97
105
 
98
106
  ## Installation
@@ -102,7 +110,7 @@ See [Installation Guide](https://marimo-team.github.io/marimo-jupyter-extension/
102
110
  ### Single Environment
103
111
 
104
112
  ```bash
105
- uv pip install 'marimo[sandbox]>=0.19.4' marimo-jupyter-extension
113
+ uv pip install 'marimo[sandbox]>=0.19.8' marimo-jupyter-extension
106
114
  ```
107
115
 
108
116
  ### Multiple Environments (JupyterHub)
@@ -160,7 +168,7 @@ See [Troubleshooting Guide](https://marimo-team.github.io/marimo-jupyter-extensi
160
168
  | marimo icon missing | Install `marimo-jupyter-extension` in Jupyter's environment |
161
169
  | marimo fails to launch | Ensure marimo is in PATH or configure `MarimoProxyConfig.marimo_path` |
162
170
  | Modules not found | Install marimo in the same environment as your packages |
163
- | Sandbox features not working | Upgrade to `marimo[sandbox]>=0.19.4` |
171
+ | Sandbox features not working | Upgrade to `marimo[sandbox]>=0.19.8` |
164
172
 
165
173
  ## Community
166
174
 
@@ -19,6 +19,7 @@
19
19
  **Highlights**
20
20
 
21
21
  - 🚀 **Launcher Integration** - marimo appears in the JupyterLab launcher with its own icon
22
+ - 🍃 **First-Class Marimo Notebook Support** - Double-click `_mo.py` files to open directly in marimo
22
23
  - 📊 **Sidebar Panel** - Monitor server status, view running sessions, and quick actions
23
24
  - 🐍 **Venv Selection** - Choose Python environment when creating new notebooks (with PEP 723 metadata)
24
25
  - 📁 **Context Menus** - Right-click `.py` files to edit with marimo, `.ipynb` files to convert
@@ -29,7 +30,7 @@
29
30
  ## Quick Start
30
31
 
31
32
  ```bash
32
- uv pip install 'marimo[sandbox]>=0.19.4' marimo-jupyter-extension
33
+ uv pip install 'marimo[sandbox]>=0.19.8' marimo-jupyter-extension
33
34
  ```
34
35
 
35
36
  Launch JupyterLab and click the marimo icon in the launcher, or use the sidebar panel.
@@ -50,9 +51,16 @@ Create new marimo notebooks from the launcher. The sidebar shows server status,
50
51
 
51
52
  When creating a new notebook, select from available Python environments. The extension discovers Jupyter kernel specs and embeds the venv path using PEP 723 script metadata.
52
53
 
54
+ ### File Type Handling
55
+
56
+ | File Type | Double-click Behavior | "Open With" Menu |
57
+ |-----------|----------------------|------------------|
58
+ | `_mo.py` | Opens in marimo | marimo available |
59
+ | `.py` | Opens in standard editor | marimo available |
60
+
53
61
  ### Context Menu Actions
54
62
 
55
- - **Edit with marimo**: Right-click any `.py` file to open it in the marimo editor
63
+ - **Edit with marimo**: Right-click any `.py` or `_mo.py` file to open it in the marimo editor
56
64
  - **Convert to marimo**: Right-click any `.ipynb` file to convert it to marimo format
57
65
 
58
66
  ## Installation
@@ -62,7 +70,7 @@ See [Installation Guide](https://marimo-team.github.io/marimo-jupyter-extension/
62
70
  ### Single Environment
63
71
 
64
72
  ```bash
65
- uv pip install 'marimo[sandbox]>=0.19.4' marimo-jupyter-extension
73
+ uv pip install 'marimo[sandbox]>=0.19.8' marimo-jupyter-extension
66
74
  ```
67
75
 
68
76
  ### Multiple Environments (JupyterHub)
@@ -120,7 +128,7 @@ See [Troubleshooting Guide](https://marimo-team.github.io/marimo-jupyter-extensi
120
128
  | marimo icon missing | Install `marimo-jupyter-extension` in Jupyter's environment |
121
129
  | marimo fails to launch | Ensure marimo is in PATH or configure `MarimoProxyConfig.marimo_path` |
122
130
  | Modules not found | Install marimo in the same environment as your packages |
123
- | Sandbox features not working | Upgrade to `marimo[sandbox]>=0.19.4` |
131
+ | Sandbox features not working | Upgrade to `marimo[sandbox]>=0.19.8` |
124
132
 
125
133
  ## Community
126
134
 
@@ -11,7 +11,7 @@ On JupyterHub deployments, this leverages existing authentication and spawning i
11
11
  ## Quick Start
12
12
 
13
13
  ```bash
14
- pip install 'marimo>=0.19.4' marimo-jupyter-extension
14
+ pip install 'marimo>=0.19.8' marimo-jupyter-extension
15
15
  ```
16
16
 
17
17
  Launch JupyterLab and click the marimo icon in the launcher.
@@ -19,6 +19,7 @@ Launch JupyterLab and click the marimo icon in the launcher.
19
19
  ## Features
20
20
 
21
21
  - **JupyterLab Integration**: marimo appears in the launcher with its own icon
22
+ - **First-Class Marimo Notebook Support**: `_mo.py` files are recognized as Marimo notebooks and open in marimo by default on double-click
22
23
  - **Sidebar Panel**: Server status, running sessions, and quick actions
23
24
  - **Venv Selection**: Choose Python environment when creating new notebooks
24
25
  - **Context Menus**: Right-click to edit .py files or convert .ipynb files
@@ -27,6 +28,13 @@ Launch JupyterLab and click the marimo icon in the launcher.
27
28
  - **Sandbox Mode**: Run marimo in isolated environments with uvx
28
29
  - **Flexible PATH**: Configure executable search paths via environment variables or config files
29
30
 
31
+ ## File Type Handling
32
+
33
+ | File Type | Double-click Behavior | "Open With" Menu |
34
+ |-----------|----------------------|------------------|
35
+ | `_mo.py` | Opens in marimo | marimo available |
36
+ | `.py` | Opens in standard editor | marimo available |
37
+
30
38
  ## Next Steps
31
39
 
32
40
  - [Installation](installation.md) - Detailed setup instructions
@@ -3,7 +3,7 @@
3
3
  ## Requirements
4
4
 
5
5
  - Python 3.10+
6
- - marimo >= 0.19.4
6
+ - marimo >= 0.19.8
7
7
  - jupyter-server-proxy (installed automatically)
8
8
 
9
9
  ## Basic Installation
@@ -11,7 +11,7 @@
11
11
  `marimo-jupyter-extension` requires marimo but does not declare it as a dependency, allowing flexible installation scenarios.
12
12
 
13
13
  ```bash
14
- pip install 'marimo>=0.19.4' marimo-jupyter-extension
14
+ pip install 'marimo>=0.19.8' marimo-jupyter-extension
15
15
  ```
16
16
 
17
17
  Or with uv:
@@ -29,7 +29,7 @@ FROM quay.io/jupyterhub/jupyterhub:latest
29
29
  RUN cd /srv/jupyterhub && jupyterhub --generate-config && \
30
30
  echo "c.JupyterHub.authenticator_class = 'dummy'" >> jupyterhub_config.py && \
31
31
  echo "c.DummyAuthenticator.password = 'demo'" >> jupyterhub_config.py && \
32
- pip install --no-cache-dir notebook 'marimo>=0.19.4' marimo-jupyter-extension
32
+ pip install --no-cache-dir notebook 'marimo>=0.19.8' marimo-jupyter-extension
33
33
  RUN useradd -ms /bin/bash demo
34
34
  ```
35
35
 
@@ -57,7 +57,7 @@ RUN curl -fsSL https://github.com/conda-forge/miniforge/releases/latest/download
57
57
  bash /root/miniforge.sh -b -p /opt/conda && rm /root/miniforge.sh
58
58
 
59
59
  # marimo in conda environment (user packages available)
60
- RUN /opt/conda/bin/pip install --no-cache-dir 'marimo>=0.19.4'
60
+ RUN /opt/conda/bin/pip install --no-cache-dir 'marimo>=0.19.8'
61
61
 
62
62
  # marimo-jupyter-extension in Jupyter's environment
63
63
  RUN /usr/bin/pip install --no-cache-dir marimo-jupyter-extension
@@ -64,7 +64,7 @@ dependencies = [
64
64
  "notebook>=7.5.0",
65
65
  "oauthenticator>=17.3.0",
66
66
  "jupyterhub-systemdspawner>=1.0.2",
67
- "marimo>=0.19.4",
67
+ "marimo>=0.19.8",
68
68
  "marimo-jupyter-extension>=0.1.0",
69
69
  ]
70
70
  ```
@@ -52,10 +52,10 @@ which jupyter
52
52
 
53
53
  **Cause**: marimo version is too old.
54
54
 
55
- **Solution**: Upgrade to marimo 0.19.4 or newer:
55
+ **Solution**: Upgrade to marimo 0.19.8 or newer:
56
56
 
57
57
  ```bash
58
- pip install 'marimo>=0.19.4'
58
+ pip install 'marimo>=0.19.8'
59
59
  ```
60
60
 
61
61
  ### JupyterHub Issues
@@ -1,11 +1,20 @@
1
1
  import { LabIcon } from '@jupyterlab/ui-components';
2
2
  import marimoIconSvg from '../style/marimo.svg';
3
+ import marimoFileIconSvg from '../style/marimo-file-icon.svg';
3
4
 
4
5
  export const marimoIcon = new LabIcon({
5
6
  name: '@marimo-team/jupyter-extension:marimo',
6
7
  svgstr: marimoIconSvg,
7
8
  });
8
9
 
10
+ /**
11
+ * Small marimo icon for file browser items
12
+ */
13
+ export const marimoFileIcon = new LabIcon({
14
+ name: '@marimo-team/jupyter-extension:marimo-file',
15
+ svgstr: marimoFileIconSvg,
16
+ });
17
+
9
18
  /**
10
19
  * SVG string as a data URI for use with kernelIconUrl
11
20
  */
@@ -0,0 +1,377 @@
1
+ import { MainAreaWidget, IFrame } from '@jupyterlab/apputils';
2
+ import { UUID } from '@lumino/coreutils';
3
+ import type { ISignal } from '@lumino/signaling';
4
+ import { leafIcon } from './icons';
5
+
6
+ /**
7
+ * Sandbox settings for marimo IFrames.
8
+ * Centralized here to ensure consistency across all IFrame creation paths.
9
+ */
10
+ const IFRAME_SANDBOX_SETTINGS: IFrame.SandboxExceptions[] = [
11
+ 'allow-same-origin',
12
+ 'allow-scripts',
13
+ 'allow-forms',
14
+ 'allow-modals',
15
+ 'allow-popups',
16
+ 'allow-downloads',
17
+ ];
18
+
19
+ /**
20
+ * Tracked widget with metadata for session management.
21
+ */
22
+ interface TrackedWidget {
23
+ widget: MainAreaWidget<IFrame>;
24
+ originalUrl: string;
25
+ widgetId: string;
26
+ /** Whether this widget has been seen in the active sessions list at least once */
27
+ wasConnected: boolean;
28
+ /** Whether this is a new notebook (not file-based) - controls title modification on disconnect */
29
+ isNewNotebook: boolean;
30
+ }
31
+
32
+ /**
33
+ * Map of initializationId to tracked widget for managing new notebooks.
34
+ * Used to update tab titles and handle disconnection states.
35
+ */
36
+ const widgetsByInitId = new Map<string, TrackedWidget>();
37
+
38
+ /**
39
+ * Map of filePath to tracked widget for managing file-based notebooks.
40
+ * Used to handle disconnection states for notebooks opened from files.
41
+ */
42
+ const widgetsByFilePath = new Map<string, TrackedWidget>();
43
+
44
+ /**
45
+ * Get an existing widget for a file path, if one exists.
46
+ */
47
+ export function getWidgetByFilePath(
48
+ filePath: string,
49
+ ): MainAreaWidget<IFrame> | undefined {
50
+ return widgetsByFilePath.get(filePath)?.widget;
51
+ }
52
+
53
+ /**
54
+ * Refresh a widget's iframe by resetting its URL.
55
+ * This forces marimo to reconnect.
56
+ */
57
+ export function refreshWidgetByFilePath(filePath: string): void {
58
+ const tracked = widgetsByFilePath.get(filePath);
59
+ if (tracked) {
60
+ // Reset URL to force iframe reload
61
+ tracked.widget.content.url = tracked.originalUrl;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Generate a data URL containing the disconnected page HTML.
67
+ */
68
+ function createDisconnectedPageUrl(widgetId: string): string {
69
+ const html = `
70
+ <!DOCTYPE html>
71
+ <html>
72
+ <head>
73
+ <style>
74
+ body {
75
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
76
+ display: flex;
77
+ flex-direction: column;
78
+ align-items: center;
79
+ justify-content: center;
80
+ height: 100vh;
81
+ margin: 0;
82
+ background: #f5f5f5;
83
+ color: #333;
84
+ }
85
+ .icon {
86
+ font-size: 64px;
87
+ margin-bottom: 16px;
88
+ }
89
+ h2 {
90
+ margin: 0 0 8px 0;
91
+ font-weight: 500;
92
+ }
93
+ p {
94
+ margin: 0 0 24px 0;
95
+ color: #666;
96
+ }
97
+ button {
98
+ background: #2196f3;
99
+ color: white;
100
+ border: none;
101
+ padding: 12px 24px;
102
+ font-size: 14px;
103
+ border-radius: 4px;
104
+ cursor: pointer;
105
+ transition: background 0.2s;
106
+ }
107
+ button:hover {
108
+ background: #1976d2;
109
+ }
110
+ </style>
111
+ </head>
112
+ <body>
113
+ <div class="icon">🌿</div>
114
+ <h2>Session Disconnected</h2>
115
+ <p>The marimo session has been closed.</p>
116
+ <button onclick="reconnect()">Reconnect</button>
117
+ <script>
118
+ function reconnect() {
119
+ window.parent.postMessage({ type: 'marimo-reconnect', widgetId: '${widgetId}' }, '*');
120
+ }
121
+ </script>
122
+ </body>
123
+ </html>
124
+ `.trim();
125
+ return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
126
+ }
127
+
128
+ /**
129
+ * Initialize the message listener for reconnect requests.
130
+ * Should be called once when the extension loads.
131
+ */
132
+ let messageListenerInitialized = false;
133
+
134
+ function initializeMessageListener(): void {
135
+ if (messageListenerInitialized) {
136
+ return;
137
+ }
138
+ messageListenerInitialized = true;
139
+
140
+ window.addEventListener('message', (event: MessageEvent<unknown>) => {
141
+ const data = event.data as { type?: string; widgetId?: string } | null;
142
+ if (data?.type === 'marimo-reconnect' && data?.widgetId) {
143
+ const widgetId = data.widgetId;
144
+ // Find the tracked widget by its widgetId in both maps
145
+ for (const tracked of [
146
+ ...widgetsByInitId.values(),
147
+ ...widgetsByFilePath.values(),
148
+ ]) {
149
+ if (tracked.widgetId === widgetId) {
150
+ // Restore the original URL to reconnect
151
+ tracked.widget.content.url = tracked.originalUrl;
152
+ // Remove "(disconnected)" from title if present (only for new notebooks)
153
+ if (
154
+ tracked.isNewNotebook &&
155
+ tracked.widget.title.label.endsWith(' (disconnected)')
156
+ ) {
157
+ tracked.widget.title.label = tracked.widget.title.label.replace(
158
+ ' (disconnected)',
159
+ '',
160
+ );
161
+ }
162
+ break;
163
+ }
164
+ }
165
+ }
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Result from createMarimoIFrame containing the IFrame and tracking metadata.
171
+ */
172
+ export interface MarimoIFrameResult {
173
+ /** The configured IFrame ready for use */
174
+ iframe: IFrame;
175
+ /** The final URL set on the IFrame */
176
+ url: string;
177
+ /** The initialization ID for new notebooks, or null for file-based */
178
+ initId: string | null;
179
+ }
180
+
181
+ /**
182
+ * Create a marimo IFrame with proper sandbox settings and URL configuration.
183
+ * This is the single entry point for creating marimo IFrames - ensures
184
+ * consistent sandbox settings and URL building across all code paths.
185
+ *
186
+ * Note: Callers are responsible for registering the widget for tracking
187
+ * using registerWidgetForTracking() after wrapping in a widget.
188
+ */
189
+ export function createMarimoIFrame(
190
+ baseUrl: string,
191
+ options: { filePath?: string; initId?: string } = {},
192
+ ): MarimoIFrameResult {
193
+ const iframe = new IFrame({
194
+ sandbox: IFRAME_SANDBOX_SETTINGS,
195
+ });
196
+ iframe.addClass('jp-MarimoWidget');
197
+
198
+ // Generate initId if not file-based (and not provided)
199
+ const initId = options.filePath
200
+ ? null
201
+ : (options.initId ?? `__new__${UUID.uuid4()}`);
202
+
203
+ // Build URL: file-based uses encoded filePath, new notebooks use initId
204
+ const url = options.filePath
205
+ ? `${baseUrl}?file=${encodeURIComponent(options.filePath)}`
206
+ : `${baseUrl}?file=${initId}`;
207
+
208
+ iframe.url = url;
209
+ return { iframe, url, initId };
210
+ }
211
+
212
+ /**
213
+ * Create a marimo widget that embeds the editor in an iframe.
214
+ * Uses createMarimoIFrame internally for consistent IFrame creation.
215
+ */
216
+ export function createMarimoWidget(
217
+ baseUrl: string,
218
+ options: { filePath?: string; label?: string } = {},
219
+ ): MainAreaWidget<IFrame> {
220
+ const { filePath, label } = options;
221
+
222
+ // Use centralized IFrame creation
223
+ const {
224
+ iframe: content,
225
+ url: finalUrl,
226
+ initId,
227
+ } = createMarimoIFrame(baseUrl, { filePath });
228
+
229
+ const widget = new MainAreaWidget({ content });
230
+ widget.id = `marimo-${UUID.uuid4()}`;
231
+
232
+ if (label) {
233
+ widget.title.label = label;
234
+ } else if (filePath) {
235
+ const parts = filePath.split('/');
236
+ widget.title.label = parts[parts.length - 1] || 'marimo';
237
+ } else {
238
+ widget.title.label = 'marimo';
239
+ }
240
+
241
+ widget.title.closable = true;
242
+ widget.title.icon = leafIcon;
243
+ widget.title.caption = filePath ? `marimo: ${filePath}` : 'marimo Editor';
244
+
245
+ // Track widgets for disconnection handling
246
+ if (initId) {
247
+ // New notebook - track by initializationId
248
+ const widgetId = `marimo-widget-${UUID.uuid4()}`;
249
+ widgetsByInitId.set(initId, {
250
+ widget,
251
+ originalUrl: finalUrl,
252
+ widgetId,
253
+ wasConnected: false,
254
+ isNewNotebook: true,
255
+ });
256
+ widget.disposed.connect(() => {
257
+ widgetsByInitId.delete(initId);
258
+ });
259
+ initializeMessageListener();
260
+ } else if (filePath) {
261
+ // File-based notebook - track by filePath
262
+ registerWidgetForTracking(widget, filePath, finalUrl);
263
+ }
264
+
265
+ return widget;
266
+ }
267
+
268
+ /**
269
+ * Update widget titles based on running session data.
270
+ * Called by the sidebar when it polls for running notebooks.
271
+ * Also detects disconnected sessions and shows reconnect UI.
272
+ */
273
+ export function updateWidgetTitles(
274
+ sessions: { initializationId: string; name: string; path: string }[],
275
+ ): void {
276
+ // Build sets of active identifiers for quick lookup
277
+ const activeInitIds = new Set(sessions.map((s) => s.initializationId));
278
+ const activePaths = new Set(sessions.map((s) => s.path));
279
+
280
+ // Update titles for active sessions (initId-based widgets)
281
+ for (const session of sessions) {
282
+ const tracked = widgetsByInitId.get(session.initializationId);
283
+ if (tracked && session.name && session.path) {
284
+ const { widget } = tracked;
285
+ // Mark as connected now that we've seen it in the sessions list
286
+ tracked.wasConnected = true;
287
+ // Remove "(disconnected)" suffix if session is back (only for new notebooks)
288
+ let currentLabel = widget.title.label;
289
+ if (tracked.isNewNotebook && currentLabel.endsWith(' (disconnected)')) {
290
+ currentLabel = currentLabel.replace(' (disconnected)', '');
291
+ widget.title.label = currentLabel;
292
+ }
293
+ // Update title if it differs
294
+ if (currentLabel !== session.name) {
295
+ widget.title.label = session.name;
296
+ widget.title.caption = `marimo: ${session.path}`;
297
+ }
298
+ }
299
+ }
300
+
301
+ // Mark file-based widgets as connected when their path appears in sessions
302
+ for (const session of sessions) {
303
+ const tracked = widgetsByFilePath.get(session.path);
304
+ if (tracked) {
305
+ tracked.wasConnected = true;
306
+ // Note: file-based widgets (isNewNotebook: false) don't add "(disconnected)"
307
+ // suffix, so we don't need to remove it here
308
+ }
309
+ }
310
+
311
+ // Check for disconnected widgets (initId-based)
312
+ // Only show disconnected if the widget was previously connected
313
+ for (const [initId, tracked] of widgetsByInitId.entries()) {
314
+ if (!activeInitIds.has(initId) && tracked.wasConnected) {
315
+ showDisconnectedPage(tracked);
316
+ }
317
+ }
318
+
319
+ // Check for disconnected widgets (filePath-based)
320
+ // Only show disconnected if the widget was previously connected
321
+ for (const [filePath, tracked] of widgetsByFilePath.entries()) {
322
+ if (!activePaths.has(filePath) && tracked.wasConnected) {
323
+ showDisconnectedPage(tracked);
324
+ }
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Show the disconnected page for a tracked widget.
330
+ */
331
+ function showDisconnectedPage(tracked: TrackedWidget): void {
332
+ const { widget, widgetId, isNewNotebook } = tracked;
333
+ const disconnectedUrl = createDisconnectedPageUrl(widgetId);
334
+
335
+ // Only update if not already showing disconnected page
336
+ if (widget.content.url !== disconnectedUrl) {
337
+ widget.content.url = disconnectedUrl;
338
+ // Only add "(disconnected)" suffix for new notebooks
339
+ // File-based notebooks shouldn't modify title (causes file rename in DocumentWidget)
340
+ if (isNewNotebook && !widget.title.label.endsWith(' (disconnected)')) {
341
+ widget.title.label = `${widget.title.label} (disconnected)`;
342
+ }
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Widget-like interface for registration.
348
+ * Allows MarimoWidgetFactory widgets to be registered for tracking.
349
+ */
350
+ interface WidgetLike {
351
+ content: IFrame;
352
+ title: { label: string; caption: string };
353
+ disposed: ISignal<unknown, void>;
354
+ }
355
+
356
+ /**
357
+ * Register an externally-created widget for disconnection tracking.
358
+ * Used by MarimoWidgetFactory for widgets created via "Open With" menu.
359
+ */
360
+ export function registerWidgetForTracking(
361
+ widget: WidgetLike,
362
+ filePath: string,
363
+ originalUrl: string,
364
+ ): void {
365
+ const widgetId = `marimo-widget-${UUID.uuid4()}`;
366
+ widgetsByFilePath.set(filePath, {
367
+ widget: widget as unknown as MainAreaWidget<IFrame>,
368
+ originalUrl,
369
+ widgetId,
370
+ wasConnected: false,
371
+ isNewNotebook: false,
372
+ });
373
+ widget.disposed.connect(() => {
374
+ widgetsByFilePath.delete(filePath);
375
+ });
376
+ initializeMessageListener();
377
+ }