mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
Emit tool:sync socket event on skill state changes (#83)
* Refactor BillingPanel to use updated user usage structure - Changed the usage data source in BillingPanel from activeTeam to user, aligning with the new IUserUsage interface. - Updated the display of token usage percentage and progress bar to reflect the new usage metrics (spentThisCycleUsd and cycleBudgetUsd). - Cleaned up commented-out code and improved layout for better readability and user experience. * Enhance skill synchronization and connection status management - Exported `deriveConnectionStatus` function from hooks for better accessibility. - Introduced `syncToolsToBackend` function to synchronize tool states with the backend, ensuring up-to-date connection status. - Integrated `syncToolsToBackend` calls in `SkillManager` lifecycle methods and socket connection events to maintain consistent state across the application. - Added new `sync.ts` file to encapsulate synchronization logic, improving code organization and maintainability. * Add Rust-side tool:sync emission and document dual socket codebase Implement tool:sync on the Rust side so desktop/mobile apps emit skill state to the backend natively, without relying on the frontend WebView. - SocketManager: add sync_tools() method, derive_connection_status() helper, and sync_tools_via_channel() for event handler context - SocketManager: emit tool:sync on "ready" (connect/reconnect) - RuntimeEngine: store SocketManager reference, call sync_tools after start_skill, stop_skill, and auto_start_skills - lib.rs: wire SocketManager into RuntimeEngine during setup - CLAUDE.md: add Dual Socket Codebase pattern noting that any socket event change must be implemented in both TypeScript and Rust Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4be6693599
commit
d0048718ea
@@ -13,7 +13,7 @@ import type {
|
||||
* Derive a unified connection status from the skill's lifecycle status
|
||||
* and its self-reported connection/auth state.
|
||||
*/
|
||||
function deriveConnectionStatus(
|
||||
export function deriveConnectionStatus(
|
||||
lifecycleStatus: string | undefined,
|
||||
setupComplete: boolean | undefined,
|
||||
skillState: Record<string, unknown> | undefined,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { SkillTransport } from "./transport";
|
||||
export { SkillRuntime } from "./runtime";
|
||||
export { skillManager } from "./manager";
|
||||
export { syncToolsToBackend } from "./sync";
|
||||
export * from "./types";
|
||||
export * from "./paths";
|
||||
export * from "./hooks";
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import { SkillRuntime } from "./runtime";
|
||||
import { syncToolsToBackend } from "./sync";
|
||||
import type {
|
||||
SkillManifest,
|
||||
SkillStatus,
|
||||
@@ -130,6 +131,7 @@ class SkillManager {
|
||||
const tools = await runtime.listTools();
|
||||
store.dispatch(setSkillTools({ skillId, tools }));
|
||||
store.dispatch(setSkillStatus({ skillId, status: "ready" }));
|
||||
syncToolsToBackend();
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
store.dispatch(setSkillError({ skillId, error: msg }));
|
||||
@@ -314,6 +316,7 @@ class SkillManager {
|
||||
store.dispatch(setSkillSetupComplete({ skillId, complete: false }));
|
||||
store.dispatch(setSkillOAuthCredential({ skillId, credential: undefined }));
|
||||
store.dispatch(setSkillState({ skillId, state: {} }));
|
||||
syncToolsToBackend();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -331,6 +334,7 @@ class SkillManager {
|
||||
}
|
||||
this.runtimes.delete(skillId);
|
||||
store.dispatch(setSkillStatus({ skillId, status: "installed" }));
|
||||
syncToolsToBackend();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,6 +411,7 @@ class SkillManager {
|
||||
type: "skills/setSkillState",
|
||||
payload: { skillId, state: newState },
|
||||
});
|
||||
syncToolsToBackend();
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Sync tool/skill state to the backend via `tool:sync` socket event.
|
||||
*
|
||||
* Called whenever skill connection state changes or the socket reconnects,
|
||||
* so the backend always has an up-to-date picture of connected tools.
|
||||
*/
|
||||
|
||||
import { isTauri as coreIsTauri } from '@tauri-apps/api/core';
|
||||
|
||||
import { socketService } from '../../services/socketService';
|
||||
import { store } from '../../store';
|
||||
import { emitViaRustSocket } from '../../utils/tauriSocket';
|
||||
import { deriveConnectionStatus } from './hooks';
|
||||
import type { SkillConnectionStatus } from './types';
|
||||
|
||||
interface ToolSyncEntry {
|
||||
skillId: string;
|
||||
name: string;
|
||||
status: SkillConnectionStatus;
|
||||
tools: string[];
|
||||
}
|
||||
|
||||
function isTauri(): boolean {
|
||||
try {
|
||||
return coreIsTauri();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all skills from Redux, derive their connection status,
|
||||
* and emit a `tool:sync` event with the full list.
|
||||
*/
|
||||
export function syncToolsToBackend(): void {
|
||||
const state = store.getState();
|
||||
const skills = state.skills.skills;
|
||||
const skillStates = state.skills.skillStates;
|
||||
|
||||
const tools: ToolSyncEntry[] = [];
|
||||
|
||||
for (const [skillId, skill] of Object.entries(skills)) {
|
||||
const connectionStatus = deriveConnectionStatus(
|
||||
skill.status,
|
||||
skill.setupComplete,
|
||||
skillStates[skillId],
|
||||
);
|
||||
|
||||
tools.push({
|
||||
skillId,
|
||||
name: skill.manifest.name,
|
||||
status: connectionStatus,
|
||||
tools: (skill.tools ?? []).map(t => t.name),
|
||||
});
|
||||
}
|
||||
|
||||
const payload = { tools };
|
||||
|
||||
if (isTauri()) {
|
||||
emitViaRustSocket('tool:sync', payload);
|
||||
} else {
|
||||
socketService.emit('tool:sync', payload);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import debug from 'debug';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
|
||||
import { MCPTool, MCPToolCall, SocketIOMCPTransportImpl } from '../lib/mcp';
|
||||
import { skillManager } from '../lib/skills';
|
||||
import { skillManager, syncToolsToBackend } from '../lib/skills';
|
||||
import { store } from '../store';
|
||||
import { resetForUser, setSocketIdForUser, setStatusForUser } from '../store/socketSlice';
|
||||
import { BACKEND_URL } from '../utils/config';
|
||||
@@ -134,6 +134,7 @@ class SocketService {
|
||||
socketLog('Connected', { socketId, userId: uid });
|
||||
store.dispatch(setStatusForUser({ userId: uid, status: 'connected' }));
|
||||
store.dispatch(setSocketIdForUser({ userId: uid, socketId }));
|
||||
syncToolsToBackend();
|
||||
});
|
||||
|
||||
this.socket.on('ready', () => {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { isTauri as coreIsTauri, invoke } from '@tauri-apps/api/core';
|
||||
import { listen, UnlistenFn } from '@tauri-apps/api/event';
|
||||
|
||||
import { syncToolsToBackend } from '../lib/skills/sync';
|
||||
import { store } from '../store';
|
||||
import { setSocketIdForUser, setStatusForUser } from '../store/socketSlice';
|
||||
import { BACKEND_URL } from './config';
|
||||
@@ -149,6 +150,10 @@ export async function setupTauriSocketListeners(): Promise<void> {
|
||||
const mappedStatus: ReduxStatus = statusMap[status] || 'disconnected';
|
||||
store.dispatch(setStatusForUser({ userId: uid, status: mappedStatus }));
|
||||
store.dispatch(setSocketIdForUser({ userId: uid, socketId: socket_id ?? null }));
|
||||
|
||||
if (mappedStatus === 'connected') {
|
||||
syncToolsToBackend();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user