diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index b0757881..bbeb3164 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -25,6 +25,7 @@ import type { IModelConfigRepo } from '@x/core/dist/models/repo.js'; import type { IOAuthRepo } from '@x/core/dist/auth/repo.js'; import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js'; import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js'; +import { ISlackConfigRepo } from '@x/core/dist/slack/repo.js'; import { isOnboardingComplete, markOnboardingComplete } from '@x/core/dist/config/note_creation_config.js'; import * as composioHandler from './composio-handler.js'; import { IAgentScheduleRepo } from '@x/core/dist/agent-schedule/repo.js'; @@ -393,6 +394,16 @@ export function setupIpcHandlers() { return { success: true }; }, + 'slack:getConfig': async () => { + const repo = container.resolve('slackConfigRepo'); + const config = await repo.getConfig(); + return { enabled: config.enabled }; + }, + 'slack:setConfig': async (_event, args) => { + const repo = container.resolve('slackConfigRepo'); + await repo.setConfig({ enabled: args.enabled }); + return { success: true }; + }, 'onboarding:getStatus': async () => { // Show onboarding if it hasn't been completed yet const complete = isOnboardingComplete(); diff --git a/apps/x/apps/renderer/src/components/connectors-popover.tsx b/apps/x/apps/renderer/src/components/connectors-popover.tsx index ad2285a0..26fb3c04 100644 --- a/apps/x/apps/renderer/src/components/connectors-popover.tsx +++ b/apps/x/apps/renderer/src/components/connectors-popover.tsx @@ -17,7 +17,6 @@ import { import { Button } from "@/components/ui/button" import { Switch } from "@/components/ui/switch" import { Separator } from "@/components/ui/separator" -import { ComposioApiKeyModal } from "@/components/composio-api-key-modal" import { GoogleClientIdModal } from "@/components/google-client-id-modal" import { getGoogleClientId, setGoogleClientId, clearGoogleClientId } from "@/lib/google-client-id-store" import { toast } from "sonner" @@ -55,11 +54,9 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha const [granolaEnabled, setGranolaEnabled] = useState(false) const [granolaLoading, setGranolaLoading] = useState(true) - // Composio/Slack state - const [composioApiKeyOpen, setComposioApiKeyOpen] = useState(false) - const [slackConnected, setSlackConnected] = useState(false) + // Slack state (agent-slack CLI) + const [slackEnabled, setSlackEnabled] = useState(false) const [slackLoading, setSlackLoading] = useState(true) - const [slackConnecting, setSlackConnecting] = useState(false) // Load available providers on mount useEffect(() => { @@ -107,76 +104,30 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha } }, []) - // Load Slack connection status - const refreshSlackStatus = useCallback(async () => { + // Load Slack config + const refreshSlackConfig = useCallback(async () => { try { setSlackLoading(true) - const result = await window.ipc.invoke('composio:get-connection-status', { toolkitSlug: 'slack' }) - setSlackConnected(result.isConnected) + const result = await window.ipc.invoke('slack:getConfig', null) + setSlackEnabled(result.enabled) } catch (error) { - console.error('Failed to load Slack status:', error) - setSlackConnected(false) + console.error('Failed to load Slack config:', error) + setSlackEnabled(false) } finally { setSlackLoading(false) } }, []) - // Connect to Slack via Composio - const startSlackConnect = useCallback(async () => { - try { - setSlackConnecting(true) - const result = await window.ipc.invoke('composio:initiate-connection', { toolkitSlug: 'slack' }) - if (!result.success) { - toast.error(result.error || 'Failed to connect to Slack') - setSlackConnecting(false) - } - // Success will be handled by composio:didConnect event - } catch (error) { - console.error('Failed to connect to Slack:', error) - toast.error('Failed to connect to Slack') - setSlackConnecting(false) - } - }, []) - - // 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) { - setComposioApiKeyOpen(true) - return - } - await startSlackConnect() - }, [startSlackConnect]) - - // 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') - // Now start the Slack connection - await startSlackConnect() - } catch (error) { - console.error('Failed to save Composio API key:', error) - toast.error('Failed to save API key') - } - }, [startSlackConnect]) - - // Disconnect from Slack - const handleDisconnectSlack = useCallback(async () => { + // Update Slack config + const handleSlackToggle = useCallback(async (enabled: boolean) => { try { setSlackLoading(true) - const result = await window.ipc.invoke('composio:disconnect', { toolkitSlug: 'slack' }) - if (result.success) { - setSlackConnected(false) - toast.success('Disconnected from Slack') - } else { - toast.error('Failed to disconnect from Slack') - } + await window.ipc.invoke('slack:setConfig', { enabled }) + setSlackEnabled(enabled) + toast.success(enabled ? 'Slack enabled' : 'Slack disabled') } catch (error) { - console.error('Failed to disconnect from Slack:', error) - toast.error('Failed to disconnect from Slack') + console.error('Failed to update Slack config:', error) + toast.error('Failed to update Slack settings') } finally { setSlackLoading(false) } @@ -187,8 +138,8 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha // Refresh Granola refreshGranolaConfig() - // Refresh Slack status - refreshSlackStatus() + // Refresh Slack config + refreshSlackConfig() // Refresh OAuth providers if (providers.length === 0) return @@ -226,7 +177,7 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha } setProviderStates(newStates) - }, [providers, refreshGranolaConfig, refreshSlackStatus]) + }, [providers, refreshGranolaConfig, refreshSlackConfig]) // Refresh statuses when popover opens or providers list changes useEffect(() => { @@ -270,26 +221,6 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha return cleanup }, [refreshAllStatuses]) - // Listen for Composio connection events - useEffect(() => { - const cleanup = window.ipc.on('composio:didConnect', (event) => { - const { toolkitSlug, success, error } = event - - if (toolkitSlug === 'slack') { - setSlackConnected(success) - setSlackConnecting(false) - - if (success) { - toast.success('Connected to Slack') - } else { - toast.error(error || 'Failed to connect to Slack') - } - } - }) - - return cleanup - }, []) - const startConnect = useCallback(async (provider: string, clientId?: string) => { setProviderStates(prev => ({ ...prev, @@ -581,42 +512,20 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
Slack - {slackLoading ? ( - Checking... - ) : ( - - Send messages and view channels - - )} + + Send messages and view channels +
-
- {slackLoading ? ( - - ) : slackConnected ? ( - - ) : ( - +
+ {slackLoading && ( + )} +
@@ -624,12 +533,6 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha - ) } diff --git a/apps/x/apps/renderer/src/components/onboarding-modal.tsx b/apps/x/apps/renderer/src/components/onboarding-modal.tsx index 4855cab7..ff3d18d1 100644 --- a/apps/x/apps/renderer/src/components/onboarding-modal.tsx +++ b/apps/x/apps/renderer/src/components/onboarding-modal.tsx @@ -2,8 +2,7 @@ import * as React from "react" import { useState, useEffect, useCallback } from "react" -import { Loader2, Mic, Mail, CheckCircle2 } from "lucide-react" -// import { MessageSquare } from "lucide-react" +import { Loader2, Mic, Mail, CheckCircle2, MessageSquare } from "lucide-react" import { Dialog, @@ -23,7 +22,6 @@ import { SelectValue, } from "@/components/ui/select" import { cn } from "@/lib/utils" -import { ComposioApiKeyModal } from "@/components/composio-api-key-modal" import { GoogleClientIdModal } from "@/components/google-client-id-modal" import { getGoogleClientId, setGoogleClientId } from "@/lib/google-client-id-store" import { toast } from "sonner" @@ -80,11 +78,9 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { const [granolaLoading, setGranolaLoading] = useState(true) const [showMoreProviders, setShowMoreProviders] = useState(false) - // Composio/Slack state - const [composioApiKeyOpen, setComposioApiKeyOpen] = useState(false) - const [slackConnected, setSlackConnected] = useState(false) - // const [slackLoading, setSlackLoading] = useState(true) - const [slackConnecting, setSlackConnecting] = useState(false) + // Slack state (agent-slack CLI) + const [slackEnabled, setSlackEnabled] = useState(false) + const [slackLoading, setSlackLoading] = useState(true) const updateProviderConfig = useCallback( (provider: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; model: string }>) => { @@ -212,64 +208,35 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { } }, []) - // Load Slack connection status - const refreshSlackStatus = useCallback(async () => { + // Load Slack config + const refreshSlackConfig = useCallback(async () => { try { - // setSlackLoading(true) - const result = await window.ipc.invoke('composio:get-connection-status', { toolkitSlug: 'slack' }) - setSlackConnected(result.isConnected) + setSlackLoading(true) + const result = await window.ipc.invoke('slack:getConfig', null) + setSlackEnabled(result.enabled) } catch (error) { - console.error('Failed to load Slack status:', error) - setSlackConnected(false) + console.error('Failed to load Slack config:', error) + setSlackEnabled(false) } finally { - // setSlackLoading(false) + setSlackLoading(false) } }, []) - // Start Slack connection - const startSlackConnect = useCallback(async () => { + // Update Slack config + const handleSlackToggle = useCallback(async (enabled: boolean) => { try { - setSlackConnecting(true) - const result = await window.ipc.invoke('composio:initiate-connection', { toolkitSlug: 'slack' }) - if (!result.success) { - toast.error(result.error || 'Failed to connect to Slack') - setSlackConnecting(false) - } - // Success will be handled by composio:didConnect event + setSlackLoading(true) + await window.ipc.invoke('slack:setConfig', { enabled }) + setSlackEnabled(enabled) + toast.success(enabled ? 'Slack enabled' : 'Slack disabled') } catch (error) { - console.error('Failed to connect to Slack:', error) - toast.error('Failed to connect to Slack') - setSlackConnecting(false) + console.error('Failed to update Slack config:', error) + toast.error('Failed to update Slack settings') + } finally { + setSlackLoading(false) } }, []) - // 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) { - setComposioApiKeyOpen(true) - return - } - await startSlackConnect() - }, [startSlackConnect]) - */ - - // 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') - // Now start the Slack connection - await startSlackConnect() - } catch (error) { - console.error('Failed to save Composio API key:', error) - toast.error('Failed to save API key') - } - }, [startSlackConnect]) - const handleNext = () => { if (currentStep < 2) { setCurrentStep((prev) => (prev + 1) as Step) @@ -317,8 +284,8 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { // Refresh Granola refreshGranolaConfig() - // Refresh Slack status - refreshSlackStatus() + // Refresh Slack config + refreshSlackConfig() // Refresh OAuth providers if (providers.length === 0) return @@ -347,7 +314,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { } setProviderStates(newStates) - }, [providers, refreshGranolaConfig, refreshSlackStatus]) + }, [providers, refreshGranolaConfig, refreshSlackConfig]) // Refresh statuses when modal opens or providers list changes useEffect(() => { @@ -381,26 +348,6 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { return cleanup }, []) - // Listen for Composio connection events - useEffect(() => { - const cleanup = window.ipc.on('composio:didConnect', (event) => { - const { toolkitSlug, success, error } = event - - if (toolkitSlug === 'slack') { - setSlackConnected(success) - setSlackConnecting(false) - - if (success) { - toast.success('Connected to Slack') - } else { - toast.error(error || 'Failed to connect to Slack') - } - } - }) - - return cleanup - }, []) - const startConnect = useCallback(async (provider: string, clientId?: string) => { setProviderStates(prev => ({ ...prev, @@ -544,7 +491,6 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { ) // Render Slack row - /* const renderSlackRow = () => (
@@ -553,41 +499,23 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
Slack - {slackLoading ? ( - Checking... - ) : ( - - Send messages and view channels - - )} + + Send messages and view channels +
-
- {slackLoading ? ( - - ) : slackConnected ? ( -
- - Connected -
- ) : ( - +
+ {slackLoading && ( + )} +
) - */ // Step 0: LLM Setup const renderLlmSetupStep = () => { @@ -783,6 +711,13 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { {providers.includes('fireflies-ai') && renderOAuthProvider('fireflies-ai', 'Fireflies', , 'AI meeting transcripts')} + {/* Team Communication Section */} +
+
+ Team Communication +
+ {renderSlackRow()} +
)} @@ -800,7 +735,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { // Step 2: Completion const renderCompletionStep = () => { - const hasConnections = connectedProviders.length > 0 || granolaEnabled || slackConnected + const hasConnections = connectedProviders.length > 0 || granolaEnabled || slackEnabled return (
@@ -841,7 +776,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { Granola (Local meeting notes)
)} - {slackConnected && ( + {slackEnabled && (
Slack (Team communication) @@ -867,12 +802,6 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { onSubmit={handleGoogleClientIdSubmit} isSubmitting={providerStates.google?.isConnecting ?? false} /> - {}}> (FSGranolaConfigRepo).singleton(), agentScheduleRepo: asClass(FSAgentScheduleRepo).singleton(), agentScheduleStateRepo: asClass(FSAgentScheduleStateRepo).singleton(), + slackConfigRepo: asClass(FSSlackConfigRepo).singleton(), }); export default container; \ No newline at end of file diff --git a/apps/x/packages/core/src/slack/repo.ts b/apps/x/packages/core/src/slack/repo.ts new file mode 100644 index 00000000..3cb9ce99 --- /dev/null +++ b/apps/x/packages/core/src/slack/repo.ts @@ -0,0 +1,41 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { WorkDir } from '../config/config.js'; +import { SlackConfig } from './types.js'; + +export interface ISlackConfigRepo { + getConfig(): Promise; + setConfig(config: SlackConfig): Promise; +} + +export class FSSlackConfigRepo implements ISlackConfigRepo { + private readonly configPath = path.join(WorkDir, 'config', 'slack.json'); + private readonly defaultConfig: SlackConfig = { enabled: false }; + + constructor() { + this.ensureConfigFile(); + } + + private async ensureConfigFile(): Promise { + try { + await fs.access(this.configPath); + } catch { + await fs.writeFile(this.configPath, JSON.stringify(this.defaultConfig, null, 2)); + } + } + + async getConfig(): Promise { + try { + const content = await fs.readFile(this.configPath, 'utf8'); + const parsed = JSON.parse(content); + return SlackConfig.parse(parsed); + } catch { + return this.defaultConfig; + } + } + + async setConfig(config: SlackConfig): Promise { + const validated = SlackConfig.parse(config); + await fs.writeFile(this.configPath, JSON.stringify(validated, null, 2)); + } +} diff --git a/apps/x/packages/core/src/slack/types.ts b/apps/x/packages/core/src/slack/types.ts new file mode 100644 index 00000000..bfd1fe07 --- /dev/null +++ b/apps/x/packages/core/src/slack/types.ts @@ -0,0 +1,6 @@ +import z from "zod"; + +export const SlackConfig = z.object({ + enabled: z.boolean(), +}); +export type SlackConfig = z.infer; diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 71491f8f..04791f9c 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -270,6 +270,20 @@ const ipcSchemas = { success: z.literal(true), }), }, + 'slack:getConfig': { + req: z.null(), + res: z.object({ + enabled: z.boolean(), + }), + }, + 'slack:setConfig': { + req: z.object({ + enabled: z.boolean(), + }), + res: z.object({ + success: z.literal(true), + }), + }, 'onboarding:getStatus': { req: z.null(), res: z.object({