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:
Steven Enamakel
2026-02-10 11:02:04 +05:30
committed by GitHub
co-authored by Claude Opus 4.6
parent 4be6693599
commit d0048718ea
10 changed files with 193 additions and 2 deletions
+1 -1
View File
@@ -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
View File
@@ -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";
+5
View File
@@ -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 };
}
+64
View File
@@ -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);
}
}
+2 -1
View File
@@ -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', () => {
+5
View File
@@ -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();
}
}
);