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:
@@ -54,8 +54,8 @@ import i18n from "../i18n";
|
||||
import { api } from "../lib/api";
|
||||
import { eventBus } from "../lib/eventBus";
|
||||
import { subscribeToPush } from "../lib/push";
|
||||
import { notify } from "../lib/notify";
|
||||
import { loadNotifPrefs } from "../lib/notifPrefs";
|
||||
import { notify, playSound } from "../lib/notify";
|
||||
import { loadNotifPrefs, soundFor, type NotifType } from "../lib/notifPrefs";
|
||||
import type { WSMessage, Session, Agent, DashboardEvent, Lane } from "../lib/types";
|
||||
|
||||
/**
|
||||
@@ -104,12 +104,20 @@ export function useNotifications() {
|
||||
|
||||
const prefs = loadNotifPrefs();
|
||||
if (!prefs.enabled) return;
|
||||
// A notification that was shown (pushed, native or local) plays its
|
||||
// type's sound; one that reached nobody plays nothing.
|
||||
const show = (type: NotifType, title: string, body: string, url?: string) =>
|
||||
void notify(title, body, url).then((r) => {
|
||||
if (r.via === "native" || r.via === "push" || r.via === "local")
|
||||
playSound(soundFor(prefs, type));
|
||||
});
|
||||
|
||||
switch (msg.type) {
|
||||
case "session_created": {
|
||||
if (!prefs.onNewSession) return;
|
||||
const s = msg.data as Session;
|
||||
notify(
|
||||
show(
|
||||
"onNewSession",
|
||||
i18n.t("errors:notifications.newSession"),
|
||||
s.name || `${i18n.t("errors:notifications.sessionDefault")}${s.id.slice(0, 8)}`,
|
||||
laneUrlForSession(s.id)
|
||||
@@ -119,7 +127,8 @@ export function useNotifications() {
|
||||
case "session_updated": {
|
||||
const s = msg.data as Session;
|
||||
if (s.status === "error" && prefs.onSessionError) {
|
||||
notify(
|
||||
show(
|
||||
"onSessionError",
|
||||
i18n.t("errors:notifications.sessionError"),
|
||||
s.name || `${i18n.t("errors:notifications.sessionDefault")}${s.id.slice(0, 8)}`,
|
||||
laneUrlForSession(s.id)
|
||||
@@ -131,7 +140,8 @@ export function useNotifications() {
|
||||
if (!prefs.onSubagentSpawn) return;
|
||||
const a = msg.data as Agent;
|
||||
if (a.type === "subagent") {
|
||||
notify(
|
||||
show(
|
||||
"onSubagentSpawn",
|
||||
i18n.t("errors:notifications.subagentSpawned"),
|
||||
a.name,
|
||||
laneUrlForSession(a.session_id)
|
||||
@@ -148,19 +158,22 @@ export function useNotifications() {
|
||||
// `onSessionComplete` / nothing at all, so the one thing the user
|
||||
// actually waits for was either off by default or unswitchable.
|
||||
if (ev.event_type === "Notification" && prefs.onAwaitingInput) {
|
||||
notify(
|
||||
show(
|
||||
"onAwaitingInput",
|
||||
i18n.t("errors:notifications.awaitingInput"),
|
||||
ev.summary || i18n.t("errors:notifications.defaultBody"),
|
||||
url
|
||||
);
|
||||
} else if (ev.event_type === "Stop" && prefs.onTurnComplete) {
|
||||
notify(
|
||||
show(
|
||||
"onTurnComplete",
|
||||
i18n.t("errors:notifications.finishedResponding"),
|
||||
ev.summary || i18n.t("errors:notifications.readyForInput"),
|
||||
url
|
||||
);
|
||||
} else if (ev.event_type === "SessionEnd" && prefs.onSessionComplete) {
|
||||
notify(
|
||||
show(
|
||||
"onSessionComplete",
|
||||
i18n.t("errors:notifications.sessionCompleted"),
|
||||
ev.summary || i18n.t("errors:notifications.sessionClosed"),
|
||||
url
|
||||
|
||||
Reference in New Issue
Block a user