Skip to content

Commit 53643f2

Browse files
committed
fix: handle desktop event-sound playback URL and browse import errors
1 parent 35f9e16 commit 53643f2

File tree

4 files changed

+122
-32
lines changed

4 files changed

+122
-32
lines changed

src/browser/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1042,7 +1042,7 @@ function AppInner() {
10421042
if (compaction?.hasContinueMessage) return;
10431043

10441044
// Play event sound (independent of notification settings).
1045-
playEventSound(eventSoundSettingsRef.current, "agent_review_ready");
1045+
playEventSound(eventSoundSettingsRef.current, "agent_review_ready", api);
10461046

10471047
// Skip notification if the selected workspace is focused (Slack-like behavior).
10481048
// Notification suppression intentionally follows selection state, not chat-route visibility.

src/browser/features/Settings/Sections/SoundsSection.tsx

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -160,24 +160,30 @@ export function SoundsSection() {
160160
return;
161161
}
162162

163-
const result = await api.projects.pickAudioFile({});
164-
if (result.filePath == null) {
165-
return;
166-
}
163+
try {
164+
const result = await api.projects.pickAudioFile({});
165+
if (result.filePath == null) {
166+
return;
167+
}
167168

168-
const importedAsset = await api.eventSounds.importFromLocalPath({ localPath: result.filePath });
169+
const importedAsset = await api.eventSounds.importFromLocalPath({
170+
localPath: result.filePath,
171+
});
169172

170-
applySettingsUpdate((prev) => {
171-
const current = getEventSoundConfig(prev, key);
172-
return updateEventSoundConfig(prev, key, {
173-
...current,
174-
source: {
175-
kind: "managed",
176-
assetId: importedAsset.assetId,
177-
label: importedAsset.originalName,
178-
},
173+
applySettingsUpdate((prev) => {
174+
const current = getEventSoundConfig(prev, key);
175+
return updateEventSoundConfig(prev, key, {
176+
...current,
177+
source: {
178+
kind: "managed",
179+
assetId: importedAsset.assetId,
180+
label: importedAsset.originalName,
181+
},
182+
});
179183
});
180-
});
184+
} catch {
185+
// Best-effort only.
186+
}
181187
};
182188

183189
const handleUpload = async (key: EventSoundKey, file: File) => {

src/browser/utils/audio/eventSounds.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ describe("eventSounds", () => {
3131
originalLocalStorage = globalThis.localStorage;
3232

3333
globalThis.window = new GlobalWindow() as unknown as Window & typeof globalThis;
34+
Object.defineProperty(globalThis.window, "api", {
35+
value: undefined,
36+
writable: true,
37+
configurable: true,
38+
});
3439
globalThis.document = globalThis.window.document;
3540
globalThis.Audio = MockAudio as unknown as typeof Audio;
3641
globalThis.localStorage = globalThis.window.localStorage;
@@ -85,4 +90,47 @@ describe("eventSounds", () => {
8590
"https://coder.example.com/@u/ws/apps/mux/assets/event-sounds/22222222-2222-2222-2222-222222222222.wav?token=stored-token"
8691
);
8792
});
93+
94+
test("uses API server status in desktop mode to build playback URL", async () => {
95+
window.location.href = "file:///app/index.html";
96+
Object.defineProperty(window, "api", {
97+
value: {},
98+
writable: true,
99+
configurable: true,
100+
});
101+
102+
const settings: EventSoundSettings = {
103+
agent_review_ready: {
104+
enabled: true,
105+
source: {
106+
kind: "managed",
107+
assetId: "33333333-3333-3333-3333-333333333333.wav",
108+
},
109+
},
110+
};
111+
112+
playEventSound(settings, "agent_review_ready", {
113+
server: {
114+
getApiServerStatus: () =>
115+
Promise.resolve({
116+
running: true,
117+
baseUrl: "http://127.0.0.1:55525",
118+
bindHost: "127.0.0.1",
119+
port: 55525,
120+
networkBaseUrls: [],
121+
token: "desktop-token",
122+
configuredBindHost: null,
123+
configuredPort: null,
124+
configuredServeWebUi: false,
125+
}),
126+
},
127+
} as unknown as Parameters<typeof playEventSound>[2]);
128+
129+
await Promise.resolve();
130+
await Promise.resolve();
131+
132+
expect(lastAudioSource).toBe(
133+
"http://127.0.0.1:55525/assets/event-sounds/33333333-3333-3333-3333-333333333333.wav?token=desktop-token"
134+
);
135+
});
88136
});
Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { APIClient } from "@/browser/contexts/API";
12
import { getStoredAuthToken } from "@/browser/components/AuthTokenModal/AuthTokenModal";
23
import { getBrowserBackendBaseUrl } from "@/browser/utils/backendBaseUrl";
34
import type { EventSoundSettings } from "@/common/config/schemas/appConfigOnDisk";
@@ -8,32 +9,62 @@ function getServerAuthToken(): string | null {
89
return urlToken?.length ? urlToken : getStoredAuthToken();
910
}
1011

11-
function toManagedPlaybackPath(assetId: string): string {
12-
// Browser mode can run behind a path-based app proxy (for example Coder),
13-
// so resolve against the backend base URL instead of assuming "/".
14-
const backendBaseUrl = getBrowserBackendBaseUrl();
15-
const playbackUrl = new URL(
16-
`assets/event-sounds/${encodeURIComponent(assetId)}`,
17-
`${backendBaseUrl}/`
18-
);
12+
function toManagedPlaybackPath(baseUrl: string, assetId: string, authToken: string | null): string {
13+
const playbackUrl = new URL(`assets/event-sounds/${encodeURIComponent(assetId)}`, `${baseUrl}/`);
1914

2015
// <audio> cannot attach Authorization headers, so pass the server token as
2116
// a query param when token-auth is in use.
22-
const authToken = getServerAuthToken();
2317
if (authToken) {
2418
playbackUrl.searchParams.set("token", authToken);
2519
}
2620

2721
return playbackUrl.toString();
2822
}
2923

24+
function toBrowserManagedPlaybackPath(assetId: string): string {
25+
// Browser mode can run behind a path-based app proxy (for example Coder),
26+
// so resolve against the backend base URL instead of assuming "/".
27+
return toManagedPlaybackPath(getBrowserBackendBaseUrl(), assetId, getServerAuthToken());
28+
}
29+
30+
async function toDesktopManagedPlaybackPath(
31+
assetId: string,
32+
apiClient: APIClient | null | undefined
33+
): Promise<string | null> {
34+
if (!apiClient?.server?.getApiServerStatus) {
35+
return null;
36+
}
37+
38+
try {
39+
const apiServerStatus = await apiClient.server.getApiServerStatus();
40+
if (!apiServerStatus.running || !apiServerStatus.baseUrl) {
41+
return null;
42+
}
43+
44+
return toManagedPlaybackPath(apiServerStatus.baseUrl, assetId, apiServerStatus.token);
45+
} catch {
46+
return null;
47+
}
48+
}
49+
50+
function playAudioFromPath(path: string, eventKey: EventSoundKey): void {
51+
const audio = new Audio(path);
52+
void audio.play().catch((error) => {
53+
console.debug("Event sound playback failed", {
54+
eventKey,
55+
error: String(error),
56+
});
57+
});
58+
}
59+
3060
/**
3161
* Attempt to play the configured sound for the given event key.
3262
* Fails silently with debug logging if no sound is configured or playback fails.
3363
*/
3464
export function playEventSound(
3565
eventSoundSettings: EventSoundSettings | undefined,
36-
eventKey: EventSoundKey
66+
eventKey: EventSoundKey,
67+
apiClient?: APIClient | null
3768
): void {
3869
if (!eventSoundSettings) {
3970
return;
@@ -44,11 +75,16 @@ export function playEventSound(
4475
return;
4576
}
4677

47-
const audio = new Audio(toManagedPlaybackPath(config.source.assetId));
48-
void audio.play().catch((error) => {
49-
console.debug("Event sound playback failed", {
50-
eventKey,
51-
error: String(error),
52-
});
78+
if (!window.api) {
79+
playAudioFromPath(toBrowserManagedPlaybackPath(config.source.assetId), eventKey);
80+
return;
81+
}
82+
83+
void toDesktopManagedPlaybackPath(config.source.assetId, apiClient).then((playbackPath) => {
84+
if (!playbackPath) {
85+
return;
86+
}
87+
88+
playAudioFromPath(playbackPath, eventKey);
5389
});
5490
}

0 commit comments

Comments
 (0)