mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Fix local reset flow and Accessibility permission handling (#324)
* refactor: clean up imports and improve code structure in skills_cli and registry_ops - Removed unused imports in `skills_cli.rs` and `registry_ops.rs` to enhance code clarity. - Streamlined import statements for better organization and readability. - Minor adjustments in `engine.rs` to maintain consistency in import usage. * fix: update UI text and styles for MemoryWorkspace and SkillSetupWizard components - Changed the title in MemoryWorkspace to "Memory (EverMind)" for clarity. - Updated text colors in SkillSetupWizard for better visibility and consistency. - Enhanced button styles and loading indicators for improved user experience. * feat: implement reset functionality for OpenHuman data and enhance app data clearing process - Added a new utility function `resetOpenHumanDataAndRestartCore` to reset local OpenHuman data and restart the core process. - Updated `clearAllAppData` in `SettingsHome` to utilize the new reset function, improving the app's data clearing process. - Enhanced error handling during the data reset and session clearing operations to ensure robustness. - Introduced tests for the new reset functionality to validate its behavior and integration. * refactor: remove screen recording permission from AccessibilityPanel - Eliminated the screen recording permission check and associated UI elements from the AccessibilityPanel component. - Updated tests to ensure the screen recording option is no longer present in the rendered output, improving clarity and focus on relevant permissions. * chore: update package.json files to integrate Husky for Git hooks - Added Husky as a dev dependency in both package.json files to manage Git hooks. - Included "prepare" and "postinstall" scripts in the main package.json to ensure Husky is set up correctly. - Cleaned up the app's package.json by removing the "prepare" script, as it is now handled in the main package.json. - Enhanced logging in tauriCommands.ts for better debugging during the reset process. * fix: update variable name for clarity in ops.rs test assertions - Changed the variable name from `data` to `value` in the test assertions to enhance clarity and better reflect its purpose. - Ensured that the assertions remain functional and maintain the integrity of the test logic. * feat: enhance skill selection logic in skills_debug_e2e.rs - Introduced a new function `select_skill_id` to improve skill selection based on environment variables and preferred skills. - Updated the skill ID retrieval process to prioritize user-defined skills while maintaining a fallback to the default skill. - Enhanced documentation to clarify the usage of environment variables for skill testing. * format
This commit is contained in:
+1
-2
@@ -40,8 +40,7 @@
|
||||
"format": "prettier --write . && cargo fmt --manifest-path ../Cargo.toml --all",
|
||||
"format:check": "prettier --check . && cargo fmt --manifest-path ../Cargo.toml --all --check",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
||||
"prepare": "husky"
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
|
||||
@@ -208,7 +208,10 @@ impl CoreProcessHandle {
|
||||
);
|
||||
|
||||
self.shutdown().await;
|
||||
log::debug!("[core] restart: shutdown complete, checking port {}", self.port);
|
||||
log::debug!(
|
||||
"[core] restart: shutdown complete, checking port {}",
|
||||
self.port
|
||||
);
|
||||
|
||||
// If we never spawned the sidecar (something else was already listening), we cannot free
|
||||
// the port — fail fast with a clear message instead of polling for 8s.
|
||||
@@ -265,12 +268,7 @@ impl CoreProcessHandle {
|
||||
}
|
||||
// Wait for the process to exit so the RPC listen socket is released before restart
|
||||
// checks the port (otherwise we can spuriously hit "port still in use").
|
||||
match timeout(
|
||||
Duration::from_secs(12),
|
||||
child.wait(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
match timeout(Duration::from_secs(12), child.wait()).await {
|
||||
Ok(Ok(status)) => {
|
||||
log::debug!("[core] child core process reaped after kill: {status}");
|
||||
}
|
||||
|
||||
@@ -143,10 +143,7 @@ async fn restart_core_process(
|
||||
/// Register (or re-register) the global dictation toggle hotkey.
|
||||
/// Emits `dictation://toggle` to all webviews when the shortcut is pressed.
|
||||
#[tauri::command]
|
||||
async fn register_dictation_hotkey(
|
||||
app: AppHandle,
|
||||
shortcut: String,
|
||||
) -> Result<(), String> {
|
||||
async fn register_dictation_hotkey(app: AppHandle, shortcut: String) -> Result<(), String> {
|
||||
log::info!("[dictation] register_dictation_hotkey: shortcut={shortcut}");
|
||||
|
||||
let old_shortcuts = {
|
||||
@@ -189,7 +186,9 @@ async fn register_dictation_hotkey(
|
||||
);
|
||||
}
|
||||
}
|
||||
return Err(format!("Failed to unregister previous shortcut '{old}': {e}"));
|
||||
return Err(format!(
|
||||
"Failed to unregister previous shortcut '{old}': {e}"
|
||||
));
|
||||
}
|
||||
unregistered_old.push(old.clone());
|
||||
}
|
||||
|
||||
@@ -350,7 +350,7 @@ export function MemoryWorkspace({ onToast }: MemoryWorkspaceProps) {
|
||||
<div className="glass rounded-2xl p-5 border border-stone-200">
|
||||
<div className="flex items-start justify-between gap-4 mb-5">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-stone-900">Memory</h2>
|
||||
<h2 className="text-lg font-semibold text-stone-900">Memory (EverMind)</h2>
|
||||
<p className="text-sm text-stone-500">
|
||||
Your AI's knowledge graph, extracted insights, and ingestion activity.
|
||||
</p>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useState } from 'react';
|
||||
import { skillManager } from '../../lib/skills/manager';
|
||||
import { useCoreState } from '../../providers/CoreStateProvider';
|
||||
import { persistor } from '../../store';
|
||||
import { resetOpenHumanDataAndRestartCore } from '../../utils/tauriCommands';
|
||||
import SettingsHeader from './components/SettingsHeader';
|
||||
import SettingsMenuItem from './components/SettingsMenuItem';
|
||||
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
|
||||
@@ -29,29 +30,31 @@ const SettingsHome = () => {
|
||||
};
|
||||
|
||||
const clearAllAppData = async () => {
|
||||
try {
|
||||
await setOnboardingCompletedFlag(false);
|
||||
} catch (err) {
|
||||
console.warn('[Settings] Failed to clear onboarding_completed in config:', err);
|
||||
}
|
||||
try {
|
||||
await clearSession();
|
||||
} catch (err) {
|
||||
console.warn('[Settings] Rust logout failed during clearAllAppData:', err);
|
||||
}
|
||||
|
||||
await persistor.purge();
|
||||
window.localStorage.clear();
|
||||
window.sessionStorage.clear();
|
||||
try {
|
||||
await resetOpenHumanDataAndRestartCore();
|
||||
} catch (err) {
|
||||
console.warn('[Settings] Failed to reset OpenHuman data dir and restart core:', err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// NEW: Clear skills databases (emails, chats, cached files)
|
||||
// Best-effort cleanup for in-memory and browser-side caches that live outside the Rust core.
|
||||
try {
|
||||
await skillManager.clearAllSkillsData();
|
||||
} catch (error) {
|
||||
console.warn('Failed to clear skills data:', error);
|
||||
// Continue with logout even if skills clearing fails
|
||||
// Continue even if skill cleanup fails because the backend reset already completed.
|
||||
}
|
||||
|
||||
await persistor.purge();
|
||||
window.localStorage.clear();
|
||||
window.sessionStorage.clear();
|
||||
|
||||
// Complete reset - redirect to login for fresh start
|
||||
window.location.hash = '/';
|
||||
};
|
||||
|
||||
@@ -85,7 +85,6 @@ const AccessibilityPanel = () => {
|
||||
}, [dispatch]);
|
||||
|
||||
const anyPermissionDenied =
|
||||
status?.permissions.screen_recording === 'denied' ||
|
||||
status?.permissions.accessibility === 'denied' ||
|
||||
status?.permissions.input_monitoring === 'denied';
|
||||
|
||||
@@ -119,10 +118,6 @@ const AccessibilityPanel = () => {
|
||||
<div className="flex-1 overflow-y-auto max-w-2xl mx-auto w-full p-4 space-y-4">
|
||||
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
|
||||
<h3 className="text-sm font-semibold text-stone-900">Permissions</h3>
|
||||
<PermissionBadge
|
||||
label="Screen Recording"
|
||||
value={status?.permissions.screen_recording ?? 'unknown'}
|
||||
/>
|
||||
<PermissionBadge
|
||||
label="Accessibility"
|
||||
value={status?.permissions.accessibility ?? 'unknown'}
|
||||
@@ -154,18 +149,11 @@ const AccessibilityPanel = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void dispatch(requestAccessibilityPermission('screen_recording'))}
|
||||
disabled={isRequestingPermissions || isRestartingCore}
|
||||
className="mt-1 rounded-lg border border-primary-500/60 bg-primary-50 px-3 py-2 text-sm text-primary-600 disabled:opacity-50">
|
||||
{isRequestingPermissions ? 'Requesting…' : 'Request Screen Recording'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void dispatch(requestAccessibilityPermission('accessibility'))}
|
||||
disabled={isRequestingPermissions || isRestartingCore}
|
||||
className="rounded-lg border border-primary-500/60 bg-primary-50 px-3 py-2 text-sm text-primary-600 disabled:opacity-50">
|
||||
className="mt-1 rounded-lg border border-primary-500/60 bg-primary-50 px-3 py-2 text-sm text-primary-600 disabled:opacity-50">
|
||||
{isRequestingPermissions ? 'Requesting…' : 'Request Accessibility'}
|
||||
</button>
|
||||
<button
|
||||
|
||||
@@ -95,6 +95,7 @@ describe('AccessibilityPanel', () => {
|
||||
expect(screen.getByText('Accessibility Automation')).toBeInTheDocument();
|
||||
expect(screen.getByText('Permissions')).toBeInTheDocument();
|
||||
expect(screen.getByText('Session')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Screen Recording')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Start Session' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -503,11 +503,11 @@ export default function SkillSetupWizard({
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-2">
|
||||
Connected!
|
||||
</h3>
|
||||
{state.message && (
|
||||
<p className="text-sm text-stone-400 mb-6">{state.message}</p>
|
||||
<p className="text-sm text-stone-600 mb-6">{state.message}</p>
|
||||
)}
|
||||
<button
|
||||
onClick={onComplete}
|
||||
@@ -536,14 +536,14 @@ export default function SkillSetupWizard({
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-2">
|
||||
Setup Failed
|
||||
</h3>
|
||||
<p className="text-sm text-stone-400 mb-6">{state.message}</p>
|
||||
<p className="text-sm text-stone-600 mb-6">{state.message}</p>
|
||||
<div className="flex space-x-3 justify-center">
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="px-6 py-2.5 text-sm font-medium text-stone-400 bg-stone-800/50 border border-stone-700 rounded-xl hover:bg-stone-800 transition-colors"
|
||||
className="px-6 py-2.5 text-sm font-medium text-stone-600 bg-stone-100 border border-stone-200 rounded-xl hover:bg-stone-200 transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
@@ -621,7 +621,7 @@ function LoadingView() {
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="ml-3 text-sm text-stone-400">
|
||||
<span className="ml-3 text-sm text-stone-600">
|
||||
Starting setup...
|
||||
</span>
|
||||
</div>
|
||||
@@ -664,17 +664,17 @@ function OAuthLoginView({
|
||||
<div className="py-6">
|
||||
{/* Provider icon */}
|
||||
<div className="flex justify-center mb-5">
|
||||
<div className="w-14 h-14 rounded-2xl bg-stone-800 border border-stone-700 flex items-center justify-center">
|
||||
<div className="w-14 h-14 rounded-2xl bg-stone-50 border border-stone-200 flex items-center justify-center shadow-sm">
|
||||
<ProviderIcon provider={provider} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title and description */}
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-2">
|
||||
Connect to {providerName}
|
||||
</h3>
|
||||
<p className="text-sm text-stone-400">
|
||||
<p className="text-sm text-stone-600">
|
||||
{waiting
|
||||
? "Waiting for authorization. Complete the login in your browser..."
|
||||
: `Sign in with your ${providerName} account to connect this skill.`}
|
||||
@@ -684,9 +684,9 @@ function OAuthLoginView({
|
||||
{/* Login button or waiting state */}
|
||||
{waiting ? (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex items-center gap-3 px-4 py-3 bg-stone-800/50 border border-stone-700 rounded-xl">
|
||||
<div className="flex items-center gap-3 px-4 py-3 bg-stone-50 border border-stone-200 rounded-xl">
|
||||
<svg
|
||||
className="animate-spin h-4 w-4 text-primary-400"
|
||||
className="animate-spin h-4 w-4 text-primary-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -705,14 +705,14 @@ function OAuthLoginView({
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm text-stone-300">
|
||||
<span className="text-sm text-stone-700">
|
||||
Waiting for {providerName} authorization...
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onLogin}
|
||||
className="text-xs text-primary-400 hover:text-primary-300 transition-colors"
|
||||
className="text-xs text-primary-600 hover:text-primary-700 transition-colors"
|
||||
>
|
||||
Open login page again
|
||||
</button>
|
||||
@@ -743,7 +743,7 @@ function OAuthLoginView({
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="w-full py-2.5 text-sm font-medium text-stone-400 bg-stone-800/50 border border-stone-700 rounded-xl hover:bg-stone-800 transition-colors"
|
||||
className="w-full py-2.5 text-sm font-medium text-stone-600 bg-stone-100 border border-stone-200 rounded-xl hover:bg-stone-200 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -796,7 +796,7 @@ function ProviderIcon({ provider }: { provider: string }) {
|
||||
);
|
||||
case "github":
|
||||
return (
|
||||
<svg className="w-7 h-7 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-7 h-7 text-stone-800" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
@@ -807,7 +807,7 @@ function ProviderIcon({ provider }: { provider: string }) {
|
||||
default:
|
||||
return (
|
||||
<svg
|
||||
className="w-7 h-7 text-stone-400"
|
||||
className="w-7 h-7 text-stone-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
import { invoke, isTauri } from '@tauri-apps/api/core';
|
||||
import { beforeEach, describe, expect, type Mock, test, vi } from 'vitest';
|
||||
|
||||
import { callCoreRpc } from '../../services/coreRpcClient';
|
||||
@@ -8,8 +8,10 @@ vi.mock('../../services/coreRpcClient', () => ({ callCoreRpc: vi.fn() }));
|
||||
|
||||
describe('tauriCommands', () => {
|
||||
const mockIsTauri = isTauri as Mock;
|
||||
const mockInvoke = invoke as Mock;
|
||||
const mockCallCoreRpc = callCoreRpc as Mock;
|
||||
let getAuthState: typeof import('../tauriCommands').getAuthState;
|
||||
let resetOpenHumanDataAndRestartCore: typeof import('../tauriCommands').resetOpenHumanDataAndRestartCore;
|
||||
let storeSession: typeof import('../tauriCommands').storeSession;
|
||||
let openhumanLocalAiStatus: typeof import('../tauriCommands').openhumanLocalAiStatus;
|
||||
let openhumanServiceStatus: typeof import('../tauriCommands').openhumanServiceStatus;
|
||||
@@ -19,6 +21,7 @@ describe('tauriCommands', () => {
|
||||
mockIsTauri.mockReturnValue(true);
|
||||
const actual = await vi.importActual<typeof import('../tauriCommands')>('../tauriCommands');
|
||||
getAuthState = actual.getAuthState;
|
||||
resetOpenHumanDataAndRestartCore = actual.resetOpenHumanDataAndRestartCore;
|
||||
storeSession = actual.storeSession;
|
||||
openhumanLocalAiStatus = actual.openhumanLocalAiStatus;
|
||||
openhumanServiceStatus = actual.openhumanServiceStatus;
|
||||
@@ -44,6 +47,13 @@ describe('tauriCommands', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('resetOpenHumanDataAndRestartCore invokes the destructive Tauri command', async () => {
|
||||
await resetOpenHumanDataAndRestartCore();
|
||||
|
||||
expect(mockCallCoreRpc).toHaveBeenCalledWith({ method: 'openhuman.config_reset_local_data' });
|
||||
expect(mockInvoke).toHaveBeenCalledWith('restart_core_process');
|
||||
});
|
||||
|
||||
test('openhumanLocalAiStatus returns upgrade hint on unknown method', async () => {
|
||||
mockCallCoreRpc.mockRejectedValueOnce(new Error('unknown method: openhuman.local_ai_status'));
|
||||
|
||||
|
||||
@@ -200,6 +200,22 @@ export async function restartCoreProcess(): Promise<void> {
|
||||
console.debug('[core] restartCoreProcess: done');
|
||||
}
|
||||
|
||||
export async function resetOpenHumanDataAndRestartCore(): Promise<void> {
|
||||
if (!isTauri()) {
|
||||
console.debug('[core] resetOpenHumanDataAndRestartCore: skipped — not running in Tauri');
|
||||
return;
|
||||
}
|
||||
console.debug(
|
||||
'[core] resetOpenHumanDataAndRestartCore: invoking openhuman.config_reset_local_data'
|
||||
);
|
||||
await callCoreRpc({ method: 'openhuman.config_reset_local_data' });
|
||||
console.debug(
|
||||
'[core] resetOpenHumanDataAndRestartCore: local data reset complete, restarting core'
|
||||
);
|
||||
await restartCoreProcess();
|
||||
console.debug('[core] resetOpenHumanDataAndRestartCore: done');
|
||||
}
|
||||
|
||||
// --- Memory Commands ---
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
"format:check": "yarn workspace openhuman-app format:check",
|
||||
"lint": "yarn workspace openhuman-app lint",
|
||||
"lint:fix": "yarn workspace openhuman-app lint:fix",
|
||||
"prepare": "husky",
|
||||
"postinstall": "husky",
|
||||
"tauri": "yarn workspace openhuman-app tauri",
|
||||
"test": "yarn workspace openhuman-app test",
|
||||
"test:coverage": "yarn workspace openhuman-app test:coverage",
|
||||
@@ -24,6 +26,9 @@
|
||||
"mock:api": "node scripts/mock-api-server.mjs",
|
||||
"typecheck": "yarn workspace openhuman-app compile"
|
||||
},
|
||||
"devDependencies": {
|
||||
"husky": "^9.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "2.10.1"
|
||||
}
|
||||
|
||||
@@ -417,9 +417,7 @@ fn run_skills_test(args: &[String]) -> Result<()> {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn build_skills_only_router() -> axum::Router {
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::{get, post};
|
||||
use axum::Json;
|
||||
|
||||
axum::Router::new()
|
||||
.route("/health", get(health))
|
||||
|
||||
+124
-1
@@ -1,6 +1,6 @@
|
||||
//! JSON-RPC / CLI controller surface for persisted config and runtime flags.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
@@ -39,6 +39,86 @@ fn fallback_workspace_dir() -> PathBuf {
|
||||
.join("workspace")
|
||||
}
|
||||
|
||||
fn default_openhuman_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".openhuman")
|
||||
}
|
||||
|
||||
fn active_workspace_marker_path(default_openhuman_dir: &Path) -> PathBuf {
|
||||
default_openhuman_dir.join("active_workspace.toml")
|
||||
}
|
||||
|
||||
fn config_openhuman_dir(config: &Config) -> PathBuf {
|
||||
config
|
||||
.config_path
|
||||
.parent()
|
||||
.map_or_else(|| PathBuf::from("."), PathBuf::from)
|
||||
}
|
||||
|
||||
async fn reset_local_data_for_paths(
|
||||
current_openhuman_dir: &Path,
|
||||
default_openhuman_dir: &Path,
|
||||
) -> Result<RpcOutcome<serde_json::Value>, String> {
|
||||
let active_workspace_marker = active_workspace_marker_path(default_openhuman_dir);
|
||||
tracing::debug!(
|
||||
current_dir = %current_openhuman_dir.display(),
|
||||
default_dir = %default_openhuman_dir.display(),
|
||||
marker = %active_workspace_marker.display(),
|
||||
"[config] reset_local_data: starting"
|
||||
);
|
||||
|
||||
let mut removed_paths = Vec::new();
|
||||
|
||||
if active_workspace_marker.exists() {
|
||||
tokio::fs::remove_file(&active_workspace_marker)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to remove active workspace marker: {e}"))?;
|
||||
tracing::debug!(
|
||||
marker = %active_workspace_marker.display(),
|
||||
"[config] reset_local_data: removed active workspace marker"
|
||||
);
|
||||
removed_paths.push(active_workspace_marker.display().to_string());
|
||||
}
|
||||
|
||||
for target_dir in [current_openhuman_dir, default_openhuman_dir] {
|
||||
if !target_dir.exists() {
|
||||
tracing::debug!(
|
||||
dir = %target_dir.display(),
|
||||
"[config] reset_local_data: directory already absent"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
tokio::fs::remove_dir_all(target_dir)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to remove {}: {e}", target_dir.display()))?;
|
||||
tracing::debug!(
|
||||
dir = %target_dir.display(),
|
||||
"[config] reset_local_data: removed directory"
|
||||
);
|
||||
removed_paths.push(target_dir.display().to_string());
|
||||
}
|
||||
|
||||
Ok(RpcOutcome::new(
|
||||
json!({
|
||||
"removed_paths": removed_paths,
|
||||
"current_openhuman_dir": current_openhuman_dir.display().to_string(),
|
||||
"default_openhuman_dir": default_openhuman_dir.display().to_string(),
|
||||
}),
|
||||
vec![
|
||||
format!(
|
||||
"reset local data for active config dir {}",
|
||||
current_openhuman_dir.display()
|
||||
),
|
||||
format!(
|
||||
"removed default data dir {} if present",
|
||||
default_openhuman_dir.display()
|
||||
),
|
||||
],
|
||||
))
|
||||
}
|
||||
|
||||
pub fn snapshot_config_json(config: &Config) -> Result<serde_json::Value, String> {
|
||||
let value = serde_json::to_value(config).map_err(|e| e.to_string())?;
|
||||
Ok(json!({
|
||||
@@ -459,3 +539,46 @@ pub fn agent_server_status() -> RpcOutcome<serde_json::Value> {
|
||||
});
|
||||
RpcOutcome::single_log(payload, "agent server status checked")
|
||||
}
|
||||
|
||||
pub async fn reset_local_data() -> Result<RpcOutcome<serde_json::Value>, String> {
|
||||
let config = load_config_with_timeout().await?;
|
||||
let current_openhuman_dir = config_openhuman_dir(&config);
|
||||
let default_openhuman_dir = default_openhuman_dir();
|
||||
reset_local_data_for_paths(¤t_openhuman_dir, &default_openhuman_dir).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn reset_local_data_removes_current_dir_default_dir_and_marker() {
|
||||
let temp = tempdir().unwrap();
|
||||
let default_openhuman_dir = temp.path().join("default-openhuman");
|
||||
let current_openhuman_dir = temp.path().join("custom-openhuman");
|
||||
let marker = active_workspace_marker_path(&default_openhuman_dir);
|
||||
|
||||
tokio::fs::create_dir_all(default_openhuman_dir.join("workspace"))
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::fs::create_dir_all(current_openhuman_dir.join("workspace"))
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::fs::write(&marker, "config_dir = '/tmp/custom-openhuman'\n")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let outcome = reset_local_data_for_paths(¤t_openhuman_dir, &default_openhuman_dir)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!current_openhuman_dir.exists());
|
||||
assert!(!default_openhuman_dir.exists());
|
||||
assert!(outcome
|
||||
.value
|
||||
.get("removed_paths")
|
||||
.and_then(|value| value.as_array())
|
||||
.is_some_and(|paths| !paths.is_empty()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ pub fn all_controller_schemas() -> Vec<ControllerSchema> {
|
||||
schemas("update_analytics_settings"),
|
||||
schemas("get_analytics_settings"),
|
||||
schemas("agent_server_status"),
|
||||
schemas("reset_local_data"),
|
||||
schemas("get_onboarding_completed"),
|
||||
schemas("set_onboarding_completed"),
|
||||
]
|
||||
@@ -154,6 +155,10 @@ pub fn all_registered_controllers() -> Vec<RegisteredController> {
|
||||
schema: schemas("agent_server_status"),
|
||||
handler: handle_agent_server_status,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: schemas("reset_local_data"),
|
||||
handler: handle_reset_local_data,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: schemas("get_onboarding_completed"),
|
||||
handler: handle_get_onboarding_completed,
|
||||
@@ -383,6 +388,14 @@ pub fn schemas(function: &str) -> ControllerSchema {
|
||||
inputs: vec![],
|
||||
outputs: vec![json_output("status", "Agent server status payload.")],
|
||||
},
|
||||
"reset_local_data" => ControllerSchema {
|
||||
namespace: "config",
|
||||
function: "reset_local_data",
|
||||
description:
|
||||
"Delete local OpenHuman data for the active config/workspace so the next restart boots clean.",
|
||||
inputs: vec![],
|
||||
outputs: vec![json_output("result", "Reset result with removed paths.")],
|
||||
},
|
||||
"get_onboarding_completed" => ControllerSchema {
|
||||
namespace: "config",
|
||||
function: "get_onboarding_completed",
|
||||
@@ -565,6 +578,10 @@ fn handle_agent_server_status(_params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async { to_json(config_rpc::agent_server_status()) })
|
||||
}
|
||||
|
||||
fn handle_reset_local_data(_params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async { to_json(config_rpc::reset_local_data().await?) })
|
||||
}
|
||||
|
||||
fn handle_get_onboarding_completed(_params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async { to_json(config_rpc::get_onboarding_completed().await?) })
|
||||
}
|
||||
|
||||
@@ -277,8 +277,6 @@ impl AccessibilityEngine {
|
||||
});
|
||||
}
|
||||
|
||||
self.request_permission(PermissionKind::ScreenRecording)
|
||||
.await?;
|
||||
self.request_permission(PermissionKind::Accessibility)
|
||||
.await?;
|
||||
self.request_permission(PermissionKind::InputMonitoring)
|
||||
@@ -286,7 +284,7 @@ impl AccessibilityEngine {
|
||||
|
||||
let mut state = self.inner.lock().await;
|
||||
state.permissions = detect_permissions();
|
||||
state.last_event = Some("permissions_requested".to_string());
|
||||
state.last_event = Some("permissions_requested:accessibility,input_monitoring".to_string());
|
||||
Ok(state.permissions.clone())
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ pub async fn accessibility_request_permissions() -> Result<RpcOutcome<Permission
|
||||
.await?;
|
||||
Ok(RpcOutcome::single_log(
|
||||
permissions,
|
||||
"accessibility permissions requested",
|
||||
"accessibility automation permissions requested",
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -96,14 +96,15 @@ pub fn schemas(function: &str) -> ControllerSchema {
|
||||
"request_permissions" => ControllerSchema {
|
||||
namespace: "screen_intelligence",
|
||||
function: "request_permissions",
|
||||
description: "Request required accessibility permissions.",
|
||||
description:
|
||||
"Request accessibility automation permissions without prompting for screen recording.",
|
||||
inputs: vec![],
|
||||
outputs: vec![json_output("permissions", "Permission status payload.")],
|
||||
},
|
||||
"request_permission" => ControllerSchema {
|
||||
namespace: "screen_intelligence",
|
||||
function: "request_permission",
|
||||
description: "Request one accessibility permission.",
|
||||
description: "Request one permission such as accessibility, input monitoring, or screen recording.",
|
||||
inputs: vec![FieldSchema {
|
||||
name: "permission",
|
||||
ty: TypeSchema::String,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use super::registry_cache::{
|
||||
cache_path, is_cache_fresh, is_local_path, local_skills_dir, read_cache, read_local_file,
|
||||
registry_url, tag_categories, write_cache,
|
||||
is_cache_fresh, is_local_path, local_skills_dir, read_cache, read_local_file, registry_url,
|
||||
tag_categories, write_cache,
|
||||
};
|
||||
use super::registry_types::{
|
||||
AvailableSkillEntry, InstalledSkillInfo, RegistrySkillEntry, RemoteSkillRegistry,
|
||||
|
||||
@@ -9,9 +9,9 @@ use super::prompt::build_subconscious_prompt;
|
||||
use super::situation_report::build_situation_report;
|
||||
use super::types::{Decision, SubconsciousStatus, TickOutput, TickResult};
|
||||
use crate::openhuman::config::Config;
|
||||
use crate::openhuman::memory::{MemoryClient, MemoryClientRef};
|
||||
use crate::openhuman::memory::MemoryClientRef;
|
||||
use anyhow::Result;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::{self, Duration};
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
//! Exercises the full skill lifecycle through the RuntimeEngine:
|
||||
//! discover → start → list tools → call tool → setup flow → tick/sync → stop
|
||||
//!
|
||||
//! By default uses the bundled `example-skill` from the openhuman-skills repo.
|
||||
//! By default uses a preferred skill from the checked-out openhuman-skills repo.
|
||||
//! Override with env vars:
|
||||
//! SKILL_DEBUG_ID — skill ID to test (default: "example-skill")
|
||||
//! SKILL_DEBUG_ID — skill ID to test
|
||||
//! SKILL_DEBUG_DIR — path to skills directory containing skill folders
|
||||
//! SKILL_DEBUG_TOOL — specific tool name to call (default: first tool found)
|
||||
//! SKILL_DEBUG_TOOL_ARGS — JSON args for the tool call (default: "{}")
|
||||
@@ -37,6 +37,46 @@ fn is_verbose() -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn manifests_in_dir(skills_dir: &Path) -> Vec<String> {
|
||||
let mut ids = Vec::new();
|
||||
let Ok(entries) = std::fs::read_dir(skills_dir) else {
|
||||
return ids;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() || !path.join("manifest.json").exists() {
|
||||
continue;
|
||||
}
|
||||
if let Some(name) = path.file_name().and_then(|name| name.to_str()) {
|
||||
ids.push(name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
ids.sort();
|
||||
ids
|
||||
}
|
||||
|
||||
fn select_skill_id(skills_dir: &Path, env_key: &str, preferred: &[&str]) -> String {
|
||||
if let Ok(skill_id) = std::env::var(env_key) {
|
||||
if !skill_id.trim().is_empty() {
|
||||
return skill_id;
|
||||
}
|
||||
}
|
||||
|
||||
let available = manifests_in_dir(skills_dir);
|
||||
for candidate in preferred {
|
||||
if available.iter().any(|id| id == candidate) {
|
||||
return (*candidate).to_string();
|
||||
}
|
||||
}
|
||||
|
||||
available
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or_else(|| "example-skill".to_string())
|
||||
}
|
||||
|
||||
fn banner(label: &str) {
|
||||
let sep = "=".repeat(60);
|
||||
eprintln!("\n{sep}");
|
||||
@@ -104,7 +144,7 @@ fn try_find_skills_dir() -> Option<PathBuf> {
|
||||
if let Some(parent) = cwd.parent() {
|
||||
for entry in std::fs::read_dir(parent).into_iter().flatten().flatten() {
|
||||
let candidate = entry.path().join("skills/skills");
|
||||
if candidate.exists() && candidate.join("example-skill/manifest.json").exists() {
|
||||
if candidate.exists() && candidate.read_dir().is_ok() {
|
||||
return Some(candidate.canonicalize().unwrap());
|
||||
}
|
||||
}
|
||||
@@ -155,8 +195,12 @@ async fn skill_full_lifecycle() {
|
||||
.is_test(true)
|
||||
.try_init();
|
||||
|
||||
let skill_id = env_or("SKILL_DEBUG_ID", "example-skill");
|
||||
let skills_dir = require_skills_dir!();
|
||||
let skill_id = select_skill_id(
|
||||
&skills_dir,
|
||||
"SKILL_DEBUG_ID",
|
||||
&["server-ping", "gmail", "notion"],
|
||||
);
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let data_dir = tmp.path().join("skills_data");
|
||||
std::fs::create_dir_all(&data_dir).expect("create data_dir");
|
||||
@@ -570,8 +614,12 @@ async fn skill_rapid_start_stop() {
|
||||
.is_test(true)
|
||||
.try_init();
|
||||
|
||||
let skill_id = env_or("SKILL_DEBUG_ID", "example-skill");
|
||||
let skills_dir = require_skills_dir!();
|
||||
let skill_id = select_skill_id(
|
||||
&skills_dir,
|
||||
"SKILL_DEBUG_ID",
|
||||
&["server-ping", "gmail", "notion"],
|
||||
);
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let data_dir = tmp.path().join("skills_data");
|
||||
std::fs::create_dir_all(&data_dir).unwrap();
|
||||
@@ -611,8 +659,8 @@ async fn skill_disconnect_flow() {
|
||||
.is_test(true)
|
||||
.try_init();
|
||||
|
||||
let skill_id = env_or("SKILL_DEBUG_ID", "example-skill");
|
||||
let skills_dir = require_skills_dir!();
|
||||
let skill_id = select_skill_id(&skills_dir, "SKILL_DEBUG_ID", &["gmail", "notion"]);
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let data_dir = tmp.path().join("skills_data");
|
||||
std::fs::create_dir_all(&data_dir).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user