Quellcode durchsuchen

Make session dialogs configurable

lint

update notebook example

offer the option to keep no kernel

integrity

lint

fix up merge conflicts

lint

update tests

Update tests

Select a kernel if necessary in the console
Steven Silvester vor 5 Jahren
Ursprung
Commit
b7e393197b

+ 5 - 3
examples/notebook/src/commands.ts

@@ -2,6 +2,7 @@
  * Set up keyboard shortcuts & commands for notebook
  */
 import { CommandRegistry } from '@lumino/commands';
+import { sessionContextDialogs } from '@jupyterlab/apputils';
 import { CompletionHandler } from '@jupyterlab/completer';
 import { NotebookPanel, NotebookActions } from '@jupyterlab/notebook';
 import {
@@ -126,12 +127,13 @@ export const SetupCommands = (
   });
   commands.addCommand(cmdIds.restart, {
     label: 'Restart Kernel',
-    execute: async () =>
-      nbWidget.context.sessionContext.session?.kernel?.restart()
+    execute: () =>
+      sessionContextDialogs.restart(nbWidget.context.sessionContext)
   });
   commands.addCommand(cmdIds.switchKernel, {
     label: 'Switch Kernel',
-    execute: async () => nbWidget.context.sessionContext.selectKernel()
+    execute: () =>
+      sessionContextDialogs.selectKernel(nbWidget.context.sessionContext)
   });
   commands.addCommand(cmdIds.runAndAdvance, {
     label: 'Run and Advance',

+ 1 - 1
examples/notebook/src/index.ts

@@ -112,7 +112,7 @@ function createApp(manager: ServiceManager.IManager): void {
   const model = new CompleterModel();
   const completer = new Completer({ editor, model });
   const connector = new KernelConnector({
-    session: nbWidget.sessionContext.session
+    session: nbWidget.context.sessionContext.session
   });
   const handler = new CompletionHandler({ completer, connector });
 

+ 16 - 1
packages/apputils-extension/src/index.ts

@@ -13,10 +13,12 @@ import {
 import {
   Dialog,
   ICommandPalette,
+  ISessionContextDialogs,
   ISplashScreen,
   IWindowResolver,
   WindowResolver,
-  Printing
+  Printing,
+  sessionContextDialogs
 } from '@jupyterlab/apputils';
 
 import { URLExt } from '@jupyterlab/coreutils';
@@ -441,6 +443,18 @@ const state: JupyterFrontEndPlugin<IStateDB> = {
   }
 };
 
+/**
+ * The default session context dialogs extension.
+ */
+const sessionDialogs: JupyterFrontEndPlugin<ISessionContextDialogs> = {
+  id: '@jupyterlab/apputils-extension:sessionDialogs',
+  provides: ISessionContextDialogs,
+  autoStart: true,
+  activate: () => {
+    return sessionContextDialogs;
+  }
+};
+
 /**
  * Export the plugins as default.
  */
@@ -452,6 +466,7 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
   settingsPlugin,
   state,
   splash,
+  sessionDialogs,
   themesPlugin,
   themesPaletteMenuPlugin
 ];

+ 154 - 155
packages/apputils/src/sessioncontext.tsx

@@ -51,7 +51,7 @@ export interface ISessionContext extends IObservableDisposable {
    * #### Notes
    * This includes starting up an initial kernel if needed.
    */
-  initialize(): Promise<void>;
+  initialize(): Promise<boolean>;
 
   /**
    * Whether the session context is ready.
@@ -164,32 +164,37 @@ export interface ISessionContext extends IObservableDisposable {
   readonly name: string;
 
   /**
-   * Kill the kernel and shutdown the session.
-   *
-   * @returns A promise that resolves when the session is shut down.
+   * The previous kernel name.
    */
-  shutdown(): Promise<void>;
+  readonly prevKernelName: string;
 
   /**
-   * Use a UX to select a new kernel for the session.
+   * The session manager used by the session.
    */
-  selectKernel(): Promise<void>;
+  readonly sessionManager: Session.IManager;
+
+  /**
+   * The kernel spec manager
+   */
+  readonly specsManager: KernelSpec.IManager;
 
   /**
-   * Restart the kernel, with confirmation UX.
+   * Kill the kernel and shutdown the session.
    *
-   * @returns A promise that resolves with whether the kernel has restarted.
+   * @returns A promise that resolves when the session is shut down.
+   */
+  shutdown(): Promise<void>;
+
+  /**
+   * Change the kernel associated with the session.
    *
-   * #### Notes
-   * This method makes it easy to get a new kernel running in a session where
-   * we used to have a session running.
+   * @param options The optional kernel model parameters to use for the new kernel.
    *
-   * * If there is a running kernel, present a confirmation dialog.
-   * * If there is no kernel, start a kernel with the last-run kernel name.
-   * * If no kernel has ever been started, this is a no-op, and resolves with
-   *   `false`.
+   * @returns A promise that resolves with the new kernel connection.
    */
-  restart(): Promise<boolean>;
+  changeKernel(
+    options?: Partial<Kernel.IModel>
+  ): Promise<Kernel.IKernelConnection>;
 }
 
 /**
@@ -247,6 +252,29 @@ export namespace ISessionContext {
     | Kernel.ConnectionStatus
     | 'initializing'
     | '';
+
+  /**
+   * An interface for a session context dialog provider.
+   */
+  export interface IDialogs {
+    /**
+     * Select a kernel for the session.
+     */
+    selectKernel(session: ISessionContext): Promise<void>;
+
+    /**
+     * Restart the session context.
+     *
+     * @returns A promise that resolves with whether the kernel has restarted.
+     *
+     * #### Notes
+     * If there is a running kernel, present a dialog.
+     * If there is no kernel, we start a kernel with the last run
+     * kernel name and resolves with `true`. If no kernel has been started,
+     * this is a no-op, and resolves with `false`.
+     */
+    restart(session: ISessionContext): Promise<boolean>;
+  }
 }
 
 /**
@@ -454,7 +482,14 @@ export class SessionContext implements ISessionContext {
   }
 
   /**
-   * Test whether the client session is disposed.
+   * The name of the previously started kernel.
+   */
+  get prevKernelName(): string {
+    return this._prevKernelName;
+  }
+
+  /**
+   * Test whether the context is disposed.
    */
   get isDisposed(): boolean {
     return this._isDisposed;
@@ -512,18 +547,7 @@ export class SessionContext implements ISessionContext {
   }
 
   /**
-   * Select a kernel for the session.
-   */
-  async selectKernel(): Promise<void> {
-    await this.initialize();
-    if (this.isDisposed) {
-      throw new Error('Disposed');
-    }
-    return this._selectKernel(true);
-  }
-
-  /**
-   * Shut down the session and kernel.
+   * Kill the kernel and shutdown the session.
    *
    * @returns A promise that resolves when the session is shut down.
    */
@@ -531,38 +555,11 @@ export class SessionContext implements ISessionContext {
     return this._session?.shutdown();
   }
 
-  /**
-   * Restart the session.
-   *
-   * @returns A promise that resolves with whether the kernel has restarted.
-   *
-   * #### Notes
-   * If there is a running kernel, present a dialog.
-   * If there is no kernel, we start a kernel with the last run
-   * kernel name and resolves with `true`.
-   */
-  async restart(): Promise<boolean> {
-    await this.initialize();
-    if (this.isDisposed) {
-      throw new Error('session already disposed');
-    }
-    let kernel = this.session?.kernel;
-    if (kernel) {
-      return SessionContext.restartKernel(kernel);
-    }
-
-    if (this._prevKernelName) {
-      await this.changeKernel({ name: this._prevKernelName });
-      return true;
-    }
-
-    // Bail if there is no previous kernel to start.
-    throw new Error('No kernel to restart');
-  }
-
   /**
    * Initialize the session.
    *
+   * @returns Whether we need to ask the user to select a kernel.
+   *
    * #### Notes
    * If a server session exists on the current path, we will connect to it.
    * If preferences include disabling `canStart` or `shouldStart`, no
@@ -571,9 +568,10 @@ export class SessionContext implements ISessionContext {
    * If a default kernel is available, we connect to it.
    * Otherwise we ask the user to select a kernel.
    */
-  async initialize(): Promise<void> {
+  async initialize(): Promise<boolean> {
     if (this._initializing || this._isReady) {
-      return this._ready.promise;
+      await this._ready.promise;
+      return false;
     }
     this._initializing = true;
     let manager = this.sessionManager;
@@ -590,15 +588,18 @@ export class SessionContext implements ISessionContext {
         return Promise.reject(err);
       }
     }
-    await this._startIfNecessary();
+    const needsSelection = await this._startIfNecessary();
     this._isReady = true;
     this._ready.resolve(undefined);
+    return needsSelection;
   }
 
   /**
    * Start the session if necessary.
+   *
+   * @returns Whether to ask the user to pick a kernel.
    */
-  private async _startIfNecessary(): Promise<void> {
+  private async _startIfNecessary(): Promise<boolean> {
     let preference = this.kernelPreference;
     if (
       this.isDisposed ||
@@ -607,7 +608,7 @@ export class SessionContext implements ISessionContext {
       preference.canStart === false
     ) {
       // Not necessary to start a kernel
-      return;
+      return false;
     }
 
     let options: Partial<Kernel.IModel> | undefined;
@@ -627,14 +628,14 @@ export class SessionContext implements ISessionContext {
     if (options) {
       try {
         await this._changeKernel(options);
-        return;
+        return false;
       } catch (err) {
         /* no-op */
       }
     }
 
     // Always fall back to selecting a kernel
-    await this._selectKernel(false);
+    return true;
   }
 
   /**
@@ -659,43 +660,6 @@ export class SessionContext implements ISessionContext {
     }
   }
 
-  /**
-   * Select a kernel.
-   *
-   * @param cancelable: whether the dialog should have a cancel button.
-   */
-  private async _selectKernel(cancelable: boolean): Promise<void> {
-    if (this.isDisposed) {
-      return Promise.resolve();
-    }
-    const buttons = cancelable
-      ? [Dialog.cancelButton(), Dialog.okButton({ label: 'Select' })]
-      : [
-          Dialog.cancelButton({ label: 'No Kernel' }),
-          Dialog.okButton({ label: 'Select' })
-        ];
-
-    let dialog = (this._dialog = new Dialog({
-      title: 'Select Kernel',
-      body: new Private.KernelSelector(this),
-      buttons
-    }));
-
-    let result = await dialog.launch();
-    dialog.dispose();
-    this._dialog = null;
-
-    if (this.isDisposed || !result.button.accept) {
-      return;
-    }
-    let model = result.value;
-    if (model === null && this._session) {
-      await this.shutdown();
-    } else if (model) {
-      await this._changeKernel(model);
-    }
-  }
-
   /**
    * Start a session and set up its signals.
    */
@@ -969,32 +933,6 @@ export namespace SessionContext {
     setBusy?: () => IDisposable;
   }
 
-  /**
-   * Restart a kernel if the user accepts the risk.
-   *
-   * Returns a promise resolving with whether the kernel was restarted.
-   */
-  export async function restartKernel(
-    kernel: Kernel.IKernelConnection
-  ): Promise<boolean> {
-    let restartBtn = Dialog.warnButton({ label: 'Restart' });
-    const result = await showDialog({
-      title: 'Restart Kernel?',
-      body:
-        'Do you want to restart the current kernel? All variables will be lost.',
-      buttons: [Dialog.cancelButton(), restartBtn]
-    });
-
-    if (kernel.isDisposed) {
-      return false;
-    }
-    if (result.button.accept) {
-      await kernel.restart();
-      return true;
-    }
-    return false;
-  }
-
   /**
    * An interface for populating a kernel selector.
    */
@@ -1021,34 +959,95 @@ export namespace SessionContext {
   export function getDefaultKernel(options: IKernelSearch): string | null {
     return Private.getDefaultKernel(options);
   }
+}
 
+/**
+ * The default implementation of the client sesison dialog provider.
+ */
+export const sessionContextDialogs: ISessionContext.IDialogs = {
   /**
-   * Populate a kernel dropdown list.
-   *
-   * @param node - The node to populate.
+   * Select a kernel for the session.
+   */
+  async selectKernel(sessionContext: ISessionContext): Promise<void> {
+    const session = sessionContext.session;
+    if (session.isDisposed) {
+      return Promise.resolve();
+    }
+    // If there is no existing kernel, offer the option
+    // to keep no kernel.
+    let label = 'Cancel';
+    if (!session.kernel) {
+      label = 'No Kernel';
+    }
+    const buttons = [
+      Dialog.cancelButton({ label }),
+      Dialog.okButton({ label: 'Select' })
+    ];
+
+    let dialog = new Dialog({
+      title: 'Select Kernel',
+      body: new Private.KernelSelector(sessionContext),
+      buttons
+    });
+
+    const result = await dialog.launch();
+    if (session.isDisposed || !result.button.accept) {
+      return;
+    }
+    let model = result.value;
+    if (model === null && session.kernel) {
+      return session.shutdown();
+    }
+    if (model) {
+      await session.changeKernel(model);
+    }
+  },
+
+  /**
+   * Restart the session.
    *
-   * @param options - The options used to populate the kernels.
+   * @returns A promise that resolves with whether the kernel has restarted.
    *
    * #### Notes
-   * Populates the list with separated sections:
-   *   - Kernels matching the preferred language (display names).
-   *   - "None" signifying no kernel.
-   *   - The remaining kernels.
-   *   - Sessions matching the preferred language (file names).
-   *   - The remaining sessions.
-   * If no preferred language is given or no kernels are found using
-   * the preferred language, the default kernel is used in the first
-   * section.  Kernels are sorted by display name.  Sessions display the
-   * base name of the file with an ellipsis overflow and a tooltip with
-   * the explicit session information.
+   * If there is a running kernel, present a dialog.
+   * If there is no kernel, we start a kernel with the last run
+   * kernel name and resolves with `true`.
    */
-  export function populateKernelSelect(
-    node: HTMLSelectElement,
-    options: IKernelSearch
-  ): void {
-    return Private.populateKernelSelect(node, options);
+  async restart(sessionContext: ISessionContext): Promise<boolean> {
+    await sessionContext.initialize();
+    if (sessionContext.isDisposed) {
+      throw new Error('session already disposed');
+    }
+    let kernel = sessionContext.session?.kernel;
+    if (!kernel && sessionContext.prevKernelName) {
+      await sessionContext.changeKernel({
+        name: sessionContext.prevKernelName
+      });
+      return true;
+    }
+    // Bail if there is no previous kernel to start.
+    if (!kernel) {
+      throw new Error('No kernel to restart');
+    }
+
+    let restartBtn = Dialog.warnButton({ label: 'Restart' });
+    const result = await showDialog({
+      title: 'Restart Kernel?',
+      body:
+        'Do you want to restart the current kernel? All variables will be lost.',
+      buttons: [Dialog.cancelButton(), restartBtn]
+    });
+
+    if (kernel.isDisposed) {
+      return false;
+    }
+    if (result.button.accept) {
+      await kernel.restart();
+      return true;
+    }
+    return false;
   }
-}
+};
 
 /**
  * The namespace for module private data.
@@ -1061,7 +1060,7 @@ namespace Private {
     /**
      * Create a new kernel selector widget.
      */
-    constructor(sessionContext: SessionContext) {
+    constructor(sessionContext: ISessionContext) {
       super({ node: createSelectorNode(sessionContext) });
     }
 
@@ -1077,7 +1076,7 @@ namespace Private {
   /**
    * Create a node for a kernel selector widget.
    */
-  function createSelectorNode(sessionContext: SessionContext) {
+  function createSelectorNode(sessionContext: ISessionContext) {
     // Create the dialog body.
     let body = document.createElement('div');
     let text = document.createElement('label');
@@ -1086,7 +1085,7 @@ namespace Private {
 
     let options = getKernelSearch(sessionContext);
     let selector = document.createElement('select');
-    SessionContext.populateKernelSelect(selector, options);
+    populateKernelSelect(selector, options);
     body.appendChild(selector);
     return body;
   }
@@ -1306,10 +1305,10 @@ namespace Private {
   }
 
   /**
-   * Get the kernel search options given a session context.
+   * Get the kernel search options given a session context and session manager.
    */
   function getKernelSearch(
-    sessionContext: SessionContext
+    sessionContext: ISessionContext
   ): SessionContext.IKernelSearch {
     return {
       specs: sessionContext.specsManager.specs,

+ 16 - 0
packages/apputils/src/tokens.ts

@@ -9,6 +9,22 @@ import { IDisposable } from '@lumino/disposable';
 
 import { ISignal } from '@lumino/signaling';
 
+import { ISessionContext } from './sessioncontext';
+
+/**
+ * An interface for the session context dialogs.
+ */
+export interface ISessionContextDialogs extends ISessionContext.IDialogs {}
+
+/* tslint:disable */
+/**
+ * The session context dialogs token.
+ */
+export const ISessionContextDialogs = new Token<ISessionContext.IDialogs>(
+  '@jupyterlab/apputils:ISessionContextDialogs'
+);
+/* tslint:enable */
+
 /* tslint:disable */
 /**
  * The theme manager token.

+ 17 - 8
packages/apputils/src/toolbar.tsx

@@ -15,7 +15,7 @@ import { AttachedProperty } from '@lumino/properties';
 
 import { PanelLayout, Widget } from '@lumino/widgets';
 
-import { ISessionContext } from './sessioncontext';
+import { ISessionContext, sessionContextDialogs } from './sessioncontext';
 
 import * as React from 'react';
 import { ReadonlyJSONObject } from '@lumino/coreutils';
@@ -383,11 +383,14 @@ export namespace Toolbar {
   /**
    * Create a restart toolbar item.
    */
-  export function createRestartButton(sessionContext: ISessionContext): Widget {
+  export function createRestartButton(
+    sessionContext: ISessionContext,
+    dialogs?: ISessionContext.IDialogs
+  ): Widget {
     return new ToolbarButton({
       iconClassName: 'jp-RefreshIcon',
       onClick: () => {
-        void sessionContext.restart();
+        void (dialogs || sessionContextDialogs).restart(sessionContext);
       },
       tooltip: 'Restart the kernel'
     });
@@ -412,10 +415,14 @@ export namespace Toolbar {
    * handle a change in context or kernel.
    */
   export function createKernelNameItem(
-    sessionContext: ISessionContext
+    sessionContext: ISessionContext,
+    dialogs?: ISessionContext.IDialogs
   ): Widget {
     const el = ReactWidget.create(
-      <Private.KernelNameComponent sessionContext={sessionContext} />
+      <Private.KernelNameComponent
+        sessionContext={sessionContext}
+        dialogs={dialogs || sessionContextDialogs}
+      />
     );
     el.addClass('jp-KernelName');
     return el;
@@ -661,6 +668,7 @@ namespace Private {
      */
     export interface IProps {
       sessionContext: ISessionContext;
+      dialogs: ISessionContext.IDialogs;
     }
   }
 
@@ -672,6 +680,9 @@ namespace Private {
    */
 
   export function KernelNameComponent(props: KernelNameComponent.IProps) {
+    const callback = () => {
+      void props.dialogs.selectKernel(props.sessionContext);
+    };
     return (
       <UseSignal
         signal={props.sessionContext.kernelChanged}
@@ -680,9 +691,7 @@ namespace Private {
         {sessionContext => (
           <ToolbarButtonComponent
             className={TOOLBAR_KERNEL_NAME_CLASS}
-            onClick={props.sessionContext.selectKernel.bind(
-              props.sessionContext
-            )}
+            onClick={callback}
             tooltip={'Switch kernel'}
             label={sessionContext?.kernelDisplayName}
           />

+ 13 - 8
packages/console-extension/src/index.ts

@@ -11,8 +11,10 @@ import {
 import {
   Dialog,
   ISessionContext,
+  ISessionContextDialogs,
   ICommandPalette,
   MainAreaWidget,
+  sessionContextDialogs,
   showDialog,
   WidgetTracker
 } from '@jupyterlab/apputils';
@@ -105,7 +107,7 @@ const tracker: JupyterFrontEndPlugin<IConsoleTracker> = {
     IRenderMimeRegistry,
     ISettingRegistry
   ],
-  optional: [ILauncher, ILabStatus],
+  optional: [ILauncher, ILabStatus, ISessionContextDialogs],
   activate: activateConsole,
   autoStart: true
 };
@@ -144,11 +146,13 @@ async function activateConsole(
   rendermime: IRenderMimeRegistry,
   settingRegistry: ISettingRegistry,
   launcher: ILauncher | null,
-  status: ILabStatus | null
+  status: ILabStatus | null,
+  sessionDialogs: ISessionContextDialogs | null
 ): Promise<IConsoleTracker> {
   const manager = app.serviceManager;
   const { commands, shell } = app;
   const category = 'Console';
+  sessionDialogs = sessionDialogs || sessionContextDialogs;
 
   // Create a widget tracker for all console panels.
   const tracker = new WidgetTracker<MainAreaWidget<ConsolePanel>>({
@@ -447,7 +451,7 @@ async function activateConsole(
       if (!current) {
         return;
       }
-      return current.console.sessionContext.restart();
+      return sessionDialogs.restart(current.console.sessionContext);
     },
     isEnabled
   });
@@ -504,7 +508,7 @@ async function activateConsole(
       if (!current) {
         return;
       }
-      return current.console.sessionContext.selectKernel();
+      return sessionDialogs.selectKernel(current.console.sessionContext);
     },
     isEnabled
   });
@@ -560,10 +564,11 @@ async function activateConsole(
       return Promise.resolve(void 0);
     },
     noun: 'Console',
-    restartKernel: current => current.content.console.sessionContext.restart(),
+    restartKernel: current =>
+      sessionDialogs.restart(current.content.console.sessionContext),
     restartKernelAndClear: current => {
-      return current.content.console.sessionContext
-        .restart()
+      return sessionDialogs
+        .restart(current.content.console.sessionContext)
         .then(restarted => {
           if (restarted) {
             current.content.console.clear();
@@ -572,7 +577,7 @@ async function activateConsole(
         });
     },
     changeKernel: current =>
-      current.content.console.sessionContext.selectKernel(),
+      sessionDialogs.selectKernel(current.content.console.sessionContext),
     shutdownKernel: current => current.content.console.sessionContext.shutdown()
   } as IKernelMenu.IKernelUser<MainAreaWidget<ConsolePanel>>);
 

+ 9 - 2
packages/console/src/panel.ts

@@ -1,7 +1,11 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import { ISessionContext, SessionContext } from '@jupyterlab/apputils';
+import {
+  ISessionContext,
+  SessionContext,
+  sessionContextDialogs
+} from '@jupyterlab/apputils';
 
 import { IEditorMimeTypeService } from '@jupyterlab/codeeditor';
 
@@ -83,7 +87,10 @@ export class ConsolePanel extends Panel {
     });
     this.addWidget(this.console);
 
-    void sessionContext.initialize().then(() => {
+    void sessionContext.initialize().then(async value => {
+      if (value) {
+        await sessionContextDialogs.selectKernel(sessionContext);
+      }
       this._connected = new Date();
       this._updateTitle();
     });

+ 13 - 4
packages/docmanager-extension/src/index.ts

@@ -12,7 +12,8 @@ import {
   showDialog,
   showErrorMessage,
   Dialog,
-  ICommandPalette
+  ICommandPalette,
+  ISessionContextDialogs
 } from '@jupyterlab/apputils';
 
 import { IChangedArgs, Time } from '@jupyterlab/coreutils';
@@ -85,14 +86,21 @@ const docManagerPlugin: JupyterFrontEndPlugin<IDocumentManager> = {
   id: pluginId,
   provides: IDocumentManager,
   requires: [ISettingRegistry],
-  optional: [ILabStatus, ICommandPalette, ILabShell, IMainMenu],
+  optional: [
+    ILabStatus,
+    ICommandPalette,
+    ILabShell,
+    IMainMenu,
+    ISessionContextDialogs
+  ],
   activate: (
     app: JupyterFrontEnd,
     settingRegistry: ISettingRegistry,
     status: ILabStatus | null,
     palette: ICommandPalette | null,
     labShell: ILabShell | null,
-    mainMenu: IMainMenu | null
+    mainMenu: IMainMenu | null,
+    sessionDialogs: ISessionContextDialogs | null
   ): IDocumentManager => {
     const { shell } = app;
     const manager = app.serviceManager;
@@ -128,7 +136,8 @@ const docManagerPlugin: JupyterFrontEndPlugin<IDocumentManager> = {
       manager,
       opener,
       when,
-      setBusy: (status && (() => status.setBusy())) ?? undefined
+      setBusy: (status && (() => status.setBusy())) ?? undefined,
+      sessionDialogs
     });
 
     // Register the file operations commands.

+ 10 - 2
packages/docmanager/src/manager.ts

@@ -1,7 +1,7 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import { ISessionContext } from '@jupyterlab/apputils';
+import { ISessionContext, sessionContextDialogs } from '@jupyterlab/apputils';
 
 import { PathExt } from '@jupyterlab/coreutils';
 
@@ -48,6 +48,7 @@ export class DocumentManager implements IDocumentManager {
   constructor(options: DocumentManager.IOptions) {
     this.registry = options.registry;
     this.services = options.manager;
+    this._dialogs = options.sessionDialogs || sessionContextDialogs;
 
     this._opener = options.opener;
     this._when = options.when || options.manager.ready;
@@ -473,7 +474,8 @@ export class DocumentManager implements IDocumentManager {
       path,
       kernelPreference,
       modelDBFactory,
-      setBusy: this._setBusy
+      setBusy: this._setBusy,
+      sessionDialogs: this._dialogs
     });
     let handler = new SaveHandler({
       context,
@@ -598,6 +600,7 @@ export class DocumentManager implements IDocumentManager {
   private _autosaveInterval = 120;
   private _when: Promise<void>;
   private _setBusy: (() => IDisposable) | undefined;
+  private _dialogs: ISessionContext.IDialogs;
 }
 
 /**
@@ -632,6 +635,11 @@ export namespace DocumentManager {
      * A function called when a kernel is busy.
      */
     setBusy?: () => IDisposable;
+
+    /**
+     * The provider for session dialogs.
+     */
+    sessionDialogs?: ISessionContext.IDialogs;
   }
 
   /**

+ 14 - 2
packages/docregistry/src/context.ts

@@ -20,7 +20,8 @@ import {
   SessionContext,
   Dialog,
   ISessionContext,
-  showErrorMessage
+  showErrorMessage,
+  sessionContextDialogs
 } from '@jupyterlab/apputils';
 
 import { PathExt } from '@jupyterlab/coreutils';
@@ -46,6 +47,7 @@ export class Context<T extends DocumentRegistry.IModel>
   constructor(options: Context.IOptions<T>) {
     let manager = (this._manager = options.manager);
     this._factory = options.factory;
+    this._dialogs = options.sessionDialogs || sessionContextDialogs;
     this._opener = options.opener || Private.noOp;
     this._path = this._manager.contents.normalize(options.path);
     const localPath = this._manager.contents.localPath(this._path);
@@ -477,7 +479,11 @@ export class Context<T extends DocumentRegistry.IModel>
       // Note: we don't wait on the session to initialize
       // so that the user can be shown the content before
       // any kernel has started.
-      void this.sessionContext.initialize();
+      void this.sessionContext.initialize().then(shouldSelect => {
+        if (shouldSelect) {
+          void this._dialogs.selectKernel(this.sessionContext);
+        }
+      });
     });
   }
 
@@ -802,6 +808,7 @@ export class Context<T extends DocumentRegistry.IModel>
   private _fileChanged = new Signal<this, Contents.IModel>(this);
   private _saveState = new Signal<this, DocumentRegistry.SaveState>(this);
   private _disposed = new Signal<this, void>(this);
+  private _dialogs: ISessionContext.IDialogs;
 }
 
 /**
@@ -846,6 +853,11 @@ export namespace Context {
      * A function to call when the kernel is busy.
      */
     setBusy?: () => IDisposable;
+
+    /**
+     * The dialogs used for the session context.
+     */
+    sessionDialogs?: ISessionContext.IDialogs;
   }
 }
 

+ 25 - 10
packages/fileeditor-extension/src/commands.ts

@@ -3,7 +3,12 @@
 
 import { JupyterFrontEnd } from '@jupyterlab/application';
 
-import { ICommandPalette, WidgetTracker } from '@jupyterlab/apputils';
+import {
+  ICommandPalette,
+  WidgetTracker,
+  ISessionContextDialogs,
+  sessionContextDialogs
+} from '@jupyterlab/apputils';
 
 import { CodeEditor } from '@jupyterlab/codeeditor';
 
@@ -698,7 +703,8 @@ export namespace Commands {
     menu: IMainMenu,
     commands: CommandRegistry,
     tracker: WidgetTracker<IDocumentWidget<FileEditor>>,
-    consoleTracker: IConsoleTracker
+    consoleTracker: IConsoleTracker,
+    sessionDialogs?: ISessionContextDialogs
   ) {
     // Add the editing commands to the settings menu.
     addEditingCommandsToSettingsMenu(menu, commands);
@@ -719,7 +725,13 @@ export namespace Commands {
     addConsoleCreatorToFileMenu(menu, commands, tracker);
 
     // Add a code runner to the run menu.
-    addCodeRunnersToRunMenu(menu, commands, tracker, consoleTracker);
+    addCodeRunnersToRunMenu(
+      menu,
+      commands,
+      tracker,
+      consoleTracker,
+      sessionDialogs
+    );
   }
 
   /**
@@ -857,7 +869,8 @@ export namespace Commands {
     menu: IMainMenu,
     commands: CommandRegistry,
     tracker: WidgetTracker<IDocumentWidget<FileEditor>>,
-    consoleTracker: IConsoleTracker
+    consoleTracker: IConsoleTracker,
+    sessionDialogs: ISessionContextDialogs | null
   ) {
     menu.runMenu.codeRunners.add({
       tracker,
@@ -875,12 +888,14 @@ export namespace Commands {
             widget.content.sessionContext.session?.path === current.context.path
         );
         if (widget) {
-          return widget.content.sessionContext.restart().then(restarted => {
-            if (restarted) {
-              void commands.execute(CommandIDs.runAllCode);
-            }
-            return restarted;
-          });
+          return (sessionDialogs || sessionContextDialogs)
+            .restart(widget.content.sessionContext)
+            .then(restarted => {
+              if (restarted) {
+                void commands.execute(CommandIDs.runAllCode);
+              }
+              return restarted;
+            });
         }
       }
     } as IRunMenu.ICodeRunner<IDocumentWidget<FileEditor>>);

+ 21 - 4
packages/fileeditor-extension/src/index.ts

@@ -7,7 +7,11 @@ import {
   JupyterFrontEndPlugin
 } from '@jupyterlab/application';
 
-import { ICommandPalette, WidgetTracker } from '@jupyterlab/apputils';
+import {
+  ICommandPalette,
+  WidgetTracker,
+  ISessionContextDialogs
+} from '@jupyterlab/apputils';
 
 import { CodeEditor, IEditorServices } from '@jupyterlab/codeeditor';
 
@@ -52,7 +56,13 @@ const plugin: JupyterFrontEndPlugin<IEditorTracker> = {
     IFileBrowserFactory,
     ISettingRegistry
   ],
-  optional: [ICommandPalette, ILauncher, IMainMenu, ILayoutRestorer],
+  optional: [
+    ICommandPalette,
+    ILauncher,
+    IMainMenu,
+    ILayoutRestorer,
+    ISessionContextDialogs
+  ],
   provides: IEditorTracker,
   autoStart: true
 };
@@ -148,7 +158,8 @@ function activate(
   palette: ICommandPalette | null,
   launcher: ILauncher | null,
   menu: IMainMenu | null,
-  restorer: ILayoutRestorer | null
+  restorer: ILayoutRestorer | null,
+  sessionDialogs: ISessionContextDialogs | null
 ): IEditorTracker {
   const id = plugin.id;
   const namespace = 'editor';
@@ -229,7 +240,13 @@ function activate(
   }
 
   if (menu) {
-    Commands.addMenuItems(menu, commands, tracker, consoleTracker);
+    Commands.addMenuItems(
+      menu,
+      commands,
+      tracker,
+      consoleTracker,
+      sessionDialogs
+    );
   }
 
   Commands.addContextMenuItems(app);

+ 33 - 15
packages/notebook-extension/src/index.ts

@@ -11,9 +11,11 @@ import {
 import {
   Dialog,
   ICommandPalette,
+  ISessionContextDialogs,
   MainAreaWidget,
   showDialog,
-  WidgetTracker
+  WidgetTracker,
+  sessionContextDialogs
 } from '@jupyterlab/apputils';
 
 import { CodeCell } from '@jupyterlab/cells';
@@ -277,7 +279,8 @@ const trackerPlugin: JupyterFrontEndPlugin<INotebookTracker> = {
     ILauncher,
     ILayoutRestorer,
     IMainMenu,
-    ISettingRegistry
+    ISettingRegistry,
+    ISessionContextDialogs
   ],
   activate: activateNotebookHandler,
   autoStart: true
@@ -503,7 +506,8 @@ function activateNotebookHandler(
   launcher: ILauncher | null,
   restorer: ILayoutRestorer | null,
   mainMenu: IMainMenu | null,
-  settingRegistry: ISettingRegistry | null
+  settingRegistry: ISettingRegistry | null,
+  sessionDialogs: ISessionContextDialogs
 ): INotebookTracker {
   const services = app.serviceManager;
 
@@ -551,7 +555,14 @@ function activateNotebookHandler(
   registry.addModelFactory(new NotebookModelFactory({}));
   registry.addWidgetFactory(factory);
 
-  addCommands(app, docManager, services, tracker, clonedOutputs);
+  addCommands(
+    app,
+    docManager,
+    services,
+    tracker,
+    clonedOutputs,
+    sessionDialogs
+  );
   if (palette) {
     populatePalette(palette, services);
   }
@@ -637,7 +648,7 @@ function activateNotebookHandler(
 
   // Add main menu notebook menu.
   if (mainMenu) {
-    populateMenus(app, mainMenu, tracker, services, palette);
+    populateMenus(app, mainMenu, tracker, services, palette, sessionDialogs);
   }
 
   // Utility function to create a new notebook.
@@ -856,10 +867,13 @@ function addCommands(
   docManager: IDocumentManager,
   services: ServiceManager,
   tracker: NotebookTracker,
-  clonedOutputs: WidgetTracker<MainAreaWidget>
+  clonedOutputs: WidgetTracker<MainAreaWidget>,
+  sessionDialogs: ISessionContextDialogs | null
 ): void {
   const { commands, shell } = app;
 
+  sessionDialogs = sessionDialogs || sessionContextDialogs;
+
   // Get the current widget and activate unless the args specify otherwise.
   function getCurrent(args: ReadonlyPartialJSONObject): NotebookPanel | null {
     const widget = tracker.currentWidget;
@@ -1133,7 +1147,7 @@ function addCommands(
       const current = getCurrent(args);
 
       if (current) {
-        return current.sessionContext.restart();
+        return sessionDialogs.restart(current.sessionContext);
       }
     },
     isEnabled
@@ -1219,7 +1233,7 @@ function addCommands(
       if (current) {
         const { content, sessionContext } = current;
 
-        return sessionContext.restart().then(() => {
+        return sessionDialogs.restart(sessionContext).then(() => {
           NotebookActions.clearAllOutputs(content);
         });
       }
@@ -1234,7 +1248,7 @@ function addCommands(
       if (current) {
         const { context, content, sessionContext } = current;
 
-        return sessionContext.restart().then(restarted => {
+        return sessionDialogs.restart(sessionContext).then(restarted => {
           if (restarted) {
             void NotebookActions.runAll(content, context.sessionContext);
           }
@@ -1597,7 +1611,7 @@ function addCommands(
       const current = getCurrent(args);
 
       if (current) {
-        return current.context.sessionContext.selectKernel();
+        return sessionDialogs.selectKernel(current.context.sessionContext);
       }
     },
     isEnabled
@@ -1975,10 +1989,13 @@ function populateMenus(
   mainMenu: IMainMenu,
   tracker: INotebookTracker,
   services: ServiceManager,
-  palette: ICommandPalette | null
+  palette: ICommandPalette | null,
+  sessionDialogs: ISessionContextDialogs | null
 ): void {
   let { commands } = app;
 
+  sessionDialogs = sessionDialogs || sessionContextDialogs;
+
   // Add undo/redo hooks to the edit menu.
   mainMenu.editMenu.undoers.add({
     tracker,
@@ -2075,16 +2092,17 @@ function populateMenus(
       return Promise.resolve(void 0);
     },
     noun: 'All Outputs',
-    restartKernel: current => current.sessionContext.restart(),
+    restartKernel: current => sessionDialogs.restart(current.sessionContext),
     restartKernelAndClear: current => {
-      return current.sessionContext.restart().then(restarted => {
+      return sessionDialogs.restart(current.sessionContext).then(restarted => {
         if (restarted) {
           NotebookActions.clearAllOutputs(current.content);
         }
         return restarted;
       });
     },
-    changeKernel: current => current.sessionContext.selectKernel(),
+    changeKernel: current =>
+      sessionDialogs.selectKernel(current.sessionContext),
     shutdownKernel: current => current.sessionContext.shutdown()
   } as IKernelMenu.IKernelUser<NotebookPanel>);
 
@@ -2151,7 +2169,7 @@ function populateMenus(
     },
     restartAndRunAll: current => {
       const { context, content } = current;
-      return context.sessionContext.restart().then(restarted => {
+      return sessionDialogs.restart(context.sessionContext).then(restarted => {
         if (restarted) {
           void NotebookActions.runAll(content, context.sessionContext);
         }

+ 12 - 4
packages/notebook/src/default-toolbar.tsx

@@ -17,7 +17,8 @@ import {
   UseSignal,
   addToolbarButtonClass,
   ReactWidget,
-  ToolbarButton
+  ToolbarButton,
+  ISessionContextDialogs
 } from '@jupyterlab/apputils';
 
 import * as nbformat from '@jupyterlab/nbformat';
@@ -197,7 +198,8 @@ export namespace ToolbarItems {
    * Get the default toolbar items for panel
    */
   export function getDefaultItems(
-    panel: NotebookPanel
+    panel: NotebookPanel,
+    sessionDialogs?: ISessionContextDialogs
   ): DocumentRegistry.IToolbarItem[] {
     return [
       { name: 'save', widget: createSaveButton(panel) },
@@ -212,13 +214,19 @@ export namespace ToolbarItems {
       },
       {
         name: 'restart',
-        widget: Toolbar.createRestartButton(panel.sessionContext)
+        widget: Toolbar.createRestartButton(
+          panel.sessionContext,
+          sessionDialogs
+        )
       },
       { name: 'cellType', widget: createCellTypeItem(panel) },
       { name: 'spacer', widget: Toolbar.createSpacerItem() },
       {
         name: 'kernelName',
-        widget: Toolbar.createKernelNameItem(panel.sessionContext)
+        widget: Toolbar.createKernelNameItem(
+          panel.sessionContext,
+          sessionDialogs
+        )
       },
       {
         name: 'kernelStatus',

+ 13 - 1
packages/notebook/src/widgetfactory.ts

@@ -15,6 +15,11 @@ import { NotebookPanel } from './panel';
 
 import { StaticNotebook } from './widget';
 
+import {
+  ISessionContextDialogs,
+  sessionContextDialogs
+} from '@jupyterlab/apputils';
+
 /**
  * A widget factory for notebook panels.
  */
@@ -37,6 +42,7 @@ export class NotebookWidgetFactory extends ABCWidgetFactory<
       options.editorConfig || StaticNotebook.defaultEditorConfig;
     this._notebookConfig =
       options.notebookConfig || StaticNotebook.defaultNotebookConfig;
+    this._sessionDialogs = options.sessionDialogs || sessionContextDialogs;
   }
 
   /*
@@ -106,11 +112,12 @@ export class NotebookWidgetFactory extends ABCWidgetFactory<
   protected defaultToolbarFactory(
     widget: NotebookPanel
   ): DocumentRegistry.IToolbarItem[] {
-    return ToolbarItems.getDefaultItems(widget);
+    return ToolbarItems.getDefaultItems(widget, this._sessionDialogs);
   }
 
   private _editorConfig: StaticNotebook.IEditorConfig;
   private _notebookConfig: StaticNotebook.INotebookConfig;
+  private _sessionDialogs: ISessionContextDialogs;
 }
 
 /**
@@ -146,5 +153,10 @@ export namespace NotebookWidgetFactory {
      * The notebook configuration.
      */
     notebookConfig?: StaticNotebook.INotebookConfig;
+
+    /**
+     * The session context dialogs.
+     */
+    sessionDialogs?: ISessionContextDialogs;
   }
 }

+ 12 - 3
packages/statusbar-extension/src/index.ts

@@ -7,7 +7,12 @@ import {
   JupyterFrontEndPlugin
 } from '@jupyterlab/application';
 
-import { ISessionContext, ICommandPalette } from '@jupyterlab/apputils';
+import {
+  ISessionContext,
+  ICommandPalette,
+  ISessionContextDialogs,
+  sessionContextDialogs
+} from '@jupyterlab/apputils';
 
 import { Cell, CodeCell } from '@jupyterlab/cells';
 
@@ -124,12 +129,14 @@ export const kernelStatus: JupyterFrontEndPlugin<void> = {
   id: '@jupyterlab/statusbar-extension:kernel-status',
   autoStart: true,
   requires: [IStatusBar, INotebookTracker, IConsoleTracker, ILabShell],
+  optional: [ISessionContextDialogs],
   activate: (
     app: JupyterFrontEnd,
     statusBar: IStatusBar,
     notebookTracker: INotebookTracker,
     consoleTracker: IConsoleTracker,
-    labShell: ILabShell
+    labShell: ILabShell,
+    sessionDialogs: ISessionContextDialogs | null
   ) => {
     // When the status item is clicked, launch the kernel
     // selection dialog for the current session.
@@ -138,7 +145,9 @@ export const kernelStatus: JupyterFrontEndPlugin<void> = {
       if (!currentSession) {
         return;
       }
-      await currentSession.selectKernel();
+      await (sessionDialogs || sessionContextDialogs).selectKernel(
+        currentSession
+      );
     };
 
     // Create the status item.

+ 70 - 148
tests/test-apputils/src/sessioncontext.spec.ts

@@ -9,7 +9,12 @@ import {
   KernelSpecManager
 } from '@jupyterlab/services';
 
-import { SessionContext, Dialog, ISessionContext } from '@jupyterlab/apputils';
+import {
+  SessionContext,
+  Dialog,
+  ISessionContext,
+  sessionContextDialogs
+} from '@jupyterlab/apputils';
 
 import { UUID } from '@lumino/coreutils';
 
@@ -236,28 +241,24 @@ describe('@jupyterlab/apputils', () => {
         other.dispose();
       });
 
-      it('should present a dialog if there is no distinct kernel to start', async () => {
+      it('should yield true if there is no distinct kernel to start', async () => {
         // Remove the kernel preference before initializing.
         sessionContext.kernelPreference = {};
-
-        const accept = acceptDialog();
-
-        await sessionContext.initialize();
-        await accept;
-        expect(sessionContext.session?.kernel?.name).to.equal(
-          specsManager.specs!.default
-        );
+        const result = await sessionContext.initialize();
+        expect(result).to.equal(true);
       });
 
       it('should be a no-op if the shouldStart kernelPreference is false', async () => {
         sessionContext.kernelPreference = { shouldStart: false };
-        await sessionContext.initialize();
+        const result = await sessionContext.initialize();
+        expect(result).to.equal(false);
         expect(sessionContext.session?.kernel).to.not.be.ok;
       });
 
       it('should be a no-op if the canStart kernelPreference is false', async () => {
         sessionContext.kernelPreference = { canStart: false };
-        await sessionContext.initialize();
+        const result = await sessionContext.initialize();
+        expect(result).to.equal(false);
         expect(sessionContext.session?.kernel).to.not.be.ok;
       });
     });
@@ -363,34 +364,6 @@ describe('@jupyterlab/apputils', () => {
       });
     });
 
-    describe('#selectKernel()', () => {
-      it('should select a kernel for the session', async () => {
-        await sessionContext.initialize();
-
-        const { id, name } = sessionContext.session?.kernel!;
-        const accept = acceptDialog();
-
-        await sessionContext.selectKernel();
-        await accept;
-
-        expect(sessionContext.session?.kernel?.id).to.not.equal(id);
-        expect(sessionContext.session?.kernel?.name).to.equal(name);
-      });
-
-      it('should keep the existing kernel if dismissed', async () => {
-        await sessionContext.initialize();
-
-        const { id, name } = sessionContext.session?.kernel!;
-        const dismiss = dismissDialog();
-
-        await sessionContext.selectKernel();
-        await dismiss;
-
-        expect(sessionContext.session?.kernel?.id).to.equal(id);
-        expect(sessionContext.session?.kernel?.name).to.equal(name);
-      });
-    });
-
     describe('#shutdown', () => {
       it('should kill the kernel and shut down the session', async () => {
         await sessionContext.initialize();
@@ -400,87 +373,6 @@ describe('@jupyterlab/apputils', () => {
       });
     });
 
-    describe('#restart()', () => {
-      it('should restart if the user accepts the dialog', async () => {
-        const emission = testEmission(sessionContext.statusChanged, {
-          find: (_, args) => args === 'restarting'
-        });
-        await sessionContext.initialize();
-        await sessionContext.session?.kernel?.info;
-        const restart = sessionContext.restart();
-
-        await acceptDialog();
-        expect(await restart).to.equal(true);
-        await emission;
-      });
-
-      it('should not restart if the user rejects the dialog', async () => {
-        let called = false;
-
-        await sessionContext.initialize();
-        sessionContext.statusChanged.connect((sender, args) => {
-          if (args === 'restarting') {
-            called = true;
-          }
-        });
-
-        const restart = sessionContext.restart();
-
-        await dismissDialog();
-        expect(await restart).to.equal(false);
-        expect(called).to.equal(false);
-      });
-
-      it('should start the same kernel as the previously started kernel', async () => {
-        await sessionContext.initialize();
-        await sessionContext.shutdown();
-        await sessionContext.restart();
-        expect(sessionContext.session?.kernel).to.be.ok;
-      });
-    });
-
-    describe('#restartKernel()', () => {
-      it('should restart if the user accepts the dialog', async () => {
-        let called = false;
-
-        sessionContext.statusChanged.connect((sender, args) => {
-          if (args === 'restarting') {
-            called = true;
-          }
-        });
-        await sessionContext.initialize();
-        await sessionContext.session!.kernel!.info;
-
-        const restart = SessionContext.restartKernel(
-          sessionContext.session?.kernel!
-        );
-
-        await acceptDialog();
-        expect(await restart).to.equal(true);
-        expect(called).to.equal(true);
-      }, 30000); // Allow for slower CI
-
-      it('should not restart if the user rejects the dialog', async () => {
-        let called = false;
-
-        sessionContext.statusChanged.connect((sender, args) => {
-          if (args === 'restarting') {
-            called = true;
-          }
-        });
-        await sessionContext.initialize();
-        await sessionContext.session!.kernel!.info;
-
-        const restart = SessionContext.restartKernel(
-          sessionContext.session?.kernel!
-        );
-
-        await dismissDialog();
-        expect(await restart).to.equal(false);
-        expect(called).to.equal(false);
-      }, 30000); // Allow for slower CI
-    });
-
     describe('.getDefaultKernel()', () => {
       beforeEach(() => {
         sessionContext.dispose();
@@ -555,43 +447,73 @@ describe('@jupyterlab/apputils', () => {
       });
     });
 
-    describe('.populateKernelSelect()', () => {
-      beforeEach(() => {
-        sessionContext.dispose();
-      });
+    describe('.sessionContextDialogs', () => {
+      describe('#selectKernel()', () => {
+        it('should select a kernel for the session', async () => {
+          await sessionContext.initialize();
+          const session = sessionContext.session;
 
-      it('should populate the select div', () => {
-        const div = document.createElement('select');
+          const { id, name } = session.kernel;
+          const accept = acceptDialog();
 
-        SessionContext.populateKernelSelect(div, {
-          specs: specsManager.specs,
-          preference: {}
+          await sessionContextDialogs.selectKernel(sessionContext);
+          await accept;
+
+          expect(session.kernel.id).to.not.equal(id);
+          expect(session.kernel.name).to.equal(name);
         });
-        expect(div.firstChild).to.be.ok;
-        expect(div.value).to.not.equal('null');
-      });
 
-      it('should select the null option', () => {
-        const div = document.createElement('select');
+        it('should keep the existing kernel if dismissed', async () => {
+          await sessionContext.initialize();
+          const session = sessionContext.session;
+
+          const { id, name } = session.kernel;
+          const dismiss = dismissDialog();
 
-        SessionContext.populateKernelSelect(div, {
-          specs: specsManager.specs,
-          preference: { shouldStart: false }
+          await sessionContextDialogs.selectKernel(sessionContext);
+          await dismiss;
+
+          expect(session.kernel.id).to.equal(id);
+          expect(session.kernel.name).to.equal(name);
         });
-        expect(div.firstChild).to.be.ok;
-        expect(div.value).to.equal('null');
       });
 
-      it('should disable the node', () => {
-        const div = document.createElement('select');
+      describe('#restart()', () => {
+        it('should restart if the user accepts the dialog', async () => {
+          const emission = testEmission(sessionContext.statusChanged, {
+            find: (_, args) => args === 'restarting'
+          });
+          await sessionContext.initialize();
+          await sessionContext.session?.kernel?.info;
+          const restart = sessionContextDialogs.restart(sessionContext);
+
+          await acceptDialog();
+          expect(await restart).to.equal(true);
+          await emission;
+        });
+
+        it('should not restart if the user rejects the dialog', async () => {
+          let called = false;
+
+          await sessionContext.initialize();
+          sessionContext.statusChanged.connect((sender, args) => {
+            if (args === 'restarting') {
+              called = true;
+            }
+          });
+
+          const restart = sessionContextDialogs.restart(sessionContext);
+          await dismissDialog();
+          expect(await restart).to.equal(false);
+          expect(called).to.equal(false);
+        });
 
-        SessionContext.populateKernelSelect(div, {
-          specs: specsManager.specs,
-          preference: { canStart: false }
+        it('should start the same kernel as the previously started kernel', async () => {
+          await sessionContext.initialize();
+          await sessionContext.shutdown();
+          await sessionContextDialogs.restart(sessionContext);
+          expect(sessionContext.session?.kernel).to.be.ok;
         });
-        expect(div.firstChild).to.be.ok;
-        expect(div.value).to.equal('null');
-        expect(div.disabled).to.equal(true);
       });
     });
   });