add to settings

This commit is contained in:
Arjun 2026-02-25 16:07:12 +05:30
parent 98080cc604
commit 36f700cc77
7 changed files with 148 additions and 242 deletions

View file

@ -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<ISlackConfigRepo>('slackConfigRepo');
const config = await repo.getConfig();
return { enabled: config.enabled };
},
'slack:setConfig': async (_event, args) => {
const repo = container.resolve<ISlackConfigRepo>('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();

View file

@ -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
</div>
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium truncate">Slack</span>
{slackLoading ? (
<span className="text-xs text-muted-foreground">Checking...</span>
) : (
<span className="text-xs text-muted-foreground truncate">
Send messages and view channels
</span>
)}
<span className="text-xs text-muted-foreground truncate">
Send messages and view channels
</span>
</div>
</div>
<div className="shrink-0">
{slackLoading ? (
<Loader2 className="size-4 animate-spin text-muted-foreground" />
) : slackConnected ? (
<Button
variant="outline"
size="sm"
onClick={handleDisconnectSlack}
className="h-7 px-2 text-xs"
>
Disconnect
</Button>
) : (
<Button
variant="default"
size="sm"
onClick={handleConnectSlack}
disabled={slackConnecting}
className="h-7 px-2 text-xs"
>
{slackConnecting ? (
<Loader2 className="size-3 animate-spin" />
) : (
"Connect"
)}
</Button>
<div className="shrink-0 flex items-center gap-2">
{slackLoading && (
<Loader2 className="size-3 animate-spin" />
)}
<Switch
checked={slackEnabled}
onCheckedChange={handleSlackToggle}
disabled={slackLoading}
/>
</div>
</div>
</>
@ -624,12 +533,6 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
</div>
</PopoverContent>
</Popover>
<ComposioApiKeyModal
open={composioApiKeyOpen}
onOpenChange={setComposioApiKeyOpen}
onSubmit={handleComposioApiKeySubmit}
isSubmitting={slackConnecting}
/>
</>
)
}

View file

@ -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 = () => (
<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">
@ -553,41 +499,23 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
</div>
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium truncate">Slack</span>
{slackLoading ? (
<span className="text-xs text-muted-foreground">Checking...</span>
) : (
<span className="text-xs text-muted-foreground truncate">
Send messages and view channels
</span>
)}
<span className="text-xs text-muted-foreground truncate">
Send messages and view channels
</span>
</div>
</div>
<div className="shrink-0">
{slackLoading ? (
<Loader2 className="size-4 animate-spin text-muted-foreground" />
) : slackConnected ? (
<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={handleConnectSlack}
disabled={slackConnecting}
>
{slackConnecting ? (
<Loader2 className="size-4 animate-spin" />
) : (
"Connect"
)}
</Button>
<div className="shrink-0 flex items-center gap-2">
{slackLoading && (
<Loader2 className="size-3 animate-spin" />
)}
<Switch
checked={slackEnabled}
onCheckedChange={handleSlackToggle}
disabled={slackLoading}
/>
</div>
</div>
)
*/
// Step 0: LLM Setup
const renderLlmSetupStep = () => {
@ -783,6 +711,13 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
{providers.includes('fireflies-ai') && renderOAuthProvider('fireflies-ai', 'Fireflies', <Mic className="size-5" />, 'AI meeting transcripts')}
</div>
{/* Team Communication Section */}
<div className="space-y-2">
<div className="px-3">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Team Communication</span>
</div>
{renderSlackRow()}
</div>
</>
)}
</div>
@ -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 (
<div className="flex flex-col items-center text-center">
@ -841,7 +776,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
<span>Granola (Local meeting notes)</span>
</div>
)}
{slackConnected && (
{slackEnabled && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<CheckCircle2 className="size-4 text-green-600" />
<span>Slack (Team communication)</span>
@ -867,12 +802,6 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
onSubmit={handleGoogleClientIdSubmit}
isSubmitting={providerStates.google?.isConnecting ?? false}
/>
<ComposioApiKeyModal
open={composioApiKeyOpen}
onOpenChange={setComposioApiKeyOpen}
onSubmit={handleComposioApiKeySubmit}
isSubmitting={slackConnecting}
/>
<Dialog open={open} onOpenChange={() => {}}>
<DialogContent
className="w-[60vw] max-w-3xl max-h-[80vh] overflow-y-auto"

View file

@ -14,6 +14,7 @@ import { FSGranolaConfigRepo, IGranolaConfigRepo } from "../knowledge/granola/re
import { IAbortRegistry, InMemoryAbortRegistry } from "../runs/abort-registry.js";
import { FSAgentScheduleRepo, IAgentScheduleRepo } from "../agent-schedule/repo.js";
import { FSAgentScheduleStateRepo, IAgentScheduleStateRepo } from "../agent-schedule/state-repo.js";
import { FSSlackConfigRepo, ISlackConfigRepo } from "../slack/repo.js";
const container = createContainer({
injectionMode: InjectionMode.PROXY,
@ -37,6 +38,7 @@ container.register({
granolaConfigRepo: asClass<IGranolaConfigRepo>(FSGranolaConfigRepo).singleton(),
agentScheduleRepo: asClass<IAgentScheduleRepo>(FSAgentScheduleRepo).singleton(),
agentScheduleStateRepo: asClass<IAgentScheduleStateRepo>(FSAgentScheduleStateRepo).singleton(),
slackConfigRepo: asClass<ISlackConfigRepo>(FSSlackConfigRepo).singleton(),
});
export default container;

View file

@ -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<SlackConfig>;
setConfig(config: SlackConfig): Promise<void>;
}
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<void> {
try {
await fs.access(this.configPath);
} catch {
await fs.writeFile(this.configPath, JSON.stringify(this.defaultConfig, null, 2));
}
}
async getConfig(): Promise<SlackConfig> {
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<void> {
const validated = SlackConfig.parse(config);
await fs.writeFile(this.configPath, JSON.stringify(validated, null, 2));
}
}

View file

@ -0,0 +1,6 @@
import z from "zod";
export const SlackConfig = z.object({
enabled: z.boolean(),
});
export type SlackConfig = z.infer<typeof SlackConfig>;

View file

@ -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({