Add sonner for notifications and update UI components to utilize it; clean up unused props in chat sidebar

This commit is contained in:
tusharmagar 2026-01-21 09:24:56 +05:30
parent dbabfd2aca
commit 500d2d6725
6 changed files with 175 additions and 124 deletions

View file

@ -43,6 +43,7 @@
"nanoid": "^5.1.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"sonner": "^2.0.7",
"streamdown": "^1.6.10",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",

View file

@ -46,6 +46,7 @@ import {
} from "@/components/ui/sidebar"
import { TooltipProvider } from "@/components/ui/tooltip"
import { Separator } from "@/components/ui/separator"
import { Toaster } from "@/components/ui/sonner"
import { stripKnowledgePrefix, toKnowledgePath, wikiLabel } from '@/lib/wiki-links'
type DirEntry = z.infer<typeof workspace.DirEntry>
@ -1647,6 +1648,7 @@ function App() {
)}
</div>
</SidebarSectionProvider>
<Toaster />
</TooltipProvider>
)
}

View file

@ -145,7 +145,6 @@ export function ChatSidebar({
recentFiles = [],
visibleFiles = [],
selectedPath,
pendingPermissionRequests = new Map(),
pendingAskHumanRequests = new Map(),
allPermissionRequests = new Map(),
permissionResponses = new Map(),

View file

@ -2,7 +2,7 @@
import * as React from "react"
import { useState, useEffect, useCallback } from "react"
import { Database, Loader2, Plug } from "lucide-react"
import { Calendar, Loader2, Mic, Mail } from "lucide-react"
import {
Popover,
@ -18,7 +18,7 @@ import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Switch } from "@/components/ui/switch"
import { Separator } from "@/components/ui/separator"
import { toast } from "@/lib/toast"
import { toast } from "sonner"
interface ProviderState {
isConnected: boolean
@ -78,10 +78,10 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
setGranolaLoading(true)
await window.ipc.invoke('granola:setConfig', { enabled })
setGranolaEnabled(enabled)
toast(enabled ? 'Granola sync enabled' : 'Granola sync disabled', 'success')
toast.success(enabled ? 'Granola sync enabled' : 'Granola sync disabled')
} catch (error) {
console.error('Failed to update Granola config:', error)
toast('Failed to update Granola sync settings', 'error')
toast.error('Failed to update Granola sync settings')
} finally {
setGranolaLoading(false)
}
@ -142,14 +142,23 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
}))
if (success) {
toast(`Successfully connected to ${provider}`, 'success')
const displayName = provider === 'fireflies-ai' ? 'Fireflies' : provider.charAt(0).toUpperCase() + provider.slice(1)
// Show detailed message for Google and Fireflies (includes sync info)
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.',
duration: 8000,
})
} else {
toast.success(`Connected to ${displayName}`)
}
// Refresh status to ensure consistency
refreshAllStatuses()
} else {
toast(error || `Failed to connect to ${provider}`, 'error')
toast.error(error || `Failed to connect to ${provider}`)
}
})
return cleanup
}, [refreshAllStatuses])
@ -168,7 +177,7 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
// Event listener will handle the actual completion
} else {
// Immediate failure (e.g., couldn't start flow)
toast(result.error || `Failed to connect to ${provider}`, 'error')
toast.error(result.error || `Failed to connect to ${provider}`)
setProviderStates(prev => ({
...prev,
[provider]: { ...prev[provider], isConnecting: false }
@ -176,7 +185,7 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
}
} catch (error) {
console.error('Failed to connect:', error)
toast(`Failed to connect to ${provider}`, 'error')
toast.error(`Failed to connect to ${provider}`)
setProviderStates(prev => ({
...prev,
[provider]: { ...prev[provider], isConnecting: false }
@ -195,7 +204,8 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
const result = await window.ipc.invoke('oauth:disconnect', { provider })
if (result.success) {
toast(`Disconnected from ${provider}`, 'success')
const displayName = provider === 'fireflies-ai' ? 'Fireflies' : provider.charAt(0).toUpperCase() + provider.slice(1)
toast.success(`Disconnected from ${displayName}`)
setProviderStates(prev => ({
...prev,
[provider]: {
@ -205,7 +215,7 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
}
}))
} else {
toast(`Failed to disconnect from ${provider}`, 'error')
toast.error(`Failed to disconnect from ${provider}`)
setProviderStates(prev => ({
...prev,
[provider]: { ...prev[provider], isLoading: false }
@ -213,7 +223,7 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
}
} catch (error) {
console.error('Failed to disconnect:', error)
toast(`Failed to disconnect from ${provider}`, 'error')
toast.error(`Failed to disconnect from ${provider}`)
setProviderStates(prev => ({
...prev,
[provider]: { ...prev[provider], isLoading: false }
@ -221,6 +231,64 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
}
}, [])
// Helper to render an OAuth provider row
const renderOAuthProvider = (provider: string, displayName: string, icon: React.ReactNode, description: string) => {
const state = providerStates[provider] || {
isConnected: false,
isLoading: true,
isConnecting: false,
}
return (
<div
key={provider}
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">
{icon}
</div>
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium truncate">{displayName}</span>
{state.isLoading ? (
<span className="text-xs text-muted-foreground">Checking...</span>
) : (
<span className="text-xs text-muted-foreground truncate">{description}</span>
)}
</div>
</div>
<div className="shrink-0">
{state.isLoading ? (
<Loader2 className="size-4 animate-spin text-muted-foreground" />
) : state.isConnected ? (
<Button
variant="outline"
size="sm"
onClick={() => handleDisconnect(provider)}
className="h-7 px-2 text-xs"
>
Disconnect
</Button>
) : (
<Button
variant="default"
size="sm"
onClick={() => handleConnect(provider)}
disabled={state.isConnecting}
className="h-7 px-2 text-xs"
>
{state.isConnecting ? (
<Loader2 className="size-3 animate-spin" />
) : (
"Connect"
)}
</Button>
)}
</div>
</div>
)
}
return (
<Popover open={open} onOpenChange={setOpen}>
{tooltip ? (
@ -252,123 +320,56 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
</p>
</div>
<div className="p-2">
{/* Data Sources Section */}
<div className="px-2 py-1.5">
<span className="text-xs font-medium text-muted-foreground">Data Sources</span>
</div>
{/* Granola */}
<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">
<Database className="size-4" />
</div>
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium truncate">Granola</span>
<span className="text-xs text-muted-foreground truncate">
Sync meeting notes
</span>
</div>
</div>
<div className="shrink-0 flex items-center gap-2">
{granolaLoading && (
<Loader2 className="size-3 animate-spin" />
)}
<Switch
checked={granolaEnabled}
onCheckedChange={handleGranolaToggle}
disabled={granolaLoading}
/>
</div>
</div>
<Separator className="my-2" />
{/* OAuth Connectors Section */}
<div className="px-2 py-1.5">
<span className="text-xs font-medium text-muted-foreground">Accounts</span>
</div>
{providersLoading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="size-5 animate-spin text-muted-foreground" />
</div>
) : providers.length === 0 ? (
<div className="text-center py-4 text-xs text-muted-foreground">
No account connectors available
</div>
) : (
<div className="flex flex-col gap-1">
{providers.map((provider) => {
const state = providerStates[provider] || {
isConnected: false,
isLoading: true,
isConnecting: false,
}
const displayName = provider.charAt(0).toUpperCase() + provider.slice(1)
return (
<div
key={provider}
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">
<Plug className="size-4" />
</div>
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium truncate">
{displayName}
</span>
{state.isLoading ? (
<span className="text-xs text-muted-foreground">
Checking...
</span>
) : (
<Badge
variant={state.isConnected ? "default" : "outline"}
className="w-fit text-xs mt-0.5"
>
{state.isConnected ? "Connected" : "Not Connected"}
</Badge>
)}
</div>
</div>
<div className="shrink-0">
{state.isConnected ? (
<Button
variant="outline"
size="sm"
onClick={() => handleDisconnect(provider)}
disabled={state.isLoading}
className="h-7 px-2 text-xs"
>
{state.isLoading ? (
<Loader2 className="size-3 animate-spin" />
) : (
"Disconnect"
)}
</Button>
) : (
<Button
variant="default"
size="sm"
onClick={() => handleConnect(provider)}
disabled={state.isConnecting || state.isLoading}
className="h-7 px-2 text-xs"
>
{state.isConnecting ? (
<Loader2 className="size-3 animate-spin" />
) : (
"Connect"
)}
</Button>
)}
</div>
<>
{/* Email & Calendar Section - Google */}
{providers.includes('google') && (
<>
<div className="px-2 py-1.5">
<span className="text-xs font-medium text-muted-foreground">Email & Calendar</span>
</div>
)
})}
</div>
{renderOAuthProvider('google', 'Google', <Mail className="size-4" />, 'Sync emails and calendar')}
<Separator className="my-2" />
</>
)}
{/* Meeting Notes Section - Granola & Fireflies */}
<div className="px-2 py-1.5">
<span className="text-xs font-medium text-muted-foreground">Meeting Notes</span>
</div>
{/* Granola */}
<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">
<Mic className="size-4" />
</div>
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium truncate">Granola</span>
<span className="text-xs text-muted-foreground truncate">
Local meeting notes
</span>
</div>
</div>
<div className="shrink-0 flex items-center gap-2">
{granolaLoading && (
<Loader2 className="size-3 animate-spin" />
)}
<Switch
checked={granolaEnabled}
onCheckedChange={handleGranolaToggle}
disabled={granolaLoading}
/>
</div>
</div>
{/* Fireflies */}
{providers.includes('fireflies-ai') && renderOAuthProvider('fireflies-ai', 'Fireflies', <Mic className="size-4" />, 'AI meeting transcripts')}
</>
)}
</div>
</PopoverContent>

View file

@ -0,0 +1,34 @@
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react"
import { Toaster as Sonner, type ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
return (
<Sonner
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }