Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import notebookSvg from '../style/icons/notebook.svg';
import logoSvg from '../style/icons/logo.svg';
import runSvg from '../style/icons/run.svg';
import runCellSvg from '../style/icons/run-cell.svg';
import stopCellSvg from '../style/icons/stop-cell.svg';
import refreshSvg from '../style/icons/refresh.svg';
import stopSvg from '../style/icons/stop.svg';
import fastForwardSvg from '../style/icons/fast-forward.svg';
Expand Down Expand Up @@ -94,6 +95,10 @@ export namespace EverywhereIcons {
name: 'everywhere:run-cell',
svgstr: runCellSvg
});
export const stopCell = new LabIcon({
name: 'everywhere:stop-cell',
svgstr: stopCellSvg
});
export const downloadCaret = new LabIcon({
name: 'everywhere:download-caret',
svgstr: downloadCaretSvg
Expand Down
112 changes: 111 additions & 1 deletion src/run-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'
import { IEditorServices } from '@jupyterlab/codeeditor';
import { ToolbarButton } from '@jupyterlab/ui-components';
import { Widget, PanelLayout } from '@lumino/widgets';
import { Notebook, NotebookPanel } from '@jupyterlab/notebook';
import { Notebook, NotebookPanel, NotebookActions } from '@jupyterlab/notebook';
import { EverywhereIcons } from './icons';
import { Cell, CodeCell, ICellModel } from '@jupyterlab/cells';
import { Message } from '@lumino/messaging';

const INPUT_PROMPT_CLASS = 'jp-InputPrompt';
const INPUT_AREA_PROMPT_INDICATOR_CLASS = 'jp-InputArea-prompt-indicator';
const INPUT_AREA_PROMPT_INDICATOR_EMPTY_CLASS = 'jp-InputArea-prompt-indicator-empty';
const INPUT_AREA_PROMPT_RUN_CLASS = 'jp-InputArea-prompt-run';
const INPUT_AREA_PROMPT_STOP_CLASS = 'jp-InputArea-prompt-stop';

export interface IInputPromptIndicator extends Widget {
executionCount: string | null;
Expand Down Expand Up @@ -84,6 +87,8 @@ export class JEInputPrompt extends Widget implements IInputPrompt {
private _customExecutionCount: string | null = null;
private _promptIndicator: InputPromptIndicator;
private _runButton: ToolbarButton;
private _stopButton: ToolbarButton;
private _ownerCell: CodeCell | null = null;

constructor(private _app: JupyterFrontEnd) {
super();
Expand All @@ -92,6 +97,7 @@ export class JEInputPrompt extends Widget implements IInputPrompt {
const layout = (this.layout = new PanelLayout());
this._promptIndicator = new InputPromptIndicator();
layout.addWidget(this._promptIndicator);

this._runButton = new ToolbarButton({
icon: EverywhereIcons.runCell,
onClick: () => {
Expand All @@ -102,6 +108,110 @@ export class JEInputPrompt extends Widget implements IInputPrompt {
this._runButton.addClass(INPUT_AREA_PROMPT_RUN_CLASS);
this._runButton.addClass('je-cell-run-button');
layout.addWidget(this._runButton);

this._stopButton = new ToolbarButton({
icon: EverywhereIcons.stopCell,
onClick: async () => {
const panel = this._app.shell.currentWidget;
if (!(panel instanceof NotebookPanel)) {
return;
}
try {
const kernel = panel.sessionContext.session?.kernel;
if (kernel && typeof kernel.interrupt === 'function') {
await kernel.interrupt();
} else {
await panel.sessionContext.restartKernel();
Copy link

Copilot AI Sep 5, 2025

Choose a reason for hiding this comment

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

Falling back to kernel restart when interrupt is unavailable may be too aggressive and could cause data loss. Consider providing user confirmation before restarting the kernel, or implement a more graceful degradation strategy.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, makes sense and I'm aware – I've added because interrupts don't really work.

}
} catch (err) {
console.warn('Failed to stop execution (interrupt/restart):', err);
}
Comment on lines +126 to +128
Copy link

Copilot AI Sep 5, 2025

Choose a reason for hiding this comment

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

Error handling silently logs to console without user feedback. Users should be notified when stop operation fails so they understand the cell may still be running.

Copilot uses AI. Check for mistakes.
},
tooltip: 'Stop running this cell'
});
this._stopButton.addClass(INPUT_AREA_PROMPT_STOP_CLASS);
this._stopButton.addClass('je-cell-stop-button');
layout.addWidget(this._stopButton);

this._applyPointerEvents(false);
}

/**
* Make sure the correct button is clickable; we disable pointer events for
* the hidden one to avoid the "wrong tooltip" and accidental clicks.
*/
private _applyPointerEvents(isExecuting: boolean) {
const runEl = this._runButton.node;
const stopEl = this._stopButton.node;
if (isExecuting) {
runEl.style.pointerEvents = 'none';
stopEl.style.pointerEvents = 'auto';
} else {
runEl.style.pointerEvents = 'auto';
stopEl.style.pointerEvents = 'none';
Comment on lines +146 to +151
Copy link

Copilot AI Sep 5, 2025

Choose a reason for hiding this comment

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

Direct DOM style manipulation should be avoided in favor of CSS classes. The CSS already includes pointer-events rules that could be toggled with classes instead of inline styles.

Suggested change
if (isExecuting) {
runEl.style.pointerEvents = 'none';
stopEl.style.pointerEvents = 'auto';
} else {
runEl.style.pointerEvents = 'auto';
stopEl.style.pointerEvents = 'none';
// Remove both classes first to ensure a clean state
runEl.classList.remove('je-pointer-events-none', 'je-pointer-events-auto');
stopEl.classList.remove('je-pointer-events-none', 'je-pointer-events-auto');
if (isExecuting) {
runEl.classList.add('je-pointer-events-none');
stopEl.classList.add('je-pointer-events-auto');
} else {
runEl.classList.add('je-pointer-events-auto');
stopEl.classList.add('je-pointer-events-none');

Copilot uses AI. Check for mistakes.
}
}

protected onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);

let w: Widget | null = this.parent;
while (w && !(w instanceof CodeCell)) {
w = w.parent;
}
if (w instanceof CodeCell) {
this._ownerCell = w;

NotebookActions.executionScheduled.connect(this._onExecutionScheduled, this);
NotebookActions.executed.connect(this._onExecuted, this);

this._setExecuting(false);
}
}

protected onBeforeDetach(msg: Message): void {
super.onBeforeDetach(msg);
if (this._ownerCell) {
NotebookActions.executionScheduled.disconnect(this._onExecutionScheduled, this);
NotebookActions.executed.disconnect(this._onExecuted, this);
}
this._ownerCell = null;
}

dispose(): void {
if (this._ownerCell) {
NotebookActions.executionScheduled.disconnect(this._onExecutionScheduled, this);
NotebookActions.executed.disconnect(this._onExecuted, this);
this._ownerCell = null;
}
super.dispose();
}

// Per-cell execution state handler
private _onExecutionScheduled(
_sender: unknown,
args: { notebook: Notebook; cell: Cell<ICellModel> }
) {
if (this._ownerCell && args.cell === this._ownerCell) {
this._setExecuting(true);
}
}

private _onExecuted(
_sender: unknown,
args: { notebook: Notebook; cell: Cell<ICellModel>; success: boolean; error?: unknown }
) {
if (this._ownerCell && args.cell === this._ownerCell) {
this._setExecuting(false);
}
}

private _setExecuting(flag: boolean) {
this.toggleClass('je-executing', flag);
if (this._ownerCell) {
this._ownerCell.toggleClass('je-executing', flag);
}
this._applyPointerEvents(flag);
}

get executionCount(): string | null {
Expand Down
22 changes: 21 additions & 1 deletion style/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -130,23 +130,43 @@
top: 5px;
}

.jp-InputArea-prompt-run.je-cell-run-button {
.jp-InputArea-prompt-run.je-cell-run-button,
.jp-InputArea-prompt-stop.je-cell-stop-button {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%) scale(calc(var(--je-scale) * 2.5));
opacity: 0;
transition: opacity 0.15s ease-in-out;
pointer-events: none;
}

/* Don't show run button on raw cells */
.jp-RawCell .jp-InputArea-prompt-run.je-cell-run-button {
display: none;
}

/* Don't show stop button on raw cells */
.jp-RawCell .jp-InputArea-prompt-stop.je-cell-stop-button {
display: none;
}

.jp-Cell:hover:not(.jp-RawCell) .jp-InputArea-prompt-run.je-cell-run-button,
.jp-Cell.jp-mod-active:not(.jp-RawCell) .jp-InputArea-prompt-run.je-cell-run-button {
opacity: 1;
pointer-events: auto;
}

.jp-Cell.je-executing:not(.jp-RawCell) .jp-InputArea-prompt-stop.je-cell-stop-button,
.jp-InputPrompt.je-executing:not(.jp-RawCell) .jp-InputArea-prompt-stop.je-cell-stop-button {
opacity: 1;
pointer-events: auto;
}

.jp-Cell.je-executing:not(.jp-RawCell) .jp-InputArea-prompt-run.je-cell-run-button,
.jp-InputPrompt.je-executing:not(.jp-RawCell) .jp-InputArea-prompt-run.je-cell-run-button {
opacity: 0;
pointer-events: none;
}

/* Hide all dirty state indicators */
Expand Down
3 changes: 3 additions & 0 deletions style/icons/stop-cell.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading