Repository: MusicalNinjaDad/HelixFlow
Pull request #7: tidy slint TaskBox
Author: login: MusicalNinjaDad
Created: 2025-05-22T18:24:49Z | Merged: 2025-05-22T18:36:34Z

## Summary by Sourcery

Refactor the Slint UI crate to use separate .slint modules and a shared `SlintTask`/`CurrentTask` model, introduce a `TaskList` iterator in the core library, and streamline testing and build workflows.

New Features:
- Extract `TaskBox` and `HelixFlow` components into standalone .slint files and load them via `slint::include_modules!`.
- Introduce a `SlintTask` struct and `CurrentTask` global to centralize UI task state.
- Add `TaskList::all` in the core library to iterate over tasks from any backend.
- Provide reusable testing macros (`prepare_slint!`, `run_slint_loop!`, `list_elements!`, `get!`, `assert_components!`) in the Slint UI crate.

Enhancements:
- Move task conversion logic into a dedicated `task.rs` file with `TryFrom<SlintTask>` and `From<Task>` implementations.
- Refactor the blocking `create_task` callback to update `CurrentTask` instead of manipulating properties directly.
- Consolidate the Slint build process to compile all .slint modules and conditionally emit debug info.

Build:
- Update `build.rs` to glob and compile all `.slint` files using a configurable `slint_build` configuration.
- Add `slint-build` and `glob` as build dependencies.

Tests:
- Add unit tests for core `TaskList`, Surreal backend `get_tasks`, and SlintTask conversion.
- Expand UI tests to cover accessibility, component callbacks, and integration using helper macros.
- Simplify integration tests by replacing manual panic hooks and event-loop management with the new testing macros.

Chores:
- Revise the `.devcontainer/Dockerfile` to set up Rust toolchains, user groups, permissions, and install additional UI libraries.
- Bump `slint`, `i-slint-backend-testing`, and `surrealdb` dependencies to Git or newer versions and add `assert_unordered`, `rstest`, and other dev dependencies to Cargo.toml.


Diff:
diff --git a/.config/nextest.toml b/.config/nextest.toml
new file mode 100644
index 0000000..18ee230
--- /dev/null
+++ b/.config/nextest.toml
@@ -0,0 +1,2 @@
+[profile.default]
+fail-fast = false
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 08ef61c..382a24f 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -53,11 +53,22 @@ RUN dnf -y install \
     which
 
 # Rust
-# and chown CARGO_HOME and RUSTUP_HOME to the default user 
+ENV RUSTUP_HOME=/opt/rustup \
+    CARGO_HOME=/opt/cargo \
+    PATH=/opt/cargo/bin:$PATH
+
+RUN groupadd rust \
+    && usermod -a -G rust root \
+    && usermod -a -G rust ${USERNAME}
+
 RUN dnf -y install \
     clang \
     mold \
-    rustup \
+    rustup
+
+USER root:rust
+
+RUN umask 0002 \
 && rustup-init -v -y \
 && rustup target add wasm32-unknown-unknown\
 && rustup component add \
@@ -65,7 +76,9 @@ RUN dnf -y install \
     llvm-tools-preview \
     rustfmt \
     rust-src
-RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash \
+
+RUN umask 0002 \
+&& curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash \
 && cargo binstall --secure -y \
     cargo-cyclonedx \        
     cargo-expand \
@@ -74,15 +87,16 @@ RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/ca
     mdbook \
     miniserve \
     wasm-pack
-RUN chmod -R a+rwX ${CARGO_HOME} \
-&& chmod -R a+rwX ${RUSTUP_HOME}
+
+USER root
 
 # Install SurrealDb server
 RUN curl -sSf https://install.surrealdb.com | sh \
 && dnf -y install logrotate
 
-# Libs for building UI
+# Libs for building & previewing UI
 RUN dnf install -y \
+    libinput \
     wayland-devel \
     xorg-x11-server-Xwayland
 
diff --git a/Cargo.toml b/Cargo.toml
index 242df0b..e98a049 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,8 +13,8 @@ anyhow = "1.0.98"
 async-trait = "0.1.88"
 log = "0.4.27"
 serde = { version = "1.0.219" }
-slint = { version = "1.11.0", features = ["backend-winit-wayland"] }
-surrealdb = { version = "2.3.0", features = ["kv-mem"] }
+slint = { git = "https://github.com/slint-ui/slint", features = ["backend-winit-wayland"] }
+surrealdb = { version = "2.3.3", features = ["kv-mem"] }
 thiserror = "2.0.12"
 tokio = { version = "1.44.2" }
 uuid = { version = "1.16.0", features = ["v7", "js"] }
@@ -22,12 +22,18 @@ uuid = { version = "1.16.0", features = ["v7", "js"] }
 # wasm
 console_error_panic_hook = { version = "0.1.7" }
 console_log = { version = "1.0.0" }
-getrandom = { version = "0.3.2", features = ["wasm_js"] }
+getrandom = { version = "0.3.3", features = ["wasm_js"] }
 wasm-bindgen = { version = "0.2" }
 
 # dev-only dependencies
-i-slint-backend-testing = "=1.11.0"
+assert_unordered = "0.3.5"
+i-slint-backend-testing = { git = "https://github.com/slint-ui/slint" }
 smol-macros = "0.1.1"
 macro_rules_attribute = "0.2.0"
+rstest = "0.25.0"
 wasm-bindgen-test = "0.3.50"
 wasm-bindgen-futures = "0.4.50"
+
+# build dependencies
+slint-build = { git = "https://github.com/slint-ui/slint" }
+glob = "0.3.2"
diff --git a/backends/helixflow-surreal/src/lib.rs b/backends/helixflow-surreal/src/lib.rs
index 19ec413..79bbeda 100644
--- a/backends/helixflow-surreal/src/lib.rs
+++ b/backends/helixflow-surreal/src/lib.rs
@@ -104,6 +104,12 @@ pub mod blocking {
                 Err(anyhow!("Unknown task ID: {}", id))
             }
         }
+
+        fn get_tasks(&self) -> anyhow::Result<impl Iterator<Item = TaskResult<Task>>> {
+            let tasks: Vec<SurrealTask> =
+                self.rt.block_on(self.db.select("Tasks").into_future())?;
+            Ok(tasks.into_iter().map(|task| task.try_into()))
+        }
     }
 
     /// Instantiate an in-memory Db with `ns` & `db` = "HelixFlow".
@@ -199,6 +205,21 @@ pub mod blocking {
             }
         }
 
+        #[test]
+        fn test_get_tasks() {
+            let backend = SurrealDb::new().unwrap();
+            let task1 = Task::new("Task 1", None);
+            backend.create(&task1).unwrap();
+            let task2 = Task::new("Task 2", None);
+            backend.create(&task2).unwrap();
+            let all_tasks: Vec<Task> = backend
+                .get_tasks()
+                .unwrap()
+                .map(|task| task.unwrap())
+                .collect();
+            assert_eq!(all_tasks, vec![task1, task2]);
+        }
+
         // #[test]
         // fn test_new_task_written_to_external_db() {
         //     {
diff --git a/helixflow-core/src/lib.rs b/helixflow-core/src/lib.rs
index dbe400a..9c24cd5 100644
--- a/helixflow-core/src/lib.rs
+++ b/helixflow-core/src/lib.rs
@@ -2,5 +2,6 @@
 //! needed for the app.
 
 #![feature(assert_matches)]
+#![feature(cfg_boolean_literals)]
 
 pub mod task;
diff --git a/helixflow-core/src/task.rs b/helixflow-core/src/task.rs
index d99c091..c5301df 100644
--- a/helixflow-core/src/task.rs
+++ b/helixflow-core/src/task.rs
@@ -4,7 +4,7 @@ use anyhow::anyhow;
 use serde::{Deserialize, Serialize};
 use std::borrow::Cow;
 use uuid::Uuid;
-
+use uuid::uuid;
 /// A Task
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
 pub struct Task {
@@ -35,6 +35,9 @@ impl Task {
     }
 }
 
+/// Iterator of `Task`s
+pub struct TaskList;
+
 #[derive(Debug, thiserror::Error)]
 pub enum TaskCreationError {
     // The #[from] anyhow::Error will convert anything that offers `into anyhow::Error`.
@@ -85,6 +88,18 @@ pub mod blocking {
         }
     }
 
+    impl TaskList {
+        pub fn all<B: StorageBackend>(
+            backend: &B,
+        ) -> TaskResult<impl Iterator<Item = TaskResult<Task>>> {
+            // You cannot propagate an error from within the iterator itself using `map(|task| task?)` because the `?` operator only works in functions that return a `Result`, not in closures.
+            // If you want to propagate errors from individual items, you should return an iterator of `TaskResult<Task>`, not just `Task`.
+            // So, change the return type to `TaskResult<impl Iterator<Item = TaskResult<Task>>>` and just return the iterator directly:
+
+            Ok(backend.get_tasks()?)
+        }
+    }
+
     /// Provide an implementation of a storage backend.
     pub trait StorageBackend {
         /// Create a new task in the backend, update the `task.id`.
@@ -95,6 +110,8 @@ pub mod blocking {
 
         /// Get an existing task from the backend
         fn get(&self, id: &Uuid) -> anyhow::Result<Task>;
+
+        fn get_tasks(&self) -> anyhow::Result<impl Iterator<Item = TaskResult<Task>>>;
     }
 
     #[derive(Clone, Copy)]
@@ -118,15 +135,34 @@ pub mod blocking {
                     id: *id,
                     description: None,
                 }),
+                "0196ca5f-d934-7ec8-b042-ae37b94b8432" => Ok(Task {
+                    name: "Task 2".into(),
+                    id: *id,
+                    description: None,
+                }),
                 _ => Err(anyhow!("Unknown task ID: {}", id)),
             }
         }
+        fn get_tasks(&self) -> anyhow::Result<impl Iterator<Item = TaskResult<Task>>> {
+            Ok(vec![
+                Ok(Task {
+                    name: "Task 1".into(),
+                    id: uuid!("0196b4c9-8447-7959-ae1f-72c7c8a3dd36"),
+                    description: None,
+                }),
+                Ok(Task {
+                    name: "Task 2".into(),
+                    id: uuid!("0196ca5f-d934-7ec8-b042-ae37b94b8432"),
+                    description: None,
+                }),
+            ]
+            .into_iter())
+        }
     }
 
     #[cfg(test)]
     pub mod tests {
         use std::assert_matches::assert_matches;
-        use uuid::uuid;
         use wasm_bindgen_test::*;
 
         use super::*;
@@ -189,7 +225,7 @@ pub mod blocking {
                 task,
                 Task {
                     name: "Task 1".into(),
-                    id: id,
+                    id,
                     description: None
                 }
             )
@@ -205,6 +241,29 @@ pub mod blocking {
                 "Unknown task ID: 0196b4c9-8447-78db-ae8a-be68a8095aa2"
             );
         }
+
+        #[test]
+        fn list_tasks() {
+            let backend = TestBackend;
+            let task1 = Task {
+                name: "Task 1".into(),
+                id: uuid!("0196b4c9-8447-7959-ae1f-72c7c8a3dd36"),
+                description: None,
+            };
+            let task2 = Task {
+                name: "Task 2".into(),
+                id: uuid!("0196ca5f-d934-7ec8-b042-ae37b94b8432"),
+                description: None,
+            };
+            let all_tasks: Vec<TaskResult<Task>> = TaskList::all(&backend).unwrap().collect();
+            assert_eq!(
+                all_tasks
+                    .into_iter()
+                    .map(|task| task.unwrap())
+                    .collect::<Vec<Task>>(),
+                vec![task1, task2]
+            );
+        }
     }
 }
 
diff --git a/helixflow/src/lib.rs b/helixflow/src/lib.rs
index 05c9cb5..4fc7ae6 100644
--- a/helixflow/src/lib.rs
+++ b/helixflow/src/lib.rs
@@ -4,7 +4,7 @@ use log::debug;
 use slint::ComponentHandle;
 
 use helixflow_slint::HelixFlow;
-use helixflow_slint::blocking::create_task;
+use helixflow_slint::task::blocking::create_task;
 use helixflow_surreal::blocking::SurrealDb;
 
 pub fn run_helixflow() {
diff --git a/helixflow/tests/slint_surreal.rs b/helixflow/tests/slint_surreal.rs
index 76eb6f4..3f2fc67 100644
--- a/helixflow/tests/slint_surreal.rs
+++ b/helixflow/tests/slint_surreal.rs
@@ -1,36 +1,20 @@
-use std::{
-    panic::{self, PanicHookInfo},
-    rc::Rc,
-    sync::OnceLock,
-};
+use std::rc::Rc;
 
 use helixflow_core::task::{Task, blocking::CRUD};
-use i_slint_backend_testing::ElementHandle;
-use slint::ComponentHandle;
 use slint::platform::PointerEventButton;
+use slint::{ComponentHandle, Global};
 
-use helixflow_slint::{HelixFlow, blocking::create_task};
+use helixflow_slint::{CurrentTask, HelixFlow, task::blocking::create_task, test::*};
 use helixflow_surreal::blocking::SurrealDb;
 
 #[test]
 fn test_slint_with_surreal() {
-    // Slint's event_loop doesn't propogate panics from background tasks
-    //   so we need to actively track if any occur.
-    static PANICKED: OnceLock<bool> = OnceLock::new();
-    static DEFAULT_HOOK: OnceLock<Box<dyn Fn(&PanicHookInfo) + Sync + Send + 'static>> =
-        OnceLock::new();
-    let _ = DEFAULT_HOOK.set(panic::take_hook());
-
-    panic::set_hook(Box::new(|info| {
-        DEFAULT_HOOK.get().unwrap()(info);
-        let _ = PANICKED.set(true);
-    }));
+    prepare_slint!();
 
     let backend = Rc::new(SurrealDb::new().unwrap());
 
-    i_slint_backend_testing::init_integration_test_with_system_time();
-
     let helixflow = HelixFlow::new().unwrap();
+    list_elements!(&helixflow);
 
     let hf = helixflow.as_weak();
     let be = Rc::downgrade(&backend);
@@ -40,32 +24,33 @@ fn test_slint_with_surreal() {
     slint::spawn_local(async move {
         let helixflow = hf.unwrap();
         helixflow.set_task_name("A valid task".into());
-        assert_eq!(helixflow.get_task_id(), "");
-        assert!(helixflow.get_create_enabled());
 
-        let creates_: Vec<_> =
-            ElementHandle::find_by_element_id(&helixflow, "HelixFlow::create").collect();
-        assert_eq!(creates_.len(), 1);
-        let create = &creates_[0];
+        let task_id_display = get!(&helixflow, "TaskBox::task_id_display");
+        assert_eq!(task_id_display.accessible_value().unwrap(), "");
 
+        let create = get!(&helixflow, "TaskBox::create");
+        assert!(helixflow.get_create_enabled());
+        assert!(create.accessible_enabled().unwrap());
         create.single_click(PointerEventButton::Left).await;
+
         slint::quit_event_loop().unwrap();
     })
     .unwrap();
 
-    slint::run_event_loop().unwrap();
+    run_slint_loop!();
 
-    assert!(PANICKED.get().is_none_or(|panicked| { !panicked }));
+    let ui_task: Task = CurrentTask::get(&helixflow).get_task().try_into().unwrap();
 
-    let task_uuid = uuid::Uuid::parse_str(&helixflow.get_task_id()).unwrap();
-    assert!(!task_uuid.is_nil());
-    assert_eq!(task_uuid.get_version(), Some(uuid::Version::SortRand));
-    assert!(helixflow.get_create_enabled());
-    let ui_task = Task {
-        name: helixflow.get_task_name().to_string().into(),
-        id: task_uuid,
-        description: None,
-    };
-    let db_task = Task::get(backend.as_ref(), &task_uuid).unwrap();
+    let task_id_display = get!(&helixflow, "TaskBox::task_id_display");
+    assert_eq!(
+        task_id_display.accessible_value().unwrap(),
+        ui_task.id.to_string()
+    );
+
+    let db_task = Task::get(backend.as_ref(), &ui_task.id).unwrap();
     assert_eq!(ui_task, db_task);
+
+    let create = get!(&helixflow, "TaskBox::create");
+    assert!(helixflow.get_create_enabled());
+    assert!(create.accessible_enabled().unwrap());
 }
diff --git a/ui/helixflow-slint/Cargo.toml b/ui/helixflow-slint/Cargo.toml
index 2ff36e0..a94e436 100644
--- a/ui/helixflow-slint/Cargo.toml
+++ b/ui/helixflow-slint/Cargo.toml
@@ -4,9 +4,19 @@ version = "0.0.1"
 edition = "2024"
 
 [dependencies]
+# Feature: test_helpers
+assert_unordered.workspace = true
+i-slint-backend-testing.workspace = true
+
+# Real dependencies
 helixflow-core.workspace = true
 slint.workspace = true
+uuid.workspace = true
 
 [dev-dependencies]
-i-slint-backend-testing.workspace = true
+rstest.workspace = true
 uuid.workspace = true
+
+[build-dependencies]
+glob.workspace = true
+slint-build.workspace = true
diff --git a/ui/helixflow-slint/build.rs b/ui/helixflow-slint/build.rs
index a728065..cbfa8e6 100644
--- a/ui/helixflow-slint/build.rs
+++ b/ui/helixflow-slint/build.rs
@@ -1,6 +1,32 @@
+use std::{env, fs, path::PathBuf};
+
 fn main() {
-    // Set SLINT_EMIT_DEBUG_INFO=1 for tests, rust-analyzer, etc.
+    let mut slint_config = slint_build::CompilerConfiguration::new();
+
     if std::env::var("DEBUG") == Ok("true".to_string()) {
-        println!("cargo:rustc-env=SLINT_EMIT_DEBUG_INFO=1");
+        println!("cargo:rustc-env=SLINT_EMIT_DEBUG_INFO=1"); // for `slint!` macro
+        slint_config = slint_config.with_debug_info(true);
+        build_all_slint_modules(&slint_config);
+    }
+
+    slint_build::compile_with_config("src/helixflow.slint", slint_config).unwrap();
+}
+
+fn build_all_slint_modules(slint_config: &slint_build::CompilerConfiguration) {
+    use glob::glob;
+
+    for slint_file in glob("src/*.slint").expect("Failed to find slint files") {
+        let mut src_file = PathBuf::new();
+        src_file.push(env::var("CARGO_MANIFEST_DIR").unwrap());
+        src_file.push(slint_file.as_ref().unwrap());
+
+        let mut rs_file = PathBuf::new();
+        rs_file.push(env::var("OUT_DIR").unwrap());
+        rs_file.push(slint_file.as_ref().unwrap().with_extension("rs"));
+
+        fs::create_dir_all(rs_file.parent().unwrap()).unwrap();
+
+        println!("Compiling: {:#?} to {:#?}", &src_file, &rs_file);
+        slint_build::compile_with_output_path(src_file, rs_file, slint_config.clone()).unwrap();
     }
 }
diff --git a/ui/helixflow-slint/src/helixflow.slint b/ui/helixflow-slint/src/helixflow.slint
new file mode 100644
index 0000000..a84bd36
--- /dev/null
+++ b/ui/helixflow-slint/src/helixflow.slint
@@ -0,0 +1,14 @@
+import { TaskBox } from "task.slint";
+export { SlintTask, CurrentTask } from "task.slint";
+
+export component HelixFlow inherits Window {
+    callback create_task;
+    in property <bool> create_enabled: true;
+    in-out property <string> task_name: taskbox.task_name;
+    taskbox := TaskBox {
+        create_task => {
+            root.create_task();
+        }
+        create_enabled: root.create_enabled;
+    }
+}
diff --git a/ui/helixflow-slint/src/lib.rs b/ui/helixflow-slint/src/lib.rs
index 08a2685..a51aee0 100644
--- a/ui/helixflow-slint/src/lib.rs
+++ b/ui/helixflow-slint/src/lib.rs
@@ -1,153 +1,116 @@
-use std::rc::Weak;
-
-use slint::slint;
-
-use helixflow_core::task::Task;
-
-slint! {
-    import { Button, LineEdit, VerticalBox } from "std-widgets.slint";
-    export component HelixFlow inherits Window {
-        callback create_task;
-        in property <bool> create_enabled: true;
-        in property <string> task_id: "";
-        in-out property <string> task_name: task_name_entry.text;
-        VerticalBox {
-            task_name_entry := LineEdit {
-                accessible_label: "Task name";
-                placeholder-text: self.accessible_label;
-            }
-            task_id_display := Text {
-                accessible_label: "Task ID";
-                text: root.task_id;
-                accessible_value: self.text;
-            }
-            create := Button {
-                enabled: root.create_enabled;
-                text: "Create";
-                clicked() => { root.create_task(); }
-            }
-        }
-    }
-}
-
-pub mod blocking {
-    use super::*;
-    use helixflow_core::task::blocking::{CRUD, StorageBackend};
-
-    pub fn create_task<BKEND>(
-        helixflow: slint::Weak<HelixFlow>,
-        backend: Weak<BKEND>,
-    ) -> impl FnMut() + 'static
-    where
-        BKEND: StorageBackend + 'static,
-    {
-        move || {
-            let helixflow = helixflow.unwrap();
-            let backend = backend.upgrade().unwrap();
-            helixflow.set_create_enabled(false);
-            let task_name: String = helixflow.get_task_name().into();
-            let task = Task::new(task_name, None);
-            task.create(backend.as_ref()).unwrap();
-            let task_id = task.id;
-            helixflow.set_task_id(format!("{task_id}").into());
-            helixflow.set_create_enabled(true);
-        }
+#![feature(assert_matches)]
+
+slint::include_modules!();
+
+pub mod task;
+
+/// Helper macros & re-exports to simplify testing: `use helixflow_slint::test::*`
+pub mod test {
+    pub use std::{
+        panic::{self, PanicHookInfo},
+        sync::OnceLock,
+    };
+
+    // TODO: Stick this module and following dependencies behind a feature flag.
+    pub use assert_unordered::assert_eq_unordered_sort;
+    pub use i_slint_backend_testing::{ElementHandle, ElementRoot};
+
+    #[macro_export]
+    #[doc(hidden)]
+    /// Slint's event_loop doesn't propogate panics from background task so we create a custom panic
+    /// handler to actively track if any occur before calling `init_integration_test_with_system_time`.
+    ///
+    /// Use `run_slint_loop!()` to run the even loop and then check for panics.
+    macro_rules! prepare_slint {
+        () => {
+            static PANICKED: OnceLock<bool> = OnceLock::new();
+            static DEFAULT_HOOK: OnceLock<Box<dyn Fn(&PanicHookInfo) + Sync + Send + 'static>> =
+                OnceLock::new();
+            let _ = DEFAULT_HOOK.set(panic::take_hook());
+
+            panic::set_hook(Box::new(|info| {
+                DEFAULT_HOOK.get().unwrap()(info);
+                let _ = PANICKED.set(true);
+            }));
+            i_slint_backend_testing::init_integration_test_with_system_time();
+        };
     }
-
-    #[cfg(test)]
-    mod test {
-        use std::rc::Rc;
-
-        use i_slint_backend_testing::{ElementHandle, ElementRoot, init_no_event_loop};
-
-        use super::*;
-
-        #[test]
-        fn test_ui_elements() {
-            init_no_event_loop();
-            let helixflow = HelixFlow::new().unwrap();
-
-            let all_elements =
-                ElementHandle::query_descendants(&helixflow.root_element()).find_all();
-            for (i, element) in all_elements.iter().enumerate() {
-                let type_name = element.type_name();
-                let label = element
-                    .accessible_label()
-                    .unwrap_or_else(|| "<no label>".into());
-                println!("Element {i}: type = {:#?}, label = {label}", type_name);
-            }
-            dbg!(all_elements.len());
-
-            let buttons: Vec<_> =
-                ElementHandle::find_by_element_type_name(&helixflow, "Button").collect();
-            assert_eq!(buttons.len(), 1);
-            let create = &buttons[0];
-            assert_eq!(create.accessible_label().unwrap().as_str(), "Create");
-
-            let ids: Vec<_> =
-                ElementHandle::find_by_element_id(&helixflow, "HelixFlow::task_id_display")
-                    .collect();
-            assert_eq!(ids.len(), 1);
-            let id = &ids[0];
-            assert_eq!(id.accessible_label().unwrap().as_str(), "Task ID");
-            assert_eq!(id.accessible_value().unwrap().as_str(), "");
-
-            let inputboxes: Vec<_> =
-                ElementHandle::find_by_element_type_name(&helixflow, "LineEdit").collect();
-            assert_eq!(inputboxes.len(), 1);
-            let task_name = &inputboxes[0];
-            assert_eq!(task_name.accessible_label().unwrap().as_str(), "Task name");
-            assert_eq!(
-                task_name.accessible_placeholder_text().unwrap().as_str(),
-                "Task name"
+    pub use prepare_slint;
+
+    #[macro_export]
+    #[doc(hidden)]
+    /// Run the event loop and check whether anything within it `panic`ked...
+    macro_rules! run_slint_loop {
+        () => {
+            slint::run_event_loop().unwrap();
+            assert!(
+                PANICKED.get().is_none_or(|panicked| { !panicked }) // just in case it was set to `false` for some reason
             );
-            assert_eq!(task_name.accessible_value().unwrap().as_str(), "");
-        }
-
-        #[test]
-        fn test_button_click() {
-            init_no_event_loop();
-            let helixflow = Rc::new(HelixFlow::new().unwrap());
-
-            let all_elements =
-                ElementHandle::query_descendants(&helixflow.root_element()).find_all();
+        };
+    }
+    pub use run_slint_loop;
+
+    #[macro_export]
+    #[doc(hidden)]
+    // List all slint elements to stdout (shown on test failure)
+    macro_rules! list_elements {
+        ($root:expr) => {
+            let all_elements = ElementHandle::query_descendants(&$root.root_element()).find_all();
             for (i, element) in all_elements.iter().enumerate() {
                 let type_name = element.type_name();
                 let label = element
                     .accessible_label()
                     .unwrap_or_else(|| "<no label>".into());
-                let value = element
-                    .accessible_value()
-                    .unwrap_or_else(|| "<no value>".into());
-                let id = element.id().unwrap_or_else(|| "<no ID>".into());
+                let elementid = element.id().unwrap_or_else(|| "<no ID>".into());
                 println!(
-                    "Element {i}: id = {:#?}, type = {:#?}, label = {label}, value = {:#?}",
-                    id, type_name, value
+                    "Element {i}: id = {elementid}, type = {:#?}, label = {label}",
+                    type_name
                 );
             }
-            dbg!(all_elements.len());
-
-            let things_called_create: Vec<_> =
-                ElementHandle::find_by_accessible_label(helixflow.as_ref(), "Create").collect();
-            assert_eq!(things_called_create.len(), 1);
-            let create = &things_called_create[0];
-            assert_eq!(create.type_name().unwrap().as_str(), "Button");
-
-            let ids: Vec<_> =
-                ElementHandle::find_by_element_id(helixflow.as_ref(), "HelixFlow::task_id_display")
-                    .collect();
-            assert_eq!(ids.len(), 1);
-            let id = &ids[0];
-
-            let hf = helixflow.as_weak();
-            helixflow.on_create_task(move || {
-                hf.unwrap().set_task_id("1".into());
-            });
-
-            assert_eq!(id.accessible_value().unwrap().as_str(), "");
-            create.invoke_accessible_default_action();
-            assert_eq!(id.accessible_label().unwrap().as_str(), "Task ID");
-            assert_eq!(id.accessible_value().unwrap().as_str(), "1");
-        }
+        };
+    }
+    pub use list_elements;
+
+    #[macro_export]
+    #[doc(hidden)]
+    /// Get a component's `ElementHandle` via the _unique_ id: `get!(&parent, "Parent::id")`
+    ///
+    /// **Panics** if the id is not unique.
+    macro_rules! get {
+        ($component:expr, $id:expr) => {{
+            let elements: Vec<_> = ElementHandle::find_by_element_id($component, $id).collect();
+            assert_eq!(
+                elements.len(),
+                1,
+                "{} elements found with id: {}",
+                elements.len(),
+                $id
+            );
+            elements.into_iter().next().unwrap()
+        }};
+    }
+    pub use get;
+
+    #[macro_export]
+    #[doc(hidden)]
+    /// Assert that the actual components match those expected based on the accessibility labels.
+    ///
+    /// ```rust,no_run
+    /// let inputboxes: impl Iterator<Item = ElementHandle> = ElementHandle::find_by_element_type_name(&taskbox, "LineEdit");
+    ///
+    /// let expected_inputboxes = ["Task name"];
+    ///
+    /// assert_components!(inputboxes, expected_inputboxes);
+    /// ```
+    macro_rules! assert_components {
+        ($actual:expr, $expected:expr) => {
+            assert_eq_unordered_sort!(
+                $actual
+                    .map(|element| element.accessible_label().unwrap())
+                    .collect::<Vec<_>>(),
+                $expected.iter().map(|&label| label.into()).collect()
+            );
+        };
     }
+    pub use assert_components;
 }
diff --git a/ui/helixflow-slint/src/task.rs b/ui/helixflow-slint/src/task.rs
new file mode 100644
index 0000000..c669c81
--- /dev/null
+++ b/ui/helixflow-slint/src/task.rs
@@ -0,0 +1,204 @@
+use helixflow_core::task::{Task, TaskCreationError, TaskResult};
+use slint::{Global, ToSharedString};
+use std::rc::Weak;
+use uuid::Uuid;
+
+use crate::{CurrentTask, HelixFlow, SlintTask};
+
+impl TryFrom<SlintTask> for Task {
+    type Error = TaskCreationError;
+    fn try_from(task: SlintTask) -> TaskResult<Task> {
+        Ok(if task.id.is_empty() {
+            Task::new(task.name.to_string(), None)
+        } else {
+            Task {
+                name: task.name.to_string().into(),
+                id: Uuid::try_parse(task.id.as_str())
+                    .map_err(|_| TaskCreationError::InvalidID { id: task.id.into() })?,
+                description: None,
+            }
+        })
+    }
+}
+
+impl From<Task> for SlintTask {
+    fn from(task: Task) -> Self {
+        Self {
+            name: task.name.into_owned().into(),
+            id: task.id.to_shared_string(),
+        }
+    }
+}
+
+pub mod blocking {
+    use super::*;
+    use helixflow_core::task::blocking::{CRUD, StorageBackend};
+
+    pub fn create_task<BKEND>(
+        helixflow: slint::Weak<HelixFlow>,
+        backend: Weak<BKEND>,
+    ) -> impl FnMut() + 'static
+    where
+        BKEND: StorageBackend + 'static,
+    {
+        move || {
+            let helixflow = helixflow.unwrap();
+            let backend = backend.upgrade().unwrap();
+            helixflow.set_create_enabled(false);
+            let task_name: String = helixflow.get_task_name().into();
+            let task = Task::new(task_name, None);
+            task.create(backend.as_ref()).unwrap();
+            CurrentTask::get(&helixflow).set_task(task.into());
+            helixflow.set_create_enabled(true);
+        }
+    }
+}
+
+#[cfg(test)]
+mod test_rs {
+    use super::*;
+
+    use rstest::*;
+
+    use std::assert_matches::assert_matches;
+    use uuid::uuid;
+
+    #[rstest]
+    fn task_no_id() {
+        let slint_task = SlintTask {
+            name: "Task 1".into(),
+            id: "".into(),
+        };
+        let task: Task = slint_task.try_into().unwrap();
+        assert_eq!(task.name, "Task 1");
+        assert!(!task.id.is_nil());
+        assert_eq!(task.description, None);
+    }
+
+    #[rstest]
+    fn task_with_id() {
+        let slint_task = SlintTask {
+            name: "Task 1".into(),
+            id: "0196b4c9-8447-7959-ae1f-72c7c8a3dd36".into(),
+        };
+        let task: Task = slint_task.try_into().unwrap();
+        let expected_task = Task {
+            name: "Task 1".into(),
+            id: uuid!("0196b4c9-8447-7959-ae1f-72c7c8a3dd36"),
+            description: None,
+        };
+        assert_eq!(task, expected_task);
+    }
+
+    #[rstest]
+    fn task_invalid_id() {
+        let slint_task = SlintTask {
+            name: "Task 1".into(),
+            id: "foo".into(),
+        };
+        let task: TaskResult<Task> = slint_task.try_into();
+        let err = task.unwrap_err();
+        assert_matches!(err, TaskCreationError::InvalidID {id} if id == "foo");
+    }
+
+    #[rstest]
+    fn from_task() {
+        let task = Task {
+            name: "Task 1".into(),
+            id: uuid!("0196b4c9-8447-7959-ae1f-72c7c8a3dd36"),
+            description: None,
+        };
+        let slint_task = SlintTask {
+            name: "Task 1".into(),
+            id: "0196b4c9-8447-7959-ae1f-72c7c8a3dd36".into(),
+        };
+        assert_eq!(slint_task, task.into());
+    }
+}
+
+#[cfg(test)]
+mod test_slint {
+    use crate::test::*;
+    use rstest::*;
+
+    use i_slint_backend_testing::init_no_event_loop;
+
+    include!(concat!(env!("OUT_DIR"), "/src/task.rs"));
+
+    #[fixture]
+    fn taskbox() -> TaskBox {
+        init_no_event_loop();
+
+        let taskbox = TaskBox::new().unwrap();
+        list_elements!(&taskbox);
+        taskbox
+    }
+
+    #[rstest]
+    fn correct_elements(taskbox: TaskBox) {
+        let inputboxes = ElementHandle::find_by_element_type_name(&taskbox, "LineEdit");
+        let buttons = ElementHandle::find_by_element_type_name(&taskbox, "Button");
+
+        let expected_inputboxes = ["Task name"];
+        let expected_buttons = ["Create"];
+
+        assert_components!(inputboxes, expected_inputboxes);
+        assert_components!(buttons, expected_buttons);
+    }
+
+    mod accessibility {
+        use i_slint_backend_testing::AccessibleRole;
+
+        use super::*;
+
+        #[rstest]
+        fn task_name(taskbox: TaskBox) {
+            let task_name = get!(&taskbox, "TaskBox::task_name_entry");
+            assert_eq!(task_name.accessible_label().unwrap().as_str(), "Task name");
+            assert_eq!(
+                task_name.accessible_placeholder_text().unwrap().as_str(),
+                "Task name"
+            );
+            assert_eq!(task_name.accessible_value().unwrap().as_str(), "");
+            assert_eq!(task_name.accessible_role(), Some(AccessibleRole::TextInput));
+        }
+
+        #[rstest]
+        fn task_id(taskbox: TaskBox) {
+            let task_id = get!(&taskbox, "TaskBox::task_id_display");
+            assert_eq!(task_id.accessible_label().unwrap().as_str(), "Task ID");
+            assert_eq!(task_id.accessible_value().unwrap().as_str(), "");
+            assert_eq!(task_id.accessible_role(), Some(AccessibleRole::Text));
+        }
+
+        #[rstest]
+        fn create(taskbox: TaskBox) {
+            let create = get!(&taskbox, "TaskBox::create");
+            assert_eq!(create.accessible_label().unwrap().as_str(), "Create");
+            assert_eq!(create.accessible_role(), Some(AccessibleRole::Button));
+        }
+    }
+
+    mod callbacks {
+        use super::*;
+
+        #[rstest]
+        fn button_click(taskbox: TaskBox) {
+            let tb = taskbox.as_weak();
+            taskbox.on_create_task(move || {
+                CurrentTask::get(&tb.unwrap()).set_task(SlintTask {
+                    name: "".into(),
+                    id: "1".into(),
+                });
+            });
+
+            let create = get!(&taskbox, "TaskBox::create");
+            let task_id = get!(&taskbox, "TaskBox::task_id_display");
+
+            assert_eq!(task_id.accessible_value().unwrap().as_str(), "");
+            create.invoke_accessible_default_action();
+            assert_eq!(task_id.accessible_label().unwrap().as_str(), "Task ID");
+            assert_eq!(task_id.accessible_value().unwrap().as_str(), "1");
+        }
+    }
+}
diff --git a/ui/helixflow-slint/src/task.slint b/ui/helixflow-slint/src/task.slint
new file mode 100644
index 0000000..db8837d
--- /dev/null
+++ b/ui/helixflow-slint/src/task.slint
@@ -0,0 +1,35 @@
+export struct SlintTask {
+    name: string,
+    id: string,
+}
+
+export global CurrentTask {
+    in-out property <SlintTask> task;
+}
+
+import { Button, LineEdit, VerticalBox } from "std-widgets.slint";
+    export component TaskBox inherits Window {
+    callback create_task;
+    in property <bool> create_enabled: true;
+    in-out property <string> task_name: task_name_entry.text;
+    VerticalBox {
+        task_name_entry := LineEdit {
+            accessible_label: "Task name";
+            placeholder-text: self.accessible_label;
+        }
+
+        task_id_display := Text {
+            accessible_label: "Task ID";
+            text: CurrentTask.task.id;
+            accessible_value: self.text;
+        }
+
+        create := Button {
+            enabled: root.create_enabled;
+            text: "Create";
+            clicked() => {
+                root.create_task();
+            }
+        }
+    }
+}
diff --git a/ui/helixflow-slint/tests/slint_TaskCRUD.rs b/ui/helixflow-slint/tests/slint_TaskCRUD.rs
index 4f56f8c..ee2a732 100644
--- a/ui/helixflow-slint/tests/slint_TaskCRUD.rs
+++ b/ui/helixflow-slint/tests/slint_TaskCRUD.rs
@@ -1,34 +1,23 @@
-use std::{
-    panic::{self, PanicHookInfo},
-    rc::Rc,
-    sync::OnceLock,
-};
-
-use i_slint_backend_testing::ElementHandle;
-use slint::ComponentHandle;
+//! Use nextest - these tests will fail on cargo test as they MUST run is separate processes
+//! for `i_slint_backend_testing::init_integration_test_with_system_time`
+
+use std::rc::Rc;
+
 use slint::platform::PointerEventButton;
+use slint::{ComponentHandle, Global};
 
-use helixflow_core::task::blocking::TestBackend;
-use helixflow_slint::{HelixFlow, blocking::create_task};
+use helixflow_core::task::{Task, blocking::TestBackend};
+use helixflow_slint::{CurrentTask, HelixFlow, task::blocking::create_task, test::*};
 
 #[test]
 fn test_set_task_id() {
-    // Slint's event_loop doesn't propogate panics from background tasks
-    //   so we need to actively track if any occur.
-    static PANICKED: OnceLock<bool> = OnceLock::new();
-    static DEFAULT_HOOK: OnceLock<Box<dyn Fn(&PanicHookInfo) + Sync + Send + 'static>> =
-        OnceLock::new();
-    let _ = DEFAULT_HOOK.set(panic::take_hook());
-
-    panic::set_hook(Box::new(|info| {
-        DEFAULT_HOOK.get().unwrap()(info);
-        let _ = PANICKED.set(true);
-    }));
-    i_slint_backend_testing::init_integration_test_with_system_time();
+    prepare_slint!();
 
     let helixflow = HelixFlow::new().unwrap();
     let backend = Rc::new(TestBackend);
 
+    list_elements!(&helixflow);
+
     let hf = helixflow.as_weak();
     let be = Rc::downgrade(&backend);
     helixflow.on_create_task(create_task(hf, be));
@@ -38,24 +27,34 @@ fn test_set_task_id() {
     slint::spawn_local(async move {
         let helixflow = hf.unwrap();
         helixflow.set_task_name("A valid task".into());
-        assert_eq!(helixflow.get_task_id(), "");
-        assert!(helixflow.get_create_enabled());
 
-        let creates_: Vec<_> =
-            ElementHandle::find_by_element_id(&helixflow, "HelixFlow::create").collect();
-        assert_eq!(creates_.len(), 1);
-        let create = &creates_[0];
+        let task_id_display = get!(&helixflow, "TaskBox::task_id_display");
+        assert_eq!(task_id_display.accessible_value().unwrap(), "");
 
+        let create = get!(&helixflow, "TaskBox::create");
+        assert!(helixflow.get_create_enabled());
+        assert!(create.accessible_enabled().unwrap());
         create.single_click(PointerEventButton::Left).await;
+
         slint::quit_event_loop().unwrap();
     })
     .unwrap();
 
-    slint::run_event_loop().unwrap();
+    run_slint_loop!();
+
+    let current_task: Task = CurrentTask::get(&helixflow).get_task().try_into().unwrap();
+    assert_eq!(current_task.name, "A valid task");
+    assert_eq!(current_task.description, None);
+    assert!(!current_task.id.is_nil());
+    assert_eq!(current_task.id.get_version(), Some(uuid::Version::SortRand));
+
+    let task_id_display = get!(&helixflow, "TaskBox::task_id_display");
+    assert_eq!(
+        task_id_display.accessible_value().unwrap(),
+        current_task.id.to_string()
+    );
 
-    assert!(PANICKED.get().is_none_or(|panicked| { !panicked }));
-    let task_uuid = uuid::Uuid::parse_str(&helixflow.get_task_id()).unwrap();
-    assert!(!task_uuid.is_nil());
-    assert_eq!(task_uuid.get_version(), Some(uuid::Version::SortRand));
+    let create = get!(&helixflow, "TaskBox::create");
     assert!(helixflow.get_create_enabled());
+    assert!(create.accessible_enabled().unwrap());
 }


Reviews:
user: login: sourcery-ai[bot]
body: Hey @MusicalNinjaDad - I've reviewed your changes - here's some feedback:

- Consider gating the big `helixflow_slint::test` module and its helper macros behind a dedicated Cargo feature (e.g. `test_helpers`) so you don’t bloat release builds with test‐only code and dependencies.
- The library crates currently require nightly features (`assert_matches`, `cfg_boolean_literals`); to stay on stable Rust consider swapping to the `assert_matches` crate or stable patterns instead of `#![feature(...)]`.
- The doc comment on `TaskList::all` is describing internal closure quirks rather than the public API—trim or rewrite it so it clearly states what callers should expect, without the `?` operator caveat.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 2 issues found
- 🟢 **Security**: all looks good
- 🟢 **Review instructions**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T18:32:20Z
user: login: MusicalNinjaDad
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-22T18:33:57Z
user: login: sourcery-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T18:34:05Z
user: login: MusicalNinjaDad
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-22T18:35:06Z
user: login: sourcery-ai[bot]
body: Hey @MusicalNinjaDad - I've reviewed your changes and they look great!

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 2 issues found
- 🟢 **Security**: all looks good
- 🟢 **Review instructions**: all looks good
- 🟡 **Testing**: 1 issue found
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T18:38:20Z

Review comments:
diff_hunk: @@ -1,6 +1,32 @@
+use std::{env, fs, path::PathBuf};
+
 fn main() {
-    // Set SLINT_EMIT_DEBUG_INFO=1 for tests, rust-analyzer, etc.
+    let mut slint_config = slint_build::CompilerConfiguration::new();
+
     if std::env::var("DEBUG") == Ok("true".to_string()) {
-        println!("cargo:rustc-env=SLINT_EMIT_DEBUG_INFO=1");
+        println!("cargo:rustc-env=SLINT_EMIT_DEBUG_INFO=1"); // for `slint!` macro
+        slint_config = slint_config.with_debug_info(true);
+        build_all_slint_modules(&slint_config);
+    }
+
+    slint_build::compile_with_config("src/helixflow.slint", slint_config).unwrap();
+}
path: ui/helixflow-slint/build.rs
user: login: sourcery-ai[bot]
body: **suggestion (performance):** Potential redundant compilation of .slint files.

Both build_all_slint_modules and main() compile src/helixflow.slint, which may cause redundant work and slower builds. Please deduplicate this logic.

```suggestion
    if std::env::var("DEBUG") == Ok("true".to_string()) {
        println!("cargo:rustc-env=SLINT_EMIT_DEBUG_INFO=1"); // for `slint!` macro
        slint_config = slint_config.with_debug_info(true);
    }
    build_all_slint_modules(&slint_config);
}
```
created_at: 2025-05-22T18:32:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,34 +1,23 @@
-use std::{
-    panic::{self, PanicHookInfo},
-    rc::Rc,
-    sync::OnceLock,
-};
-
-use i_slint_backend_testing::ElementHandle;
-use slint::ComponentHandle;
+//! Use nextest - these tests will fail on cargo test as they MUST run is separate processes
+//! for `i_slint_backend_testing::init_integration_test_with_system_time`
+
+use std::rc::Rc;
+
 use slint::platform::PointerEventButton;
+use slint::{ComponentHandle, Global};
 
-use helixflow_core::task::blocking::TestBackend;
-use helixflow_slint::{HelixFlow, blocking::create_task};
+use helixflow_core::task::{Task, blocking::TestBackend};
+use helixflow_slint::{CurrentTask, HelixFlow, task::blocking::create_task, test::*};
 
 #[test]
 fn test_set_task_id() {
-    // Slint's event_loop doesn't propogate panics from background tasks
-    //   so we need to actively track if any occur.
-    static PANICKED: OnceLock<bool> = OnceLock::new();
-    static DEFAULT_HOOK: OnceLock<Box<dyn Fn(&PanicHookInfo) + Sync + Send + 'static>> =
-        OnceLock::new();
-    let _ = DEFAULT_HOOK.set(panic::take_hook());
-
-    panic::set_hook(Box::new(|info| {
-        DEFAULT_HOOK.get().unwrap()(info);
-        let _ = PANICKED.set(true);
-    }));
-    i_slint_backend_testing::init_integration_test_with_system_time();
+    prepare_slint!();
 
     let helixflow = HelixFlow::new().unwrap();
     let backend = Rc::new(TestBackend);
 
+    list_elements!(&helixflow);
path: ui/helixflow-slint/tests/slint_TaskCRUD.rs
user: login: sourcery-ai[bot]
body: **nitpick:** Consider making `list_elements!` invocation conditional.

Since the macro outputs on every run, consider wrapping it with a condition (like an environment variable or debug flag) to reduce noise in successful test runs, or remove it if only needed for temporary debugging.
created_at: 2025-05-22T18:32:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,153 +1,116 @@
-use std::rc::Weak;
-
-use slint::slint;
-
-use helixflow_core::task::Task;
-
-slint! {
-    import { Button, LineEdit, VerticalBox } from "std-widgets.slint";
-    export component HelixFlow inherits Window {
-        callback create_task;
-        in property <bool> create_enabled: true;
-        in property <string> task_id: "";
-        in-out property <string> task_name: task_name_entry.text;
-        VerticalBox {
-            task_name_entry := LineEdit {
-                accessible_label: "Task name";
-                placeholder-text: self.accessible_label;
-            }
-            task_id_display := Text {
-                accessible_label: "Task ID";
-                text: root.task_id;
-                accessible_value: self.text;
-            }
-            create := Button {
-                enabled: root.create_enabled;
-                text: "Create";
-                clicked() => { root.create_task(); }
-            }
-        }
-    }
-}
-
-pub mod blocking {
-    use super::*;
-    use helixflow_core::task::blocking::{CRUD, StorageBackend};
-
-    pub fn create_task<BKEND>(
-        helixflow: slint::Weak<HelixFlow>,
-        backend: Weak<BKEND>,
-    ) -> impl FnMut() + 'static
-    where
-        BKEND: StorageBackend + 'static,
-    {
-        move || {
-            let helixflow = helixflow.unwrap();
-            let backend = backend.upgrade().unwrap();
-            helixflow.set_create_enabled(false);
-            let task_name: String = helixflow.get_task_name().into();
-            let task = Task::new(task_name, None);
-            task.create(backend.as_ref()).unwrap();
-            let task_id = task.id;
-            helixflow.set_task_id(format!("{task_id}").into());
-            helixflow.set_create_enabled(true);
-        }
+#![feature(assert_matches)]
+
+slint::include_modules!();
+
+pub mod task;
+
+/// Helper macros & re-exports to simplify testing: `use helixflow_slint::test::*`
+pub mod test {
path: ui/helixflow-slint/src/lib.rs
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider moving the test helper module and its macros from `lib.rs` to a separate file and gating it behind a feature flag to keep the main library focused and avoid shipping test code in normal builds.

Consider pulling the entire test-helper module (and its macros) out of `lib.rs` into its own file (or even its own crate behind a feature flag), and gating it so it doesn’t ship in normal builds. For example:

1) In `src/lib.rs` keep only the public API and a minimal stub:

```rust
#![feature(assert_matches)]
slint::include_modules!();
pub mod task;

// only compile this in tests (or under a special feature)
#[cfg(all(test, feature = "slint-test-utils"))]
pub mod test;
```

2) Create `src/test.rs` and move everything you had inside `pub mod test { … }` into that file, e.g.:

```rust
// src/test.rs

// only available in test or via feature
use std::{
    panic::{self, PanicHookInfo},
    sync::OnceLock,
};

pub use assert_unordered::assert_eq_unordered_sort;
pub use i_slint_backend_testing::{ElementHandle, ElementRoot};

// your macros, unmodified:
#[macro_export]
#[doc(hidden)]
macro_rules! prepare_slint {
    () => {
        /* ... */
    };
}
pub use prepare_slint;

// … rest of your macros and re-exports …
```

3) In `Cargo.toml` add a feature so consumers opt-into the test helpers:

```toml
[features]
slint-test-utils = []
```

This way:

- `lib.rs` stays small and focused.
- All test helpers live in one file.
- They’re only compiled when you enable the feature (or run `cargo test`).
- You still re-export them via `pub mod test` when you want them.
created_at: 2025-05-22T18:32:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,6 +1,32 @@
+use std::{env, fs, path::PathBuf};
+
 fn main() {
-    // Set SLINT_EMIT_DEBUG_INFO=1 for tests, rust-analyzer, etc.
+    let mut slint_config = slint_build::CompilerConfiguration::new();
+
     if std::env::var("DEBUG") == Ok("true".to_string()) {
-        println!("cargo:rustc-env=SLINT_EMIT_DEBUG_INFO=1");
+        println!("cargo:rustc-env=SLINT_EMIT_DEBUG_INFO=1"); // for `slint!` macro
+        slint_config = slint_config.with_debug_info(true);
+        build_all_slint_modules(&slint_config);
+    }
+
+    slint_build::compile_with_config("src/helixflow.slint", slint_config).unwrap();
+}
path: ui/helixflow-slint/build.rs
user: login: MusicalNinjaDad
body: @sourcery-ai issue
created_at: 2025-05-22T18:33:57Z
author_association: OWNER
diff_hunk: @@ -1,6 +1,32 @@
+use std::{env, fs, path::PathBuf};
+
 fn main() {
-    // Set SLINT_EMIT_DEBUG_INFO=1 for tests, rust-analyzer, etc.
+    let mut slint_config = slint_build::CompilerConfiguration::new();
+
     if std::env::var("DEBUG") == Ok("true".to_string()) {
-        println!("cargo:rustc-env=SLINT_EMIT_DEBUG_INFO=1");
+        println!("cargo:rustc-env=SLINT_EMIT_DEBUG_INFO=1"); // for `slint!` macro
+        slint_config = slint_config.with_debug_info(true);
+        build_all_slint_modules(&slint_config);
+    }
+
+    slint_build::compile_with_config("src/helixflow.slint", slint_config).unwrap();
+}
path: ui/helixflow-slint/build.rs
user: login: sourcery-ai[bot]
body: I have created an issue for your comment: https://github.com/MusicalNinjaDad/HelixFlow/issues/8
created_at: 2025-05-22T18:34:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,153 +1,116 @@
-use std::rc::Weak;
-
-use slint::slint;
-
-use helixflow_core::task::Task;
-
-slint! {
-    import { Button, LineEdit, VerticalBox } from "std-widgets.slint";
-    export component HelixFlow inherits Window {
-        callback create_task;
-        in property <bool> create_enabled: true;
-        in property <string> task_id: "";
-        in-out property <string> task_name: task_name_entry.text;
-        VerticalBox {
-            task_name_entry := LineEdit {
-                accessible_label: "Task name";
-                placeholder-text: self.accessible_label;
-            }
-            task_id_display := Text {
-                accessible_label: "Task ID";
-                text: root.task_id;
-                accessible_value: self.text;
-            }
-            create := Button {
-                enabled: root.create_enabled;
-                text: "Create";
-                clicked() => { root.create_task(); }
-            }
-        }
-    }
-}
-
-pub mod blocking {
-    use super::*;
-    use helixflow_core::task::blocking::{CRUD, StorageBackend};
-
-    pub fn create_task<BKEND>(
-        helixflow: slint::Weak<HelixFlow>,
-        backend: Weak<BKEND>,
-    ) -> impl FnMut() + 'static
-    where
-        BKEND: StorageBackend + 'static,
-    {
-        move || {
-            let helixflow = helixflow.unwrap();
-            let backend = backend.upgrade().unwrap();
-            helixflow.set_create_enabled(false);
-            let task_name: String = helixflow.get_task_name().into();
-            let task = Task::new(task_name, None);
-            task.create(backend.as_ref()).unwrap();
-            let task_id = task.id;
-            helixflow.set_task_id(format!("{task_id}").into());
-            helixflow.set_create_enabled(true);
-        }
+#![feature(assert_matches)]
+
+slint::include_modules!();
+
+pub mod task;
+
+/// Helper macros & re-exports to simplify testing: `use helixflow_slint::test::*`
+pub mod test {
path: ui/helixflow-slint/src/lib.rs
user: login: MusicalNinjaDad
body: @sourcery-ai issue
created_at: 2025-05-22T18:35:06Z
author_association: OWNER
diff_hunk: @@ -199,6 +205,21 @@ pub mod blocking {
             }
         }
 
+        #[test]
+        fn test_get_tasks() {
path: backends/helixflow-surreal/src/lib.rs
user: login: sourcery-ai[bot]
body: **issue (testing):** Test relies on deterministic task order

Database queries may return tasks in any order, causing test flakiness. Sort results or use order-independent assertions to ensure reliability.
created_at: 2025-05-22T18:38:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,153 +1,116 @@
-use std::rc::Weak;
-
-use slint::slint;
-
-use helixflow_core::task::Task;
-
-slint! {
-    import { Button, LineEdit, VerticalBox } from "std-widgets.slint";
-    export component HelixFlow inherits Window {
-        callback create_task;
-        in property <bool> create_enabled: true;
-        in property <string> task_id: "";
-        in-out property <string> task_name: task_name_entry.text;
-        VerticalBox {
-            task_name_entry := LineEdit {
-                accessible_label: "Task name";
-                placeholder-text: self.accessible_label;
-            }
-            task_id_display := Text {
-                accessible_label: "Task ID";
-                text: root.task_id;
-                accessible_value: self.text;
-            }
-            create := Button {
-                enabled: root.create_enabled;
-                text: "Create";
-                clicked() => { root.create_task(); }
-            }
-        }
-    }
-}
-
-pub mod blocking {
-    use super::*;
-    use helixflow_core::task::blocking::{CRUD, StorageBackend};
-
-    pub fn create_task<BKEND>(
-        helixflow: slint::Weak<HelixFlow>,
-        backend: Weak<BKEND>,
-    ) -> impl FnMut() + 'static
-    where
-        BKEND: StorageBackend + 'static,
-    {
-        move || {
-            let helixflow = helixflow.unwrap();
-            let backend = backend.upgrade().unwrap();
-            helixflow.set_create_enabled(false);
-            let task_name: String = helixflow.get_task_name().into();
-            let task = Task::new(task_name, None);
-            task.create(backend.as_ref()).unwrap();
-            let task_id = task.id;
-            helixflow.set_task_id(format!("{task_id}").into());
-            helixflow.set_create_enabled(true);
-        }
+#![feature(assert_matches)]
+
+slint::include_modules!();
+
+pub mod task;
+
+/// Helper macros & re-exports to simplify testing: `use helixflow_slint::test::*`
+pub mod test {
+    pub use std::{
+        panic::{self, PanicHookInfo},
+        sync::OnceLock,
+    };
+
+    // TODO: Stick this module and following dependencies behind a feature flag.
+    pub use assert_unordered::assert_eq_unordered_sort;
+    pub use i_slint_backend_testing::{ElementHandle, ElementRoot};
+
+    #[macro_export]
+    #[doc(hidden)]
+    /// Slint's event_loop doesn't propogate panics from background task so we create a custom panic
+    /// handler to actively track if any occur before calling `init_integration_test_with_system_time`.
+    ///
+    /// Use `run_slint_loop!()` to run the even loop and then check for panics.
+    macro_rules! prepare_slint {
+        () => {
+            static PANICKED: OnceLock<bool> = OnceLock::new();
+            static DEFAULT_HOOK: OnceLock<Box<dyn Fn(&PanicHookInfo) + Sync + Send + 'static>> =
+                OnceLock::new();
+            let _ = DEFAULT_HOOK.set(panic::take_hook());
+
+            panic::set_hook(Box::new(|info| {
path: ui/helixflow-slint/src/lib.rs
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** Original panic hook not restored

Restore the default panic hook after run_slint_loop to prevent side effects in subsequent code.
created_at: 2025-05-22T18:38:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,5 +2,6 @@
 //! needed for the app.
 
 #![feature(assert_matches)]
+#![feature(cfg_boolean_literals)]
path: helixflow-core/src/lib.rs
user: login: sourcery-ai[bot]
body: **nitpick:** Unused unstable feature enabled

Remove the cfg_boolean_literals feature flag since it's not used, reducing unnecessary nightly dependencies.
created_at: 2025-05-22T18:38:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,6 +1,32 @@
+use std::{env, fs, path::PathBuf};
+
 fn main() {
path: ui/helixflow-slint/build.rs
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider replacing the custom file iteration logic with the built-in compile_dir function to simplify the build script.

```suggestion
You can remove the entire `build_all_slint_modules` and instead use the built‐in directory compiler:

```rust
use std::env;
use slint_build::{compile_dir, compile_with_config, CompilerConfiguration};

fn main() {
    let mut config = CompilerConfiguration::new();

    if env::var("DEBUG") == Ok("true".into()) {
        println!("cargo:rustc-env=SLINT_EMIT_DEBUG_INFO=1");
        config = config.with_debug_info(true);
        // replaces build_all_slint_modules:
        compile_dir("src", &config).unwrap();
    }

    // compile your main .slint
    compile_with_config("src/helixflow.slint", config).unwrap();
}
```

Then delete:

- The `build_all_slint_modules` function  
- `use glob::glob;`, `use std::{fs, path::PathBuf}`, and the manual `fs::create_dir_all` logic  

This preserves debug vs release behavior and removes boilerplate. ```
created_at: 2025-05-22T18:38:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-22T18:24:53Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide

This pull request refactors the Slint-based UI into modular TaskBox and HelixFlow components with a shared SlintTask and CurrentTask global, extends core and backend APIs with TaskList::all and get_tasks support, implements bi-directional conversions between UI and core tasks, introduces reusable Slint integration test macros, and streamlines build and container setup along with dependency upgrades.

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Modularize Slint UI into TaskBox and HelixFlow components | <ul><li>Extract TaskBox and HelixFlow into separate .slint files</li><li>Use slint::include_modules! to import modules at build time</li><li>Define shared SlintTask struct and CurrentTask global</li><li>Move create_task logic to update CurrentTask</li></ul> | `ui/helixflow-slint/src/lib.rs`<br/>`ui/helixflow-slint/src/task.slint`<br/>`ui/helixflow-slint/src/helixflow.slint` |
| Add TaskList::all API and backend get_tasks support | <ul><li>Introduce TaskList struct with all() returning an iterator</li><li>Extend StorageBackend trait with get_tasks()</li><li>Implement get_tasks in TestBackend and SurrealDb</li><li>Add corresponding unit tests for task iteration</li></ul> | `helixflow-core/src/task.rs`<br/>`backends/helixflow-surreal/src/lib.rs` |
| Implement conversions between SlintTask and core Task | <ul><li>Add TryFrom<SlintTask> for Task</li><li>Add From<Task> for SlintTask</li><li>Write unit tests for both conversion paths</li></ul> | `ui/helixflow-slint/src/task.rs` |
| Introduce reusable Slint integration test macros | <ul><li>Define prepare_slint, run_slint_loop, list_elements, get, and assert_components macros</li><li>Rework existing UI tests to use these macros</li><li>Centralize panic handling and element querying logic</li></ul> | `ui/helixflow-slint/src/lib.rs`<br/>`ui/helixflow-slint/tests/slint_TaskCRUD.rs`<br/>`helixflow/tests/slint_surreal.rs` |
| Enhance build and container configuration | <ul><li>Update build.rs to glob-compile all .slint modules and toggle debug info</li><li>Revise .devcontainer Dockerfile for unified Rust toolchain setup and permissions</li><li>Upgrade dependencies to Git sources and bump versions in Cargo.toml</li><li>Add nextest.toml and workspace test/build dependencies</li></ul> | `ui/helixflow-slint/build.rs`<br/>`.devcontainer/Dockerfile`<br/>`Cargo.toml`<br/>`ui/helixflow-slint/Cargo.toml`<br/>`.config/nextest.toml` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR
user: login: MusicalNinjaDad
created_at: 2025-05-22T18:32:06Z
body: @sourcery-ai review
author_association: OWNER