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.
This commit is contained in:
Steven Enamakel
2026-02-09 09:37:10 +05:30
parent 6ae0043ab1
commit ab9e7b7a0f
7 changed files with 72 additions and 19 deletions
+33 -3
View File
@@ -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<RwLock<SkillState>>,
skill_id: &str,
timer_state: &Arc<RwLock<qjs_ops::TimerState>>,
ops_state: &Arc<RwLock<qjs_ops::SkillState>>,
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<String, serde_json::Value> = 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 {
@@ -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(())
},
))?;
@@ -86,12 +86,16 @@ pub struct SkillContext {
pub struct SkillState {
#[serde(flatten)]
pub data: serde_json::Map<String, serde_json::Value>,
/// 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,
}
}
}
+2 -4
View File
@@ -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]);
+2 -8
View File
@@ -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) => {
</div>
)}
{isPreparing && (
<p className="text-xs opacity-50">Preparing AI model download...</p>
)}
{isPreparing && <p className="text-xs opacity-50">Preparing AI model download...</p>}
{error && !isLoading && (
<div className="space-y-2">
+2 -3
View File
@@ -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)));
}
};
+27 -1
View File
@@ -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<SkillManifest[]> {
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<string, unknown> }>(
'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;