feat: complete desktop notification system UI integration

- Add desktop notification settings to NotificationSettings component
- Update SettingsView to pass desktop notification props
- Add desktop notification message handlers in webviewMessageHandler
- Update global settings types to include desktop notification settings
- Add translation keys for desktop notification UI elements
- Integrate desktop notification settings into state management

This completes the desktop notification system implementation with:
- Cross-platform OS notifications using node-notifier
- User configurable settings for different notification types
- Timeout control for notification display duration
- Full UI integration in VSCode extension settings panel
This commit is contained in:
Roo Code 2025-07-18 18:02:42 +00:00
parent c2fe775757
commit fe1d864053
8 changed files with 188 additions and 1 deletions

View file

@ -69,6 +69,13 @@ export const globalSettingsSchema = z.object({
soundEnabled: z.boolean().optional(),
soundVolume: z.number().optional(),
// Desktop notification settings
desktopNotificationsEnabled: z.boolean().optional(),
desktopNotificationApprovalRequests: z.boolean().optional(),
desktopNotificationErrors: z.boolean().optional(),
desktopNotificationTaskCompletion: z.boolean().optional(),
desktopNotificationTimeout: z.number().optional(),
maxOpenTabsContext: z.number().optional(),
maxWorkspaceFiles: z.number().optional(),
showRooIgnoredFiles: z.boolean().optional(),

View file

@ -1407,6 +1407,7 @@ export class ClineProvider
listApiConfigMeta,
pinnedApiConfigs,
mode,
modeApiConfigs,
customModePrompts,
customSupportPrompts,
enhancementApiConfigId,
@ -1434,6 +1435,11 @@ export class ClineProvider
profileThresholds,
alwaysAllowFollowupQuestions,
followupAutoApproveTimeoutMs,
desktopNotificationsEnabled,
desktopNotificationApprovalRequests,
desktopNotificationErrors,
desktopNotificationTaskCompletion,
desktopNotificationTimeout,
} = await this.getState()
const telemetryKey = process.env.POSTHOG_API_KEY
@ -1506,6 +1512,7 @@ export class ClineProvider
listApiConfigMeta: listApiConfigMeta ?? [],
pinnedApiConfigs: pinnedApiConfigs ?? {},
mode: mode ?? defaultModeSlug,
modeApiConfigs: modeApiConfigs ?? {},
customModePrompts: customModePrompts ?? {},
customSupportPrompts: customSupportPrompts ?? {},
enhancementApiConfigId,
@ -1553,6 +1560,12 @@ export class ClineProvider
hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false,
alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false,
followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000,
// Desktop notification settings
desktopNotificationsEnabled: desktopNotificationsEnabled ?? false,
desktopNotificationApprovalRequests: desktopNotificationApprovalRequests ?? true,
desktopNotificationErrors: desktopNotificationErrors ?? true,
desktopNotificationTaskCompletion: desktopNotificationTaskCompletion ?? true,
desktopNotificationTimeout: desktopNotificationTimeout ?? 10000,
}
}
@ -1715,6 +1728,12 @@ export class ClineProvider
codebaseIndexSearchMinScore: stateValues.codebaseIndexConfig?.codebaseIndexSearchMinScore,
},
profileThresholds: stateValues.profileThresholds ?? {},
// Desktop notification settings
desktopNotificationsEnabled: stateValues.desktopNotificationsEnabled ?? false,
desktopNotificationApprovalRequests: stateValues.desktopNotificationApprovalRequests ?? true,
desktopNotificationErrors: stateValues.desktopNotificationErrors ?? true,
desktopNotificationTaskCompletion: stateValues.desktopNotificationTaskCompletion ?? true,
desktopNotificationTimeout: stateValues.desktopNotificationTimeout ?? 10000,
}
}

View file

@ -964,6 +964,26 @@ export const webviewMessageHandler = async (
case "stopTts":
stopTts()
break
case "desktopNotificationsEnabled":
await updateGlobalState("desktopNotificationsEnabled", message.bool ?? true)
await provider.postStateToWebview()
break
case "desktopNotificationApprovalRequests":
await updateGlobalState("desktopNotificationApprovalRequests", message.bool ?? true)
await provider.postStateToWebview()
break
case "desktopNotificationErrors":
await updateGlobalState("desktopNotificationErrors", message.bool ?? true)
await provider.postStateToWebview()
break
case "desktopNotificationTaskCompletion":
await updateGlobalState("desktopNotificationTaskCompletion", message.bool ?? true)
await provider.postStateToWebview()
break
case "desktopNotificationTimeout":
await updateGlobalState("desktopNotificationTimeout", message.value ?? 10000)
await provider.postStateToWebview()
break
case "diffEnabled":
const diffEnabled = message.bool ?? true
await updateGlobalState("diffEnabled", diffEnabled)

View file

@ -195,6 +195,11 @@ export type ExtensionState = Pick<
| "ttsSpeed"
| "soundEnabled"
| "soundVolume"
| "desktopNotificationsEnabled"
| "desktopNotificationApprovalRequests"
| "desktopNotificationErrors"
| "desktopNotificationTaskCompletion"
| "desktopNotificationTimeout"
// | "maxOpenTabsContext" // Optional in GlobalSettings, required here.
// | "maxWorkspaceFiles" // Optional in GlobalSettings, required here.
// | "showRooIgnoredFiles" // Optional in GlobalSettings, required here.

View file

@ -92,6 +92,14 @@ export interface WebviewMessage {
| "ttsEnabled"
| "ttsSpeed"
| "soundVolume"
| "desktopNotificationsEnabled"
| "desktopNotificationApprovalRequests"
| "desktopNotificationErrors"
| "desktopNotificationTaskCompletion"
| "desktopNotificationUserInputRequired"
| "desktopNotificationSessionTimeouts"
| "desktopNotificationTimeout"
| "desktopNotificationSound"
| "diffEnabled"
| "enableCheckpoints"
| "browserViewportSize"

View file

@ -13,7 +13,23 @@ type NotificationSettingsProps = HTMLAttributes<HTMLDivElement> & {
ttsSpeed?: number
soundEnabled?: boolean
soundVolume?: number
setCachedStateField: SetCachedStateField<"ttsEnabled" | "ttsSpeed" | "soundEnabled" | "soundVolume">
// Desktop notification settings
desktopNotificationsEnabled?: boolean
desktopNotificationApprovalRequests?: boolean
desktopNotificationErrors?: boolean
desktopNotificationTaskCompletion?: boolean
desktopNotificationTimeout?: number
setCachedStateField: SetCachedStateField<
| "ttsEnabled"
| "ttsSpeed"
| "soundEnabled"
| "soundVolume"
| "desktopNotificationsEnabled"
| "desktopNotificationApprovalRequests"
| "desktopNotificationErrors"
| "desktopNotificationTaskCompletion"
| "desktopNotificationTimeout"
>
}
export const NotificationSettings = ({
@ -21,6 +37,11 @@ export const NotificationSettings = ({
ttsSpeed,
soundEnabled,
soundVolume,
desktopNotificationsEnabled,
desktopNotificationApprovalRequests,
desktopNotificationErrors,
desktopNotificationTaskCompletion,
desktopNotificationTimeout,
setCachedStateField,
...props
}: NotificationSettingsProps) => {
@ -100,6 +121,78 @@ export const NotificationSettings = ({
</div>
</div>
)}
<div>
<VSCodeCheckbox
checked={desktopNotificationsEnabled}
onChange={(e: any) => setCachedStateField("desktopNotificationsEnabled", e.target.checked)}
data-testid="desktop-notifications-enabled-checkbox">
<span className="font-medium">{t("settings:notifications.desktop.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:notifications.desktop.description")}
</div>
</div>
{desktopNotificationsEnabled && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<VSCodeCheckbox
checked={desktopNotificationApprovalRequests}
onChange={(e: any) => setCachedStateField("desktopNotificationApprovalRequests", e.target.checked)}
data-testid="desktop-notification-approval-requests-checkbox">
<span className="font-medium">{t("settings:notifications.desktop.approvalRequests.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:notifications.desktop.approvalRequests.description")}
</div>
</div>
<div>
<VSCodeCheckbox
checked={desktopNotificationErrors}
onChange={(e: any) => setCachedStateField("desktopNotificationErrors", e.target.checked)}
data-testid="desktop-notification-errors-checkbox">
<span className="font-medium">{t("settings:notifications.desktop.errors.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:notifications.desktop.errors.description")}
</div>
</div>
<div>
<VSCodeCheckbox
checked={desktopNotificationTaskCompletion}
onChange={(e: any) => setCachedStateField("desktopNotificationTaskCompletion", e.target.checked)}
data-testid="desktop-notification-task-completion-checkbox">
<span className="font-medium">{t("settings:notifications.desktop.taskCompletion.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:notifications.desktop.taskCompletion.description")}
</div>
</div>
<div>
<label className="block font-medium mb-1">
{t("settings:notifications.desktop.timeout.label")}
</label>
<div className="flex items-center gap-2">
<Slider
min={0}
max={60}
step={1}
value={[Math.round((desktopNotificationTimeout ?? 10000) / 1000)]}
onValueChange={([value]) => setCachedStateField("desktopNotificationTimeout", value * 1000)}
data-testid="desktop-notification-timeout-slider"
/>
<span className="w-10">{Math.round((desktopNotificationTimeout ?? 10000) / 1000)}s</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:notifications.desktop.timeout.description")}
</div>
</div>
</div>
)}
</Section>
</div>
)

View file

@ -176,6 +176,11 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
alwaysAllowFollowupQuestions,
alwaysAllowUpdateTodoList,
followupAutoApproveTimeoutMs,
desktopNotificationsEnabled,
desktopNotificationApprovalRequests,
desktopNotificationErrors,
desktopNotificationTaskCompletion,
desktopNotificationTimeout,
} = cachedState
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
@ -323,6 +328,11 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
vscode.postMessage({ type: "profileThresholds", values: profileThresholds })
vscode.postMessage({ type: "desktopNotificationsEnabled", bool: desktopNotificationsEnabled })
vscode.postMessage({ type: "desktopNotificationApprovalRequests", bool: desktopNotificationApprovalRequests })
vscode.postMessage({ type: "desktopNotificationErrors", bool: desktopNotificationErrors })
vscode.postMessage({ type: "desktopNotificationTaskCompletion", bool: desktopNotificationTaskCompletion })
vscode.postMessage({ type: "desktopNotificationTimeout", value: desktopNotificationTimeout })
setChangeDetected(false)
}
}
@ -640,6 +650,11 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
ttsSpeed={ttsSpeed}
soundEnabled={soundEnabled}
soundVolume={soundVolume}
desktopNotificationsEnabled={desktopNotificationsEnabled}
desktopNotificationApprovalRequests={desktopNotificationApprovalRequests}
desktopNotificationErrors={desktopNotificationErrors}
desktopNotificationTaskCompletion={desktopNotificationTaskCompletion}
desktopNotificationTimeout={desktopNotificationTimeout}
setCachedStateField={setCachedStateField}
/>
)}

View file

@ -426,6 +426,26 @@
"label": "Enable text-to-speech",
"description": "When enabled, Roo will read aloud its responses using text-to-speech.",
"speedLabel": "Speed"
},
"desktop": {
"label": "Enable desktop notifications",
"description": "When enabled, Roo will show OS-level desktop notifications for important events.",
"approvalRequests": {
"label": "Show approval request notifications",
"description": "Get notified when Roo needs approval to perform actions."
},
"errors": {
"label": "Show error notifications",
"description": "Get notified when errors occur during task execution."
},
"taskCompletion": {
"label": "Show task completion notifications",
"description": "Get notified when tasks are completed successfully."
},
"timeout": {
"label": "Notification timeout",
"description": "How long notifications stay visible (in seconds). Set to 0 for no timeout."
}
}
},
"contextManagement": {