diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9c5d7f2fd729762d8947d4ab8e7fc2e81446bf10..26b076d7cbffd63d2347afa8eb42580c776c2299 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 * Added support for Java 14.
 * Added B parser version information to `:version` output.
+* Improved interrupt handling so that only the currently running command is interrupted, rather than terminating the entire kernel. This means that interrupts now no longer reset the kernel state (loaded machine, current animator state, local variables, etc.).
 * Updated ProB 2 to version 3.11.0.
 * Fixed a parse error when a line comment is used on the last line of an expression while any `:let` variables are defined.
 * Fixed detection of B machines in cells without `::load`. Previously only single-line machines were recognized.
diff --git a/src/main/java/de/prob2/jupyter/Main.java b/src/main/java/de/prob2/jupyter/Main.java
index 574ab1f3d8ae8013bba02652488b46eaa22d1177..4289a0400d977ac67cccba1a84af3782fc5a1335 100644
--- a/src/main/java/de/prob2/jupyter/Main.java
+++ b/src/main/java/de/prob2/jupyter/Main.java
@@ -119,6 +119,7 @@ public final class Main {
 		kernelJsonData.add("argv", kernelJsonArgv);
 		kernelJsonData.addProperty("display_name", "ProB 2");
 		kernelJsonData.addProperty("language", "prob");
+		kernelJsonData.addProperty("interrupt_mode", "message");
 		
 		final Gson gson = new GsonBuilder()
 			.setPrettyPrinting()
diff --git a/src/main/java/de/prob2/jupyter/ProBKernel.java b/src/main/java/de/prob2/jupyter/ProBKernel.java
index d32646d1cb65d6132ecc0d0cc97b909dc36fe995..f9eedb5273899bd7c8929935b20f46e9d6e9f004 100644
--- a/src/main/java/de/prob2/jupyter/ProBKernel.java
+++ b/src/main/java/de/prob2/jupyter/ProBKernel.java
@@ -14,6 +14,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -192,6 +193,7 @@ public final class ProBKernel extends BaseKernel {
 	private final @NotNull AnimationSelector animationSelector;
 	
 	private final @NotNull Map<@NotNull String, @NotNull Command> commands;
+	private final @NotNull AtomicReference<@Nullable Thread> currentEvalThread;
 	private final @NotNull Map<@NotNull String, @NotNull String> variables;
 	
 	private @NotNull Path currentMachineDirectory;
@@ -208,6 +210,7 @@ public final class ProBKernel extends BaseKernel {
 			.map(injector::getInstance)
 			.collect(Collectors.toMap(Command::getName, cmd -> cmd));
 		
+		this.currentEvalThread = new AtomicReference<>(null);
 		this.variables = new HashMap<>();
 		
 		this.animationSelector.changeCurrentAnimation(new Trace(classicalBFactory.create("(initial Jupyter machine)", "MACHINE repl END").load()));
@@ -319,10 +322,7 @@ public final class ProBKernel extends BaseKernel {
 		return MACHINE_CODE_PATTERN.matcher(code).matches();
 	}
 	
-	@Override
-	public @Nullable DisplayData eval(final String expr) {
-		assert expr != null;
-		
+	private @Nullable DisplayData evalInternal(final @NotNull String expr) {
 		final Matcher commandMatcher = COMMAND_PATTERN.matcher(expr);
 		if (commandMatcher.matches()) {
 			// The input is a command, execute it directly.
@@ -340,6 +340,19 @@ public final class ProBKernel extends BaseKernel {
 		}
 	}
 	
+	@Override
+	public @Nullable DisplayData eval(final String expr) {
+		assert expr != null;
+		
+		this.currentEvalThread.set(Thread.currentThread());
+		
+		try {
+			return evalInternal(expr);
+		} finally {
+			this.currentEvalThread.set(null);
+		}
+	}
+	
 	@Override
 	public @Nullable DisplayData inspect(final @NotNull String code, final int at, final boolean extraDetail) {
 		// Note: We ignore the extraDetail parameter, because in practice it is always false. This is because the inspect_request messages sent by Jupyter Notebook always have their detail_level set to 0.
@@ -442,6 +455,15 @@ public final class ProBKernel extends BaseKernel {
 		this.animationSelector.getCurrentTrace().getStateSpace().kill();
 	}
 	
+	@Override
+	public void interrupt() {
+		final Thread evalThread = this.currentEvalThread.get();
+		if (evalThread != null) {
+			evalThread.interrupt();
+		}
+		this.animationSelector.getCurrentTrace().getStateSpace().sendInterrupt();
+	}
+	
 	private @NotNull List<@NotNull String> formatErrorSource(final @NotNull List<@NotNull String> sourceLines, final @NotNull ErrorItem.Location location) {
 		if (sourceLines.isEmpty()) {
 			return Collections.singletonList(this.errorStyler.primary("// Source code not known"));