Repository: jupyterlab/jupyter-chat (language: python)

Issue:
Compatibility with Notebook
### Problem

The chat extension is not compatible with Notebook application currently.

### Proposed Solution

Fix the extension to make it work with Notebook



Discussion / hints:
This may be helpful: https://github.com/jupyterlab/jupyter-ai/pull/827
For traceability, the `@jupyter/react-components` (dependency of `jupyterlab-chat`) was missing form the shared scope of Notebook: https://github.com/jupyter/notebook/pull/7530

An other related issue, which leads to unexpected behavior with the side panel https://github.com/jupyter/notebook/issues/7534

This could be fixed here, but I would expect it to be fixed upstream instead.

Fix patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 656cf27..9ec0162 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -140,7 +140,7 @@ jobs:
     - name: Execute integration tests
       working-directory: ui-tests
       run: |
-        jlpm playwright test --retries=2
+        jlpm test --retries=2
 
     - name: Upload Playwright Test report
       if: always()
@@ -151,6 +151,63 @@ jobs:
           ui-tests/test-results
           ui-tests/playwright-report
 
+  integration-tests-notebook:
+    name: Integration tests notebook
+    needs: build_extension
+    runs-on: ubuntu-latest
+
+    env:
+      PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers
+
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v3
+
+    - name: Base Setup
+      uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+
+    - name: Download extension package
+      uses: actions/download-artifact@v3
+      with:
+        name: jupyterlab_chat-artifacts
+
+    - name: Install the extension
+      run: |
+        set -eux
+        python -m pip install "notebook>=7.0.0,<8" jupyterlab_chat*.whl
+
+    - name: Install dependencies
+      working-directory: ui-tests
+      env:
+        YARN_ENABLE_IMMUTABLE_INSTALLS: 0
+        PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
+      run: jlpm install
+
+    - name: Set up browser cache
+      uses: actions/cache@v3
+      with:
+        path: |
+          ${{ github.workspace }}/pw-browsers
+        key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }}
+
+    - name: Install browser
+      run: jlpm playwright install chromium
+      working-directory: ui-tests
+
+    - name: Execute integration tests
+      working-directory: ui-tests
+      run: |
+        jlpm test:notebook --retries=2
+
+    - name: Upload Playwright Test report
+      if: always()
+      uses: actions/upload-artifact@v3
+      with:
+        name: jupyterlab_chat-notebook-playwright-tests
+        path: |
+          ui-tests/test-results
+          ui-tests/playwright-report
+
   typing-tests:
     name: Typing test
     runs-on: ubuntu-latest
diff --git a/python/jupyterlab-chat/package.json b/python/jupyterlab-chat/package.json
index 9977369..3f28bff 100644
--- a/python/jupyterlab-chat/package.json
+++ b/python/jupyterlab-chat/package.json
@@ -57,6 +57,7 @@
     "watch:labextension": "jupyter labextension watch ."
   },
   "dependencies": {
+    "@jupyter-notebook/application": "^7.2.0",
     "@jupyter/docprovider": "^2.1.4",
     "@jupyter/ydoc": "^1.1.1",
     "@jupyterlab/application": "^4.2.0",
diff --git a/python/jupyterlab-chat/src/index.ts b/python/jupyterlab-chat/src/index.ts
index 4a25326..94a047e 100644
--- a/python/jupyterlab-chat/src/index.ts
+++ b/python/jupyterlab-chat/src/index.ts
@@ -3,6 +3,7 @@
  * Distributed under the terms of the Modified BSD License.
  */
 
+import { NotebookShell } from '@jupyter-notebook/application';
 import {
   ActiveCellManager,
   AutocompletionRegistry,
@@ -517,8 +518,24 @@ const chatCommands: JupyterFrontEndPlugin<void> = {
             }
 
             if (inSidePanel && chatPanel) {
-              // The chat is opened in the chat panel.
-              app.shell.activateById(chatPanel.id);
+              /**
+               * The chat is opened in the chat panel, ensure the chat panel is opened.
+               *
+               * NOTES: In Notebook application, the panel is collapsed when using the
+               * `activateById` when it is already opened.
+               * See https://github.com/jupyter/notebook/issues/7534
+               */
+              if (app.shell instanceof NotebookShell) {
+                const shell: NotebookShell = app.shell;
+                if (
+                  shell.leftHandler?.currentWidget?.id !== chatPanel.id ||
+                  !shell.leftHandler.isVisible
+                ) {
+                  shell.activateById(chatPanel.id);
+                }
+              } else {
+                app.shell.activateById(chatPanel.id);
+              }
 
               if (chatPanel.openIfExists(filepath)) {
                 return;
diff --git a/yarn.lock b/yarn.lock
index 60d0c1c..137b263 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2430,6 +2430,25 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@jupyter-notebook/application@npm:^7.2.0":
+  version: 7.2.1
+  resolution: "@jupyter-notebook/application@npm:7.2.1"
+  dependencies:
+    "@jupyterlab/application": ~4.2.0
+    "@jupyterlab/coreutils": ~6.2.0
+    "@jupyterlab/docregistry": ~4.2.0
+    "@jupyterlab/rendermime-interfaces": ~3.10.0
+    "@jupyterlab/ui-components": ~4.2.0
+    "@lumino/algorithm": ^2.0.1
+    "@lumino/coreutils": ^2.1.2
+    "@lumino/messaging": ^2.0.1
+    "@lumino/polling": ^2.1.2
+    "@lumino/signaling": ^2.1.2
+    "@lumino/widgets": ^2.3.2
+  checksum: 14114a2f7144e2a7bbf5e80476ef9f4b229bd9ca65d5620a97aba2e9d666cd4ae2a04cb4c96f6b0575e6383a930073945fae580eb441e3b3d48fc0ac61629376
+  languageName: node
+  linkType: hard
+
 "@jupyter/chat@^0.6.2, @jupyter/chat@workspace:packages/jupyter-chat":
   version: 0.0.0-use.local
   resolution: "@jupyter/chat@workspace:packages/jupyter-chat"
@@ -2546,7 +2565,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@jupyterlab/application@npm:^4.2.0":
+"@jupyterlab/application@npm:^4.2.0, @jupyterlab/application@npm:~4.2.0":
   version: 4.2.3
   resolution: "@jupyterlab/application@npm:4.2.3"
   dependencies:
@@ -2788,6 +2807,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@jupyterlab/coreutils@npm:~6.2.0":
+  version: 6.2.6
+  resolution: "@jupyterlab/coreutils@npm:6.2.6"
+  dependencies:
+    "@lumino/coreutils": ^2.1.2
+    "@lumino/disposable": ^2.1.2
+    "@lumino/signaling": ^2.1.2
+    minimist: ~1.2.0
+    path-browserify: ^1.0.0
+    url-parse: ~1.5.4
+  checksum: edb955a1d96a96546dce96a04329fe3ee38be301805337b9fd6ae7f21227625aab76f77ce5adacb80675831aed9b869525af787cbbbd64acf415ed67256e693a
+  languageName: node
+  linkType: hard
+
 "@jupyterlab/docmanager@npm:^4.2.3":
   version: 4.2.3
   resolution: "@jupyterlab/docmanager@npm:4.2.3"
@@ -2813,7 +2846,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@jupyterlab/docregistry@npm:^4.2.0, @jupyterlab/docregistry@npm:^4.2.3":
+"@jupyterlab/docregistry@npm:^4.2.0, @jupyterlab/docregistry@npm:^4.2.3, @jupyterlab/docregistry@npm:~4.2.0":
   version: 4.2.3
   resolution: "@jupyterlab/docregistry@npm:4.2.3"
   dependencies:
@@ -3045,6 +3078,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@jupyterlab/rendermime-interfaces@npm:~3.10.0":
+  version: 3.10.6
+  resolution: "@jupyterlab/rendermime-interfaces@npm:3.10.6"
+  dependencies:
+    "@lumino/coreutils": ^1.11.0 || ^2.1.2
+    "@lumino/widgets": ^1.37.2 || ^2.3.2
+  checksum: c3fed69c4df34ad203870522af9741662029b59aacfff07f19e4725750746d18195b4f41123fa1280c13aa8303e2a1a30697a50fc98bdba53bb4cbbbdda2d2b1
+  languageName: node
+  linkType: hard
+
 "@jupyterlab/rendermime@npm:^4.2.0, @jupyterlab/rendermime@npm:^4.2.3":
   version: 4.2.3
   resolution: "@jupyterlab/rendermime@npm:4.2.3"
@@ -3190,7 +3233,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@jupyterlab/ui-components@npm:^4.2.0, @jupyterlab/ui-components@npm:^4.2.3":
+"@jupyterlab/ui-components@npm:^4.2.0, @jupyterlab/ui-components@npm:^4.2.3, @jupyterlab/ui-components@npm:~4.2.0":
   version: 4.2.3
   resolution: "@jupyterlab/ui-components@npm:4.2.3"
   dependencies:
@@ -10020,6 +10063,7 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "jupyterlab-chat-extension@workspace:python/jupyterlab-chat"
   dependencies:
+    "@jupyter-notebook/application": ^7.2.0
     "@jupyter/docprovider": ^2.1.4
     "@jupyter/ydoc": ^1.1.1
     "@jupyterlab/application": ^4.2.0


Test patch:
diff --git a/ui-tests/jupyter_server_test_config_notebook.py b/ui-tests/jupyter_server_test_config_notebook.py
new file mode 100644
index 0000000..3d7d497
--- /dev/null
+++ b/ui-tests/jupyter_server_test_config_notebook.py
@@ -0,0 +1,27 @@
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+"""Server configuration for integration tests.
+!! Never use this configuration in production because it
+opens the server to the world and provide access to JupyterLab
+JavaScript objects through the global window variable.
+"""
+from pathlib import Path
+import jupyterlab
+
+c.ServerApp.port = 8888
+c.ServerApp.port_retries = 0
+c.ServerApp.open_browser = False
+
+c.ServerApp.token = ""
+c.ServerApp.password = ""
+c.ServerApp.disable_check_xsrf = True
+
+c.JupyterNotebookApp.expose_app_in_browser = True
+
+c.LabServerApp.extra_labextensions_path = str(Path(jupyterlab.__file__).parent / "galata")
+
+c.FileContentsManager.delete_to_trash = False
+
+# Uncomment to set server log level to debug level
+# c.ServerApp.log_level = "DEBUG"
diff --git a/ui-tests/package.json b/ui-tests/package.json
index 42797e4..ce3c3cc 100644
--- a/ui-tests/package.json
+++ b/ui-tests/package.json
@@ -5,7 +5,9 @@
   "private": true,
   "scripts": {
     "start": "jupyter lab --config jupyter_server_test_config.py",
+    "start:notebook": "jupyter notebook --config jupyter_server_test_config_notebook.py",
     "test": "jlpm playwright test",
+    "test:notebook": "jlpm playwright test --config=playwright.notebook.config.js",
     "test:update": "jlpm playwright test --update-snapshots"
   },
   "devDependencies": {
diff --git a/ui-tests/playwright.config.js b/ui-tests/playwright.config.js
index d22e0df..a392819 100644
--- a/ui-tests/playwright.config.js
+++ b/ui-tests/playwright.config.js
@@ -16,6 +16,7 @@ module.exports = {
     timeout: 120 * 1000,
     reuseExistingServer: !process.env.CI
   },
+  testIgnore: 'tests/notebook-application.spec.ts',
   use: {
     contextOptions: {
       permissions: ['clipboard-read', 'clipboard-write']
diff --git a/ui-tests/playwright.notebook.config.js b/ui-tests/playwright.notebook.config.js
new file mode 100644
index 0000000..28c7ef0
--- /dev/null
+++ b/ui-tests/playwright.notebook.config.js
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) Jupyter Development Team.
+ * Distributed under the terms of the Modified BSD License.
+ */
+
+/**
+ * Configuration for Playwright using default from @jupyterlab/galata
+ */
+const baseConfig = require('@jupyterlab/galata/lib/playwright-config');
+
+module.exports = {
+  ...baseConfig,
+  webServer: {
+    command: 'jlpm start:notebook',
+    url: 'http://localhost:8888/tree',
+    timeout: 120 * 1000,
+    reuseExistingServer: !process.env.CI
+  },
+  testMatch: 'tests/notebook-application.spec.ts'
+};
diff --git a/ui-tests/tests/notebook-application.spec.ts b/ui-tests/tests/notebook-application.spec.ts
new file mode 100644
index 0000000..36ceb6b
--- /dev/null
+++ b/ui-tests/tests/notebook-application.spec.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) Jupyter Development Team.
+ * Distributed under the terms of the Modified BSD License.
+ */
+
+import { expect, test as base } from '@jupyterlab/galata';
+
+export const test = base.extend({
+  waitForApplication: async ({ baseURL }, use, testInfo) => {
+    const waitIsReady = async (page): Promise<void> => {
+      await page.waitForSelector('#main-panel');
+    };
+    await use(waitIsReady);
+  },
+});
+
+test.use({
+  autoGoto: false,
+  appPath: '',
+  viewport: { width: 1024, height: 900 },
+});
+
+const NAME = 'my_chat';
+const FILENAME = `${NAME}.chat`;
+
+test.describe('#NotebookApp', () => {
+  test.beforeEach(async ({ page }) => {
+    // Create a chat file
+    await page.filebrowser.contents.uploadContent('{}', 'text', FILENAME);
+  });
+
+  test.afterEach(async ({ page }) => {
+    if (await page.filebrowser.contents.fileExists(FILENAME)) {
+      await page.filebrowser.contents.deleteFile(FILENAME);
+    }
+  });
+
+  test('Should open side panel and list existing chats', async ({ page }) => {
+    await page.goto('tree');
+    await page.menu.clickMenuItem('View>Left Sidebar>Show Jupyter Chat');
+    const panel = page.locator('#jp-left-stack');
+    await expect(panel).toBeVisible();
+    await expect(panel.locator('.jp-lab-chat-sidepanel')).toBeVisible();
+
+    const select = panel.locator(
+      '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-lab-chat-open select'
+    );
+
+    await expect(select.locator('option')).toHaveCount(2);
+    await expect(select.locator('option').last()).toHaveText(NAME);
+  });
+
+  test('Should open main panel in a separate tab', async ({ page, context }) => {
+    await page.goto('tree');
+
+    const pagePromise = context.waitForEvent('page');
+    await page.dblclick(`.jp-FileBrowser-listing >> text=${FILENAME}`);
+
+    const newPage = await pagePromise;
+    //wait for Load
+    // await newPage.waitForLoadState();
+
+    await expect(newPage.locator('.jp-MainAreaWidget')).toHaveClass(/jp-lab-chat-main-panel/);
+  });
+});