Unverified 提交 0409aa03 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Add native notification when chat stream completes (#2413)

## Summary - Add a native notification that appears when a chat response completes while the Dyad app window is not focused - Controlled by a new setting "Show notification when chat completes" in Workflow Settings - Enabled by default for a seamless notification experience ## Implementation - Added `enableChatCompletionNotifications` setting to UserSettingsSchema (default: true) - Added `isWindowFocused` IPC contract and handler to check window focus state from main process - Added notification logic to `useStreamChat`'s `onEnd` callback using the Web Notification API - Created `ChatCompletionNotificationSwitch` component for the settings toggle - Added the toggle to the Workflow Settings section in the Settings page ## Test plan 1. Start the app and enable notifications in Workflow Settings (enabled by default) 2. Start a chat and switch focus to another application 3. Wait for the chat response to complete 4. Verify a native notification appears saying "Chat response completed" 5. When Dyad is focused, verify no notification appears 6. Disable the setting and verify notifications stop appearing 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2413"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Mostly additive UI/renderer changes with an optional IPC payload field; main risk is runtime differences around `Notification` permissions/focus detection across platforms. > > **Overview** > Adds an opt-in Workflow Settings toggle (`enableChatCompletionNotifications`) that requests Web Notification permission when enabled. > > When a chat stream ends, the renderer now conditionally fires a native `Notification` if the setting is enabled, permission is granted, and the window is not focused, using the app name as the title and a truncated `chatSummary`/chat title as the body. > > Extends the `ChatResponseEnd` IPC payload with optional `chatSummary` and populates it from both the standard stream handler and the local-agent handler so the renderer can display richer completion notifications. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 02cb0c28d1bd72cf01e48f6923f4d2285df4aaa3. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds a native desktop notification when a chat response completes while Dyad isn’t focused, controlled by a new Workflow Settings toggle enabled by default. Helps you notice finished replies while multitasking. - **New Features** - New setting: “Show notification when chat completes” (default on). - Requests notification permission when you enable it; notifications show only if permission is granted. - Sends a notification via the Web Notification API when the window isn’t focused (document.hasFocus), using the app name as the title and the chat summary (or chat title) as the body. <sup>Written for commit 02cb0c28d1bd72cf01e48f6923f4d2285df4aaa3. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 e991d732
import { useSettings } from "@/hooks/useSettings";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
export function ChatCompletionNotificationSwitch() {
const { settings, updateSettings } = useSettings();
const isEnabled = settings?.enableChatCompletionNotifications === true;
return (
<div className="flex items-center space-x-2">
<Switch
id="chat-completion-notifications"
checked={isEnabled}
onCheckedChange={async (checked) => {
if (checked) {
if (Notification.permission === "denied") {
return;
}
if (Notification.permission === "default") {
const permission = await Notification.requestPermission();
if (permission !== "granted") {
return;
}
}
}
updateSettings({
enableChatCompletionNotifications: checked,
});
}}
/>
<Label htmlFor="chat-completion-notifications">
Show notification when chat completes
</Label>
</div>
);
}
......@@ -14,7 +14,8 @@ import {
} from "@/atoms/chatAtoms";
import { ipc } from "@/ipc/types";
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
import type { ChatResponseEnd } from "@/ipc/types";
import type { ChatResponseEnd, App } from "@/ipc/types";
import type { ChatSummary } from "@/lib/schemas";
import { useChats } from "./useChats";
import { useLoadApp } from "./useLoadApp";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
......@@ -175,6 +176,34 @@ export function useStreamChat({
// Remove from pending set now that stream is complete
pendingStreamChatIds.delete(chatId);
// Show native notification if enabled and window is not focused
// Fire-and-forget to avoid blocking UI updates
const notificationsEnabled =
settings?.enableChatCompletionNotifications === true;
if (
notificationsEnabled &&
Notification.permission === "granted" &&
!document.hasFocus()
) {
const app = queryClient.getQueryData<App | null>(
queryKeys.apps.detail({ appId: selectedAppId }),
);
const chats = queryClient.getQueryData<ChatSummary[]>(
queryKeys.chats.list({ appId: selectedAppId }),
);
const chat = chats?.find((c) => c.id === chatId);
const appName = app?.name ?? "Dyad";
const rawTitle = response.chatSummary ?? chat?.title;
const body = rawTitle
? rawTitle.length > 80
? rawTitle.slice(0, 80) + "…"
: rawTitle
: "Chat response completed";
new Notification(appName, {
body,
});
}
if (response.updatedFiles) {
if (settings?.autoExpandPreviewPanel) {
setIsPreviewOpen(true);
......
......@@ -1532,11 +1532,13 @@ ${problemReport.problems
updatedFiles: status.updatedFiles ?? false,
extraFiles: status.extraFiles,
extraFilesError: status.extraFilesError,
chatSummary,
} satisfies ChatResponseEnd);
} else {
safeSend(event.sender, "chat:response:end", {
chatId: req.chatId,
updatedFiles: false,
chatSummary,
} satisfies ChatResponseEnd);
}
}
......
......@@ -108,6 +108,7 @@ export const ChatResponseEndSchema = z.object({
extraFilesError: z.string().optional(),
totalTokens: z.number().optional(),
contextWindow: z.number().optional(),
chatSummary: z.string().optional(),
});
export type ChatResponseEnd = z.infer<typeof ChatResponseEndSchema>;
......
......@@ -303,6 +303,7 @@ export const UserSettingsSchema = z
enableAutoFixProblems: z.boolean().optional(),
autoExpandPreviewPanel: z.boolean().optional(),
enableChatCompletionNotifications: z.boolean().optional(),
enableNativeGit: z.boolean().optional(),
enableAutoUpdate: z.boolean(),
releaseChannel: ReleaseChannelSchema,
......
......@@ -21,6 +21,7 @@ import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { AutoFixProblemsSwitch } from "@/components/AutoFixProblemsSwitch";
import { AutoExpandPreviewSwitch } from "@/components/AutoExpandPreviewSwitch";
import { ChatCompletionNotificationSwitch } from "@/components/ChatCompletionNotificationSwitch";
import { AutoUpdateSwitch } from "@/components/AutoUpdateSwitch";
import { ReleaseChannelSelector } from "@/components/ReleaseChannelSelector";
import { NeonIntegration } from "@/components/NeonIntegration";
......@@ -337,6 +338,14 @@ export function WorkflowSettings() {
Automatically expand the preview panel when code changes are made.
</div>
</div>
<div className="space-y-1 mt-4">
<ChatCompletionNotificationSwitch />
<div className="text-sm text-gray-500 dark:text-gray-400">
Show a native notification when a chat response completes while the
app is not focused.
</div>
</div>
</div>
);
}
......
......@@ -473,6 +473,7 @@ export async function handleLocalAgentStream(
safeSend(event.sender, "chat:response:end", {
chatId: req.chatId,
updatedFiles: !readOnly,
chatSummary: ctx.chatSummary,
} satisfies ChatResponseEnd);
return true; // Success
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论