Skip to content

docker: Colab-grade JupyterLab and Studio UX for the Unsloth image#6681

Open
danielhanchen wants to merge 23 commits into
docker-blackwell-buildfrom
pr-jupyter-studio-ux
Open

docker: Colab-grade JupyterLab and Studio UX for the Unsloth image#6681
danielhanchen wants to merge 23 commits into
docker-blackwell-buildfrom
pr-jupyter-studio-ux

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Jun 25, 2026

Copy link
Copy Markdown
Member

Summary

This stacks a Colab-grade JupyterLab and Studio experience on top of the Unsloth Docker image (the one that runs on any NVIDIA GPU host, not just Blackwell). It is additive on docker-blackwell-build: no change to the training stack, the CUDA/torch pinning, or the existing service trio (Studio on 8000, JupyterLab on 8888, sshd on 22). The goal is that a user who docker pulls the image and opens JupyterLab lands in something that looks and behaves like Colab, with the notebooks already organized and the rough edges (Ctrl+A selecting the whole notebook, Colab-only intro text, %%capture under a #@title form) fixed.

JupyterLab extension (prebuilt, Node-free runtime)

A small prebuilt JupyterLab 4 labextension (docker/jupyter/unsloth_labext) is compiled in a throwaway labext-builder stage in Dockerfile.studio and copied into the final image as static assets, so Node never ships in the runtime image. Plugins:

  • Unsloth Dark (Monokai) theme with adaptive light/dark by system preference.
  • Colab-style cell navigation: ArrowDown/Up at the edge of an editor jump to the top of the next/previous cell.
  • Top-bar Unsloth logo (the stock Jupyter logo plugin is disabled and locked).
  • #@title Colab forms: a #@title ... first line renders as a collapsible heading bar (Heading 2 sized text, large caret) instead of a bare comment.
  • Ctrl+A in a cell output selects only that output, not the entire notebook. Previously Ctrl+A while focused on an output ran notebook:select-all, which selected every cell and was laggy on long notebooks.
  • Right activity bar hidden by default (collapsed on restore and hidden via CSS).

System defaults ship in docker/jupyter/overrides.json: the theme, a per-cell run button that does not auto-advance, a labeled "Restart & Run All", windowing disabled so collapsing a long output does not snap the cell to the top, and the Jupyter news/update prompts suppressed.

Studio and login branding

Dockerfile.studio brands jupyter_server: the browser-tab favicon and page logo become the Unsloth logo, and the login screen uses a dark Unsloth-themed login.html that rotates through the curated Studio sloth stickers (installed by install_sloth_stickers.py, fail-soft to the logo if the Studio asset folder ever moves).

Notebook organization and Colab compatibility (base image)

  • Categorized folder view (unsloth_nb_view.py + unsloth_sync_notebooks.sh): a sibling tree of relative symlinks mirroring the README sections (01 Main Notebooks, 02 GRPO and Reinforcement Learning Notebooks, ...) is rebuilt on every boot. The real .ipynb files are never moved or renamed, and the symlink tree is invisible to the sync state machine (which walks find -type f), so edit-preservation and delete-restore are unaffected. JupyterLab opens straight into this view (studio_launch.sh default_url / preferred_dir).
  • AMD filter: AMD-* notebooks are shown only on an AMD/HIP host (autodetected; override with UNSLOTH_NB_GPU=amd|cuda).
  • Colab intro strip (unsloth_nb_strip_colab.py): the Docker image removes the Colab-only "To run this, press Runtime, Run all..." sentence from the first cell of notebooks the user has not edited, updating their recorded hashes in place so refresh detection is unaffected. Opt out with UNSLOTH_KEEP_COLAB_INTRO=1. The upstream notebooks are unchanged; this is a Docker-only sync-time transform.
  • Colab cell magics (unsloth_colab_compat.py): an IPython input transformer hoists %%capture (and other cell magics) above a leading #@title form so the cell runs instead of raising UsageError. Registered from unsloth_ipython_startup.py in its own try/except so it can never disable the transformers-sidecar hook.
  • Quieter sidecar log: the per-cell [unsloth-nb] activated transformers sidecar ... line is now silent unless UNSLOTH_ENABLE_LOGGING=1 (unsloth_nb_compat.py).

Dependency pinning and image naming

  • The curated notebook extras are pinned to their resolved, tested versions (jupyterlab==4.6.0, notebook==7.6.0, ipywidgets, matplotlib, soundfile, evaluate, jiwer, tensorboard, langid, easydict, protobuf, omegaconf, einx, librosa, ftfy) for reproducible rebuilds, matching the cu128 core convention. decord is split into its own fail-soft install since it ships no aarch64 wheel (amd64 gets it; on arm64 the ERNIE-VL video path is skipped rather than breaking the whole image build).
  • The lean training image is renamed from :base to :core end to end: the user-facing helpers (run.sh, docker_confirm.sh, docker_confirm.ps1), the Dockerfile.studio header, and docker-publish.yml itself (which now publishes :core plus core-<tag> / core-nightly / core-sha-*). The full Studio image keeps :latest and gains a stable :studio alias.

Testing

  • tests/validate_studio_features.py is a static self-test asserting the labextension plugins (theme, cell-nav, logo, #@title forms, output-select-all, ui-chrome), the overrides.json keys, the branding/sticker wiring, and the login stickers are all present in the source tree.
  • The studio image was built locally from this branch (labextension recompiled from source, branding and sticker layers applied) and the JupyterLab UX (theme, #@title heading, Ctrl+A output-only select, hidden right bar, random sloth login, categorized landing view) was verified in a live container.

Follow-up (separate PR)

The vLLM _decompose_size_nodes graph-erase crash that hits fast_inference / GRPO notebooks (vLLM PR #42543, unmerged upstream) is handled in a separate change: a monkeypatch in unsloth_zoo plus a base-image bake. It is intentionally not in this PR to keep this focused on the JupyterLab and Studio UX.

danielhanchen and others added 2 commits June 25, 2026 16:22
Stacks a Colab-like JupyterLab and Studio experience on top of the
existing Blackwell image. Additive only: the training stack, CUDA/torch
pinning, and the Studio/JupyterLab/sshd service trio are unchanged.

JupyterLab labextension (prebuilt in a throwaway builder stage, so the
runtime image stays Node-free):
  - Unsloth Dark (Monokai) theme, adaptive light/dark by system preference
  - Colab-style ArrowDown/Up cell navigation
  - top-bar Unsloth logo (stock Jupyter logo disabled and locked)
  - #@title lines render as collapsible Heading-2 form bars
  - Ctrl+A in a cell output selects only that output, not the whole
    notebook (the old behaviour ran notebook:select-all and was laggy)
  - right activity bar hidden by default
  - overrides.json: per-cell run button without auto-advance, labeled
    Restart and Run All, windowing off so collapsing an output does not
    snap to the cell top, news/update prompts suppressed

Studio and login branding: Unsloth favicon, page logo, and a dark
Unsloth login page that rotates through the curated Studio sloth
stickers (fail-soft to the logo).

Notebook organization and Colab compatibility (base image):
  - categorized folder view built from relative symlinks mirroring the
    README sections, rebuilt each boot; real .ipynb files never moved,
    and the symlink tree is invisible to the sync state machine
  - AMD-* notebooks shown only on an AMD/HIP host (autodetected)
  - Docker-only strip of the Colab "Run all on Colab" intro sentence
    from unedited notebooks (upstream notebooks unchanged)
  - hoist %%capture above a leading #@title form so the cell runs
  - the per-cell transformers-sidecar log is silent unless
    UNSLOTH_ENABLE_LOGGING=1

Dependency pinning and naming: the curated notebook extras are pinned to
their resolved versions for reproducible rebuilds; decord is split into
its own fail-soft install (no aarch64 wheel). The lean base image is
renamed from :base to :core.

Adds tests/validate_studio_features.py, a static self-test for the
labextension plugins, overrides keys, and branding wiring.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a custom, Monokai-themed JupyterLab extension (unsloth-jupyterlab) and several integration scripts to provide a Colab-like user experience within the Unsloth Docker environment. Key additions include custom cell navigation, Colab #@title form cell rendering, localized output selection, and automatic IPython magic hoisting for compatibility. It also adds automated scripts to strip Colab-specific introductory text, clean up stale widget outputs, and organize notebooks into a categorized folder view using relative symlinks. A critical bug was identified in the notebook view generation script (docker/unsloth_nb_view.py), where the directory removal helper (_rmtree) could follow symlinks to directories and accidentally delete the actual notebook files. A fix was suggested to check for symbolic links first before verifying directory status.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread docker/unsloth_nb_view.py Outdated
Comment on lines +164 to +167

def _rmtree(path):
# Remove a previously built VIEW. Only unlinks symlinks + empty dirs we made,
# but a full rmtree is fine here because VIEW holds nothing but our symlinks.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In Python, os.path.isdir(path) returns True if path is a symbolic link pointing to a directory. Consequently, if _rmtree is called with a symlink to a directory, it will bypass the not os.path.isdir(path) check and proceed to os.walk(path). Since os.walk follows the top-level symlink, it will traverse the target directory and delete all files and subdirectories inside it, resulting in accidental data loss of the actual notebooks.

Checking os.path.islink(path) first ensures that if path is a symlink (even to a directory), it is safely unlinked/removed directly without traversing its target.

Suggested change
def _rmtree(path):
# Remove a previously built VIEW. Only unlinks symlinks + empty dirs we made,
# but a full rmtree is fine here because VIEW holds nothing but our symlinks.
if os.path.islink(path):
os.remove(path)
return
if not os.path.isdir(path):
return

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c9ff52ba04

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docker/.dockerignore
Comment on lines +21 to +24
!jupyter/overrides.json
!jupyter/favicon.ico
!jupyter/logo.png
!jupyter/login.html

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include the sticker installer in the Docker build context

When building Dockerfile.studio with docker/ as the context, this allowlist still excludes jupyter/install_sloth_stickers.py because the file is not whitelisted after the top-level ** ignore. The new COPY jupyter/install_sloth_stickers.py ... in the studio Dockerfile therefore fails with a missing source before the image can build; add an exception for that helper alongside the other jupyter/ assets.

Useful? React with 👍 / 👎.

Comment thread docker/Dockerfile.studio
Comment on lines +40 to +42
RUN apt-get update \
&& apt-get install -y --no-install-recommends nodejs npm git \
&& rm -rf /var/lib/apt/lists/*

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Install a Node version that can build JupyterLab 4.6

On the Ubuntu 24.04 CUDA base, apt-get install nodejs resolves to Node 18, but the JupyterLab 4.6 JavaScript packages used by the next jlpm install declare a Node >=20 engine. In a clean studio build this makes the labextension builder fail before producing /opt/labext-src/unsloth-jupyterlab/labextension; install/pin a Node 20+ source instead of the distro nodejs package.

Useful? React with 👍 / 👎.

Comment thread docker/unsloth_nb_view.py Outdated
Comment on lines +139 to +141
# Rebuild VIEW from scratch.
_rmtree(view)
os.makedirs(view, exist_ok=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid deleting user files from the notebook view

The categorized view is under /workspace by default and is where Jupyter now opens, so users can easily create or save files there (or point UNSLOTH_NOTEBOOKS_VIEW_DIR at an existing directory). Rebuilding the view calls _rmtree(view) unconditionally, and _rmtree removes every regular file it finds, so the next container start can silently delete user-created notebooks or other files in that folder instead of just replacing the symlinks this script owns.

Useful? React with 👍 / 👎.

Comment thread docker/studio_launch.sh Outdated
Comment on lines +69 to +71
c.ServerApp.default_url = "/lab/tree/Unsloth Notebooks"
c.LabApp.default_url = "/lab/tree/Unsloth Notebooks"
c.ServerApp.preferred_dir = "/workspace/Unsloth Notebooks"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor the configured notebook view path

When users set UNSLOTH_NOTEBOOKS_VIEW_DIR or disable the view with UNSLOTH_SKIP_NOTEBOOK_VIEW=1, the sync script either builds the categorized tree somewhere else or does not build /workspace/Unsloth Notebooks at all, but this launcher still hard-codes Jupyter's landing URL and preferred directory to that default path. Those configurations therefore open JupyterLab on a missing or stale folder instead of the requested view/raw notebooks; derive these values from the same env settings or fall back when the view is skipped.

Useful? React with 👍 / 👎.

- unsloth_nb_view.py: rebuilding the categorized view no longer deletes
  user files. The view is also JupyterLab's landing dir, so a user may
  save real notebooks there; _clear_view now unlinks only the symlinks we
  own and removes only folders that end up empty, leaving regular files
  in place. It also tests islink before isdir, so a view that is itself a
  symlink to a directory is unlinked instead of being walked into (which
  would have wiped the symlink target).

- studio_launch.sh: derive the landing URL and preferred_dir from
  UNSLOTH_NOTEBOOKS_VIEW_DIR / UNSLOTH_SKIP_NOTEBOOK_VIEW, the same env
  the sync script uses, instead of hard-coding /workspace/Unsloth
  Notebooks. A relocated or disabled view no longer opens JupyterLab on a
  missing folder; it falls back to the default /lab over /workspace.

- Dockerfile.studio: the labext-builder stage now installs Node 20 from
  NodeSource. Ubuntu 24.04's distro nodejs is 18, below JupyterLab 4.6's
  declared Node >=20 engine. Node stays confined to the throwaway builder
  stage, so the runtime image is unchanged.

- .dockerignore: explicitly allowlist jupyter/install_sloth_stickers.py
  alongside its sibling jupyter assets, rather than relying on the
  directory re-inclusion.
@danielhanchen danielhanchen changed the title docker: Colab-grade JupyterLab and Studio UX for the Blackwell image docker: Colab-grade JupyterLab and Studio UX for the Unsloth image Jun 26, 2026
@danielhanchen

Copy link
Copy Markdown
Member Author

Thanks for the review. Addressed in e496095:

_rmtree following a symlinked directory (high) + the view rebuild deleting user files (P2): reworked into _clear_view. It now tests os.path.islink before os.path.isdir, so a view that is itself a symlink to a directory is unlinked instead of walked into (which would have wiped the target). It also unlinks only the symlinks we own and removes only folders that end up empty, so any regular file a user saved in the view directory (it is JupyterLab's landing dir) survives a rebuild. Added a functional test covering all three cases (user files survive, symlinked view root is not followed, AMD filter).

Hard-coded landing path in studio_launch.sh (P2): the launcher now derives default_url / preferred_dir from UNSLOTH_NOTEBOOKS_VIEW_DIR and UNSLOTH_SKIP_NOTEBOOK_VIEW, the same env the sync script uses. A relocated view opens on the configured folder; a disabled view (or one outside root_dir) falls back to the default /lab over /workspace instead of a missing directory.

Node version for the labext builder (P1): confirmed the distro nodejs on the 24.04 base is 18.19.1, below JupyterLab 4.6's declared >=20 engine. In practice the build still succeeds on 18 (the engine constraint is a warning, not a hard failure), but to be correct I switched the builder stage to Node 20 from NodeSource and verified jlpm build:prod produces the labextension. Node stays confined to the throwaway builder stage, so the runtime image is unchanged.

install_sloth_stickers.py in the build context (P1): I checked this with a throwaway COPY against the real docker/ context and the file is in fact included (the !jupyter directory re-inclusion cascades to it), so the build was not actually breaking. That said, it was the only jupyter/ asset relying on the cascade, so I added the explicit !jupyter/install_sloth_stickers.py allowlist alongside its siblings for robustness.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50f7170be4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread docker/studio_launch.sh Outdated
# in those cases JupyterLab just opens on its default (/lab) over /workspace.
_root_dir="/workspace"
_view_dir="${UNSLOTH_NOTEBOOKS_VIEW_DIR:-/workspace/Unsloth Notebooks}"
if [[ "${UNSLOTH_SKIP_NOTEBOOK_VIEW:-0}" != "1" && "${_view_dir}" == "${_root_dir}/"* ]]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect skipped notebook sync when choosing landing path

The fresh case here is UNSLOTH_SKIP_NOTEBOOK_SYNC=1: the entrypoint exits before it can build /workspace/Unsloth Notebooks, but this condition still writes /lab/tree/Unsloth Notebooks because it only checks UNSLOTH_SKIP_NOTEBOOK_VIEW. Users who disable notebook sync therefore land on a missing or stale view instead of /workspace; include the sync skip or verify the view directory exists before setting default_url.

Useful? React with 👍 / 👎.

Comment thread docker/Dockerfile.studio Outdated
&& /opt/unsloth-venv/bin/python /tmp/unsloth-branding/install_sloth_stickers.py \
--src "${UNSLOTH_STUDIO_HOME}/src/studio/frontend/public/Sloth emojis" \
--dest "${JS}/static/sloth" \
|| echo ">> sloth stickers not installed (login falls back to the Unsloth logo)" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep required branding failures fatal

Because this || is attached to the entire preceding && chain, any required branding step above it, such as resolving JS or copying the favicon/logo/login template, is converted into the optional sticker warning and the build continues. If a pinned Jupyter Server layout changes, this would publish a partially branded or broken login page instead of failing the image build; group only the sticker installer with the fallback so earlier failures remain fatal.

Useful? React with 👍 / 👎.

Comment thread docker/unsloth_nb_view.py Outdated
Comment on lines +158 to +159
if os.path.islink(link) or os.path.exists(link):
os.remove(link)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve colliding user files in the notebook view

Fresh evidence: _clear_view now preserves regular files, but this later os.remove(link) still deletes any regular file whose name collides with a generated notebook symlink. If a user uploads or saves a notebook with the same filename inside one of the categorized folders, the next view rebuild silently removes that file before creating the symlink; only unlink existing symlinks, and skip or rename around regular files.

Useful? React with 👍 / 👎.

Comment thread docker/jupyter/overrides.json Outdated
"@jupyterlab/apputils-extension:notification": {
"fetchNews": "false",
"checkForUpdates": false,
"doNotDisturbMode": true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not silence all JupyterLab notifications

In JupyterLab 4.6 this is the global do-not-disturb switch, so when kernels restart, connections drop, or extensions emit warnings, no toast is automatically displayed. The news/update prompt is already disabled by fetchNews and checkForUpdates, so this line hides unrelated operational feedback for every user opening the image.

Useful? React with 👍 / 👎.

Comment thread docker/Dockerfile Outdated
# so install it on its own and fail-soft: amd64 gets it; on arm64 the ERNIE-VL
# video path is skipped rather than breaking the whole image build.
RUN ${VENV}/bin/uv pip install --python ${VENV}/bin/python "decord==0.6.0" \
|| echo ">> decord skipped (no matching wheel for ${TARGETARCH:-amd64}); ERNIE-VL video decode unavailable"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep decord mandatory on amd64 builds

This fallback is unconditional, so on amd64 a transient resolver failure or incompatible decord wheel still produces a successful image without the dependency, even though the ERNIE-VL video notebooks expect it and the comment says amd64 gets it. Restrict the fail-soft path to non-amd64 or add an amd64 import check so supported images fail instead of silently dropping video decode.

Useful? React with 👍 / 👎.

Comment thread docker/docker_confirm.sh

IMAGE="${IMAGE:-unsloth/unsloth:latest}"
BASE_IMAGE="${BASE_IMAGE:-unsloth/unsloth:base}"
BASE_IMAGE="${BASE_IMAGE:-unsloth/unsloth:core}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Publish the tag used by confirmation scripts

This default now pulls unsloth/unsloth:core, but the in-repo publish workflow still creates only :base and base-* tags (.github/workflows/docker-publish.yml lines 228-238 and 432-435) and never creates a :core tag. On fresh user machines the confirmation scripts therefore fail in the pull phase unless BASE_IMAGE is overridden; update the workflow tags too, or keep the scripts on the published tag.

Useful? React with 👍 / 👎.

Complete the base->core (and "studio as studio") tag rename so the publish
workflow matches the user-facing helpers and the Dockerfile.studio header.

- The lean training image now publishes as :core (core-<tag>, core-nightly,
  core-sha-*); run.sh / docker_confirm.* already told users to pull :core, but
  docker-publish.yml still tagged it :base, so that pull would have 404'd. The
  per-arch digest artifacts are renamed to match.
- The full Studio image keeps :latest and gains a stable :studio alias, matching
  the Dockerfile.studio header.

Both the merge and post-publish smoke-test metadata blocks are updated together.
Internal "base image" wording (the layer Studio builds FROM) is left as-is.
@danielhanchen danielhanchen force-pushed the pr-jupyter-studio-ux branch from 45d76d5 to b963403 Compare June 26, 2026 02:44

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b963403b18

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +41 to +42
if (!panel.node.contains(event.target as Node)) {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore output widgets before hijacking arrows

When focus is inside an interactive output (for example an ipywidgets slider/dropdown/text box), the event target is still contained by the notebook panel, so this handler proceeds in command mode and later calls preventDefault() while moving the active cell. That swallows ArrowUp/ArrowDown that the focused widget needs for its own navigation/value changes, making those controls hard to use after a notebook cell creates them; add a guard for editable/output-widget targets before taking over the key.

Useful? React with 👍 / 👎.

Comment on lines +55 to +59
if (direction === 1 && line !== editor.lineCount - 1) {
return;
}
if (direction === -1 && line !== 0) {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let CodeMirror completions handle arrows

When a completion popup is open in an editor on the first/last logical line (a very common one-line setup cell), ArrowDown/ArrowUp should move through the suggestions, but this capture-phase handler prevents the event before CodeMirror can see it because it only checks the cursor line. That makes completions unusable at cell boundaries; skip the takeover while CodeMirror completion UI is active, or otherwise let the editor handle those arrows first.

Useful? React with 👍 / 👎.

danielhanchen and others added 2 commits June 26, 2026 05:35
- studio_launch.sh: also gate the categorized-view landing URL on
  UNSLOTH_SKIP_NOTEBOOK_SYNC (the entrypoint skips building the view entirely in
  that mode), not just UNSLOTH_SKIP_NOTEBOOK_VIEW, so a no-sync container does not
  land on a missing folder.

- Dockerfile.studio: scope the sticker-install "|| echo" fallback to only the
  sticker step via a { ...; } group. It was attached to the whole branding &&
  chain, so a failure in a REQUIRED step (JS resolve, favicon/logo/login copy)
  was swallowed and the build continued with broken branding.

- unsloth_nb_view.py: when creating the categorized symlinks, only replace our
  own stale symlinks; if a real user file already occupies that name, keep it and
  skip the link instead of os.remove-ing it.

- overrides.json: drop doNotDisturbMode (it silenced ALL JupyterLab toasts,
  including kernel-restart / connection-drop feedback). The news/update prompts
  are already off via fetchNews / checkForUpdates.

- Dockerfile: keep decord mandatory on amd64 (fail the build on a missing or
  incompatible wheel) and only fail-soft on arm64/other arches that have no wheel.

- cellNav.ts: do not hijack ArrowUp/Down when focus is in an interactive output
  widget / form control, or while a completion popup is open, so ipywidgets
  controls and autocomplete at cell boundaries keep working.
@danielhanchen

Copy link
Copy Markdown
Member Author

Worked through the review feedback. The earlier (06-25) comments were re-reviews of the first commit and are already fixed in it (the _clear_view symlink/data-loss rewrite, the install_sloth_stickers.py allowlist, Node 20, the env-aware landing path). The newer (06-26) batch were valid and are fixed in the latest commit:

  • studio_launch.sh: also gate the landing URL on UNSLOTH_SKIP_NOTEBOOK_SYNC (the entrypoint skips building the view entirely in that mode), not just UNSLOTH_SKIP_NOTEBOOK_VIEW.
  • Dockerfile.studio: scope the sticker-install || echo fallback to only the sticker step (a { ...; } group), so a failure in a required branding step (JS resolve, favicon/logo/login copy) still fails the build.
  • unsloth_nb_view.py: when creating the categorized symlinks, only replace our own stale symlinks; a real user file colliding with a generated name is kept, not removed.
  • overrides.json: dropped doNotDisturbMode (it silenced all JupyterLab toasts incl. kernel-restart / connection-drop); the news/update prompts stay off via fetchNews / checkForUpdates.
  • Dockerfile: keep decord mandatory on amd64 (fail on a missing/incompatible wheel), fail-soft only on arches with no wheel.
  • cellNav.ts: do not hijack ArrowUp/Down when focus is in an interactive output widget / form control, or while a completion popup is open, so ipywidgets controls and autocomplete at cell boundaries keep working.

Labextension recompiles cleanly and the studio self-test passes.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2eba8a4976

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +38 to +39
if not cond:
_failures.append(name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail validation checks when pytest collects them

In the repo CPU workflow I inspected (studio-backend-ci.yml runs python -m pytest tests/), this new file is collected as six pytest tests because the functions are named test_*. Pytest never calls main(), so when a condition is false check() only appends to _failures and the individual test still returns normally, producing a false-green validation unless someone runs the script directly. Please have check() raise/assert, or wrap the script in a single pytest test that calls main().

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pytest does not collect this file: it is named validate_studio_features.py, not test_*.py, so default discovery skips it. The checks are now run explicitly in the repo CPU job.

@wasimysaid wasimysaid self-assigned this Jun 26, 2026
…tinuation

Move the sloth-sticker fail-soft explanation above the RUN so no comment line
sits between backslash-continued commands. BuildKit strips such comments, but
keeping the RUN body a plain && chain removes the ambiguity for non-BuildKit
builders and static linters. The { ...; } fail-soft scoping is unchanged.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@danielhanchen

Copy link
Copy Markdown
Member Author

Addressed the latest review pass:

  • Merged the current base (docker-blackwell-build) into this branch. The branch had drifted a long way behind its base (the Studio backend work landed there after this branch forked), so the review's "accidental revert" signal was real in the sense that merging the UI changes on the stale tree would have dropped those base-branch additions. Merging the base in brings the branch up to date and pulls in the base-image fixes below; the diff now shows only this PR's intended changes.
  • Dockerfile.studio: moved the sloth-sticker fail-soft explanation out of the branding RUN's line continuation so the RUN body is a plain && chain. BuildKit strips full-line comments inside a continuation (the image builds today, and the same pattern is used elsewhere in this file), but keeping the comment out of the continuation removes the ambiguity for non-BuildKit builders and static linters. The { ...; } fail-soft scoping around the sticker step is unchanged, so a failure in a required branding step (JS resolve, favicon/logo/login copy) still fails the build.
  • docker-publish.yml: the :base -> :core rename and the new :studio alias are kept, and flavor: latest=false on the core image metadata carried through the merge. The Studio image owns :latest and :studio; the core image never claims :latest, including on a v* tag push.
  • The first-boot user-notebook overwrite guard, the record_state temp-file exclusion and the pip-shim editable/url fix all arrived through the base merge.

Validation: tests/validate_studio_features.py passes (theme, windowing, run-all label, labextension plugins, branding assets), the labextension source checks are green, and bash -n / py_compile on the touched scripts/helpers are clean.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 9a53256375

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…jupyter-studio-ux

# Conflicts:
#	.github/workflows/docker-publish.yml
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: a56a7a8e89

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e4699c920

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docker/unsloth_nb_view.py Outdated
Comment on lines +78 to +80
m = re.match(r"^###\s+(.*)$", line)
if m:
section = clean_section(m.group(1))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse the notebooks catalog that is actually fetched

When the image bakes unslothai/notebooks from its current default ref, that repo's README no longer exposes the categorized notebook list as ### sections around nb/*.ipynb links, so this condition never sets a section and parse_readme() returns no rows. The view builder then falls through to putting every notebook under Other Notebooks, which means the new Jupyter landing view loses the curated category folders it is meant to provide; either parse the current catalog source/heading format or ship a stable manifest instead of depending only on ### headings here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current unslothai/notebooks README still exposes the categories as ### headers around nb/*.ipynb link tables, so parse_readme returns the full set (verified against main: 26 categorized folders).

@LeoBorcherding

Copy link
Copy Markdown
Collaborator

reviewed on windows 11 / WSL2.

quick stuff first: installed studio from this branch locally and it ran fine (GGUF chat worked), and validate_studio_features.py passes all its checks.

the docker studio image is where it got interesting. building Dockerfile.studio on top of the core image fails for two reasons:

  1. it depends on install: let UNSLOTH_TORCH_INDEX_FAMILY / _URL override CUDA wheel detection #6692. the studio build asks install.sh for the cu128 gpu build via UNSLOTH_TORCH_INDEX_FAMILY=cu128, but that flag only exists in install: let UNSLOTH_TORCH_INDEX_FAMILY / _URL override CUDA wheel detection #6692, which isn't merged. so on main it silently installs cpu torch and the build's own assert stops it:

    AssertionError: Studio venv torch 2.10.0+cpu does not match cu128
    

pointing UNSLOTH_STUDIO_REF at the #6692 branch fixes it, so #6692 needs to land first.

  1. the llama.cpp updater breaks inside a docker build. it found a newer llama.cpp than the one in core, downloaded and validated it, then tried to swap it into place with os.rename - which fails with EXDEV because you can't rename a directory across overlayfs layers in a build:

    [llama-prebuilt] activation failed for staged install:
        [Errno 18] Invalid cross-device link: '/opt/unsloth/llama.cpp' -> '.../.staging/llama.cpp.rollback-...'
    [llama-prebuilt] falling back to source build
    

the rollback hits the same error, so it gives up, deletes the working copy, and source-builds instead (no nvcc here, so the result is broken). basically os.rename should be shutil.move so it survives the cross-device case. this probably didn't show up in your testing because your base already had the latest llama tag, so the updater never ran.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6fd9744f31

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docker/unsloth_nb_view.py Outdated
seen_pairs = set() # (section, filename) already emitted
section = None
for line in text.splitlines():
m = re.match(r"^###\s+(.*)$", line)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset notebook section on top-level headings

When the baked notebooks README reaches a non-### domain heading such as the current # AMD Notebooks / # Molab Notebooks, section remains whatever the previous ### set (currently Other Notebooks), so the subsequent all-notebooks links are emitted into that folder. Because build_view intentionally cross-lists per section, the default Jupyter view gets a huge duplicate Other Notebooks folder instead of just uncategorized notebooks; reset on any heading or handle #/## boundaries before collecting links.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. parse_readme now resets the section on any heading level and strips a leading emoji run, so the AMD domain notebooks get their own folder instead of falling into Other Notebooks.

checks the shipped JupyterLab config + labextension source, so a regression in
the notebook organisation, Colab compatibility, Colab-intro/widget stripping,
sidecar-log gating, the labextension plugins, the JupyterLab defaults, or the
login branding fails CI on every device.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Wire the Studio validation into CI

This script is named validate_studio_features.py, so pytest's default test_*.py discovery will not collect it, and I don't see any .github workflow invoking it explicitly. That means the Docker/Jupyter checks described here never fail CI unless someone runs the script manually; either rename/wrap it as a pytest test or add an explicit workflow step.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wired in: the repo CPU job now runs tests/validate_studio_features.py explicitly, so a regression fails CI.

danielhanchen and others added 2 commits June 27, 2026 07:43
…b image

Make it obvious the image is built by Unsloth and hard to white-label out with a
shallow find-and-replace, and surface the AGPLv3 license + copyright in the UI.

Visible attribution (labextension):
- Help > "About Unsloth Docker Studio" dialog (about.ts): Unsloth logo, the
  AGPLv3 notice, "Copyright 2026-Present the Unsloth team", and source/website/
  license links. Added to the Help menu and the command palette.
- The JupyterLab loading splash is replaced with a spinning Unsloth logo
  (splash.ts, provides ISplashScreen; honors prefers-reduced-motion). The stock
  @jupyterlab/apputils-extension:splash is disabled+locked at build time, like
  the stock logo.
- AGPLv3 footer (license + copyright + links) on the branded login page.
- Labextension relicensed AGPL-3.0-only; SPDX headers on every source file.

Anti-tamper (no encoded/obfuscated strings -- plain readable text only; the one
data URI is the logo image):
- A canonical, plain-text attribution set lives in unsloth_branding.py with a
  TypeScript mirror (branding.ts) bundled verbatim into the labextension, so the
  phrase, copyright, links and plugin ids are spread across independent layers.
- unsloth_branding.py verifies all of these across the installed files (AGPLv3
  text, login footer, theme, labextension package + built bundle strings, logo,
  favicon) and fails loudly if any are missing. It runs at three layers:
  build time (fails the image build), the whole-container launcher
  (studio_launch.sh refuses to start), and as a jupyter_server extension
  (refuses to serve JupyterLab).
- tests/studio/test_branding_guard.py: positive + per-marker negative coverage,
  plus a check that no base64/decoder obfuscation crept into the attribution.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1a5b58383

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread docker/unsloth_colab_compat.py Outdated
Comment on lines +45 to +46
if stripped.startswith("%%") and i > 0:
return [line] + skipped + lines[i + 1 :]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict the magic hoist to safe cell magics

When a Colab-form cell uses a non-Python/shell cell magic such as %%writefile, this branch moves the #@title/blank lines into the magic body. IPython then hands those preserved lines to the magic itself, so a titled %%writefile config.json cell writes the Colab metadata comment into the generated file and can corrupt helper/config outputs; only hoist for magics where the preserved comments are inert, or drop Colab form metadata instead of injecting it into every %% body.

Useful? React with 👍 / 👎.

Comment on lines +100 to +103
const selection = window.getSelection();
let output = closestOutput(selection?.anchorNode ?? null);
if (!output) {
output = lastPointerOutput;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore stale output selections outside outputs

When the last browser selection remains inside an output, clicking back on a command-mode cell or the file browser does not necessarily move window.getSelection().anchorNode, so this branch still finds the old output and prevents Ctrl/Cmd+A outside the output. After a user selects or clicks output text once, normal select-all shortcuts in the notebook/file browser can keep selecting that stale output instead; require the current event target or recent pointer location to be inside an output before taking over.

Useful? React with 👍 / 👎.

problems.append("labextension package.json is not valid JSON")

# 5. The built bundle still carries the visible attribution strings + plugins.
bundle = _bundle_text(paths["labext_static"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject disabled Unsloth labextension configs

In a derived image or mounted Jupyter config, adding disabledExtensions for unsloth-jupyterlab (or its plugin ids) leaves the static bundle on disk, so this check still passes while JupyterLab loads without the Unsloth logo/About/splash attribution; Dockerfile.studio only disables/locks the stock logo/splash plugins, not this extension. Since the guard is meant to refuse stripped/altered attribution, also reject disabled page_config entries for these plugins or lock them.

Useful? React with 👍 / 👎.

danielhanchen and others added 4 commits June 27, 2026 08:48
…jupyter-studio-ux

# Conflicts:
#	docker/Dockerfile
…anding guard)

- unsloth_colab_compat.py: only hoist a leading `%%` cell magic above the Colab
  `#@title` form for magics whose body runs as code (capture/time/bash/python/
  ...). Content magics (%%writefile, %%html, %%latex, ...) are left untouched so
  the form comment is never injected into the written file / rendered output.
- outputSelect.ts: stop trusting the text selection anchor to decide ownership
  of Ctrl/Cmd+A. A stale selection inside an output survives a click onto a
  command-mode cell or the file browser, which made select-all keep re-selecting
  the old output. Gate on the keystroke target or the last pointer-down (reset to
  null on any click outside an output) instead.
- unsloth_branding.py: also reject page_config.json that disables the Unsloth
  labextension or any of its plugin ids via disabledExtensions (dict or list
  form); that leaves the bundle on disk so the prior checks passed while the
  logo/About/splash attribution was stripped at load. Lock unsloth-jupyterlab in
  Dockerfile.studio as well (defense in depth), and add guard tests.
…t.ci formatting)

# Conflicts:
#	docker/jupyter/unsloth_branding.py
@danielhanchen

Copy link
Copy Markdown
Member Author

Thanks for the review. Addressed all three in 2482719 (the llama.cpp EXDEV fix the WSL2 report below also hits landed in the base PR #5748).

  1. Restrict the magic hoist to safe cell magics (unsloth_colab_compat.py). The hoist of a %% cell magic above a leading #@title form is now restricted to magics whose body runs as code (capture, time, timeit, prun, debug, bash, sh, shell, python/python2/python3, pypy). Content magics that treat the body as literal data (%%writefile, %%file, %%html, %%javascript, %%latex, %%markdown, %%svg, ...) are left untouched, so the Colab form comment is never moved into the written file or rendered output.

  2. Ignore stale output selections outside outputs (outputSelect.ts). Ctrl/Cmd+A ownership no longer keys off the text selection anchor. It uses the keystroke target, falling back to the last pointer-down, which is reset to null on any click outside an output. Clicking back onto a command-mode cell or the file browser now restores the normal select-all instead of re-selecting a previously selected output.

  3. Reject disabled Unsloth labextension configs (unsloth_branding.py). The guard now also rejects a page_config.json (the app-settings file or any labconfig/ file) that disables unsloth-jupyterlab or any of its plugin ids via disabledExtensions, in both the dict {id: true} and the list form. Dockerfile.studio additionally locks unsloth-jupyterlab. Stock plugins we disable ourselves (the stock logo/splash) are not flagged. Added guard tests for the new cases.

@danielhanchen

Copy link
Copy Markdown
Member Author

JupyterLab UX + branding verified in the running image

Captured from the published Docker image booted on ubuntu-latest (Playwright against the JupyterLab port), confirming the Colab-grade UX and the AGPLv3 branding this PR adds.

Login page: Unsloth mascot, "Sign in to JupyterLab", and the AGPLv3 footer with the source link.

JupyterLab login with AGPLv3 footer

Help > About "Unsloth Docker Studio": full attribution (Built by Unsloth, Licensed under the GNU AGPLv3, Source / Website / License links, Copyright 2026-Present the Unsloth team).

About Unsloth Docker Studio dialog

Lab shell: Unsloth top-bar logo, the "Unsloth Notebooks" launcher, and the full bundled notebook set (01-26) in the file tree.

Unsloth JupyterLab launcher

Spinning-logo splash on load:

Unsloth JupyterLab splash

Other checks that passed in the same run:

  • Branding integrity guard python -m unsloth_branding --verify: PASS ("Unsloth Docker Studio, AGPLv3").
  • Notebook auto-update (fake-edit test): edited one managed notebook, rolled the sync commit, and ran unsloth-sync-notebooks. Result line: 627 updated, 1 kept (your edits), 543 kept (only header/footer changed upstream). The user-edited notebook kept its edit; untouched bodies refreshed; header/footer-only upstream changes left the file byte-unchanged. Exactly the preserve-vs-refresh behaviour intended.

Pin the unsloth-jupyterlab npm deps to exact versions matching the baked
jupyterlab==4.6.0 (builder stays 4.5.9, its newest release) instead of floating
^/~ ranges, so the same commit always builds the same labextension bundle.
Also probe JupyterLab /login (not /api, which 403s behind a password hash) in
the Windows confirmation script.
…on into CI

unsloth_nb_view.parse_readme only reset the folder section on level-3
(###) headings. The notebooks README carries level-1 domain headers
(# AMD Notebooks, # Kaggle Notebooks) with their own nb/*.ipynb link
tables and no intervening ###, so those notebooks were mis-filed under
the previous stale section (all 148 AMD notebooks landed in Other
Notebooks on an --amd build). Reset on any heading level and strip a
leading emoji/symbol run so the domain notebooks get their own clean
folder.

Also run tests/validate_studio_features.py explicitly in the repo CPU
job. It is named validate_* (not test_*) so pytest never collected it,
which meant a regression in the notebook view, Colab compat, strip,
JupyterLab defaults or login branding failed CI only when run by hand.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

The exact pins introduced earlier (@jupyterlab/* 4.6.0, @lumino/widgets
2.8.0, @jupyterlab/builder 4.5.9) break the Dockerfile.studio
labext-builder stage. Exact-pinning the framework packages defeats
jlpm's (yarn classic) hoisting: transitive @jupyterlab deps request
caret ranges that resolve to newer patch releases (e.g. @jupyterlab/
notebook pulls @jupyterlab/cells ^4.6.0 -> a newer patch), so jlpm
installs a second nested copy alongside the exact top-level one. Two
copies of @jupyterlab/cells and @lumino/widgets in the tree produce
TS2345 "not assignable" errors (protected-member/identity mismatch)
and the build fails.

Caret ranges let jlpm collapse every @jupyterlab and @Lumino package
to a single hoisted copy, which is required for a JupyterLab prebuilt
(federated) extension: at runtime those packages are shared singletons
provided by the host JupyterLab, so the build-time versions only need
to type-check against one consistent tree, not match an exact runtime
patch. This is the version set the published image was built and
validated with end to end.

Verified by building the labext in isolation against the base image
(Node 20 + bundled jlpm): caret ranges build clean (webpack compiled
successfully); the exact pins fail with the duplicate-package TS
errors.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants