diff --git a/app/knip.json b/app/knip.json new file mode 100644 index 000000000..668da51ad --- /dev/null +++ b/app/knip.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "entry": ["src/main.tsx", "test/e2e/specs/**/*.spec.ts"], + "project": [ + "src/**/*.{ts,tsx}", + "test/**/*.{ts,tsx}", + "test/e2e/globals.d.ts", + "*.config.{js,ts}" + ], + "ignoreDependencies": [ + "@tauri-apps/cli", + "@testing-library/dom", + "@wdio/appium-service", + "@wdio/cli", + "@wdio/local-runner", + "@wdio/mocha-framework", + "@wdio/spec-reporter", + "buffer", + "eslint", + "husky", + "os-browserify", + "prettier", + "process", + "util" + ], + "ignoreBinaries": ["eslint", "knip", "open", "prettier", "tauri", "tsc", "vite", "vitest"] +} diff --git a/app/package.json b/app/package.json index b89ad3c1c..95d06bd69 100644 --- a/app/package.json +++ b/app/package.json @@ -44,10 +44,10 @@ "format:check": "prettier --check . && yarn rust:format:check", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "eslint . --ext .ts,.tsx --fix", - "knip": "knip" + "knip": "knip --config knip.json", + "knip:production": "knip --config knip.json --production" }, "dependencies": { - "@heroicons/react": "^2.2.0", "@noble/hashes": "^2.0.1", "@noble/secp256k1": "^3.0.0", "@reduxjs/toolkit": "^2.11.2", diff --git a/app/src/__tests__/setup.ts b/app/src/__tests__/setup.ts deleted file mode 100644 index 193e8d302..000000000 --- a/app/src/__tests__/setup.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Global test setup — runs before every test file. - * - * - Silences console output during tests (unless DEBUG_TESTS=1) - * - Resets rate limiter module-level state between tests - */ -import { beforeEach, vi } from 'vitest'; - -// Silence console during tests to keep output clean -if (!process.env.DEBUG_TESTS) { - vi.spyOn(console, 'log').mockImplementation(() => {}); - vi.spyOn(console, 'warn').mockImplementation(() => {}); - vi.spyOn(console, 'error').mockImplementation(() => {}); -} - -// Reset rate limiter per-request counter before each test. -// The module keeps mutable state (callHistory, lastCallTime, callsInCurrentRequest) -// that leaks between tests if not cleared. -beforeEach(async () => { - try { - const { resetRequestCallCount } = await import('../lib/mcp/rateLimiter'); - if (typeof resetRequestCallCount === 'function') { - resetRequestCallCount(); - } - } catch { - // Module may be fully mocked in some test files — safe to skip - } -}); diff --git a/app/src/assets/icons/ExchangeIcon.tsx b/app/src/assets/icons/ExchangeIcon.tsx deleted file mode 100644 index b83dfe738..000000000 --- a/app/src/assets/icons/ExchangeIcon.tsx +++ /dev/null @@ -1,9 +0,0 @@ -const ExchangeIcon = ({ className = 'w-5 h-5' }: { className?: string }) => { - return ( - - - - ); -}; - -export default ExchangeIcon; diff --git a/app/src/assets/icons/GmailIcon.tsx b/app/src/assets/icons/GmailIcon.tsx deleted file mode 100644 index c55f5e45c..000000000 --- a/app/src/assets/icons/GmailIcon.tsx +++ /dev/null @@ -1,10 +0,0 @@ -const GmailIcon = ({ className = 'w-4 h-4' }: { className?: string }) => { - return ( - - - - - ); -}; - -export default GmailIcon; diff --git a/app/src/assets/icons/WalletIcon.tsx b/app/src/assets/icons/WalletIcon.tsx deleted file mode 100644 index 18d7daec5..000000000 --- a/app/src/assets/icons/WalletIcon.tsx +++ /dev/null @@ -1,9 +0,0 @@ -const WalletIcon = ({ className = 'w-5 h-5' }: { className?: string }) => { - return ( - - - - ); -}; - -export default WalletIcon; diff --git a/app/src/components/DesignSystemShowcase.tsx b/app/src/components/DesignSystemShowcase.tsx deleted file mode 100644 index 70beecccb..000000000 --- a/app/src/components/DesignSystemShowcase.tsx +++ /dev/null @@ -1,394 +0,0 @@ -import React from 'react'; - -/** - * Design System Showcase Component - * - * This component demonstrates the premium design system created for the crypto platform. - * It showcases all the major components and styles that capture the calm, trustworthy - * aesthetic from the reference screenshots while elevating it for professional crypto users. - */ - -const DesignSystemShowcase: React.FC = () => { - return ( -
- {/* Hero Section - Similar to Screenshot #1 but elevated */} -
- {/* Atmospheric Background */} -
-
-
- -
- {/* Greeting Section */} -
-

- Good afternoon, - Cyrus -

-

Your portfolio is up 12.5% today

- - {/* Status Card */} -
-
-
- - -
- Markets Open -
-
- 26°C and clear -
-
- - {/* Quick Actions */} -
- - - -
-
-
- - {/* Color Palette Section */} -
-

Color System

- -
- {/* Primary Colors */} -
-

Primary Ocean

-
-
-
-
-

Primary 500

-

#5B9BF3

-
-
-
-
-
-

Primary 600

-

#4A83DD

-
-
-
-
- - {/* Success Colors */} -
-

Sage Success

-
-
-
-
-

Sage 500

-

#4DC46F

-
-
-
-
-
-

Sage 600

-

#3BA858

-
-
-
-
- - {/* Accent Colors */} -
-

Accent Palette

-
-
-
-
-

Lavender

-

#9B8AFB

-
-
-
-
-
-

Sky

-

#7DD3FC

-
-
-
-
-
-
- - {/* Typography Section */} -
-

Typography Scale

- -
-
-

Display · 4.5rem

-

Beautiful Typography

-
- -
-

Heading 1 · 3rem

-

- Clear Information Hierarchy -

-
- -
-

Heading 2 · 1.875rem

-

- Structured Content Design -

-
- -
-

Body · 1rem

-

- The typography system is designed to create clear visual hierarchy while maintaining - excellent readability. Each text size has been carefully calibrated with appropriate - line heights and letter spacing to ensure optimal legibility across all device sizes. -

-
- -
-

Monospace · For prices and data

-

- $48,392.50 +2.45% -

-
-
-
- - {/* Card Components Section */} -
-

Card Components

- -
- {/* Market Card */} -
-
-
-
-
- -
-
-

Bitcoin

-

BTC

-
-
- Trending -
- -
-

$48,392.50

-
- +$1,185.20 - (+2.45%) -
-
-
-
- - {/* Interactive Card */} -
-

Portfolio Overview

-

- Track your investments and monitor performance in real-time. -

-
- View Details - - - -
-
- - {/* Elevated Card */} -
-
-

Quick Stats

-
- - - -
-
-
-
- Total Value - $125,430 -
-
- 24h Change - +3.2% -
-
- Holdings - 12 -
-
-
-
-
- - {/* Navigation Pattern - Similar to Screenshot #2 */} -
-

- Navigation & Settings -

- -
-
- {/* User Profile Section */} -
-
-
-
-

Cyrus Amini

-

Premium Member

-
-
-
- - {/* Navigation Items */} - -
-
-
- - {/* Form Elements */} -
-

Form Elements

- -
-
-
- - -
- -
- -
- $ - -
-
- -
- - -
- -
- - -
- - -
-
-
-
- ); -}; - -export default DesignSystemShowcase; diff --git a/app/src/components/GmailConnectionIndicator.tsx b/app/src/components/GmailConnectionIndicator.tsx deleted file mode 100644 index bc26dabfd..000000000 --- a/app/src/components/GmailConnectionIndicator.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import GmailIcon from '../assets/icons/GmailIcon'; - -interface GmailConnectionIndicatorProps { - description?: string; - className?: string; -} - -const GmailConnectionIndicator = ({ - description, - className = '', -}: GmailConnectionIndicatorProps) => { - // Gmail is always offline for now (placeholder) - const gmailIsOnline = false; - - return ( -
-
-
-
- - - {gmailIsOnline ? 'Connected to Gmail' : 'Gmail is Offline'} - -
-
- {description && ( -

{description}

- )} -
- ); -}; - -export default GmailConnectionIndicator; diff --git a/app/src/components/MiniSidebar.tsx b/app/src/components/MiniSidebar.tsx deleted file mode 100644 index 636614d33..000000000 --- a/app/src/components/MiniSidebar.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; - -import { useCoreState } from '../providers/CoreStateProvider'; -import { useAppSelector } from '../store/hooks'; -import { isTauri } from '../utils/tauriCommands'; -import DaemonHealthIndicator from './daemon/DaemonHealthIndicator'; -import DaemonHealthPanel from './daemon/DaemonHealthPanel'; - -const navItems = [ - { - id: 'home', - label: 'Home', - path: '/home', - icon: ( - - - - ), - }, - { - id: 'skills', - label: 'Skills', - path: '/skills', - icon: ( - - - - ), - }, - { - id: 'conversations', - label: 'Conversations', - path: '/conversations', - icon: ( - - - - ), - }, - { - id: 'intelligence', - label: 'Intelligence', - path: '/intelligence', - icon: ( - - - - ), - }, - // { - // id: 'invites', - // label: 'Invite Friends', - // path: '/invites', - // icon: ( - // - // - // - // ), - // }, - { - id: 'settings', - label: 'Settings', - path: '/settings', - icon: ( - - - - - ), - }, - { - id: 'channels', - label: 'Channels', - path: '/channels', - icon: ( - - - - ), - }, - { - id: 'cron-jobs', - label: 'Cron Jobs', - path: '/settings/cron-jobs', - icon: ( - - - - ), - }, -]; - -const MiniSidebar = () => { - const location = useLocation(); - const navigate = useNavigate(); - const { snapshot } = useCoreState(); - const token = snapshot.sessionToken; - const [showDaemonPanel, setShowDaemonPanel] = useState(false); - - // Unread count for Conversations: threads with lastMessageAt > lastViewedAt (must be before early return) - const conversationsUnreadCount = useAppSelector(state => { - const { threads, lastViewedAt } = state.thread; - if (threads.length === 0) return 0; - return threads.filter(t => { - const viewed = lastViewedAt[t.id]; - const lastMsg = new Date(t.lastMessageAt || t.createdAt).getTime(); - return viewed == null || lastMsg > viewed; - }).length; - }); - - // Hide sidebar on public/setup routes and when not authenticated. - const hiddenPaths = ['/', '/login']; - if ( - !token || - hiddenPaths.some(path => location.pathname === path || location.pathname.startsWith(`${path}/`)) - ) { - return null; - } - - const isActive = (path: string) => { - if (path === '/settings') { - return ( - location.pathname === '/settings' || - (location.pathname.startsWith('/settings/') && - !location.pathname.startsWith('/settings/cron-jobs')) - ); - } - if (path === '/channels') return location.pathname.startsWith('/channels'); - if (path === '/settings/cron-jobs') return location.pathname.startsWith('/settings/cron-jobs'); - if (path === '/conversations') return location.pathname.startsWith('/conversations'); - return location.pathname === path; - }; - - return ( - <> -
- {/* Navigation Items */} -
- {navItems.map(item => { - const active = isActive(item.path); - const showUnreadBadge = item.id === 'conversations' && conversationsUnreadCount > 0; - return ( -
- - {showUnreadBadge && ( - - {conversationsUnreadCount > 99 ? '99+' : conversationsUnreadCount} - - )} - {/* Tooltip - appears to the right */} -
- {item.label} -
-
- ); - })} -
- - {/* Daemon Health Indicator - Only show in Tauri mode */} - {isTauri() && ( -
-
- setShowDaemonPanel(true)} /> -
- {/* Tooltip */} -
- Agent Status -
-
- )} -
- - {/* Daemon Health Panel Modal */} - {showDaemonPanel && ( -
-
- setShowDaemonPanel(false)} /> -
-
- )} - - ); -}; - -export default MiniSidebar; diff --git a/app/src/components/PrivacyFeatureCard.tsx b/app/src/components/PrivacyFeatureCard.tsx deleted file mode 100644 index 02522d7c1..000000000 --- a/app/src/components/PrivacyFeatureCard.tsx +++ /dev/null @@ -1,19 +0,0 @@ -interface PrivacyFeatureCardProps { - title: string; - description: string; -} - -const PrivacyFeatureCard = ({ title, description }: PrivacyFeatureCardProps) => { - return ( -
-
-
-

{title}

-

{description}

-
-
-
- ); -}; - -export default PrivacyFeatureCard; diff --git a/app/src/components/TelegramConnectionIndicator.tsx b/app/src/components/TelegramConnectionIndicator.tsx deleted file mode 100644 index de5fcea7a..000000000 --- a/app/src/components/TelegramConnectionIndicator.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import TelegramIcon from '../assets/icons/telegram.svg'; -import { useSkillConnectionStatus } from '../lib/skills/hooks'; -import type { SkillConnectionStatus } from '../lib/skills/types'; - -interface TelegramConnectionIndicatorProps { - className?: string; -} - -const STATUS_CONFIG: Record = { - connected: { dot: 'bg-sage-500', text: 'text-sage-500', label: 'Telegram Connected' }, - connecting: { dot: 'bg-amber-500', text: 'text-amber-500', label: 'Telegram Connecting...' }, - not_authenticated: { - dot: 'bg-amber-500', - text: 'text-amber-500', - label: 'Telegram Not Authenticated', - }, - disconnected: { dot: 'bg-gray-500', text: 'text-gray-500', label: 'Telegram Disconnected' }, - error: { dot: 'bg-coral-500', text: 'text-coral-500', label: 'Telegram Error' }, - offline: { dot: 'bg-gray-500', text: 'text-gray-500', label: 'Telegram Offline' }, - setup_required: { dot: 'bg-gray-500', text: 'text-gray-500', label: 'Telegram Not Set Up' }, -}; - -const TelegramConnectionIndicator = ({ className = '' }: TelegramConnectionIndicatorProps) => { - const status = useSkillConnectionStatus('telegram'); - const config = STATUS_CONFIG[status]; - const isActive = status === 'connected'; - - return ( -
-
-
-
- Telegram - {config.label} -
-
-
- ); -}; - -export default TelegramConnectionIndicator; diff --git a/app/src/components/TypewriterGreeting.tsx b/app/src/components/TypewriterGreeting.tsx deleted file mode 100644 index 2763db4bf..000000000 --- a/app/src/components/TypewriterGreeting.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useEffect, useState } from 'react'; - -interface TypewriterGreetingProps { - greetings: string[]; - typingSpeed?: number; - deletingSpeed?: number; - pauseTime?: number; - className?: string; -} - -const TypewriterGreeting = ({ - greetings, - typingSpeed = 100, - deletingSpeed = 50, - pauseTime = 2000, - className = '', -}: TypewriterGreetingProps) => { - const [currentGreetingIndex, setCurrentGreetingIndex] = useState(0); - const [displayedText, setDisplayedText] = useState(''); - const [isDeleting, setIsDeleting] = useState(false); - const [isPaused, setIsPaused] = useState(false); - - useEffect(() => { - if (greetings.length === 0) return; - - const currentGreeting = greetings[currentGreetingIndex]; - const speed = isDeleting ? deletingSpeed : typingSpeed; - - if (isPaused) { - const pauseTimer = setTimeout(() => { - setIsPaused(false); - setIsDeleting(true); - }, pauseTime); - return () => clearTimeout(pauseTimer); - } - - const timer = setTimeout(() => { - if (!isDeleting) { - // Typing - if (displayedText.length < currentGreeting.length) { - setDisplayedText(currentGreeting.slice(0, displayedText.length + 1)); - } else { - // Finished typing, pause before deleting - setIsPaused(true); - } - } else { - // Deleting - if (displayedText.length > 0) { - setDisplayedText(displayedText.slice(0, -1)); - } else { - // Finished deleting, move to next greeting - setIsDeleting(false); - setCurrentGreetingIndex(prev => (prev + 1) % greetings.length); - } - } - }, speed); - - return () => clearTimeout(timer); - }, [ - displayedText, - isDeleting, - isPaused, - currentGreetingIndex, - greetings, - typingSpeed, - deletingSpeed, - pauseTime, - ]); - - return ( -

- {displayedText} - | -

- ); -}; - -export default TypewriterGreeting; diff --git a/app/src/components/WalletInfoSection.tsx b/app/src/components/WalletInfoSection.tsx deleted file mode 100644 index 74ca795c8..000000000 --- a/app/src/components/WalletInfoSection.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; - -import { useUser } from '../hooks/useUser'; -import { useSkillConnectionStatus } from '../lib/skills/hooks'; -import { skillManager } from '../lib/skills/manager'; -import { useCoreState } from '../providers/CoreStateProvider'; -import { buildManualSentryEvent, enqueueError } from '../services/errorReportQueue'; - -function truncateAddress(address: string): string { - if (!address || address.length < 12) return address; - return `${address.slice(0, 6)}…${address.slice(-4)}`; -} - -export default function WalletInfoSection() { - const walletStatus = useSkillConnectionStatus('wallet'); - const { user } = useUser(); - const { snapshot } = useCoreState(); - const primaryAddress = user?._id ? snapshot.localState.primaryWalletAddress : undefined; - - const [networkName, setNetworkName] = useState(null); - const [balance, setBalance] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const isConnected = walletStatus === 'connected' && !!primaryAddress; - const cancelledRef = useRef(false); - const retryTimerRef = useRef | null>(null); - const fetchWalletInfoRef = useRef<(address: string, attempt?: number) => Promise>(null!); - - const fetchWalletInfo = useCallback(async (address: string, attempt = 0): Promise => { - if (cancelledRef.current) return; - - if (!skillManager.isSkillRunning('wallet')) { - if (attempt < 5 && !cancelledRef.current) { - retryTimerRef.current = setTimeout( - () => fetchWalletInfoRef.current(address, attempt + 1), - 1500 - ); - return; - } - if (!cancelledRef.current) { - setLoading(false); - setNetworkName(null); - setBalance(null); - setError(null); - } - return; - } - - try { - // Wallet skill only supports Ethereum Mainnet (chain_id "1") - const listRes = await skillManager.callTool('wallet', 'list_networks', {}); - if (cancelledRef.current) return; - const listText = listRes.content?.[0]?.text; - if (!listText || listRes.isError) { - if (!cancelledRef.current) { - setError('Could not load networks'); - setLoading(false); - } - return; - } - const listData = JSON.parse(listText) as { - networks?: Array<{ chain_id?: string; name?: string; chain_type?: string }>; - }; - const networks = Array.isArray(listData.networks) ? listData.networks : []; - // Prefer Ethereum Mainnet (skill always has it); else first EVM, else first network - const ethMainnet = networks.find(n => n?.chain_id === '1' && n?.chain_type === 'evm'); - const firstEvm = networks.find(n => n && n.chain_type === 'evm'); - const chosen = ethMainnet ?? firstEvm ?? networks.find(Boolean); - if (!chosen || cancelledRef.current) { - if (!cancelledRef.current) setLoading(false); - return; - } - - const networkNameVal = chosen.name ?? chosen.chain_id ?? 'Unknown'; - if (!cancelledRef.current) setNetworkName(networkNameVal); - - const chainId = chosen.chain_id ?? ''; - if (!chainId) { - if (!cancelledRef.current) { - setBalance('—'); - setLoading(false); - } - return; - } - - const balanceRes = await skillManager.callTool('wallet', 'get_balance', { - address, - chain_id: chainId, - chain_type: chosen.chain_type ?? 'evm', - }); - if (cancelledRef.current) return; - const balanceText = balanceRes.content?.[0]?.text; - if (!balanceText || balanceRes.isError) { - if (!cancelledRef.current) { - setBalance('—'); - setLoading(false); - } - return; - } - const balanceData = JSON.parse(balanceText) as { - balance_eth?: string; - symbol?: string; - error?: string; - }; - if (balanceData.error) { - if (!cancelledRef.current) { - setBalance('—'); - setLoading(false); - } - return; - } - const eth = balanceData.balance_eth ?? '0'; - const symbol = balanceData.symbol ?? 'ETH'; - const parsed = parseFloat(eth); - const value = Number.isFinite(parsed) ? parsed : 0; - const display = value < 0.0001 ? '0' : value.toFixed(4); - if (!cancelledRef.current) { - setBalance(`${display} ${symbol}`); - setLoading(false); - } - } catch (e) { - if (!cancelledRef.current) { - const msg = e instanceof Error ? e.message : String(e); - const isTransient = - msg.includes('not running') || msg.includes('not started') || msg.includes('transport'); - if (isTransient && attempt < 3) { - retryTimerRef.current = setTimeout( - () => fetchWalletInfoRef.current(address, attempt + 1), - 2000 - ); - return; - } - console.error('[WalletInfoSection] Failed to load wallet info:', e); - enqueueError({ - id: crypto.randomUUID(), - timestamp: Date.now(), - source: 'manual', - title: 'Failed to load wallet info', - message: e instanceof Error ? e.message : String(e), - sentryEvent: buildManualSentryEvent( - { type: 'WalletInfoLoadError', value: e instanceof Error ? e.message : String(e) }, - { component: 'WalletInfoSection' } - ), - originalError: e instanceof Error ? e : new Error(String(e)), - }); - setError('Failed to load wallet info'); - setBalance(null); - setNetworkName(null); - setLoading(false); - } - } - }, []); - - fetchWalletInfoRef.current = fetchWalletInfo; - - useEffect(() => { - cancelledRef.current = false; - if (retryTimerRef.current) { - clearTimeout(retryTimerRef.current); - retryTimerRef.current = null; - } - - if (!isConnected || !primaryAddress) { - setLoading(false); - setNetworkName(null); - setBalance(null); - setError(null); - return; - } - - setLoading(true); - setError(null); - - fetchWalletInfo(primaryAddress); - - return () => { - cancelledRef.current = true; - if (retryTimerRef.current) clearTimeout(retryTimerRef.current); - }; - }, [isConnected, primaryAddress, fetchWalletInfo]); - - if (!isConnected) return null; - - return ( -
-
- - - - Web3 Wallet -
-
-
- Address - - {primaryAddress ? truncateAddress(primaryAddress) : '—'} - -
-
- Network - - {loading ? ( - Loading… - ) : error ? ( - {error} - ) : ( - (networkName ?? '—') - )} - -
-
- Balance - - {loading ? ( - Loading… - ) : error ? ( - '—' - ) : ( - (balance ?? '—') - )} - -
-
-
- ); -} diff --git a/app/src/components/daemon/DaemonHealthIndicator.tsx b/app/src/components/daemon/DaemonHealthIndicator.tsx deleted file mode 100644 index ca59aeb75..000000000 --- a/app/src/components/daemon/DaemonHealthIndicator.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Daemon Health Indicator - * - * Compact status indicator showing daemon health with a colored dot and optional label. - * Can be clicked to show detailed health information. - */ -import type React from 'react'; - -import type { DaemonStatus } from '../../features/daemon/store'; -import { formatRelativeTime, useDaemonHealth } from '../../hooks/useDaemonHealth'; - -interface Props { - userId?: string; - size?: 'sm' | 'md' | 'lg'; - showLabel?: boolean; - onClick?: () => void; - className?: string; -} - -const DaemonHealthIndicator: React.FC = ({ - userId, - size = 'md', - showLabel = false, - onClick, - className = '', -}) => { - const daemonHealth = useDaemonHealth(userId); - - // Size configurations - const sizeConfig = { - sm: { dot: 'w-2 h-2', text: 'text-xs', container: 'gap-1.5' }, - md: { dot: 'w-3 h-3', text: 'text-sm', container: 'gap-2' }, - lg: { dot: 'w-4 h-4', text: 'text-base', container: 'gap-2.5' }, - }; - - const config = sizeConfig[size]; - - // Status color mapping - const getStatusColor = (status: DaemonStatus): string => { - switch (status) { - case 'running': - return 'bg-green-500'; - case 'starting': - return 'bg-yellow-500'; - case 'error': - return 'bg-red-500'; - case 'disconnected': - default: - return 'bg-gray-500'; - } - }; - - // Status text mapping - const getStatusText = (status: DaemonStatus): string => { - switch (status) { - case 'running': - return 'Running'; - case 'starting': - return 'Starting'; - case 'error': - return 'Error'; - case 'disconnected': - default: - return 'Disconnected'; - } - }; - - // Tooltip content - const getTooltipContent = (): string => { - const { status, componentCount, healthyComponentCount, errorComponentCount, lastUpdate } = - daemonHealth; - - let tooltip = `Status: ${getStatusText(status)}`; - - if (componentCount > 0) { - tooltip += `\nComponents: ${healthyComponentCount}/${componentCount} healthy`; - if (errorComponentCount > 0) { - tooltip += ` (${errorComponentCount} errors)`; - } - } - - if (lastUpdate) { - tooltip += `\nLast update: ${formatRelativeTime(lastUpdate)}`; - } - - return tooltip; - }; - - const statusColor = getStatusColor(daemonHealth.status); - const statusText = getStatusText(daemonHealth.status); - - const containerClasses = ` - flex items-center ${config.container} - ${onClick ? 'cursor-pointer hover:opacity-80 transition-opacity' : ''} - ${className} - `.trim(); - - return ( -
-
- {showLabel && ( - {statusText} - )} -
- ); -}; - -export default DaemonHealthIndicator; diff --git a/app/src/components/daemon/DaemonHealthPanel.tsx b/app/src/components/daemon/DaemonHealthPanel.tsx deleted file mode 100644 index dc1525177..000000000 --- a/app/src/components/daemon/DaemonHealthPanel.tsx +++ /dev/null @@ -1,322 +0,0 @@ -/** - * Agent Health Panel - * - * Detailed health breakdown component showing agent status, component health, - * and providing manual control buttons for agent lifecycle management. - */ -import { - ArrowPathIcon, - CheckCircleIcon, - ClockIcon, - PlayIcon, - StopIcon, - XCircleIcon, - XMarkIcon, -} from '@heroicons/react/24/outline'; -import { useEffect, useState } from 'react'; - -import type { ComponentHealth, DaemonStatus } from '../../features/daemon/store'; -import { formatRelativeTime, useDaemonHealth } from '../../hooks/useDaemonHealth'; -import { IS_DEV } from '../../utils/config'; -import { - isTauri, - openhumanGetDaemonHostConfig, - openhumanSetDaemonHostConfig, -} from '../../utils/tauriCommands'; - -interface Props { - userId?: string; - onClose?: () => void; - className?: string; -} - -const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => { - const daemonHealth = useDaemonHealth(userId); - const [operationLoading, setOperationLoading] = useState(null); - const [showTray, setShowTray] = useState(true); - const [trayLoaded, setTrayLoaded] = useState(false); - - // Handle agent operations with loading states - const handleOperation = async (operation: () => Promise, operationName: string) => { - setOperationLoading(operationName); - try { - await operation(); - } catch (error) { - console.error(`[AgentHealthPanel] ${operationName} failed:`, error); - } finally { - setOperationLoading(null); - } - }; - - // Status styling - const getStatusStyling = (status: DaemonStatus) => { - switch (status) { - case 'running': - return { - bg: 'bg-green-900/20 border-green-500/30', - text: 'text-green-400', - icon: CheckCircleIcon, - }; - case 'starting': - return { - bg: 'bg-yellow-900/20 border-yellow-500/30', - text: 'text-yellow-400', - icon: ClockIcon, - }; - case 'error': - return { bg: 'bg-red-900/20 border-red-500/30', text: 'text-red-400', icon: XCircleIcon }; - case 'disconnected': - default: - return { - bg: 'bg-gray-900/20 border-gray-500/30', - text: 'text-gray-400', - icon: XCircleIcon, - }; - } - }; - - // Component status styling - const getComponentStyling = (component: ComponentHealth) => { - switch (component.status) { - case 'ok': - return { bg: 'bg-green-500', text: 'text-green-400', icon: CheckCircleIcon }; - case 'error': - return { bg: 'bg-red-500', text: 'text-red-400', icon: XCircleIcon }; - case 'starting': - return { bg: 'bg-yellow-500', text: 'text-yellow-400', icon: ClockIcon }; - } - }; - - const statusStyling = getStatusStyling(daemonHealth.status); - const StatusIcon = statusStyling.icon; - - useEffect(() => { - if (!isTauri()) { - return; - } - const loadTray = async () => { - try { - const result = await openhumanGetDaemonHostConfig(); - setShowTray(result.result.show_tray); - setTrayLoaded(true); - } catch (error) { - console.error('[AgentHealthPanel] Failed to load daemon host config:', error); - } - }; - void loadTray(); - }, []); - - const updateTraySetting = async (enabled: boolean) => { - if (!isTauri()) { - return; - } - const previous = showTray; - setShowTray(enabled); - setOperationLoading('tray'); - try { - await openhumanSetDaemonHostConfig(enabled); - } catch (error) { - setShowTray(previous); - console.error('[AgentHealthPanel] Failed to save daemon host config:', error); - } finally { - setOperationLoading(null); - } - }; - - return ( -
- {/* Header */} -
-

Agent Status

- {onClose && ( - - )} -
- - {/* Overall Status */} -
-
-
- -
-
- Status: {daemonHealth.status.charAt(0).toUpperCase() + daemonHealth.status.slice(1)} -
- {daemonHealth.healthSnapshot && ( -
- PID: {daemonHealth.healthSnapshot.pid} • Uptime: {daemonHealth.uptimeText} -
- )} - {daemonHealth.lastUpdate && ( -
- Last update: {formatRelativeTime(daemonHealth.lastUpdate)} -
- )} -
-
- - {/* Recovery indicator */} - {daemonHealth.isRecovering && ( -
- - Recovering... -
- )} -
-
- - {/* Component Health */} - {daemonHealth.componentCount > 0 && ( -
-

- Components ({daemonHealth.healthyComponentCount}/{daemonHealth.componentCount} healthy) -

-
- {Object.entries(daemonHealth.components).map(([name, component]) => { - const componentStyling = getComponentStyling(component); - const ComponentIcon = componentStyling.icon; - - return ( -
-
-
-
- - {name} -
-
- Updated: {formatRelativeTime(component.updated_at)} -
- {component.restart_count > 0 && ( -
- Restarts: {component.restart_count} -
- )} - {component.last_error && component.status === 'error' && ( -
- Error: {component.last_error} -
- )} -
-
- ); - })} -
-
- )} - - {/* Auto-start Toggle */} -
-
-
Auto-start Agent
-
Automatically start agent on app launch
-
- -
- -
-
-
Show Daemon Tray
-
Display tray icon in daemon host mode
-
Requires daemon host restart to apply
-
- -
- - {/* Control Actions */} -
- - - - - - - -
- - {/* Connection Info */} - {daemonHealth.connectionAttempts > 0 && ( -
-
- Connection attempts: {daemonHealth.connectionAttempts} -
-
- )} - - {/* Debug Info (development only) */} - {IS_DEV && daemonHealth.healthSnapshot && ( -
- Debug Info -
-            {JSON.stringify(daemonHealth.healthSnapshot, null, 2)}
-          
-
- )} -
- ); -}; - -export default DaemonHealthPanel; diff --git a/app/src/components/intelligence/index.ts b/app/src/components/intelligence/index.ts deleted file mode 100644 index 58b34fc59..000000000 --- a/app/src/components/intelligence/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Intelligence Components -// Exports for the AI-powered actionable insights dashboard - -export { ActionableCard } from './ActionableCard'; -export { ConfirmationModal } from './ConfirmationModal'; -export { Toast, ToastContainer } from './Toast'; -export { MOCK_ACTIONABLE_ITEMS } from './mockData'; -export { groupItemsByTime, filterItems, getItemStats } from './utils'; - -// Re-export types for convenience -export type { - ActionableItem, - ActionableItemSource, - ActionableItemPriority, - ActionableItemStatus, - TimeGroup, - ToastNotification, - ConfirmationModal as ConfirmationModalType, -} from '../../types/intelligence'; diff --git a/app/src/components/intelligence/mockData.ts b/app/src/components/intelligence/mockData.ts deleted file mode 100644 index 87458201d..000000000 --- a/app/src/components/intelligence/mockData.ts +++ /dev/null @@ -1,311 +0,0 @@ -import type { ActionableItem } from '../../types/intelligence'; - -// Helper function to create dates relative to now -function createDate(minutesAgo: number): Date { - return new Date(Date.now() - minutesAgo * 60 * 1000); -} - -function createDateDays(daysAgo: number): Date { - return new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000); -} - -export const MOCK_ACTIONABLE_ITEMS: ActionableItem[] = [ - // Today - Fresh items - { - id: '1', - title: 'Reply to 2 critical emails expecting response within 24hrs', - description: - 'Messages from john@coinbase.com and sarah@ethereum.org about partnership proposals', - source: 'email', - priority: 'critical', - status: 'active', - createdAt: createDate(2), - updatedAt: createDate(2), - expiresAt: createDate(-1440), // Expires in 24 hours - actionable: true, - hasComplexAction: true, - sourceLabel: 'Gmail', - }, - - { - id: '2', - title: 'Meeting with Sarah in 30 minutes - prepare documents?', - description: - 'Q4 strategy meeting. Need to review the crypto market analysis and portfolio updates.', - source: 'calendar', - priority: 'important', - status: 'active', - createdAt: createDate(5), - updatedAt: createDate(5), - actionable: true, - hasComplexAction: false, - sourceLabel: 'Calendar', - }, - - { - id: '3', - title: 'Order lunch for your 2pm work session?', - description: - 'You usually order from "Crypto Café" around this time. Your favorite: Bitcoin Bowl ($15.99)', - source: 'ai_insight', - priority: 'normal', - status: 'active', - createdAt: createDate(15), - updatedAt: createDate(15), - actionable: true, - hasComplexAction: false, - sourceLabel: 'AI Assistant', - }, - - { - id: '4', - title: '5 unread messages from Alpha Trading Group', - description: - '@cryptowhale mentioned you about the ETH analysis. 3 other traders are discussing market trends.', - source: 'telegram', - priority: 'normal', - status: 'active', - createdAt: createDate(25), - updatedAt: createDate(25), - actionable: true, - hasComplexAction: true, - sourceLabel: 'Telegram', - }, - - { - id: '5', - title: 'Free 45min slot available for gym session', - description: 'Your calendar shows a gap between 3:15-4:00 PM. LA Fitness has availability.', - source: 'ai_insight', - priority: 'normal', - status: 'active', - createdAt: createDate(45), - updatedAt: createDate(45), - actionable: true, - hasComplexAction: false, - sourceLabel: 'AI Planner', - }, - - // Yesterday - { - id: '6', - title: 'Alex mentioned you in Alpha Human Development chat', - description: '"@john can you review the new trading algorithm?" - 3 hours ago', - source: 'telegram', - priority: 'important', - status: 'active', - createdAt: createDate(180), - updatedAt: createDate(180), - actionable: true, - hasComplexAction: true, - sourceLabel: 'Telegram', - }, - - { - id: '7', - title: 'Update available for DeFi trading bot', - description: - 'Version 2.1.4 includes security patches and 15% better performance. Requires restart.', - source: 'system', - priority: 'important', - status: 'active', - createdAt: createDate(360), - updatedAt: createDate(360), - actionable: true, - requiresConfirmation: true, - sourceLabel: 'System', - }, - - { - id: '8', - title: 'Backup your crypto wallet keys', - description: 'Last backup was 30 days ago. Your portfolio is up 34% since then.', - source: 'security', - priority: 'critical', - status: 'active', - createdAt: createDate(420), - updatedAt: createDate(420), - actionable: true, - requiresConfirmation: false, - hasComplexAction: true, - sourceLabel: 'Security', - }, - - // This week - { - id: '9', - title: 'Schedule meeting with VCs for Series A discussion', - description: 'Follow up on last weeks pitch. Andreessen Horowitz and Sequoia are interested.', - source: 'email', - priority: 'critical', - status: 'active', - createdAt: createDateDays(2), - updatedAt: createDateDays(2), - actionable: true, - hasComplexAction: true, - sourceLabel: 'Gmail', - }, - - { - id: '10', - title: 'Review 12 pending GitHub pull requests', - description: - 'Features for v0.21.0 release. 8 from team members, 4 from community contributors.', - source: 'system', - priority: 'important', - status: 'active', - createdAt: createDateDays(3), - updatedAt: createDateDays(3), - actionable: true, - hasComplexAction: true, - sourceLabel: 'GitHub', - }, - - { - id: '11', - title: 'BTC price alert: Approaching your target of $65,000', - description: 'Currently at $64,420. You set this alert 2 weeks ago. Consider taking profits?', - source: 'trading', - priority: 'important', - status: 'active', - createdAt: createDateDays(3), - updatedAt: createDateDays(3), - actionable: true, - hasComplexAction: false, - sourceLabel: 'Trading Bot', - }, - - { - id: '12', - title: 'Complete KYC verification for Binance account', - description: 'Required for withdrawal limits above $2,000. Upload ID and proof of address.', - source: 'trading', - priority: 'normal', - status: 'active', - createdAt: createDateDays(4), - updatedAt: createDateDays(4), - actionable: true, - hasComplexAction: true, - sourceLabel: 'Binance', - }, - - // Older items - { - id: '13', - title: 'Respond to podcast interview request from Lex Fridman', - description: 'Topic: "The Future of Decentralized Communication". Suggested dates: next month.', - source: 'email', - priority: 'important', - status: 'active', - createdAt: createDateDays(8), - updatedAt: createDateDays(8), - actionable: true, - hasComplexAction: true, - sourceLabel: 'Gmail', - }, - - { - id: '14', - title: 'Submit tax documents for Q3 crypto transactions', - description: '47 transactions across 5 exchanges. Deadline: October 15th.', - source: 'system', - priority: 'critical', - status: 'active', - createdAt: createDateDays(10), - updatedAt: createDateDays(10), - expiresAt: createDate(-14400), // Expires in 10 days - actionable: true, - hasComplexAction: true, - requiresConfirmation: false, - sourceLabel: 'TaxBot', - }, - - { - id: '15', - title: 'Renew SSL certificate for api.openhuman.com', - description: - 'Certificate expires in 5 days. Automatic renewal failed - manual intervention required.', - source: 'system', - priority: 'critical', - status: 'active', - createdAt: createDateDays(12), - updatedAt: createDateDays(12), - actionable: true, - hasComplexAction: false, - requiresConfirmation: false, - sourceLabel: 'DevOps', - }, - - { - id: '16', - title: 'Plan team building event for remote employees', - description: - '15 team members across 8 time zones. Budget approved: $5,000. Preference: virtual escape room.', - source: 'ai_insight', - priority: 'normal', - status: 'active', - createdAt: createDateDays(14), - updatedAt: createDateDays(14), - actionable: true, - hasComplexAction: true, - sourceLabel: 'HR Assistant', - }, - - // Expired/old items that should show as low priority - { - id: '17', - title: 'Update LinkedIn profile with latest achievements', - description: 'Add recent TechCrunch feature and Y Combinator alumni status.', - source: 'ai_insight', - priority: 'normal', - status: 'active', - createdAt: createDateDays(20), - updatedAt: createDateDays(20), - actionable: true, - hasComplexAction: false, - sourceLabel: 'Career AI', - }, - - { - id: '18', - title: 'Review and approve marketing budget for Q4', - description: '$50K allocated for influencer partnerships and conference sponsorships.', - source: 'email', - priority: 'important', - status: 'active', - createdAt: createDateDays(21), - updatedAt: createDateDays(21), - actionable: true, - hasComplexAction: true, - sourceLabel: 'Team Email', - }, - - { - id: '19', - title: 'Check server capacity for expected user growth', - description: 'Daily active users grew 23% this month. Current infrastructure may need scaling.', - source: 'system', - priority: 'normal', - status: 'active', - createdAt: createDateDays(25), - updatedAt: createDateDays(25), - actionable: true, - hasComplexAction: false, - sourceLabel: 'Monitoring', - }, - - { - id: '20', - title: 'Organize crypto portfolio - consolidate across 7 wallets', - description: - 'Assets spread across MetaMask, Ledger, Trust Wallet, and 4 others. Gas fees optimized for Sunday.', - source: 'security', - priority: 'normal', - status: 'active', - createdAt: createDateDays(30), - updatedAt: createDateDays(30), - actionable: true, - hasComplexAction: true, - sourceLabel: 'Portfolio AI', - }, -]; diff --git a/app/src/components/settings/panels/components/ActionPanel.tsx b/app/src/components/settings/panels/components/ActionPanel.tsx deleted file mode 100644 index b73d73386..000000000 --- a/app/src/components/settings/panels/components/ActionPanel.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { CheckIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline'; -import React from 'react'; - -interface ActionPanelProps { - children: React.ReactNode; - hasChanges?: boolean; - success?: boolean | string; - error?: string; - className?: string; -} - -const ActionPanel: React.FC = ({ - children, - hasChanges = false, - success = false, - error, - className = '', -}) => { - return ( -
-
- {children} - {hasChanges && ( -
-
- Unsaved changes -
- )} -
- - {success && ( -
- - {typeof success === 'string' ? success : 'Operation completed successfully'} -
- )} - - {error && ( -
- - {error} -
- )} -
- ); -}; - -interface PrimaryButtonProps { - onClick: () => void; - loading?: boolean; - disabled?: boolean; - variant?: 'primary' | 'secondary' | 'outline'; - children: React.ReactNode; - className?: string; -} - -const PrimaryButton: React.FC = ({ - onClick, - loading = false, - disabled = false, - variant = 'primary', - children, - className = '', -}) => { - const baseClasses = - 'px-6 py-3 rounded-lg font-medium transition-all duration-200 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed'; - const variantClasses = { - primary: - 'bg-primary-600 hover:bg-primary-500 active:bg-primary-700 text-white shadow-soft hover:shadow-lg hover:shadow-primary-500/25 focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2 focus:ring-offset-white', - secondary: - 'bg-stone-100 hover:bg-stone-200 active:bg-stone-300 text-stone-900 border border-stone-200 focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2 focus:ring-offset-white', - outline: - 'border border-stone-200 text-stone-900 hover:bg-stone-100 active:bg-stone-200 focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2 focus:ring-offset-white', - }; - - return ( - - ); -}; - -export default ActionPanel; -export { PrimaryButton }; diff --git a/app/src/components/settings/panels/components/InputGroup.tsx b/app/src/components/settings/panels/components/InputGroup.tsx deleted file mode 100644 index 70b8f22de..000000000 --- a/app/src/components/settings/panels/components/InputGroup.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; - -interface InputGroupProps { - title?: string; - description?: string; - children: React.ReactNode; - className?: string; -} - -const InputGroup: React.FC = ({ - title, - description, - children, - className = '', -}) => { - return ( -
- {title && ( -
-

{title}

- {description &&

{description}

} -
- )} - -
-
{children}
-
-
- ); -}; - -interface FieldProps { - label: string; - children: React.ReactNode; - helpText?: string; - className?: string; - fullWidth?: boolean; -} - -const Field: React.FC = ({ - label, - children, - helpText, - className = '', - fullWidth = false, -}) => { - return ( - - ); -}; - -interface CheckboxFieldProps { - label: string; - checked: boolean; - onChange: (checked: boolean) => void; - helpText?: string; - className?: string; -} - -const CheckboxField: React.FC = ({ - label, - checked, - onChange, - helpText, - className = '', -}) => { - return ( -
- - {helpText &&

{helpText}

} -
- ); -}; - -export default InputGroup; -export { Field, CheckboxField }; diff --git a/app/src/components/settings/panels/components/SectionCard.tsx b/app/src/components/settings/panels/components/SectionCard.tsx deleted file mode 100644 index 75b7ea011..000000000 --- a/app/src/components/settings/panels/components/SectionCard.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; -import React, { useState } from 'react'; - -interface SectionCardProps { - title: string; - priority: 'critical' | 'infrastructure' | 'development' | 'tools'; - icon: React.ReactElement; - children: React.ReactNode; - collapsible?: boolean; - defaultExpanded?: boolean; - hasChanges?: boolean; - loading?: boolean; -} - -const priorityStyles = { - critical: 'bg-gradient-to-br from-primary-50 to-white border-primary-200', - infrastructure: 'bg-gradient-to-br from-stone-50 to-white border-stone-200', - development: 'bg-gradient-to-br from-amber-50 to-white border-amber-200', - tools: 'bg-stone-50 border-stone-200', -} as const; - -const priorityIconColors = { - critical: 'text-primary-500', - infrastructure: 'text-stone-500', - development: 'text-amber-500', - tools: 'text-stone-400', -} as const; - -const SectionCard: React.FC = ({ - title, - priority, - icon, - children, - collapsible = false, - defaultExpanded = true, - hasChanges = false, - loading = false, -}) => { - const [isExpanded, setIsExpanded] = useState(defaultExpanded); - - const handleToggle = () => { - if (collapsible) { - setIsExpanded(!isExpanded); - } - }; - - return ( -
-
-
-
- {loading ? ( -
- ) : ( - React.cloneElement(icon as React.ReactElement<{ className?: string }>, { - className: 'h-5 w-5', - }) - )} -
-
-

{title}

- {hasChanges &&
} - {loading && Loading...} -
-
- {collapsible && ( -
- {isExpanded ? ( - - ) : ( - - )} -
- )} -
- - {(!collapsible || isExpanded) && ( -
-
{children}
-
- )} -
- ); -}; - -export default SectionCard; diff --git a/app/src/components/settings/panels/components/ValidatedField.tsx b/app/src/components/settings/panels/components/ValidatedField.tsx deleted file mode 100644 index 68c425927..000000000 --- a/app/src/components/settings/panels/components/ValidatedField.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { CheckCircleIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline'; -import React from 'react'; - -interface ValidatedFieldProps { - label: string; - value: string; - onChange: (value: string) => void; - error?: string; - required?: boolean; - type?: 'text' | 'password' | 'url' | 'number'; - placeholder?: string; - helpText?: string; - className?: string; - fullWidth?: boolean; - validation?: 'valid' | 'invalid' | 'none'; - disabled?: boolean; -} - -const ValidatedField: React.FC = ({ - label, - value, - onChange, - error, - required = false, - type = 'text', - placeholder, - helpText, - className = '', - fullWidth = false, - validation = 'none', - disabled = false, -}) => { - const hasError = !!error; - const isValid = validation === 'valid' && !hasError && value.trim() !== ''; - - const inputClasses = ` - w-full px-4 py-3 rounded-lg border transition-all duration-200 - focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white - disabled:opacity-50 disabled:cursor-not-allowed - ${ - hasError - ? 'border-coral-500/60 bg-coral-50 text-coral-700 placeholder-coral-400/50 focus:border-coral-500 focus:ring-coral-500/30' - : isValid - ? 'border-sage-500/60 bg-sage-50 text-stone-900 placeholder-stone-400 focus:border-sage-500 focus:ring-sage-500/30' - : 'border-stone-200 bg-white text-stone-900 placeholder-stone-400 focus:border-primary-500/50 focus:ring-primary-500/30' - } - `; - - return ( - - ); -}; - -interface ValidatedSelectProps { - label: string; - value: string; - onChange: (value: string) => void; - options: Array<{ value: string; label: string; description?: string }>; - error?: string; - required?: boolean; - placeholder?: string; - helpText?: string; - className?: string; - fullWidth?: boolean; - disabled?: boolean; - validation?: 'valid' | 'invalid' | 'none'; -} - -const ValidatedSelect: React.FC = ({ - label, - value, - onChange, - options, - error, - required = false, - placeholder = 'Select option...', - helpText, - className = '', - fullWidth = false, - disabled = false, - validation = 'none', -}) => { - const hasError = !!error; - const isValid = validation === 'valid' && !hasError && value.trim() !== ''; - - const selectClasses = ` - w-full px-4 py-3 rounded-lg border transition-all duration-200 - focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white - disabled:opacity-50 disabled:cursor-not-allowed - ${ - hasError - ? 'border-coral-500/60 bg-coral-50 text-coral-700 focus:border-coral-500 focus:ring-coral-500/30' - : isValid - ? 'border-sage-500/60 bg-sage-50 text-stone-900 focus:border-sage-500 focus:ring-sage-500/30' - : 'border-stone-200 bg-white text-stone-900 focus:border-primary-500/50 focus:ring-primary-500/30' - } - `; - - return ( - - ); -}; - -export default ValidatedField; -export { ValidatedSelect }; diff --git a/app/src/data/countries.ts b/app/src/data/countries.ts deleted file mode 100644 index ed87e1362..000000000 --- a/app/src/data/countries.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Country } from '../types/onboarding'; - -export const countries: Country[] = [ - { code: 'US', name: 'United States', flag: '🇺🇸', dialCode: '+1' }, - { code: 'CA', name: 'Canada', flag: '🇨🇦', dialCode: '+1' }, - { code: 'GB', name: 'United Kingdom', flag: '🇬🇧', dialCode: '+44' }, - { code: 'DE', name: 'Germany', flag: '🇩🇪', dialCode: '+49' }, - { code: 'FR', name: 'France', flag: '🇫🇷', dialCode: '+33' }, - { code: 'JP', name: 'Japan', flag: '🇯🇵', dialCode: '+81' }, - { code: 'KR', name: 'South Korea', flag: '🇰🇷', dialCode: '+82' }, - { code: 'CN', name: 'China', flag: '🇨🇳', dialCode: '+86' }, - { code: 'IN', name: 'India', flag: '🇮🇳', dialCode: '+91' }, - { code: 'AU', name: 'Australia', flag: '🇦🇺', dialCode: '+61' }, - { code: 'BR', name: 'Brazil', flag: '🇧🇷', dialCode: '+55' }, - { code: 'MX', name: 'Mexico', flag: '🇲🇽', dialCode: '+52' }, - { code: 'AR', name: 'Argentina', flag: '🇦🇷', dialCode: '+54' }, - { code: 'CL', name: 'Chile', flag: '🇨🇱', dialCode: '+56' }, - { code: 'SG', name: 'Singapore', flag: '🇸🇬', dialCode: '+65' }, - { code: 'HK', name: 'Hong Kong', flag: '🇭🇰', dialCode: '+852' }, - { code: 'CH', name: 'Switzerland', flag: '🇨🇭', dialCode: '+41' }, - { code: 'NL', name: 'Netherlands', flag: '🇳🇱', dialCode: '+31' }, - { code: 'SE', name: 'Sweden', flag: '🇸🇪', dialCode: '+46' }, - { code: 'NO', name: 'Norway', flag: '🇳🇴', dialCode: '+47' }, -]; diff --git a/app/src/hooks/useIntelligenceApiFallback.ts b/app/src/hooks/useIntelligenceApiFallback.ts index d548fa44d..ba94c29da 100644 --- a/app/src/hooks/useIntelligenceApiFallback.ts +++ b/app/src/hooks/useIntelligenceApiFallback.ts @@ -1,8 +1,15 @@ import { useCallback, useState } from 'react'; -import type { ConnectedTool } from '../services/intelligenceApi'; import type { ActionableItemStatus, ChatMessage } from '../types/intelligence'; +interface ConnectedTool { + name: string; + description: string; + parameters: Record; + skillId: string; + enabled: boolean; +} + /** * Local-only implementations of Intelligence action hooks. * Items come from the local conscious memory layer — actions are applied in-memory. diff --git a/app/src/hooks/useLocalModelStatus.ts b/app/src/hooks/useLocalModelStatus.ts deleted file mode 100644 index 0cb697074..000000000 --- a/app/src/hooks/useLocalModelStatus.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * useLocalModelStatus — returns true when a local AI model is ready and serving. - * - * Polls openhumanLocalAiStatus() every POLL_INTERVAL_MS. - * Returns false in non-Tauri environments (browser dev / remote-only). - * This is the gate used by the multi-bubble delivery path. - */ -import { useEffect, useRef, useState } from 'react'; - -import { isTauri, openhumanLocalAiStatus } from '../utils/tauriCommands'; - -const POLL_INTERVAL_MS = 12_000; - -export function useLocalModelStatus(): boolean { - const [isActive, setIsActive] = useState(false); - const mountedRef = useRef(true); - - useEffect(() => { - mountedRef.current = true; - - if (!isTauri()) return; - - const check = async () => { - try { - const res = await openhumanLocalAiStatus(); - if (mountedRef.current) { - setIsActive(res.result?.state === 'ready'); - } - } catch { - if (mountedRef.current) setIsActive(false); - } - }; - - void check(); - const id = setInterval(() => void check(), POLL_INTERVAL_MS); - - return () => { - mountedRef.current = false; - clearInterval(id); - }; - }, []); - - return isActive; -} diff --git a/app/src/hooks/useSocket.ts b/app/src/hooks/useSocket.ts deleted file mode 100644 index 85b7949b9..000000000 --- a/app/src/hooks/useSocket.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { useEffect, useRef } from 'react'; -import type { Socket } from 'socket.io-client'; - -import { socketService } from '../services/socketService'; -import { useAppSelector } from '../store/hooks'; -import { selectSocketStatus } from '../store/socketSelectors'; - -/** - * React hook for using the Socket.IO connection - * Note: The socket connection is managed by SocketProvider based on JWT token. - * This hook provides access to the socket instance and methods. - * - * @example - * ```tsx - * const { socket, isConnected, emit, on, off } = useSocket(); - * - * useEffect(() => { - * on('ready', () => { - * console.log('Socket ready!'); - * }); - * - * return () => { - * off('ready'); - * }; - * }, [on, off]); - * ``` - */ -export const useSocket = () => { - const listenersRef = useRef void }>>([]); - const socketStatus = useAppSelector(selectSocketStatus); - - useEffect(() => { - return () => { - // Cleanup: remove all listeners registered through this hook - listenersRef.current.forEach(({ event, callback }) => { - socketService.off(event, callback); - }); - listenersRef.current = []; - }; - }, []); - - const emit = (event: string, data?: unknown) => { - socketService.emit(event, data); - }; - - const on = (event: string, callback: (...args: unknown[]) => void) => { - socketService.on(event, callback); - listenersRef.current.push({ event, callback }); - }; - - const off = (event: string, callback?: (...args: unknown[]) => void) => { - socketService.off(event, callback); - if (callback) { - listenersRef.current = listenersRef.current.filter( - listener => listener.event !== event || listener.callback !== callback - ); - } else { - listenersRef.current = listenersRef.current.filter(listener => listener.event !== event); - } - }; - - const once = (event: string, callback: (...args: unknown[]) => void) => { - socketService.once(event, callback); - }; - - return { - socket: socketService.getSocket() as Socket | null, - isConnected: socketStatus === 'connected', - status: socketStatus, - emit, - on, - off, - once, - }; -}; diff --git a/app/src/hooks/useToolsUpdates.ts b/app/src/hooks/useToolsUpdates.ts deleted file mode 100644 index 10535ec31..000000000 --- a/app/src/hooks/useToolsUpdates.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * React hook to listen for tools updates and trigger re-renders - * - * Components using this hook will automatically re-render when TOOLS.md - * is updated and the cache is refreshed. - */ -import { useEffect, useState } from 'react'; - -import { forceToolsCacheRefresh } from '../lib/tools/file-watcher'; - -interface ToolsUpdateEvent { - timestamp: number; -} - -/** - * Hook to listen for tools updates - * @returns timestamp of last tools update (triggers re-renders) - */ -export function useToolsUpdates(): number { - const [lastUpdate, setLastUpdate] = useState(0); - - useEffect(() => { - const handleToolsUpdate = (event: CustomEvent) => { - console.log('🔔 Tools update detected, triggering component re-render'); - setLastUpdate(event.detail.timestamp); - }; - - // Listen for tools-updated events - window.addEventListener('tools-updated', handleToolsUpdate as EventListener); - - return () => { - window.removeEventListener('tools-updated', handleToolsUpdate as EventListener); - }; - }, []); - - return lastUpdate; -} - -/** - * Hook to get a callback that forces tools refresh - * @returns function to manually trigger tools refresh - */ -export function useForceToolsRefresh(): () => Promise { - const forceRefresh = async () => { - return forceToolsCacheRefresh(); - }; - - return forceRefresh; -} diff --git a/app/src/lib/skills/tool-bridge.ts b/app/src/lib/skills/tool-bridge.ts deleted file mode 100644 index 62bde7b39..000000000 --- a/app/src/lib/skills/tool-bridge.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Tool Bridge — converts skill tools to AI tool registry format. - * - * Namespaces tool names as `__` to avoid collisions. - * Each tool's execute function delegates to skillManager.callTool(). - */ - -import { skillManager } from "./manager"; -import type { SkillToolDefinition } from "./types"; - -export interface BridgedTool { - name: string; - description: string; - parameters: Record; - execute: (args: Record) => Promise; - skillId: string; - originalName: string; -} - -/** - * Convert a skill's tools into bridged tools with namespaced names. - */ -export function bridgeSkillTools( - skillId: string, - tools: SkillToolDefinition[], -): BridgedTool[] { - return tools.map((tool) => ({ - name: `${skillId}__${tool.name}`, - description: tool.description, - parameters: tool.inputSchema, - skillId, - originalName: tool.name, - execute: async (args: Record): Promise => { - const result = await skillManager.callTool(skillId, tool.name, args); - if (result.isError) { - throw new Error( - result.content.map((c) => c.text).join("\n") || "Tool execution failed", - ); - } - return result.content.map((c) => c.text).join("\n"); - }, - })); -} - -/** - * Extract the skill ID and original tool name from a namespaced name. - */ -export function parseToolName(namespacedName: string): { - skillId: string; - toolName: string; -} | null { - const idx = namespacedName.indexOf("__"); - if (idx === -1) return null; - return { - skillId: namespacedName.substring(0, idx), - toolName: namespacedName.substring(idx + 2), - }; -} diff --git a/app/src/lib/tools/file-watcher.ts b/app/src/lib/tools/file-watcher.ts deleted file mode 100644 index c6fcf28c7..000000000 --- a/app/src/lib/tools/file-watcher.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Legacy UI-side tools cache watcher removed. - * Tool/config refresh now comes from core RPC and socket events. - */ -export function startToolsFileWatcher(): void { - // no-op -} - -export function stopToolsFileWatcher(): void { - // no-op -} - -export async function forceToolsCacheRefresh(): Promise { - // Preserve compatibility for callers that expect a resolved promise. - return Promise.resolve(); -} diff --git a/app/src/lib/webhooks/urls.ts b/app/src/lib/webhooks/urls.ts deleted file mode 100644 index 43fa61727..000000000 --- a/app/src/lib/webhooks/urls.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { BACKEND_URL } from '../../utils/config'; - -const DEFAULT_BACKEND_URL = 'https://api.tinyhumans.ai'; - -function normalizedBackendUrl(baseUrl?: string): string { - const value = (baseUrl || BACKEND_URL || DEFAULT_BACKEND_URL).trim(); - return value.replace(/\/+$/, ''); -} - -export function buildWebhookIngressUrl(tunnelUuid: string, baseUrl?: string): string { - return `${normalizedBackendUrl(baseUrl)}/webhooks/ingress/${encodeURIComponent(tunnelUuid)}`; -} diff --git a/app/src/overlay/parentCoreRpc.ts b/app/src/overlay/parentCoreRpc.ts deleted file mode 100644 index 48f564ca3..000000000 --- a/app/src/overlay/parentCoreRpc.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * HTTP JSON-RPC to the desktop core sidecar shared by the main app and overlay. - * Mirrors the naming normalization in app/src/services/coreRpcClient.ts (subset). - */ - -let nextJsonRpcId = 1; - -export const normalizeLegacyMethod = (method: string): string => { - if (method.startsWith('openhuman.accessibility_')) { - return method.replace('openhuman.accessibility_', 'openhuman.screen_intelligence_'); - } - return method; -}; - -/** RpcOutcome with non-empty logs serializes as `{ result, logs }` in the core. */ -const unwrapCliCompatibleJson = (raw: unknown): T => { - if ( - raw !== null && - typeof raw === 'object' && - Object.prototype.hasOwnProperty.call(raw, 'result') && - Object.prototype.hasOwnProperty.call(raw, 'logs') - ) { - const keys = Object.keys(raw); - const { logs } = raw as { logs: unknown }; - if (keys.length === 2 && Array.isArray(logs)) { - return (raw as { result: T }).result; - } - } - return raw as T; -}; - -interface JsonRpcError { - code: number; - message: string; - data?: unknown; -} - -interface JsonRpcResponse { - jsonrpc?: string; - id?: number | string | null; - result?: T; - error?: JsonRpcError; -} - -const DEFAULT_RPC_TIMEOUT_MS = 10_000; - -const isJsonRpcEnvelope = (value: unknown): value is JsonRpcResponse => { - if (value === null || typeof value !== 'object') { - return false; - } - - return ( - Object.prototype.hasOwnProperty.call(value, 'error') || - Object.prototype.hasOwnProperty.call(value, 'result') || - Object.prototype.hasOwnProperty.call(value, 'id') || - Object.prototype.hasOwnProperty.call(value, 'jsonrpc') - ); -}; - -export const callParentCoreRpc = async ( - rpcUrl: string, - method: string, - params: Record = {}, - timeoutMs: number = DEFAULT_RPC_TIMEOUT_MS -): Promise => { - const normalizedMethod = normalizeLegacyMethod(method); - const payload = { - jsonrpc: '2.0' as const, - id: nextJsonRpcId++, - method: normalizedMethod, - params, - }; - - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), timeoutMs); - - let response: Response; - try { - response = await fetch(rpcUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - signal: controller.signal, - }); - } catch (err) { - clearTimeout(timer); - if ( - (err instanceof DOMException && err.name === 'AbortError') || - (err instanceof Error && err.name === 'AbortError') - ) { - throw new Error( - `Core RPC request timed out after ${timeoutMs}ms (method: ${normalizedMethod})` - ); - } - throw err; - } finally { - clearTimeout(timer); - } - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Core RPC HTTP ${response.status}: ${text || response.statusText}`); - } - - const json = await response.json(); - if (!isJsonRpcEnvelope(json)) { - throw new Error('Invalid JSON-RPC envelope'); - } - - if (json.error) { - throw new Error(json.error.message || 'Core RPC returned an error'); - } - if (!Object.prototype.hasOwnProperty.call(json, 'result')) { - throw new Error('Core RPC response missing result'); - } - - return unwrapCliCompatibleJson(json.result); -}; diff --git a/app/src/pages/onboarding/steps/AnalyticsStep.tsx b/app/src/pages/onboarding/steps/AnalyticsStep.tsx deleted file mode 100644 index e378097ae..000000000 --- a/app/src/pages/onboarding/steps/AnalyticsStep.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useState } from 'react'; - -import { isTauri, openhumanUpdateAnalyticsSettings } from '../../../utils/tauriCommands'; - -interface AnalyticsStepProps { - onNext: (analyticsEnabled: boolean) => void; -} - -const AnalyticsStep = ({ onNext }: AnalyticsStepProps) => { - const [selectedOption, setSelectedOption] = useState('shareAnalytics'); - - const handleContinue = () => { - const enabled = selectedOption === 'shareAnalytics'; - - // Sync to core config so the Rust process also respects the setting - if (isTauri()) { - openhumanUpdateAnalyticsSettings({ enabled }).catch(() => { - /* best-effort */ - }); - } - - onNext(enabled); - }; - - return ( -
-
-

Anonymized Analytics

-

- We collect fully anonymized usage data to help improve the app. No personal data, - messages, or wallet keys are ever collected. You can change this anytime in Settings. -

-
- -
-
setSelectedOption('shareAnalytics')}> -
-
-
- {selectedOption === 'shareAnalytics' && ( -
- )} -
-
-
-

- Share Anonymized Usage Data -

-

- Share anonymized crash reports and usage analytics to help us improve features and - performance. All data is fully anonymized. -

-
-
-
- -
setSelectedOption('maximumPrivacy')}> -
-
-
- {selectedOption === 'maximumPrivacy' && ( -
- )} -
-
-
-

Don't Collect Anything

-

- We won't collect any usage analytics or crash reports. Keep all your data completely - private. -

-
-
-
-
- -
-
- - - -
-

- You can change this setting anytime -

-

- Your privacy preferences can be updated in Settings > Privacy & Security -

-
-
-
- - -
- ); -}; - -export default AnalyticsStep; diff --git a/app/src/pages/onboarding/steps/ConnectStep.tsx b/app/src/pages/onboarding/steps/ConnectStep.tsx deleted file mode 100644 index 9d29de53f..000000000 --- a/app/src/pages/onboarding/steps/ConnectStep.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { useState } from 'react'; - -import BinanceIcon from '../../../assets/icons/binance.svg'; -import GoogleIcon from '../../../assets/icons/GoogleIcon'; -import MetamaskIcon from '../../../assets/icons/metamask.svg'; -import NotionIcon from '../../../assets/icons/notion.svg'; -import SkillSetupModal from '../../../components/skills/SkillSetupModal'; -import type { SkillConnectionStatus } from '../../../lib/skills/types'; - -interface ConnectStepProps { - onNext: () => void; -} - -interface ConnectOption { - id: string; - name: string; - description: string; - icon: React.ReactElement; - comingSoon?: boolean; - skillId?: string; -} - -const STATUS_BADGE: Record = { - connected: { label: 'Connected', classes: 'bg-sage-500/20 text-sage-400' }, - connecting: { label: 'Connecting...', classes: 'bg-amber-500/20 text-amber-400' }, - not_authenticated: { label: 'Not Authenticated', classes: 'bg-amber-500/20 text-amber-400' }, - disconnected: { label: 'Disconnected', classes: 'bg-stone-500/20 text-stone-400' }, - error: { label: 'Error', classes: 'bg-coral-500/20 text-coral-400' }, - offline: { label: 'Offline', classes: 'bg-stone-500/20 text-stone-400' }, - setup_required: { label: 'Connect', classes: 'bg-primary-500/20 text-primary-400' }, -}; - -function ConnectOptionRow({ - option, - onConnect, -}: { - option: ConnectOption; - onConnect: (option: ConnectOption) => void; -}) { - const connectionStatus = 'setup_required' as SkillConnectionStatus; - const disabled = option.comingSoon; - - let badge: React.ReactElement; - if (option.comingSoon) { - badge = ( - Coming Soon - ); - } else if (option.skillId) { - const cfg = STATUS_BADGE[connectionStatus]; - badge = {cfg.label}; - } else { - badge = ( - - Connect - - ); - } - - return ( - - ); -} - -const ConnectStep = ({ onNext }: ConnectStepProps) => { - const [setupModalOpen, setSetupModalOpen] = useState(false); - const [activeSkillId, setActiveSkillId] = useState(null); - const [activeSkillName, setActiveSkillName] = useState(''); - const [activeSkillDescription, setActiveSkillDescription] = useState(''); - - const connectOptions: ConnectOption[] = [ - { - id: 'google', - name: 'Google', - description: 'Manage emails, contacts and calendar events', - icon: , - comingSoon: true, - }, - { - id: 'notion', - name: 'Notion', - description: 'Manage tasks, documents and everything else in your Notion', - icon: Notion, - comingSoon: true, - }, - { - id: 'wallet', - name: 'Web3 Wallet', - description: 'Trade the trenches in a safe and secure way.', - icon: Metamask, - comingSoon: true, - }, - { - id: 'exchange', - name: 'Crypto Trading Exchanges', - description: 'Connect and make trades with deep insights.', - icon: Binance, - comingSoon: true, - }, - ]; - - const handleConnect = (option: ConnectOption) => { - if (option.comingSoon) return; - if (option.skillId) { - setActiveSkillId(option.skillId); - setActiveSkillName(option.name); - setActiveSkillDescription(option.description); - setSetupModalOpen(true); - } - }; - - return ( -
-
-

Connect Accounts

-

- The more accounts you connect, the more powerful the intelligence will be. -

-
- -
- {connectOptions.map(option => ( - - ))} -
- -
-
-
-

- Remember everything is private & encrypted! -

-

- All data and credentials are stored locally and follows a strict zero-data retention - policy so you won't have to worry about anything getting leaked. -

-
-
-
- - - - {/* Setup modal */} - {setupModalOpen && activeSkillId && ( - { - setSetupModalOpen(false); - setActiveSkillId(null); - }} - /> - )} -
- ); -}; - -export default ConnectStep; diff --git a/app/src/pages/onboarding/steps/InviteCodeStep.tsx b/app/src/pages/onboarding/steps/InviteCodeStep.tsx deleted file mode 100644 index 3b089ce36..000000000 --- a/app/src/pages/onboarding/steps/InviteCodeStep.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useState } from 'react'; - -import { inviteApi } from '../../../services/api/inviteApi'; - -interface InviteCodeStepProps { - onNext: () => void; -} - -const InviteCodeStep = ({ onNext }: InviteCodeStepProps) => { - const [code, setCode] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(false); - - const handleRedeem = async () => { - const trimmed = code.trim(); - if (!trimmed) return; - - setIsLoading(true); - setError(null); - - try { - await inviteApi.redeemInviteCode(trimmed); - setSuccess(true); - setTimeout(() => onNext(), 1500); - } catch (err) { - const msg = - err && typeof err === 'object' && 'error' in err - ? String((err as { error: string }).error) - : 'Invalid or expired invite code'; - setError(msg); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
-

Have an Invite Code?

-

- Enter an invite code from a friend to unlock free credits. You can also skip this step. -

-
- - {success ? ( -
-
- - - -
-

Invite code redeemed successfully!

-
- ) : ( - <> -
- setCode(e.target.value.toUpperCase())} - onKeyDown={e => e.key === 'Enter' && handleRedeem()} - placeholder="Enter invite code" - className="w-full px-4 py-3 bg-stone-50 border border-stone-200 rounded-xl text-center font-mono text-lg tracking-widest text-stone-900 placeholder:text-stone-400 placeholder:tracking-normal placeholder:font-sans placeholder:text-sm focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:border-primary-500/50 transition-all" - disabled={isLoading} - /> - {error &&

{error}

} -
- -
- - -
- - )} -
- ); -}; - -export default InviteCodeStep; diff --git a/app/src/pages/onboarding/steps/MnemonicStep.tsx b/app/src/pages/onboarding/steps/MnemonicStep.tsx deleted file mode 100644 index 0af6f0407..000000000 --- a/app/src/pages/onboarding/steps/MnemonicStep.tsx +++ /dev/null @@ -1,337 +0,0 @@ -import { type KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; - -import { skillManager } from '../../../lib/skills/manager'; -import { useCoreState } from '../../../providers/CoreStateProvider'; -import { - deriveAesKeyFromMnemonic, - deriveEvmAddressFromMnemonic, - generateMnemonicPhrase, - MNEMONIC_GENERATE_WORD_COUNT, - validateMnemonicPhrase, -} from '../../../utils/cryptoKeys'; -import OnboardingNextButton from '../components/OnboardingNextButton'; - -const BIP39_IMPORT_LENGTHS = [12, 15, 18, 21, 24] as const; - -const IMPORT_SLOTS_INITIAL = MNEMONIC_GENERATE_WORD_COUNT; - -interface MnemonicStepProps { - onNext: () => void | Promise; - onBack?: () => void; -} - -const MnemonicStep = ({ onNext, onBack: _onBack }: MnemonicStepProps) => { - const { snapshot, setEncryptionKey } = useCoreState(); - const user = snapshot.currentUser; - const [mode, setMode] = useState<'generate' | 'import'>('generate'); - const [copied, setCopied] = useState(false); - const [confirmed, setConfirmed] = useState(false); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const mnemonic = useMemo(() => generateMnemonicPhrase(), []); - const words = useMemo(() => mnemonic.split(' '), [mnemonic]); - - const [importWords, setImportWords] = useState(Array(IMPORT_SLOTS_INITIAL).fill('')); - const [importValid, setImportValid] = useState(null); - const inputRefs = useRef<(HTMLInputElement | null)[]>([]); - - useEffect(() => { - if (copied) { - const timer = setTimeout(() => setCopied(false), 3000); - return () => clearTimeout(timer); - } - }, [copied]); - - useEffect(() => { - setConfirmed(false); - setError(null); - setImportValid(null); - setImportWords(Array(IMPORT_SLOTS_INITIAL).fill('')); - }, [mode]); - - const handleCopy = useCallback(async () => { - try { - await navigator.clipboard.writeText(mnemonic); - setCopied(true); - } catch { - const textarea = document.createElement('textarea'); - textarea.value = mnemonic; - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - document.body.appendChild(textarea); - textarea.select(); - document.execCommand('copy'); - document.body.removeChild(textarea); - setCopied(true); - } - }, [mnemonic]); - - const handleImportWordChange = useCallback( - (index: number, value: string) => { - const pastedWords = value.trim().split(/\s+/).filter(Boolean); - if (pastedWords.length > 1) { - const fullPhraseLen = pastedWords.length; - if (BIP39_IMPORT_LENGTHS.includes(fullPhraseLen as (typeof BIP39_IMPORT_LENGTHS)[number])) { - setImportWords(pastedWords.map(w => w.toLowerCase())); - setImportValid(null); - inputRefs.current[fullPhraseLen - 1]?.focus(); - return; - } - const newWords = [...importWords]; - const slotCount = newWords.length; - for (let i = 0; i < Math.min(pastedWords.length, slotCount - index); i++) { - newWords[index + i] = pastedWords[i].toLowerCase(); - } - setImportWords(newWords); - setImportValid(null); - const nextEmpty = newWords.findIndex(w => !w); - const focusIndex = nextEmpty === -1 ? slotCount - 1 : nextEmpty; - inputRefs.current[focusIndex]?.focus(); - return; - } - - const newWords = [...importWords]; - newWords[index] = value.toLowerCase().trim(); - setImportWords(newWords); - setImportValid(null); - - if (value.trim() && index < newWords.length - 1) { - inputRefs.current[index + 1]?.focus(); - } - }, - [importWords] - ); - - const handleImportKeyDown = useCallback( - (index: number, e: KeyboardEvent) => { - if (e.key === 'Backspace' && !importWords[index] && index > 0) { - inputRefs.current[index - 1]?.focus(); - } - }, - [importWords] - ); - - const handleValidateImport = useCallback(() => { - const phrase = importWords.join(' ').trim(); - const filledWords = importWords.filter(w => w.trim()); - const n = filledWords.length; - - if (!BIP39_IMPORT_LENGTHS.includes(n as (typeof BIP39_IMPORT_LENGTHS)[number])) { - setError(`Recovery phrase must be ${BIP39_IMPORT_LENGTHS.join(', ')} words (you have ${n}).`); - setImportValid(false); - return false; - } - - const isValid = validateMnemonicPhrase(phrase); - setImportValid(isValid); - - if (!isValid) { - setError('Invalid recovery phrase. Please check your words and try again.'); - return false; - } - - setError(null); - return true; - }, [importWords]); - - const handleContinue = async () => { - setError(null); - setLoading(true); - - try { - let phraseToUse: string; - - if (mode === 'import') { - if (!handleValidateImport()) { - setLoading(false); - return; - } - phraseToUse = importWords.join(' ').trim(); - } else { - if (!confirmed) { - setLoading(false); - return; - } - phraseToUse = mnemonic; - } - - const aesKey = deriveAesKeyFromMnemonic(phraseToUse); - const walletAddress = deriveEvmAddressFromMnemonic(phraseToUse); - - if (!user?._id) { - setError('User not loaded. Please sign in again or refresh the page.'); - return; - } - await setEncryptionKey(aesKey); - await skillManager.setWalletAddress(walletAddress); - await onNext(); - } catch (e) { - setError(e instanceof Error ? e.message : 'Something went wrong. Please try again.'); - } finally { - setLoading(false); - } - }; - - const importWordCount = importWords.filter(w => w.trim()).length; - const isImportComplete = - importWords.every(w => w.trim()) && - BIP39_IMPORT_LENGTHS.includes(importWordCount as (typeof BIP39_IMPORT_LENGTHS)[number]); - const canContinue = mode === 'generate' ? confirmed : isImportComplete; - - return ( -
- {mode === 'generate' ? ( - <> -
-

Lastly, your Recovery Phrase

-

- Write down these {MNEMONIC_GENERATE_WORD_COUNT} words in order and store them - somewhere safe. This phrase encrypts your data and can never be recovered if lost. You - can always back up later. -

-
- -
-
- {words.map((word, index) => ( -
- - {index + 1}. - - {word} -
- ))} -
-
- - - - - - - - ) : ( - <> -
-

Import Recovery Phrase

-

- Enter your recovery phrase below, or paste the full phrase into any field (12 words - for new backups; 24-word phrases from older versions still work). -

-
- -
-
- {importWords.map((word, index) => ( -
- - {index + 1}. - - { - inputRefs.current[index] = el; - }} - type="text" - value={word} - onChange={e => handleImportWordChange(index, e.target.value)} - onKeyDown={e => handleImportKeyDown(index, e)} - autoComplete="off" - spellCheck={false} - className={`w-full font-mono text-sm font-medium px-2 py-1.5 rounded-lg border bg-white outline-none transition-colors text-stone-900 ${ - importValid === false && word.trim() - ? 'border-coral-400 focus:border-coral-300' - : importValid === true - ? 'border-sage-400 focus:border-sage-300' - : 'border-stone-300 focus:border-primary-400' - }`} - /> -
- ))} -
-
- - {importValid === true && ( -
- - - - Valid recovery phrase -
- )} - - - - )} - - {error &&

{error}

} - - -
- ); -}; - -export default MnemonicStep; diff --git a/app/src/pages/onboarding/steps/ToolsStep.tsx b/app/src/pages/onboarding/steps/ToolsStep.tsx deleted file mode 100644 index 36b999094..000000000 --- a/app/src/pages/onboarding/steps/ToolsStep.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useState } from 'react'; - -import { - getDefaultEnabledTools, - getToolsByCategory, - TOOL_CATEGORIES, - type ToolCategory, -} from '../../../utils/toolDefinitions'; -import OnboardingNextButton from '../components/OnboardingNextButton'; - -interface ToolsStepProps { - onNext: (enabledTools: string[]) => void; - onBack?: () => void; -} - -const CATEGORY_DESCRIPTIONS: Record = { - System: 'Shell access and version control', - Files: 'Read and write files on disk', - Vision: 'Screen capture and image analysis', - Web: 'Browser, HTTP, and web search', - Memory: 'Persistent recall for the AI', - Automation: 'Cron jobs and scheduled tasks', -}; - -const ToolsStep = ({ onNext, onBack: _onBack }: ToolsStepProps) => { - const toolsByCategory = getToolsByCategory(); - const [enabled, setEnabled] = useState>(() => { - const defaults: Record = {}; - const defaultEnabled = getDefaultEnabledTools(); - for (const cat of TOOL_CATEGORIES) { - for (const tool of toolsByCategory[cat]) { - defaults[tool.id] = defaultEnabled.includes(tool.id); - } - } - return defaults; - }); - - const toggle = (toolId: string) => { - setEnabled(prev => ({ ...prev, [toolId]: !prev[toolId] })); - }; - - const enabledList = Object.entries(enabled) - .filter(([, v]) => v) - .map(([k]) => k); - - return ( -
-
-

Enable Tools

-

- Choose which capabilities OpenHuman can use on your behalf. -

-
- -
- {TOOL_CATEGORIES.map(category => { - const tools = toolsByCategory[category]; - if (tools.length === 0) return null; - return ( -
-
-

- {category} -

-

{CATEGORY_DESCRIPTIONS[category]}

-
-
- {tools.map(tool => ( - - ))} -
-
- ); - })} -
- - onNext(enabledList)} /> -
- ); -}; - -export default ToolsStep; diff --git a/app/src/services/__tests__/agentToolRegistry.test.ts b/app/src/services/__tests__/agentToolRegistry.test.ts deleted file mode 100644 index 0a5f2dc00..000000000 --- a/app/src/services/__tests__/agentToolRegistry.test.ts +++ /dev/null @@ -1,363 +0,0 @@ -import { invoke } from '@tauri-apps/api/core'; -import { beforeEach, describe, expect, type Mock, test, vi } from 'vitest'; - -import { AgentToolRegistry } from '../agentToolRegistry'; - -// Mock Tauri invoke -vi.mock('@tauri-apps/api/core'); - -describe('AgentToolRegistry', () => { - let service: AgentToolRegistry; - const mockInvoke = invoke as Mock; - - beforeEach(() => { - service = AgentToolRegistry.getInstance(); - vi.clearAllMocks(); - service.clearCache(); // Clear cache between tests - }); - - describe('loadToolSchemas', () => { - test('should load tool schemas from Tauri using OpenClaw format', async () => { - const mockSchemas = [ - { - type: 'function', - function: { - name: 'github_list_issues', - description: 'List GitHub issues for a repository', - parameters: { - type: 'object', - properties: { - owner: { type: 'string', description: 'Repository owner' }, - repo: { type: 'string', description: 'Repository name' }, - }, - required: ['owner', 'repo'], - }, - }, - }, - { - type: 'function', - function: { - name: 'notion_create_page', - description: 'Create a new Notion page', - parameters: { - type: 'object', - properties: { - title: { type: 'string', description: 'Page title' }, - content: { type: 'string', description: 'Page content' }, - }, - required: ['title'], - }, - }, - }, - ]; - - mockInvoke.mockResolvedValue(mockSchemas); - - const schemas = await service.loadToolSchemas(); - - expect(schemas).toHaveLength(2); - expect(schemas[0].function.name).toBe('github_list_issues'); - expect(schemas[1].function.name).toBe('notion_create_page'); - expect(mockInvoke).toHaveBeenCalledWith('runtime_get_tool_schemas'); - }); - - test('should cache tool schemas to avoid repeated calls', async () => { - const mockSchemas = [ - { - type: 'function', - function: { - name: 'test_tool', - description: 'Test tool', - parameters: { type: 'object', properties: {} }, - }, - }, - ]; - - mockInvoke.mockResolvedValue(mockSchemas); - - // First call - const schemas1 = await service.loadToolSchemas(); - // Second call - const schemas2 = await service.loadToolSchemas(); - - expect(schemas1).toEqual(schemas2); - // Should only invoke Tauri once due to caching (TTL = 5 minutes) - expect(mockInvoke).toHaveBeenCalledTimes(1); - }); - - test('should force reload when requested', async () => { - const mockSchemas = [ - { - type: 'function', - function: { - name: 'test_tool', - description: 'Test tool', - parameters: { type: 'object', properties: {} }, - }, - }, - ]; - - mockInvoke.mockResolvedValue(mockSchemas); - - // First call - await service.loadToolSchemas(); - // Force reload - await service.loadToolSchemas(true); - - // Should invoke Tauri twice - expect(mockInvoke).toHaveBeenCalledTimes(2); - }); - - test('should handle empty tool schema response', async () => { - mockInvoke.mockResolvedValue([]); - - const schemas = await service.loadToolSchemas(); - - expect(schemas).toHaveLength(0); - expect(mockInvoke).toHaveBeenCalledWith('runtime_get_tool_schemas'); - }); - - test('should return empty schemas when skill runtime invoke fails', async () => { - const errorMessage = 'Failed to load tool schemas'; - mockInvoke.mockRejectedValue(new Error(errorMessage)); - - const schemas = await service.loadToolSchemas(); - - expect(schemas).toHaveLength(0); - }); - }); - - describe('executeTool', () => { - test('should execute tool using OpenClaw format with success', async () => { - const mockResult = { - success: true, - output: '{"issues": [{"title": "Bug fix", "number": 1}]}', - error: null, - execution_time: 1500, - }; - - mockInvoke.mockResolvedValue(mockResult); - - const result = await service.executeTool( - 'github', - 'list_issues', - '{"owner":"user","repo":"test"}' - ); - - expect(result.status).toBe('success'); - expect(result.result).toBe(mockResult.output); - expect(result.executionTimeMs).toBe(1500); - expect(result.toolName).toBe('list_issues'); - expect(result.skillId).toBe('github'); - - // Verify correct tool_id format and arguments - expect(mockInvoke).toHaveBeenCalledWith('runtime_execute_tool', { - toolId: 'github_list_issues', - args: '{"owner":"user","repo":"test"}', - }); - }); - - test('should handle tool execution failure', async () => { - const mockResult = { - success: false, - output: '', - error: 'Tool not found: invalid_tool', - execution_time: 100, - }; - - mockInvoke.mockResolvedValue(mockResult); - - const result = await service.executeTool('invalid', 'tool', '{}'); - - expect(result.status).toBe('error'); - expect(result.errorMessage).toBe('Tool not found: invalid_tool'); - expect(result.result).toBe('Tool not found: invalid_tool'); - expect(result.executionTimeMs).toBe(100); - }); - - test('should handle tool execution without execution_time', async () => { - const mockResult = { - success: true, - output: 'Success', - error: null, - // No execution_time provided - }; - - mockInvoke.mockResolvedValue(mockResult); - - const startTime = Date.now(); - const result = await service.executeTool('test', 'tool', '{}'); - const endTime = Date.now(); - - expect(result.status).toBe('success'); - expect(result.executionTimeMs).toBeGreaterThanOrEqual(0); - expect(result.executionTimeMs).toBeLessThanOrEqual(endTime - startTime + 10); // Allow small margin - }); - - test('should handle Tauri invoke exception', async () => { - const errorMessage = 'Network error'; - mockInvoke.mockRejectedValue(new Error(errorMessage)); - - const result = await service.executeTool('test', 'tool', '{}'); - - expect(result.status).toBe('error'); - expect(result.errorMessage).toBe(errorMessage); - expect(result.result).toBe(errorMessage); - expect(result.executionTimeMs).toBeGreaterThanOrEqual(0); - }); - - test('should generate unique execution IDs', async () => { - const mockResult = { success: true, output: 'test', error: null, execution_time: 100 }; - - mockInvoke.mockResolvedValue(mockResult); - - const result1 = await service.executeTool('test', 'tool1', '{}'); - const result2 = await service.executeTool('test', 'tool2', '{}'); - - expect(result1.id).not.toBe(result2.id); - expect(result1.id).toMatch(/^exec_\d+_[a-z0-9]+$/); - expect(result2.id).toMatch(/^exec_\d+_[a-z0-9]+$/); - }); - }); - - describe('tool management methods', () => { - beforeEach(async () => { - const mockSchemas = [ - { - type: 'function', - function: { - name: 'github_list_issues', - description: 'List GitHub issues', - parameters: { type: 'object', properties: {} }, - }, - }, - { - type: 'function', - function: { - name: 'github_create_issue', - description: 'Create GitHub issue', - parameters: { type: 'object', properties: {} }, - }, - }, - { - type: 'function', - function: { - name: 'notion_create_page', - description: 'Create Notion page', - parameters: { type: 'object', properties: {} }, - }, - }, - ]; - - mockInvoke.mockResolvedValue(mockSchemas); - await service.loadToolSchemas(); - }); - - test('getToolByName should find tool by name', () => { - const tool = service.getToolByName('github_list_issues'); - - expect(tool).toBeDefined(); - expect(tool?.function.name).toBe('github_list_issues'); - expect(tool?.function.description).toBe('List GitHub issues'); - }); - - test('getToolByName should return undefined for non-existent tool', () => { - const tool = service.getToolByName('non_existent_tool'); - - expect(tool).toBeUndefined(); - }); - - test('getAllTools should return all loaded tools', () => { - const tools = service.getAllTools(); - - expect(tools).toHaveLength(3); - expect(tools.map(t => t.function.name)).toEqual([ - 'github_list_issues', - 'github_create_issue', - 'notion_create_page', - ]); - }); - - test('getToolsBySkill should organize tools by skill ID', () => { - const toolsBySkill = service.getToolsBySkill(); - - // Skill ID is the substring before the last underscore in tool function names - expect(toolsBySkill).toHaveProperty('github_list'); - expect(toolsBySkill).toHaveProperty('github_create'); - expect(toolsBySkill).toHaveProperty('notion_create'); - expect(toolsBySkill.github_list).toHaveLength(1); - expect(toolsBySkill.github_create).toHaveLength(1); - expect(toolsBySkill.notion_create).toHaveLength(1); - - expect(toolsBySkill.github_list[0].function.name).toBe('github_list_issues'); - expect(toolsBySkill.github_create[0].function.name).toBe('github_create_issue'); - expect(toolsBySkill.notion_create[0].function.name).toBe('notion_create_page'); - }); - - test('getToolStats should return accurate statistics', () => { - const stats = service.getToolStats(); - - expect(stats.totalTools).toBe(3); - expect(stats.skillCount).toBe(3); - expect(stats.categories).toHaveProperty('GitHub', 2); - expect(stats.categories).toHaveProperty('Notion', 1); - }); - }); - - describe('helper methods', () => { - test('extractSkillIdFromToolName should parse skill ID correctly', () => { - // Use reflection to access private method - const extractMethod = (service as any).extractSkillIdFromToolName.bind(service); - - // Uses lastIndexOf('_'): skill id is everything before the final underscore - expect(extractMethod('github_list_issues')).toBe('github_list'); - expect(extractMethod('notion_create_page')).toBe('notion_create'); - expect(extractMethod('complex_skill_name_tool_name')).toBe('complex_skill_name_tool'); - expect(extractMethod('invalid_format')).toBe('invalid'); - expect(extractMethod('simpletool')).toBeNull(); - }); - - test('extractCategoryFromSkillId should categorize skills correctly', () => { - // Use reflection to access private method - const extractMethod = (service as any).extractCategoryFromSkillId.bind(service); - - expect(extractMethod('github')).toBe('GitHub'); - expect(extractMethod('github_enterprise')).toBe('GitHub'); - expect(extractMethod('notion')).toBe('Notion'); - expect(extractMethod('telegram')).toBe('Telegram'); - expect(extractMethod('gmail')).toBe('Email'); - expect(extractMethod('calendar')).toBe('Calendar'); - expect(extractMethod('slack')).toBe('Slack'); - expect(extractMethod('crypto_wallet')).toBe('Crypto'); - expect(extractMethod('unknown_skill')).toBe('Other'); - }); - }); - - describe('clearCache', () => { - test('should clear cached tool schemas', async () => { - const mockSchemas = [ - { - type: 'function', - function: { - name: 'test_tool', - description: 'Test tool', - parameters: { type: 'object', properties: {} }, - }, - }, - ]; - - mockInvoke.mockResolvedValue(mockSchemas); - - // Load schemas - await service.loadToolSchemas(); - expect(mockInvoke).toHaveBeenCalledTimes(1); - - // Clear cache - service.clearCache(); - - // Load again - should call Tauri again - await service.loadToolSchemas(); - expect(mockInvoke).toHaveBeenCalledTimes(2); - }); - }); -}); diff --git a/app/src/services/agentToolRegistry.ts b/app/src/services/agentToolRegistry.ts deleted file mode 100644 index 4febb2e47..000000000 --- a/app/src/services/agentToolRegistry.ts +++ /dev/null @@ -1,297 +0,0 @@ -/** - * Agent Tool Registry Service - * - * Unified tool discovery and execution using the consolidated systems: - * - telegram_get_tools: Get Telegram tools in OpenAI-compatible format - * - telegram_execute_tool: Execute Telegram tools with enhanced validation - * - Fallback to skill system for non-Telegram tools (temporary) - */ -import { invoke } from '@tauri-apps/api/core'; - -import type { AgentToolExecution, AgentToolSchema, IAgentToolRegistry } from '../types/agent'; - -// OpenClaw format types from Rust -interface OpenClawToolSchema { - type: string; - function: { name: string; description: string; parameters: Record }; -} - -interface OpenClawToolResult { - success: boolean; - output: string; - error?: string; - execution_time?: number; -} - -export class AgentToolRegistry implements IAgentToolRegistry { - private static instance: AgentToolRegistry; - private toolSchemas: AgentToolSchema[] = []; - private lastLoadTime = 0; - private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes - - static getInstance(): AgentToolRegistry { - if (!this.instance) { - this.instance = new AgentToolRegistry(); - } - return this.instance; - } - - /** - * Load tool schemas from unified systems (Telegram + skill system fallback) - */ - async loadToolSchemas(forceReload = false): Promise { - const now = Date.now(); - - // Return cached tools if still fresh - if (!forceReload && this.toolSchemas.length > 0 && now - this.lastLoadTime < this.CACHE_TTL) { - return this.toolSchemas; - } - - try { - console.log('🔧 Loading tool schemas from unified systems...'); - - const allTools: AgentToolSchema[] = []; - - // Note: Telegram tools removed - no longer available - console.log('🔧 Telegram tools not available (unified system removed)'); - - // 2. Load other tools from skill system (fallback for non-Telegram) - try { - console.log('🔧 Loading non-Telegram tools from skill system...'); - const skillTools = await invoke('runtime_get_tool_schemas'); - - // Filter out telegram tools to avoid duplicates - const nonTelegramTools = skillTools.filter( - tool => - !tool.function.name.includes('telegram') && - !tool.function.name.includes('tg') && - !this.extractCategoryFromSkillId( - this.extractSkillIdFromToolName(tool.function.name) || '' - ).includes('Telegram') - ); - - const skillSchemas = nonTelegramTools.map(tool => ({ - type: 'function' as const, - function: { - name: tool.function.name, - description: tool.function.description, - parameters: tool.function.parameters as AgentToolSchema['function']['parameters'], - }, - })); - - allTools.push(...skillSchemas); - console.log(`✅ Loaded ${skillSchemas.length} non-Telegram tools from skill system`); - } catch (error) { - console.warn('⚠️ Failed to load tools from skill system:', error); - } - - this.toolSchemas = allTools; - this.lastLoadTime = now; - - console.log(`✅ Tool registry updated: ${this.toolSchemas.length} total tools available`); - - return this.toolSchemas; - } catch (error) { - console.error('❌ Failed to load tool schemas:', error); - throw new Error(`Failed to load tool schemas: ${error}`); - } - } - - /** - * Execute a tool using unified systems (Telegram unified or skill system fallback) - */ - async executeTool( - skillId: string, - toolName: string, - toolArguments: string - ): Promise { - const startTime = Date.now(); - const executionId = `exec_${startTime}_${Math.random().toString(36).substr(2, 9)}`; - - const execution: AgentToolExecution = { - id: executionId, - toolName, - skillId, - arguments: toolArguments, - status: 'running', - startTime, - }; - - console.log(`🚀 [TOOL EXECUTION START] Executing tool: ${toolName} (skillId: ${skillId})`); - console.log(`📝 [ARGUMENTS] Raw arguments:`, { - arguments: toolArguments, - type: typeof toolArguments, - length: toolArguments?.length, - isString: typeof toolArguments === 'string', - parsed: (() => { - try { - return typeof toolArguments === 'string' ? JSON.parse(toolArguments) : toolArguments; - } catch (e) { - return 'Failed to parse: ' + e; - } - })(), - }); - - try { - // Determine if this is a Telegram tool - const isTelegramTool = - skillId.includes('telegram') || - skillId.includes('tg') || - toolName.includes('telegram') || - toolName.includes('tg') || - this.extractCategoryFromSkillId(skillId).includes('Telegram'); - - let result: OpenClawToolResult; - - if (isTelegramTool) { - // Telegram tools no longer available - console.log(`🔧 [TELEGRAM TOOL] Tool "${toolName}" not available (unified system removed)`); - result = { - success: false, - output: '', - error: 'Telegram tools are no longer available (unified system removed)', - }; - } else { - // Use skill system for non-Telegram tools - const toolId = `${skillId}_${toolName}`; - console.log(`🔧 [BEFORE INVOKE] Calling runtime_execute_tool with:`); - console.log(` toolId: "${toolId}"`); - console.log(` args: ${toolArguments}`); - console.log(` args type: ${typeof toolArguments}`); - - result = await invoke('runtime_execute_tool', { - toolId, - args: toolArguments, - }); - } - - console.log(`🔧 [AFTER INVOKE] Tool execution result:`, result); - - execution.endTime = Date.now(); - // Use execution time from Rust if available, otherwise calculate locally - execution.executionTimeMs = result.execution_time || execution.endTime - execution.startTime; - - if (!result.success) { - execution.status = 'error'; - execution.errorMessage = result.error || 'Unknown error occurred'; - execution.result = execution.errorMessage; - - console.log(`❌ Tool execution failed: ${toolName} (${execution.executionTimeMs}ms)`); - console.log(`❌ Error:`, execution.errorMessage); - } else { - execution.status = 'success'; - execution.result = result.output; - - console.log(`✅ Tool execution completed: ${toolName} (${execution.executionTimeMs}ms)`); - } - - return execution; - } catch (error) { - execution.endTime = Date.now(); - execution.executionTimeMs = execution.endTime - execution.startTime; - execution.status = 'error'; - execution.errorMessage = error instanceof Error ? error.message : String(error); - execution.result = execution.errorMessage; - - console.error(`❌ Tool execution error: ${toolName}`, error); - - return execution; - } - } - - /** - * Get a specific tool by name - */ - getToolByName(toolName: string): AgentToolSchema | undefined { - return this.toolSchemas.find(tool => tool.function.name === toolName); - } - - /** - * Get all available tools - */ - getAllTools(): AgentToolSchema[] { - return [...this.toolSchemas]; - } - - /** - * Get tools organized by skill - */ - getToolsBySkill(): Record { - const toolsBySkill: Record = {}; - - for (const tool of this.toolSchemas) { - // Extract skill ID from tool name (format: skillId_toolName) - const skillId = this.extractSkillIdFromToolName(tool.function.name) || 'unknown'; - - if (!toolsBySkill[skillId]) { - toolsBySkill[skillId] = []; - } - toolsBySkill[skillId].push(tool); - } - - return toolsBySkill; - } - - /** - * Get tool execution statistics - */ - getToolStats(): { totalTools: number; skillCount: number; categories: Record } { - const categories: Record = {}; - const skills = new Set(); - - for (const tool of this.toolSchemas) { - const skillId = this.extractSkillIdFromToolName(tool.function.name) || 'unknown'; - skills.add(skillId); - - // Categorize by skill name - const category = this.extractCategoryFromSkillId(skillId); - categories[category] = (categories[category] || 0) + 1; - } - - return { totalTools: this.toolSchemas.length, skillCount: skills.size, categories }; - } - - /** - * Clear the tool registry cache - */ - clearCache(): void { - this.toolSchemas = []; - this.lastLoadTime = 0; - console.log('🔧 Tool registry cache cleared'); - } - - // ============================================================================= - // Private Helper Methods - // ============================================================================= - - /** - * Extract skill ID from tool name (format: skillId_toolName) - */ - private extractSkillIdFromToolName(toolName: string): string | null { - const underscoreIndex = toolName.lastIndexOf('_'); - if (underscoreIndex === -1) { - return null; - } - return toolName.substring(0, underscoreIndex); - } - - /** - * Extract category name from skill ID for organization - */ - private extractCategoryFromSkillId(skillId: string): string { - // Common skill naming patterns - if (skillId.includes('github') || skillId.includes('git')) return 'GitHub'; - if (skillId.includes('notion')) return 'Notion'; - if (skillId.includes('telegram') || skillId.includes('tg')) return 'Telegram'; - if (skillId.includes('email') || skillId.includes('gmail')) return 'Email'; - if (skillId.includes('calendar')) return 'Calendar'; - if (skillId.includes('slack')) return 'Slack'; - if (skillId.includes('discord')) return 'Discord'; - if (skillId.includes('twitter') || skillId.includes('x')) return 'Social'; - if (skillId.includes('file') || skillId.includes('fs')) return 'File System'; - if (skillId.includes('crypto') || skillId.includes('blockchain')) return 'Crypto'; - if (skillId.includes('ai') || skillId.includes('ml')) return 'AI/ML'; - - return 'Other'; - } -} diff --git a/app/src/services/api/__tests__/managedDmApi.test.ts b/app/src/services/api/__tests__/managedDmApi.test.ts deleted file mode 100644 index f76295bae..000000000 --- a/app/src/services/api/__tests__/managedDmApi.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { managedDmApi } from '../managedDmApi'; - -vi.mock('../../apiClient', () => ({ apiClient: { post: vi.fn(), get: vi.fn() } })); - -const apiClient = vi.mocked((await import('../../apiClient')).apiClient); - -describe('managedDmApi', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('initiates managed dm through the backend api', async () => { - apiClient.post.mockResolvedValueOnce({ - data: { - token: 'dm-token', - deepLink: 'https://t.me/openhuman_bot?start=manageddm_dm-token', - expiresAt: '2026-04-04T12:00:00.000Z', - }, - }); - - await expect(managedDmApi.initiateManagedDm()).resolves.toEqual({ - token: 'dm-token', - deepLink: 'https://t.me/openhuman_bot?start=manageddm_dm-token', - expiresAt: '2026-04-04T12:00:00.000Z', - }); - }); - - it('polls until verified and returns the verified status', async () => { - apiClient.get - .mockResolvedValueOnce({ data: { verified: false, telegramUsername: null } }) - .mockResolvedValueOnce({ data: { verified: true, telegramUsername: 'telegram-user' } }); - - await expect( - managedDmApi.pollManagedDmStatusUntilVerified('dm-token', { intervalMs: 0, timeoutMs: 100 }) - ).resolves.toEqual({ verified: true, telegramUsername: 'telegram-user' }); - expect(apiClient.get).toHaveBeenCalledTimes(2); - }); -}); diff --git a/app/src/services/api/actionableItemsApi.ts b/app/src/services/api/actionableItemsApi.ts deleted file mode 100644 index 06564dfd9..000000000 --- a/app/src/services/api/actionableItemsApi.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { ApiResponse } from '../../types/api'; -import { apiClient } from '../apiClient'; - -interface ActionableItem { - id: string; - title: string; - description?: string; - status: 'pending' | 'dismissed' | 'snoozed' | 'completed'; - createdAt: string; - updatedAt: string; - // Add other fields based on backend schema -} - -interface ExecutionSession { - id: string; - itemId: string; - status: 'running' | 'pending' | 'completed' | 'failed'; - // Add other fields based on backend schema -} - -interface ThreadData { - threadId: string; - conversationId: string; -} - -/** - * Actionable Items API endpoints - */ -export const actionableItemsApi = { - /** - * List actionable items for the authenticated user - * GET /telegram/actionable-items - */ - getActionableItems: async (): Promise => { - const response = await apiClient.get>( - '/telegram/actionable-items' - ); - return response.data; - }, - - /** - * Update an actionable item (dismiss, snooze, etc.) - * PATCH /telegram/actionable-items/:itemId - */ - updateActionableItem: async ( - itemId: string, - updates: { status?: ActionableItem['status']; snoozeUntil?: string } - ): Promise => { - const response = await apiClient.patch>( - `/telegram/actionable-items/${itemId}`, - updates - ); - return response.data; - }, - - /** - * Get or create conversation thread for an actionable item - * GET /telegram/actionable-items/:itemId/thread - */ - getItemThread: async (itemId: string): Promise => { - const response = await apiClient.get>( - `/telegram/actionable-items/${itemId}/thread` - ); - return response.data; - }, - - /** - * Get current execution session for an actionable item - * GET /telegram/actionable-items/:itemId/session - */ - getItemSession: async (itemId: string): Promise => { - const response = await apiClient.get>( - `/telegram/actionable-items/${itemId}/session` - ); - return response.data; - }, - - /** - * Start execution of an actionable item - * POST /telegram/actionable-items/:itemId/execute - */ - executeItem: async (itemId: string): Promise => { - const response = await apiClient.post>( - `/telegram/actionable-items/${itemId}/execute` - ); - return response.data; - }, - - /** - * Get execution session status - * GET /telegram/execution-sessions/:sessionId - */ - getExecutionSession: async (sessionId: string): Promise => { - const response = await apiClient.get>( - `/telegram/execution-sessions/${sessionId}` - ); - return response.data; - }, - - /** - * Confirm or reject pending execution step - * POST /telegram/execution-sessions/:sessionId/confirm - */ - confirmExecutionStep: async ( - sessionId: string, - action: 'confirm' | 'reject', - data?: unknown - ): Promise => { - const response = await apiClient.post>( - `/telegram/execution-sessions/${sessionId}/confirm`, - { action, data } - ); - return response.data; - }, -}; diff --git a/app/src/services/api/apiKeysApi.ts b/app/src/services/api/apiKeysApi.ts deleted file mode 100644 index 82c70a6ae..000000000 --- a/app/src/services/api/apiKeysApi.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { ApiResponse } from '../../types/api'; -import { apiClient } from '../apiClient'; - -interface ApiKey { - id: string; - name: string; - keyPreview: string; // First few characters of the key - createdAt: string; - lastUsedAt?: string; - // Add other fields based on backend schema -} - -interface CreateApiKeyData { - name: string; -} - -interface CreateApiKeyResponse { - id: string; - name: string; - key: string; // Full key only returned on creation - createdAt: string; -} - -/** - * API Keys management endpoints - */ -export const apiKeysApi = { - /** - * Create API key - * POST /api-keys - */ - createApiKey: async (data: CreateApiKeyData): Promise => { - const response = await apiClient.post>('/api-keys', data); - return response.data; - }, - - /** - * List API keys - * GET /api-keys - */ - getApiKeys: async (): Promise => { - const response = await apiClient.get>('/api-keys'); - return response.data; - }, - - /** - * Revoke API key - * DELETE /api-keys/:keyId - */ - revokeApiKey: async (keyId: string): Promise => { - await apiClient.delete>(`/api-keys/${keyId}`); - }, -}; diff --git a/app/src/services/api/authApi.ts b/app/src/services/api/authApi.ts index d28cb6d12..b79c84335 100644 --- a/app/src/services/api/authApi.ts +++ b/app/src/services/api/authApi.ts @@ -1,54 +1,5 @@ -import { base64ToBytes, encryptIntegrationTokens } from '../../utils/integrationTokensCrypto'; -import { callCoreCommand } from '../coreCommandClient'; import { callCoreRpc } from '../coreRpcClient'; -interface IntegrationTokensResponse { - success: boolean; - data?: { encrypted: string }; -} - -interface IntegrationTokensPayload { - accessToken: string; - refreshToken?: string; - expiresAt: string; -} - -type LinkableChannel = 'telegram' | 'discord'; - -interface RawChannelLinkTokenData { - token?: string; - linkToken?: string; - jwtToken?: string; - url?: string; - linkUrl?: string; - authUrl?: string; - deepLinkUrl?: string; - expiresAt?: string; - expires_at?: string; - [key: string]: unknown; -} - -export interface ChannelLinkTokenResult { - token: string; - launchUrl?: string; - expiresAt?: string; -} - -function bytesToHex(bytes: Uint8Array): string { - return Array.from(bytes) - .map(byte => byte.toString(16).padStart(2, '0')) - .join(''); -} - -function normalizeKeyToHex(rawKey: string): string { - const trimmed = rawKey.trim(); - const maybeHex = trimmed.replace(/^0x/i, ''); - if (maybeHex.length === 64 && /^[0-9a-fA-F]+$/.test(maybeHex)) { - return maybeHex.toLowerCase(); - } - return bytesToHex(base64ToBytes(trimmed)); -} - /** * Consume a verified login token and return the JWT. * Works for both Telegram and OAuth login tokens. @@ -65,69 +16,3 @@ export async function consumeLoginToken(loginToken: string): Promise { } return jwtToken; } - -/** - * Fetch encrypted OAuth tokens for an integration using a client-provided key. - * POST /auth/integrations/:integrationId/tokens (auth required) - */ -export async function fetchIntegrationTokens( - integrationId: string, - key: string -): Promise { - const response = await callCoreRpc<{ result: IntegrationTokensPayload }>({ - method: 'openhuman.auth.oauth_fetch_integration_tokens', - params: { integrationId, key }, - }); - const tokens = response.result; - if (!tokens?.accessToken || !tokens?.expiresAt) { - throw new Error('Integration token handoff did not return required fields'); - } - - const encrypted = await encryptIntegrationTokens( - JSON.stringify({ - accessToken: tokens.accessToken, - refreshToken: tokens.refreshToken ?? '', - expiresAt: tokens.expiresAt, - }), - normalizeKeyToHex(key) - ); - return { success: true, data: { encrypted } }; -} - -/** - * Create a short-lived link token that can be handed to a messaging channel login flow. - * POST /auth/channels/:channel/link-token (auth required) - */ -export async function createChannelLinkToken( - channel: LinkableChannel -): Promise { - const data = await callCoreCommand( - 'openhuman.auth_create_channel_link_token', - { channel } - ); - const token = - typeof data?.token === 'string' - ? data.token - : typeof data?.linkToken === 'string' - ? data.linkToken - : typeof data?.jwtToken === 'string' - ? data.jwtToken - : ''; - - if (!token) { - throw new Error('Channel link token response missing token'); - } - - const launchUrlCandidates = [data?.url, data?.linkUrl, data?.authUrl, data?.deepLinkUrl]; - const launchUrl = launchUrlCandidates.find( - (value): value is string => typeof value === 'string' && value.trim().length > 0 - ); - const expiresAt = - typeof data?.expiresAt === 'string' - ? data.expiresAt - : typeof data?.expires_at === 'string' - ? data.expires_at - : undefined; - - return { token, launchUrl, expiresAt }; -} diff --git a/app/src/services/api/feedbackApi.ts b/app/src/services/api/feedbackApi.ts deleted file mode 100644 index be2ecec6d..000000000 --- a/app/src/services/api/feedbackApi.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { ApiResponse } from '../../types/api'; -import { apiClient } from '../apiClient'; - -interface FeedbackItem { - id: string; - type: 'bug' | 'feature_request' | 'general'; - title: string; - description: string; - steps?: string; - status: 'open' | 'in_progress' | 'closed'; - createdAt: string; - updatedAt: string; - // Add other fields based on backend schema -} - -interface CreateFeedbackData { - type: FeedbackItem['type']; - title: string; - description: string; - steps?: string; -} - -interface UpdateFeedbackData { - title?: string; - description?: string; - steps?: string; -} - -/** - * Feedback API endpoints - */ -export const feedbackApi = { - /** - * Submit feedback (bug, feature_request, general) - * POST /feedback - */ - createFeedback: async (feedback: CreateFeedbackData): Promise => { - const response = await apiClient.post>('/feedback', feedback); - return response.data; - }, - - /** - * List current user's feedback - * GET /feedback - */ - getFeedback: async (): Promise => { - const response = await apiClient.get>('/feedback'); - return response.data; - }, - - /** - * Get a single feedback item - * GET /feedback/:id - */ - getFeedbackById: async (id: string): Promise => { - const response = await apiClient.get>(`/feedback/${id}`); - return response.data; - }, - - /** - * Update feedback (description, steps, etc.) - * PUT /feedback/:id - */ - updateFeedback: async (id: string, updates: UpdateFeedbackData): Promise => { - const response = await apiClient.put>(`/feedback/${id}`, updates); - return response.data; - }, -}; diff --git a/app/src/services/api/inferenceApi.ts b/app/src/services/api/inferenceApi.ts deleted file mode 100644 index 386c7bbdd..000000000 --- a/app/src/services/api/inferenceApi.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { apiClient } from '../apiClient'; - -// ── Request types ──────────────────────────────────────────────────────────── - -export type ChatRole = 'system' | 'user' | 'assistant' | 'tool'; - -export interface ToolCall { - id: string; - type: 'function'; - function: { name: string; arguments: string }; -} - -export interface ChatMessage { - role: ChatRole; - content: string | null; - tool_calls?: ToolCall[]; - tool_call_id?: string; -} - -export interface ToolFunction { - name: string; - description: string; - parameters: Record; -} - -export interface Tool { - type: 'function'; - function: ToolFunction; -} - -export interface ChatCompletionRequest { - model: string; - messages: ChatMessage[]; - tools?: Tool[]; - tool_choice?: 'auto' | 'none' | { type: 'function'; function: { name: string } }; - openhuman?: { trace_tools?: boolean }; - stream?: boolean; - temperature?: number; - max_tokens?: number; -} - -export interface TextCompletionRequest { - model: string; - prompt: string; - stream?: boolean; - temperature?: number; - max_tokens?: number; -} - -// ── Response types (OpenAI-compatible) ─────────────────────────────────────── - -export interface ModelInfo { - id: string; - object: string; - created: number; - owned_by: string; -} - -export interface ModelsListResponse { - object: string; - data: ModelInfo[]; -} - -export interface ChatCompletionChoice { - index: number; - message: ChatMessage & { tool_calls?: ToolCall[] }; - finish_reason: string | null; -} - -export interface TextCompletionChoice { - index: number; - text: string; - finish_reason: string | null; -} - -export interface CompletionUsage { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; -} - -export interface ChatCompletionResponse { - id: string; - object: string; - created: number; - model: string; - choices: ChatCompletionChoice[]; - usage: CompletionUsage; -} - -export interface TextCompletionResponse { - id: string; - object: string; - created: number; - model: string; - choices: TextCompletionChoice[]; - usage: CompletionUsage; -} - -// ── API ─────────────────────────────────────────────────────────────────────── - -export const inferenceApi = { - /** GET /openai/v1/models — list available models */ - listModels: async (): Promise => { - return apiClient.get('/openai/v1/models'); - }, - - /** POST /openai/v1/chat/completions — create a chat completion */ - createChatCompletion: async (body: ChatCompletionRequest): Promise => { - return apiClient.post('/openai/v1/chat/completions', body); - }, - - /** POST /openai/v1/completions — create a text completion */ - createCompletion: async (body: TextCompletionRequest): Promise => { - return apiClient.post('/openai/v1/completions', body); - }, -}; diff --git a/app/src/services/api/managedDmApi.ts b/app/src/services/api/managedDmApi.ts deleted file mode 100644 index 3d6e394bf..000000000 --- a/app/src/services/api/managedDmApi.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { apiClient } from '../apiClient'; - -export interface ManagedDmInitiateResponse { - token: string; - deepLink: string; - expiresAt: string; -} - -export interface ManagedDmStatusResponse { - verified: boolean; - telegramUsername?: string | null; - expiresAt?: string; -} - -interface ApiEnvelope { - success: boolean; - data: T; -} - -export interface ManagedDmPollOptions { - intervalMs?: number; - timeoutMs?: number; - signal?: AbortSignal; -} - -const DEFAULT_POLL_INTERVAL_MS = 3_000; -const DEFAULT_POLL_TIMEOUT_MS = 5 * 60 * 1_000; - -const sleep = (ms: number, signal?: AbortSignal): Promise => - new Promise(resolve => { - if (signal?.aborted) { - resolve(); - return; - } - - const timeoutId = window.setTimeout(() => { - cleanup(); - resolve(); - }, ms); - - const onAbort = () => { - cleanup(); - resolve(); - }; - - const cleanup = () => { - window.clearTimeout(timeoutId); - signal?.removeEventListener('abort', onAbort); - }; - - signal?.addEventListener('abort', onAbort, { once: true }); - }); - -export async function initiateManagedDm(): Promise { - const response = await apiClient.post>( - '/telegram/managed-dm/initiate' - ); - return response.data; -} - -export async function getManagedDmStatus(token: string): Promise { - const response = await apiClient.get>( - `/telegram/managed-dm/status/${encodeURIComponent(token)}` - ); - return response.data; -} - -export async function pollManagedDmStatusUntilVerified( - token: string, - options: ManagedDmPollOptions = {} -): Promise { - const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS; - const timeoutMs = options.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS; - const startedAt = Date.now(); - - while (Date.now() - startedAt < timeoutMs) { - if (options.signal?.aborted) { - return null; - } - - try { - const status = await getManagedDmStatus(token); - if (status.verified) { - return status; - } - } catch { - // Best-effort polling: keep trying until timeout or cancellation. - } - - await sleep(intervalMs, options.signal); - } - - return null; -} - -export const managedDmApi = { - initiateManagedDm, - getManagedDmStatus, - pollManagedDmStatusUntilVerified, -}; diff --git a/app/src/services/api/settingsApi.ts b/app/src/services/api/settingsApi.ts deleted file mode 100644 index f74a07ffe..000000000 --- a/app/src/services/api/settingsApi.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { ApiResponse } from '../../types/api'; -import { apiClient } from '../apiClient'; - -interface UserSettings { - // Add specific settings types based on backend schema - [key: string]: unknown; -} - -/** - * Settings API endpoints - */ -export const settingsApi = { - /** - * Get user settings - * GET /settings - */ - getSettings: async (): Promise => { - const response = await apiClient.get>('/settings'); - return response.data; - }, - - /** - * Update user settings - * PATCH /settings - */ - updateSettings: async (settings: Partial): Promise => { - const response = await apiClient.patch>('/settings', settings); - return response.data; - }, - - /** - * Set platforms connected - * POST /settings/platforms-connected - */ - setPlatformsConnected: async (platforms: string[]): Promise => { - await apiClient.post>('/settings/platforms-connected', { platforms }); - }, -}; diff --git a/app/src/services/daemonHealthService.ts b/app/src/services/daemonHealthService.ts index 2a2a8c671..e61fc235f 100644 --- a/app/src/services/daemonHealthService.ts +++ b/app/src/services/daemonHealthService.ts @@ -1,8 +1,7 @@ /** * Daemon Health Service * - * Manages health monitoring for the openhuman daemon by polling - * `openhuman.health_snapshot` over core RPC from the frontend. + * Polls the Rust core health snapshot and keeps the frontend daemon store in sync. */ import { type ComponentHealth, @@ -15,14 +14,10 @@ import { callCoreRpc } from './coreRpcClient'; export class DaemonHealthService { private healthTimeoutId: ReturnType | null = null; - private readonly HEALTH_TIMEOUT_MS = 30000; // 30 seconds + private readonly HEALTH_TIMEOUT_MS = 30000; private pollingIntervalId: ReturnType | null = null; private readonly POLL_MS = 2000; - /** - * Setup health event listener from the Rust daemon. - * Should be called once when the app starts in Tauri mode. - */ async setupHealthListener(): Promise<(() => void) | null> { if (this.pollingIntervalId) { return () => this.cleanup(); @@ -37,7 +32,7 @@ export class DaemonHealthService { this.startHealthTimeout(); } } catch { - // Health endpoint can fail while sidecar is starting; timeout will mark disconnected. + // The health endpoint can fail while the sidecar is starting. } }; @@ -50,9 +45,6 @@ export class DaemonHealthService { return () => this.cleanup(); } - /** - * Cleanup the health event listener. - */ cleanup(): void { if (this.pollingIntervalId) { clearInterval(this.pollingIntervalId); @@ -65,9 +57,6 @@ export class DaemonHealthService { } } - /** - * Parse the health snapshot received from Rust. - */ private parseHealthSnapshot(payload: unknown): HealthSnapshot | null { try { if (!payload || typeof payload !== 'object') { @@ -75,8 +64,6 @@ export class DaemonHealthService { } const data = payload as Record; - - // Validate required fields if ( typeof data.pid !== 'number' || typeof data.updated_at !== 'string' || @@ -87,7 +74,6 @@ export class DaemonHealthService { return null; } - // Parse components const components: Record = {}; const componentsData = data.components as Record; @@ -105,13 +91,12 @@ export class DaemonHealthService { continue; } - // Validate status is a valid ComponentStatus if (comp.status !== 'ok' && comp.status !== 'error' && comp.status !== 'starting') { continue; } components[name] = { - status: comp.status as 'ok' | 'error' | 'starting', + status: comp.status, updated_at: comp.updated_at, last_ok: typeof comp.last_ok === 'string' ? comp.last_ok : undefined, last_error: typeof comp.last_error === 'string' ? comp.last_error : undefined, @@ -120,9 +105,9 @@ export class DaemonHealthService { } return { - pid: data.pid as number, - updated_at: data.updated_at as string, - uptime_seconds: data.uptime_seconds as number, + pid: data.pid, + updated_at: data.updated_at, + uptime_seconds: data.uptime_seconds, components, }; } catch (error) { @@ -131,33 +116,20 @@ export class DaemonHealthService { } } - /** - * Update daemon state based on received health snapshot. - */ private updateDaemonStoreFromHealth(snapshot: HealthSnapshot): void { - const userId = this.getUserId(); - try { - updateHealthSnapshot(userId, snapshot); + updateHealthSnapshot(this.getUserId(), snapshot); } catch (error) { console.error('[DaemonHealth] Error updating daemon store from health:', error); } } - /** - * Start or restart the health timeout. - * If no health events are received within the timeout period, - * the daemon status will be set to 'disconnected'. - */ private startHealthTimeout(): void { - // Clear existing timeout if (this.healthTimeoutId) { clearTimeout(this.healthTimeoutId); } const userId = this.getUserId(); - - // Set new timeout this.healthTimeoutId = setTimeout(() => { console.warn('[DaemonHealth] Health timeout reached - setting status to disconnected'); setDaemonStatus(userId, 'disconnected'); @@ -165,19 +137,25 @@ export class DaemonHealthService { }, this.HEALTH_TIMEOUT_MS); } - /** - * Get the current user ID for daemon state management. - */ private getUserId(): string { const token = getCoreStateSnapshot().snapshot.sessionToken; - if (!token) return '__pending__'; + if (!token) { + return '__pending__'; + } try { const parts = token.split('.'); - if (parts.length !== 3) return '__pending__'; + if (parts.length !== 3) { + return '__pending__'; + } + const payloadBase64 = parts[1].replace(/-/g, '+').replace(/_/g, '/'); const payloadJson = atob(payloadBase64); - const payload = JSON.parse(payloadJson); + const payload = JSON.parse(payloadJson) as { + sub?: string; + tgUserId?: string; + userId?: string; + }; return payload.tgUserId || payload.userId || payload.sub || '__pending__'; } catch { return '__pending__'; @@ -185,5 +163,4 @@ export class DaemonHealthService { } } -// Export singleton instance export const daemonHealthService = new DaemonHealthService(); diff --git a/app/src/services/intelligenceApi.ts b/app/src/services/intelligenceApi.ts deleted file mode 100644 index 293ea3358..000000000 --- a/app/src/services/intelligenceApi.ts +++ /dev/null @@ -1,186 +0,0 @@ -import type { ActionableItemStatus } from '../types/intelligence'; -import { apiClient } from './apiClient'; - -/** - * Backend API response types for Intelligence system - */ -export interface BackendActionableItem { - id: string; - title: string; - description?: string; - source: string; - priority: string; - status: string; - createdAt: string; - updatedAt: string; - expiresAt?: string; - snoozeUntil?: string; - actionable: boolean; - requiresConfirmation?: boolean; - hasComplexAction?: boolean; - dismissedAt?: string; - completedAt?: string; - reminderCount?: number; - // Backend-specific fields - threadId?: string; - executionStatus?: 'idle' | 'running' | 'completed' | 'failed'; - currentSessionId?: string; -} - -export interface BackendChatMessage { - id: string; - content: string; - role: 'user' | 'assistant'; - timestamp: string; - threadId: string; -} - -export interface BackendThreadResponse { - threadId: string; - messages: BackendChatMessage[]; -} - -export interface BackendExecutionResponse { - executionId: string; - sessionId: string; - status: 'started' | 'running' | 'completed' | 'failed'; -} - -export interface ConnectedTool { - name: string; - description: string; - parameters: Record; - skillId: string; - enabled: boolean; -} - -/** - * Intelligence API Service - handles all REST API calls for the Intelligence system - */ -export class IntelligenceApiService { - /** - * Get all actionable items from the backend - */ - async getActionableItems(): Promise { - try { - const response = await apiClient.get<{ items: BackendActionableItem[] }>( - '/telegram/actionable-items' - ); - return response.items || []; - } catch (error) { - console.error('Failed to fetch actionable items:', error); - throw error; - } - } - - /** - * Update the status of an actionable item - */ - async updateItemStatus(itemId: string, status: ActionableItemStatus): Promise { - try { - await apiClient.patch(`/actionable-items/${itemId}`, { status }); - } catch (error) { - console.error('Failed to update item status:', error); - throw error; - } - } - - /** - * Snooze an actionable item until a specific time - */ - async snoozeItem(itemId: string, snoozeUntil: Date): Promise { - try { - await apiClient.patch(`/actionable-items/${itemId}`, { - status: 'snoozed', - snoozeUntil: snoozeUntil.toISOString(), - }); - } catch (error) { - console.error('Failed to snooze item:', error); - throw error; - } - } - - /** - * Get or create a conversation thread for an actionable item - */ - async getOrCreateThread(itemId: string): Promise { - try { - const response = await apiClient.get(`/${itemId}/thread`); - return response; - } catch (error) { - console.error('Failed to get or create thread:', error); - throw error; - } - } - - /** - * Get chat history for a specific thread - */ - async getChatHistory(threadId: string): Promise { - try { - const response = await apiClient.get<{ messages: BackendChatMessage[] }>( - `/threads/${threadId}/messages` - ); - return response.messages || []; - } catch (error) { - console.error('Failed to get chat history:', error); - throw error; - } - } - - /** - * Start task execution for an actionable item with connected tools - */ - async executeTask( - itemId: string, - connectedTools: ConnectedTool[] - ): Promise { - try { - const response = await apiClient.post(`/${itemId}/execute`, { - connectedTools, - }); - return response; - } catch (error) { - console.error('Failed to execute task:', error); - throw error; - } - } - - /** - * Get execution status for a specific execution ID - */ - async getExecutionStatus( - executionId: string - ): Promise<{ - status: 'running' | 'completed' | 'failed'; - progress?: Array>; - result?: unknown; - }> { - try { - const response = await apiClient.get(`/executions/${executionId}/status`); - return response as { - status: 'running' | 'completed' | 'failed'; - progress?: Array>; - result?: unknown; - }; - } catch (error) { - console.error('Failed to get execution status:', error); - throw error; - } - } - - /** - * Cancel a running execution - */ - async cancelExecution(executionId: string): Promise { - try { - await apiClient.post(`/executions/${executionId}/cancel`); - } catch (error) { - console.error('Failed to cancel execution:', error); - throw error; - } - } -} - -// Export singleton instance -export const intelligenceApi = new IntelligenceApiService(); diff --git a/app/src/types/agent.ts b/app/src/types/agent.ts deleted file mode 100644 index 673500dec..000000000 --- a/app/src/types/agent.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Agent Tool Registry Types - * - * Minimal type definitions for agent tool registry functionality. - * Based on OpenClaw compatibility requirements. - */ - -/** - * Tool schema for AI provider registration (OpenAI function calling format) - */ -export interface AgentToolSchema { - type: 'function'; - function: { - name: string; - description: string; - parameters: { - type: 'object'; - properties: Record; - required?: string[]; - additionalProperties?: boolean; - }; - }; -} - -/** - * Tool execution record - */ -export interface AgentToolExecution { - id: string; - toolName: string; - skillId: string; - arguments: string; // JSON string - status: 'running' | 'success' | 'error'; - startTime: number; - endTime?: number; - executionTimeMs?: number; - result?: string; - errorMessage?: string; -} - -/** - * Agent tool registry service interface - */ -export interface IAgentToolRegistry { - /** - * Load tool schemas from the skill system - */ - loadToolSchemas(forceReload?: boolean): Promise; - - /** - * Execute a tool using the skill system - */ - executeTool( - skillId: string, - toolName: string, - toolArguments: string - ): Promise; - - /** - * Get a specific tool by name - */ - getToolByName(toolName: string): AgentToolSchema | undefined; - - /** - * Get all available tools - */ - getAllTools(): AgentToolSchema[]; -} diff --git a/app/src/types/intelligence-chat-api.ts b/app/src/types/intelligence-chat-api.ts deleted file mode 100644 index b00ec90dd..000000000 --- a/app/src/types/intelligence-chat-api.ts +++ /dev/null @@ -1,387 +0,0 @@ -/** - * Intelligence Chat API Types - * - * TypeScript definitions for backend integration - * These types ensure consistency between frontend and backend implementations - */ - -// ===== Core Types ===== - -export type ConversationFlow = - | 'discovery' - | 'planning' - | 'confirmation' - | 'execution' - | 'completion' - | 'auto_close'; - -export type MessageType = - | 'message' - | 'plan' - | 'progress' - | 'completion' - | 'discovery_question' - | 'confirmation_request'; - -export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed'; - -export type SessionStatus = 'active' | 'executing' | 'completed' | 'failed'; - -export type WorkflowType = - | 'meeting_prep' - | 'email_response' - | 'system_task' - | 'document_analysis' - | 'calendar_management'; - -// ===== Request/Response Types ===== - -export interface CreateChatSessionRequest { - actionable_item_id: string; - user_id: string; - context: { item_type: WorkflowType; metadata: Record }; -} - -export interface CreateChatSessionResponse { - success: true; - data: { - session_id: string; - initial_message: { - content: string; - type: MessageType; - options: string[]; - context: Record; - }; - expected_flow: ConversationFlow[]; - }; -} - -export interface ChatMessage { - id: string; - content: string; - sender: 'ai' | 'user'; - timestamp: string; // ISO 8601 - type: MessageType; - metadata?: Record; -} - -export interface ChatSessionDetails { - session_id: string; - status: SessionStatus; - current_flow: ConversationFlow; - messages: ChatMessage[]; - context: Record; - execution_id?: string; -} - -export interface SendMessageRequest { - content: string; - sender: 'user'; - current_flow: ConversationFlow; - context: Record; -} - -export interface SendMessageResponse { - success: true; - data: { - ai_response: { - content: string; - type: MessageType; - next_flow: ConversationFlow; - options: string[]; - metadata: Record; - }; - should_execute: boolean; - execution_plan?: ExecutionPlan; - }; -} - -// ===== Execution Types ===== - -export interface ExecutionStep { - id: string; - label: string; - status: TaskStatus; - estimated_duration: number; - dependencies: string[]; - started_at?: string; // ISO 8601 - completed_at?: string; // ISO 8601 - progress_percentage?: number; - result?: Record; -} - -export interface ExecutionPlan { - id: string; - steps: ExecutionStep[]; - estimated_total_duration: number; - requirements: { gmail_access?: boolean; notion_access?: boolean; calendar_access?: boolean }; -} - -export interface StartExecutionRequest { - execution_plan_id: string; - confirmed: true; - modifications?: Record; -} - -export interface StartExecutionResponse { - success: true; - data: { - execution_id: string; - status: 'started'; - estimated_duration: number; - steps: ExecutionStep[]; - }; -} - -export interface ExecutionStatusResponse { - success: true; - data: { - execution_id: string; - status: 'running' | 'completed' | 'failed'; - current_step?: { id: string; label: string; status: TaskStatus; progress_percentage: number }; - completed_steps: string[]; - results?: ExecutionResults; - }; -} - -export interface ExecutionResults { - summary: string; - artifacts: Artifact[]; - metrics: { - total_duration: number; - emails_processed?: number; - documents_created?: number; - apis_called?: number; - }; -} - -export interface Artifact { - type: 'notion_doc' | 'email_draft' | 'calendar_event' | 'document' | 'link'; - title: string; - url: string; - created_at: string; // ISO 8601 - metadata: Record; -} - -// ===== WebSocket Event Types ===== - -export interface WebSocketMessage { - type: string; - data: T; - timestamp?: string; // ISO 8601 -} - -// Tool definition for chat initialization -export interface ChatTool { - name: string; - description: string; - inputSchema: { type: 'object'; properties: Record; required?: string[] }; -} - -// Client → Server Events -export interface ChatInitEvent { - type: 'chat:init'; - data: { tools: ChatTool[]; sessionId?: string; timestamp: number }; -} - -export interface AuthenticateEvent { - type: 'authenticate'; - data: { token: string; session_id: string }; -} - -export interface JoinSessionEvent { - type: 'join_session'; - data: { session_id: string }; -} - -// Server → Client Events -export interface AuthenticatedEvent { - type: 'authenticated'; - data: { user_id: string; session_id: string }; -} - -export interface ExecutionStepProgressEvent { - type: 'execution_step_progress'; - data: { - session_id: string; - execution_id: string; - step_id: string; - status: TaskStatus; - progress_percentage: number; - message: string; - timestamp: string; // ISO 8601 - }; -} - -export interface ExecutionCompleteEvent { - type: 'execution_complete'; - data: { - session_id: string; - execution_id: string; - status: 'completed' | 'failed'; - results: ExecutionResults; - }; -} - -export interface AIThinkingEvent { - type: 'ai_thinking'; - data: { session_id: string; message: string; estimated_time: number }; -} - -export interface ErrorEvent { - type: 'error'; - data: { - session_id: string; - error_code: ErrorCode; - message: string; - details: Record; - retry_after?: number; - }; -} - -// ===== Error Types ===== - -export type ErrorCode = - | 'SESSION_NOT_FOUND' - | 'EXECUTION_FAILED' - | 'RATE_LIMITED' - | 'INVALID_INPUT' - | 'SOURCE_UNAVAILABLE' - | 'AI_UNAVAILABLE' - | 'TIMEOUT' - | 'UNAUTHORIZED' - | 'FORBIDDEN' - | 'INTERNAL_ERROR'; - -export interface APIError { - code: ErrorCode; - message: string; - details: Record; -} - -// ===== Standard API Response Envelope ===== - -export interface APIResponse { - success: boolean; - data: T | null; - error: APIError | null; - meta: { - timestamp: string; // ISO 8601 - request_id: string; - rate_limit: { - remaining: number; - reset_at: string; // ISO 8601 - }; - }; -} - -// ===== Meeting Preparation Specific Types ===== - -export interface MeetingPrepWorkflow { - workflow_type: 'meeting_preparation'; - inputs: { - meeting_title: string; - participant: string; - time_context: string; - document_sources: ('gmail' | 'notion' | 'calendar')[]; - output_format: 'notion_doc' | 'email_summary' | 'both'; - }; - processing_steps: MeetingPrepStep[]; -} - -export interface MeetingPrepStep { - step: 'fetch_gmail' | 'access_notion' | 'analyze_context' | 'consolidate_docs' | 'generate_link'; - action: string; - params: Record; -} - -// ===== Service Integration Types ===== - -export interface ExternalServiceConfig { - gmail?: { enabled: boolean; scopes: string[]; rate_limit: number }; - notion?: { enabled: boolean; workspace_id: string; rate_limit: number }; - calendar?: { enabled: boolean; calendar_ids: string[] }; -} - -export interface ServiceAuthStatus { - gmail: 'connected' | 'disconnected' | 'error'; - notion: 'connected' | 'disconnected' | 'error'; - calendar: 'connected' | 'disconnected' | 'error'; -} - -// ===== Performance & Monitoring Types ===== - -export interface PerformanceMetrics { - response_time: number; - execution_time: number; - external_api_calls: number; - tokens_used: number; - cost_estimate: number; -} - -export interface SessionMetrics { - session_id: string; - user_id: string; - started_at: string; // ISO 8601 - ended_at?: string; // ISO 8601 - total_messages: number; - execution_count: number; - performance: PerformanceMetrics; - satisfaction_score?: number; -} - -// ===== Type Guards ===== - -export function isWebSocketMessage(obj: unknown): obj is WebSocketMessage { - if (typeof obj !== 'object' || obj === null) return false; - const o = obj as Record; - return typeof o.type === 'string' && 'data' in o; -} - -export function isAPIResponse(obj: unknown): obj is APIResponse { - return ( - typeof obj === 'object' && - obj !== null && - typeof (obj as { success?: unknown }).success === 'boolean' && - ('data' in (obj as object) || 'error' in (obj as object)) && - 'meta' in (obj as object) - ); -} - -export function isExecutionStepProgressEvent(obj: unknown): obj is ExecutionStepProgressEvent { - if (!isWebSocketMessage(obj) || obj.type !== 'execution_step_progress') return false; - const d = obj.data; - return typeof d === 'object' && d !== null && 'step_id' in d && 'progress_percentage' in d; -} - -// ===== Utility Types ===== - -export type ChatEventHandler = (event: WebSocketMessage) => void; - -export interface ChatClientConfig { - websocket_url: string; - api_base_url: string; - auth_token: string; - retry_attempts: number; - timeout_ms: number; -} - -// ===== Frontend State Types ===== - -export interface ChatState { - session: ChatSessionDetails | null; - messages: ChatMessage[]; - currentFlow: ConversationFlow; - isExecuting: boolean; - executionProgress: ExecutionStep[]; - isConnected: boolean; - lastError: APIError | null; -} - -export interface ChatActions { - sendMessage: (content: string) => Promise; - startExecution: (planId: string) => Promise; - connect: () => Promise; - disconnect: () => void; - clearError: () => void; -} diff --git a/app/src/types/onboarding.ts b/app/src/types/onboarding.ts deleted file mode 100644 index 88e612edf..000000000 --- a/app/src/types/onboarding.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Onboarding flow types and interfaces - -export interface OnboardingStep { - id: string; - title: string; - description: string; - component: React.ComponentType; -} - -export interface UserProfile { - name?: string; - phone?: string; - countryCode?: string; - privacySettings?: PrivacySettings; - analyticsPreferences?: AnalyticsPreferences; -} - -export interface PrivacySettings { - enterpriseGradeSecurity: boolean; - dataEncryption: boolean; - secureBackups: boolean; -} - -export interface AnalyticsPreferences { - shareAnalytics: boolean; - emailConnected: boolean; - maximumPrivacy: boolean; -} - -export interface WeatherData { - location: string; - temperature: number; - condition: string; - icon: string; -} - -export interface Country { - code: string; - name: string; - flag: string; - dialCode: string; -} diff --git a/app/src/utils/deeplink.ts b/app/src/utils/deeplink.ts deleted file mode 100644 index 652bc1bcd..000000000 --- a/app/src/utils/deeplink.ts +++ /dev/null @@ -1,32 +0,0 @@ -export type WebLoginMethod = 'phone' | 'telegram'; - -export interface PhoneLoginContext { - method: 'phone'; - phoneNumber: string; - countryCode: string; -} - -// The shape of the Telegram user object is defined by Telegram. -// We keep it as unknown here and let the backend interpret it. -export interface TelegramLoginContext { - method: 'telegram'; - telegramUser: unknown; -} - -export type WebLoginContext = PhoneLoginContext | TelegramLoginContext; - -const DESKTOP_SCHEME = 'openhuman'; - -export const buildDesktopDeeplink = (token: string): string => { - const encoded = encodeURIComponent(token); - return `${DESKTOP_SCHEME}://auth?token=${encoded}`; -}; - -export const buildPaymentSuccessDeeplink = (sessionId: string): string => { - const encoded = encodeURIComponent(sessionId); - return `${DESKTOP_SCHEME}://payment/success?session_id=${encoded}`; -}; - -export const buildPaymentCancelDeeplink = (): string => { - return `${DESKTOP_SCHEME}://payment/cancel`; -}; diff --git a/app/src/utils/intelligenceTransforms.ts b/app/src/utils/intelligenceTransforms.ts deleted file mode 100644 index d6ad26136..000000000 --- a/app/src/utils/intelligenceTransforms.ts +++ /dev/null @@ -1,210 +0,0 @@ -import type { MCPTool } from '../lib/mcp'; -import type { - BackendActionableItem, - BackendChatMessage, - ConnectedTool, -} from '../services/intelligenceApi'; -import type { - ActionableItem, - ActionableItemPriority, - ActionableItemSource, - ActionableItemStatus, - ChatMessage, -} from '../types/intelligence'; - -/** - * Transform backend actionable item to frontend format - */ -export function transformBackendItemToFrontend(backendItem: BackendActionableItem): ActionableItem { - return { - id: backendItem.id, - title: backendItem.title, - description: backendItem.description, - source: backendItem.source as ActionableItemSource, - priority: backendItem.priority as ActionableItemPriority, - status: backendItem.status as ActionableItemStatus, - createdAt: new Date(backendItem.createdAt), - updatedAt: new Date(backendItem.updatedAt), - expiresAt: backendItem.expiresAt ? new Date(backendItem.expiresAt) : undefined, - snoozeUntil: backendItem.snoozeUntil ? new Date(backendItem.snoozeUntil) : undefined, - actionable: backendItem.actionable, - requiresConfirmation: backendItem.requiresConfirmation, - hasComplexAction: backendItem.hasComplexAction, - dismissedAt: backendItem.dismissedAt ? new Date(backendItem.dismissedAt) : undefined, - completedAt: backendItem.completedAt ? new Date(backendItem.completedAt) : undefined, - reminderCount: backendItem.reminderCount, - // Backend integration fields - threadId: backendItem.threadId, - executionStatus: backendItem.executionStatus, - currentSessionId: backendItem.currentSessionId, - }; -} - -/** - * Transform multiple backend items to frontend format - */ -export function transformBackendItemsToFrontend( - backendItems: BackendActionableItem[] -): ActionableItem[] { - return backendItems.map(transformBackendItemToFrontend); -} - -/** - * Transform backend chat message to frontend format - */ -export function transformBackendMessageToFrontend(backendMessage: BackendChatMessage): ChatMessage { - return { - id: backendMessage.id, - content: backendMessage.content, - sender: backendMessage.role === 'user' ? 'user' : 'ai', - timestamp: new Date(backendMessage.timestamp), - }; -} - -/** - * Transform multiple backend messages to frontend format - */ -export function transformBackendMessagesToFrontend( - backendMessages: BackendChatMessage[] -): ChatMessage[] { - return backendMessages.map(transformBackendMessageToFrontend); -} - -/** - * Transform frontend chat message to backend format - */ -export function transformFrontendMessageToBackend( - message: ChatMessage, - threadId: string -): Omit { - return { - content: message.content, - role: message.sender === 'user' ? 'user' : 'assistant', - timestamp: message.timestamp.toISOString(), - threadId, - }; -} - -/** - * Transform MCP tools to connected tools format for backend - */ -export function transformMCPToConnectedTools(mcpTools: MCPTool[]): ConnectedTool[] { - return mcpTools.map(tool => { - const [skillId, toolName] = tool.name.split('__'); - - return { - name: toolName || tool.name, - description: tool.description, - parameters: (tool.inputSchema || {}) as unknown as Record, - skillId: skillId || 'unknown', - enabled: true, - }; - }); -} - -/** - * Transform connected tools back to MCP format - */ -export function transformConnectedToolsToMCP(connectedTools: ConnectedTool[]): MCPTool[] { - return connectedTools.map(tool => ({ - name: `${tool.skillId}__${tool.name}`, - description: tool.description, - inputSchema: { type: 'object', properties: tool.parameters || {} }, - })); -} - -/** - * Validate actionable item data from backend - */ -export function validateBackendItem(item: unknown): item is BackendActionableItem { - if (typeof item !== 'object' || item === null) return false; - const o = item as Record; - return ( - typeof o.id === 'string' && - typeof o.title === 'string' && - typeof o.source === 'string' && - typeof o.priority === 'string' && - typeof o.status === 'string' && - typeof o.createdAt === 'string' && - typeof o.updatedAt === 'string' && - typeof o.actionable === 'boolean' - ); -} - -/** - * Create a new chat message in frontend format - */ -export function createChatMessage( - content: string, - sender: 'user' | 'ai', - id?: string -): ChatMessage { - return { - id: id || `${sender}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - content, - sender, - timestamp: new Date(), - }; -} - -/** - * Map execution status to user-friendly labels - */ -export function getExecutionStatusLabel(status?: string): string { - switch (status) { - case 'idle': - return 'Ready'; - case 'running': - return 'In Progress'; - case 'completed': - return 'Completed'; - case 'failed': - return 'Failed'; - default: - return 'Unknown'; - } -} - -/** - * Get priority display information - */ -export function getPriorityInfo(priority: ActionableItemPriority): { - label: string; - className: string; - color: string; -} { - switch (priority) { - case 'critical': - return { label: 'Critical', className: 'text-coral-400 bg-coral-500/10', color: 'coral' }; - case 'important': - return { label: 'Important', className: 'text-amber-400 bg-amber-500/10', color: 'amber' }; - case 'normal': - return { label: 'Normal', className: 'text-sage-400 bg-sage-500/10', color: 'sage' }; - } -} - -/** - * Get source display information - */ -export function getSourceInfo(source: ActionableItemSource): { - label: string; - icon: string; - className: string; -} { - switch (source) { - case 'email': - return { label: 'Email', icon: '📧', className: 'text-blue-400 bg-blue-500/10' }; - case 'calendar': - return { label: 'Calendar', icon: '📅', className: 'text-green-400 bg-green-500/10' }; - case 'telegram': - return { label: 'Telegram', icon: '💬', className: 'text-blue-400 bg-blue-500/10' }; - case 'ai_insight': - return { label: 'AI Insight', icon: '🤖', className: 'text-purple-400 bg-purple-500/10' }; - case 'system': - return { label: 'System', icon: '⚙️', className: 'text-stone-400 bg-stone-500/10' }; - case 'trading': - return { label: 'Trading', icon: '📈', className: 'text-yellow-400 bg-yellow-500/10' }; - case 'security': - return { label: 'Security', icon: '🔒', className: 'text-red-400 bg-red-500/10' }; - } -} diff --git a/app/test/e2e/globals.d.ts b/app/test/e2e/globals.d.ts deleted file mode 100644 index 6e1dfb538..000000000 --- a/app/test/e2e/globals.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Global declarations for E2E tests (WebDriverIO globals) -declare namespace NodeJS { - interface Global { - browser: any; - } -} - -declare const browser: any; diff --git a/package.json b/package.json index 50d5bf896..941c6f3ee 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "dev:app": "yarn workspace openhuman-app dev:app", "format": "yarn workspace openhuman-app format", "format:check": "yarn workspace openhuman-app format:check", + "knip": "yarn workspace openhuman-app knip", + "knip:production": "yarn workspace openhuman-app knip:production", "lint": "yarn workspace openhuman-app lint", "lint:fix": "yarn workspace openhuman-app lint:fix", "prepare": "husky", diff --git a/src/core/cli.rs b/src/core/cli.rs index 9533fb9f0..a84ca5b8d 100644 --- a/src/core/cli.rs +++ b/src/core/cli.rs @@ -355,10 +355,11 @@ fn run_namespace_command( }; // Domain adapters can intercept specific namespace/function combinations. - if args.len() > 1 && is_help(&args[1]) { - if autocomplete_cli_adapter::maybe_print_start_help(namespace, function) { - return Ok(()); - } + if args.len() > 1 + && is_help(&args[1]) + && autocomplete_cli_adapter::maybe_print_start_help(namespace, function) + { + return Ok(()); } if let Some(value) = autocomplete_cli_adapter::maybe_handle_namespace_start(namespace, function, &args[1..])? diff --git a/src/core/logging.rs b/src/core/logging.rs index db71d4e82..4f1d15b27 100644 --- a/src/core/logging.rs +++ b/src/core/logging.rs @@ -49,7 +49,7 @@ where let time_styled = Style::new().dimmed().paint(time.to_string()); write!(writer, "{time_styled}:")?; - let tag = format!("{level}"); + let tag = level.to_string(); let level_styled = match *meta.level() { Level::ERROR => Style::new().fg(Color::Red).bold().paint(tag), Level::WARN => Style::new().fg(Color::Yellow).bold().paint(tag), @@ -59,7 +59,7 @@ where }; write!(writer, "{level_styled}:")?; - let scope = format!("{target}"); + let scope = target.to_string(); let scope_styled = Style::new().fg(Color::Fixed(247)).paint(scope); write!(writer, "{scope_styled} ")?; } else { diff --git a/src/core/skills_cli.rs b/src/core/skills_cli.rs index 7309da6cd..26de13711 100644 --- a/src/core/skills_cli.rs +++ b/src/core/skills_cli.rs @@ -236,7 +236,7 @@ fn run_skills_list(args: &[String]) -> Result<()> { if manifests.is_empty() { println!("No skills found."); } else { - println!("{:<20} {:<10} {}", "ID", "VERSION", "NAME"); + println!("{:<20} {:<10} NAME", "ID", "VERSION"); println!("{}", "-".repeat(60)); for m in &manifests { println!( diff --git a/src/openhuman/agent/harness/self_healing.rs b/src/openhuman/agent/harness/self_healing.rs index 4ef973965..703155303 100644 --- a/src/openhuman/agent/harness/self_healing.rs +++ b/src/openhuman/agent/harness/self_healing.rs @@ -168,7 +168,7 @@ fn extract_command_name(error: &str) -> Option { // Pattern: "'CMD' is not recognized" if error.contains("is not recognized") { - let stripped = error.replace('\'', "").replace('"', ""); + let stripped = error.replace(['\'', '"'], ""); if let Some(cmd) = stripped.split_whitespace().next() { if cmd.len() < 64 { return Some(cmd.to_string()); diff --git a/src/openhuman/agent/harness/types.rs b/src/openhuman/agent/harness/types.rs index 7eb01b36f..0e577c6c4 100644 --- a/src/openhuman/agent/harness/types.rs +++ b/src/openhuman/agent/harness/types.rs @@ -11,7 +11,9 @@ pub type TaskId = String; /// Current execution status of a DAG task node. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum TaskStatus { + #[default] Pending, Running, Completed, @@ -19,12 +21,6 @@ pub enum TaskStatus { Cancelled, } -impl Default for TaskStatus { - fn default() -> Self { - Self::Pending - } -} - /// Request sent from the orchestrator to spawn a sub-agent. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SubAgentRequest { diff --git a/src/openhuman/autocomplete/core/engine.rs b/src/openhuman/autocomplete/core/engine.rs index 0d7ac301a..ed06f715d 100644 --- a/src/openhuman/autocomplete/core/engine.rs +++ b/src/openhuman/autocomplete/core/engine.rs @@ -188,10 +188,7 @@ impl AutocompleteEngine { state.updated_at_ms = Some(Utc::now().timestamp_millis()); // Only notify if this is a *new* error message. - let is_new_error = state - .last_notified_error - .as_ref() - .map_or(true, |prev| prev != &err); + let is_new_error = state.last_notified_error.as_ref() != Some(&err); if is_new_error { state.last_notified_error = Some(err.clone()); } diff --git a/src/openhuman/autocomplete/core/text.rs b/src/openhuman/autocomplete/core/text.rs index 381e0a6f7..d369166d0 100644 --- a/src/openhuman/autocomplete/core/text.rs +++ b/src/openhuman/autocomplete/core/text.rs @@ -29,8 +29,7 @@ pub(super) fn sanitize_suggestion(text: &str) -> String { } }; let cleaned = stripped_prefix - .replace('\t', " ") - .replace('→', " ") + .replace(['\t', '→'], " ") .replace('\r', "") .split_whitespace() .collect::>() diff --git a/src/openhuman/channels/bus.rs b/src/openhuman/channels/bus.rs index 086747f5f..75a966cac 100644 --- a/src/openhuman/channels/bus.rs +++ b/src/openhuman/channels/bus.rs @@ -12,6 +12,12 @@ use serde_json::json; /// sending replies back to the originating channel via the backend REST API. pub struct ChannelInboundSubscriber; +impl Default for ChannelInboundSubscriber { + fn default() -> Self { + Self::new() + } +} + impl ChannelInboundSubscriber { pub fn new() -> Self { Self diff --git a/src/openhuman/channels/providers/presentation.rs b/src/openhuman/channels/providers/presentation.rs index 631a16973..769a57b30 100644 --- a/src/openhuman/channels/providers/presentation.rs +++ b/src/openhuman/channels/providers/presentation.rs @@ -277,8 +277,8 @@ fn split_sentences(text: &str) -> Vec { /// Group sentences into 2-3 bubbles. fn group_sentences(sentences: &[String]) -> Vec { - let target_count = std::cmp::min(3, (sentences.len() + 1) / 2); - let group_size = (sentences.len() + target_count - 1) / target_count; + let target_count = std::cmp::min(3, sentences.len().div_ceil(2)); + let group_size = sentences.len().div_ceil(target_count); let mut groups: Vec = Vec::new(); for chunk in sentences.chunks(group_size) { diff --git a/src/openhuman/config/schema/dictation.rs b/src/openhuman/config/schema/dictation.rs index 02fc71607..fbc3d6c7e 100644 --- a/src/openhuman/config/schema/dictation.rs +++ b/src/openhuman/config/schema/dictation.rs @@ -6,19 +6,15 @@ use serde::{Deserialize, Serialize}; /// Activation mode for the dictation hotkey. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "lowercase")] +#[derive(Default)] pub enum DictationActivationMode { /// Press once to start, press again to stop. Toggle, /// Hold to record, release to stop (push-to-talk). + #[default] Push, } -impl Default for DictationActivationMode { - fn default() -> Self { - Self::Push - } -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct DictationConfig { /// Whether voice dictation is enabled. diff --git a/src/openhuman/config/schema/load.rs b/src/openhuman/config/schema/load.rs index 9c1610d7a..7656a4d35 100644 --- a/src/openhuman/config/schema/load.rs +++ b/src/openhuman/config/schema/load.rs @@ -30,7 +30,7 @@ struct ActiveWorkspaceState { } fn default_config_dir() -> Result { - Ok(default_root_openhuman_dir()?) + default_root_openhuman_dir() } /// Returns the root openhuman directory (`~/.openhuman`), independent of any diff --git a/src/openhuman/config/schema/orchestrator.rs b/src/openhuman/config/schema/orchestrator.rs index 03b9a2d7a..fbadae0b4 100644 --- a/src/openhuman/config/schema/orchestrator.rs +++ b/src/openhuman/config/schema/orchestrator.rs @@ -47,7 +47,7 @@ pub struct OrchestratorConfig { /// Per-archetype configuration override. /// /// Any field left `None` uses the archetype's built-in default. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] pub struct ArchetypeConfig { /// Model name or hint override (e.g. "coding-v1", "local:phi3"). #[serde(default)] @@ -104,16 +104,3 @@ impl Default for OrchestratorConfig { } } } - -impl Default for ArchetypeConfig { - fn default() -> Self { - Self { - model: None, - system_prompt: None, - temperature: None, - max_tool_iterations: None, - timeout_secs: None, - sandbox: None, - } - } -} diff --git a/src/openhuman/config/schema/tools.rs b/src/openhuman/config/schema/tools.rs index 68670e03f..1e811a385 100644 --- a/src/openhuman/config/schema/tools.rs +++ b/src/openhuman/config/schema/tools.rs @@ -260,7 +260,7 @@ impl Default for IntegrationToggle { /// location search (Google Places), and phone calls (Twilio). The backend /// handles external API calls, billing, and rate limiting; the client only /// forwards requests and displays results. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] pub struct IntegrationsConfig { /// Master switch — set to `true` to register integration tools. #[serde(default)] @@ -286,16 +286,3 @@ pub struct IntegrationsConfig { #[serde(default)] pub parallel: IntegrationToggle, } - -impl Default for IntegrationsConfig { - fn default() -> Self { - Self { - enabled: false, - backend_url: None, - auth_token: None, - twilio: IntegrationToggle::default(), - google_places: IntegrationToggle::default(), - parallel: IntegrationToggle::default(), - } - } -} diff --git a/src/openhuman/config/schema/voice_server.rs b/src/openhuman/config/schema/voice_server.rs index fce571258..f1eaf89b6 100644 --- a/src/openhuman/config/schema/voice_server.rs +++ b/src/openhuman/config/schema/voice_server.rs @@ -6,19 +6,15 @@ use serde::{Deserialize, Serialize}; /// Activation mode for the voice server hotkey. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum VoiceActivationMode { /// Single press toggles recording on/off. Tap, /// Hold to record, release to stop. + #[default] Push, } -impl Default for VoiceActivationMode { - fn default() -> Self { - Self::Push - } -} - /// Configuration for the voice dictation server. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct VoiceServerConfig { diff --git a/src/openhuman/local_ai/parse.rs b/src/openhuman/local_ai/parse.rs index a7657573c..60ec6bffa 100644 --- a/src/openhuman/local_ai/parse.rs +++ b/src/openhuman/local_ai/parse.rs @@ -19,15 +19,8 @@ pub(crate) fn parse_suggestions(raw: &str, limit: usize) -> Vec { fn normalize_inline_text(value: &str) -> String { value - .replace('\u{200B}', "") - .replace('\u{200C}', "") - .replace('\u{200D}', "") - .replace('\u{FEFF}', "") - .replace('\u{00A0}', " ") - .replace('\u{2028}', " ") - .replace('\u{2029}', " ") - .replace('\t', " ") - .replace('→', " ") + .replace(['\u{200B}', '\u{200C}', '\u{200D}', '\u{FEFF}'], "") + .replace(['\u{00A0}', '\u{2028}', '\u{2029}', '\t', '→'], " ") } fn trim_generation_prefixes(mut value: &str) -> &str { @@ -37,7 +30,7 @@ fn trim_generation_prefixes(mut value: &str) -> &str { for prefix in ["suffix:", "completion:", "result:", "output:"] { if value .get(..prefix.len()) - .map_or(false, |s| s.eq_ignore_ascii_case(prefix)) + .is_some_and(|s| s.eq_ignore_ascii_case(prefix)) { value = value.get(prefix.len()..).unwrap_or(value).trim_start(); break; diff --git a/src/openhuman/local_ai/service/whisper_engine.rs b/src/openhuman/local_ai/service/whisper_engine.rs index 0d94798ad..6a73a0b17 100644 --- a/src/openhuman/local_ai/service/whisper_engine.rs +++ b/src/openhuman/local_ai/service/whisper_engine.rs @@ -352,7 +352,7 @@ fn decode_wav_to_f32(data: &[u8]) -> Result, String> { } pos += 8 + chunk_size; - if chunk_size % 2 != 0 { + if !chunk_size.is_multiple_of(2) { pos += 1; } } diff --git a/src/openhuman/memory/store/unified/events.rs b/src/openhuman/memory/store/unified/events.rs index 51505da33..be7c24735 100644 --- a/src/openhuman/memory/store/unified/events.rs +++ b/src/openhuman/memory/store/unified/events.rs @@ -197,7 +197,7 @@ pub fn events_for_segment( ORDER BY created_at ASC", )?; let rows = stmt - .query_map(params![segment_id], |row| row_to_event(row))? + .query_map(params![segment_id], row_to_event)? .collect::, _>>()?; Ok(rows) } @@ -318,7 +318,7 @@ pub fn extract_events_heuristic(text: &str) -> Vec<(EventType, String)> { // Split into sentences (rough heuristic). let sentences: Vec<&str> = text - .split(|c: char| c == '.' || c == '!' || c == '?' || c == '\n') + .split(['.', '!', '?', '\n']) .map(str::trim) .filter(|s| s.len() > 5) .collect(); diff --git a/src/openhuman/memory/store/unified/profile.rs b/src/openhuman/memory/store/unified/profile.rs index 6b8ca3d0e..ddec35e62 100644 --- a/src/openhuman/memory/store/unified/profile.rs +++ b/src/openhuman/memory/store/unified/profile.rs @@ -189,7 +189,7 @@ pub fn profile_load_all(conn: &Arc>) -> anyhow::Result, _>>()?; Ok(rows) } @@ -208,7 +208,7 @@ pub fn profile_facets_by_type( ORDER BY evidence_count DESC", )?; let rows = stmt - .query_map(params![facet_type.as_str()], |row| row_to_facet(row))? + .query_map(params![facet_type.as_str()], row_to_facet)? .collect::, _>>()?; Ok(rows) } diff --git a/src/openhuman/memory/store/unified/segments.rs b/src/openhuman/memory/store/unified/segments.rs index 2d1e143f9..3d25a89dd 100644 --- a/src/openhuman/memory/store/unified/segments.rs +++ b/src/openhuman/memory/store/unified/segments.rs @@ -285,7 +285,7 @@ pub fn open_segment_for_session( ORDER BY created_at DESC LIMIT 1", params![session_id], - |row| row_to_segment(row), + row_to_segment, ) .optional()?; Ok(row) @@ -308,7 +308,7 @@ pub fn segments_by_namespace( LIMIT ?2", )?; let rows = stmt - .query_map(params![namespace, limit as i64], |row| row_to_segment(row))? + .query_map(params![namespace, limit as i64], row_to_segment)? .collect::, _>>()?; Ok(rows) } @@ -327,7 +327,7 @@ pub fn segment_get( FROM conversation_segments WHERE segment_id = ?1", params![segment_id], - |row| row_to_segment(row), + row_to_segment, ) .optional()?; Ok(row) @@ -349,7 +349,7 @@ pub fn segments_pending_summary( LIMIT ?1", )?; let rows = stmt - .query_map(params![limit as i64], |row| row_to_segment(row))? + .query_map(params![limit as i64], row_to_segment)? .collect::, _>>()?; Ok(rows) } diff --git a/src/openhuman/skills/qjs_skill_instance/js_handlers.rs b/src/openhuman/skills/qjs_skill_instance/js_handlers.rs index c69114555..62d1eb9a7 100644 --- a/src/openhuman/skills/qjs_skill_instance/js_handlers.rs +++ b/src/openhuman/skills/qjs_skill_instance/js_handlers.rs @@ -623,7 +623,7 @@ pub(crate) async fn handle_js_call( } // Log progress every 2 seconds so we can see if the loop is running - if poll_count % 400 == 0 { + if poll_count.is_multiple_of(400) { let elapsed = 30 - deadline .duration_since(tokio::time::Instant::now()) diff --git a/src/openhuman/skills/quickjs_libs/qjs_ops/ops_net.rs b/src/openhuman/skills/quickjs_libs/qjs_ops/ops_net.rs index cd2f092cd..0c97b48ae 100644 --- a/src/openhuman/skills/quickjs_libs/qjs_ops/ops_net.rs +++ b/src/openhuman/skills/quickjs_libs/qjs_ops/ops_net.rs @@ -185,7 +185,7 @@ pub fn register<'js>( } { - let ws = ws_state.clone(); + let _ws = ws_state.clone(); ops.set( "ws_recv", Function::new(ctx.clone(), move |_id: u32| { diff --git a/src/openhuman/skills/types.rs b/src/openhuman/skills/types.rs index 965e4068b..d6a93ab3c 100644 --- a/src/openhuman/skills/types.rs +++ b/src/openhuman/skills/types.rs @@ -224,7 +224,7 @@ impl ToolResult { .iter() .filter_map(|c| match c { ToolContent::Text { text } => Some(text.as_str()), - ToolContent::Json { data } => None, + ToolContent::Json { data: _ } => None, }) .collect::>() .join("\n") diff --git a/src/openhuman/socket/schemas.rs b/src/openhuman/socket/schemas.rs index d1f6a4104..c94acb5da 100644 --- a/src/openhuman/socket/schemas.rs +++ b/src/openhuman/socket/schemas.rs @@ -195,7 +195,7 @@ fn handle_state(_params: Map) -> ControllerFuture { let mgr = require_manager()?; let state = mgr.get_state(); log::debug!("[socket:rpc] state → {:?}", state.status); - Ok(serde_json::to_value(state).map_err(|e| format!("serialize: {e}"))?) + serde_json::to_value(state).map_err(|e| format!("serialize: {e}")) }) } diff --git a/src/openhuman/subconscious/engine.rs b/src/openhuman/subconscious/engine.rs index 9aef37ef3..46c20fcde 100644 --- a/src/openhuman/subconscious/engine.rs +++ b/src/openhuman/subconscious/engine.rs @@ -56,19 +56,18 @@ impl SubconsciousEngine { ) -> Self { // Seed default system tasks eagerly so they show in the UI immediately, // without waiting for the first tick. - let seeded = - match store::with_connection(&workspace_dir, |conn| store::seed_default_tasks(conn)) { - Ok(count) => { - if count > 0 { - info!("[subconscious] seeded {count} tasks on init"); - } - true + let seeded = match store::with_connection(&workspace_dir, store::seed_default_tasks) { + Ok(count) => { + if count > 0 { + info!("[subconscious] seeded {count} tasks on init"); } - Err(e) => { - warn!("[subconscious] seed on init failed: {e}"); - false - } - }; + true + } + Err(e) => { + warn!("[subconscious] seed on init failed: {e}"); + false + } + }; Self { workspace_dir, @@ -416,7 +415,7 @@ impl SubconsciousEngine { // ── Internal methods ───────────────────────────────────────────────────── fn seed_tasks(&self) { - match store::with_connection(&self.workspace_dir, |conn| store::seed_default_tasks(conn)) { + match store::with_connection(&self.workspace_dir, store::seed_default_tasks) { Ok(count) => { if count > 0 { info!("[subconscious] seeded {count} default tasks"); diff --git a/src/openhuman/subconscious/global.rs b/src/openhuman/subconscious/global.rs index 3199d8400..23763da2a 100644 --- a/src/openhuman/subconscious/global.rs +++ b/src/openhuman/subconscious/global.rs @@ -93,9 +93,8 @@ pub async fn bootstrap_after_login() -> Result<(), String> { // SubconsciousEngine::new calls seed_default_tasks() inside the // constructor, so by the time this returns the 3 system defaults are // present in `/subconscious/subconscious.db`. - get_or_init_engine().await.map_err(|e| { + get_or_init_engine().await.inspect_err(|_e| { BOOTSTRAPPED.store(false, Ordering::SeqCst); - e })?; tracing::info!( workspace = %config.workspace_dir.display(), diff --git a/src/openhuman/tools/ask_clarification.rs b/src/openhuman/tools/ask_clarification.rs index faed21176..641600d6f 100644 --- a/src/openhuman/tools/ask_clarification.rs +++ b/src/openhuman/tools/ask_clarification.rs @@ -10,6 +10,12 @@ use serde_json::json; /// event channel and waits for a response before continuing. pub struct AskClarificationTool; +impl Default for AskClarificationTool { + fn default() -> Self { + Self::new() + } +} + impl AskClarificationTool { pub fn new() -> Self { Self diff --git a/src/openhuman/tools/spawn_subagent.rs b/src/openhuman/tools/spawn_subagent.rs index 231d73f12..d70840fa7 100644 --- a/src/openhuman/tools/spawn_subagent.rs +++ b/src/openhuman/tools/spawn_subagent.rs @@ -9,6 +9,12 @@ use serde_json::json; /// Available only to the Orchestrator archetype. pub struct SpawnSubagentTool; +impl Default for SpawnSubagentTool { + fn default() -> Self { + Self::new() + } +} + impl SpawnSubagentTool { pub fn new() -> Self { Self @@ -63,7 +69,7 @@ impl Tool for SpawnSubagentTool { let prompt = args.get("prompt").and_then(|v| v.as_str()).unwrap_or(""); - let context = args.get("context").and_then(|v| v.as_str()).unwrap_or(""); + let _context = args.get("context").and_then(|v| v.as_str()).unwrap_or(""); if prompt.is_empty() { return Ok(ToolResult::error("prompt is required")); diff --git a/src/openhuman/tree_summarizer/bus.rs b/src/openhuman/tree_summarizer/bus.rs index 3d32f4ee9..26aa80245 100644 --- a/src/openhuman/tree_summarizer/bus.rs +++ b/src/openhuman/tree_summarizer/bus.rs @@ -9,6 +9,12 @@ use async_trait::async_trait; /// Subscribes to tree summarizer events and logs activity. pub struct TreeSummarizerEventSubscriber; +impl Default for TreeSummarizerEventSubscriber { + fn default() -> Self { + Self::new() + } +} + impl TreeSummarizerEventSubscriber { pub fn new() -> Self { Self diff --git a/src/openhuman/tree_summarizer/store.rs b/src/openhuman/tree_summarizer/store.rs index 233e88e67..6d0515527 100644 --- a/src/openhuman/tree_summarizer/store.rs +++ b/src/openhuman/tree_summarizer/store.rs @@ -294,8 +294,8 @@ pub fn get_tree_status(config: &Config, namespace: &str) -> Result { // Try to parse timestamp from path if let Some(ts) = timestamp_from_hour_path( &name, - &month_entry.file_name().to_string_lossy().to_string(), - &day_entry.file_name().to_string_lossy().to_string(), + month_entry.file_name().to_string_lossy().as_ref(), + day_entry.file_name().to_string_lossy().as_ref(), &hname, ) { match &oldest { @@ -672,10 +672,8 @@ fn count_md_files(dir: &Path) -> Result { continue; // skip buffer directories } count += count_md_files(&entry.path())?; - } else if ft.is_file() { - if entry.path().extension().map(|e| e == "md").unwrap_or(false) { - count += 1; - } + } else if ft.is_file() && entry.path().extension().map(|e| e == "md").unwrap_or(false) { + count += 1; } } Ok(count) diff --git a/src/openhuman/tree_summarizer/types.rs b/src/openhuman/tree_summarizer/types.rs index 04eb46cff..c5c621f4d 100644 --- a/src/openhuman/tree_summarizer/types.rs +++ b/src/openhuman/tree_summarizer/types.rs @@ -126,7 +126,7 @@ pub struct QueryResult { /// Rough token estimate: ~4 characters per token. pub fn estimate_tokens(text: &str) -> u32 { - (text.len() as u32 + 3) / 4 + (text.len() as u32).div_ceil(4) } /// Derive the parent node ID from a node ID. diff --git a/src/openhuman/voice/audio_capture.rs b/src/openhuman/voice/audio_capture.rs index 52f5530e6..599dfe80a 100644 --- a/src/openhuman/voice/audio_capture.rs +++ b/src/openhuman/voice/audio_capture.rs @@ -304,7 +304,7 @@ fn record_on_thread( }; // If the preferred config failed, retry with the device's default config. - let (stream, source_sample_rate, source_channels) = match stream { + let (stream, source_sample_rate, _source_channels) = match stream { Ok(s) => (s, source_sample_rate, source_channels), Err(ref preferred_err) => { warn!( diff --git a/src/openhuman/voice/hotkey.rs b/src/openhuman/voice/hotkey.rs index 0b5b3bb47..b9df0e7c7 100644 --- a/src/openhuman/voice/hotkey.rs +++ b/src/openhuman/voice/hotkey.rs @@ -21,19 +21,15 @@ const LOG_PREFIX: &str = "[voice_hotkey]"; Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema, )] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum ActivationMode { /// Single press toggles recording on/off. Tap, /// Hold to record, release to stop. + #[default] Push, } -impl Default for ActivationMode { - fn default() -> Self { - Self::Push - } -} - /// Events emitted by the hotkey listener. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HotkeyEvent { diff --git a/src/openhuman/webhooks/bus.rs b/src/openhuman/webhooks/bus.rs index bdf5c3d62..43a3b6a1a 100644 --- a/src/openhuman/webhooks/bus.rs +++ b/src/openhuman/webhooks/bus.rs @@ -27,6 +27,12 @@ fn error_body(message: &str) -> String { /// flow: lookup tunnel → dispatch to skill/echo → emit response via socket. pub struct WebhookRequestSubscriber; +impl Default for WebhookRequestSubscriber { + fn default() -> Self { + Self::new() + } +} + impl WebhookRequestSubscriber { pub fn new() -> Self { Self