diff --git a/packages/desktop/src/renderer/App.tsx b/packages/desktop/src/renderer/App.tsx
index 68b811f83..477aabee6 100644
--- a/packages/desktop/src/renderer/App.tsx
+++ b/packages/desktop/src/renderer/App.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
-import { Toaster } from 'sonner';
+import { Toaster, TOAST_DURATION_MS, TOAST_OPTIONS } from '@/lib/toast';
import LeftNav from './layouts/LeftNav';
import MainArea from './layouts/MainArea';
import RightPanel from './layouts/RightPanel';
@@ -8,7 +8,7 @@ import Setup from './layouts/Setup';
import Settings from './layouts/Settings';
import ApprovalDialog from './components/ApprovalDialog';
import CommandPalette from './components/CommandPalette';
-import { useUiStore } from './stores/uiStore';
+import { useUiStore, type Theme } from './stores/uiStore';
import { useTaskStore } from './stores/taskStore';
import { useFileStore } from './stores/fileStore';
import { composer } from './platform';
@@ -20,6 +20,19 @@ import { TooltipProvider } from '@/components/ui/tooltip';
import { motionDuration, motionEase } from '@/styles/design-tokens';
import { useSettingsStore } from './stores/settingsStore';
+function AppToaster({ themeMode }: { themeMode: Theme }) {
+ return (
+
+ );
+}
+
export default function App() {
const [ready, setReady] = useState(false);
const [needsSetup, setNeedsSetup] = useState(false);
@@ -204,17 +217,7 @@ export default function App() {
setReady(true);
}}
/>
-
+
);
}
@@ -268,17 +271,7 @@ export default function App() {
>
)}
-
+
diff --git a/packages/desktop/src/renderer/components/AgentBuilderDialog/index.tsx b/packages/desktop/src/renderer/components/AgentBuilderDialog/index.tsx
index 7d84d92e9..f93678de1 100644
--- a/packages/desktop/src/renderer/components/AgentBuilderDialog/index.tsx
+++ b/packages/desktop/src/renderer/components/AgentBuilderDialog/index.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { Sparkles, Send, Loader2, Bot, User, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
diff --git a/packages/desktop/src/renderer/components/ChatInput/index.tsx b/packages/desktop/src/renderer/components/ChatInput/index.tsx
index 690fd2d3c..c35e7d56d 100644
--- a/packages/desktop/src/renderer/components/ChatInput/index.tsx
+++ b/packages/desktop/src/renderer/components/ChatInput/index.tsx
@@ -16,7 +16,7 @@ import {
} from 'lucide-react';
import { type KeyboardEvent, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import AgentIcon from '@/components/AgentIcon';
import ToolbarButton from '@/components/semantic/ToolbarButton';
import { Button } from '@/components/ui/button';
diff --git a/packages/desktop/src/renderer/components/ChatInput/useChatSend.ts b/packages/desktop/src/renderer/components/ChatInput/useChatSend.ts
index 8a0d29d10..07004b96c 100644
--- a/packages/desktop/src/renderer/components/ChatInput/useChatSend.ts
+++ b/packages/desktop/src/renderer/components/ChatInput/useChatSend.ts
@@ -9,7 +9,7 @@ import {
type SetStateAction,
} from 'react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import type {
Task,
AgentInfo,
diff --git a/packages/desktop/src/renderer/components/ChatInput/useContextFolders.ts b/packages/desktop/src/renderer/components/ChatInput/useContextFolders.ts
index 3bd14e94a..7671d1f03 100644
--- a/packages/desktop/src/renderer/components/ChatInput/useContextFolders.ts
+++ b/packages/desktop/src/renderer/components/ChatInput/useContextFolders.ts
@@ -1,6 +1,6 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import type { FileIndexEntry } from '@clawwork/shared';
import { useTaskStore } from '../../stores/taskStore';
diff --git a/packages/desktop/src/renderer/components/ChatInput/utils.ts b/packages/desktop/src/renderer/components/ChatInput/utils.ts
index 7e99e5529..084d2c82d 100644
--- a/packages/desktop/src/renderer/components/ChatInput/utils.ts
+++ b/packages/desktop/src/renderer/components/ChatInput/utils.ts
@@ -1,4 +1,4 @@
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import type { PendingAttachment } from './types';
import { MAX_ATTACHMENT_SIZE, GATEWAY_INJECTED_MODEL } from './constants';
diff --git a/packages/desktop/src/renderer/components/ChatMessage.tsx b/packages/desktop/src/renderer/components/ChatMessage.tsx
index 8fd76d3e1..e115b56c3 100644
--- a/packages/desktop/src/renderer/components/ChatMessage.tsx
+++ b/packages/desktop/src/renderer/components/ChatMessage.tsx
@@ -4,7 +4,7 @@ import { motion } from 'framer-motion';
import type { Message } from '@clawwork/shared';
import { Check, Copy, File, FileCode, Loader2, Save } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import { cn } from '@/lib/utils';
import { motion as motionPresets } from '@/styles/design-tokens';
import { copyTextToClipboard } from '@/lib/clipboard';
diff --git a/packages/desktop/src/renderer/components/TeamBuilderDialog/index.tsx b/packages/desktop/src/renderer/components/TeamBuilderDialog/index.tsx
index 4b76edd00..4fc8c3e9a 100644
--- a/packages/desktop/src/renderer/components/TeamBuilderDialog/index.tsx
+++ b/packages/desktop/src/renderer/components/TeamBuilderDialog/index.tsx
@@ -14,7 +14,7 @@ import {
ChevronRight,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import type { ModelCatalogEntry, InstallEvent } from '@clawwork/shared';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
diff --git a/packages/desktop/src/renderer/hooks/useGatewayDispatcherSetup.ts b/packages/desktop/src/renderer/hooks/useGatewayDispatcherSetup.ts
index 41ab6df14..7738b40e0 100644
--- a/packages/desktop/src/renderer/hooks/useGatewayDispatcherSetup.ts
+++ b/packages/desktop/src/renderer/hooks/useGatewayDispatcherSetup.ts
@@ -1,7 +1,7 @@
import { createGatewayDispatcher } from '@clawwork/core';
import type { ExecApprovalRequest, ModelCatalogEntry, AgentInfo } from '@clawwork/shared';
import { parseAgentIdFromSessionKey, parseTaskIdFromSessionKey } from '@clawwork/shared';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import { hydrateFromLocal, retrySyncPending, syncFromGateway, syncSessionMessages } from '../lib/session-sync';
import i18n from '../i18n';
import { composer, composerBridge, ports, useMessageStore, useTaskStore, useUiStore, useRoomStore } from '../platform';
diff --git a/packages/desktop/src/renderer/layouts/CronPanel/index.tsx b/packages/desktop/src/renderer/layouts/CronPanel/index.tsx
index 518f85f09..41c2dfe93 100644
--- a/packages/desktop/src/renderer/layouts/CronPanel/index.tsx
+++ b/packages/desktop/src/renderer/layouts/CronPanel/index.tsx
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Clock, Plus, Search, RefreshCw, ChevronDown, Server, Loader2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import { useUiStore } from '@/stores/uiStore';
import { cn } from '@/lib/utils';
import { motion as motionPresets, STAGGER_STEP } from '@/styles/design-tokens';
diff --git a/packages/desktop/src/renderer/layouts/Settings/components/PairMobileDialog.tsx b/packages/desktop/src/renderer/layouts/Settings/components/PairMobileDialog.tsx
index c43a1639b..bb85dfff8 100644
--- a/packages/desktop/src/renderer/layouts/Settings/components/PairMobileDialog.tsx
+++ b/packages/desktop/src/renderer/layouts/Settings/components/PairMobileDialog.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { QRCodeSVG } from 'qrcode.react';
import { Smartphone, Loader2 } from 'lucide-react';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import { useTranslation } from 'react-i18next';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
diff --git a/packages/desktop/src/renderer/layouts/Settings/sections/AboutSection.tsx b/packages/desktop/src/renderer/layouts/Settings/sections/AboutSection.tsx
index a5304ea71..4ecc9dd81 100644
--- a/packages/desktop/src/renderer/layouts/Settings/sections/AboutSection.tsx
+++ b/packages/desktop/src/renderer/layouts/Settings/sections/AboutSection.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { Star, Bug, RefreshCw, Loader2, Download, RotateCcw, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import SettingRow from '@/components/semantic/SettingRow';
diff --git a/packages/desktop/src/renderer/layouts/Settings/sections/AgentsSection.tsx b/packages/desktop/src/renderer/layouts/Settings/sections/AgentsSection.tsx
index 817c49e83..03850d1cb 100644
--- a/packages/desktop/src/renderer/layouts/Settings/sections/AgentsSection.tsx
+++ b/packages/desktop/src/renderer/layouts/Settings/sections/AgentsSection.tsx
@@ -17,7 +17,7 @@ import {
Sparkles,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import { cn } from '@/lib/utils';
import { motion as motionPresets } from '@/styles/design-tokens';
import { Button } from '@/components/ui/button';
diff --git a/packages/desktop/src/renderer/layouts/Settings/sections/GatewaysSection.tsx b/packages/desktop/src/renderer/layouts/Settings/sections/GatewaysSection.tsx
index 9bdb8d99b..0b4c1392d 100644
--- a/packages/desktop/src/renderer/layouts/Settings/sections/GatewaysSection.tsx
+++ b/packages/desktop/src/renderer/layouts/Settings/sections/GatewaysSection.tsx
@@ -14,7 +14,7 @@ import {
X,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import { cn } from '@/lib/utils';
import { motion as motionPresets } from '@/styles/design-tokens';
import { Button } from '@/components/ui/button';
diff --git a/packages/desktop/src/renderer/layouts/Settings/sections/GeneralSection.tsx b/packages/desktop/src/renderer/layouts/Settings/sections/GeneralSection.tsx
index 720a6b53d..90637a4de 100644
--- a/packages/desktop/src/renderer/layouts/Settings/sections/GeneralSection.tsx
+++ b/packages/desktop/src/renderer/layouts/Settings/sections/GeneralSection.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { Moon, Sun, Monitor, Bell, Smartphone } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import { modKey } from '@/lib/utils';
import {
useUiStore,
diff --git a/packages/desktop/src/renderer/layouts/Settings/sections/SkillsSection.tsx b/packages/desktop/src/renderer/layouts/Settings/sections/SkillsSection.tsx
index 93a13d0f4..e950f7519 100644
--- a/packages/desktop/src/renderer/layouts/Settings/sections/SkillsSection.tsx
+++ b/packages/desktop/src/renderer/layouts/Settings/sections/SkillsSection.tsx
@@ -16,7 +16,7 @@ import {
Settings2,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import { cn } from '@/lib/utils';
import { motion as motionPresets, motionDuration } from '@/styles/design-tokens';
import { useUiStore } from '@/stores/uiStore';
diff --git a/packages/desktop/src/renderer/layouts/Settings/sections/SystemSection.tsx b/packages/desktop/src/renderer/layouts/Settings/sections/SystemSection.tsx
index 05a4416cc..812a4f925 100644
--- a/packages/desktop/src/renderer/layouts/Settings/sections/SystemSection.tsx
+++ b/packages/desktop/src/renderer/layouts/Settings/sections/SystemSection.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { MonitorDot, Zap, FolderOpen, Loader2, ExternalLink } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import Toggle from '../components/Toggle';
diff --git a/packages/desktop/src/renderer/layouts/TeamsPanel/TeamDetailView.tsx b/packages/desktop/src/renderer/layouts/TeamsPanel/TeamDetailView.tsx
index 1f752d95b..f78c03bb5 100644
--- a/packages/desktop/src/renderer/layouts/TeamsPanel/TeamDetailView.tsx
+++ b/packages/desktop/src/renderer/layouts/TeamsPanel/TeamDetailView.tsx
@@ -1,7 +1,7 @@
import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import { ArrowLeft, MessageSquare, Pencil, Check, X, Loader2, Puzzle, Package } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import type { Team } from '@clawwork/shared';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
diff --git a/packages/desktop/src/renderer/layouts/TeamsPanel/TeamHubDetailView.tsx b/packages/desktop/src/renderer/layouts/TeamsPanel/TeamHubDetailView.tsx
index 4eaba9b34..f791f9b17 100644
--- a/packages/desktop/src/renderer/layouts/TeamsPanel/TeamHubDetailView.tsx
+++ b/packages/desktop/src/renderer/layouts/TeamsPanel/TeamHubDetailView.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { ArrowLeft, Download, Check, Loader2, Package, User, Tag } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import type { TeamHubEntry, ParsedTeam, AgentFileSet } from '@clawwork/shared';
import { extractSkillSlugs } from '@clawwork/core';
import { Button } from '@/components/ui/button';
diff --git a/packages/desktop/src/renderer/layouts/TeamsPanel/TeamsHubTab.tsx b/packages/desktop/src/renderer/layouts/TeamsPanel/TeamsHubTab.tsx
index 944549cd3..3cf04965a 100644
--- a/packages/desktop/src/renderer/layouts/TeamsPanel/TeamsHubTab.tsx
+++ b/packages/desktop/src/renderer/layouts/TeamsPanel/TeamsHubTab.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { Search, Settings, RefreshCw, Loader2, Store } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import type { TeamHubRegistry, TeamHubEntry } from '@clawwork/shared';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
diff --git a/packages/desktop/src/renderer/layouts/TeamsPanel/index.tsx b/packages/desktop/src/renderer/layouts/TeamsPanel/index.tsx
index 2a844c868..1aadf88cd 100644
--- a/packages/desktop/src/renderer/layouts/TeamsPanel/index.tsx
+++ b/packages/desktop/src/renderer/layouts/TeamsPanel/index.tsx
@@ -1,7 +1,7 @@
import { useState, useCallback, useEffect, useMemo } from 'react';
import { Users, Plus, Sparkles } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import type { Team, TeamHubEntry } from '@clawwork/shared';
import { cn } from '@/lib/utils';
import WindowTitlebar from '@/components/semantic/WindowTitlebar';
diff --git a/packages/desktop/src/renderer/lib/export-session.ts b/packages/desktop/src/renderer/lib/export-session.ts
index 03eff81c8..f1e6e3cfc 100644
--- a/packages/desktop/src/renderer/lib/export-session.ts
+++ b/packages/desktop/src/renderer/lib/export-session.ts
@@ -1,4 +1,4 @@
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import i18n from '../i18n';
export function exportToFiles(taskId: string): void {
diff --git a/packages/desktop/src/renderer/lib/toast.ts b/packages/desktop/src/renderer/lib/toast.ts
new file mode 100644
index 000000000..21bccf23f
--- /dev/null
+++ b/packages/desktop/src/renderer/lib/toast.ts
@@ -0,0 +1,38 @@
+import { toast as sonnerToast } from 'sonner';
+import type { ExternalToast, ToasterProps } from 'sonner';
+
+export { Toaster } from 'sonner';
+
+export const TOAST_DURATION_MS = 4000;
+const WARNING_TOAST_DURATION_MS = 8000;
+const ERROR_TOAST_DURATION_MS = 10000;
+const TOAST_CLOSE_BUTTON_CLASS =
+ '!left-auto !right-3 !top-1/2 !h-7 !w-7 !-translate-y-1/2 !translate-x-0 !border-0 !bg-transparent !text-[var(--text-muted)] hover:!bg-[var(--bg-hover)] hover:!text-[var(--text-primary)]';
+
+export const TOAST_OPTIONS: NonNullable = {
+ closeButton: true,
+ closeButtonAriaLabel: 'Close notification',
+ classNames: {
+ toast: 'pr-11',
+ closeButton: TOAST_CLOSE_BUTTON_CLASS,
+ },
+ style: {
+ background: 'var(--bg-elevated)',
+ border: '1px solid var(--border)',
+ color: 'var(--text-primary)',
+ },
+};
+
+type ToastMessage = Parameters[0];
+type RendererToast = typeof sonnerToast;
+
+export const toast: RendererToast = Object.assign(
+ (message: ToastMessage, data?: ExternalToast) => sonnerToast(message, data),
+ sonnerToast,
+ {
+ warning: (message: ToastMessage, data?: ExternalToast) =>
+ sonnerToast.warning(message, { duration: WARNING_TOAST_DURATION_MS, ...data }),
+ error: (message: ToastMessage, data?: ExternalToast) =>
+ sonnerToast.error(message, { duration: ERROR_TOAST_DURATION_MS, ...data }),
+ },
+);
diff --git a/packages/desktop/src/renderer/platform/index.ts b/packages/desktop/src/renderer/platform/index.ts
index cc7c7305f..14ee52449 100644
--- a/packages/desktop/src/renderer/platform/index.ts
+++ b/packages/desktop/src/renderer/platform/index.ts
@@ -20,7 +20,7 @@ import type {
SystemSessionState,
SystemSessionService,
} from '@clawwork/core';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import { createElectronPorts } from './electron-adapter';
import i18n from '../i18n';
import { syncSettingsUpdate } from '../stores/settingsStore';
diff --git a/packages/desktop/src/renderer/stores/approvalStore.ts b/packages/desktop/src/renderer/stores/approvalStore.ts
index 2cdc60632..f3303b4ea 100644
--- a/packages/desktop/src/renderer/stores/approvalStore.ts
+++ b/packages/desktop/src/renderer/stores/approvalStore.ts
@@ -1,7 +1,7 @@
import { create } from 'zustand';
import { parseTaskIdFromSessionKey } from '@clawwork/shared';
import type { ApprovalDecision, ExecApprovalRequest } from '@clawwork/shared';
-import { toast } from 'sonner';
+import { toast } from '@/lib/toast';
import i18n from '../i18n';
import { useTaskStore } from './taskStore';
import { useUiStore } from './uiStore';
diff --git a/packages/desktop/test/conductor-catalog-fallback.test.tsx b/packages/desktop/test/conductor-catalog-fallback.test.tsx
index 0928e1836..9cf399328 100644
--- a/packages/desktop/test/conductor-catalog-fallback.test.tsx
+++ b/packages/desktop/test/conductor-catalog-fallback.test.tsx
@@ -79,7 +79,7 @@ vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
-vi.mock('sonner', () => ({
+vi.mock('../src/renderer/lib/toast', () => ({
toast: {
error: vi.fn(),
},
@@ -112,7 +112,7 @@ vi.mock('../src/renderer/platform', () => ({
useTeamStore: makeStoreMock(mocks.teamState),
}));
-import { toast } from 'sonner';
+import { toast } from '../src/renderer/lib/toast';
import { useChatSend } from '../src/renderer/components/ChatInput/useChatSend';
/* ---------- helpers ---------- */