Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const globalSettingsSchema = z.object({
ttsSpeed: z.number().optional(),
soundEnabled: z.boolean().optional(),
soundVolume: z.number().optional(),
systemNotificationsEnabled: z.boolean().optional(),

maxOpenTabsContext: z.number().optional(),
maxWorkspaceFiles: z.number().optional(),
Expand Down Expand Up @@ -281,6 +282,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
ttsSpeed: 1,
soundEnabled: false,
soundVolume: 0.5,
systemNotificationsEnabled: false,

terminalOutputLineLimit: 500,
terminalOutputCharacterLimit: DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
Expand Down
3 changes: 3 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1579,6 +1579,7 @@ export class ClineProvider
soundEnabled,
ttsEnabled,
ttsSpeed,
systemNotificationsEnabled,
diffEnabled,
enableCheckpoints,
taskHistory,
Expand Down Expand Up @@ -1689,6 +1690,7 @@ export class ClineProvider
soundEnabled: soundEnabled ?? false,
ttsEnabled: ttsEnabled ?? false,
ttsSpeed: ttsSpeed ?? 1.0,
systemNotificationsEnabled: systemNotificationsEnabled ?? false,
diffEnabled: diffEnabled ?? true,
enableCheckpoints: enableCheckpoints ?? true,
shouldShowAnnouncement:
Expand Down Expand Up @@ -1897,6 +1899,7 @@ export class ClineProvider
soundEnabled: stateValues.soundEnabled ?? false,
ttsEnabled: stateValues.ttsEnabled ?? false,
ttsSpeed: stateValues.ttsSpeed ?? 1.0,
systemNotificationsEnabled: stateValues.systemNotificationsEnabled ?? false,
diffEnabled: stateValues.diffEnabled ?? true,
enableCheckpoints: stateValues.enableCheckpoints ?? true,
soundVolume: stateValues.soundVolume,
Expand Down
15 changes: 15 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { checkExistKey } from "../../shared/checkExistApiConfig"
import { experimentDefault } from "../../shared/experiments"
import { Terminal } from "../../integrations/terminal/Terminal"
import { openFile } from "../../integrations/misc/open-file"
import { showSystemNotification } from "../../integrations/notifications"
import { openImage, saveImage } from "../../integrations/misc/image-handler"
import { selectImages } from "../../integrations/misc/process-images"
import { getTheme } from "../../integrations/theme/getTheme"
Expand Down Expand Up @@ -992,6 +993,20 @@ export const webviewMessageHandler = async (
await updateGlobalState("soundVolume", soundVolume)
await provider.postStateToWebview()
break
case "showSystemNotification":
const isSystemNotificationsEnabled = getGlobalState("systemNotificationsEnabled") ?? false
if (!isSystemNotificationsEnabled) {
break
}
if (message.notificationOptions) {
showSystemNotification(message.notificationOptions)
}
break
case "systemNotificationsEnabled":
const systemNotificationsEnabled = message.bool ?? true
await updateGlobalState("systemNotificationsEnabled", systemNotificationsEnabled)
await provider.postStateToWebview()
break
case "ttsEnabled":
const ttsEnabled = message.bool ?? true
await updateGlobalState("ttsEnabled", ttsEnabled)
Expand Down
110 changes: 110 additions & 0 deletions src/integrations/notifications/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { execa } from "execa"
import { platform } from "os"
import * as vscode from "vscode"

interface NotificationOptions {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NotificationOptions interface is quite basic. Based on the issue requirements, shouldn't it support priority levels and action buttons?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not supported yet

title?: string
subtitle?: string
message: string
force?: boolean // Force notification even if the window is focused
}

async function showMacOSNotification(options: NotificationOptions): Promise<void> {
const { title, subtitle = "", message } = options

const script = `display notification "${message}" with title "${title}" subtitle "${subtitle}" sound name "Tink"`

try {
await execa("osascript", ["-e", script])
} catch (error) {
throw new Error(`Failed to show macOS notification: ${error}`)
}
}

async function showWindowsNotification(options: NotificationOptions): Promise<void> {
const { subtitle, message } = options

const script = `
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null

$template = @"
<toast>
<visual>
<binding template="ToastText02">
<text id="1">${subtitle}</text>
<text id="2">${message}</text>
</binding>
</visual>
</toast>
"@

$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($template)
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Roo Code").Show($toast)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows implementation hardcodes "Roo Code" which might not work correctly in all configurations. Could we make this configurable or detect the actual application name?

`

try {
await execa("powershell", ["-Command", script])
} catch (error) {
throw new Error(`Failed to show Windows notification: ${error}`)
}
}

async function showLinuxNotification(options: NotificationOptions): Promise<void> {
const { title = "", subtitle = "", message } = options

// Combine subtitle and message if subtitle exists
const fullMessage = subtitle ? `${subtitle}\n${message}` : message

try {
await execa("notify-send", [title, fullMessage])
} catch (error: any) {
if (error.code === "ENOENT") {
throw new Error(
"notify-send is not installed. Please install libnotify-bin (apt install libnotify-bin on Debian/Ubuntu)",
)
}
throw new Error(`Failed to show Linux notification: ${error}`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Linux implementation assumes notify-send is available but doesn't handle the case where it's not installed. Consider checking for the binary first or providing a helpful error message:

Suggested change
throw new Error(`Failed to show Linux notification: ${error}`)
} catch (error: any) {
if (error.code === 'ENOENT') {
throw new Error('notify-send is not installed. Please install libnotify-bin (apt install libnotify-bin on Debian/Ubuntu)')
}
throw new Error(`Failed to show Linux notification: ${error}`)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix

}
}

export async function showSystemNotification(options: NotificationOptions): Promise<void> {
try {
if (vscode.window.state.focused && !options.force) {
// If the window is focused, do not show a notification
return
}

const { title = "Roo Code", message } = options

if (!message) {
throw new Error("Message is required")
}

const escape = (str: string = "") => str.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
const escapedOptions = {
...options,
title: escape(title),
message: escape(message),
subtitle: escape(options.subtitle),
}

switch (platform()) {
case "darwin":
await showMacOSNotification(escapedOptions)
break
case "win32":
await showWindowsNotification(escapedOptions)
break
case "linux":
await showLinuxNotification(escapedOptions)
break
default:
throw new Error("Unsupported platform")
}
} catch (error) {
console.error("Could not show system notification", error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling feedback to users. When notifications fail, users have no way to know. Consider implementing a fallback mechanism:

Suggested change
console.error("Could not show system notification", error)
} catch (error) {
console.error("Could not show system notification", error);
// Fallback to VSCode's built-in notification
vscode.window.showInformationMessage(options.message);
}

}
}
2 changes: 2 additions & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export interface ExtensionMessage {
| "remoteBrowserEnabled"
| "ttsStart"
| "ttsStop"
| "showSystemNotification"
| "maxReadFileLine"
| "fileSearchResults"
| "toggleApiConfigPin"
Expand Down Expand Up @@ -240,6 +241,7 @@ export type ExtensionState = Pick<
| "ttsSpeed"
| "soundEnabled"
| "soundVolume"
| "systemNotificationsEnabled"
// | "maxOpenTabsContext" // Optional in GlobalSettings, required here.
// | "maxWorkspaceFiles" // Optional in GlobalSettings, required here.
// | "showRooIgnoredFiles" // Optional in GlobalSettings, required here.
Expand Down
8 changes: 8 additions & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ export interface WebviewMessage {
| "ttsEnabled"
| "ttsSpeed"
| "soundVolume"
| "showSystemNotification"
| "systemNotificationsEnabled"
| "diffEnabled"
| "enableCheckpoints"
| "browserViewportSize"
Expand Down Expand Up @@ -291,6 +293,12 @@ export interface WebviewMessage {
codebaseIndexMistralApiKey?: string
codebaseIndexVercelAiGatewayApiKey?: string
}
notificationOptions?: {
title?: string
subtitle?: string
message: string
force?: boolean // Force notification even if the window is focused
}
}

export const checkoutDiffPayloadSchema = z.object({
Expand Down
10 changes: 9 additions & 1 deletion webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import RooCloudCTA from "@src/components/welcome/RooCloudCTA"
import { StandardTooltip } from "@src/components/ui"
import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
import { showSystemNotification } from "@src/utils/showSystemNotification"

import TelemetryBanner from "../common/TelemetryBanner"
import VersionIndicator from "../common/VersionIndicator"
Expand All @@ -56,7 +57,6 @@ import SystemPromptWarning from "./SystemPromptWarning"
import ProfileViolationWarning from "./ProfileViolationWarning"
import { CheckpointWarning } from "./CheckpointWarning"
import { QueuedMessages } from "./QueuedMessages"

export interface ChatViewProps {
isHidden: boolean
showAnnouncement: boolean
Expand Down Expand Up @@ -280,6 +280,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
switch (lastMessage.ask) {
case "api_req_failed":
playSound("progress_loop")
showSystemNotification(t("settings:notifications.system.apiReqFailed"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good integration with the chat events! However, consider adding test coverage for these notification triggers to ensure they work correctly across different scenarios.

setSendingDisabled(true)
setClineAsk("api_req_failed")
setEnableButtons(true)
Expand All @@ -288,6 +289,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
break
case "mistake_limit_reached":
playSound("progress_loop")
showSystemNotification(t("settings:notifications.system.mistakeLimitReached"))
setSendingDisabled(false)
setClineAsk("mistake_limit_reached")
setEnableButtons(true)
Expand All @@ -297,6 +299,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
case "followup":
if (!isPartial) {
playSound("notification")
showSystemNotification(t("settings:notifications.system.followup"))
}
setSendingDisabled(isPartial)
setClineAsk("followup")
Expand All @@ -311,6 +314,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
case "tool":
if (!isAutoApproved(lastMessage) && !isPartial) {
playSound("notification")
showSystemNotification(t("settings:notifications.system.toolRequest"))
}
setSendingDisabled(isPartial)
setClineAsk("tool")
Expand Down Expand Up @@ -347,6 +351,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
case "browser_action_launch":
if (!isAutoApproved(lastMessage) && !isPartial) {
playSound("notification")
showSystemNotification(t("settings:notifications.system.browserAction"))
}
setSendingDisabled(isPartial)
setClineAsk("browser_action_launch")
Expand All @@ -357,6 +362,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
case "command":
if (!isAutoApproved(lastMessage) && !isPartial) {
playSound("notification")
showSystemNotification(t("settings:notifications.system.command"))
}
setSendingDisabled(isPartial)
setClineAsk("command")
Expand All @@ -374,6 +380,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
case "use_mcp_server":
if (!isAutoApproved(lastMessage) && !isPartial) {
playSound("notification")
showSystemNotification(t("settings:notifications.system.useMcpServer"))
}
setSendingDisabled(isPartial)
setClineAsk("use_mcp_server")
Expand All @@ -385,6 +392,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
// extension waiting for feedback. but we can just present a new task button
if (!isPartial) {
playSound("celebration")
showSystemNotification(t("settings:notifications.system.completionResult"))
}
setSendingDisabled(isPartial)
setClineAsk("completion_result")
Expand Down
42 changes: 40 additions & 2 deletions webview-ui/src/components/settings/NotificationSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,48 @@
import { HTMLAttributes } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { VSCodeCheckbox, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { Bell } from "lucide-react"

import { SetCachedStateField } from "./types"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
import { Slider } from "../ui"
import { vscode } from "../../utils/vscode"

type NotificationSettingsProps = HTMLAttributes<HTMLDivElement> & {
ttsEnabled?: boolean
ttsSpeed?: number
soundEnabled?: boolean
soundVolume?: number
setCachedStateField: SetCachedStateField<"ttsEnabled" | "ttsSpeed" | "soundEnabled" | "soundVolume">
systemNotificationsEnabled?: boolean
setCachedStateField: SetCachedStateField<
"ttsEnabled" | "ttsSpeed" | "soundEnabled" | "soundVolume" | "systemNotificationsEnabled"
>
isChangeDetected: boolean
}

export const NotificationSettings = ({
ttsEnabled,
ttsSpeed,
soundEnabled,
soundVolume,
systemNotificationsEnabled,
isChangeDetected,
setCachedStateField,
...props
}: NotificationSettingsProps) => {
const { t } = useAppTranslation()

const onTestNotificationClick = () => {
vscode.postMessage({
type: "showSystemNotification",
notificationOptions: {
title: t("settings:notifications.system.testTitle"),
message: t("settings:notifications.system.testMessage"),
force: true,
},
})
}
return (
<div {...props}>
<SectionHeader>
Expand All @@ -35,6 +53,26 @@ export const NotificationSettings = ({
</SectionHeader>

<Section>
<div>
<VSCodeCheckbox
checked={systemNotificationsEnabled}
onChange={(e: any) => setCachedStateField("systemNotificationsEnabled", e.target.checked)}
data-testid="system-notifications-enabled-checkbox">
<span className="font-medium">{t("settings:notifications.system.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-2">
{t("settings:notifications.system.description")}
</div>
{systemNotificationsEnabled && !isChangeDetected && (
<VSCodeButton
appearance="secondary"
onClick={onTestNotificationClick}
data-testid="test-system-notification-button">
{t("settings:notifications.system.testButton")}
</VSCodeButton>
)}
</div>

<div>
<VSCodeCheckbox
checked={ttsEnabled}
Expand Down
Loading
Loading