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
5 changes: 5 additions & 0 deletions .changeset/sound-notifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Add sound playback for notification events in the VS Code extension. Users can select from 45+ notification sounds or use the system default sound. A test button allows previewing sounds before committing to a selection. Sounds play on agent completion, permission requests, questions, and errors.
3 changes: 3 additions & 0 deletions packages/kilo-vscode/.vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ AGENTS.md

# Include bin/ directory for CLI binary (production)
!bin/**

# Include audio files for sound notifications
!audio-wav/**
Binary file added packages/kilo-vscode/audio-wav/alert-01.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/alert-02.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/alert-03.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/alert-04.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/alert-05.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/alert-06.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/alert-07.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/alert-08.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/alert-09.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/alert-10.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/bip-bop-01.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/bip-bop-02.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/bip-bop-03.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/bip-bop-04.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/bip-bop-05.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/bip-bop-06.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/bip-bop-07.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/bip-bop-08.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/bip-bop-09.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/bip-bop-10.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/nope-01.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/nope-02.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/nope-03.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/nope-04.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/nope-05.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/nope-06.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/nope-07.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/nope-08.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/nope-09.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/nope-10.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/nope-11.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/nope-12.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/staplebops-01.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/staplebops-02.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/staplebops-03.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/staplebops-04.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/staplebops-05.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/staplebops-06.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/staplebops-07.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/yup-01.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/yup-02.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/yup-03.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/yup-04.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/yup-05.wav
Binary file not shown.
Binary file added packages/kilo-vscode/audio-wav/yup-06.wav
Binary file not shown.
155 changes: 149 additions & 6 deletions packages/kilo-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -952,30 +952,173 @@
"default": true,
"description": "Show notification on errors"
},
"kilo-code.new.notifications.playWhenFocused": {
"type": "boolean",
"default": false,
"description": "Play sounds even when VS Code is focused"
},
"kilo-code.new.sounds.agent": {
"type": "string",
"default": "default",
"default": "system",
"enum": [
"default",
"none"
"none",
"system",
"alert-01",
"alert-02",
"alert-03",
"alert-04",
"alert-05",
"alert-06",
"alert-07",
"alert-08",
"alert-09",
"alert-10",
"bip-bop-01",
"bip-bop-02",
"bip-bop-03",
"bip-bop-04",
"bip-bop-05",
"bip-bop-06",
"bip-bop-07",
"bip-bop-08",
"bip-bop-09",
"bip-bop-10",
"staplebops-01",
"staplebops-02",
"staplebops-03",
"staplebops-04",
"staplebops-05",
"staplebops-06",
"staplebops-07",
"nope-01",
"nope-02",
"nope-03",
"nope-04",
"nope-05",
"nope-06",
"nope-07",
"nope-08",
"nope-09",
"nope-10",
"nope-11",
"nope-12",
"yup-01",
"yup-02",
"yup-03",
"yup-04",
"yup-05",
"yup-06"
],
"description": "Sound to play when agent completes"
},
"kilo-code.new.sounds.permissions": {
"type": "string",
"default": "default",
"default": "system",
"enum": [
"default",
"none"
"none",
"system",
"alert-01",
"alert-02",
"alert-03",
"alert-04",
"alert-05",
"alert-06",
"alert-07",
"alert-08",
"alert-09",
"alert-10",
"bip-bop-01",
"bip-bop-02",
"bip-bop-03",
"bip-bop-04",
"bip-bop-05",
"bip-bop-06",
"bip-bop-07",
"bip-bop-08",
"bip-bop-09",
"bip-bop-10",
"staplebops-01",
"staplebops-02",
"staplebops-03",
"staplebops-04",
"staplebops-05",
"staplebops-06",
"staplebops-07",
"nope-01",
"nope-02",
"nope-03",
"nope-04",
"nope-05",
"nope-06",
"nope-07",
"nope-08",
"nope-09",
"nope-10",
"nope-11",
"nope-12",
"yup-01",
"yup-02",
"yup-03",
"yup-04",
"yup-05",
"yup-06"
],
"description": "Sound to play on permission requests"
},
"kilo-code.new.sounds.errors": {
"type": "string",
"default": "default",
"default": "system",
"enum": [
"default",
"none"
"none",
"system",
"alert-01",
"alert-02",
"alert-03",
"alert-04",
"alert-05",
"alert-06",
"alert-07",
"alert-08",
"alert-09",
"alert-10",
"bip-bop-01",
"bip-bop-02",
"bip-bop-03",
"bip-bop-04",
"bip-bop-05",
"bip-bop-06",
"bip-bop-07",
"bip-bop-08",
"bip-bop-09",
"bip-bop-10",
"staplebops-01",
"staplebops-02",
"staplebops-03",
"staplebops-04",
"staplebops-05",
"staplebops-06",
"staplebops-07",
"nope-01",
"nope-02",
"nope-03",
"nope-04",
"nope-05",
"nope-06",
"nope-07",
"nope-08",
"nope-09",
"nope-10",
"nope-11",
"nope-12",
"yup-01",
"yup-02",
"yup-03",
"yup-04",
"yup-05",
"yup-06"
],
"description": "Sound to play on errors"
},
Expand Down
32 changes: 16 additions & 16 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { watchFontSizeConfig } from "./kilo-provider/font-size"
import { getTerminalContents } from "./services/terminal/context"
import { disposeGitChangesTarget } from "./kilo-provider/git-changes-target"
import { interceptMessage } from "./kilo-provider/git-changes-request"
import { SoundNotificationService } from "./util/sound-notification"
import { matchFollowup, recordFollowup, type Followup } from "./kilo-provider/followup-session"
import { clearCommandsCache, loadCommands } from "./kilo-provider/commands"
import { fetchMessagePage, MESSAGE_PAGE_LIMIT } from "./kilo-provider/message-page"
Expand Down Expand Up @@ -160,7 +161,6 @@ const mapAgent = (a: Agent) => ({
model: a.model,
})

// message.part.* events are always session-scoped; drop them when the session is unknown.
const SESSION_SCOPED_PART_EVENTS = new Set(["message.part.updated", "message.part.delta", "message.part.removed"])
const isSessionScopedPartEvent = (type: string) => SESSION_SCOPED_PART_EVENTS.has(type)

Expand Down Expand Up @@ -244,7 +244,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private chatAutocomplete: ChatTextAreaAutocomplete | null = null
private projectDirectory: string | null | undefined
private slimEditMetadata = true

private pendingFollowup: Followup | null = null
private followupListeners: Array<(session: Session, directory: string) => void> = []
private statsPoller: GitStatsPoller | null = null
Expand All @@ -263,6 +262,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private diffVirtualProvider: import("./DiffVirtualProvider").DiffVirtualProvider | undefined
private remoteService: RemoteStatusService | null = null
private unsubscribeRemote: (() => void) | null = null
private soundService: SoundNotificationService

constructor(
private readonly extensionUri: vscode.Uri,
Expand All @@ -272,7 +272,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
) {
this.projectDirectory = opts.projectDirectory
this.slimEditMetadata = opts.slimEditMetadata ?? true

this.soundService = new SoundNotificationService(connectionService)
this.soundService.setupFocusTracking()
TelemetryProxy.getInstance().setProvider(this)
}

Expand Down Expand Up @@ -443,7 +444,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
// legacy-migration end
}

public resolveWebviewView(
webviewView: vscode.WebviewView,
_context: vscode.WebviewViewResolveContext,
Expand Down Expand Up @@ -972,6 +972,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "requestNotificationSettings":
this.sendNotificationSettings()
break
case "testNotification":
this.soundService.handleTestNotification(message.settingType)
break
case "requestTimelineSetting":
this.sendTimelineSetting()
break
Expand Down Expand Up @@ -1104,17 +1107,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
await this.handleFetchMarketplaceData()
break
}
case "filterMarketplaceItems": {
// Client-side filtering — no server action needed
case "filterMarketplaceItems":
break
}
case "installMarketplaceItem": {
const workspace = this.getProjectDirectory(this.currentSession?.id)
const scope = message.mpInstallOptions?.target ?? "project"
const result = await this.getMarketplace().install(message.mpItem, message.mpInstallOptions, workspace)
if (result.success) {
await this.invalidateAfterMarketplaceChange(scope)
}
if (result.success) await this.invalidateAfterMarketplaceChange(scope)
this.postMessage({
type: "marketplaceInstallResult",
success: result.success,
Expand Down Expand Up @@ -1296,7 +1295,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
// Get current state and push to webview
const serverInfo = this.connectionService.getServerInfo()
this.connectionState = this.connectionService.getConnectionState()

if (serverInfo) {
const langConfig = vscode.workspace.getConfiguration("kilo-code.new")
this.postMessage({
Expand Down Expand Up @@ -2387,9 +2385,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
notifyAgent: notifications.get<boolean>("agent", true),
notifyPermissions: notifications.get<boolean>("permissions", true),
notifyErrors: notifications.get<boolean>("errors", true),
soundAgent: sounds.get<string>("agent", "default"),
soundPermissions: sounds.get<string>("permissions", "default"),
soundErrors: sounds.get<string>("errors", "default"),
playWhenFocused: notifications.get<boolean>("playWhenFocused", false),
soundAgent: sounds.get<string>("agent", "system"),
soundPermissions: sounds.get<string>("permissions", "system"),
soundErrors: sounds.get<string>("errors", "system"),
},
})
}
Expand All @@ -2401,7 +2400,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
visible: config.get<boolean>("showTaskTimeline", true),
})
}

/** Returns the number of sessions currently in "busy" state. */
private getBusySessionCount(): number {
return getBusySessionCount(this.sessionStatusMap)
Expand Down Expand Up @@ -3178,6 +3176,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.streams.flush(sid)
this.postMessage(msg)
}
this.soundService.playSoundForEvent(event, sid, this.trackedSessionIds)
return
}

Expand Down Expand Up @@ -3254,7 +3253,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
if (event.type === "indexing.status" && directory) {
if (!sameDirectory(directory, this.getWorkspaceDirectory(this.currentSession?.id))) return
}

this.soundService.playSoundForEvent(event, sessionID, this.trackedSessionIds)
const msg = mapSSEEventToWebviewMessage(event, sessionID)
if (!msg) return
if (msg.type === "partUpdated") {
Expand Down Expand Up @@ -3587,6 +3586,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.telemetryStateDisposable?.dispose()
this.autoApproveBridge?.dispose()
this.visibleTaskStreams.clear()
this.soundService.dispose()
this.streams.dispose()
this.isWebviewReady = false
this.promptRecoveryQueued = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export class KiloConnectionService {
private connectPromise: Promise<void> | null = null
private healthPollTimer: ReturnType<typeof setInterval> | null = null
private remoteService: import("../RemoteStatusService").RemoteStatusService | null = null
private disposed = false

private readonly eventListeners: Set<SSEEventListener> = new Set()
private readonly stateListeners: Set<StateListener> = new Set()
Expand All @@ -84,6 +85,8 @@ export class KiloConnectionService {
* Used primarily for message.part.updated where only messageID may be present.
*/
private readonly messageSessionIdsByMessageId: Map<string, string> = new Map()
private readonly notifiedEvents: Set<string> = new Set()
private readonly notifiedEventTimers: Map<string, NodeJS.Timeout> = new Map()

/** Provider key → single focused session ID. */
private readonly focused: Map<string, string> = new Map()
Expand Down Expand Up @@ -302,6 +305,29 @@ export class KiloConnectionService {
}
}

/**
* Check-and-mark deduplication for OS notifications.
* Returns true the first time a given sessionID + eventType (+ optional requestID) combination is seen,
* and false for all subsequent calls until the entry expires (TTL).
* This prevents multiple KiloProvider instances from emitting duplicate
* OS notifications for the same backend event.
* When requestID is provided, distinct requests in the same session are not suppressed.
*/
shouldNotify(sessionID: string, eventType: string, requestID?: string): boolean {
if (this.disposed) return false
const key = requestID ? `${sessionID}:${eventType}:${requestID}` : `${sessionID}:${eventType}`
if (this.notifiedEvents.has(key)) return false
this.notifiedEvents.add(key)
const existing = this.notifiedEventTimers.get(key)
if (existing) clearTimeout(existing)
const timer = setTimeout(() => {
this.notifiedEvents.delete(key)
this.notifiedEventTimers.delete(key)
}, 5_000)
this.notifiedEventTimers.set(key, timer)
return true
}

/**
* Subscribe to migration-complete events broadcast from any KiloProvider. Returns unsubscribe function.
*/
Expand Down Expand Up @@ -478,6 +504,7 @@ export class KiloConnectionService {
* Clean up everything: kill server, close SSE, clear listeners.
*/
dispose(): void {
this.disposed = true
this.stopHealthPoll()
this.sseClient?.dispose()
this.serverManager.dispose()
Expand All @@ -490,6 +517,11 @@ export class KiloConnectionService {
this.clearPendingPromptsListeners.clear()
this.directoryProviders.clear()
this.messageSessionIdsByMessageId.clear()
this.notifiedEvents.clear()
for (const timer of this.notifiedEventTimers.values()) {
clearTimeout(timer)
}
this.notifiedEventTimers.clear()
this.focused.clear()
this.opened.clear()
if (this.debounceTimer) {
Expand Down
Loading
Loading