summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/context/notification.tsx
blob: 2b258ebd6f3dff13dfd19c904c60903be48ba2e4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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)
        },
      },
    }
  },
})