From 130090311220a1dddd30954592aff7b2f6629ba6 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 9 Mar 2026 13:00:30 +0000 Subject: [PATCH 01/23] Add event sound settings to config schema and node config --- src/common/config/eventSoundTypes.ts | 8 +++ .../config/schemas/appConfigOnDisk.test.ts | 49 +++++++++++++++ src/common/config/schemas/appConfigOnDisk.ts | 11 ++++ src/common/types/project.ts | 9 ++- src/node/config.test.ts | 59 +++++++++++++++++++ src/node/config.ts | 22 +++++-- 6 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 src/common/config/eventSoundTypes.ts diff --git a/src/common/config/eventSoundTypes.ts b/src/common/config/eventSoundTypes.ts new file mode 100644 index 0000000000..df6baf2f27 --- /dev/null +++ b/src/common/config/eventSoundTypes.ts @@ -0,0 +1,8 @@ +// Event keys for the sound system — expand this union as new events are added. +export const EVENT_SOUND_KEYS = ["agent_review_ready"] as const; +export type EventSoundKey = (typeof EVENT_SOUND_KEYS)[number]; + +// Human-readable labels for settings UI. +export const EVENT_SOUND_LABELS: Record = { + agent_review_ready: "Agent finished (waiting for review)", +}; diff --git a/src/common/config/schemas/appConfigOnDisk.test.ts b/src/common/config/schemas/appConfigOnDisk.test.ts index 707585efd6..3e5d519dc3 100644 --- a/src/common/config/schemas/appConfigOnDisk.test.ts +++ b/src/common/config/schemas/appConfigOnDisk.test.ts @@ -62,6 +62,55 @@ describe("AppConfigOnDiskSchema", () => { ).toBe(true); }); + it("accepts missing eventSoundSettings", () => { + const result = AppConfigOnDiskSchema.safeParse({}); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.eventSoundSettings).toBeUndefined(); + } + }); + + it("parses valid eventSoundSettings and applies entry defaults", () => { + const result = AppConfigOnDiskSchema.safeParse({ + eventSoundSettings: { + agent_review_ready: { + enabled: true, + }, + }, + }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.eventSoundSettings).toEqual({ + agent_review_ready: { + enabled: true, + filePath: null, + }, + }); + } + }); + + it("preserves unknown eventSoundSettings keys", () => { + const result = AppConfigOnDiskSchema.safeParse({ + eventSoundSettings: { + future_event: { + filePath: "/tmp/future.wav", + }, + }, + }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.eventSoundSettings).toEqual({ + future_event: { + enabled: false, + filePath: "/tmp/future.wav", + }, + }); + } + }); + it("preserves unknown fields via passthrough", () => { const valid = { futureField: "something" }; diff --git a/src/common/config/schemas/appConfigOnDisk.ts b/src/common/config/schemas/appConfigOnDisk.ts index ed65a8208a..0eb91c6db6 100644 --- a/src/common/config/schemas/appConfigOnDisk.ts +++ b/src/common/config/schemas/appConfigOnDisk.ts @@ -30,6 +30,13 @@ export const FeatureFlagOverrideSchema = z.enum(["default", "on", "off"]); export const UpdateChannelSchema = z.enum(["stable", "nightly"]); +export const EventSoundConfigSchema = z.object({ + enabled: z.boolean().default(false), + filePath: z.string().nullable().default(null), +}); + +export const EventSoundSettingsSchema = z.record(z.string(), EventSoundConfigSchema).optional(); + export const AppConfigOnDiskSchema = z .object({ projects: z.array(z.tuple([z.string(), ProjectConfigSchema])).optional(), @@ -63,6 +70,7 @@ export const AppConfigOnDiskSchema = z updateChannel: UpdateChannelSchema.optional(), runtimeEnablement: RuntimeEnablementOverridesSchema.optional(), defaultRuntime: RuntimeEnablementIdSchema.optional(), + eventSoundSettings: EventSoundSettingsSchema, onePasswordAccountName: z.string().optional(), }) .passthrough(); @@ -74,4 +82,7 @@ export type SubagentAiDefaults = z.infer; export type FeatureFlagOverride = z.infer; export type UpdateChannel = z.infer; +export type EventSoundConfig = z.infer; +export type EventSoundSettings = z.infer; + export type AppConfigOnDisk = z.infer; diff --git a/src/common/types/project.ts b/src/common/types/project.ts index 81d608c9ea..a32b1d6beb 100644 --- a/src/common/types/project.ts +++ b/src/common/types/project.ts @@ -3,7 +3,11 @@ * Kept lightweight for preload script usage. */ -import type { FeatureFlagOverride, UpdateChannel } from "@/common/config/schemas/appConfigOnDisk"; +import type { + EventSoundSettings, + FeatureFlagOverride, + UpdateChannel, +} from "@/common/config/schemas/appConfigOnDisk"; import type { z } from "zod"; import type { ProjectConfigSchema, @@ -138,6 +142,9 @@ export interface ProjectsConfig { */ runtimeEnablement?: Partial>; + /** Event sound settings keyed by sound event name. */ + eventSoundSettings?: EventSoundSettings; + /** Optional 1Password account name used for desktop SDK account selection. */ onePasswordAccountName?: string; } diff --git a/src/node/config.test.ts b/src/node/config.test.ts index 3a74224cd9..6f22c5b732 100644 --- a/src/node/config.test.ts +++ b/src/node/config.test.ts @@ -182,6 +182,65 @@ describe("Config", () => { }); }); + describe("event sound settings", () => { + it("loads eventSoundSettings and applies schema defaults", () => { + const configFile = path.join(tempDir, "config.json"); + fs.writeFileSync( + configFile, + JSON.stringify({ + projects: [], + eventSoundSettings: { + agent_review_ready: { + enabled: true, + filePath: "/tmp/review-ready.wav", + }, + future_event: { + filePath: "/tmp/future-event.wav", + }, + }, + }) + ); + + const loaded = config.loadConfigOrDefault(); + expect(loaded.eventSoundSettings).toEqual({ + agent_review_ready: { + enabled: true, + filePath: "/tmp/review-ready.wav", + }, + future_event: { + enabled: false, + filePath: "/tmp/future-event.wav", + }, + }); + }); + + it("round-trips eventSoundSettings through editConfig", async () => { + const eventSoundSettings = { + agent_review_ready: { + enabled: true, + filePath: "/tmp/review-ready.wav", + }, + future_event: { + enabled: false, + filePath: null, + }, + }; + + await config.editConfig((cfg) => { + cfg.eventSoundSettings = eventSoundSettings; + return cfg; + }); + + const reloaded = config.loadConfigOrDefault(); + expect(reloaded.eventSoundSettings).toEqual(eventSoundSettings); + + const raw = JSON.parse(fs.readFileSync(path.join(tempDir, "config.json"), "utf-8")) as { + eventSoundSettings?: unknown; + }; + expect(raw.eventSoundSettings).toEqual(eventSoundSettings); + }); + }); + describe("model preferences", () => { it("should preserve explicit gateway-scoped defaultModel and hiddenModels", async () => { await config.editConfig((cfg) => { diff --git a/src/node/config.ts b/src/node/config.ts index f2b4b6ebea..298b4d17a1 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -19,10 +19,11 @@ import type { FeatureFlagOverride, UpdateChannel, } from "@/common/types/project"; -import type { - AppConfigOnDisk, - BaseProviderConfig as ProviderConfig, - ProvidersConfig as CanonicalProvidersConfig, +import { + EventSoundSettingsSchema, + type AppConfigOnDisk, + type BaseProviderConfig as ProviderConfig, + type ProvidersConfig as CanonicalProvidersConfig, } from "@/common/config/schemas"; import { DEFAULT_TASK_SETTINGS, @@ -142,6 +143,11 @@ function areStringArraysEqual(a: string[], b: string[]): boolean { return true; } +function parseEventSoundSettings(value: unknown): AppConfigOnDisk["eventSoundSettings"] { + const result = EventSoundSettingsSchema.safeParse(value); + return result.success ? result.data : undefined; +} + function normalizeOptionalModelString(value: unknown): string | undefined { if (typeof value !== "string") { return undefined; @@ -584,6 +590,8 @@ export class Config { ? undefined : layoutPresetsRaw; + const eventSoundSettings = parseEventSoundSettings(parsed.eventSoundSettings); + return { projects: projectsMap, apiServerBindHost: parseOptionalNonEmptyString(parsed.apiServerBindHost), @@ -616,6 +624,7 @@ export class Config { updateChannel, defaultRuntime, runtimeEnablement, + eventSoundSettings, onePasswordAccountName: parseOptionalNonEmptyString(parsed.onePasswordAccountName), }; } @@ -790,6 +799,11 @@ export class Config { data.defaultRuntime = defaultRuntime; } + const eventSoundSettings = parseEventSoundSettings(config.eventSoundSettings); + if (eventSoundSettings !== undefined) { + data.eventSoundSettings = eventSoundSettings; + } + const onePasswordAccountName = parseOptionalNonEmptyString(config.onePasswordAccountName); if (onePasswordAccountName) { data.onePasswordAccountName = onePasswordAccountName; From 10fac1f4d464a4126fb5e916edd041abc38abf06 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 9 Mar 2026 12:46:47 +0000 Subject: [PATCH 02/23] =?UTF-8?q?=F0=9F=A4=96=20feat:=20add=20pickAudioFil?= =?UTF-8?q?e=20IPC=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a projects.pickAudioFile oRPC endpoint wired through ProjectService using the existing picker callback pattern. Desktop mode now registers an Electron open-file dialog filtered to audio extensions, while non-desktop mode safely returns null when no picker is registered. --- src/common/orpc/schemas/api.ts | 4 ++++ src/desktop/main.ts | 17 +++++++++++++++++ src/node/orpc/router.ts | 7 +++++++ src/node/services/projectService.ts | 10 ++++++++++ src/node/services/serviceContainer.ts | 4 ++++ 5 files changed, 42 insertions(+) diff --git a/src/common/orpc/schemas/api.ts b/src/common/orpc/schemas/api.ts index 467ec62ff2..0813f753c8 100644 --- a/src/common/orpc/schemas/api.ts +++ b/src/common/orpc/schemas/api.ts @@ -556,6 +556,10 @@ export const projects = { input: z.void(), output: z.string().nullable(), }, + pickAudioFile: { + input: z.object({}), + output: z.object({ filePath: z.string().nullable() }), + }, remove: { input: z.object({ projectPath: z.string(), force: z.boolean().nullish() }).passthrough(), output: ResultSchema(z.void(), ProjectRemoveErrorSchema), diff --git a/src/desktop/main.ts b/src/desktop/main.ts index 4b85644aca..070ff6772a 100644 --- a/src/desktop/main.ts +++ b/src/desktop/main.ts @@ -813,6 +813,23 @@ async function loadServices(): Promise { return res.canceled || res.filePaths.length === 0 ? null : res.filePaths[0]; }); + services.setProjectAudioFilePicker(async () => { + const win = BrowserWindow.getFocusedWindow(); + if (!win) return null; + + const res = await dialog.showOpenDialog(win, { + properties: ["openFile"], + filters: [ + { + name: "Audio Files", + extensions: ["mp3", "wav", "ogg", "m4a", "aac", "flac", "webm"], + }, + ], + }); + + return res.canceled || res.filePaths.length === 0 ? null : res.filePaths[0]; + }); + services.setTerminalWindowManager(terminalWindowManager); loadTokenizerModules().catch((error) => { diff --git a/src/node/orpc/router.ts b/src/node/orpc/router.ts index 2e006f1f54..a1d771dfe4 100644 --- a/src/node/orpc/router.ts +++ b/src/node/orpc/router.ts @@ -2382,6 +2382,13 @@ export const router = (authToken?: string) => { .handler(async ({ context }) => { return context.projectService.pickDirectory(); }), + pickAudioFile: t + .input(schemas.projects.pickAudioFile.input) + .output(schemas.projects.pickAudioFile.output) + .handler(async ({ context }) => { + const filePath = await context.projectService.pickAudioFile(); + return { filePath }; + }), getFileCompletions: t .input(schemas.projects.getFileCompletions.input) .output(schemas.projects.getFileCompletions.output) diff --git a/src/node/services/projectService.ts b/src/node/services/projectService.ts index ba7269df00..68b11bc4ed 100644 --- a/src/node/services/projectService.ts +++ b/src/node/services/projectService.ts @@ -319,6 +319,7 @@ interface FileCompletionsCacheEntry { export class ProjectService { private readonly fileCompletionsCache = new Map(); private directoryPicker?: () => Promise; + private audioFilePicker?: () => Promise; private readonly sshPromptService: SshPromptService | undefined; private workspaceService?: WorkspaceRemover; @@ -342,6 +343,15 @@ export class ProjectService { return this.directoryPicker(); } + setAudioFilePicker(picker: () => Promise) { + this.audioFilePicker = picker; + } + + async pickAudioFile(): Promise { + if (!this.audioFilePicker) return null; + return this.audioFilePicker(); + } + async create( projectPath: string ): Promise> { diff --git a/src/node/services/serviceContainer.ts b/src/node/services/serviceContainer.ts index 8beb426f4f..d8185cba5a 100644 --- a/src/node/services/serviceContainer.ts +++ b/src/node/services/serviceContainer.ts @@ -601,6 +601,10 @@ export class ServiceContainer { this.projectService.setDirectoryPicker(picker); } + setProjectAudioFilePicker(picker: () => Promise): void { + this.projectService.setAudioFilePicker(picker); + } + setTerminalWindowManager(manager: TerminalWindowManager): void { this.terminalService.setTerminalWindowManager(manager); } From e87830b84031641529df883a3ea74e718b751416 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 9 Mar 2026 15:40:09 +0000 Subject: [PATCH 03/23] feat: add sounds settings section --- .../Settings/Sections/SoundsSection.tsx | 212 ++++++++++++++++++ .../features/Settings/SettingsPage.tsx | 8 + src/common/orpc/schemas/api.ts | 10 + src/node/orpc/router.ts | 18 ++ 4 files changed, 248 insertions(+) create mode 100644 src/browser/features/Settings/Sections/SoundsSection.tsx diff --git a/src/browser/features/Settings/Sections/SoundsSection.tsx b/src/browser/features/Settings/Sections/SoundsSection.tsx new file mode 100644 index 0000000000..e006285d34 --- /dev/null +++ b/src/browser/features/Settings/Sections/SoundsSection.tsx @@ -0,0 +1,212 @@ +import { useEffect, useRef, useState } from "react"; + +import { Button } from "@/browser/components/Button/Button"; +import { Switch } from "@/browser/components/Switch/Switch"; +import { useAPI } from "@/browser/contexts/API"; +import { + EVENT_SOUND_KEYS, + EVENT_SOUND_LABELS, + type EventSoundKey, +} from "@/common/config/eventSoundTypes"; +import type { EventSoundConfig, EventSoundSettings } from "@/common/config/schemas/appConfigOnDisk"; + +// Browser mode doesn't expose the native file picker bridge from preload. +const isDesktopMode = typeof window !== "undefined" && Boolean(window.api); + +function getEventSoundConfig(settings: EventSoundSettings, key: EventSoundKey): EventSoundConfig { + const config = settings?.[key]; + return { + enabled: config?.enabled === true, + filePath: + typeof config?.filePath === "string" && config.filePath.length > 0 ? config.filePath : null, + }; +} + +function updateEventSoundConfig( + settings: EventSoundSettings, + key: EventSoundKey, + config: EventSoundConfig +): EventSoundSettings { + return { + ...(settings ?? {}), + [key]: config, + }; +} + +export function SoundsSection() { + const { api } = useAPI(); + const [eventSoundSettings, setEventSoundSettings] = useState(undefined); + + const loadNonceRef = useRef(0); + const saveChainRef = useRef>(Promise.resolve()); + const pendingSettingsRef = useRef<{ hasPending: boolean; settings: EventSoundSettings }>({ + hasPending: false, + settings: undefined, + }); + + useEffect(() => { + if (!api?.config?.getConfig) { + return; + } + + const loadNonce = ++loadNonceRef.current; + + void api.config + .getConfig() + .then((config) => { + if (loadNonce !== loadNonceRef.current) { + return; + } + + setEventSoundSettings(config.eventSoundSettings); + }) + .catch(() => { + // Best-effort only. + }); + }, [api]); + + const queueSettingsSave = (nextSettings: EventSoundSettings) => { + if (!api?.config?.updateEventSoundSettings) { + return; + } + + pendingSettingsRef.current.hasPending = true; + pendingSettingsRef.current.settings = nextSettings; + + // Serialize writes so rapid toggles/file selections cannot persist out-of-order settings. + saveChainRef.current = saveChainRef.current + .catch(() => { + // Best-effort only. + }) + .then(async () => { + for (;;) { + if (!pendingSettingsRef.current.hasPending) { + return; + } + + const pendingSettings = pendingSettingsRef.current.settings; + pendingSettingsRef.current.hasPending = false; + + try { + await api.config.updateEventSoundSettings({ eventSoundSettings: pendingSettings }); + } catch { + // Best-effort only. + } + } + }); + }; + + const applySettingsUpdate = (updater: (prev: EventSoundSettings) => EventSoundSettings) => { + // If the user changed a value while initial load is in flight, keep local edits authoritative. + loadNonceRef.current++; + + setEventSoundSettings((prev) => { + const next = updater(prev); + queueSettingsSave(next); + return next; + }); + }; + + const handleEnabledChange = (key: EventSoundKey, enabled: boolean) => { + applySettingsUpdate((prev) => { + const current = getEventSoundConfig(prev, key); + return updateEventSoundConfig(prev, key, { + ...current, + enabled, + }); + }); + }; + + const handleBrowse = async (key: EventSoundKey) => { + if (!api?.projects?.pickAudioFile) { + return; + } + + const result = await api.projects.pickAudioFile({}); + if (result.filePath == null) { + return; + } + + applySettingsUpdate((prev) => { + const current = getEventSoundConfig(prev, key); + return updateEventSoundConfig(prev, key, { + ...current, + filePath: result.filePath, + }); + }); + }; + + const handleClearFile = (key: EventSoundKey) => { + applySettingsUpdate((prev) => { + const current = getEventSoundConfig(prev, key); + return updateEventSoundConfig(prev, key, { + ...current, + filePath: null, + }); + }); + }; + + const canPersist = Boolean(api?.config?.updateEventSoundSettings); + + return ( +
+
+

Event sounds

+

+ Configure optional audio alerts for key events in Mux. +

+
+ +
+ {EVENT_SOUND_KEYS.map((key) => { + const soundConfig = getEventSoundConfig(eventSoundSettings, key); + const fileLabel = soundConfig.filePath ?? "No file selected"; + + return ( +
+
+
+
{EVENT_SOUND_LABELS[key]}
+
+ {fileLabel} +
+
+ handleEnabledChange(key, checked)} + disabled={!canPersist} + aria-label={`Toggle sound for ${EVENT_SOUND_LABELS[key]}`} + /> +
+ +
+ {isDesktopMode ? ( + + ) : null} + {soundConfig.filePath ? ( + + ) : null} +
+
+ ); + })} +
+
+ ); +} diff --git a/src/browser/features/Settings/SettingsPage.tsx b/src/browser/features/Settings/SettingsPage.tsx index 370f5fe18b..88baaed6e7 100644 --- a/src/browser/features/Settings/SettingsPage.tsx +++ b/src/browser/features/Settings/SettingsPage.tsx @@ -16,6 +16,7 @@ import { ShieldCheck, Server, Lock, + Volume2, } from "lucide-react"; import { useSettings } from "@/browser/contexts/SettingsContext"; import { useOnboardingPause } from "@/browser/features/SplashScreens/SplashScreenProvider"; @@ -37,6 +38,7 @@ import { ExperimentsSection } from "./Sections/ExperimentsSection"; import { ServerAccessSection } from "./Sections/ServerAccessSection"; import { KeybindsSection } from "./Sections/KeybindsSection"; import { SecuritySection } from "./Sections/SecuritySection"; +import { SoundsSection } from "./Sections/SoundsSection"; import type { SettingsSection } from "./types"; const BASE_SECTIONS: SettingsSection[] = [ @@ -46,6 +48,12 @@ const BASE_SECTIONS: SettingsSection[] = [ icon: , component: GeneralSection, }, + { + id: "sounds", + label: "Sounds", + icon: , + component: SoundsSection, + }, { id: "tasks", label: "Agents", diff --git a/src/common/orpc/schemas/api.ts b/src/common/orpc/schemas/api.ts index 0813f753c8..16591ef0ad 100644 --- a/src/common/orpc/schemas/api.ts +++ b/src/common/orpc/schemas/api.ts @@ -73,6 +73,7 @@ import { import { PolicyGetResponseSchema } from "./policy"; import { AgentAiDefaultsSchema, + EventSoundSettingsSchema, SubagentAiDefaultsSchema, UpdateChannelSchema, } from "../../config/schemas/appConfigOnDisk"; @@ -1701,6 +1702,7 @@ export const config = { muxGovernorUrl: z.string().nullable(), muxGovernorEnrolled: z.boolean(), llmDebugLogs: z.boolean(), + eventSoundSettings: EventSoundSettingsSchema, onePasswordAccountName: z.string().nullish(), }), }, @@ -1779,6 +1781,14 @@ export const config = { .strict(), output: z.void(), }, + updateEventSoundSettings: { + input: z + .object({ + eventSoundSettings: EventSoundSettingsSchema, + }) + .strict(), + output: z.void(), + }, unenrollMuxGovernor: { input: z.void(), output: z.void(), diff --git a/src/node/orpc/router.ts b/src/node/orpc/router.ts index a1d771dfe4..f762431f2c 100644 --- a/src/node/orpc/router.ts +++ b/src/node/orpc/router.ts @@ -596,6 +596,7 @@ export const router = (authToken?: string) => { muxGovernorUrl, muxGovernorEnrolled, llmDebugLogs: config.llmDebugLogs === true, + eventSoundSettings: config.eventSoundSettings, onePasswordAccountName: config.onePasswordAccountName ?? null, }; }), @@ -964,6 +965,23 @@ export const router = (authToken?: string) => { return config; }); }), + updateEventSoundSettings: t + .input(schemas.config.updateEventSoundSettings.input) + .output(schemas.config.updateEventSoundSettings.output) + .handler(async ({ context, input }) => { + await context.config.editConfig((config) => { + const nextSettings = input.eventSoundSettings; + if (!nextSettings || Object.keys(nextSettings).length === 0) { + const { eventSoundSettings: _eventSoundSettings, ...rest } = config; + return rest; + } + + return { + ...config, + eventSoundSettings: nextSettings, + }; + }); + }), unenrollMuxGovernor: t .input(schemas.config.unenrollMuxGovernor.input) .output(schemas.config.unenrollMuxGovernor.output) From 96e49913380ad73c2a8ffc166ae6c9e5a065d63b Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 9 Mar 2026 15:43:35 +0000 Subject: [PATCH 04/23] Add renderer event sound playback for response completion --- src/browser/App.tsx | 19 ++++++++++++++++- src/browser/utils/audio/eventSounds.ts | 28 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/browser/utils/audio/eventSounds.ts diff --git a/src/browser/App.tsx b/src/browser/App.tsx index 79d09597c9..221bd0e4e9 100644 --- a/src/browser/App.tsx +++ b/src/browser/App.tsx @@ -94,6 +94,8 @@ import { getErrorMessage } from "@/common/utils/errors"; import assert from "@/common/utils/assert"; import { createProjectRefs } from "@/common/utils/multiProject"; import { MULTI_PROJECT_SIDEBAR_SECTION_ID } from "@/common/constants/multiProject"; +import type { EventSoundSettings } from "@/common/config/schemas/appConfigOnDisk"; +import { playEventSound } from "@/browser/utils/audio/eventSounds"; import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults"; import { LandingPage } from "@/browser/features/LandingPage/LandingPage"; import { LoadingScreen } from "@/browser/components/LoadingScreen/LoadingScreen"; @@ -1002,6 +1004,21 @@ function AppInner() { return; } + // Play event sound (independent of notification settings). + if (api) { + void api.config + .getConfig() + .then((config) => { + playEventSound( + (config as { eventSoundSettings?: EventSoundSettings }).eventSoundSettings, + "agent_review_ready" + ); + }) + .catch((error) => { + console.debug("Failed to load event sound settings", { error: String(error) }); + }); + } + // Skip notification if the selected workspace is focused (Slack-like behavior). // Notification suppression intentionally follows selection state, not chat-route visibility. const isWorkspaceFocused = @@ -1038,7 +1055,7 @@ function AppInner() { return () => { unsubscribe?.(); }; - }, [setSelectedWorkspace, workspaceStore]); + }, [api, setSelectedWorkspace, workspaceStore]); // Show auth modal if authentication is required if (status === "auth_required") { diff --git a/src/browser/utils/audio/eventSounds.ts b/src/browser/utils/audio/eventSounds.ts new file mode 100644 index 0000000000..2312ba0a14 --- /dev/null +++ b/src/browser/utils/audio/eventSounds.ts @@ -0,0 +1,28 @@ +import type { EventSoundSettings } from "@/common/config/schemas/appConfigOnDisk"; +import type { EventSoundKey } from "@/common/config/eventSoundTypes"; + +/** + * Attempt to play the configured sound for the given event key. + * Fails silently with debug logging if no sound is configured or playback fails. + */ +export function playEventSound( + eventSoundSettings: EventSoundSettings | undefined, + eventKey: EventSoundKey +): void { + if (!eventSoundSettings) { + return; + } + + const config = eventSoundSettings[eventKey]; + if (!config?.enabled || !config.filePath) { + return; + } + + const audio = new Audio(config.filePath); + void audio.play().catch((error) => { + console.debug("Event sound playback failed", { + eventKey, + error: String(error), + }); + }); +} From 19e0ee3cb539be104ff4e6c64f0c97a49df1611a Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 9 Mar 2026 15:48:48 +0000 Subject: [PATCH 05/23] Expose event sound settings to renderer config API --- src/browser/App.tsx | 5 +---- src/common/orpc/schemas/api.ts | 1 + src/node/orpc/router.ts | 1 + 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/browser/App.tsx b/src/browser/App.tsx index 221bd0e4e9..cc99d1957a 100644 --- a/src/browser/App.tsx +++ b/src/browser/App.tsx @@ -1009,10 +1009,7 @@ function AppInner() { void api.config .getConfig() .then((config) => { - playEventSound( - (config as { eventSoundSettings?: EventSoundSettings }).eventSoundSettings, - "agent_review_ready" - ); + playEventSound(config.eventSoundSettings, "agent_review_ready"); }) .catch((error) => { console.debug("Failed to load event sound settings", { error: String(error) }); diff --git a/src/common/orpc/schemas/api.ts b/src/common/orpc/schemas/api.ts index 16591ef0ad..86c087d634 100644 --- a/src/common/orpc/schemas/api.ts +++ b/src/common/orpc/schemas/api.ts @@ -1704,6 +1704,7 @@ export const config = { llmDebugLogs: z.boolean(), eventSoundSettings: EventSoundSettingsSchema, onePasswordAccountName: z.string().nullish(), + eventSoundSettings: EventSoundSettingsSchema, }), }, saveConfig: { diff --git a/src/node/orpc/router.ts b/src/node/orpc/router.ts index f762431f2c..faec66d307 100644 --- a/src/node/orpc/router.ts +++ b/src/node/orpc/router.ts @@ -598,6 +598,7 @@ export const router = (authToken?: string) => { llmDebugLogs: config.llmDebugLogs === true, eventSoundSettings: config.eventSoundSettings, onePasswordAccountName: config.onePasswordAccountName ?? null, + eventSoundSettings: config.eventSoundSettings, }; }), onConfigChanged: t From 96a07b79459b63d6cbb2a74bf5da74ae2dc37435 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 9 Mar 2026 15:53:30 +0000 Subject: [PATCH 06/23] fix: remove duplicate eventSoundSettings properties --- src/common/orpc/schemas/api.ts | 1 - src/node/orpc/router.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/common/orpc/schemas/api.ts b/src/common/orpc/schemas/api.ts index 86c087d634..16591ef0ad 100644 --- a/src/common/orpc/schemas/api.ts +++ b/src/common/orpc/schemas/api.ts @@ -1704,7 +1704,6 @@ export const config = { llmDebugLogs: z.boolean(), eventSoundSettings: EventSoundSettingsSchema, onePasswordAccountName: z.string().nullish(), - eventSoundSettings: EventSoundSettingsSchema, }), }, saveConfig: { diff --git a/src/node/orpc/router.ts b/src/node/orpc/router.ts index faec66d307..f762431f2c 100644 --- a/src/node/orpc/router.ts +++ b/src/node/orpc/router.ts @@ -598,7 +598,6 @@ export const router = (authToken?: string) => { llmDebugLogs: config.llmDebugLogs === true, eventSoundSettings: config.eventSoundSettings, onePasswordAccountName: config.onePasswordAccountName ?? null, - eventSoundSettings: config.eventSoundSettings, }; }), onConfigChanged: t From 0ed7c6ff892f2e6f9fe4107003977f606496e80d Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 9 Mar 2026 17:02:59 +0000 Subject: [PATCH 07/23] fix: address codex event sound review feedback --- src/browser/App.tsx | 37 +++++++++++++++++++------- src/browser/utils/audio/eventSounds.ts | 27 ++++++++++++++++++- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/browser/App.tsx b/src/browser/App.tsx index cc99d1957a..e7e17fb8d5 100644 --- a/src/browser/App.tsx +++ b/src/browser/App.tsx @@ -240,6 +240,32 @@ function AppInner() { workspaceMetadataRef.current = workspaceMetadata; }, [workspaceMetadata]); + const eventSoundSettingsRef = useRef(undefined); + useEffect(() => { + if (!api) { + eventSoundSettingsRef.current = undefined; + return; + } + + let isCancelled = false; + void api.config + .getConfig() + .then((config) => { + if (!isCancelled) { + eventSoundSettingsRef.current = config.eventSoundSettings; + } + }) + .catch((error) => { + if (!isCancelled) { + console.debug("Failed to load event sound settings", { error: String(error) }); + } + }); + + return () => { + isCancelled = true; + }; + }, [api]); + const handleOpenMuxChat = useCallback(() => { // User requested an F1 shortcut to jump straight into Chat with Mux. const metadata = workspaceMetadataRef.current.get(MUX_HELP_CHAT_WORKSPACE_ID); @@ -1005,16 +1031,7 @@ function AppInner() { } // Play event sound (independent of notification settings). - if (api) { - void api.config - .getConfig() - .then((config) => { - playEventSound(config.eventSoundSettings, "agent_review_ready"); - }) - .catch((error) => { - console.debug("Failed to load event sound settings", { error: String(error) }); - }); - } + playEventSound(eventSoundSettingsRef.current, "agent_review_ready"); // Skip notification if the selected workspace is focused (Slack-like behavior). // Notification suppression intentionally follows selection state, not chat-route visibility. diff --git a/src/browser/utils/audio/eventSounds.ts b/src/browser/utils/audio/eventSounds.ts index 2312ba0a14..29b77f5ecf 100644 --- a/src/browser/utils/audio/eventSounds.ts +++ b/src/browser/utils/audio/eventSounds.ts @@ -1,6 +1,31 @@ import type { EventSoundSettings } from "@/common/config/schemas/appConfigOnDisk"; import type { EventSoundKey } from "@/common/config/eventSoundTypes"; +const URI_SCHEME_RE = /^[a-zA-Z][a-zA-Z\d+.-]*:/; +const WINDOWS_DRIVE_PATH_RE = /^[a-zA-Z]:\//; + +function toAudioSource(filePath: string): string { + if (URI_SCHEME_RE.test(filePath)) { + return filePath; + } + + if (filePath.startsWith("\\\\")) { + const normalizedUncPath = filePath.replace(/\\/g, "/"); + return `file:${encodeURI(normalizedUncPath)}`; + } + + const normalizedPath = filePath.replace(/\\/g, "/"); + if (WINDOWS_DRIVE_PATH_RE.test(normalizedPath)) { + return `file:///${encodeURI(normalizedPath)}`; + } + + if (normalizedPath.startsWith("/")) { + return `file://${encodeURI(normalizedPath)}`; + } + + return normalizedPath; +} + /** * Attempt to play the configured sound for the given event key. * Fails silently with debug logging if no sound is configured or playback fails. @@ -18,7 +43,7 @@ export function playEventSound( return; } - const audio = new Audio(config.filePath); + const audio = new Audio(toAudioSource(config.filePath)); void audio.play().catch((error) => { console.debug("Event sound playback failed", { eventKey, From 481ef89bf9b4c73505ef9e1d47eb3330cbc244e0 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 9 Mar 2026 17:12:17 +0000 Subject: [PATCH 08/23] fix: sync event sound settings ref after settings edits --- src/browser/App.tsx | 24 +++++++++++++++++-- .../Settings/Sections/SoundsSection.tsx | 6 +++++ src/common/constants/events.ts | 9 +++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/browser/App.tsx b/src/browser/App.tsx index e7e17fb8d5..e33a011707 100644 --- a/src/browser/App.tsx +++ b/src/browser/App.tsx @@ -39,7 +39,7 @@ import { CommandPalette } from "./components/CommandPalette/CommandPalette"; import { buildCoreSources, type BuildSourcesParams } from "./utils/commands/sources"; import { THINKING_LEVELS, type ThinkingLevel } from "@/common/types/thinking"; -import { CUSTOM_EVENTS } from "@/common/constants/events"; +import { CUSTOM_EVENTS, type CustomEventType } from "@/common/constants/events"; import { isWorkspaceForkSwitchEvent } from "./utils/workspaceEvents"; import { getAgentIdKey, @@ -242,9 +242,25 @@ function AppInner() { const eventSoundSettingsRef = useRef(undefined); useEffect(() => { + const handleEventSoundSettingsChanged = ( + event: CustomEventType + ) => { + eventSoundSettingsRef.current = event.detail.eventSoundSettings; + }; + + window.addEventListener( + CUSTOM_EVENTS.EVENT_SOUND_SETTINGS_CHANGED, + handleEventSoundSettingsChanged as EventListener + ); + if (!api) { eventSoundSettingsRef.current = undefined; - return; + return () => { + window.removeEventListener( + CUSTOM_EVENTS.EVENT_SOUND_SETTINGS_CHANGED, + handleEventSoundSettingsChanged as EventListener + ); + }; } let isCancelled = false; @@ -263,6 +279,10 @@ function AppInner() { return () => { isCancelled = true; + window.removeEventListener( + CUSTOM_EVENTS.EVENT_SOUND_SETTINGS_CHANGED, + handleEventSoundSettingsChanged as EventListener + ); }; }, [api]); diff --git a/src/browser/features/Settings/Sections/SoundsSection.tsx b/src/browser/features/Settings/Sections/SoundsSection.tsx index e006285d34..74f19acf08 100644 --- a/src/browser/features/Settings/Sections/SoundsSection.tsx +++ b/src/browser/features/Settings/Sections/SoundsSection.tsx @@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react"; import { Button } from "@/browser/components/Button/Button"; import { Switch } from "@/browser/components/Switch/Switch"; import { useAPI } from "@/browser/contexts/API"; +import { CUSTOM_EVENTS, createCustomEvent } from "@/common/constants/events"; import { EVENT_SOUND_KEYS, EVENT_SOUND_LABELS, @@ -103,6 +104,11 @@ export function SoundsSection() { setEventSoundSettings((prev) => { const next = updater(prev); queueSettingsSave(next); + window.dispatchEvent( + createCustomEvent(CUSTOM_EVENTS.EVENT_SOUND_SETTINGS_CHANGED, { + eventSoundSettings: next, + }) + ); return next; }); }; diff --git a/src/common/constants/events.ts b/src/common/constants/events.ts index b3a45790cb..ec6f4ae6c9 100644 --- a/src/common/constants/events.ts +++ b/src/common/constants/events.ts @@ -7,6 +7,7 @@ import type { ThinkingLevel } from "@/common/types/thinking"; import type { ReviewNoteDataForDisplay } from "@/common/types/message"; +import type { EventSoundSettings } from "@/common/config/schemas/appConfigOnDisk"; import type { FilePart } from "@/common/orpc/schemas"; export const CUSTOM_EVENTS = { @@ -116,6 +117,11 @@ export const CUSTOM_EVENTS = { * Detail: { enabled: boolean } */ LLM_DEBUG_LOGS_CHANGED: "mux:llmDebugLogsChanged", + /** + * Event emitted when event sound settings are edited in Settings. + * Detail: { eventSoundSettings: EventSoundSettings } + */ + EVENT_SOUND_SETTINGS_CHANGED: "mux:eventSoundSettingsChanged", } as const; /** @@ -175,6 +181,9 @@ export interface CustomEventPayloads { [CUSTOM_EVENTS.LLM_DEBUG_LOGS_CHANGED]: { enabled: boolean; }; + [CUSTOM_EVENTS.EVENT_SOUND_SETTINGS_CHANGED]: { + eventSoundSettings: EventSoundSettings; + }; } /** From c493fed7b4cc0a5ddd5eea45f2e1985efe916276 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 9 Mar 2026 17:19:45 +0000 Subject: [PATCH 09/23] fix: handle Windows drive paths in event sound URLs --- src/browser/utils/audio/eventSounds.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/browser/utils/audio/eventSounds.ts b/src/browser/utils/audio/eventSounds.ts index 29b77f5ecf..b6ea8c1975 100644 --- a/src/browser/utils/audio/eventSounds.ts +++ b/src/browser/utils/audio/eventSounds.ts @@ -5,10 +5,6 @@ const URI_SCHEME_RE = /^[a-zA-Z][a-zA-Z\d+.-]*:/; const WINDOWS_DRIVE_PATH_RE = /^[a-zA-Z]:\//; function toAudioSource(filePath: string): string { - if (URI_SCHEME_RE.test(filePath)) { - return filePath; - } - if (filePath.startsWith("\\\\")) { const normalizedUncPath = filePath.replace(/\\/g, "/"); return `file:${encodeURI(normalizedUncPath)}`; @@ -19,6 +15,10 @@ function toAudioSource(filePath: string): string { return `file:///${encodeURI(normalizedPath)}`; } + if (URI_SCHEME_RE.test(filePath)) { + return filePath; + } + if (normalizedPath.startsWith("/")) { return `file://${encodeURI(normalizedPath)}`; } From df36e71046a54c7c589d3845ec48cedd898879e7 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 9 Mar 2026 17:29:41 +0000 Subject: [PATCH 10/23] fix: guard event sound config load against stale overwrite --- src/browser/App.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/browser/App.tsx b/src/browser/App.tsx index e33a011707..d173997f2e 100644 --- a/src/browser/App.tsx +++ b/src/browser/App.tsx @@ -241,10 +241,12 @@ function AppInner() { }, [workspaceMetadata]); const eventSoundSettingsRef = useRef(undefined); + const eventSoundSettingsLoadVersionRef = useRef(0); useEffect(() => { const handleEventSoundSettingsChanged = ( event: CustomEventType ) => { + eventSoundSettingsLoadVersionRef.current += 1; eventSoundSettingsRef.current = event.detail.eventSoundSettings; }; @@ -264,10 +266,11 @@ function AppInner() { } let isCancelled = false; + const loadVersion = eventSoundSettingsLoadVersionRef.current; void api.config .getConfig() .then((config) => { - if (!isCancelled) { + if (!isCancelled && loadVersion === eventSoundSettingsLoadVersionRef.current) { eventSoundSettingsRef.current = config.eventSoundSettings; } }) From 623e38c6b8bbc7d5173be25b7952eb46d9ce17c4 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 9 Mar 2026 17:38:19 +0000 Subject: [PATCH 11/23] fix: encode special characters in file audio URLs --- src/browser/utils/audio/eventSounds.ts | 27 +++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/browser/utils/audio/eventSounds.ts b/src/browser/utils/audio/eventSounds.ts index b6ea8c1975..7e7c148efe 100644 --- a/src/browser/utils/audio/eventSounds.ts +++ b/src/browser/utils/audio/eventSounds.ts @@ -4,15 +4,32 @@ import type { EventSoundKey } from "@/common/config/eventSoundTypes"; const URI_SCHEME_RE = /^[a-zA-Z][a-zA-Z\d+.-]*:/; const WINDOWS_DRIVE_PATH_RE = /^[a-zA-Z]:\//; +function toFileUrlFromAbsolutePath(normalizedPath: string): string { + const fileUrl = new URL("file:///"); + fileUrl.pathname = normalizedPath.startsWith("/") ? normalizedPath : `/${normalizedPath}`; + return fileUrl.toString(); +} + +function toFileUrlFromUncPath(filePath: string): string { + const normalizedUncPath = filePath.replace(/\\/g, "/").replace(/^\/\/+/, ""); + const [host, ...pathParts] = normalizedUncPath.split("/"); + if (!host) { + return toFileUrlFromAbsolutePath(`/${normalizedUncPath}`); + } + + const fileUrl = new URL(`file://${host}`); + fileUrl.pathname = `/${pathParts.join("/")}`; + return fileUrl.toString(); +} + function toAudioSource(filePath: string): string { - if (filePath.startsWith("\\\\")) { - const normalizedUncPath = filePath.replace(/\\/g, "/"); - return `file:${encodeURI(normalizedUncPath)}`; + if (filePath.startsWith("\\\\") || filePath.startsWith("//")) { + return toFileUrlFromUncPath(filePath); } const normalizedPath = filePath.replace(/\\/g, "/"); if (WINDOWS_DRIVE_PATH_RE.test(normalizedPath)) { - return `file:///${encodeURI(normalizedPath)}`; + return toFileUrlFromAbsolutePath(normalizedPath); } if (URI_SCHEME_RE.test(filePath)) { @@ -20,7 +37,7 @@ function toAudioSource(filePath: string): string { } if (normalizedPath.startsWith("/")) { - return `file://${encodeURI(normalizedPath)}`; + return toFileUrlFromAbsolutePath(normalizedPath); } return normalizedPath; From 2506310697dc2e30bc768f05e51fac1002b872d0 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 10 Mar 2026 10:51:59 +0000 Subject: [PATCH 12/23] feat: add managed event sound asset storage --- .../Settings/Sections/SoundsSection.tsx | 24 +- src/browser/utils/audio/eventSounds.ts | 46 +- src/common/config/eventSoundTypes.ts | 27 ++ .../config/schemas/appConfigOnDisk.test.ts | 39 +- src/common/config/schemas/appConfigOnDisk.ts | 18 +- src/common/orpc/schemas.ts | 1 + src/common/orpc/schemas/api.ts | 41 ++ src/node/config.test.ts | 30 +- src/node/orpc/context.ts | 2 + src/node/orpc/router.ts | 26 ++ src/node/orpc/server.test.ts | 53 +++ src/node/orpc/server.ts | 23 + .../services/eventSoundAssetService.test.ts | 180 ++++++++ src/node/services/eventSoundAssetService.ts | 408 ++++++++++++++++++ src/node/services/serviceContainer.ts | 5 + 15 files changed, 859 insertions(+), 64 deletions(-) create mode 100644 src/node/services/eventSoundAssetService.test.ts create mode 100644 src/node/services/eventSoundAssetService.ts diff --git a/src/browser/features/Settings/Sections/SoundsSection.tsx b/src/browser/features/Settings/Sections/SoundsSection.tsx index 74f19acf08..e7c1a67f05 100644 --- a/src/browser/features/Settings/Sections/SoundsSection.tsx +++ b/src/browser/features/Settings/Sections/SoundsSection.tsx @@ -18,8 +18,7 @@ function getEventSoundConfig(settings: EventSoundSettings, key: EventSoundKey): const config = settings?.[key]; return { enabled: config?.enabled === true, - filePath: - typeof config?.filePath === "string" && config.filePath.length > 0 ? config.filePath : null, + source: config?.source ?? null, }; } @@ -124,7 +123,7 @@ export function SoundsSection() { }; const handleBrowse = async (key: EventSoundKey) => { - if (!api?.projects?.pickAudioFile) { + if (!api?.projects?.pickAudioFile || !api?.eventSounds?.importFromLocalPath) { return; } @@ -133,11 +132,16 @@ export function SoundsSection() { return; } + const importedAsset = await api.eventSounds.importFromLocalPath({ localPath: result.filePath }); + applySettingsUpdate((prev) => { const current = getEventSoundConfig(prev, key); return updateEventSoundConfig(prev, key, { ...current, - filePath: result.filePath, + source: { + kind: "managed", + assetId: importedAsset.assetId, + }, }); }); }; @@ -147,7 +151,7 @@ export function SoundsSection() { const current = getEventSoundConfig(prev, key); return updateEventSoundConfig(prev, key, { ...current, - filePath: null, + source: null, }); }); }; @@ -166,7 +170,7 @@ export function SoundsSection() {
{EVENT_SOUND_KEYS.map((key) => { const soundConfig = getEventSoundConfig(eventSoundSettings, key); - const fileLabel = soundConfig.filePath ?? "No file selected"; + const fileLabel = soundConfig.source?.assetId ?? "No sound selected"; return (
@@ -193,12 +197,16 @@ export function SoundsSection() { onClick={() => { void handleBrowse(key); }} - disabled={!canPersist || !api?.projects?.pickAudioFile} + disabled={ + !canPersist || + !api?.projects?.pickAudioFile || + !api?.eventSounds?.importFromLocalPath + } > Browse ) : null} - {soundConfig.filePath ? ( + {soundConfig.source ? (
+ { + const key = uploadTargetKeyRef.current; + const file = event.target.files?.[0]; + event.target.value = ""; + + if (!key || !file) { + return; + } + + void handleUpload(key, file); + }} + /> +
{EVENT_SOUND_KEYS.map((key) => { const soundConfig = getEventSoundConfig(eventSoundSettings, key); - const fileLabel = soundConfig.source?.assetId ?? "No sound selected"; + const fileLabel = + soundConfig.source?.label ?? soundConfig.source?.assetId ?? "No sound selected"; return (
@@ -205,7 +298,16 @@ export function SoundsSection() { > Browse - ) : null} + ) : ( + + )} {soundConfig.source ? ( From a48d5583e57da5617ab28385fcef98230ccf0702 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 10 Mar 2026 14:34:10 +0000 Subject: [PATCH 15/23] =?UTF-8?q?=F0=9F=A4=96=20fix:=20make=20managed=20ev?= =?UTF-8?q?ent=20sounds=20playable=20in=20browser=20auth=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `4.59`_ --- src/browser/utils/audio/eventSounds.ts | 24 +++++++++++++++++++++++- src/node/orpc/server.test.ts | 14 +++++++++++--- src/node/orpc/server.ts | 8 ++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/browser/utils/audio/eventSounds.ts b/src/browser/utils/audio/eventSounds.ts index 3bc2257b6b..aa03785508 100644 --- a/src/browser/utils/audio/eventSounds.ts +++ b/src/browser/utils/audio/eventSounds.ts @@ -1,8 +1,30 @@ +import { getStoredAuthToken } from "@/browser/components/AuthTokenModal/AuthTokenModal"; +import { getBrowserBackendBaseUrl } from "@/browser/utils/backendBaseUrl"; import type { EventSoundSettings } from "@/common/config/schemas/appConfigOnDisk"; import type { EventSoundKey } from "@/common/config/eventSoundTypes"; +function getServerAuthToken(): string | null { + const urlToken = new URLSearchParams(window.location.search).get("token")?.trim(); + return urlToken?.length ? urlToken : getStoredAuthToken(); +} + function toManagedPlaybackPath(assetId: string): string { - return `/assets/event-sounds/${encodeURIComponent(assetId)}`; + // Browser mode can run behind a path-based app proxy (for example Coder), + // so resolve against the backend base URL instead of assuming "/". + const backendBaseUrl = getBrowserBackendBaseUrl(); + const playbackUrl = new URL( + `/assets/event-sounds/${encodeURIComponent(assetId)}`, + `${backendBaseUrl}/` + ); + + //