From 1899c95c0afe96eeacc4bb087f6e404f00397ce5 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:41:20 +0530 Subject: [PATCH] added composio oauth for gmail and removed calendar from sync --- apps/x/apps/main/src/main.ts | 5 +- .../src/components/connectors-popover.tsx | 159 +++++++++++++++--- .../src/components/onboarding-modal.tsx | 138 ++++++++++++--- .../packages/core/src/knowledge/sync_gmail.ts | 20 ++- 4 files changed, 273 insertions(+), 49 deletions(-) diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts index 6ddab7bc..c276301b 100644 --- a/apps/x/apps/main/src/main.ts +++ b/apps/x/apps/main/src/main.ts @@ -5,7 +5,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import { dirname } from "node:path"; import { updateElectronApp, UpdateSourceType } from "update-electron-app"; import { init as initGmailSync } from "@x/core/dist/knowledge/sync_gmail.js"; -import { init as initCalendarSync } from "@x/core/dist/knowledge/sync_calendar.js"; + import { init as initFirefliesSync } from "@x/core/dist/knowledge/sync_fireflies.js"; import { init as initGranolaSync } from "@x/core/dist/knowledge/granola/sync.js"; import { init as initGraphBuilder } from "@x/core/dist/knowledge/build_graph.js"; @@ -134,9 +134,6 @@ app.whenReady().then(async () => { // start gmail sync initGmailSync(); - // start calendar sync - initCalendarSync(); - // start fireflies sync initFirefliesSync(); diff --git a/apps/x/apps/renderer/src/components/connectors-popover.tsx b/apps/x/apps/renderer/src/components/connectors-popover.tsx index 1799ab75..882a8d48 100644 --- a/apps/x/apps/renderer/src/components/connectors-popover.tsx +++ b/apps/x/apps/renderer/src/components/connectors-popover.tsx @@ -41,8 +41,12 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps) const [granolaEnabled, setGranolaEnabled] = useState(false) const [granolaLoading, setGranolaLoading] = useState(true) - // Composio/Slack state + // Composio state (Gmail + Slack) const [composioApiKeyOpen, setComposioApiKeyOpen] = useState(false) + const [composioApiKeyTarget, setComposioApiKeyTarget] = useState<'gmail' | 'slack'>('gmail') + const [gmailConnected, setGmailConnected] = useState(false) + const [gmailLoading, setGmailLoading] = useState(true) + const [gmailConnecting, setGmailConnecting] = useState(false) const [slackConnected, setSlackConnected] = useState(false) const [slackLoading, setSlackLoading] = useState(true) const [slackConnecting, setSlackConnecting] = useState(false) @@ -93,6 +97,20 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps) } }, []) + // Load Gmail connection status + const refreshGmailStatus = useCallback(async () => { + try { + setGmailLoading(true) + const result = await window.ipc.invoke('composio:get-connection-status', { toolkitSlug: 'gmail' }) + setGmailConnected(result.isConnected) + } catch (error) { + console.error('Failed to load Gmail status:', error) + setGmailConnected(false) + } finally { + setGmailLoading(false) + } + }, []) + // Load Slack connection status const refreshSlackStatus = useCallback(async () => { try { @@ -107,6 +125,53 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps) } }, []) + // Connect to Gmail via Composio + const startGmailConnect = useCallback(async () => { + try { + setGmailConnecting(true) + const result = await window.ipc.invoke('composio:initiate-connection', { toolkitSlug: 'gmail' }) + if (!result.success) { + toast.error(result.error || 'Failed to connect to Gmail') + setGmailConnecting(false) + } + // Success will be handled by composio:didConnect event + } catch (error) { + console.error('Failed to connect to Gmail:', error) + toast.error('Failed to connect to Gmail') + setGmailConnecting(false) + } + }, []) + + // Handle Gmail connect button click + const handleConnectGmail = useCallback(async () => { + const configResult = await window.ipc.invoke('composio:is-configured', null) + if (!configResult.configured) { + setComposioApiKeyTarget('gmail') + setComposioApiKeyOpen(true) + return + } + await startGmailConnect() + }, [startGmailConnect]) + + // Disconnect from Gmail + const handleDisconnectGmail = useCallback(async () => { + try { + setGmailLoading(true) + const result = await window.ipc.invoke('composio:disconnect', { toolkitSlug: 'gmail' }) + if (result.success) { + setGmailConnected(false) + toast.success('Disconnected from Gmail') + } else { + toast.error('Failed to disconnect from Gmail') + } + } catch (error) { + console.error('Failed to disconnect from Gmail:', error) + toast.error('Failed to disconnect from Gmail') + } finally { + setGmailLoading(false) + } + }, []) + // Connect to Slack via Composio const startSlackConnect = useCallback(async () => { try { @@ -126,9 +191,9 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps) // Handle Slack connect button click const handleConnectSlack = useCallback(async () => { - // Check if Composio is configured const configResult = await window.ipc.invoke('composio:is-configured', null) if (!configResult.configured) { + setComposioApiKeyTarget('slack') setComposioApiKeyOpen(true) return } @@ -141,13 +206,17 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps) await window.ipc.invoke('composio:set-api-key', { apiKey }) setComposioApiKeyOpen(false) toast.success('Composio API key saved') - // Now start the Slack connection - await startSlackConnect() + // Start the connection for whichever toolkit triggered the API key prompt + if (composioApiKeyTarget === 'gmail') { + await startGmailConnect() + } else { + await startSlackConnect() + } } catch (error) { console.error('Failed to save Composio API key:', error) toast.error('Failed to save API key') } - }, [startSlackConnect]) + }, [composioApiKeyTarget, startGmailConnect, startSlackConnect]) // Disconnect from Slack const handleDisconnectSlack = useCallback(async () => { @@ -173,7 +242,8 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps) // Refresh Granola refreshGranolaConfig() - // Refresh Slack status + // Refresh Composio connections + refreshGmailStatus() refreshSlackStatus() // Refresh OAuth providers @@ -202,7 +272,7 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps) ) setProviderStates(newStates) - }, [providers, refreshGranolaConfig, refreshSlackStatus]) + }, [providers, refreshGranolaConfig, refreshGmailStatus, refreshSlackStatus]) // Refresh statuses when popover opens or providers list changes useEffect(() => { @@ -227,7 +297,7 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps) if (success) { const displayName = provider === 'fireflies-ai' ? 'Fireflies' : provider.charAt(0).toUpperCase() + provider.slice(1) - // Show detailed message for Google and Fireflies (includes sync info) + // Show detailed message for providers that sync in background if (provider === 'google' || provider === 'fireflies-ai') { toast.success(`Connected to ${displayName}`, { description: 'Syncing your data in the background. This may take a few minutes before changes appear.', @@ -251,7 +321,19 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps) const cleanup = window.ipc.on('composio:didConnect', (event) => { const { toolkitSlug, success, error } = event - if (toolkitSlug === 'slack') { + if (toolkitSlug === 'gmail') { + setGmailConnected(success) + setGmailConnecting(false) + + if (success) { + toast.success('Connected to Gmail', { + description: 'Syncing your emails in the background. This may take a few minutes.', + duration: 8000, + }) + } else { + toast.error(error || 'Failed to connect to Gmail') + } + } else if (toolkitSlug === 'slack') { setSlackConnected(success) setSlackConnecting(false) @@ -431,16 +513,55 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps) ) : ( <> - {/* Email & Calendar Section - Google */} - {providers.includes('google') && ( - <> -
- Email & Calendar + {/* Email Section - Gmail via Composio */} +
+ Email +
+
+
+
+
- {renderOAuthProvider('google', 'Google', , 'Sync emails and calendar')} - - - )} +
+ Gmail + {gmailLoading ? ( + Checking... + ) : ( + Sync emails + )} +
+
+
+ {gmailLoading ? ( + + ) : gmailConnected ? ( + + ) : ( + + )} +
+
+ + {/* Meeting Notes Section - Granola & Fireflies */}
@@ -537,7 +658,7 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps) open={composioApiKeyOpen} onOpenChange={setComposioApiKeyOpen} onSubmit={handleComposioApiKeySubmit} - isSubmitting={slackConnecting} + isSubmitting={gmailConnecting || slackConnecting} /> ) diff --git a/apps/x/apps/renderer/src/components/onboarding-modal.tsx b/apps/x/apps/renderer/src/components/onboarding-modal.tsx index 074ad645..0675697c 100644 --- a/apps/x/apps/renderer/src/components/onboarding-modal.tsx +++ b/apps/x/apps/renderer/src/components/onboarding-modal.tsx @@ -42,8 +42,12 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { const [granolaEnabled, setGranolaEnabled] = useState(false) const [granolaLoading, setGranolaLoading] = useState(true) - // Composio/Slack state + // Composio state (Gmail + Slack) const [composioApiKeyOpen, setComposioApiKeyOpen] = useState(false) + const [composioApiKeyTarget, setComposioApiKeyTarget] = useState<'gmail' | 'slack'>('gmail') + const [gmailConnected, setGmailConnected] = useState(false) + const [gmailLoading, setGmailLoading] = useState(true) + const [gmailConnecting, setGmailConnecting] = useState(false) const [slackConnected, setSlackConnected] = useState(false) const [slackLoading, setSlackLoading] = useState(true) const [slackConnecting, setSlackConnecting] = useState(false) @@ -101,6 +105,47 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { } }, []) + // Load Gmail connection status + const refreshGmailStatus = useCallback(async () => { + try { + setGmailLoading(true) + const result = await window.ipc.invoke('composio:get-connection-status', { toolkitSlug: 'gmail' }) + setGmailConnected(result.isConnected) + } catch (error) { + console.error('Failed to load Gmail status:', error) + setGmailConnected(false) + } finally { + setGmailLoading(false) + } + }, []) + + // Connect to Gmail via Composio + const startGmailConnect = useCallback(async () => { + try { + setGmailConnecting(true) + const result = await window.ipc.invoke('composio:initiate-connection', { toolkitSlug: 'gmail' }) + if (!result.success) { + toast.error(result.error || 'Failed to connect to Gmail') + setGmailConnecting(false) + } + } catch (error) { + console.error('Failed to connect to Gmail:', error) + toast.error('Failed to connect to Gmail') + setGmailConnecting(false) + } + }, []) + + // Handle Gmail connect button click + const handleConnectGmail = useCallback(async () => { + const configResult = await window.ipc.invoke('composio:is-configured', null) + if (!configResult.configured) { + setComposioApiKeyTarget('gmail') + setComposioApiKeyOpen(true) + return + } + await startGmailConnect() + }, [startGmailConnect]) + // Load Slack connection status const refreshSlackStatus = useCallback(async () => { try { @@ -134,9 +179,9 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { // Connect to Slack via Composio (checks if configured first) const handleConnectSlack = useCallback(async () => { - // Check if Composio is configured const configResult = await window.ipc.invoke('composio:is-configured', null) if (!configResult.configured) { + setComposioApiKeyTarget('slack') setComposioApiKeyOpen(true) return } @@ -149,20 +194,24 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { await window.ipc.invoke('composio:set-api-key', { apiKey }) setComposioApiKeyOpen(false) toast.success('Composio API key saved') - // Now start the Slack connection - await startSlackConnect() + if (composioApiKeyTarget === 'gmail') { + await startGmailConnect() + } else { + await startSlackConnect() + } } catch (error) { console.error('Failed to save Composio API key:', error) toast.error('Failed to save API key') } - }, [startSlackConnect]) + }, [composioApiKeyTarget, startGmailConnect, startSlackConnect]) // Check connection status for all providers const refreshAllStatuses = useCallback(async () => { // Refresh Granola refreshGranolaConfig() - // Refresh Slack status + // Refresh Composio connections + refreshGmailStatus() refreshSlackStatus() // Refresh OAuth providers @@ -191,7 +240,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { ) setProviderStates(newStates) - }, [providers, refreshGranolaConfig, refreshSlackStatus]) + }, [providers, refreshGranolaConfig, refreshGmailStatus, refreshSlackStatus]) // Refresh statuses when modal opens or providers list changes useEffect(() => { @@ -230,7 +279,16 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { const cleanup = window.ipc.on('composio:didConnect', (event) => { const { toolkitSlug, success, error } = event - if (toolkitSlug === 'slack') { + if (toolkitSlug === 'gmail') { + setGmailConnected(success) + setGmailConnecting(false) + + if (success) { + toast.success('Connected to Gmail') + } else { + toast.error(error || 'Failed to connect to Gmail') + } + } else if (toolkitSlug === 'slack') { setSlackConnected(success) setSlackConnecting(false) @@ -377,6 +435,48 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
) + // Render Gmail row (Composio) + const renderGmailRow = () => ( +
+
+
+ +
+
+ Gmail + {gmailLoading ? ( + Checking... + ) : ( + Sync emails + )} +
+
+
+ {gmailLoading ? ( + + ) : gmailConnected ? ( +
+ + Connected +
+ ) : ( + + )} +
+
+ ) + // Render Slack row const renderSlackRow = () => (
@@ -470,15 +570,13 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
) : ( <> - {/* Email & Calendar Section */} - {providers.includes('google') && ( -
-
- Email & Calendar -
- {renderOAuthProvider('google', 'Google', , 'Sync emails and calendar events')} + {/* Email Section - Gmail via Composio */} +
+
+ Email
- )} + {renderGmailRow()} +
{/* Meeting Notes Section */}
@@ -513,7 +611,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { // Step 2: Completion const CompletionStep = () => { - const hasConnections = connectedProviders.length > 0 || granolaEnabled || slackConnected + const hasConnections = connectedProviders.length > 0 || gmailConnected || granolaEnabled || slackConnected return (
@@ -536,10 +634,10 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {

Connected accounts:

- {connectedProviders.includes('google') && ( + {gmailConnected && (
- Google (Email & Calendar) + Gmail (Email)
)} {connectedProviders.includes('fireflies-ai') && ( @@ -578,7 +676,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { open={composioApiKeyOpen} onOpenChange={setComposioApiKeyOpen} onSubmit={handleComposioApiKeySubmit} - isSubmitting={slackConnecting} + isSubmitting={gmailConnecting || slackConnecting} /> {}}> ): string { * Returns the newest message date (as ISO string) found in the thread, or null. */ async function processThread(connectedAccountId: string, threadId: string, syncDir: string): Promise { - const threadResult = await executeAction( - 'GMAIL_FETCH_MESSAGE_BY_THREAD_ID', - connectedAccountId, - { thread_id: threadId, user_id: 'me' } - ); + let threadResult; + try { + threadResult = await executeAction( + 'GMAIL_FETCH_MESSAGE_BY_THREAD_ID', + connectedAccountId, + { thread_id: threadId, user_id: 'me' } + ); + } catch (error) { + console.warn(`[Gmail] Skipping thread ${threadId} (fetch failed):`, error instanceof Error ? error.message : error); + return null; + } if (!threadResult.success || !threadResult.data) { console.error(`[Gmail] Failed to fetch thread ${threadId}:`, threadResult.error); @@ -233,7 +239,9 @@ async function processThread(connectedAccountId: string, threadId: string, syncD console.log(`[Gmail] Synced Thread: ${firstParsed.subject} (${threadId})`); } - return newestDate ? newestDate.toISOString() : null; + if (!newestDate) return null; + // Add 1 second so the `after:` query (epoch-second granularity) excludes this email next sync + return new Date(newestDate.getTime() + 1000).toISOString(); } async function performSync() {