summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/context
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-12 05:14:47 -0600
committerAdam <[email protected]>2025-12-12 05:14:51 -0600
commit04b4dacee3c0705725241ab3d92ac83ccb9a80a9 (patch)
treecf4a6ce9894e915fc174cb5c19d4c05ed5342bd8 /packages/desktop/src/context
parentc0e30f48c6cc497d849626026699f71503a6074d (diff)
downloadopencode-04b4dacee3c0705725241ab3d92ac83ccb9a80a9.tar.gz
opencode-04b4dacee3c0705725241ab3d92ac83ccb9a80a9.zip
feat(desktop): basic alerting
Diffstat (limited to 'packages/desktop/src/context')
-rw-r--r--packages/desktop/src/context/layout.tsx9
-rw-r--r--packages/desktop/src/context/notification.tsx130
2 files changed, 132 insertions, 7 deletions
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index 9cafdce96..4ec0af601 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -7,15 +7,10 @@ import { useGlobalSDK } from "./global-sdk"
import { Project } from "@opencode-ai/sdk/v2"
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
-
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
-export function isAvatarColorKey(value: string): value is AvatarColorKey {
- return AVATAR_COLOR_KEYS.includes(value as AvatarColorKey)
-}
-
export function getAvatarColors(key?: string) {
- if (key && isAvatarColorKey(key)) {
+ if (key && AVATAR_COLOR_KEYS.includes(key as AvatarColorKey)) {
return {
background: `var(--avatar-background-${key})`,
foreground: `var(--avatar-text-${key})`,
@@ -50,7 +45,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
}),
{
- name: "default-layout.v7",
+ name: "layout.v1",
},
)
const [ephemeral, setEphemeral] = createStore<{
diff --git a/packages/desktop/src/context/notification.tsx b/packages/desktop/src/context/notification.tsx
new file mode 100644
index 000000000..4e334126f
--- /dev/null
+++ b/packages/desktop/src/context/notification.tsx
@@ -0,0 +1,130 @@
+import { createStore } from "solid-js/store"
+import { createSimpleContext } from "@opencode-ai/ui/context"
+import { makePersisted } from "@solid-primitives/storage"
+import { useGlobalSDK } from "./global-sdk"
+import { EventSessionError } from "@opencode-ai/sdk/v2"
+import { makeAudioPlayer } from "@solid-primitives/audio"
+import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
+
+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 type AudioSettings = {
+ enabled: boolean
+ volume: number
+}
+
+export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
+ name: "Notification",
+ init: () => {
+ const idlePlayer = makeAudioPlayer(idleSound)
+ const globalSDK = useGlobalSDK()
+
+ const [store, setStore] = makePersisted(
+ createStore({
+ list: [] as Notification[],
+ audio: {
+ enabled: true,
+ volume: 1,
+ } as AudioSettings,
+ }),
+ {
+ name: "notification.v1",
+ },
+ )
+
+ // onMount(() => {
+ // const daysToKeep = 7
+ // // setStore("list", (n) => n.filter((n) => !n.viewed && n.time + 1000 * 60 * 60 * 24 * daysToKeep < Date.now()))
+ // })
+
+ 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": {
+ if (store.audio.enabled) {
+ idlePlayer.setVolume(store.audio.volume)
+ idlePlayer.play()
+ }
+ const session = event.properties.sessionID
+ setStore("list", store.list.length, {
+ ...base,
+ type: "turn-complete",
+ session,
+ })
+ break
+ }
+ case "session.error": {
+ const session = event.properties.sessionID ?? "global"
+ // errorPlayer.play()
+ setStore("list", store.list.length, {
+ ...base,
+ type: "error",
+ session,
+ 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)
+ },
+ },
+ audio: {
+ get settings() {
+ return store.audio
+ },
+ setEnabled(enabled: boolean) {
+ setStore("audio", "enabled", enabled)
+ },
+ setVolume(volume: number) {
+ const clamped = Math.max(0, Math.min(1, volume))
+ setStore("audio", "volume", clamped)
+ },
+ },
+ }
+ },
+})