From f61c52de64a3a80dca1bbd46d37243f2bc563c15 Mon Sep 17 00:00:00 2001
From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com>
Date: Thu, 4 Sep 2025 16:57:26 +0530
Subject: [PATCH 1/5] Add stop cell button SVG
---
src/icons.ts | 5 +++++
style/icons/stop-cell.svg | 3 +++
2 files changed, 8 insertions(+)
create mode 100644 style/icons/stop-cell.svg
diff --git a/src/icons.ts b/src/icons.ts
index 23ccb342..a9e5d86a 100644
--- a/src/icons.ts
+++ b/src/icons.ts
@@ -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';
@@ -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
diff --git a/style/icons/stop-cell.svg b/style/icons/stop-cell.svg
new file mode 100644
index 00000000..59141dfb
--- /dev/null
+++ b/style/icons/stop-cell.svg
@@ -0,0 +1,3 @@
+
From 49979f219759e5e1fc72a31b7191639494250ea9 Mon Sep 17 00:00:00 2001
From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com>
Date: Fri, 5 Sep 2025 14:08:02 +0530
Subject: [PATCH 2/5] Add initial logic for handling execution events
---
src/run-button.ts | 112 +++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 111 insertions(+), 1 deletion(-)
diff --git a/src/run-button.ts b/src/run-button.ts
index 18e31452..ab977bcb 100644
--- a/src/run-button.ts
+++ b/src/run-button.ts
@@ -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;
@@ -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();
@@ -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: () => {
@@ -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();
+ }
+ } catch (err) {
+ console.warn('Failed to stop execution (interrupt/restart):', err);
+ }
+ },
+ 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';
+ }
+ }
+
+ 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 }
+ ) {
+ if (this._ownerCell && args.cell === this._ownerCell) {
+ this._setExecuting(true);
+ }
+ }
+
+ private _onExecuted(
+ _sender: unknown,
+ args: { notebook: Notebook; cell: Cell; 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 {
From ab8978e1132323f00486ddb40fd8d09f2785de0a Mon Sep 17 00:00:00 2001
From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com>
Date: Fri, 5 Sep 2025 14:09:22 +0530
Subject: [PATCH 3/5] Styling
---
style/base.css | 22 +++++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)
diff --git a/style/base.css b/style/base.css
index 7eddffd7..108ab469 100644
--- a/style/base.css
+++ b/style/base.css
@@ -130,13 +130,15 @@
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 */
@@ -144,9 +146,27 @@
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 */
From cf9447ca62acc76841cffa3147ed62b36617ec72 Mon Sep 17 00:00:00 2001
From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com>
Date: Mon, 8 Sep 2025 17:07:40 +0530
Subject: [PATCH 4/5] Align `[*]` prompt indicators to vertical centre
---
style/base.css | 1 +
1 file changed, 1 insertion(+)
diff --git a/style/base.css b/style/base.css
index 108ab469..3e3e67be 100644
--- a/style/base.css
+++ b/style/base.css
@@ -122,6 +122,7 @@
.jp-InputArea-prompt-indicator {
left: 0;
line-height: 25px;
+ transform: translateY(75%);
}
.jp-InputArea-prompt-indicator::before {
From 0283bda425765a02236503706a6b2ee3a2090db1 Mon Sep 17 00:00:00 2001
From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com>
Date: Mon, 8 Sep 2025 21:14:50 +0530
Subject: [PATCH 5/5] Revert "Align `[*]` prompt indicators to vertical centre"
This reverts commit cf9447ca62acc76841cffa3147ed62b36617ec72.
---
style/base.css | 1 -
1 file changed, 1 deletion(-)
diff --git a/style/base.css b/style/base.css
index 3e3e67be..108ab469 100644
--- a/style/base.css
+++ b/style/base.css
@@ -122,7 +122,6 @@
.jp-InputArea-prompt-indicator {
left: 0;
line-height: 25px;
- transform: translateY(75%);
}
.jp-InputArea-prompt-indicator::before {