From ab9e7b7a0f8dd366ce6b427ed324aba8cb5dc692 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Mon, 9 Feb 2026 09:37:10 +0530 Subject: [PATCH] Refactor code for improved readability and functionality - Simplified error handling in `useModelStatus` and `ModelProvider` by consolidating dispatch calls. - Streamlined greeting array initialization in `Welcome` component for better readability. - Enhanced `SkillProvider` to listen for skill state changes and dispatch updates accordingly. - Updated Rust backend to sync published state and emit events for skill state changes. - Introduced a `dirty` flag in `SkillState` to track modifications for efficient state management. --- src-tauri/src/runtime/qjs_skill_instance.rs | 36 +++++++++++++++++-- .../quickjs-libs/qjs_ops/ops_state.rs | 2 ++ .../services/quickjs-libs/qjs_ops/types.rs | 4 +++ src/hooks/useModelStatus.ts | 6 ++-- src/pages/Welcome.tsx | 10 ++---- src/providers/ModelProvider.tsx | 5 ++- src/providers/SkillProvider.tsx | 28 ++++++++++++++- 7 files changed, 72 insertions(+), 19 deletions(-) diff --git a/src-tauri/src/runtime/qjs_skill_instance.rs b/src-tauri/src/runtime/qjs_skill_instance.rs index 0b0b1abe9..47aa444df 100644 --- a/src-tauri/src/runtime/qjs_skill_instance.rs +++ b/src-tauri/src/runtime/qjs_skill_instance.rs @@ -256,7 +256,7 @@ impl QjsSkillInstance { log::info!("[skill:{}] Running (QuickJS)", config.skill_id); // Run the event loop - run_event_loop(&rt, &ctx, &mut rx, &state, &config.skill_id, &timer_state).await; + run_event_loop(&rt, &ctx, &mut rx, &state, &config.skill_id, &timer_state, &published_state, _deps.app_handle.as_ref()).await; }) } } @@ -270,7 +270,8 @@ impl QjsSkillInstance { /// 1. Polls for ready timers and fires their callbacks /// 2. Checks for incoming messages (non-blocking) /// 3. Runs the QuickJS job queue for promises/async ops -/// 4. Sleeps efficiently when idle +/// 4. Syncs published state from ops → instance and emits Tauri events +/// 5. Sleeps efficiently when idle async fn run_event_loop( rt: &rquickjs::AsyncRuntime, ctx: &rquickjs::AsyncContext, @@ -278,6 +279,8 @@ async fn run_event_loop( state: &Arc>, skill_id: &str, timer_state: &Arc>, + ops_state: &Arc>, + app_handle: Option<&tauri::AppHandle>, ) { // Maximum sleep duration when no timers are pending const MAX_IDLE_SLEEP: Duration = Duration::from_millis(100); @@ -317,7 +320,34 @@ async fn run_event_loop( // 3. Drive QuickJS job queue (process pending promises) drive_jobs(rt).await; - // 4. Calculate sleep duration based on next timer + // 4. Sync ops-level published state → instance published_state + emit event + { + let mut ops = ops_state.write(); + if ops.dirty { + ops.dirty = false; + // Convert serde_json::Map → HashMap for the instance snapshot + let new_map: HashMap = ops + .data + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + state.write().published_state = new_map.clone(); + + // Emit Tauri event so the frontend picks up the change + if let Some(handle) = app_handle { + use tauri::Emitter; + let _ = handle.emit( + "skill-state-changed", + serde_json::json!({ + "skillId": skill_id, + "state": new_map, + }), + ); + } + } + } + + // 5. Calculate sleep duration based on next timer let sleep_duration = { let (_, next_timer) = qjs_ops::poll_timers(timer_state); match next_timer { diff --git a/src-tauri/src/services/quickjs-libs/qjs_ops/ops_state.rs b/src-tauri/src/services/quickjs-libs/qjs_ops/ops_state.rs index af14eea2f..ddf1b892a 100644 --- a/src-tauri/src/services/quickjs-libs/qjs_ops/ops_state.rs +++ b/src-tauri/src/services/quickjs-libs/qjs_ops/ops_state.rs @@ -35,6 +35,7 @@ pub fn register<'js>( serde_json::from_str(&value_json).map_err(|e| js_err(e.to_string()))?; let mut state = ss.write(); state.data.insert(key, value); + state.dirty = true; Ok(()) }, ))?; @@ -50,6 +51,7 @@ pub fn register<'js>( for (k, v) in partial { state.data.insert(k, v); } + state.dirty = true; Ok(()) }, ))?; diff --git a/src-tauri/src/services/quickjs-libs/qjs_ops/types.rs b/src-tauri/src/services/quickjs-libs/qjs_ops/types.rs index 48078ada4..fbf58beb4 100644 --- a/src-tauri/src/services/quickjs-libs/qjs_ops/types.rs +++ b/src-tauri/src/services/quickjs-libs/qjs_ops/types.rs @@ -86,12 +86,16 @@ pub struct SkillContext { pub struct SkillState { #[serde(flatten)] pub data: serde_json::Map, + /// Set to true when data is modified; the event loop clears it after syncing. + #[serde(skip)] + pub dirty: bool, } impl Default for SkillState { fn default() -> Self { Self { data: serde_json::Map::new(), + dirty: false, } } } diff --git a/src/hooks/useModelStatus.ts b/src/hooks/useModelStatus.ts index 4b661aa88..2d2bfb4e5 100644 --- a/src/hooks/useModelStatus.ts +++ b/src/hooks/useModelStatus.ts @@ -3,11 +3,11 @@ import { useCallback } from 'react'; import { useAppDispatch, useAppSelector } from '../store/hooks'; import { + type ModelStatus, setDownloadTriggered, setModelError, setModelLoading, setModelStatus, - type ModelStatus, } from '../store/modelSlice'; /** @@ -39,9 +39,7 @@ export const useModelStatus = () => { await fetchStatus(); } catch (error) { console.error('[useModelStatus] Failed to start download:', error); - dispatch( - setModelError(error instanceof Error ? error.message : 'Failed to download model') - ); + dispatch(setModelError(error instanceof Error ? error.message : 'Failed to download model')); } }, [dispatch, fetchStatus]); diff --git a/src/pages/Welcome.tsx b/src/pages/Welcome.tsx index ae9901f35..c4d1855b1 100644 --- a/src/pages/Welcome.tsx +++ b/src/pages/Welcome.tsx @@ -10,11 +10,7 @@ interface WelcomeProps { } const Welcome = ({ isWeb }: WelcomeProps) => { - const greetings = [ - 'Hello HAL9000! 👋', - "Let's cook! 🔥", - 'The A-Team is here! 👊', - ]; + const greetings = ['Hello HAL9000! 👋', "Let's cook! 🔥", 'The A-Team is here! 👊']; const { isAvailable, isDownloaded, isLoading, downloadProgress, error, startDownload } = useModelStatus(); @@ -64,9 +60,7 @@ const Welcome = ({ isWeb }: WelcomeProps) => { )} - {isPreparing && ( -

Preparing AI model download...

- )} + {isPreparing &&

Preparing AI model download...

} {error && !isLoading && (
diff --git a/src/providers/ModelProvider.tsx b/src/providers/ModelProvider.tsx index 1d39d7297..bd575ec8e 100644 --- a/src/providers/ModelProvider.tsx +++ b/src/providers/ModelProvider.tsx @@ -4,11 +4,11 @@ import { useEffect } from 'react'; import { useAppDispatch, useAppSelector } from '../store/hooks'; import { + type ModelStatus, setDownloadTriggered, setModelError, setModelLoading, setModelStatus, - type ModelStatus, } from '../store/modelSlice'; const POLL_INTERVAL = 1000; @@ -93,8 +93,7 @@ const ModelProvider = ({ children }: { children: React.ReactNode }) => { if (!cancelled) dispatch(setModelStatus(finalStatus)); } catch (err) { console.error('[ModelProvider] Download failed:', err); - if (!cancelled) - dispatch(setModelError(err instanceof Error ? err.message : String(err))); + if (!cancelled) dispatch(setModelError(err instanceof Error ? err.message : String(err))); } }; diff --git a/src/providers/SkillProvider.tsx b/src/providers/SkillProvider.tsx index 3e8ccfd97..677c19ad5 100644 --- a/src/providers/SkillProvider.tsx +++ b/src/providers/SkillProvider.tsx @@ -5,11 +5,13 @@ * engine, registers them in Redux, and auto-starts skills with completed setup. */ import { invoke } from '@tauri-apps/api/core'; +import { listen } from '@tauri-apps/api/event'; import { type ReactNode, useEffect, useRef } from 'react'; import { skillManager } from '../lib/skills/manager'; import type { SkillManifest } from '../lib/skills/types'; -import { useAppSelector } from '../store/hooks'; +import { useAppDispatch, useAppSelector } from '../store/hooks'; +import { setSkillState } from '../store/skillsSlice'; import { DEV_AUTO_LOAD_SKILL, IS_DEV } from '../utils/config'; // --------------------------------------------------------------------------- @@ -53,8 +55,32 @@ async function discoverSkills(): Promise { export default function SkillProvider({ children }: { children: ReactNode }) { const { token } = useAppSelector(state => state.auth); const skillsState = useAppSelector(state => state.skills.skills); + const dispatch = useAppDispatch(); const initRef = useRef(false); + // Listen for skill state changes emitted from the Rust runtime event loop + useEffect(() => { + let unlisten: (() => void) | undefined; + + listen<{ skillId: string; state: Record }>( + 'skill-state-changed', + event => { + const { skillId, state: newState } = event.payload; + dispatch(setSkillState({ skillId, state: newState })); + } + ) + .then(fn => { + unlisten = fn; + }) + .catch(err => { + console.error('[SkillProvider] Failed to listen for skill-state-changed:', err); + }); + + return () => { + unlisten?.(); + }; + }, [dispatch]); + useEffect(() => { if (!token) return; if (initRef.current) return;