Skip to content

Commit 75cd720

Browse files
committed
system notification like kilocode
1 parent 966ed76 commit 75cd720

29 files changed

+462
-3
lines changed

packages/types/src/global-settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export const globalSettingsSchema = z.object({
101101
ttsSpeed: z.number().optional(),
102102
soundEnabled: z.boolean().optional(),
103103
soundVolume: z.number().optional(),
104+
systemNotificationsEnabled: z.boolean().optional(),
104105

105106
maxOpenTabsContext: z.number().optional(),
106107
maxWorkspaceFiles: z.number().optional(),
@@ -281,6 +282,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
281282
ttsSpeed: 1,
282283
soundEnabled: false,
283284
soundVolume: 0.5,
285+
systemNotificationsEnabled: false,
284286

285287
terminalOutputLineLimit: 500,
286288
terminalOutputCharacterLimit: DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,

src/core/webview/ClineProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1579,6 +1579,7 @@ export class ClineProvider
15791579
soundEnabled,
15801580
ttsEnabled,
15811581
ttsSpeed,
1582+
systemNotificationsEnabled,
15821583
diffEnabled,
15831584
enableCheckpoints,
15841585
taskHistory,
@@ -1689,6 +1690,7 @@ export class ClineProvider
16891690
soundEnabled: soundEnabled ?? false,
16901691
ttsEnabled: ttsEnabled ?? false,
16911692
ttsSpeed: ttsSpeed ?? 1.0,
1693+
systemNotificationsEnabled: systemNotificationsEnabled ?? false,
16921694
diffEnabled: diffEnabled ?? true,
16931695
enableCheckpoints: enableCheckpoints ?? true,
16941696
shouldShowAnnouncement:
@@ -1897,6 +1899,7 @@ export class ClineProvider
18971899
soundEnabled: stateValues.soundEnabled ?? false,
18981900
ttsEnabled: stateValues.ttsEnabled ?? false,
18991901
ttsSpeed: stateValues.ttsSpeed ?? 1.0,
1902+
systemNotificationsEnabled: stateValues.systemNotificationsEnabled ?? false,
19001903
diffEnabled: stateValues.diffEnabled ?? true,
19011904
enableCheckpoints: stateValues.enableCheckpoints ?? true,
19021905
soundVolume: stateValues.soundVolume,

src/core/webview/webviewMessageHandler.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { checkExistKey } from "../../shared/checkExistApiConfig"
3333
import { experimentDefault } from "../../shared/experiments"
3434
import { Terminal } from "../../integrations/terminal/Terminal"
3535
import { openFile } from "../../integrations/misc/open-file"
36+
import { showSystemNotification } from "../../integrations/notifications"
3637
import { openImage, saveImage } from "../../integrations/misc/image-handler"
3738
import { selectImages } from "../../integrations/misc/process-images"
3839
import { getTheme } from "../../integrations/theme/getTheme"
@@ -992,6 +993,20 @@ export const webviewMessageHandler = async (
992993
await updateGlobalState("soundVolume", soundVolume)
993994
await provider.postStateToWebview()
994995
break
996+
case "showSystemNotification":
997+
const isSystemNotificationsEnabled = getGlobalState("systemNotificationsEnabled") ?? false
998+
if (!isSystemNotificationsEnabled) {
999+
break
1000+
}
1001+
if (message.notificationOptions) {
1002+
showSystemNotification(message.notificationOptions)
1003+
}
1004+
break
1005+
case "systemNotificationsEnabled":
1006+
const systemNotificationsEnabled = message.bool ?? true
1007+
await updateGlobalState("systemNotificationsEnabled", systemNotificationsEnabled)
1008+
await provider.postStateToWebview()
1009+
break
9951010
case "ttsEnabled":
9961011
const ttsEnabled = message.bool ?? true
9971012
await updateGlobalState("ttsEnabled", ttsEnabled)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { execa } from "execa"
2+
import { platform } from "os"
3+
4+
interface NotificationOptions {
5+
title?: string
6+
subtitle?: string
7+
message: string
8+
}
9+
10+
async function showMacOSNotification(options: NotificationOptions): Promise<void> {
11+
const { title, subtitle = "", message } = options
12+
13+
const script = `display notification "${message}" with title "${title}" subtitle "${subtitle}" sound name "Tink"`
14+
15+
try {
16+
await execa("osascript", ["-e", script])
17+
} catch (error) {
18+
throw new Error(`Failed to show macOS notification: ${error}`)
19+
}
20+
}
21+
22+
async function showWindowsNotification(options: NotificationOptions): Promise<void> {
23+
const { subtitle, message } = options
24+
25+
const script = `
26+
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
27+
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
28+
29+
$template = @"
30+
<toast>
31+
<visual>
32+
<binding template="ToastText02">
33+
<text id="1">${subtitle}</text>
34+
<text id="2">${message}</text>
35+
</binding>
36+
</visual>
37+
</toast>
38+
"@
39+
40+
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
41+
$xml.LoadXml($template)
42+
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
43+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Roo Code").Show($toast)
44+
`
45+
46+
try {
47+
await execa("powershell", ["-Command", script])
48+
} catch (error) {
49+
throw new Error(`Failed to show Windows notification: ${error}`)
50+
}
51+
}
52+
53+
async function showLinuxNotification(options: NotificationOptions): Promise<void> {
54+
const { title = "", subtitle = "", message } = options
55+
56+
// Combine subtitle and message if subtitle exists
57+
const fullMessage = subtitle ? `${subtitle}\n${message}` : message
58+
59+
try {
60+
await execa("notify-send", [title, fullMessage])
61+
} catch (error) {
62+
throw new Error(`Failed to show Linux notification: ${error}`)
63+
}
64+
}
65+
66+
export async function showSystemNotification(options: NotificationOptions): Promise<void> {
67+
try {
68+
const { title = "Roo Code", message } = options
69+
70+
if (!message) {
71+
throw new Error("Message is required")
72+
}
73+
74+
const escapedOptions = {
75+
...options,
76+
title: title.replace(/"/g, '\\"'),
77+
message: message.replace(/"/g, '\\"'),
78+
subtitle: options.subtitle?.replace(/"/g, '\\"') || "",
79+
}
80+
81+
switch (platform()) {
82+
case "darwin":
83+
await showMacOSNotification(escapedOptions)
84+
break
85+
case "win32":
86+
await showWindowsNotification(escapedOptions)
87+
break
88+
case "linux":
89+
await showLinuxNotification(escapedOptions)
90+
break
91+
default:
92+
throw new Error("Unsupported platform")
93+
}
94+
} catch (error) {
95+
console.error("Could not show system notification", error)
96+
}
97+
}

src/shared/ExtensionMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export interface ExtensionMessage {
9999
| "remoteBrowserEnabled"
100100
| "ttsStart"
101101
| "ttsStop"
102+
| "showSystemNotification"
102103
| "maxReadFileLine"
103104
| "fileSearchResults"
104105
| "toggleApiConfigPin"
@@ -240,6 +241,7 @@ export type ExtensionState = Pick<
240241
| "ttsSpeed"
241242
| "soundEnabled"
242243
| "soundVolume"
244+
| "systemNotificationsEnabled"
243245
// | "maxOpenTabsContext" // Optional in GlobalSettings, required here.
244246
// | "maxWorkspaceFiles" // Optional in GlobalSettings, required here.
245247
// | "showRooIgnoredFiles" // Optional in GlobalSettings, required here.

src/shared/WebviewMessage.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ export interface WebviewMessage {
9797
| "ttsEnabled"
9898
| "ttsSpeed"
9999
| "soundVolume"
100+
| "showSystemNotification"
101+
| "systemNotificationsEnabled"
100102
| "diffEnabled"
101103
| "enableCheckpoints"
102104
| "browserViewportSize"
@@ -291,6 +293,11 @@ export interface WebviewMessage {
291293
codebaseIndexMistralApiKey?: string
292294
codebaseIndexVercelAiGatewayApiKey?: string
293295
}
296+
notificationOptions?: {
297+
title?: string
298+
subtitle?: string
299+
message: string
300+
}
294301
}
295302

296303
export const checkoutDiffPayloadSchema = z.object({

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import RooCloudCTA from "@src/components/welcome/RooCloudCTA"
4141
import { StandardTooltip } from "@src/components/ui"
4242
import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
4343
import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
44+
import { showSystemNotification } from "@src/utils/showSystemNotification"
4445

4546
import TelemetryBanner from "../common/TelemetryBanner"
4647
import VersionIndicator from "../common/VersionIndicator"
@@ -56,7 +57,6 @@ import SystemPromptWarning from "./SystemPromptWarning"
5657
import ProfileViolationWarning from "./ProfileViolationWarning"
5758
import { CheckpointWarning } from "./CheckpointWarning"
5859
import { QueuedMessages } from "./QueuedMessages"
59-
6060
export interface ChatViewProps {
6161
isHidden: boolean
6262
showAnnouncement: boolean
@@ -280,6 +280,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
280280
switch (lastMessage.ask) {
281281
case "api_req_failed":
282282
playSound("progress_loop")
283+
showSystemNotification(t("settings:notifications.system.apiReqFailed"))
283284
setSendingDisabled(true)
284285
setClineAsk("api_req_failed")
285286
setEnableButtons(true)
@@ -288,6 +289,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
288289
break
289290
case "mistake_limit_reached":
290291
playSound("progress_loop")
292+
showSystemNotification(t("settings:notifications.system.mistakeLimitReached"))
291293
setSendingDisabled(false)
292294
setClineAsk("mistake_limit_reached")
293295
setEnableButtons(true)
@@ -297,6 +299,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
297299
case "followup":
298300
if (!isPartial) {
299301
playSound("notification")
302+
showSystemNotification(t("settings:notifications.system.followup"))
300303
}
301304
setSendingDisabled(isPartial)
302305
setClineAsk("followup")
@@ -311,6 +314,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
311314
case "tool":
312315
if (!isAutoApproved(lastMessage) && !isPartial) {
313316
playSound("notification")
317+
showSystemNotification(t("settings:notifications.system.toolRequest"))
314318
}
315319
setSendingDisabled(isPartial)
316320
setClineAsk("tool")
@@ -347,6 +351,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
347351
case "browser_action_launch":
348352
if (!isAutoApproved(lastMessage) && !isPartial) {
349353
playSound("notification")
354+
showSystemNotification(t("settings:notifications.system.browserAction"))
350355
}
351356
setSendingDisabled(isPartial)
352357
setClineAsk("browser_action_launch")
@@ -357,6 +362,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
357362
case "command":
358363
if (!isAutoApproved(lastMessage) && !isPartial) {
359364
playSound("notification")
365+
showSystemNotification(t("settings:notifications.system.command"))
360366
}
361367
setSendingDisabled(isPartial)
362368
setClineAsk("command")
@@ -374,6 +380,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
374380
case "use_mcp_server":
375381
if (!isAutoApproved(lastMessage) && !isPartial) {
376382
playSound("notification")
383+
showSystemNotification(t("settings:notifications.system.useMcpServer"))
377384
}
378385
setSendingDisabled(isPartial)
379386
setClineAsk("use_mcp_server")
@@ -385,6 +392,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
385392
// extension waiting for feedback. but we can just present a new task button
386393
if (!isPartial) {
387394
playSound("celebration")
395+
showSystemNotification(t("settings:notifications.system.completionResult"))
388396
}
389397
setSendingDisabled(isPartial)
390398
setClineAsk("completion_result")

webview-ui/src/components/settings/NotificationSettings.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,47 @@
11
import { HTMLAttributes } from "react"
22
import { useAppTranslation } from "@/i18n/TranslationContext"
3-
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
3+
import { VSCodeCheckbox, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
44
import { Bell } from "lucide-react"
55

66
import { SetCachedStateField } from "./types"
77
import { SectionHeader } from "./SectionHeader"
88
import { Section } from "./Section"
99
import { Slider } from "../ui"
10+
import { vscode } from "../../utils/vscode"
1011

1112
type NotificationSettingsProps = HTMLAttributes<HTMLDivElement> & {
1213
ttsEnabled?: boolean
1314
ttsSpeed?: number
1415
soundEnabled?: boolean
1516
soundVolume?: number
16-
setCachedStateField: SetCachedStateField<"ttsEnabled" | "ttsSpeed" | "soundEnabled" | "soundVolume">
17+
systemNotificationsEnabled?: boolean
18+
setCachedStateField: SetCachedStateField<
19+
"ttsEnabled" | "ttsSpeed" | "soundEnabled" | "soundVolume" | "systemNotificationsEnabled"
20+
>
21+
isChangeDetected: boolean
1722
}
1823

1924
export const NotificationSettings = ({
2025
ttsEnabled,
2126
ttsSpeed,
2227
soundEnabled,
2328
soundVolume,
29+
systemNotificationsEnabled,
30+
isChangeDetected,
2431
setCachedStateField,
2532
...props
2633
}: NotificationSettingsProps) => {
2734
const { t } = useAppTranslation()
35+
36+
const onTestNotificationClick = () => {
37+
vscode.postMessage({
38+
type: "showSystemNotification",
39+
notificationOptions: {
40+
title: t("settings.notifications.system.testTitle"),
41+
message: t("settings.notifications.system.testMessage"),
42+
},
43+
})
44+
}
2845
return (
2946
<div {...props}>
3047
<SectionHeader>
@@ -35,6 +52,26 @@ export const NotificationSettings = ({
3552
</SectionHeader>
3653

3754
<Section>
55+
<div>
56+
<VSCodeCheckbox
57+
checked={systemNotificationsEnabled}
58+
onChange={(e: any) => setCachedStateField("systemNotificationsEnabled", e.target.checked)}
59+
data-testid="system-notifications-enabled-checkbox">
60+
<span className="font-medium">{t("settings:notifications.system.label")}</span>
61+
</VSCodeCheckbox>
62+
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-2">
63+
{t("settings:notifications.system.description")}
64+
</div>
65+
{systemNotificationsEnabled && !isChangeDetected && (
66+
<VSCodeButton
67+
appearance="secondary"
68+
onClick={onTestNotificationClick}
69+
data-testid="test-system-notification-button">
70+
{t("settings:notifications.system.testButton")}
71+
</VSCodeButton>
72+
)}
73+
</div>
74+
3875
<div>
3976
<VSCodeCheckbox
4077
checked={ttsEnabled}

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
157157
ttsEnabled,
158158
ttsSpeed,
159159
soundVolume,
160+
systemNotificationsEnabled,
160161
telemetrySetting,
161162
terminalOutputLineLimit,
162163
terminalOutputCharacterLimit,
@@ -319,6 +320,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
319320
vscode.postMessage({ type: "ttsEnabled", bool: ttsEnabled })
320321
vscode.postMessage({ type: "ttsSpeed", value: ttsSpeed })
321322
vscode.postMessage({ type: "soundVolume", value: soundVolume })
323+
vscode.postMessage({ type: "systemNotificationsEnabled", bool: systemNotificationsEnabled })
322324
vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
323325
vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints })
324326
vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize })
@@ -695,7 +697,9 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
695697
ttsSpeed={ttsSpeed}
696698
soundEnabled={soundEnabled}
697699
soundVolume={soundVolume}
700+
systemNotificationsEnabled={systemNotificationsEnabled}
698701
setCachedStateField={setCachedStateField}
702+
isChangeDetected={isChangeDetected}
699703
/>
700704
)}
701705

0 commit comments

Comments
 (0)