diff --git a/apps/x/apps/main/src/composio-handler.ts b/apps/x/apps/main/src/composio-handler.ts
index e5b25d1a..f72613ad 100644
--- a/apps/x/apps/main/src/composio-handler.ts
+++ b/apps/x/apps/main/src/composio-handler.ts
@@ -3,6 +3,7 @@ import { createAuthServer } from './auth-server.js';
import * as composioClient from '@x/core/dist/composio/client.js';
import { composioAccountsRepo } from '@x/core/dist/composio/repo.js';
import type { LocalConnectedAccount } from '@x/core/dist/composio/types.js';
+import { triggerSync as triggerGmailSync } from '@x/core/dist/knowledge/sync_gmail.js';
const REDIRECT_URI = 'http://localhost:8081/oauth/callback';
@@ -151,6 +152,9 @@ export async function initiateConnection(toolkitSlug: string): Promise<{
if (accountStatus.status === 'ACTIVE') {
emitComposioEvent({ toolkitSlug, success: true });
+ if (toolkitSlug === 'gmail') {
+ triggerGmailSync();
+ }
} else {
emitComposioEvent({
toolkitSlug,
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/main/src/oauth-handler.ts b/apps/x/apps/main/src/oauth-handler.ts
index 5b55e8b7..58ab0809 100644
--- a/apps/x/apps/main/src/oauth-handler.ts
+++ b/apps/x/apps/main/src/oauth-handler.ts
@@ -7,7 +7,6 @@ import { getProviderConfig, getAvailableProviders } from '@x/core/dist/auth/prov
import container from '@x/core/dist/di/container.js';
import { IOAuthRepo } from '@x/core/dist/auth/repo.js';
import { IClientRegistrationRepo } from '@x/core/dist/auth/client-repo.js';
-import { triggerSync as triggerGmailSync } from '@x/core/dist/knowledge/sync_gmail.js';
import { triggerSync as triggerCalendarSync } from '@x/core/dist/knowledge/sync_calendar.js';
import { triggerSync as triggerFirefliesSync } from '@x/core/dist/knowledge/sync_fireflies.js';
import { emitOAuthEvent } from './ipc.js';
@@ -194,7 +193,6 @@ export async function connectProvider(provider: string): Promise<{ success: bool
// Trigger immediate sync for relevant providers
if (provider === 'google') {
- triggerGmailSync();
triggerCalendarSync();
} else if (provider === 'fireflies-ai') {
triggerFirefliesSync();
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}
/>