summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/context
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2025-12-12 20:23:06 +0000
committerDavid Hill <[email protected]>2025-12-12 20:23:06 +0000
commit9b77246246ab0d81aaee23514c343ccb93c36847 (patch)
tree09749dd9d91275b7fec4035fafbefc9e4cd0cb6c /packages/desktop/src/context
parentcf3bc1e0a6324a01ae10a2849476d7dc07e4876b (diff)
parentd51c6ca39f382ef616157680ca7366b6ee58713e (diff)
downloadopencode-9b77246246ab0d81aaee23514c343ccb93c36847.tar.gz
opencode-9b77246246ab0d81aaee23514c343ccb93c36847.zip
Merge branch 'dev' of https://github.com/sst/opencode into dev
Diffstat (limited to 'packages/desktop/src/context')
-rw-r--r--packages/desktop/src/context/global-sync.tsx78
-rw-r--r--packages/desktop/src/context/layout.tsx24
-rw-r--r--packages/desktop/src/context/notification.tsx106
-rw-r--r--packages/desktop/src/context/sync.tsx9
4 files changed, 170 insertions, 47 deletions
diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx
index 2a24a845c..8151a2c6f 100644
--- a/packages/desktop/src/context/global-sync.tsx
+++ b/packages/desktop/src/context/global-sync.tsx
@@ -55,45 +55,20 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
const globalSDK = useGlobalSDK()
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
+ path: Path
project: Project[]
provider: ProviderListResponse
provider_auth: ProviderAuthResponse
children: Record<string, State>
}>({
ready: false,
+ path: { state: "", config: "", worktree: "", directory: "", home: "" },
project: [],
provider: { all: [], connected: [], default: {} },
provider_auth: {},
children: {},
})
- async function bootstrapInstance(directory: string) {
- const [store, setStore] = child(directory)
- const sdk = createOpencodeClient({
- baseUrl: globalSDK.url,
- directory,
- })
- const load = {
- project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
- provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
- path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
- agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
- session: () =>
- sdk.session.list().then((x) => {
- const sessions = (x.data ?? [])
- .slice()
- .sort((a, b) => a.id.localeCompare(b.id))
- .slice(0, store.limit)
- setStore("session", sessions)
- }),
- status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
- config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
- changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
- node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
- }
- await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
- }
-
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
function child(directory: string) {
if (!children[directory]) {
@@ -120,6 +95,38 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
return children[directory]
}
+ async function loadSessions(directory: string) {
+ globalSDK.client.session.list({ directory }).then((x) => {
+ const sessions = (x.data ?? [])
+ .slice()
+ .filter((s) => !s.time.archived)
+ .sort((a, b) => a.id.localeCompare(b.id))
+ .slice(0, 5)
+ const [, setStore] = child(directory)
+ setStore("session", sessions)
+ })
+ }
+
+ async function bootstrapInstance(directory: string) {
+ const [, setStore] = child(directory)
+ const sdk = createOpencodeClient({
+ baseUrl: globalSDK.url,
+ directory,
+ })
+ const load = {
+ project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
+ provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
+ path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
+ agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
+ session: () => loadSessions(directory),
+ status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
+ config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
+ changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
+ node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
+ }
+ await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
+ }
+
globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
@@ -156,6 +163,17 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
}
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
+ if (event.properties.info.time.archived) {
+ if (result.found) {
+ setStore(
+ "session",
+ produce((draft) => {
+ draft.splice(result.index, 1)
+ }),
+ )
+ }
+ break
+ }
if (result.found) {
setStore("session", result.index, reconcile(event.properties.info))
break
@@ -224,6 +242,9 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
async function bootstrap() {
return Promise.all([
+ globalSDK.client.path.get().then((x) => {
+ setGlobalStore("path", x.data!)
+ }),
globalSDK.client.project.list().then(async (x) => {
setGlobalStore(
"project",
@@ -252,6 +273,9 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
},
child,
bootstrap,
+ project: {
+ loadSessions,
+ },
}
},
})
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index 9cafdce96..3d5cad761 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<{
@@ -97,21 +92,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const enriched = createMemo(() => store.projects.flatMap(enrich))
const list = createMemo(() => enriched().flatMap(colorize))
- async function loadProjectSessions(directory: string) {
- const [, setStore] = globalSync.child(directory)
- globalSdk.client.session.list({ directory }).then((x) => {
- const sessions = (x.data ?? [])
- .slice()
- .sort((a, b) => a.id.localeCompare(b.id))
- .slice(0, 5)
- setStore("session", sessions)
- })
- }
-
onMount(() => {
Promise.all(
store.projects.map((project) => {
- return loadProjectSessions(project.worktree)
+ return globalSync.project.loadSessions(project.worktree)
}),
)
})
@@ -121,7 +105,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
list,
open(directory: string) {
if (store.projects.find((x) => x.worktree === directory)) return
- loadProjectSessions(directory)
+ globalSync.project.loadSessions(directory)
setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x])
},
close(directory: string) {
diff --git a/packages/desktop/src/context/notification.tsx b/packages/desktop/src/context/notification.tsx
new file mode 100644
index 000000000..744e4fdf3
--- /dev/null
+++ b/packages/desktop/src/context/notification.tsx
@@ -0,0 +1,106 @@
+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 const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
+ name: "Notification",
+ init: () => {
+ const idlePlayer = makeAudioPlayer(idleSound)
+ const globalSDK = useGlobalSDK()
+
+ const [store, setStore] = makePersisted(
+ createStore({
+ list: [] as Notification[],
+ }),
+ {
+ 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": {
+ 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)
+ },
+ },
+ }
+ },
+})
diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx
index 85758c5b6..2ab54b3ae 100644
--- a/packages/desktop/src/context/sync.tsx
+++ b/packages/desktop/src/context/sync.tsx
@@ -65,6 +65,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
})
},
more: createMemo(() => store.session.length >= store.limit),
+ archive: async (sessionID: string) => {
+ await sdk.client.session.update({ sessionID, time: { archived: Date.now() } })
+ setStore(
+ produce((draft) => {
+ const match = Binary.search(draft.session, sessionID, (s) => s.id)
+ if (match.found) draft.session.splice(match.index, 1)
+ }),
+ )
+ },
},
absolute,
get directory() {