summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/context/notification.tsx
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-22 19:38:50 -0600
committerAdam <[email protected]>2025-12-22 19:39:00 -0600
commit794fe8f381c846f5241800363023d892c12cf495 (patch)
treebff98689edfa635a2a9f39cb4ea61639b97f5b2d /packages/app/src/context/notification.tsx
parenta4eebf9f08262f6bf63017710e2e6d9672ec6708 (diff)
downloadopencode-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.tsx127
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)
+ },
+ },
+ }
+ },
+})