-
Notifications
You must be signed in to change notification settings - Fork 5.6k
feat(web): notification (and sound) when a thread finish #7348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import { useEffect, useMemo, useRef } from "react"; | ||
| import { useRouter } from "@tanstack/react-router"; | ||
| import { projectKey } from "@t3tools/client-runtime/state/entities"; | ||
| import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment"; | ||
| import { useClientSettings } from "~/hooks/useSettings"; | ||
| import { showNotification, useNotificationPermission } from "~/notificationPermission"; | ||
| import { playNotificationChime, primeNotificationChime } from "~/notificationChime"; | ||
| import { useProjects, useThreadShells } from "~/state/entities"; | ||
| import { buildThreadRouteParams } from "~/threadRoutes"; | ||
| import { | ||
| deriveThreadCompletionNotifications, | ||
| NO_OBSERVED_THREADS, | ||
| type ObservedThreads, | ||
| type ThreadCompletionNotification, | ||
| } from "~/threadCompletionNotifications.logic"; | ||
|
|
||
| /** | ||
| * Raises an OS notification when a thread finishes its turn. | ||
| * | ||
| * Belongs in the app shell rather than the chat route: a turn that finishes | ||
| * while the user is in Settings should still reach them, and remounting would | ||
| * re-seed the watcher and swallow it. | ||
| */ | ||
| export function ThreadCompletionNotifications() { | ||
| const enabled = useClientSettings((settings) => settings.threadCompletionNotifications); | ||
| const { permission } = useNotificationPermission(); | ||
|
|
||
| // Nothing below this line subscribes to thread updates until the user has | ||
| // both opted in and granted permission. | ||
| if (!enabled || permission !== "granted") return null; | ||
| return <ThreadCompletionWatcher />; | ||
| } | ||
|
|
||
| function ThreadCompletionWatcher() { | ||
| const soundEnabled = useClientSettings((settings) => settings.threadCompletionNotificationSound); | ||
| const volume = useClientSettings((settings) => settings.threadCompletionNotificationVolume); | ||
| const threads = useThreadShells(); | ||
| const projects = useProjects(); | ||
| const router = useRouter(); | ||
| const observed = useRef<ObservedThreads>(NO_OBSERVED_THREADS); | ||
|
|
||
| const projectTitles = useMemo(() => { | ||
| const titles = new Map<string, string>(); | ||
| for (const project of projects) { | ||
| titles.set(projectKey(scopeProjectRef(project.environmentId, project.id)), project.title); | ||
| } | ||
| return titles; | ||
| }, [projects]); | ||
|
|
||
| // A reload leaves the audio context locked until the page sees a gesture, | ||
| // and the user has no reason to open Settings again to unlock it. | ||
| useEffect(() => { | ||
| if (!soundEnabled) return; | ||
| const unlock = () => primeNotificationChime(); | ||
| const options = { capture: true, once: true } as const; | ||
| window.addEventListener("pointerdown", unlock, options); | ||
| window.addEventListener("keydown", unlock, options); | ||
| return () => { | ||
| window.removeEventListener("pointerdown", unlock, options); | ||
| window.removeEventListener("keydown", unlock, options); | ||
| }; | ||
| }, [soundEnabled]); | ||
|
|
||
| useEffect(() => { | ||
| const result = deriveThreadCompletionNotifications({ threads, observed: observed.current }); | ||
| observed.current = result.observed; | ||
|
|
||
| let delivered = 0; | ||
| for (const notification of result.notifications) { | ||
| if (show(notification, projectTitles, router)) delivered += 1; | ||
| } | ||
|
|
||
| // One chime for the batch: two threads settling in the same snapshot | ||
| // should not stack two sounds. | ||
| if (soundEnabled && delivered > 0) { | ||
| playNotificationChime(volume); | ||
| } | ||
| }, [projectTitles, router, soundEnabled, threads, volume]); | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| function show( | ||
| notification: ThreadCompletionNotification, | ||
| projectTitles: ReadonlyMap<string, string>, | ||
| router: ReturnType<typeof useRouter>, | ||
| ): boolean { | ||
| const threadRef = scopeThreadRef(notification.environmentId, notification.threadId); | ||
| const projectTitle = projectTitles.get( | ||
| projectKey(scopeProjectRef(notification.environmentId, notification.projectId)), | ||
| ); | ||
|
|
||
| return showNotification({ | ||
| title: notification.title, | ||
| body: projectTitle ? `${projectTitle} · Turn finished` : "Turn finished", | ||
| // Turn-scoped: replacing a thread's earlier card would re-render it | ||
| // without re-alerting, which is the one thing this must not do. | ||
| tag: `t3code-thread-complete:${notification.turnId}`, | ||
| onClick: () => { | ||
| void router.navigate({ | ||
| to: "/$environmentId/$threadId", | ||
| params: buildThreadRouteParams(threadRef), | ||
| }); | ||
| }, | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -23,15 +23,18 @@ import { | |||||||||||||||||||||||||||||||||||||||||||||||
| MAX_CODE_FONT_SIZE, | ||||||||||||||||||||||||||||||||||||||||||||||||
| MAX_GLASS_OPACITY, | ||||||||||||||||||||||||||||||||||||||||||||||||
| MAX_INTERFACE_FONT_SIZE, | ||||||||||||||||||||||||||||||||||||||||||||||||
| MAX_NOTIFICATION_VOLUME, | ||||||||||||||||||||||||||||||||||||||||||||||||
| MAX_PROMPT_FONT_SIZE, | ||||||||||||||||||||||||||||||||||||||||||||||||
| MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, | ||||||||||||||||||||||||||||||||||||||||||||||||
| MAX_TERMINAL_FONT_SIZE, | ||||||||||||||||||||||||||||||||||||||||||||||||
| MIN_CODE_FONT_SIZE, | ||||||||||||||||||||||||||||||||||||||||||||||||
| MIN_GLASS_OPACITY, | ||||||||||||||||||||||||||||||||||||||||||||||||
| MIN_INTERFACE_FONT_SIZE, | ||||||||||||||||||||||||||||||||||||||||||||||||
| MIN_NOTIFICATION_VOLUME, | ||||||||||||||||||||||||||||||||||||||||||||||||
| MIN_PROMPT_FONT_SIZE, | ||||||||||||||||||||||||||||||||||||||||||||||||
| MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, | ||||||||||||||||||||||||||||||||||||||||||||||||
| MIN_TERMINAL_FONT_SIZE, | ||||||||||||||||||||||||||||||||||||||||||||||||
| type UnifiedSettings, | ||||||||||||||||||||||||||||||||||||||||||||||||
| } from "@t3tools/contracts/settings"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { resolveServerBackgroundActivitySettings } from "@t3tools/shared/backgroundActivitySettings"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { createModelSelection } from "@t3tools/shared/model"; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -64,6 +67,12 @@ import { | |||||||||||||||||||||||||||||||||||||||||||||||
| import { useLocalStorage } from "../../hooks/useLocalStorage"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { useThreadActions } from "../../hooks/useThreadActions"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||
| type NotificationPermissionState, | ||||||||||||||||||||||||||||||||||||||||||||||||
| showNotification, | ||||||||||||||||||||||||||||||||||||||||||||||||
| useNotificationPermission, | ||||||||||||||||||||||||||||||||||||||||||||||||
| } from "../../notificationPermission"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { playNotificationChime, primeNotificationChime } from "../../notificationChime"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { useDesktopUpdateState } from "../../state/desktopUpdate"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||
| getCustomModelOptionsByInstance, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1763,6 +1772,199 @@ function LegacyFeaturesSection() { | |||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const NOTIFICATION_PERMISSION_STATUS: Partial<Record<NotificationPermissionState, string>> = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| default: "Notification permission was reset. Use Test to ask for it again.", | ||||||||||||||||||||||||||||||||||||||||||||||||
| denied: | ||||||||||||||||||||||||||||||||||||||||||||||||
| "Notifications are blocked for this site. Allow them in your browser's site settings to turn this on.", | ||||||||||||||||||||||||||||||||||||||||||||||||
| unsupported: "This browser cannot show system notifications.", | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| function ThreadCompletionNotificationVolumeRow({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| volume, | ||||||||||||||||||||||||||||||||||||||||||||||||
| updateSettings, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| volume: number; | ||||||||||||||||||||||||||||||||||||||||||||||||
| updateSettings: ReturnType<typeof useUpdatePrimarySettings>; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const previewTimerRef = useRef<number | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect( | ||||||||||||||||||||||||||||||||||||||||||||||||
| () => () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (previewTimerRef.current !== null) window.clearTimeout(previewTimerRef.current); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| [], | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const volumeRatio = | ||||||||||||||||||||||||||||||||||||||||||||||||
| (volume - MIN_NOTIFICATION_VOLUME) / (MAX_NOTIFICATION_VOLUME - MIN_NOTIFICATION_VOLUME); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const volumeSliderStyle = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| "--settings-slider-progress": `${volumeRatio * 100}%`, | ||||||||||||||||||||||||||||||||||||||||||||||||
| "--settings-slider-fill-offset": `${0.5 - volumeRatio}rem`, | ||||||||||||||||||||||||||||||||||||||||||||||||
| } as CSSProperties; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <SettingsRow | ||||||||||||||||||||||||||||||||||||||||||||||||
| {...searchableSetting("thread-completion-notification-volume")} | ||||||||||||||||||||||||||||||||||||||||||||||||
| description="Set the volume of the completion notification chime." | ||||||||||||||||||||||||||||||||||||||||||||||||
| control={ | ||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex w-full items-center gap-3 sm:w-52"> | ||||||||||||||||||||||||||||||||||||||||||||||||
| <output | ||||||||||||||||||||||||||||||||||||||||||||||||
| className="min-w-12 rounded-md bg-muted px-2 py-1 text-center font-mono text-xs font-medium tabular-nums text-foreground" | ||||||||||||||||||||||||||||||||||||||||||||||||
| htmlFor="thread-completion-notification-volume-slider" | ||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||
| {volume}% | ||||||||||||||||||||||||||||||||||||||||||||||||
| </output> | ||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label="Notification volume" | ||||||||||||||||||||||||||||||||||||||||||||||||
| className="settings-slider min-w-0 flex-1" | ||||||||||||||||||||||||||||||||||||||||||||||||
| id="thread-completion-notification-volume-slider" | ||||||||||||||||||||||||||||||||||||||||||||||||
| max={MAX_NOTIFICATION_VOLUME} | ||||||||||||||||||||||||||||||||||||||||||||||||
| min={MIN_NOTIFICATION_VOLUME} | ||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={(event) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const nextVolume = Number(event.currentTarget.value); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| !Number.isInteger(nextVolume) || | ||||||||||||||||||||||||||||||||||||||||||||||||
| nextVolume < MIN_NOTIFICATION_VOLUME || | ||||||||||||||||||||||||||||||||||||||||||||||||
| nextVolume > MAX_NOTIFICATION_VOLUME | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| primeNotificationChime(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| updateSettings({ threadCompletionNotificationVolume: nextVolume }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (previewTimerRef.current !== null) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| window.clearTimeout(previewTimerRef.current); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| previewTimerRef.current = window.setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| previewTimerRef.current = null; | ||||||||||||||||||||||||||||||||||||||||||||||||
| playNotificationChime(nextVolume); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, 400); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| step={10} | ||||||||||||||||||||||||||||||||||||||||||||||||
| style={volumeSliderStyle} | ||||||||||||||||||||||||||||||||||||||||||||||||
| type="range" | ||||||||||||||||||||||||||||||||||||||||||||||||
| value={volume} | ||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1809
to
+1846
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This reconstructs the glass-opacity slider row (lines 1031-1058): identical Posted via Macroscope — UI Consistency |
||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| function ThreadCompletionNotificationRows({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| settings, | ||||||||||||||||||||||||||||||||||||||||||||||||
| updateSettings, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| settings: UnifiedSettings; | ||||||||||||||||||||||||||||||||||||||||||||||||
| updateSettings: ReturnType<typeof useUpdatePrimarySettings>; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const { permission, request } = useNotificationPermission(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const enabled = settings.threadCompletionNotifications; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // The stored preference is what the switch reflects, so a permission the | ||||||||||||||||||||||||||||||||||||||||||||||||
| // user revokes later still leaves them a switch they can turn off. | ||||||||||||||||||||||||||||||||||||||||||||||||
| const enable = useCallback(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (permission === "denied" || permission === "unsupported") return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Synchronously, while the click is still the current task: an audio | ||||||||||||||||||||||||||||||||||||||||||||||||
| // context created after awaiting the permission prompt starts suspended | ||||||||||||||||||||||||||||||||||||||||||||||||
| // and stays that way. | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (settings.threadCompletionNotificationSound) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| primeNotificationChime(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| void request().then((next) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (next === "granted") { | ||||||||||||||||||||||||||||||||||||||||||||||||
| updateSettings({ threadCompletionNotifications: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, [permission, request, settings.threadCompletionNotificationSound, updateSettings]); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const testNotification = useCallback(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const soundEnabled = settings.threadCompletionNotificationSound; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (soundEnabled) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| primeNotificationChime(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| void (async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const nextPermission = permission === "granted" ? permission : await request(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (nextPermission !== "granted") return; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const delivered = showNotification({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Test notification", | ||||||||||||||||||||||||||||||||||||||||||||||||
| body: "Notifications are working.", | ||||||||||||||||||||||||||||||||||||||||||||||||
| tag: "t3code-thread-complete:test", | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (delivered && soundEnabled) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| playNotificationChime(settings.threadCompletionNotificationVolume); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| })(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, [ | ||||||||||||||||||||||||||||||||||||||||||||||||
| permission, | ||||||||||||||||||||||||||||||||||||||||||||||||
| request, | ||||||||||||||||||||||||||||||||||||||||||||||||
| settings.threadCompletionNotificationSound, | ||||||||||||||||||||||||||||||||||||||||||||||||
| settings.threadCompletionNotificationVolume, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||
| <SettingsRow | ||||||||||||||||||||||||||||||||||||||||||||||||
| {...searchableSetting("thread-completion-notifications")} | ||||||||||||||||||||||||||||||||||||||||||||||||
| description="Show a system notification when a thread finishes its turn. Only fires while T3 Code is open in a tab." | ||||||||||||||||||||||||||||||||||||||||||||||||
| status={ | ||||||||||||||||||||||||||||||||||||||||||||||||
| enabled || permission === "denied" || permission === "unsupported" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ? NOTIFICATION_PERMISSION_STATUS[permission] | ||||||||||||||||||||||||||||||||||||||||||||||||
| : null | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| control={ | ||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center gap-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||
| {enabled ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <Button type="button" size="xs" variant="outline" onClick={testNotification}> | ||||||||||||||||||||||||||||||||||||||||||||||||
| Test | ||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) : null} | ||||||||||||||||||||||||||||||||||||||||||||||||
| <Switch | ||||||||||||||||||||||||||||||||||||||||||||||||
| checked={enabled} | ||||||||||||||||||||||||||||||||||||||||||||||||
| onCheckedChange={(checked) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (checked) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| enable(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| updateSettings({ threadCompletionNotifications: false }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label="Thread completion notifications" | ||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1923
to
+1933
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When permission is
Suggested change
Posted via Macroscope — UI Consistency |
||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| {enabled ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <SettingsRow | ||||||||||||||||||||||||||||||||||||||||||||||||
| {...searchableSetting("thread-completion-notification-sound")} | ||||||||||||||||||||||||||||||||||||||||||||||||
| description="Play a short chime with each completion notification." | ||||||||||||||||||||||||||||||||||||||||||||||||
| control={ | ||||||||||||||||||||||||||||||||||||||||||||||||
| <Switch | ||||||||||||||||||||||||||||||||||||||||||||||||
| checked={settings.threadCompletionNotificationSound} | ||||||||||||||||||||||||||||||||||||||||||||||||
| onCheckedChange={(checked) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const next = Boolean(checked); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (next) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| primeNotificationChime(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| updateSettings({ threadCompletionNotificationSound: next }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label="Thread completion notification sound" | ||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) : null} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| {enabled && settings.threadCompletionNotificationSound ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <ThreadCompletionNotificationVolumeRow | ||||||||||||||||||||||||||||||||||||||||||||||||
| volume={settings.threadCompletionNotificationVolume} | ||||||||||||||||||||||||||||||||||||||||||||||||
| updateSettings={updateSettings} | ||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) : null} | ||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export function GeneralSettingsPanel() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const settings = usePrimarySettings(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const updateSettings = useUpdatePrimarySettings(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2256,6 +2458,8 @@ export function GeneralSettingsPanel() { | |||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| <ThreadCompletionNotificationRows settings={settings} updateSettings={updateSettings} /> | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| {isElectron ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <SettingsRow | ||||||||||||||||||||||||||||||||||||||||||||||||
| {...searchableSetting("quit-confirmation")} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restore skips notification settings
Medium Severity
The new General settings
threadCompletionNotifications,threadCompletionNotificationSound, andthreadCompletionNotificationVolumeare omitted fromchangedSettingLabelsand therestoreDefaultsupdateSettingspayload. Changing them never dirties Restore defaults, and Restore leaves them unchanged while resetting neighboring General rows.Reviewed by Cursor Bugbot for commit 6c24b9c89ad9718b207759950851abf0a40e9561. Configure here.