Fix/monday patches (#7)

* Update Welcome page to integrate OAuth provider and adjust branding

- Added OAuthProviderButton to the Welcome component for Google authentication.
- Updated the branding from AlphaHuman to OpenHuman in the FeaturesStep component.
- Removed unnecessary Lottie animation from the Onboarding page for a cleaner layout.
- Changed the backend URL to point to the new API endpoint for TinyHumans.

* Refactor routing and sidebar components for improved clarity

- Commented out the Mnemonic route and related logic in AppRoutes for future consideration.
- Updated MiniSidebar to comment out the Invite Friends section.
- Removed unused navigation logic and upgrade call-to-action from the Home page.
- Adjusted Onboarding navigation to redirect to Home instead of Mnemonic.

* Comment out unused settings menu items for future consideration in SettingsHome component

* Add core process and RPC functionality for Alphahuman

- Introduced new binaries: `alphahuman-core` and `alphahuman-cli` for core process management and command-line interaction.
- Implemented `CoreProcessHandle` for managing the lifecycle of the core process.
- Added `core_rpc` module for handling RPC requests and responses.
- Created `core_server` module to manage core server logic and routing.
- Updated `alphahuman` commands to utilize the new RPC structure for health checks, security policies, and configuration management.
- Refactored existing code to streamline interactions with the core process and improve overall architecture.

* Update dependencies and enhance CLI functionality

- Added `clap` for command-line argument parsing in the `alphahuman-cli`.
- Updated `Cargo.lock` and `Cargo.toml` to include `clap` and its features.
- Refactored `alphahuman-cli` to utilize structured command handling with subcommands.
- Improved core process management and logging in `core_process.rs`.
- Enhanced routing and error handling in `core_server.rs` with new root and not found handlers.
- Updated various commands in `alphahuman.rs` to ensure core process is running before executing RPC calls.

* Enhance service management and CLI functionality

- Added new functions for daemon program arguments and command line construction in the service module.
- Updated macOS and Linux installation functions to dynamically generate program arguments and command lines.
- Introduced a new `Reinstall` command in the CLI for easier service management.
- Refactored service command handling to utilize local service functions for improved clarity and maintainability.

* Refactor daemon references to agent in components and hooks

- Updated terminology from "Daemon" to "Agent" in MiniSidebar, DaemonHealthPanel, and TauriCommandsPanel for consistency.
- Modified useDaemonHealth hook to probe agent status and handle agent lifecycle management.
- Introduced new agent server status interface and function in Tauri commands for improved status checking.
- Adjusted related comments and error handling to reflect the changes in terminology.

* Refactor Home component and enhance macOS service management

- Updated Home component to navigate to in-app conversations instead of opening a Telegram bot link.
- Improved macOS service management by implementing modern lifecycle commands and adding compatibility fallbacks for service control.
- Introduced new utility functions for handling macOS GUI domain and service targets.

* Refactor Home component to enhance navigation

- Removed SkillsGrid component and replaced it with a button that navigates to the Skills page.
- Improved layout with additional margin for better spacing in the Home component.

* Refactor SkillProvider and enhance memory bridge functionality

- Removed Gmail and Notion state synchronization functions from SkillProvider to streamline the component.
- Introduced a new memory bridge in the QuickJS library for skills to send memory payloads to the backend.
- Updated SkillContext to include an app handle for better integration with memory operations.
- Enhanced state management in the Rust backend to support memory insertion from skills.

* Enhance memory insertion functionality and update QuickJS integration

- Added support for additional parameters in the `store_skill_sync` method, including `source_type`, `metadata`, `priority`, `created_at`, `updated_at`, and `document_id`.
- Refactored the `memory_insert` operation to accept a structured input, improving validation and error handling for title and content.
- Updated the JavaScript memory bridge to simplify the insertion process by removing the provider parameter and directly using metadata.
- Enhanced logging for successful memory insertions to improve traceability.

* Update subproject reference in skills module

* Implement skill synchronization UI enhancements

- Introduced a new `skillsSyncUi.ts` module to manage synchronization UI state for skills.
- Updated `Skills.tsx` to utilize the new synchronization state, providing visual feedback during sync operations.
- Refactored sync handling in `SkillCard` to improve user experience with progress indicators and messages.
- Added unit tests for the synchronization UI state logic to ensure correct behavior across various scenarios.
This commit is contained in:
Steven Enamakel
2026-03-24 23:22:09 -07:00
committed by GitHub
parent 37318bc73d
commit aacfd45057
10 changed files with 371 additions and 128 deletions
+1 -1
Submodule skills updated: c389a2be6d...ff2534fc4d
+19 -2
View File
@@ -6,8 +6,8 @@
use std::sync::Arc;
use tinyhumansai::{
DeleteMemoryParams, InsertMemoryParams, QueryMemoryParams, RecallMemoryParams,
TinyHumanConfig, TinyHumanMemoryClient,
DeleteMemoryParams, InsertMemoryParams, Priority, QueryMemoryParams, RecallMemoryParams,
SourceType, TinyHumanConfig, TinyHumanMemoryClient,
};
/// Shared, cloneable handle to the memory client.
@@ -59,6 +59,12 @@ impl MemoryClient {
integration_id: &str,
title: &str,
content: &str,
source_type: Option<SourceType>,
metadata: Option<serde_json::Value>,
priority: Option<Priority>,
created_at: Option<f64>,
updated_at: Option<f64>,
document_id: Option<String>,
) -> Result<(), String> {
let namespace = format!("skill:{skill_id}:{integration_id}");
log::info!("[memory] store_skill_sync: entry (namespace={namespace}, title={title:?}, content_len={})", content.len());
@@ -72,6 +78,12 @@ impl MemoryClient {
title: title.to_string(),
content: content.to_string(),
namespace: namespace.clone(),
source_type,
metadata,
priority,
created_at,
updated_at,
document_id,
..Default::default()
})
.await
@@ -236,6 +248,11 @@ mod tests {
integration_id,
"Gmail OAuth sync — test@alphahuman.dev",
&serde_json::to_string_pretty(&dummy_content).unwrap(),
None,
None,
None,
None,
None,
)
.await;
+25 -2
View File
@@ -178,6 +178,7 @@ impl QjsSkillInstance {
let skill_context = qjs_ops::SkillContext {
skill_id: skill_id.clone(),
data_dir: data_dir.clone(),
app_handle: _deps.app_handle.clone(),
};
if let Err(e) = qjs_ops::register_ops(
@@ -603,7 +604,18 @@ async fn handle_message(
let title = format!("{} OAuth sync — {}", skill, integration_id);
tokio::spawn(async move {
if let Err(e) = client
.store_skill_sync(&skill, &integration_id, &title, &content)
.store_skill_sync(
&skill,
&integration_id,
&title,
&content,
None,
None,
None,
None,
None,
None,
)
.await
{
log::warn!("[memory] store_skill_sync failed: {e}");
@@ -639,7 +651,18 @@ async fn handle_message(
let title = format!("{} periodic sync", skill);
tokio::spawn(async move {
if let Err(e) = client
.store_skill_sync(&skill, "default", &title, &content)
.store_skill_sync(
&skill,
"default",
&title,
&content,
None,
None,
None,
None,
None,
None,
)
.await
{
log::warn!("[memory] store_skill_sync failed: {e}");
+20
View File
@@ -741,6 +741,26 @@ globalThis.platform = {
},
};
// ============================================================================
// Memory Bridge (for skills to send memory payloads to backend)
// ============================================================================
globalThis.memory = {
/**
* Insert a memory payload through the native memory bridge.
* Provider is inferred from the current skill ID on the Rust side.
* @param {object} metadata - Memory payload metadata.
* @returns {boolean}
*/
insert: function (metadata) {
if (!metadata || typeof metadata !== 'object') {
throw new Error('memory.insert requires an object payload');
}
__ops.memory_insert(JSON.stringify(metadata));
return true;
},
};
// ============================================================================
// State Bridge API (for skills to publish state)
// ============================================================================
@@ -2,10 +2,26 @@
use parking_lot::RwLock;
use rquickjs::{Ctx, Function, Object};
use serde::Deserialize;
use std::sync::Arc;
use tauri::Manager;
use tinyhumansai::{Priority, SourceType};
use super::types::{js_err, SkillContext, SkillState};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct JsMemoryInsertInput {
title: String,
content: String,
source_type: Option<String>,
metadata: Option<serde_json::Value>,
priority: Option<String>,
created_at: Option<f64>,
updated_at: Option<f64>,
document_id: Option<String>,
}
pub fn register<'js>(
ctx: &Ctx<'js>,
ops: &Object<'js>,
@@ -72,7 +88,7 @@ pub fn register<'js>(
}
{
let sc = skill_context;
let sc = skill_context.clone();
ops.set("data_write", Function::new(ctx.clone(),
move |filename: String, content: String| -> rquickjs::Result<()> {
let path = sc.data_dir.join(&filename);
@@ -81,5 +97,87 @@ pub fn register<'js>(
))?;
}
// ========================================================================
// Memory Bridge (1)
// ========================================================================
{
let sc = skill_context;
ops.set("memory_insert", Function::new(ctx.clone(),
move |metadata_json: String| -> rquickjs::Result<()> {
let input: JsMemoryInsertInput =
serde_json::from_str(&metadata_json).map_err(|e| js_err(e.to_string()))?;
if input.title.trim().is_empty() {
return Err(js_err("memory.insert requires a non-empty title"));
}
if input.content.trim().is_empty() {
return Err(js_err("memory.insert requires non-empty content"));
}
let app_handle = sc
.app_handle
.clone()
.ok_or_else(|| js_err("App handle not available for memory insert"))?;
let memory_state = app_handle
.try_state::<crate::commands::memory::MemoryState>()
.ok_or_else(|| js_err("Memory state not available"))?;
let client_opt = memory_state
.0
.lock()
.map_err(|_| js_err("Failed to lock memory state"))?
.clone();
let client = client_opt.ok_or_else(|| js_err("Memory client is not initialized"))?;
let skill_id = sc.skill_id.clone();
let integration_id = sc.skill_id.clone();
let source_type = match input.source_type.as_deref() {
Some("doc") => Some(SourceType::Doc),
Some("chat") => Some(SourceType::Chat),
Some("email") => Some(SourceType::Email),
Some(_) => return Err(js_err("sourceType must be one of: doc, chat, email")),
None => None,
};
let priority = match input.priority.as_deref() {
Some("high") => Some(Priority::High),
Some("medium") => Some(Priority::Medium),
Some("low") => Some(Priority::Low),
Some(_) => return Err(js_err("priority must be one of: high, medium, low")),
None => None,
};
let metadata = input.metadata.unwrap_or_else(|| serde_json::json!({}));
tokio::spawn(async move {
if let Err(e) = client
.store_skill_sync(
&skill_id,
&integration_id,
&input.title,
&input.content,
source_type,
Some(metadata),
priority,
input.created_at,
input.updated_at,
input.document_id,
)
.await
{
log::warn!("[quickjs] memory_insert failed for '{}': {}", integration_id, e);
} else {
log::info!(
"[quickjs] memory_insert stored '{}': title='{}'",
integration_id,
input.title
);
}
});
Ok(())
},
))?;
}
Ok(())
}
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use tauri::AppHandle;
// ============================================================================
// Timer State
@@ -76,6 +77,7 @@ pub fn poll_timers(timer_state: &RwLock<TimerState>) -> (Vec<u32>, Option<Durati
pub struct SkillContext {
pub skill_id: String,
pub data_dir: PathBuf,
pub app_handle: Option<AppHandle>,
}
// ============================================================================
+29 -10
View File
@@ -14,6 +14,7 @@ import SkillSetupModal from '../components/skills/SkillSetupModal';
import { deriveConnectionStatus, useSkillConnectionStatus } from '../lib/skills/hooks';
import { skillManager } from '../lib/skills/manager';
import type { SkillConnectionStatus, SkillHostConnectionState } from '../lib/skills/types';
import { deriveSkillSyncUiState } from './skillsSyncUi';
import { useAppSelector } from '../store/hooks';
import { IS_DEV } from '../utils/config';
@@ -43,7 +44,6 @@ function statusDotClass(status: SkillConnectionStatus): string {
interface SkillCardProps {
skill: SkillListEntry;
onSetup: () => void;
onSync: () => void;
}
function SkillCard({ skill, onSetup }: SkillCardProps) {
@@ -52,17 +52,19 @@ function SkillCard({ skill, onSetup }: SkillCardProps) {
const skillState = useAppSelector(state => state.skills.skillStates[skill.id]) as
| (SkillHostConnectionState & Record<string, unknown>)
| undefined;
const [syncing, setSyncing] = useState(false);
const [manualSyncing, setManualSyncing] = useState(false);
const syncUi = useMemo(() => deriveSkillSyncUiState(skill.id, skillState), [skill.id, skillState]);
const isSyncing = manualSyncing || syncUi.isSyncing;
const handleSync = async (e: React.MouseEvent) => {
e.stopPropagation();
setSyncing(true);
setManualSyncing(true);
try {
await skillManager.triggerSync(skill.id);
} catch (err) {
console.error(`Sync failed for ${skill.id}:`, err);
} finally {
setSyncing(false);
setManualSyncing(false);
}
};
@@ -98,6 +100,26 @@ function SkillCard({ skill, onSetup }: SkillCardProps) {
{subtitleParts.length > 0 && (
<p className="text-[11px] text-stone-500 truncate mt-0.5">{subtitleParts.join(' · ')}</p>
)}
{isSyncing && (
<div className="mt-1.5">
<div className="h-1.5 w-full overflow-hidden rounded-full bg-stone-800">
{syncUi.progressPercent != null ? (
<div
className="h-full rounded-full bg-primary-400 transition-all duration-300"
style={{ width: `${syncUi.progressPercent}%` }}
/>
) : (
<div className="h-full w-1/2 rounded-full bg-primary-400/80 animate-pulse" />
)}
</div>
{syncUi.progressMessage && (
<p className="text-[11px] text-primary-300 truncate mt-1">{syncUi.progressMessage}</p>
)}
{syncUi.metricsText && (
<p className="text-[11px] text-stone-500 truncate mt-0.5">{syncUi.metricsText}</p>
)}
</div>
)}
</div>
{/* Actions */}
@@ -106,12 +128,12 @@ function SkillCard({ skill, onSetup }: SkillCardProps) {
<>
{/* Sync */}
<button
onClick={syncing ? undefined : handleSync}
disabled={syncing}
onClick={isSyncing ? undefined : handleSync}
disabled={isSyncing}
className="w-7 h-7 flex items-center justify-center rounded-lg text-stone-400 hover:text-white hover:bg-white/10 transition-colors disabled:opacity-40"
title="Sync">
<svg
className={`w-3.5 h-3.5 ${syncing ? 'animate-spin' : ''}`}
className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
@@ -282,9 +304,6 @@ export default function Skills() {
key={skill.id}
skill={skill}
onSetup={() => openSkillSetup(skill)}
onSync={() => {
skillManager.triggerSync(skill.id).catch(console.error);
}}
/>
))}
</div>
+58
View File
@@ -0,0 +1,58 @@
import { describe, expect, it } from 'vitest';
import { deriveSkillSyncUiState } from '../skillsSyncUi';
describe('deriveSkillSyncUiState', () => {
it('uses explicit progress and message for gmail', () => {
const result = deriveSkillSyncUiState('gmail', {
syncInProgress: true,
syncProgress: 42,
syncProgressMessage: 'Fetching page 2...',
totalEmails: 120,
newEmailsCount: 5,
});
expect(result.isSyncing).toBe(true);
expect(result.progressPercent).toBe(42);
expect(result.progressMessage).toBe('Fetching page 2...');
expect(result.metricsText).toContain('120 emails');
expect(result.metricsText).toContain('5 new emails');
});
it('falls back to indeterminate mode and default message when no numeric progress', () => {
const result = deriveSkillSyncUiState('notion', {
syncInProgress: true,
totalPages: 34,
pagesWithSummary: 12,
summariesPending: 4,
});
expect(result.isSyncing).toBe(true);
expect(result.progressPercent).toBeNull();
expect(result.progressMessage).toBe('Syncing Notion documents...');
expect(result.metricsText).toContain('34 pages');
expect(result.metricsText).toContain('12 pages summarized');
});
it('returns no sync UI when sync is idle', () => {
const result = deriveSkillSyncUiState('google-drive', {
syncInProgress: false,
syncProgress: 80,
syncProgressMessage: 'Syncing',
totalDocuments: 11,
});
expect(result.isSyncing).toBe(false);
expect(result.progressPercent).toBeNull();
expect(result.progressMessage).toBeNull();
expect(result.metricsText).toBeNull();
});
it('clamps out-of-range progress values', () => {
const high = deriveSkillSyncUiState('gmail', { syncInProgress: true, syncProgress: 150 });
const low = deriveSkillSyncUiState('gmail', { syncInProgress: true, syncProgress: -20 });
expect(high.progressPercent).toBe(100);
expect(low.progressPercent).toBe(0);
});
});
+118
View File
@@ -0,0 +1,118 @@
import type { SkillHostConnectionState } from '../lib/skills/types';
export interface SkillSyncUiState {
isSyncing: boolean;
progressPercent: number | null;
progressMessage: string | null;
metricsText: string | null;
}
type SkillStateRecord = SkillHostConnectionState & Record<string, unknown>;
function readNumber(value: unknown): number | null {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string' && value.trim() !== '') {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
function readBoolean(value: unknown): boolean | null {
return typeof value === 'boolean' ? value : null;
}
function clampPercent(value: number): number {
if (value < 0) return 0;
if (value > 100) return 100;
return value;
}
function formatNumber(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
return String(n);
}
function buildMetricsText(state: SkillStateRecord): string | null {
const values = {
newEmailsCount: readNumber(state.newEmailsCount),
totalEmails: readNumber(state.totalEmails),
totalDocuments: readNumber(state.totalDocuments),
totalPages: readNumber(state.totalPages),
pagesWithSummary: readNumber(state.pagesWithSummary),
summariesPending: readNumber(state.summariesPending),
totalFiles: readNumber(state.totalFiles),
itemsDone: readNumber(state.itemsDone),
itemsTotal: readNumber(state.itemsTotal),
};
const parts: string[] = [];
if (values.newEmailsCount != null) parts.push(`${formatNumber(values.newEmailsCount)} new emails`);
if (values.totalEmails != null) parts.push(`${formatNumber(values.totalEmails)} emails`);
if (values.totalDocuments != null) parts.push(`${formatNumber(values.totalDocuments)} docs`);
if (values.totalPages != null) parts.push(`${formatNumber(values.totalPages)} pages`);
if (values.pagesWithSummary != null)
parts.push(`${formatNumber(values.pagesWithSummary)} pages summarized`);
if (values.summariesPending != null)
parts.push(`${formatNumber(values.summariesPending)} summaries pending`);
if (values.totalFiles != null) parts.push(`${formatNumber(values.totalFiles)} files`);
if (values.itemsDone != null && values.itemsTotal != null && values.itemsTotal > 0) {
parts.push(`${formatNumber(values.itemsDone)}/${formatNumber(values.itemsTotal)} items`);
}
if (parts.length === 0) return null;
return parts.slice(0, 3).join(' · ');
}
function defaultProgressMessage(skillId: string): string {
if (skillId === 'gmail') return 'Syncing emails...';
if (skillId === 'google-drive') return 'Syncing documents...';
if (skillId === 'notion') return 'Syncing Notion documents...';
return 'Syncing...';
}
export function deriveSkillSyncUiState(
skillId: string,
skillState: SkillStateRecord | undefined
): SkillSyncUiState {
if (!skillState) {
return {
isSyncing: false,
progressPercent: null,
progressMessage: null,
metricsText: null,
};
}
const isSyncing = readBoolean(skillState.syncInProgress) === true;
const explicitProgress =
readNumber(skillState.syncProgress) ??
readNumber(skillState.progressPercent) ??
readNumber(skillState.progress);
const itemDone = readNumber(skillState.itemsDone);
const itemTotal = readNumber(skillState.itemsTotal);
const ratioProgress =
explicitProgress == null && itemDone != null && itemTotal != null && itemTotal > 0
? (itemDone / itemTotal) * 100
: null;
const progressPercent =
explicitProgress != null
? clampPercent(explicitProgress)
: ratioProgress != null
? clampPercent(ratioProgress)
: null;
const progressMessageRaw =
typeof skillState.syncProgressMessage === 'string' ? skillState.syncProgressMessage.trim() : '';
return {
isSyncing,
progressPercent: isSyncing ? progressPercent : null,
progressMessage: isSyncing ? progressMessageRaw || defaultProgressMessage(skillId) : null,
metricsText: isSyncing ? buildMetricsText(skillState) : null,
};
}
-112
View File
@@ -8,32 +8,10 @@ import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { type ReactNode, useEffect, useRef } from 'react';
import {
type GmailStateForSync,
syncGmailMetadataToBackend,
} from '../lib/gmail/services/metadataSync';
import {
type NotionStateForSync,
syncNotionMetadataToBackend,
} from '../lib/notion/services/metadataSync';
import { skillManager } from '../lib/skills/manager';
import type { SkillManifest } from '../lib/skills/types';
import { buildManualSentryEvent, enqueueError } from '../services/errorReportQueue';
import {
GmailEmailBatch,
type GmailProfile,
setGmailEmails,
setGmailProfile,
} from '../store/gmailSlice';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import {
type NotionPageSummary,
type NotionSummary,
type NotionUserProfile,
setNotionPages,
setNotionProfile,
setNotionSummaries,
} from '../store/notionSlice';
import { setSkillError, setSkillState, setSkillStatus } from '../store/skillsSlice';
import { DEV_AUTO_LOAD_SKILL, IS_DEV } from '../utils/config';
@@ -87,74 +65,12 @@ function parseSkillStatePayload(
return { skillId, state };
}
/** Sync profile and emails from gmail skill state into gmailSlice and send to backend via socket. */
function syncGmailStateToSlice(
gmailState: Record<string, unknown> | undefined,
dispatch: ReturnType<typeof useAppDispatch>
): void {
if (!gmailState || typeof gmailState !== 'object') return;
dispatch(
setGmailProfile(
gmailState.profile !== undefined && gmailState.profile != null
? (gmailState.profile as GmailProfile)
: null
)
);
dispatch(setGmailEmails(gmailState.emails as GmailEmailBatch | null));
syncGmailMetadataToBackend(gmailState as GmailStateForSync);
}
/** Sync profile, pages, and summaries from notion skill state into notionSlice and backend metadata. */
function syncNotionStateToSlice(
notionState: Record<string, unknown> | undefined,
dispatch: ReturnType<typeof useAppDispatch>
): void {
if (!notionState || typeof notionState !== 'object') return;
const profile =
notionState.profile !== undefined && notionState.profile != null
? (notionState.profile as NotionUserProfile)
: null;
const pages = Array.isArray(notionState.pages) ? (notionState.pages as NotionPageSummary[]) : [];
const summaries = Array.isArray(notionState.summaries)
? (notionState.summaries as NotionSummary[])
: [];
// Update profile in notionSlice if present
dispatch(setNotionProfile(profile));
if (pages.length > 0) {
dispatch(setNotionPages(pages));
}
if (summaries.length > 0) {
dispatch(setNotionSummaries(summaries));
}
const stateForSync: NotionStateForSync = { profile, pages, summaries };
syncNotionMetadataToBackend(stateForSync);
}
export default function SkillProvider({ children }: { children: ReactNode }) {
const { token } = useAppSelector(state => state.auth);
const skillsState = useAppSelector(state => state.skills.skills);
const skillStates = useAppSelector(state => state.skills.skillStates);
const dispatch = useAppDispatch();
const initRef = useRef(false);
// Keep gmailSlice in sync with skills.skillStates.gmail (event handler + rehydration)
const gmailSkillState = skillStates?.gmail as Record<string, unknown> | undefined;
useEffect(() => {
if (!gmailSkillState) return;
syncGmailStateToSlice(gmailSkillState, dispatch);
}, [gmailSkillState, dispatch]);
// Keep notionSlice pages in sync with skills.skillStates.notion
const notionSkillState = skillStates?.notion as Record<string, unknown> | undefined;
useEffect(() => {
if (!notionSkillState) return;
syncNotionStateToSlice(notionSkillState, dispatch);
}, [notionSkillState, dispatch]);
// Listen for skill state changes emitted from the Rust runtime event loop
useEffect(() => {
let unlisten: (() => void) | undefined;
@@ -166,15 +82,6 @@ export default function SkillProvider({ children }: { children: ReactNode }) {
const { skillId, state: newState } = parsed;
console.log('🚀 ~ SkillProvider ~ newState:', skillId, newState);
dispatch(setSkillState({ skillId, state: newState }));
// Transfer Gmail skill state to gmail store (also synced by effect from skillStates.gmail)
if (skillId === 'gmail') {
syncGmailStateToSlice(newState, dispatch);
}
// Transfer Notion skill state to notion store (also synced by effect from skillStates.notion)
if (skillId === 'notion') {
syncNotionStateToSlice(newState, dispatch);
}
})
.then(fn => {
unlisten = fn;
@@ -188,25 +95,6 @@ export default function SkillProvider({ children }: { children: ReactNode }) {
};
}, [dispatch]);
// Fallback: when gmail skill is ready, fetch state from backend (covers events missed before listener attached)
const gmailStatus = skillsState?.gmail?.status;
useEffect(() => {
if (gmailStatus !== 'ready') return;
const timeoutId = window.setTimeout(() => {
invoke<{ state?: Record<string, unknown> } | null>('runtime_get_skill_state', {
skillId: 'gmail',
})
.then(snapshot => {
if (snapshot?.state && typeof snapshot.state === 'object') {
dispatch(setSkillState({ skillId: 'gmail', state: snapshot.state }));
syncGmailStateToSlice(snapshot.state, dispatch);
}
})
.catch(() => {});
}, 800);
return () => window.clearTimeout(timeoutId);
}, [gmailStatus, dispatch]);
// Listen for skill runtime errors and surface them in the error notification
useEffect(() => {
let unlisten: (() => void) | undefined;