diff --git a/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx
index 4b7280e6c4..5af5c7235c 100644
--- a/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx
+++ b/site/src/pages/AgentsPage/AgentSettingsBehaviorPageView.tsx
@@ -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<
>
)}
+
+ {/* ── Kyleosophy toggle (always visible) ── */}
+
+
+ Kyleosophy
+
+
+
+ Replace the standard completion chime. IYKYK.
+
+
{
+ setKylesophyEnabled(checked);
+ setKylesophyLocal(checked);
+ }}
+ aria-label="Enable Kyleosophy"
+ />
+
+
{showDefaultPromptPreview && (
{
});
});
+// ---------------------------------------------------------------------------
+// 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).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).mock
+ .calls[0][0] as string;
+ expect(url).toBe("/chime.mp3");
+ });
});
diff --git a/site/src/pages/AgentsPage/utils/chime.ts b/site/src/pages/AgentsPage/utils/chime.ts
index 1e365b0b71..f0339055a0 100644
--- a/site/src/pages/AgentsPage/utils/chime.ts
+++ b/site/src/pages/AgentsPage/utils/chime.ts
@@ -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);
}
diff --git a/site/static/chime_1.mp3 b/site/static/chime_1.mp3
new file mode 100644
index 0000000000..67a11ec2ae
Binary files /dev/null and b/site/static/chime_1.mp3 differ
diff --git a/site/static/chime_2.mp3 b/site/static/chime_2.mp3
new file mode 100644
index 0000000000..fc0a277631
Binary files /dev/null and b/site/static/chime_2.mp3 differ
diff --git a/site/static/chime_3.mp3 b/site/static/chime_3.mp3
new file mode 100644
index 0000000000..578215df88
Binary files /dev/null and b/site/static/chime_3.mp3 differ
diff --git a/site/static/chime_4.mp3 b/site/static/chime_4.mp3
new file mode 100644
index 0000000000..455f668e67
Binary files /dev/null and b/site/static/chime_4.mp3 differ
diff --git a/site/static/chime_5.mp3 b/site/static/chime_5.mp3
new file mode 100644
index 0000000000..abeb3453c6
Binary files /dev/null and b/site/static/chime_5.mp3 differ
diff --git a/site/static/chime_6.mp3 b/site/static/chime_6.mp3
new file mode 100644
index 0000000000..719d51ad00
Binary files /dev/null and b/site/static/chime_6.mp3 differ
diff --git a/site/static/chime_7.mp3 b/site/static/chime_7.mp3
new file mode 100644
index 0000000000..b3bb8267a3
Binary files /dev/null and b/site/static/chime_7.mp3 differ
diff --git a/site/static/chime_8.mp3 b/site/static/chime_8.mp3
new file mode 100644
index 0000000000..a32a482013
Binary files /dev/null and b/site/static/chime_8.mp3 differ