diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index dd2f7e8..1c8ccaf 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -62,6 +62,30 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "asynchronous-codec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] + [[package]] name = "atk" version = "0.15.1" @@ -200,6 +224,9 @@ name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +dependencies = [ + "serde", +] [[package]] name = "cairo-rs" @@ -294,8 +321,10 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.48.5", ] @@ -442,6 +471,16 @@ dependencies = [ "memoffset", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9bcf5bdbfdd6030fb4a1c497b5d5fc5921aa2f60d359a17e249c0e6df3de153" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.17" @@ -485,7 +524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -495,7 +534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e" dependencies = [ "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -519,7 +558,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -530,7 +569,20 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.41", + "syn 2.0.48", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core", ] [[package]] @@ -642,6 +694,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-as-inner" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "env_logger" version = "0.10.1" @@ -765,6 +829,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -798,9 +863,15 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + [[package]] name = "futures-task" version = "0.3.29" @@ -814,8 +885,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1303,6 +1377,12 @@ dependencies = [ "serde", ] +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + [[package]] name = "infer" version = "0.13.0" @@ -1408,6 +1488,28 @@ dependencies = [ "treediff", ] +[[package]] +name = "kernel-sidecar" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a006238b112b51539d964cd628a315364e32bfed0b257dca9baed83f583b546f" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "enum-as-inner", + "hex", + "indoc", + "lazy_static", + "rand 0.8.5", + "ring", + "serde", + "serde_json", + "tokio", + "uuid", + "zeromq", +] + [[package]] name = "kuchikiki" version = "0.8.2" @@ -1615,6 +1717,7 @@ name = "nteract-tauri" version = "0.0.0" dependencies = [ "env_logger", + "kernel-sidecar", "log", "serde", "serde_json", @@ -1622,6 +1725,7 @@ dependencies = [ "tauri-build", "tokio", "ulid", + "zeromq", ] [[package]] @@ -1918,7 +2022,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -2053,9 +2157,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] @@ -2071,9 +2175,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2229,6 +2333,20 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom 0.2.11", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2342,7 +2460,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -2364,7 +2482,7 @@ checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -2402,7 +2520,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -2531,6 +2649,12 @@ dependencies = [ "system-deps 5.0.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2591,9 +2715,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.41" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -2944,7 +3068,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -3028,7 +3152,21 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] @@ -3118,7 +3256,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -3211,6 +3349,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" @@ -3236,6 +3380,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "getrandom 0.2.11", + "rand 0.8.5", + "serde", ] [[package]] @@ -3325,7 +3471,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -3347,7 +3493,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3854,3 +4000,30 @@ dependencies = [ "linux-raw-sys", "rustix", ] + +[[package]] +name = "zeromq" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ad3ffd65d6ae06a9eece312a64c3dfa2151a70a5c99051e2080828653cbda45" +dependencies = [ + "async-trait", + "asynchronous-codec", + "bytes", + "crossbeam-queue", + "dashmap", + "futures-channel", + "futures-io", + "futures-task", + "futures-util", + "log", + "num-traits", + "once_cell", + "parking_lot", + "rand 0.8.5", + "regex", + "thiserror", + "tokio", + "tokio-util", + "uuid", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ce030ed..a367cc1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,10 +16,12 @@ tauri-build = { version = "1.5", features = [] } tauri = { version = "1.5", features = ["shell-open"] } serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.35.1", features = ["full"] } +zeromq = { version = "0.3.5", features = ["tcp-transport", "tokio-runtime"], default-features = false } serde_json = "1.0" ulid = "1.1.0" log = "0.4.20" env_logger = "0.10.1" +kernel-sidecar = "0.1.0" [features] # this feature is used for production builds or when `devPath` points to the filesystem diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c949a84..21e1e0b 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -4,71 +4,103 @@ use tokio::sync::Mutex; use std::collections::HashMap; +use std::sync::Arc; use tauri::{Manager, State, Window}; -use ulid::Ulid; -// Structures for the notebook and cells -use log::{debug, info, warn}; -use env_logger; +// Structures for the notebook and cells +use log::{debug, info}; -use tokio::task; +use kernel_sidecar::{client::Client, handlers::SimpleOutputHandler}; -struct Notebook { - cells: HashMap, - cell_order: Vec, -} - -// TODO: Implement cell types markdown and code -struct Cell { - id: String, - content: String, -} +use kernel_sidecar::handlers::{DebugHandler, Handler}; +use kernel_sidecar::kernels::JupyterKernel; +use kernel_sidecar::notebook::Notebook; // AppState that holds the mapping from Window to Notebook struct AppState { + // Notebooks are just the document model, and could exist even if there's no underlying kernel notebooks: Mutex>, + // kernels are subprocesses started by the main tauri process, this is mostly here to keep + // a reference to them so they don't drop out of scope. + kernels: Mutex>, + // A kernel client is like jupyter_client in that it manages ZMQ connections to a Kernel. If + // a kernel is started outside this app, we could connect to it given a connection file, so + // kernel_clients map doesn't necessarily need to match 1:1 with kernels map + kernel_clients: Mutex>, } impl AppState { fn new() -> Self { Self { notebooks: Mutex::new(HashMap::new()), + kernels: Mutex::new(HashMap::new()), + kernel_clients: Mutex::new(HashMap::new()), } } async fn create_notebook(&self, window_id: &str) { let mut notebooks = self.notebooks.lock().await; - notebooks.insert( - window_id.to_string(), - Notebook { - cells: HashMap::new(), - cell_order: Vec::new(), - }, - ); + let nb = Notebook::new(); + notebooks.insert(window_id.to_string(), nb); + } + + async fn start_kernel(&self, window_id: &str) -> (JupyterKernel, Client) { + info!("Starting kernel for window with ID: {}", window_id); + let silent = true; // true = send ipykernel subprocess stdout to /dev/null + let kernel = JupyterKernel::ipython(silent); + let client = Client::new(kernel.connection_info.clone()).await; + (kernel, client) } // Perform the cell execution within the AppState context - async fn execute_cell(&self, window_id: &str, cell_id: &str) -> bool { + async fn execute_cell(&self, window: Window, cell_id: &str) -> bool { + let window_id = window.label(); // Use the window label as a unique identifier debug!( - "Attempting to execute cell with ID: {} in window with ID: {}", + "Executing cell with ID: {} in window with ID: {}", cell_id, window_id ); - let mut notebooks = self.notebooks.lock().await; + let mut kernel_clients = self.kernel_clients.lock().await; if let Some(notebook) = notebooks.get_mut(window_id) { - notebook.execute_cell(cell_id).await; - true - } else { - false + if let Some(cell) = notebook.get_cell(cell_id) { + // Start kernel if it doesn't exist + if !kernel_clients.contains_key(window_id) { + let (kernel, client) = self.start_kernel(window_id).await; + let mut kernels = self.kernels.lock().await; + kernels.insert(window_id.to_string(), kernel); + kernel_clients.insert(window_id.to_string(), client); + } + let kernel_client = kernel_clients.get(window_id).unwrap(); + + let source = cell.get_source(); + let debug_handler = Arc::new(Mutex::new(DebugHandler::new())); + let output_handler = Arc::new(Mutex::new(SimpleOutputHandler::new())); + let handlers: Vec>> = + vec![debug_handler.clone(), output_handler.clone()]; + + let action = kernel_client.execute_request(source, handlers).await; + action.await; + + let finished_output = &output_handler.lock().await.output; + + window.emit( + format!("cell-outputs-{}", cell_id).as_str(), + Some(finished_output.clone()), + ).unwrap(); + return true; + } } + false } + // Return cell_id async fn create_cell(&self, window_id: &str) -> Option { debug!("Creating a new cell in window with ID: {}", window_id); let mut notebooks = self.notebooks.lock().await; if let Some(notebook) = notebooks.get_mut(window_id) { - Some(notebook.create_cell()) + let new_cell = notebook.add_code_cell(""); + Some(new_cell.id().to_string()) } else { None } @@ -83,7 +115,9 @@ impl AppState { let mut notebooks = self.notebooks.lock().await; if let Some(notebook) = notebooks.get_mut(window_id) { - notebook.update_cell(cell_id, new_content); + if let Some(cell) = notebook.get_mut_cell(cell_id) { + cell.set_source(new_content); + } true } else { false @@ -91,57 +125,8 @@ impl AppState { } } -impl Notebook { - async fn execute_cell(&mut self, cell_id: &str) { - if let Some(cell) = self.cells.get(cell_id) { - let cell_content = cell.content.clone(); - let result = task::spawn_blocking(move || { - println!("Executing cell with content: {}", cell_content); - "Pretend that execution got queued" - }) - .await; - - match result { - Ok(_) => { - println!("Cell execution queued"); - } - Err(_) => { - println!("Cell failed to queue"); - } - } - } else { - warn!("Cell with ID: {} not found", cell_id); - } - } - - fn create_cell(&mut self) -> String { - let cell_id = Ulid::new().to_string(); - let new_cell = Cell { - id: cell_id.clone(), - content: String::new(), - }; - - self.cells.insert(cell_id.clone(), new_cell); - self.cell_order.push(cell_id.clone()); - - debug!("Created cell with ID: {}", cell_id.clone()); - - cell_id - } - - // Method to update an existing cell in the notebook - fn update_cell(&mut self, cell_id: &str, new_content: &str) { - if let Some(cell) = self.cells.get_mut(cell_id) { - cell.content = new_content.to_string(); - } - } -} - #[tauri::command] -async fn create_cell( - state: State<'_, AppState>, - window: Window, -) -> Result, String> { +async fn create_cell(state: State<'_, AppState>, window: Window) -> Result, String> { let window_id = window.label(); // Use the window label as a unique identifier Ok(state.create_cell(window_id).await) } @@ -152,8 +137,7 @@ async fn execute_cell( window: Window, cell_id: &str, ) -> Result { - let window_id = window.label(); // Use the window label as a unique identifier - Ok(state.execute_cell(window_id, cell_id).await) + Ok(state.execute_cell(window, cell_id).await) } #[tauri::command] diff --git a/src/components/Cell.tsx b/src/components/Cell.tsx index 649eaac..ad538bc 100644 --- a/src/components/Cell.tsx +++ b/src/components/Cell.tsx @@ -4,12 +4,13 @@ import { useCell } from "@/hooks/useCell"; import { Editor } from "@/components//Editor"; const Cell = ({ cellId }: { cellId: string }) => { - const { executeCell, cellState, executionCount } = useCell(cellId); + const { executeCell, executionState, executionCount, outputs } = + useCell(cellId); let actionIcon = "▶"; let showExecutionCountAs = executionCount === null ? " " : executionCount; - switch (cellState) { + switch (executionState) { case "idle": // Let them try again case "errored": @@ -24,16 +25,25 @@ const Cell = ({ cellId }: { cellId: string }) => { } return ( -
- - +
+
+ + +
+ {outputs && outputs.length > 0 ? ( +
{JSON.stringify(outputs, null, 2)}
+ ) : null}
); }; diff --git a/src/hooks/useCell.ts b/src/hooks/useCell.ts index 396e00a..5cc678a 100644 --- a/src/hooks/useCell.ts +++ b/src/hooks/useCell.ts @@ -1,46 +1,82 @@ -import { useState, useCallback } from "react"; +import { useState, useEffect, useCallback, useReducer } from "react"; import { invoke } from "@tauri-apps/api/tauri"; +import { listen } from '@tauri-apps/api/event' -type CellStates = "idle" | "queued" | "busy" | "errored" | "submitted"; - -export function useCell(cellId: string) { - const [content, setContent] = useState(""); - - // We also find out the execution count, whether the cell is queued, busy, or - // errored, and the output of the cell. We can use this to display the - // execution count, a spinner, or an error message. - - // Execution count is only set by the backend. - - const executionCount = null; // TODO: get from backend +enum ExecutionState { + Idle = "idle", + Queued = "queued", + Busy = "busy", + Errored = "errored", + Submitted = "submitted" +} - // Queued, busy, and errored are set by the backend. However, we have to - // have our own state for queued for before the backend has acknowledged the - // execution request. +type Action = { type: 'setSubmitted' } | { type: 'reset' } | {type: "setOutputs", outputs: []} - const [executionSubmitted, setExecutionSubmitted] = useState(false); +type CellState = { + executionState: ExecutionState; + outputs: []; +} - let cellState: CellStates = "idle"; +const initialState: CellState = { + executionState: ExecutionState.Idle, + outputs: [] +}; - if (executionSubmitted) { - cellState = "submitted"; +function cellReducer(state: CellState, action: Action) { + switch (action.type) { + case 'setSubmitted': + return { ...state, executionState: ExecutionState.Submitted }; + case 'reset': + return { ...state, executionState: ExecutionState.Idle }; + case 'setOutputs': + return {...state, outputs: action.outputs } + default: + return state; } +} +export function useCell(cellId: string) { + const [content, setContent] = useState(""); + const [state, dispatch] = useReducer(cellReducer, initialState); + const executeCell = useCallback(async () => { - setExecutionSubmitted(true); - await invoke("execute_cell", { cellId }); - setExecutionSubmitted(false); + dispatch({ type: 'setSubmitted' }); + try { + await invoke("execute_cell", { cellId }); + } catch (error) { + console.error(error); + // Handle error + } + dispatch({ type: 'reset' }); }, [cellId]); - const updateCell = useCallback( - async (newContent: string) => { + const updateCell = useCallback(async (newContent: string) => { + try { await invoke("update_cell", { cellId, newContent }); setContent(newContent); - }, - [cellId] - ); + } catch (error) { + console.error(error); + // Handle error + } + }, [cellId]); + + useEffect(() => { + const setupListener = async () => { + const unlisten = await listen(`cell-outputs-${cellId}`, (event) => { + // Q(Kyle): Do we need to check if the event.windowLabel matches ours + const outputs = event.payload as []; - // TODO(kyle): Listen for events from the backend to update the cell content + dispatch({type: "setOutputs", outputs}) + + }); + + return () => { + unlisten(); + }; + }; + + setupListener(); + }, [cellId]); - return { content, executeCell, updateCell, cellState: cellState as CellStates, executionCount }; + return { ...state, content, executeCell, updateCell, executionCount: null}; }