opencode/packages/desktop/src/context/notification.tsx

129 lines
3.9 KiB
TypeScript
Raw Normal View History

2025-12-12 19:14:47 +08:00
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { makePersisted } from "@solid-primitives/storage"
import { useGlobalSDK } from "./global-sdk"
2025-12-16 00:13:29 +08:00
import { useGlobalSync } from "./global-sync"
import { Binary } from "@opencode-ai/util/binary"
2025-12-12 19:14:47 +08:00
import { EventSessionError } from "@opencode-ai/sdk/v2"
import { makeAudioPlayer } from "@solid-primitives/audio"
import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
2025-12-15 19:54:44 +08:00
import errorSound from "@opencode-ai/ui/audio/nope-03.aac"
2025-12-12 19:14:47 +08:00
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: () => {
2025-12-17 02:25:15 +08:00
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)
}
2025-12-12 19:14:47 +08:00
const globalSDK = useGlobalSDK()
2025-12-16 00:13:29 +08:00
const globalSync = useGlobalSync()
2025-12-12 19:14:47 +08:00
const [store, setStore] = makePersisted(
createStore({
list: [] as Notification[],
}),
{
name: "notification.v1",
},
)
globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
const base = {
directory,
time: Date.now(),
viewed: false,
}
switch (event.type) {
2025-12-12 19:14:47 +08:00
case "session.idle": {
2025-12-16 00:13:29 +08:00
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
2025-12-17 03:34:22 +08:00
try {
idlePlayer?.play()
} catch {}
2025-12-12 19:14:47 +08:00
setStore("list", store.list.length, {
...base,
type: "turn-complete",
2025-12-16 00:13:29 +08:00
session: sessionID,
2025-12-12 19:14:47 +08:00
})
break
}
case "session.error": {
2025-12-16 00:13:29 +08:00
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
}
2025-12-17 03:34:22 +08:00
try {
errorPlayer?.play()
} catch {}
2025-12-12 19:14:47 +08:00
setStore("list", store.list.length, {
...base,
type: "error",
2025-12-16 00:13:29 +08:00
session: sessionID ?? "global",
2025-12-12 19:14:47 +08:00
error: "error" in event.properties ? event.properties.error : undefined,
})
break
}
}
})
return {
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)
},
},
}
},
})