diff --git a/app/src/components/intelligence/SyncBudgetDialog.tsx b/app/src/components/intelligence/SyncBudgetDialog.tsx deleted file mode 100644 index e80ca7ab86..0000000000 --- a/app/src/components/intelligence/SyncBudgetDialog.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { useCallback, useState } from 'react'; - -import { useT } from '../../lib/i18n/I18nContext'; -import { updateMemorySource } from '../../services/memorySourcesService'; -import Button from '../ui/Button'; - -interface SyncBudgetDialogProps { - source: { - id: string; - label: string; - max_tokens_per_sync?: number | null; - max_cost_per_sync_usd?: number | null; - sync_depth_days?: number | null; - }; - onClose: () => void; - onSaved: () => void; -} - -export default function SyncBudgetDialog({ source, onClose, onSaved }: SyncBudgetDialogProps) { - const { t } = useT(); - const [maxTokens, setMaxTokens] = useState(source.max_tokens_per_sync?.toString() ?? ''); - const [maxCost, setMaxCost] = useState(source.max_cost_per_sync_usd?.toString() ?? ''); - const [depthDays, setDepthDays] = useState(source.sync_depth_days?.toString() ?? ''); - const [saving, setSaving] = useState(false); - const [error, setError] = useState(null); - - const handleSave = useCallback(async () => { - setSaving(true); - setError(null); - try { - await updateMemorySource(source.id, { - max_tokens_per_sync: maxTokens ? Number(maxTokens) : undefined, - max_cost_per_sync_usd: maxCost ? Number(maxCost) : undefined, - sync_depth_days: depthDays ? Number(depthDays) : undefined, - } as Parameters[1]); - onSaved(); - onClose(); - } catch (e) { - setError(e instanceof Error ? e.message : String(e)); - } finally { - setSaving(false); - } - }, [source.id, maxTokens, maxCost, depthDays, onSaved, onClose]); - - return ( -
-
e.stopPropagation()}> -

{t('syncBudget.title')}

-

{source.label}

- -
-
- -

- {t('syncBudget.maxTokensHelp')} -

- setMaxTokens(e.target.value)} - placeholder={t('syncBudget.unlimited')} - className="w-full px-3 py-1.5 rounded-md border border-line bg-surface text-sm font-mono" - /> -
- -
- -

{t('syncBudget.maxCostHelp')}

- setMaxCost(e.target.value)} - placeholder={t('syncBudget.unlimited')} - className="w-full px-3 py-1.5 rounded-md border border-line bg-surface text-sm font-mono" - /> -
- -
- -

- {t('syncBudget.syncDepthHelp')} -

- -
-
- - {error &&

{error}

} - -
- - -
-
-
- ); -} diff --git a/app/src/components/intelligence/SyncConfirmDialog.tsx b/app/src/components/intelligence/SyncConfirmDialog.tsx deleted file mode 100644 index 045f090681..0000000000 --- a/app/src/components/intelligence/SyncConfirmDialog.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { useT } from '../../lib/i18n/I18nContext'; -import { callCoreRpc } from '../../services/coreRpcClient'; -import Button from '../ui/Button'; - -interface SyncEstimate { - item_count: number; - estimated_tokens: number; - estimated_cost_usd: number; - budget_max_cost_usd: number | null; - budget_max_tokens: number | null; -} - -interface SyncConfirmDialogProps { - sourceId: string; - onConfirm: () => void; - onCancel: () => void; -} - -export default function SyncConfirmDialog({ - sourceId, - onConfirm, - onCancel, -}: SyncConfirmDialogProps) { - const { t } = useT(); - const [estimate, setEstimate] = useState(null); - const [error, setError] = useState(null); - - useEffect(() => { - let cancelled = false; - setEstimate(null); - setError(null); - (async () => { - try { - const resp = await callCoreRpc<{ result: SyncEstimate }>({ - method: 'openhuman.memory_sources_estimate_sync_cost', - params: { source_id: sourceId }, - }); - if (!cancelled) setEstimate(resp.result); - } catch (e) { - if (!cancelled) setError(e instanceof Error ? e.message : String(e)); - } - })(); - return () => { - cancelled = true; - }; - }, [sourceId]); - - const tokenStr = estimate - ? estimate.estimated_tokens > 1000 - ? `${Math.round(estimate.estimated_tokens / 1000)}k` - : String(estimate.estimated_tokens) - : ''; - - return ( -
-
e.stopPropagation()}> -

{t('syncConfirm.title')}

- - {!estimate && !error && ( -

{t('syncConfirm.estimating')}

- )} - - {error &&

{error}

} - - {estimate && ( -
-

- {t('syncConfirm.message') - .replace('{items}', String(estimate.item_count)) - .replace('{tokens}', tokenStr) - .replace('{cost}', estimate.estimated_cost_usd.toFixed(4))} -

- {estimate.budget_max_cost_usd != null && ( -

- {t('syncConfirm.budgetNote').replace( - '{max}', - estimate.budget_max_cost_usd.toFixed(2) - )} -

- )} -
- )} - -
- - -
-
-
- ); -} diff --git a/app/src/hooks/__tests__/useScreenIntelligenceItems.test.ts b/app/src/hooks/__tests__/useScreenIntelligenceItems.test.ts deleted file mode 100644 index fdd8eea662..0000000000 --- a/app/src/hooks/__tests__/useScreenIntelligenceItems.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import type { AccessibilityVisionSummary } from '../../utils/tauriCommands'; - -// Test the mapping logic directly (extracted from the hook for testability) -function confidenceToPriority(confidence: number): 'critical' | 'important' | 'normal' { - if (confidence > 0.9) return 'critical'; - if (confidence > 0.7) return 'important'; - return 'normal'; -} - -function mapSummaryToItem(summary: AccessibilityVisionSummary) { - return { - id: `si-${summary.id}`, - title: summary.actionable_notes.slice(0, 120), - description: [summary.ui_state, summary.key_text].filter(Boolean).join(' - '), - source: 'ai_insight' as const, - priority: confidenceToPriority(summary.confidence), - status: 'active' as const, - createdAt: new Date(summary.captured_at_ms), - updatedAt: new Date(summary.captured_at_ms), - actionable: true, - sourceLabel: summary.app_name ?? 'Screen Intelligence', - }; -} - -const makeSummary = ( - overrides: Partial = {} -): AccessibilityVisionSummary => ({ - id: 'vision-123', - captured_at_ms: 1700000000000, - app_name: 'Safari', - window_title: 'GitHub', - ui_state: 'editor open', - key_text: 'fn main()', - actionable_notes: 'Consider adding tests', - confidence: 0.85, - ...overrides, -}); - -describe('useScreenIntelligenceItems mapping', () => { - it('maps VisionSummary to ActionableItem correctly', () => { - const summary = makeSummary(); - const item = mapSummaryToItem(summary); - - expect(item.id).toBe('si-vision-123'); - expect(item.title).toBe('Consider adding tests'); - expect(item.description).toBe('editor open - fn main()'); - expect(item.source).toBe('ai_insight'); - expect(item.priority).toBe('important'); - expect(item.status).toBe('active'); - expect(item.sourceLabel).toBe('Safari'); - expect(item.actionable).toBe(true); - }); - - it('handles empty array', () => { - const items: AccessibilityVisionSummary[] = []; - const mapped = items.map(mapSummaryToItem); - expect(mapped).toEqual([]); - }); - - it('derives critical priority from high confidence', () => { - const item = mapSummaryToItem(makeSummary({ confidence: 0.95 })); - expect(item.priority).toBe('critical'); - }); - - it('derives normal priority from low confidence', () => { - const item = mapSummaryToItem(makeSummary({ confidence: 0.5 })); - expect(item.priority).toBe('normal'); - }); - - it('derives important priority from medium confidence', () => { - const item = mapSummaryToItem(makeSummary({ confidence: 0.8 })); - expect(item.priority).toBe('important'); - }); - - it('uses Screen Intelligence as default sourceLabel when app_name is null', () => { - const item = mapSummaryToItem(makeSummary({ app_name: null })); - expect(item.sourceLabel).toBe('Screen Intelligence'); - }); - - it('filters empty strings from description parts', () => { - const item = mapSummaryToItem(makeSummary({ ui_state: '', key_text: 'some text' })); - expect(item.description).toBe('some text'); - }); - - it('truncates long actionable_notes in title', () => { - const longNotes = 'A'.repeat(200); - const item = mapSummaryToItem(makeSummary({ actionable_notes: longNotes })); - expect(item.title.length).toBe(120); - }); -}); - -describe('confidenceToPriority', () => { - it('returns critical for > 0.9', () => { - expect(confidenceToPriority(0.91)).toBe('critical'); - expect(confidenceToPriority(1.0)).toBe('critical'); - }); - - it('returns important for > 0.7 and <= 0.9', () => { - expect(confidenceToPriority(0.71)).toBe('important'); - expect(confidenceToPriority(0.9)).toBe('important'); - }); - - it('returns normal for <= 0.7', () => { - expect(confidenceToPriority(0.7)).toBe('normal'); - expect(confidenceToPriority(0.0)).toBe('normal'); - }); -}); diff --git a/app/src/hooks/useIntelligenceApiFallback.ts b/app/src/hooks/useIntelligenceApiFallback.ts deleted file mode 100644 index ba94c29da7..0000000000 --- a/app/src/hooks/useIntelligenceApiFallback.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { useCallback, useState } from 'react'; - -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. - */ - -interface UseUpdateActionableItemResult { - mutateAsync: (variables: { - itemId: string; - status: ActionableItemStatus; - }) => Promise<{ itemId: string; status: ActionableItemStatus; updatedAt: Date }>; - loading: boolean; - error: string | null; -} - -/** - * Hook for updating actionable item status (local-only). - */ -export const useUpdateActionableItem = (): UseUpdateActionableItemResult => { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const mutateAsync = useCallback( - async (variables: { itemId: string; status: ActionableItemStatus }) => { - setLoading(true); - setError(null); - try { - // Items are managed locally; just acknowledge the status change. - return { ...variables, updatedAt: new Date() }; - } finally { - setLoading(false); - } - }, - [] - ); - - return { mutateAsync, loading, error }; -}; - -interface UseSnoozeActionableItemResult { - mutateAsync: (variables: { - itemId: string; - snoozeUntil: Date; - }) => Promise<{ itemId: string; snoozeUntil: Date; updatedAt: Date }>; - loading: boolean; - error: string | null; -} - -/** - * Hook for snoozing actionable item (local-only). - */ -export const useSnoozeActionableItem = (): UseSnoozeActionableItemResult => { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const mutateAsync = useCallback(async (variables: { itemId: string; snoozeUntil: Date }) => { - setLoading(true); - setError(null); - try { - return { ...variables, updatedAt: new Date() }; - } finally { - setLoading(false); - } - }, []); - - return { mutateAsync, loading, error }; -}; - -interface UseChatSessionResult { - data: { threadId: string; messages: ChatMessage[] } | null; - loading: boolean; - error: string | null; -} - -/** - * Chat session stub (local-only — no remote thread API). - */ -export const useChatSession = (_itemId: string | null): UseChatSessionResult => { - return { data: null, loading: false, error: null }; -}; - -interface UseExecuteTaskResult { - mutateAsync: (variables: { - itemId: string; - connectedTools: ConnectedTool[]; - }) => Promise<{ executionId: string; sessionId: string; status: string }>; - loading: boolean; - error: string | null; -} - -/** - * Task execution stub (local-only — no remote execution API). - */ -export const useExecuteTask = (): UseExecuteTaskResult => { - const mutateAsync = useCallback( - async (_variables: { itemId: string; connectedTools: ConnectedTool[] }) => { - return { executionId: '', sessionId: '', status: 'unsupported' }; - }, - [] - ); - - return { mutateAsync, loading: false, error: null }; -}; - -// Export query key utilities for consistency -export const intelligenceKeys = { - all: ['intelligence'] as const, - items: () => [...intelligenceKeys.all, 'items'] as const, - item: (id: string) => [...intelligenceKeys.all, 'item', id] as const, - thread: (itemId: string) => [...intelligenceKeys.all, 'thread', itemId] as const, - messages: (threadId: string) => [...intelligenceKeys.all, 'messages', threadId] as const, - execution: (executionId: string) => [...intelligenceKeys.all, 'execution', executionId] as const, -}; diff --git a/app/src/hooks/useIntelligenceStats.ts b/app/src/hooks/useIntelligenceStats.ts deleted file mode 100644 index 19ec075be0..0000000000 --- a/app/src/hooks/useIntelligenceStats.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; - -import { callCoreRpc } from '../services/coreRpcClient'; -import { aiListMemoryFiles, type GraphRelation, memoryGraphQuery } from '../utils/tauriCommands'; - -export type AIStatus = 'idle' | 'initializing' | 'ready' | 'error'; - -const POLL_MS = 5000; - -interface SessionEntry { - sessionId: string; - updatedAt: number; - inputTokens: number; - outputTokens: number; - totalTokens: number; - compactionCount: number; - memoryFlushAt?: number; -} - -interface SessionStats { - total: number; - totalTokens: number; - compactions: number; - memoryFlushes: number; -} - -export interface IntelligenceStats { - sessions: SessionStats | null; - memoryFiles: number | null; - entities: Record | null; - entityError: boolean; - aiStatus: AIStatus; - isLoading: boolean; - refetch: () => void; -} - -/** Derive entity-type counts from local graph relations. */ -function entityCountsFromRelations(relations: GraphRelation[]): Record { - const counts: Record = {}; - for (const rel of relations) { - const types = (rel.attrs?.entity_types ?? {}) as Record; - const subjectType = types.subject ?? 'entity'; - const objectType = types.object ?? 'entity'; - counts[subjectType] = (counts[subjectType] ?? 0) + 1; - counts[objectType] = (counts[objectType] ?? 0) + 1; - } - return counts; -} - -export function useIntelligenceStats(): IntelligenceStats { - const [aiStatus, setAiStatus] = useState('idle'); - const [sessions, setSessions] = useState(null); - const [memoryFiles, setMemoryFiles] = useState(null); - const [entities, setEntities] = useState | null>(null); - const [entityError, setEntityError] = useState(false); - const [isLoading, setIsLoading] = useState(true); - - const fetchStats = useCallback(async () => { - setAiStatus('initializing'); - setIsLoading(true); - let hasSuccess = false; - - // Fetch local stats (Tauri invoke) - try { - const index = await callCoreRpc>({ - method: 'ai.sessions_load_index', - }); - const entries = Object.values(index); - setSessions({ - total: entries.length, - totalTokens: entries.reduce((sum, e) => sum + (e.totalTokens || 0), 0), - compactions: entries.reduce((sum, e) => sum + (e.compactionCount || 0), 0), - memoryFlushes: entries.filter(e => e.memoryFlushAt).length, - }); - hasSuccess = true; - } catch { - setSessions(null); - } - - try { - // Empty string lists the memory root; the resolver joins it - // onto `/memory/`, so passing 'memory' here would - // double up to `/memory/memory` and miss the dir. - const files = await aiListMemoryFiles(''); - setMemoryFiles(files.length); - hasSuccess = true; - } catch { - setMemoryFiles(null); - } - - // Derive entity counts from local graph store - try { - const relations = await memoryGraphQuery(); - const counts = entityCountsFromRelations(relations); - if (Object.keys(counts).length > 0) { - setEntities(counts); - setEntityError(false); - } else { - setEntities(null); - setEntityError(false); - } - hasSuccess = true; - } catch { - setEntities(null); - setEntityError(true); - } - - setAiStatus(hasSuccess ? 'ready' : 'error'); - setIsLoading(false); - }, []); - - useEffect(() => { - void fetchStats(); - const intervalId = window.setInterval(() => { - void fetchStats(); - }, POLL_MS); - - return () => { - window.clearInterval(intervalId); - }; - }, [fetchStats]); - - return { sessions, memoryFiles, entities, entityError, aiStatus, isLoading, refetch: fetchStats }; -} diff --git a/app/src/hooks/useScreenIntelligenceItems.ts b/app/src/hooks/useScreenIntelligenceItems.ts deleted file mode 100644 index 694cf74d29..0000000000 --- a/app/src/hooks/useScreenIntelligenceItems.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useMemo } from 'react'; - -import { useScreenIntelligenceState } from '../features/screen-intelligence/useScreenIntelligenceState'; -import type { ActionableItem, ActionableItemPriority } from '../types/intelligence'; - -function confidenceToPriority(confidence: number): ActionableItemPriority { - if (confidence > 0.9) return 'critical'; - if (confidence > 0.7) return 'important'; - return 'normal'; -} - -export function useScreenIntelligenceItems() { - const { recentVisionSummaries, isLoadingVision, refreshVision } = useScreenIntelligenceState({ - loadVision: true, - visionLimit: 20, - pollMs: 2000, - }); - - const items: ActionableItem[] = useMemo(() => { - return recentVisionSummaries.map(summary => ({ - id: `si-${summary.id}`, - title: summary.actionable_notes.slice(0, 120), - description: [summary.ui_state, summary.key_text].filter(Boolean).join(' - '), - source: 'ai_insight' as const, - priority: confidenceToPriority(summary.confidence), - status: 'active' as const, - createdAt: new Date(summary.captured_at_ms), - updatedAt: new Date(summary.captured_at_ms), - actionable: true, - sourceLabel: summary.app_name ?? 'Screen Intelligence', - })); - }, [recentVisionSummaries]); - - return { items, loading: isLoadingVision, refresh: () => refreshVision(20) }; -}