feat(ui): Orca Dark / Orca Light themes and notification sounds

Two optional named themes from Orca's neutral palette join the Appearance
picker (Orca Dark keeps a blue accent so white-on-accent buttons stay
legible; Orca Light's status colours are darkened to clear 4.5:1 on their
chips). A shown notification can play a sound — None (default), Chime, Pop
or Alert — chosen globally and per notification type in Settings →
Notifications; the WAVs are generated by scripts/gen-notification-sounds.js.
This commit is contained in:
2026-10-01 17:10:46 +07:00
parent eb90244182
commit b71024f244
20 changed files with 487 additions and 55 deletions
+24 -1
View File
@@ -3,12 +3,26 @@
* the defaults, and the `localStorage` read/write. Settings (which edits them)
* and `useNotifications` (which obeys them) both import from here; they used to
* carry private copies, so adding a notification type meant editing the same
* literal twice and a miss went unnoticed until a toggle did nothing.
* literal twice and a miss went unnoticed until a toggle did nothing. A
* shown notification also plays a sound: `sound` is the default, and
* `soundByType` may override it per notification type (`soundFor`).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
export const NOTIF_KEY = "agent-monitor-notifications";
export const NOTIF_SOUNDS = ["none", "chime", "pop", "alert"] as const;
export type NotifSound = (typeof NOTIF_SOUNDS)[number];
/** The per-type flags — every key a notification is gated on. */
export type NotifType =
| "onNewSession"
| "onSessionError"
| "onAwaitingInput"
| "onTurnComplete"
| "onSessionComplete"
| "onSubagentSpawn";
export interface NotifPrefs {
/** Master switch; when false no notification fires, whatever the flags say. */
enabled: boolean;
@@ -21,6 +35,9 @@ export interface NotifPrefs {
/** The whole session closed (`SessionEnd`), not just one turn. */
onSessionComplete: boolean;
onSubagentSpawn: boolean;
/** The sound a shown notification plays, unless its type overrides it. */
sound: NotifSound;
soundByType?: Partial<Record<NotifType, NotifSound>>;
}
// The two "Claude is waiting for you" types default ON: they are the reason
@@ -34,8 +51,14 @@ export const defaultNotifPrefs: NotifPrefs = {
onTurnComplete: true,
onSessionComplete: false,
onSubagentSpawn: false,
sound: "none",
};
/** The sound for one notification type: its override, else the default. */
export function soundFor(prefs: NotifPrefs, type: NotifType): NotifSound {
return prefs.soundByType?.[type] ?? prefs.sound;
}
/** Reads prefs, merged over the defaults so a stored blob written by an older
* build (missing the newer flags) still yields a complete object. */
export function loadNotifPrefs(): NotifPrefs {