diff --git a/backend/src/backend/scripts.py b/backend/src/backend/scripts.py index 9d099dd..7f03d51 100644 --- a/backend/src/backend/scripts.py +++ b/backend/src/backend/scripts.py @@ -103,13 +103,22 @@ def safe_script_name(value: str, script_type: str) -> str: def _jupyter_path(script_type: str, script_id: str) -> str: """Return the in-Jupyter path used for a script. - The path is ``notebooks/{script_id}.{ext}`` — deterministic and - collision-free because ``script_id`` is a fresh ULID. The user's - ``script_name`` is kept on the Scripts row as a display label only; - the on-disk filename is owned by the database. + The path is ``{script_id}.{ext}`` — deterministic and + collision-free because ``script_id`` is a fresh ULID. Jupyter's + contents API treats the suffix after ``/api/contents/`` as a path + relative to its ``--notebook-dir`` (the workspace root), so a + ``notebooks/`` prefix here would push the file into a non-existent + ``notebooks/`` subdir on disk. The user's ``script_name`` is kept on + the Scripts row as a display label only; the on-disk filename is + owned by the database. + + The ``/notebooks/`` segment in the user's browser URL + (``/jupyter//notebooks/.ipynb``) is jupyter's URL route + for the editor view, not a filesystem path — jupyter routes that + URL to the file at the workspace root. """ ext = ".ipynb" if script_type == "notebook" else ".py" - return f"notebooks/{script_id}{ext}" + return f"{script_id}{ext}" def validate_script_content(content: str, script_type: str) -> bytes: