diff --git a/public/sounds/notification.wav b/public/sounds/notification.wav new file mode 100644 index 0000000..d74a1f8 Binary files /dev/null and b/public/sounds/notification.wav differ diff --git a/src-tauri/src/commands/system_settings.rs b/src-tauri/src/commands/system_settings.rs index 4953062..e7ffaf0 100644 --- a/src-tauri/src/commands/system_settings.rs +++ b/src-tauri/src/commands/system_settings.rs @@ -6,7 +6,7 @@ use crate::app_error::AppCommandError; use crate::db::service::app_metadata_service; #[cfg(feature = "tauri-runtime")] use crate::db::AppDatabase; -use crate::models::{SystemLanguageSettings, SystemProxySettings}; +use crate::models::{SystemLanguageSettings, SystemNotificationSettings, SystemProxySettings}; #[cfg(feature = "tauri-runtime")] use crate::models::SystemRenderingSettings; #[cfg(feature = "tauri-runtime")] @@ -16,6 +16,7 @@ use crate::network::proxy; const SYSTEM_PROXY_SETTINGS_KEY: &str = "system_proxy_settings"; const SYSTEM_LANGUAGE_SETTINGS_KEY: &str = "system_language_settings"; +const SYSTEM_NOTIFICATION_SETTINGS_KEY: &str = "system_notification_settings"; #[cfg(feature = "tauri-runtime")] const LANGUAGE_SETTINGS_UPDATED_EVENT: &str = "app://language-settings-updated"; @@ -174,3 +175,46 @@ pub async fn update_system_rendering_settings( })?; Ok(settings) } + +pub(crate) async fn load_system_notification_settings( + conn: &DatabaseConnection, +) -> Result { + let raw = app_metadata_service::get_value(conn, SYSTEM_NOTIFICATION_SETTINGS_KEY) + .await + .map_err(AppCommandError::from)?; + + let Some(raw) = raw else { + return Ok(SystemNotificationSettings::new_enabled()); + }; + + serde_json::from_str::(&raw).map_err(|e| { + AppCommandError::configuration_invalid("Failed to parse stored notification settings") + .with_detail(e.to_string()) + }) +} + +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] +pub async fn get_system_notification_settings( + db: State<'_, AppDatabase>, +) -> Result { + load_system_notification_settings(&db.conn).await +} + +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] +pub async fn update_system_notification_settings( + settings: SystemNotificationSettings, + db: State<'_, AppDatabase>, +) -> Result { + let serialized = serde_json::to_string(&settings).map_err(|e| { + AppCommandError::invalid_input("Failed to serialize notification settings") + .with_detail(e.to_string()) + })?; + + app_metadata_service::upsert_value(&db.conn, SYSTEM_NOTIFICATION_SETTINGS_KEY, &serialized) + .await + .map_err(AppCommandError::from)?; + + Ok(settings) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e632c89..98e0db6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -312,6 +312,8 @@ mod tauri_app { system_settings::update_system_language_settings, system_settings::get_system_rendering_settings, system_settings::update_system_rendering_settings, + system_settings::get_system_notification_settings, + system_settings::update_system_notification_settings, version_control::detect_git, version_control::test_git_path, version_control::get_git_settings, diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 7622a7a..2abd788 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -21,7 +21,7 @@ pub use message::{ }; pub use system::{ GitCredentials, GitDetectResult, GitHubAccountsSettings, GitHubTokenValidation, GitSettings, - SystemLanguageSettings, SystemProxySettings, + SystemLanguageSettings, SystemNotificationSettings, SystemProxySettings, }; #[cfg(feature = "tauri-runtime")] pub use system::SystemRenderingSettings; diff --git a/src-tauri/src/models/system.rs b/src-tauri/src/models/system.rs index 864b6ae..ed3c5e6 100644 --- a/src-tauri/src/models/system.rs +++ b/src-tauri/src/models/system.rs @@ -44,6 +44,22 @@ pub struct SystemRenderingSettings { pub disable_hardware_acceleration: bool, } +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default)] +pub struct SystemNotificationSettings { + pub task_completion: bool, + pub sound_enabled: bool, +} + +impl SystemNotificationSettings { + pub fn new_enabled() -> Self { + Self { + task_completion: true, + sound_enabled: true, + } + } +} + // --- Version Control --- /// Explicit credentials for a single git remote operation. diff --git a/src/components/settings/system-network-settings.tsx b/src/components/settings/system-network-settings.tsx index 2eca233..f7a8f59 100644 --- a/src/components/settings/system-network-settings.tsx +++ b/src/components/settings/system-network-settings.tsx @@ -3,11 +3,13 @@ import { useCallback, useEffect, useMemo, useState } from "react" import { ArrowUpCircle, + Bell, CheckCircle2, Languages, Loader2, MonitorCog, RefreshCw, + Volume2, Wifi, } from "lucide-react" import { Github } from "@lobehub/icons" @@ -31,9 +33,11 @@ import { import { getSystemProxySettings, getSystemRenderingSettings, + getSystemNotificationSettings, updateSystemLanguageSettings, updateSystemProxySettings, updateSystemRenderingSettings, + updateSystemNotificationSettings, } from "@/lib/api" import { isDesktop, openUrl } from "@/lib/platform" import type { AppLocale } from "@/lib/types" @@ -96,6 +100,9 @@ export function SystemNetworkSettings() { ) const renderingDirty = processStartLoaded && persistedDisableHwAccel !== processStartDisableHwAccel + const [notifTaskCompletion, setNotifTaskCompletion] = useState(true) + const [notifSoundEnabled, setNotifSoundEnabled] = useState(true) + const [savingNotification, setSavingNotification] = useState(false) const [currentVersion, setCurrentVersion] = useState("") const [availableUpdate, setAvailableUpdate] = useState(null) const [checkingUpdate, setCheckingUpdate] = useState(false) @@ -171,13 +178,15 @@ export function SystemNetworkSettings() { setLoadError(null) try { - const [proxySettings, version, renderingSettings] = await Promise.all([ - getSystemProxySettings(), - getCurrentAppVersion(), - renderingSettingsLoadable - ? getSystemRenderingSettings() - : Promise.resolve(null), - ]) + const [proxySettings, version, renderingSettings, notificationSettings] = + await Promise.all([ + getSystemProxySettings(), + getCurrentAppVersion(), + renderingSettingsLoadable + ? getSystemRenderingSettings() + : Promise.resolve(null), + getSystemNotificationSettings(), + ]) setEnabled(proxySettings.enabled) setProxyUrl(proxySettings.proxy_url ?? "") @@ -191,6 +200,10 @@ export function SystemNetworkSettings() { setProcessStartLoaded(true) } } + if (notificationSettings) { + setNotifTaskCompletion(notificationSettings.task_completion) + setNotifSoundEnabled(notificationSettings.sound_enabled) + } } catch (err) { const message = err instanceof Error ? err.message : String(err) setLoadError(message) @@ -291,6 +304,26 @@ export function SystemNetworkSettings() { [languageSettings.language, setLanguageSettings, t] ) + const saveNotificationSettings = useCallback( + async (taskCompletion: boolean, soundEnabled: boolean) => { + setSavingNotification(true) + try { + const result = await updateSystemNotificationSettings({ + task_completion: taskCompletion, + sound_enabled: soundEnabled, + }) + setNotifTaskCompletion(result.task_completion) + setNotifSoundEnabled(result.sound_enabled) + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + toast.error(t("notificationSaveFailed", { message })) + } finally { + setSavingNotification(false) + } + }, + [t] + ) + const formatUpdateError = useCallback( (error: unknown, action: UpdateAction): string => { const { kind, rawMessage } = normalizeAppUpdateError(error) @@ -716,6 +749,46 @@ export function SystemNetworkSettings() { + +
+
+ +

{t("notificationTitle")}

+
+ +

+ {t("notificationDescription")} +

+ + + + +
) diff --git a/src/contexts/acp-connections-context.tsx b/src/contexts/acp-connections-context.tsx index cff4172..5e0ad2f 100644 --- a/src/contexts/acp-connections-context.tsx +++ b/src/contexts/acp-connections-context.tsx @@ -22,6 +22,7 @@ import { acpCancel, acpRespondPermission, acpDisconnect, + getSystemNotificationSettings, } from "@/lib/api" import type { AgentType, @@ -1364,6 +1365,19 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) { pushAlertRef.current = pushAlert }, [pushAlert]) + // Notification settings (loaded once, refreshed lazily) + const notificationSettingsRef = useRef({ + task_completion: true, + sound_enabled: true, + }) + useEffect(() => { + getSystemNotificationSettings() + .then((s) => { + notificationSettingsRef.current = s + }) + .catch(() => {}) + }, []) + // Ref-based store — mutations don't trigger React state updates const storeRef = useRef({ connections: new Map(), @@ -1948,14 +1962,16 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) { } // Send OS notification when window is not focused { + const ns = notificationSettingsRef.current const nc = storeRef.current.connections.get(contextKey) - if (nc) { + if (nc && ns.task_completion) { const agentLabel = AGENT_LABELS[nc.agentType] const fn = folderNameRef.current const title = fn ? `${fn} - Codeg` : "Codeg" sendSystemNotification( title, - t("notificationTurnComplete", { agent: agentLabel }) + t("notificationTurnComplete", { agent: agentLabel }), + { sound: ns.sound_enabled } ).catch(() => {}) } } diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 66b6715..98532d8 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -145,7 +145,12 @@ "downloadFailed": "فشل تنزيل حزمة التحديث. يرجى المحاولة مرة أخرى لاحقًا.", "installFailed": "فشل تثبيت التحديث. يرجى إغلاق التطبيق ثم إعادة المحاولة.", "unknown": "فشل التحديث. يرجى المحاولة مرة أخرى لاحقًا." - } + }, + "notificationTitle": "الإشعارات", + "notificationDescription": "تكوين إشعارات إتمام المهام وتنبيهات الصوت.", + "notificationTaskCompletion": "إشعار عند إتمام المهمة", + "notificationSoundEnabled": "تشغيل صوت الإشعار", + "notificationSaveFailed": "فشل حفظ إعدادات الإشعارات: {message}" }, "VersionControlSettings": { "loading": "جارٍ التحميل...", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 70220a0..da7b1f9 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -145,7 +145,12 @@ "downloadFailed": "Das Update-Paket konnte nicht heruntergeladen werden. Bitte später erneut versuchen.", "installFailed": "Das Update konnte nicht installiert werden. Bitte App schließen und erneut versuchen.", "unknown": "Update fehlgeschlagen. Bitte später erneut versuchen." - } + }, + "notificationTitle": "Benachrichtigungen", + "notificationDescription": "Benachrichtigungen bei Aufgabenerledigung und Soundalarme konfigurieren.", + "notificationTaskCompletion": "Bei Aufgabenerledigung benachrichtigen", + "notificationSoundEnabled": "Benachrichtigungston abspielen", + "notificationSaveFailed": "Fehler beim Speichern der Benachrichtigungseinstellungen: {message}" }, "VersionControlSettings": { "loading": "Laden...", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 524c454..8fb15a5 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -145,7 +145,12 @@ "downloadFailed": "Failed to download update package. Please try again later.", "installFailed": "Failed to install update. Please close the app and try again.", "unknown": "Update failed. Please try again later." - } + }, + "notificationTitle": "Notifications", + "notificationDescription": "Configure task completion notifications and sound alerts.", + "notificationTaskCompletion": "Notify on task completion", + "notificationSoundEnabled": "Play notification sound", + "notificationSaveFailed": "Failed to save notification settings: {message}" }, "VersionControlSettings": { "loading": "Loading...", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index d13f2f5..23f92f2 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -145,7 +145,12 @@ "downloadFailed": "No se pudo descargar el paquete de actualización. Inténtalo más tarde.", "installFailed": "No se pudo instalar la actualización. Cierra la app e inténtalo de nuevo.", "unknown": "La actualización falló. Inténtalo más tarde." - } + }, + "notificationTitle": "Notificaciones", + "notificationDescription": "Configurar notificaciones de finalización de tareas y alertas de sonido.", + "notificationTaskCompletion": "Notificar al completar tarea", + "notificationSoundEnabled": "Reproducir sonido de notificación", + "notificationSaveFailed": "Error al guardar configuración de notificaciones: {message}" }, "VersionControlSettings": { "loading": "Cargando...", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index c5862a3..408ecb8 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -145,7 +145,12 @@ "downloadFailed": "Impossible de télécharger le paquet de mise à jour. Veuillez réessayer plus tard.", "installFailed": "Impossible d’installer la mise à jour. Fermez l’app puis réessayez.", "unknown": "La mise à jour a échoué. Veuillez réessayer plus tard." - } + }, + "notificationTitle": "Notifications", + "notificationDescription": "Configurer les notifications de fin de tâche et les alertes sonores.", + "notificationTaskCompletion": "Notifier à la fin de la tâche", + "notificationSoundEnabled": "Jouer un son de notification", + "notificationSaveFailed": "Échec de l'enregistrement des paramètres de notification : {message}" }, "VersionControlSettings": { "loading": "Chargement...", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 30b1df5..19d265a 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -145,7 +145,12 @@ "downloadFailed": "更新パッケージのダウンロードに失敗しました。しばらくしてから再試行してください。", "installFailed": "更新のインストールに失敗しました。アプリを閉じて再試行してください。", "unknown": "更新に失敗しました。しばらくしてから再試行してください。" - } + }, + "notificationTitle": "通知", + "notificationDescription": "タスク完了通知とサウンドアラートを設定します。", + "notificationTaskCompletion": "タスク完了時に通知する", + "notificationSoundEnabled": "通知音を再生する", + "notificationSaveFailed": "通知設定の保存に失敗しました:{message}" }, "VersionControlSettings": { "loading": "読み込み中...", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 5ee5b3a..21dc02e 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -145,7 +145,12 @@ "downloadFailed": "업데이트 패키지 다운로드에 실패했습니다. 잠시 후 다시 시도하세요.", "installFailed": "업데이트 설치에 실패했습니다. 앱을 종료한 뒤 다시 시도하세요.", "unknown": "업데이트에 실패했습니다. 잠시 후 다시 시도하세요." - } + }, + "notificationTitle": "알림", + "notificationDescription": "작업 완료 알림 및 소리 알림을 설정합니다.", + "notificationTaskCompletion": "작업 완료 시 알림", + "notificationSoundEnabled": "알림 소리 재생", + "notificationSaveFailed": "알림 설정 저장 실패: {message}" }, "VersionControlSettings": { "loading": "로딩 중...", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 0d5c3b4..5b471e0 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -145,7 +145,12 @@ "downloadFailed": "Falha ao baixar o pacote de atualização. Tente novamente mais tarde.", "installFailed": "Falha ao instalar a atualização. Feche o app e tente novamente.", "unknown": "Falha na atualização. Tente novamente mais tarde." - } + }, + "notificationTitle": "Notificações", + "notificationDescription": "Configurar notificações de conclusão de tarefas e alertas sonoros.", + "notificationTaskCompletion": "Notificar ao concluir tarefa", + "notificationSoundEnabled": "Reproduzir som de notificação", + "notificationSaveFailed": "Falha ao salvar configurações de notificação: {message}" }, "VersionControlSettings": { "loading": "Carregando...", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 8abe47e..05aec5f 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -145,7 +145,12 @@ "downloadFailed": "下载更新包失败,请稍后重试。", "installFailed": "安装更新失败,请关闭应用后重试。", "unknown": "更新失败,请稍后重试。" - } + }, + "notificationTitle": "通知", + "notificationDescription": "配置任务完成通知和声音提醒。", + "notificationTaskCompletion": "任务完成时通知", + "notificationSoundEnabled": "播放通知声音", + "notificationSaveFailed": "保存通知设置失败:{message}" }, "VersionControlSettings": { "loading": "加载中...", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 828b755..4aa26c2 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -145,7 +145,12 @@ "downloadFailed": "下載更新包失敗,請稍後再試。", "installFailed": "安裝更新失敗,請關閉應用後重試。", "unknown": "更新失敗,請稍後再試。" - } + }, + "notificationTitle": "通知", + "notificationDescription": "設定任務完成通知和聲音提醒。", + "notificationTaskCompletion": "任務完成時通知", + "notificationSoundEnabled": "播放通知聲音", + "notificationSaveFailed": "儲存通知設定失敗:{message}" }, "VersionControlSettings": { "loading": "載入中...", diff --git a/src/lib/api.ts b/src/lib/api.ts index ec91105..4bb5994 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -48,6 +48,7 @@ import type { SystemLanguageSettings, SystemProxySettings, SystemRenderingSettings, + SystemNotificationSettings, GitCredentials, GitDetectResult, PackageManagerInfo, @@ -469,6 +470,16 @@ export async function updateSystemRenderingSettings( return getTransport().call("update_system_rendering_settings", { settings }) } +export async function getSystemNotificationSettings(): Promise { + return getTransport().call("get_system_notification_settings") +} + +export async function updateSystemNotificationSettings( + settings: SystemNotificationSettings +): Promise { + return getTransport().call("update_system_notification_settings", { settings }) +} + // --- Version Control --- export async function detectGit(): Promise { diff --git a/src/lib/notification.ts b/src/lib/notification.ts index 5c538d9..6a60237 100644 --- a/src/lib/notification.ts +++ b/src/lib/notification.ts @@ -1,9 +1,31 @@ import { getTransport } from "./transport" import { isDesktop } from "./transport" +let cachedAudio: HTMLAudioElement | null = null + +function getNotificationAudio(): HTMLAudioElement { + if (!cachedAudio) { + cachedAudio = new Audio("/sounds/notification.wav") + } + return cachedAudio +} + +export function playNotificationSound(): void { + try { + const audio = getNotificationAudio() + audio.currentTime = 0 + audio.play().catch(() => { + // Autoplay may be blocked; silently ignore + }) + } catch { + // Ignore audio playback errors + } +} + export async function sendSystemNotification( title: string, - body: string + body: string, + options?: { sound?: boolean } ): Promise { if (!document.hidden) return if (isDesktop()) { @@ -19,4 +41,8 @@ export async function sendSystemNotification( } } } + + if (options?.sound !== false) { + playNotificationSound() + } } diff --git a/src/lib/types.ts b/src/lib/types.ts index 369832c..8d38238 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -638,6 +638,11 @@ export interface SystemRenderingSettings { disable_hardware_acceleration: boolean } +export interface SystemNotificationSettings { + task_completion: boolean + sound_enabled: boolean +} + // --- Version Control --- export interface GitCredentials {