use composio for google optionally

This commit is contained in:
Arjun 2026-03-02 17:52:23 +05:30
parent d2bb11f104
commit 17bb625ab9
7 changed files with 740 additions and 20 deletions

View file

@ -4,6 +4,7 @@ import * as composioClient from '@x/core/dist/composio/client.js';
import { composioAccountsRepo } from '@x/core/dist/composio/repo.js';
import type { LocalConnectedAccount, ZExecuteActionResponse } from '@x/core/dist/composio/types.js';
import { z } from 'zod';
import { triggerSync as triggerGmailSync } from '@x/core/dist/knowledge/sync_gmail.js';
const REDIRECT_URI = 'http://localhost:8081/oauth/callback';
@ -152,6 +153,9 @@ export async function initiateConnection(toolkitSlug: string): Promise<{
if (accountStatus.status === 'ACTIVE') {
emitComposioEvent({ toolkitSlug, success: true });
if (toolkitSlug === 'gmail') {
triggerGmailSync();
}
} else {
emitComposioEvent({
toolkitSlug,
@ -266,6 +270,13 @@ export function listConnected(): { toolkits: string[] } {
return { toolkits: composioAccountsRepo.getConnectedToolkits() };
}
/**
* Check if Composio should be used for Google services (Gmail, etc.)
*/
export function useComposioForGoogle(): { enabled: boolean } {
return { enabled: composioClient.useComposioForGoogle() };
}
/**
* Execute a Composio action
*/

View file

@ -543,6 +543,9 @@ export function setupIpcHandlers() {
'composio:execute-action': async (_event, args) => {
return composioHandler.executeAction(args.actionSlug, args.toolkitSlug, args.input);
},
'composio:use-composio-for-google': async () => {
return composioHandler.useComposioForGoogle();
},
// Agent schedule handlers
'agent-schedule:getConfig': async () => {
const repo = container.resolve<IAgentScheduleRepo>('agentScheduleRepo');

View file

@ -20,6 +20,7 @@ import { Separator } from "@/components/ui/separator"
import { GoogleClientIdModal } from "@/components/google-client-id-modal"
import { getGoogleClientId, setGoogleClientId, clearGoogleClientId } from "@/lib/google-client-id-store"
import { toast } from "sonner"
import { ComposioApiKeyModal } from "@/components/composio-api-key-modal"
interface ProviderState {
isConnected: boolean
@ -54,6 +55,10 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
const [granolaEnabled, setGranolaEnabled] = useState(false)
const [granolaLoading, setGranolaLoading] = useState(true)
// Composio API key state
const [composioApiKeyOpen, setComposioApiKeyOpen] = useState(false)
const [composioApiKeyTarget, setComposioApiKeyTarget] = useState<'slack' | 'gmail'>('gmail')
// Slack state (agent-slack CLI)
const [slackEnabled, setSlackEnabled] = useState(false)
const [slackLoading, setSlackLoading] = useState(true)
@ -64,7 +69,13 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
const [slackDiscovering, setSlackDiscovering] = useState(false)
const [slackDiscoverError, setSlackDiscoverError] = useState<string | null>(null)
// Load available providers on mount
// Composio/Gmail state
const [useComposioForGoogle, setUseComposioForGoogle] = useState(false)
const [gmailConnected, setGmailConnected] = useState(false)
const [gmailLoading, setGmailLoading] = useState(true)
const [gmailConnecting, setGmailConnecting] = useState(false)
// Load available providers and composio-for-google flag on mount
useEffect(() => {
async function loadProviders() {
try {
@ -78,7 +89,16 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
setProvidersLoading(false)
}
}
async function loadComposioForGoogleFlag() {
try {
const result = await window.ipc.invoke('composio:use-composio-for-google', null)
setUseComposioForGoogle(result.enabled)
} catch (error) {
console.error('Failed to check composio-for-google flag:', error)
}
}
loadProviders()
loadComposioForGoogleFlag()
}, [])
// Load Granola config
@ -150,6 +170,80 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
}
}, [])
// 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)
}
// 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)
}
}, [])
// Handle Composio API key submission
const handleComposioApiKeySubmit = useCallback(async (apiKey: string) => {
try {
await window.ipc.invoke('composio:set-api-key', { apiKey })
setComposioApiKeyOpen(false)
toast.success('Composio API key saved')
await startGmailConnect()
} catch (error) {
console.error('Failed to save Composio API key:', error)
toast.error('Failed to save API key')
}
}, [startGmailConnect])
// Save selected Slack workspaces
const handleSlackSaveWorkspaces = useCallback(async () => {
const selected = slackAvailableWorkspaces.filter(w => slackSelectedUrls.has(w.url))
@ -193,6 +287,11 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
// Refresh Slack config
refreshSlackConfig()
// Refresh Gmail Composio status if enabled
if (useComposioForGoogle) {
refreshGmailStatus()
}
// Refresh OAuth providers
if (providers.length === 0) return
@ -229,7 +328,7 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
}
setProviderStates(newStates)
}, [providers, refreshGranolaConfig, refreshSlackConfig])
}, [providers, refreshGranolaConfig, refreshSlackConfig, refreshGmailStatus, useComposioForGoogle])
// Refresh statuses when popover opens or providers list changes
useEffect(() => {
@ -273,6 +372,30 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
return cleanup
}, [refreshAllStatuses])
// Listen for Composio connection events (Gmail)
useEffect(() => {
const cleanup = window.ipc.on('composio:didConnect', (event) => {
const { toolkitSlug, success, error } = event
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 before changes appear.',
duration: 8000,
})
} else {
toast.error(error || 'Failed to connect to Gmail')
}
}
})
return cleanup
}, [])
const startConnect = useCallback(async (provider: string, clientId?: string) => {
setProviderStates(prev => ({
...prev,
@ -516,13 +639,63 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
</>
)}
{/* Email & Calendar Section - Google */}
{providers.includes('google') && (
{/* Email & Calendar Section */}
{(useComposioForGoogle || providers.includes('google')) && (
<>
<div className="px-2 py-1.5">
<span className="text-xs font-medium text-muted-foreground">Email & Calendar</span>
<span className="text-xs font-medium text-muted-foreground">
{useComposioForGoogle ? 'Email' : 'Email & Calendar'}
</span>
</div>
{renderOAuthProvider('google', 'Google', <Mail className="size-4" />, 'Sync emails and calendar')}
{useComposioForGoogle ? (
<div className="flex items-center justify-between gap-3 rounded-md px-3 py-2 hover:bg-accent">
<div className="flex items-center gap-3 min-w-0">
<div className="flex size-8 items-center justify-center rounded-md bg-muted">
<Mail className="size-4" />
</div>
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium truncate">Gmail</span>
{gmailLoading ? (
<span className="text-xs text-muted-foreground">Checking...</span>
) : (
<span className="text-xs text-muted-foreground truncate">
Sync emails
</span>
)}
</div>
</div>
<div className="shrink-0">
{gmailLoading ? (
<Loader2 className="size-4 animate-spin text-muted-foreground" />
) : gmailConnected ? (
<Button
variant="outline"
size="sm"
onClick={handleDisconnectGmail}
className="h-7 px-2 text-xs"
>
Disconnect
</Button>
) : (
<Button
variant="default"
size="sm"
onClick={handleConnectGmail}
disabled={gmailConnecting}
className="h-7 px-2 text-xs"
>
{gmailConnecting ? (
<Loader2 className="size-3 animate-spin" />
) : (
"Connect"
)}
</Button>
)}
</div>
</div>
) : (
renderOAuthProvider('google', 'Google', <Mail className="size-4" />, 'Sync emails and calendar')
)}
<Separator className="my-2" />
</>
)}
@ -652,6 +825,12 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
</div>
</PopoverContent>
</Popover>
<ComposioApiKeyModal
open={composioApiKeyOpen}
onOpenChange={setComposioApiKeyOpen}
onSubmit={handleComposioApiKeySubmit}
isSubmitting={gmailConnecting}
/>
</>
)
}

View file

@ -25,6 +25,7 @@ import { cn } from "@/lib/utils"
import { GoogleClientIdModal } from "@/components/google-client-id-modal"
import { getGoogleClientId, setGoogleClientId } from "@/lib/google-client-id-store"
import { toast } from "sonner"
import { ComposioApiKeyModal } from "@/components/composio-api-key-modal"
interface ProviderState {
isConnected: boolean
@ -78,6 +79,10 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
const [granolaLoading, setGranolaLoading] = useState(true)
const [showMoreProviders, setShowMoreProviders] = useState(false)
// Composio API key state
const [composioApiKeyOpen, setComposioApiKeyOpen] = useState(false)
const [composioApiKeyTarget, setComposioApiKeyTarget] = useState<'slack' | 'gmail'>('gmail')
// Slack state (agent-slack CLI)
const [slackEnabled, setSlackEnabled] = useState(false)
const [slackLoading, setSlackLoading] = useState(true)
@ -88,6 +93,12 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
const [slackDiscovering, setSlackDiscovering] = useState(false)
const [slackDiscoverError, setSlackDiscoverError] = useState<string | null>(null)
// Composio/Gmail state
const [useComposioForGoogle, setUseComposioForGoogle] = useState(false)
const [gmailConnected, setGmailConnected] = useState(false)
const [gmailLoading, setGmailLoading] = useState(true)
const [gmailConnecting, setGmailConnecting] = useState(false)
const updateProviderConfig = useCallback(
(provider: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string }>) => {
setProviderConfigs(prev => ({
@ -115,7 +126,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
.filter(([, state]) => state.isConnected)
.map(([provider]) => provider)
// Load available providers on mount
// Load available providers and composio-for-google flag on mount
useEffect(() => {
if (!open) return
@ -131,7 +142,16 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
setProvidersLoading(false)
}
}
async function loadComposioForGoogleFlag() {
try {
const result = await window.ipc.invoke('composio:use-composio-for-google', null)
setUseComposioForGoogle(result.enabled)
} catch (error) {
console.error('Failed to check composio-for-google flag:', error)
}
}
loadProviders()
loadComposioForGoogleFlag()
}, [open])
// Load LLM models catalog on open
@ -254,6 +274,60 @@ 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])
// Handle Composio API key submission
const handleComposioApiKeySubmit = useCallback(async (apiKey: string) => {
try {
await window.ipc.invoke('composio:set-api-key', { apiKey })
setComposioApiKeyOpen(false)
toast.success('Composio API key saved')
await startGmailConnect()
} catch (error) {
console.error('Failed to save Composio API key:', error)
toast.error('Failed to save API key')
}
}, [startGmailConnect])
// Save selected Slack workspaces
const handleSlackSaveWorkspaces = useCallback(async () => {
const selected = slackAvailableWorkspaces.filter(w => slackSelectedUrls.has(w.url))
@ -341,6 +415,11 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
// Refresh Slack config
refreshSlackConfig()
// Refresh Gmail Composio status if enabled
if (useComposioForGoogle) {
refreshGmailStatus()
}
// Refresh OAuth providers
if (providers.length === 0) return
@ -368,7 +447,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
}
setProviderStates(newStates)
}, [providers, refreshGranolaConfig, refreshSlackConfig])
}, [providers, refreshGranolaConfig, refreshSlackConfig, refreshGmailStatus, useComposioForGoogle])
// Refresh statuses when modal opens or providers list changes
useEffect(() => {
@ -402,6 +481,30 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
return cleanup
}, [])
// Listen for Composio connection events (Gmail)
useEffect(() => {
const cleanup = window.ipc.on('composio:didConnect', (event) => {
const { toolkitSlug, success, error } = event
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 before changes appear.',
duration: 8000,
})
} else {
toast.error(error || 'Failed to connect to Gmail')
}
}
})
return cleanup
}, [])
const startConnect = useCallback(async (provider: string, clientId?: string) => {
setProviderStates(prev => ({
...prev,
@ -544,6 +647,50 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
</div>
)
// Render Gmail Composio row
const renderGmailRow = () => (
<div className="flex items-center justify-between gap-3 rounded-md px-3 py-3 hover:bg-accent">
<div className="flex items-center gap-3 min-w-0">
<div className="flex size-10 items-center justify-center rounded-md bg-muted">
<Mail className="size-5" />
</div>
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium truncate">Gmail</span>
{gmailLoading ? (
<span className="text-xs text-muted-foreground">Checking...</span>
) : (
<span className="text-xs text-muted-foreground truncate">
Sync emails
</span>
)}
</div>
</div>
<div className="shrink-0">
{gmailLoading ? (
<Loader2 className="size-4 animate-spin text-muted-foreground" />
) : gmailConnected ? (
<div className="flex items-center gap-1.5 text-sm text-green-600">
<CheckCircle2 className="size-4" />
<span>Connected</span>
</div>
) : (
<Button
variant="default"
size="sm"
onClick={handleConnectGmail}
disabled={gmailConnecting}
>
{gmailConnecting ? (
<Loader2 className="size-4 animate-spin" />
) : (
"Connect"
)}
</Button>
)}
</div>
</div>
)
// Render Slack row
const renderSlackRow = () => (
<div className="rounded-md px-3 py-3 hover:bg-accent">
@ -835,13 +982,18 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
</div>
) : (
<>
{/* Email & Calendar Section */}
{providers.includes('google') && (
{/* Email / Email & Calendar Section */}
{(useComposioForGoogle || providers.includes('google')) && (
<div className="space-y-2">
<div className="px-3">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Email & Calendar</span>
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
{useComposioForGoogle ? 'Email' : 'Email & Calendar'}
</span>
</div>
{renderOAuthProvider('google', 'Google', <Mail className="size-5" />, 'Sync emails and calendar events')}
{useComposioForGoogle
? renderGmailRow()
: renderOAuthProvider('google', 'Google', <Mail className="size-5" />, 'Sync emails and calendar events')
}
</div>
)}
@ -878,7 +1030,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
// Step 2: Completion
const renderCompletionStep = () => {
const hasConnections = connectedProviders.length > 0 || granolaEnabled || slackEnabled
const hasConnections = connectedProviders.length > 0 || granolaEnabled || slackEnabled || gmailConnected
return (
<div className="flex flex-col items-center text-center">
@ -901,6 +1053,12 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
<div className="rounded-lg border bg-muted/50 p-4">
<p className="text-sm font-medium mb-2">Connected accounts:</p>
<div className="space-y-1">
{gmailConnected && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<CheckCircle2 className="size-4 text-green-600" />
<span>Gmail (Email)</span>
</div>
)}
{connectedProviders.includes('google') && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<CheckCircle2 className="size-4 text-green-600" />
@ -945,6 +1103,12 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
onSubmit={handleGoogleClientIdSubmit}
isSubmitting={providerStates.google?.isConnecting ?? false}
/>
<ComposioApiKeyModal
open={composioApiKeyOpen}
onOpenChange={setComposioApiKeyOpen}
onSubmit={handleComposioApiKeySubmit}
isSubmitting={gmailConnecting}
/>
<Dialog open={open} onOpenChange={() => {}}>
<DialogContent
className="w-[60vw] max-w-3xl max-h-[80vh] overflow-y-auto"