diff options
| author | Adam <[email protected]> | 2025-12-22 19:38:50 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-22 19:39:00 -0600 |
| commit | 794fe8f381c846f5241800363023d892c12cf495 (patch) | |
| tree | bff98689edfa635a2a9f39cb4ea61639b97f5b2d /packages/app/src/context/notification.tsx | |
| parent | a4eebf9f08262f6bf63017710e2e6d9672ec6708 (diff) | |
| download | opencode-794fe8f381c846f5241800363023d892c12cf495.tar.gz opencode-794fe8f381c846f5241800363023d892c12cf495.zip | |
chore: rename packages/desktop -> packages/app
Diffstat (limited to 'packages/app/src/context/notification.tsx')
| -rw-r--r-- | packages/app/src/context/notification.tsx | 127 |
1 files changed, 127 insertions, 0 deletions
diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx new file mode 100644 index 000000000..2b258ebd6 --- /dev/null +++ b/packages/app/src/context/notification.tsx @@ -0,0 +1,127 @@ +import { createStore } from "solid-js/store" +import { createSimpleContext } from "@opencode-ai/ui/context" +import { useGlobalSDK } from "./global-sdk" +import { useGlobalSync } from "./global-sync" +import { Binary } from "@opencode-ai/util/binary" +import { EventSessionError } from "@opencode-ai/sdk/v2" +import { makeAudioPlayer } from "@solid-primitives/audio" +import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac" +import errorSound from "@opencode-ai/ui/audio/nope-03.aac" +import { persisted } from "@/utils/persist" + +type NotificationBase = { + directory?: string + session?: string + metadata?: any + time: number + viewed: boolean +} + +type TurnCompleteNotification = NotificationBase & { + type: "turn-complete" +} + +type ErrorNotification = NotificationBase & { + type: "error" + error: EventSessionError["properties"]["error"] +} + +export type Notification = TurnCompleteNotification | ErrorNotification + +export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({ + name: "Notification", + init: () => { + let idlePlayer: ReturnType<typeof makeAudioPlayer> | undefined + let errorPlayer: ReturnType<typeof makeAudioPlayer> | undefined + + try { + idlePlayer = makeAudioPlayer(idleSound) + errorPlayer = makeAudioPlayer(errorSound) + } catch (err) { + console.log("Failed to load audio", err) + } + + const globalSDK = useGlobalSDK() + const globalSync = useGlobalSync() + + const [store, setStore, _, ready] = persisted( + "notification.v1", + createStore({ + list: [] as Notification[], + }), + ) + + globalSDK.event.listen((e) => { + const directory = e.name + const event = e.details + const base = { + directory, + time: Date.now(), + viewed: false, + } + switch (event.type) { + case "session.idle": { + const sessionID = event.properties.sessionID + const [syncStore] = globalSync.child(directory) + const match = Binary.search(syncStore.session, sessionID, (s) => s.id) + const isChild = match.found && syncStore.session[match.index].parentID + if (isChild) break + try { + idlePlayer?.play() + } catch {} + setStore("list", store.list.length, { + ...base, + type: "turn-complete", + session: sessionID, + }) + break + } + case "session.error": { + const sessionID = event.properties.sessionID + if (sessionID) { + const [syncStore] = globalSync.child(directory) + const match = Binary.search(syncStore.session, sessionID, (s) => s.id) + const isChild = match.found && syncStore.session[match.index].parentID + if (isChild) break + } + try { + errorPlayer?.play() + } catch {} + setStore("list", store.list.length, { + ...base, + type: "error", + session: sessionID ?? "global", + error: "error" in event.properties ? event.properties.error : undefined, + }) + break + } + } + }) + + return { + ready, + session: { + all(session: string) { + return store.list.filter((n) => n.session === session) + }, + unseen(session: string) { + return store.list.filter((n) => n.session === session && !n.viewed) + }, + markViewed(session: string) { + setStore("list", (n) => n.session === session, "viewed", true) + }, + }, + project: { + all(directory: string) { + return store.list.filter((n) => n.directory === directory) + }, + unseen(directory: string) { + return store.list.filter((n) => n.directory === directory && !n.viewed) + }, + markViewed(directory: string) { + setStore("list", (n) => n.directory === directory, "viewed", true) + }, + }, + } + }, +}) |
