mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): add Kyleosophy alternative completion chimes (#23891)
- Add "Enable Kyleosophy" toggle to Settings > Behavior
- When enabled, replaces standard completion chime with random Kyle
sound clips
- Ships 8 alternative `.mp3` files as static assets (~82KB total)
- localStorage preference (`agents.kyleosophy`), defaults to off
- Pauses orphaned Audio elements on sound URL change to prevent overlap
<details><summary>Review findings addressed</summary>
- **P2** Stale JSDoc on `playChimeAudio` — updated to reflect
parameterized behavior
- **P3** Overlapping audio on rapid completions — added
`chimeAudio?.pause()` before replacement
- **P3** Test ordering dependency — pinned `Math.random` for
determinism, documented cache behavior
- **Nit** Setter naming — `setLocalKyleosophy` → `setKylesophyLocal`
- Toggle moved to bottom of Behavior page per product request
- Description changed to "IYKYK" per product request
</details>
> 🤖 Written by a Coder Agent. Reviewed by a human.
This commit is contained in:
@@ -13,6 +13,7 @@ import { DurationField } from "./components/DurationField/DurationField";
|
||||
import { SectionHeader } from "./components/SectionHeader";
|
||||
import { TextPreviewDialog } from "./components/TextPreviewDialog";
|
||||
import { UserCompactionThresholdSettings } from "./components/UserCompactionThresholdSettings";
|
||||
import { getKylesophyEnabled, setKylesophyEnabled } from "./utils/chime";
|
||||
|
||||
const textareaMaxHeight = 240;
|
||||
const textareaBaseClassName =
|
||||
@@ -124,6 +125,7 @@ export const AgentSettingsBehaviorPageView: FC<
|
||||
const [isUserPromptOverflowing, setIsUserPromptOverflowing] = useState(false);
|
||||
const [isSystemPromptOverflowing, setIsSystemPromptOverflowing] =
|
||||
useState(false);
|
||||
const [kylesophyEnabled, setKylesophyLocal] = useState(getKylesophyEnabled);
|
||||
|
||||
// ── Derived state ──
|
||||
const hasLoadedSystemPrompt = systemPromptData !== undefined;
|
||||
@@ -510,6 +512,26 @@ export const AgentSettingsBehaviorPageView: FC<
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
<hr className="my-5 border-0 border-t border-solid border-border" />
|
||||
{/* ── Kyleosophy toggle (always visible) ── */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="m-0 text-[13px] font-semibold text-content-primary">
|
||||
Kyleosophy
|
||||
</h3>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<p className="!mt-0.5 m-0 flex-1 text-xs text-content-secondary">
|
||||
Replace the standard completion chime. IYKYK.
|
||||
</p>
|
||||
<Switch
|
||||
checked={kylesophyEnabled}
|
||||
onCheckedChange={(checked) => {
|
||||
setKylesophyEnabled(checked);
|
||||
setKylesophyLocal(checked);
|
||||
}}
|
||||
aria-label="Enable Kyleosophy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{showDefaultPromptPreview && (
|
||||
<TextPreviewDialog
|
||||
content={defaultSystemPrompt}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
getChimeEnabled,
|
||||
getKylesophyEnabled,
|
||||
KYLEOSOPHY_SOUNDS,
|
||||
LOCK_HOLD_MS,
|
||||
maybePlayChime,
|
||||
setChimeEnabled,
|
||||
setKylesophyEnabled,
|
||||
} from "./chime";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -69,6 +72,40 @@ describe("getChimeEnabled / setChimeEnabled", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kyleosophy preference helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getKylesophyEnabled / setKylesophyEnabled", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("defaults to false when nothing is stored", () => {
|
||||
expect(getKylesophyEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when stored as 'true'", () => {
|
||||
localStorage.setItem("agents.kyleosophy", "true");
|
||||
expect(getKylesophyEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when stored as 'false'", () => {
|
||||
localStorage.setItem("agents.kyleosophy", "false");
|
||||
expect(getKylesophyEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it("setKylesophyEnabled persists the value", () => {
|
||||
setKylesophyEnabled(false);
|
||||
expect(localStorage.getItem("agents.kyleosophy")).toBe("false");
|
||||
expect(getKylesophyEnabled()).toBe(false);
|
||||
|
||||
setKylesophyEnabled(true);
|
||||
expect(localStorage.getItem("agents.kyleosophy")).toBe("true");
|
||||
expect(getKylesophyEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// maybePlayChime
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -235,4 +272,44 @@ describe("maybePlayChime", () => {
|
||||
// Should play immediately without needing to advance timers.
|
||||
expect(playSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// -- Kyleosophy sound selection --
|
||||
|
||||
it("uses a kyleosophy sound when kyleosophy is enabled", async () => {
|
||||
setKylesophyEnabled(true);
|
||||
vi.spyOn(document, "hidden", "get").mockReturnValue(true);
|
||||
// Pin the random selection so the test is deterministic.
|
||||
vi.spyOn(Math, "random").mockReturnValue(0.5);
|
||||
|
||||
const audioSpy = vi.spyOn(globalThis, "Audio" as never);
|
||||
|
||||
await triggerAndSettle("running", "waiting", "chat-1", "chat-2");
|
||||
expect(playSpy).toHaveBeenCalledTimes(1);
|
||||
expect(audioSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const url = (audioSpy as unknown as ReturnType<typeof vi.fn>).mock
|
||||
.calls[0][0] as string;
|
||||
// Math.floor(0.5 * 8) = 4 → "/chime_5.mp3"
|
||||
expect(url).toBe("/chime_5.mp3");
|
||||
expect(KYLEOSOPHY_SOUNDS).toContain(url);
|
||||
});
|
||||
|
||||
it("uses default chime.mp3 when kyleosophy is disabled", async () => {
|
||||
setKylesophyEnabled(false);
|
||||
vi.spyOn(document, "hidden", "get").mockReturnValue(true);
|
||||
|
||||
// Force a fresh Audio element by spying on the constructor
|
||||
// before any call in this test. The previous test left
|
||||
// lastSoundUrl pointing at a kyleosophy URL, so switching
|
||||
// back to /chime.mp3 will always trigger a new Audio().
|
||||
const audioSpy = vi.spyOn(globalThis, "Audio" as never);
|
||||
|
||||
await triggerAndSettle("running", "waiting", "chat-1", "chat-2");
|
||||
expect(playSpy).toHaveBeenCalledTimes(1);
|
||||
expect(audioSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const url = (audioSpy as unknown as ReturnType<typeof vi.fn>).mock
|
||||
.calls[0][0] as string;
|
||||
expect(url).toBe("/chime.mp3");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const CHIME_PREFERENCE_KEY = "agents.chime-on-completion";
|
||||
const KYLEOSOPHY_PREFERENCE_KEY = "agents.kyleosophy";
|
||||
|
||||
export function getChimeEnabled(): boolean {
|
||||
try {
|
||||
@@ -19,20 +20,55 @@ export function setChimeEnabled(enabled: boolean): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function getKylesophyEnabled(): boolean {
|
||||
try {
|
||||
const stored = localStorage.getItem(KYLEOSOPHY_PREFERENCE_KEY);
|
||||
return stored === null ? false : stored === "true";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function setKylesophyEnabled(enabled: boolean): void {
|
||||
try {
|
||||
localStorage.setItem(KYLEOSOPHY_PREFERENCE_KEY, String(enabled));
|
||||
} catch {
|
||||
// Silently ignore storage errors (e.g. private browsing
|
||||
// quota exceeded).
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the completion chime audio file. The file is a short,
|
||||
* warm two-tone bell sound shipped as a static asset.
|
||||
*
|
||||
* A single Audio element is reused across calls so the browser
|
||||
* only fetches the file once.
|
||||
* Alternative completion sounds for Kyleosophy mode. All are
|
||||
* shipped as static assets alongside chime.mp3.
|
||||
*/
|
||||
export const KYLEOSOPHY_SOUNDS: readonly string[] = [
|
||||
"/chime_1.mp3", // absolutely massive
|
||||
"/chime_2.mp3", // dope
|
||||
"/chime_3.mp3", // great
|
||||
"/chime_4.mp3", // oh god
|
||||
"/chime_5.mp3", // okay
|
||||
"/chime_6.mp3", // open up a pr
|
||||
"/chime_7.mp3", // sweet
|
||||
"/chime_8.mp3", // yep
|
||||
];
|
||||
|
||||
/**
|
||||
* Play a completion sound. When Kyleosophy is enabled a random
|
||||
* voice clip is selected; otherwise the default bell chime is
|
||||
* used. The Audio element is cached and reused when the sound
|
||||
* URL hasn't changed between calls.
|
||||
*/
|
||||
let chimeAudio: HTMLAudioElement | null = null;
|
||||
let lastSoundUrl: string | null = null;
|
||||
|
||||
function playChimeAudio(): void {
|
||||
function playChimeAudio(soundUrl = "/chime.mp3"): void {
|
||||
try {
|
||||
if (!chimeAudio) {
|
||||
chimeAudio = new Audio("/chime.mp3");
|
||||
if (!chimeAudio || soundUrl !== lastSoundUrl) {
|
||||
chimeAudio?.pause();
|
||||
chimeAudio = new Audio(soundUrl);
|
||||
chimeAudio.volume = 0.5;
|
||||
lastSoundUrl = soundUrl;
|
||||
}
|
||||
// Reset to the start in case a previous play hasn't
|
||||
// finished yet.
|
||||
@@ -75,9 +111,9 @@ export const LOCK_HOLD_MS = 2000;
|
||||
* Falls back to playing immediately when the Web Locks API is
|
||||
* not available (preserving the original single-tab behavior).
|
||||
*/
|
||||
function playChime(chatID: string): void {
|
||||
function playChime(chatID: string, soundUrl?: string): void {
|
||||
if (typeof navigator === "undefined" || !navigator.locks) {
|
||||
playChimeAudio();
|
||||
playChimeAudio(soundUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,7 +129,7 @@ function playChime(chatID: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
playChimeAudio();
|
||||
playChimeAudio(soundUrl);
|
||||
|
||||
// Hold the lock briefly so that tabs receiving the
|
||||
// WebSocket event a bit later will see the lock as
|
||||
@@ -149,5 +185,9 @@ export function maybePlayChime(
|
||||
return;
|
||||
}
|
||||
|
||||
playChime(chatID);
|
||||
const soundUrl = getKylesophyEnabled()
|
||||
? KYLEOSOPHY_SOUNDS[Math.floor(Math.random() * KYLEOSOPHY_SOUNDS.length)]
|
||||
: undefined;
|
||||
|
||||
playChime(chatID, soundUrl);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user