Play sound effects for notifications and events (#38)

Co-authored-by: HeavenOSK <heavenosk@gmail.com>
This commit is contained in:
Matt Rubens
2024-12-01 22:25:10 -05:00
committed by GitHub
parent ccb973ecaf
commit 4b74f290d4
14 changed files with 236 additions and 2 deletions

View File

@@ -20,6 +20,7 @@ import { Cline } from "../Cline"
import { openMention } from "../mentions"
import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
import { playSound, setSoundEnabled } from "../../utils/sound"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -61,6 +62,7 @@ type GlobalStateKey =
| "openRouterModelId"
| "openRouterModelInfo"
| "allowedCommands"
| "soundEnabled"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
@@ -520,6 +522,18 @@ export class ClineProvider implements vscode.WebviewViewProvider {
break;
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
case "playSound":
if (message.audioType) {
const soundPath = path.join(this.context.extensionPath, "audio", `${message.audioType}.wav`)
playSound(soundPath)
}
break
case "soundEnabled":
const enabled = message.bool ?? true
await this.updateGlobalState("soundEnabled", enabled)
setSoundEnabled(enabled)
await this.postStateToWebview()
break
}
},
null,
@@ -825,6 +839,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowWrite,
alwaysAllowExecute,
alwaysAllowBrowser,
soundEnabled,
taskHistory,
} = await this.getState()
@@ -845,6 +860,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
taskHistory: (taskHistory || [])
.filter((item) => item.ts && item.task)
.sort((a, b) => b.ts - a.ts),
soundEnabled: soundEnabled ?? true,
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
allowedCommands,
}
@@ -935,6 +951,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowBrowser,
taskHistory,
allowedCommands,
soundEnabled,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -968,6 +985,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("alwaysAllowBrowser") as Promise<boolean | undefined>,
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
this.getGlobalState("allowedCommands") as Promise<string[] | undefined>,
this.getGlobalState("soundEnabled") as Promise<boolean | undefined>,
])
let apiProvider: ApiProvider
@@ -1019,6 +1037,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowBrowser: alwaysAllowBrowser ?? false,
taskHistory,
allowedCommands,
soundEnabled,
}
}

View File

@@ -41,6 +41,7 @@ export interface ExtensionState {
alwaysAllowBrowser?: boolean
uriScheme?: string
allowedCommands?: string[]
soundEnabled?: boolean
}
export interface ClineMessage {

View File

@@ -1,5 +1,7 @@
import { ApiConfiguration, ApiProvider } from "./api"
export type AudioType = "notification" | "celebration" | "progress_loop"
export interface WebviewMessage {
type:
| "apiConfiguration"
@@ -27,12 +29,15 @@ export interface WebviewMessage {
| "cancelTask"
| "refreshOpenRouterModels"
| "alwaysAllowBrowser"
| "playSound"
| "soundEnabled"
text?: string
askResponse?: ClineAskResponse
apiConfiguration?: ApiConfiguration
images?: string[]
bool?: boolean
commands?: string[]
audioType?: AudioType
}
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"

68
src/utils/sound.ts Normal file
View File

@@ -0,0 +1,68 @@
import * as vscode from "vscode"
import * as path from "path"
/**
* Minimum interval (in milliseconds) to prevent continuous playback
*/
const MIN_PLAY_INTERVAL = 500
/**
* Timestamp of when sound was last played
*/
let lastPlayedTime = 0
/**
* Determine if a file is a WAV file
* @param filepath string
* @returns boolean
*/
export const isWAV = (filepath: string): boolean => {
return path.extname(filepath).toLowerCase() === ".wav"
}
let isSoundEnabled = true
/**
* Set sound configuration
* @param enabled boolean
*/
export const setSoundEnabled = (enabled: boolean): void => {
isSoundEnabled = enabled
}
/**
* Play a sound file
* @param filepath string
* @return void
*/
export const playSound = (filepath: string): void => {
try {
if (!isSoundEnabled) {
return
}
if (!filepath) {
return
}
if (!isWAV(filepath)) {
throw new Error("Only wav files are supported.")
}
const currentTime = Date.now()
if (currentTime - lastPlayedTime < MIN_PLAY_INTERVAL) {
return // Skip playback within minimum interval to prevent continuous playback
}
const player = require("play-sound")()
player.play(filepath, function (err: any) {
if (err) {
throw new Error("Failed to play sound effect")
}
})
lastPlayedTime = currentTime
} catch (error: any) {
vscode.window.showErrorMessage(error.message)
}
}