diff --git a/apps/x/apps/main/src/composio-handler.ts b/apps/x/apps/main/src/composio-handler.ts index cace8c74..afae1387 100644 --- a/apps/x/apps/main/src/composio-handler.ts +++ b/apps/x/apps/main/src/composio-handler.ts @@ -277,6 +277,13 @@ export async function useComposioForGoogle(): Promise<{ enabled: boolean }> { return { enabled: await composioClient.useComposioForGoogle() }; } +/** + * Check if Composio should be used for Google Calendar + */ +export async function useComposioForGoogleCalendar(): Promise<{ enabled: boolean }> { + return { enabled: await composioClient.useComposioForGoogleCalendar() }; +} + /** * Execute a Composio action */ diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index 35856e08..aca3024e 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -546,6 +546,9 @@ export function setupIpcHandlers() { 'composio:use-composio-for-google': async () => { return composioHandler.useComposioForGoogle(); }, + 'composio:use-composio-for-google-calendar': async () => { + return composioHandler.useComposioForGoogleCalendar(); + }, // Agent schedule handlers 'agent-schedule:getConfig': async () => { const repo = container.resolve('agentScheduleRepo'); diff --git a/apps/x/apps/renderer/src/components/connectors-popover.tsx b/apps/x/apps/renderer/src/components/connectors-popover.tsx index 268fcfe7..54e312f9 100644 --- a/apps/x/apps/renderer/src/components/connectors-popover.tsx +++ b/apps/x/apps/renderer/src/components/connectors-popover.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { useState, useEffect, useCallback } from "react" -import { AlertTriangle, Loader2, Mic, Mail, MessageSquare, User } from "lucide-react" +import { AlertTriangle, Loader2, Mic, Mail, Calendar, MessageSquare, User } from "lucide-react" import { Popover, @@ -75,6 +75,12 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha const [gmailLoading, setGmailLoading] = useState(true) const [gmailConnecting, setGmailConnecting] = useState(false) + // Composio/Google Calendar state + const [useComposioForGoogleCalendar, setUseComposioForGoogleCalendar] = useState(false) + const [googleCalendarConnected, setGoogleCalendarConnected] = useState(false) + const [googleCalendarLoading, setGoogleCalendarLoading] = useState(true) + const [googleCalendarConnecting, setGoogleCalendarConnecting] = useState(false) + // Load available providers and composio-for-google flag on mount useEffect(() => { async function loadProviders() { @@ -97,8 +103,17 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha console.error('Failed to check composio-for-google flag:', error) } } + async function loadComposioForGoogleCalendarFlag() { + try { + const result = await window.ipc.invoke('composio:use-composio-for-google-calendar', null) + setUseComposioForGoogleCalendar(result.enabled) + } catch (error) { + console.error('Failed to check composio-for-google-calendar flag:', error) + } + } loadProviders() loadComposioForGoogleFlag() + loadComposioForGoogleCalendarFlag() }, []) // Load Granola config @@ -184,6 +199,20 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha } }, []) + // Load Google Calendar connection status + const refreshGoogleCalendarStatus = useCallback(async () => { + try { + setGoogleCalendarLoading(true) + const result = await window.ipc.invoke('composio:get-connection-status', { toolkitSlug: 'googlecalendar' }) + setGoogleCalendarConnected(result.isConnected) + } catch (error) { + console.error('Failed to load Google Calendar status:', error) + setGoogleCalendarConnected(false) + } finally { + setGoogleCalendarLoading(false) + } + }, []) + // Connect to Gmail via Composio const startGmailConnect = useCallback(async () => { try { @@ -212,6 +241,52 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha await startGmailConnect() }, [startGmailConnect]) + // Connect to Google Calendar via Composio + const startGoogleCalendarConnect = useCallback(async () => { + try { + setGoogleCalendarConnecting(true) + const result = await window.ipc.invoke('composio:initiate-connection', { toolkitSlug: 'googlecalendar' }) + if (!result.success) { + toast.error(result.error || 'Failed to connect to Google Calendar') + setGoogleCalendarConnecting(false) + } + } catch (error) { + console.error('Failed to connect to Google Calendar:', error) + toast.error('Failed to connect to Google Calendar') + setGoogleCalendarConnecting(false) + } + }, []) + + // Handle Google Calendar connect button click + const handleConnectGoogleCalendar = useCallback(async () => { + const configResult = await window.ipc.invoke('composio:is-configured', null) + if (!configResult.configured) { + setComposioApiKeyTarget('gmail') + setComposioApiKeyOpen(true) + return + } + await startGoogleCalendarConnect() + }, [startGoogleCalendarConnect]) + + // Disconnect from Google Calendar + const handleDisconnectGoogleCalendar = useCallback(async () => { + try { + setGoogleCalendarLoading(true) + const result = await window.ipc.invoke('composio:disconnect', { toolkitSlug: 'googlecalendar' }) + if (result.success) { + setGoogleCalendarConnected(false) + toast.success('Disconnected from Google Calendar') + } else { + toast.error('Failed to disconnect from Google Calendar') + } + } catch (error) { + console.error('Failed to disconnect from Google Calendar:', error) + toast.error('Failed to disconnect from Google Calendar') + } finally { + setGoogleCalendarLoading(false) + } + }, []) + // Disconnect from Gmail const handleDisconnectGmail = useCallback(async () => { try { @@ -292,6 +367,11 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha refreshGmailStatus() } + // Refresh Google Calendar Composio status if enabled + if (useComposioForGoogleCalendar) { + refreshGoogleCalendarStatus() + } + // Refresh OAuth providers if (providers.length === 0) return @@ -328,7 +408,7 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha } setProviderStates(newStates) - }, [providers, refreshGranolaConfig, refreshSlackConfig, refreshGmailStatus, useComposioForGoogle]) + }, [providers, refreshGranolaConfig, refreshSlackConfig, refreshGmailStatus, useComposioForGoogle, refreshGoogleCalendarStatus, useComposioForGoogleCalendar]) // Refresh statuses when popover opens or providers list changes useEffect(() => { @@ -372,7 +452,7 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha return cleanup }, [refreshAllStatuses]) - // Listen for Composio connection events (Gmail) + // Listen for Composio connection events (Gmail, Google Calendar) useEffect(() => { const cleanup = window.ipc.on('composio:didConnect', (event) => { const { toolkitSlug, success, error } = event @@ -390,6 +470,17 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha toast.error(error || 'Failed to connect to Gmail') } } + + if (toolkitSlug === 'googlecalendar') { + setGoogleCalendarConnected(success) + setGoogleCalendarConnecting(false) + + if (success) { + toast.success('Connected to Google Calendar') + } else { + toast.error(error || 'Failed to connect to Google Calendar') + } + } }) return cleanup @@ -640,11 +731,11 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha )} {/* Email & Calendar Section */} - {(useComposioForGoogle || providers.includes('google')) && ( + {(useComposioForGoogle || useComposioForGoogleCalendar || providers.includes('google')) && ( <>
- {useComposioForGoogle ? 'Email' : 'Email & Calendar'} + Email & Calendar
{useComposioForGoogle ? ( @@ -696,6 +787,53 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha ) : ( renderOAuthProvider('google', 'Google', , 'Sync emails and calendar') )} + {useComposioForGoogleCalendar && ( +
+
+
+ +
+
+ Google Calendar + {googleCalendarLoading ? ( + Checking... + ) : ( + + Sync calendar events + + )} +
+
+
+ {googleCalendarLoading ? ( + + ) : googleCalendarConnected ? ( + + ) : ( + + )} +
+
+ )} )} diff --git a/apps/x/apps/renderer/src/components/onboarding-modal.tsx b/apps/x/apps/renderer/src/components/onboarding-modal.tsx index 9060a669..a9da3ec1 100644 --- a/apps/x/apps/renderer/src/components/onboarding-modal.tsx +++ b/apps/x/apps/renderer/src/components/onboarding-modal.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { useState, useEffect, useCallback } from "react" -import { Loader2, Mic, Mail, CheckCircle2, MessageSquare } from "lucide-react" +import { Loader2, Mic, Mail, Calendar, CheckCircle2, MessageSquare } from "lucide-react" import { Dialog, @@ -99,6 +99,12 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { const [gmailLoading, setGmailLoading] = useState(true) const [gmailConnecting, setGmailConnecting] = useState(false) + // Composio/Google Calendar state + const [useComposioForGoogleCalendar, setUseComposioForGoogleCalendar] = useState(false) + const [googleCalendarConnected, setGoogleCalendarConnected] = useState(false) + const [googleCalendarLoading, setGoogleCalendarLoading] = useState(true) + const [googleCalendarConnecting, setGoogleCalendarConnecting] = useState(false) + const updateProviderConfig = useCallback( (provider: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string }>) => { setProviderConfigs(prev => ({ @@ -150,8 +156,17 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { console.error('Failed to check composio-for-google flag:', error) } } + async function loadComposioForGoogleCalendarFlag() { + try { + const result = await window.ipc.invoke('composio:use-composio-for-google-calendar', null) + setUseComposioForGoogleCalendar(result.enabled) + } catch (error) { + console.error('Failed to check composio-for-google-calendar flag:', error) + } + } loadProviders() loadComposioForGoogleFlag() + loadComposioForGoogleCalendarFlag() }, [open]) // Load LLM models catalog on open @@ -288,6 +303,20 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { } }, []) + // Load Google Calendar connection status + const refreshGoogleCalendarStatus = useCallback(async () => { + try { + setGoogleCalendarLoading(true) + const result = await window.ipc.invoke('composio:get-connection-status', { toolkitSlug: 'googlecalendar' }) + setGoogleCalendarConnected(result.isConnected) + } catch (error) { + console.error('Failed to load Google Calendar status:', error) + setGoogleCalendarConnected(false) + } finally { + setGoogleCalendarLoading(false) + } + }, []) + // Connect to Gmail via Composio const startGmailConnect = useCallback(async () => { try { @@ -315,6 +344,33 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { await startGmailConnect() }, [startGmailConnect]) + // Connect to Google Calendar via Composio + const startGoogleCalendarConnect = useCallback(async () => { + try { + setGoogleCalendarConnecting(true) + const result = await window.ipc.invoke('composio:initiate-connection', { toolkitSlug: 'googlecalendar' }) + if (!result.success) { + toast.error(result.error || 'Failed to connect to Google Calendar') + setGoogleCalendarConnecting(false) + } + } catch (error) { + console.error('Failed to connect to Google Calendar:', error) + toast.error('Failed to connect to Google Calendar') + setGoogleCalendarConnecting(false) + } + }, []) + + // Handle Google Calendar connect button click + const handleConnectGoogleCalendar = useCallback(async () => { + const configResult = await window.ipc.invoke('composio:is-configured', null) + if (!configResult.configured) { + setComposioApiKeyTarget('gmail') + setComposioApiKeyOpen(true) + return + } + await startGoogleCalendarConnect() + }, [startGoogleCalendarConnect]) + // Handle Composio API key submission const handleComposioApiKeySubmit = useCallback(async (apiKey: string) => { try { @@ -420,6 +476,11 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { refreshGmailStatus() } + // Refresh Google Calendar Composio status if enabled + if (useComposioForGoogleCalendar) { + refreshGoogleCalendarStatus() + } + // Refresh OAuth providers if (providers.length === 0) return @@ -447,7 +508,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { } setProviderStates(newStates) - }, [providers, refreshGranolaConfig, refreshSlackConfig, refreshGmailStatus, useComposioForGoogle]) + }, [providers, refreshGranolaConfig, refreshSlackConfig, refreshGmailStatus, useComposioForGoogle, refreshGoogleCalendarStatus, useComposioForGoogleCalendar]) // Refresh statuses when modal opens or providers list changes useEffect(() => { @@ -481,7 +542,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { return cleanup }, []) - // Listen for Composio connection events (Gmail) + // Listen for Composio connection events (Gmail, Google Calendar) useEffect(() => { const cleanup = window.ipc.on('composio:didConnect', (event) => { const { toolkitSlug, success, error } = event @@ -499,6 +560,17 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { toast.error(error || 'Failed to connect to Gmail') } } + + if (toolkitSlug === 'googlecalendar') { + setGoogleCalendarConnected(success) + setGoogleCalendarConnecting(false) + + if (success) { + toast.success('Connected to Google Calendar') + } else { + toast.error(error || 'Failed to connect to Google Calendar') + } + } }) return cleanup @@ -691,6 +763,50 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { ) + // Render Google Calendar Composio row + const renderGoogleCalendarRow = () => ( +
+
+
+ +
+
+ Google Calendar + {googleCalendarLoading ? ( + Checking... + ) : ( + + Sync calendar events + + )} +
+
+
+ {googleCalendarLoading ? ( + + ) : googleCalendarConnected ? ( +
+ + Connected +
+ ) : ( + + )} +
+
+ ) + // Render Slack row const renderSlackRow = () => (
@@ -983,17 +1099,18 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { ) : ( <> {/* Email / Email & Calendar Section */} - {(useComposioForGoogle || providers.includes('google')) && ( + {(useComposioForGoogle || useComposioForGoogleCalendar || providers.includes('google')) && (
- {useComposioForGoogle ? 'Email' : 'Email & Calendar'} + {(useComposioForGoogle || useComposioForGoogleCalendar) ? 'Email & Calendar' : 'Email & Calendar'}
{useComposioForGoogle ? renderGmailRow() : renderOAuthProvider('google', 'Google', , 'Sync emails and calendar events') } + {useComposioForGoogleCalendar && renderGoogleCalendarRow()}
)} @@ -1030,7 +1147,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { // Step 2: Completion const renderCompletionStep = () => { - const hasConnections = connectedProviders.length > 0 || granolaEnabled || slackEnabled || gmailConnected + const hasConnections = connectedProviders.length > 0 || granolaEnabled || slackEnabled || gmailConnected || googleCalendarConnected return (
@@ -1059,6 +1176,12 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { Gmail (Email)
)} + {googleCalendarConnected && ( +
+ + Google Calendar +
+ )} {connectedProviders.includes('google') && (
diff --git a/apps/x/packages/core/src/composio/client.ts b/apps/x/packages/core/src/composio/client.ts index f1aeee7e..3602bef6 100644 --- a/apps/x/packages/core/src/composio/client.ts +++ b/apps/x/packages/core/src/composio/client.ts @@ -49,6 +49,7 @@ async function getAuthHeaders(): Promise> { const ZComposioConfig = z.object({ apiKey: z.string().optional(), use_composio_for_google: z.boolean().optional(), + use_composio_for_google_calendar: z.boolean().optional(), }); type ComposioConfig = z.infer; @@ -113,6 +114,15 @@ export async function useComposioForGoogle(): Promise { return config.use_composio_for_google === true; } +/** + * Check if Composio should be used for Google Calendar + */ +export async function useComposioForGoogleCalendar(): Promise { + if (await isSignedIn()) return true; + const config = loadConfig(); + return config.use_composio_for_google_calendar === true; +} + /** * Make an API call to Composio */ diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 557df845..b7b53a5d 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -386,6 +386,12 @@ const ipcSchemas = { enabled: z.boolean(), }), }, + 'composio:use-composio-for-google-calendar': { + req: z.null(), + res: z.object({ + enabled: z.boolean(), + }), + }, 'composio:didConnect': { req: z.object({ toolkitSlug: z.string(),