diff options
| author | David Hill <[email protected]> | 2025-12-12 20:23:06 +0000 |
|---|---|---|
| committer | David Hill <[email protected]> | 2025-12-12 20:23:06 +0000 |
| commit | 9b77246246ab0d81aaee23514c343ccb93c36847 (patch) | |
| tree | 09749dd9d91275b7fec4035fafbefc9e4cd0cb6c /packages/desktop/src/context | |
| parent | cf3bc1e0a6324a01ae10a2849476d7dc07e4876b (diff) | |
| parent | d51c6ca39f382ef616157680ca7366b6ee58713e (diff) | |
| download | opencode-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.tsx | 78 | ||||
| -rw-r--r-- | packages/desktop/src/context/layout.tsx | 24 | ||||
| -rw-r--r-- | packages/desktop/src/context/notification.tsx | 106 | ||||
| -rw-r--r-- | packages/desktop/src/context/sync.tsx | 9 |
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() { |
