diff --git a/app/src/components/intelligence/ScreenIntelligenceDebugPanel.tsx b/app/src/components/intelligence/ScreenIntelligenceDebugPanel.tsx index 700b1b259..855f09138 100644 --- a/app/src/components/intelligence/ScreenIntelligenceDebugPanel.tsx +++ b/app/src/components/intelligence/ScreenIntelligenceDebugPanel.tsx @@ -68,11 +68,15 @@ const ScreenIntelligenceDebugPanelContent = ({

Permissions

-
- - - -
+ {status?.platform_supported === false ? ( +

Platform not supported

+ ) : ( +
+ + + +
+ )} {/* Session Status */} diff --git a/app/src/components/intelligence/__tests__/ScreenIntelligenceDebugPanel.test.tsx b/app/src/components/intelligence/__tests__/ScreenIntelligenceDebugPanel.test.tsx index cbefe7947..f4dd46e26 100644 --- a/app/src/components/intelligence/__tests__/ScreenIntelligenceDebugPanel.test.tsx +++ b/app/src/components/intelligence/__tests__/ScreenIntelligenceDebugPanel.test.tsx @@ -117,6 +117,20 @@ describe('ScreenIntelligenceDebugPanel', () => { ); }); + it('shows "Platform not supported" and hides permission dots when platform_supported is false', () => { + const state: ScreenIntelligenceState = { + ...baseState, + status: { ...baseState.status!, platform_supported: false }, + }; + + render(); + + expect(screen.getByText('Platform not supported')).toBeInTheDocument(); + expect(screen.queryByText('Screen')).not.toBeInTheDocument(); + expect(screen.queryByText('Accessibility')).not.toBeInTheDocument(); + expect(screen.queryByText('Input')).not.toBeInTheDocument(); + }); + it('renders capture failures without breaking the diagnostics panel', async () => { const state: ScreenIntelligenceState = { ...baseState, diff --git a/app/src/components/skills/ScreenIntelligenceSetupModal.tsx b/app/src/components/skills/ScreenIntelligenceSetupModal.tsx index 66a3efef6..6de1c22df 100644 --- a/app/src/components/skills/ScreenIntelligenceSetupModal.tsx +++ b/app/src/components/skills/ScreenIntelligenceSetupModal.tsx @@ -147,6 +147,54 @@ export default function ScreenIntelligenceSetupModal({ onClose, initialStep }: P navigate('/settings/screen-intelligence'); }; + if (status?.platform_supported === false) { + return createPortal( +
{ + if (e.target === e.currentTarget) onClose(); + }}> +
+
+
+
+ + + +
+

Screen Intelligence

+
+ +
+
+

+ Screen Intelligence is currently available on macOS only. +

+ +
+
+
, + document.body + ); + } + return createPortal(
({ + useScreenIntelligenceState: vi.fn(), +})); + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual>('react-router-dom'); + return { ...actual, useNavigate: vi.fn(() => vi.fn()) }; +}); + +vi.mock('../../../utils/tauriCommands', async () => { + const actual = await vi.importActual>('../../../utils/tauriCommands'); + return { + ...actual, + openhumanUpdateScreenIntelligenceSettings: vi.fn().mockResolvedValue(undefined), + }; +}); + +const baseState: ScreenIntelligenceState = { + status: { + platform_supported: true, + permissions: { + screen_recording: 'unknown', + accessibility: 'unknown', + input_monitoring: 'unknown', + }, + features: { screen_monitoring: false }, + session: { + active: false, + started_at_ms: null, + expires_at_ms: null, + remaining_ms: null, + ttl_secs: 300, + panic_hotkey: 'Cmd+Shift+.', + stop_reason: null, + frames_in_memory: 0, + last_capture_at_ms: null, + last_context: null, + vision_enabled: true, + vision_state: 'idle', + vision_queue_depth: 0, + last_vision_at_ms: null, + last_vision_summary: null, + }, + config: { + enabled: false, + capture_policy: 'hybrid', + policy_mode: 'all_except_blacklist', + baseline_fps: 1, + vision_enabled: true, + session_ttl_secs: 300, + panic_stop_hotkey: 'Cmd+Shift+.', + autocomplete_enabled: true, + use_vision_model: true, + keep_screenshots: false, + allowlist: [], + denylist: [], + }, + denylist: [], + is_context_blocked: false, + }, + lastRestartSummary: null, + recentVisionSummaries: [], + captureTestResult: null, + isCaptureTestRunning: false, + isLoading: false, + isRequestingPermissions: false, + isRestartingCore: false, + isStartingSession: false, + isStoppingSession: false, + isLoadingVision: false, + isFlushingVision: false, + lastError: null, + refreshStatus: vi.fn().mockResolvedValue(null), + requestPermission: vi.fn().mockResolvedValue(null), + refreshPermissionsWithRestart: vi.fn().mockResolvedValue(null), + startSession: vi.fn().mockResolvedValue(null), + stopSession: vi.fn().mockResolvedValue(null), + refreshVision: vi.fn().mockResolvedValue([]), + flushVision: vi.fn().mockResolvedValue(undefined), + runCaptureTest: vi.fn().mockResolvedValue(undefined), + clearError: vi.fn(), +}; + +describe('ScreenIntelligenceSetupModal', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('shows macOS-only message and a Close button when platform_supported is false', () => { + vi.mocked(useScreenIntelligenceState).mockReturnValue({ + ...baseState, + status: { ...baseState.status!, platform_supported: false }, + }); + + render(); + + expect(screen.getByText(/macOS only/i)).toBeInTheDocument(); + expect(screen.getByText('Close', { selector: 'button' })).toBeInTheDocument(); + expect(screen.queryByText('Grant permissions')).not.toBeInTheDocument(); + expect(screen.queryByText('Screen Recording')).not.toBeInTheDocument(); + }); + + it('calls onClose when the Close button is clicked on the unsupported-platform screen', () => { + const onClose = vi.fn(); + vi.mocked(useScreenIntelligenceState).mockReturnValue({ + ...baseState, + status: { ...baseState.status!, platform_supported: false }, + }); + + render(); + + screen.getByText('Close', { selector: 'button' }).click(); + + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('renders the permissions setup flow when platform_supported is true', () => { + vi.mocked(useScreenIntelligenceState).mockReturnValue(baseState); + + render(); + + expect(screen.getByText('Grant permissions')).toBeInTheDocument(); + expect(screen.getByText('Screen Recording')).toBeInTheDocument(); + expect(screen.getByText('Accessibility')).toBeInTheDocument(); + expect(screen.getByText('Input Monitoring')).toBeInTheDocument(); + expect(screen.queryByText(/macOS only/i)).not.toBeInTheDocument(); + }); +});