summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/context
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-11-18 11:00:27 -0600
committeropencode <[email protected]>2025-11-18 17:07:34 +0000
commitfc5fc2c570e6ac5a6f9647080b80f7fec0c4b605 (patch)
treea3971bfbe17308df9f2d3e1771c0ab544900c099 /packages/desktop/src/context
parent4069999b782cc00d4e707f5eca32082bdfad45bc (diff)
downloadopencode-fc5fc2c570e6ac5a6f9647080b80f7fec0c4b605.tar.gz
opencode-fc5fc2c570e6ac5a6f9647080b80f7fec0c4b605.zip
wip(desktop): new layout work
Diffstat (limited to 'packages/desktop/src/context')
-rw-r--r--packages/desktop/src/context/global-sdk.tsx33
-rw-r--r--packages/desktop/src/context/global-sync.tsx174
-rw-r--r--packages/desktop/src/context/layout.tsx25
-rw-r--r--packages/desktop/src/context/local.tsx2
-rw-r--r--packages/desktop/src/context/sdk.tsx25
-rw-r--r--packages/desktop/src/context/session.tsx26
-rw-r--r--packages/desktop/src/context/sync.tsx128
7 files changed, 265 insertions, 148 deletions
diff --git a/packages/desktop/src/context/global-sdk.tsx b/packages/desktop/src/context/global-sdk.tsx
new file mode 100644
index 000000000..59bd9b87d
--- /dev/null
+++ b/packages/desktop/src/context/global-sdk.tsx
@@ -0,0 +1,33 @@
+import { createOpencodeClient, type Event } from "@opencode-ai/sdk/client"
+import { createSimpleContext } from "./helper"
+import { createGlobalEmitter } from "@solid-primitives/event-bus"
+import { onCleanup } from "solid-js"
+
+export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
+ name: "GlobalSDK",
+ init: (props: { url: string }) => {
+ const abort = new AbortController()
+ const sdk = createOpencodeClient({
+ baseUrl: props.url,
+ signal: abort.signal,
+ })
+
+ const emitter = createGlobalEmitter<{
+ [key: string]: Event
+ }>()
+
+ sdk.global.event().then(async (events) => {
+ for await (const event of events.stream) {
+ console.log("event", event)
+ // console.log("event", event.payload.type)
+ emitter.emit(event.directory, event.payload)
+ }
+ })
+
+ onCleanup(() => {
+ abort.abort()
+ })
+
+ return { url: props.url, client: sdk, event: emitter }
+ },
+})
diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx
new file mode 100644
index 000000000..99c679c93
--- /dev/null
+++ b/packages/desktop/src/context/global-sync.tsx
@@ -0,0 +1,174 @@
+import type {
+ Message,
+ Agent,
+ Provider,
+ Session,
+ Part,
+ Config,
+ Path,
+ File,
+ FileNode,
+ Project,
+ FileDiff,
+ Todo,
+} from "@opencode-ai/sdk"
+import { createStore, produce, reconcile } from "solid-js/store"
+import { Binary } from "@/utils/binary"
+import { createSimpleContext } from "./helper"
+import { useGlobalSDK } from "./global-sdk"
+
+type State = {
+ ready: boolean
+ provider: Provider[]
+ agent: Agent[]
+ project: Project
+ config: Config
+ path: Path
+ session: Session[]
+ session_diff: {
+ [sessionID: string]: FileDiff[]
+ }
+ todo: {
+ [sessionID: string]: Todo[]
+ }
+ limit: number
+ message: {
+ [sessionID: string]: Message[]
+ }
+ part: {
+ [messageID: string]: Part[]
+ }
+ node: FileNode[]
+ changes: File[]
+}
+
+export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({
+ name: "GlobalSync",
+ init: () => {
+ const [globalStore, setGlobalStore] = createStore<{
+ ready: boolean
+ defaultProject?: Project // TODO: remove this when we can select projects
+ projects: Project[]
+ children: Record<string, State>
+ }>({
+ ready: false,
+ projects: [],
+ children: {},
+ })
+
+ const children: Record<string, ReturnType<typeof createStore<State>>> = {}
+
+ function child(directory: string) {
+ if (!children[directory]) {
+ setGlobalStore("children", directory, {
+ project: { id: "", worktree: "", time: { created: 0, initialized: 0 } },
+ config: {},
+ path: { state: "", config: "", worktree: "", directory: "" },
+ ready: false,
+ agent: [],
+ provider: [],
+ session: [],
+ session_diff: {},
+ todo: {},
+ limit: 10,
+ message: {},
+ part: {},
+ node: [],
+ changes: [],
+ })
+ children[directory] = createStore(globalStore.children[directory])
+ }
+ return children[directory]
+ }
+
+ const sdk = useGlobalSDK()
+ sdk.event.listen((e) => {
+ const directory = e.name
+ const [store, setStore] = child(directory)
+
+ const event = e.details
+ switch (event.type) {
+ case "session.updated": {
+ const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
+ if (result.found) {
+ setStore("session", result.index, reconcile(event.properties.info))
+ break
+ }
+ setStore(
+ "session",
+ produce((draft) => {
+ draft.splice(result.index, 0, event.properties.info)
+ }),
+ )
+ break
+ }
+ case "session.diff":
+ setStore("session_diff", event.properties.sessionID, event.properties.diff)
+ break
+ case "todo.updated":
+ setStore("todo", event.properties.sessionID, event.properties.todos)
+ break
+ case "message.updated": {
+ const messages = store.message[event.properties.info.sessionID]
+ if (!messages) {
+ setStore("message", event.properties.info.sessionID, [event.properties.info])
+ break
+ }
+ const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
+ if (result.found) {
+ setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
+ break
+ }
+ setStore(
+ "message",
+ event.properties.info.sessionID,
+ produce((draft) => {
+ draft.splice(result.index, 0, event.properties.info)
+ }),
+ )
+ break
+ }
+ case "message.part.updated": {
+ const part = event.properties.part
+ const parts = store.part[part.messageID]
+ if (!parts) {
+ setStore("part", part.messageID, [part])
+ break
+ }
+ const result = Binary.search(parts, part.id, (p) => p.id)
+ if (result.found) {
+ setStore("part", part.messageID, result.index, reconcile(part))
+ break
+ }
+ setStore(
+ "part",
+ part.messageID,
+ produce((draft) => {
+ draft.splice(result.index, 0, part)
+ }),
+ )
+ break
+ }
+ }
+ })
+
+ Promise.all([
+ sdk.client.project.list().then((x) =>
+ setGlobalStore(
+ "projects",
+ x.data!.filter((x) => !x.worktree.includes("opencode-test")),
+ ),
+ ),
+ // TODO: remove this when we can select projects
+ sdk.client.project.current().then((x) => setGlobalStore("defaultProject", x.data)),
+ ]).then(() => setGlobalStore("ready", true))
+
+ return {
+ data: globalStore,
+ get ready() {
+ return globalStore.ready
+ },
+ child,
+ }
+ },
+})
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index 9e4af90aa..d6edd84d1 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -2,12 +2,15 @@ import { createStore } from "solid-js/store"
import { createMemo } from "solid-js"
import { createSimpleContext } from "./helper"
import { makePersisted } from "@solid-primitives/storage"
+import { useGlobalSync } from "./global-sync"
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
init: () => {
+ const globalSync = useGlobalSync()
const [store, setStore] = makePersisted(
createStore({
+ projects: [] as { directory: string; expanded: boolean }[],
sidebar: {
opened: true,
width: 280,
@@ -17,11 +20,31 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
}),
{
- name: "__default-layout",
+ name: "___default-layout",
},
)
return {
+ projects: {
+ list: createMemo(() =>
+ globalSync.data.defaultProject
+ ? [{ directory: globalSync.data.defaultProject!.worktree, expanded: true }, ...store.projects]
+ : store.projects,
+ ),
+ open(directory: string) {
+ if (store.projects.find((x) => x.directory === directory)) return
+ setStore("projects", (x) => [...x, { directory, expanded: true }])
+ },
+ close(directory: string) {
+ setStore("projects", (x) => x.filter((x) => x.directory !== directory))
+ },
+ expand(directory: string) {
+ setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: true } : x)))
+ },
+ collapse(directory: string) {
+ setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: false } : x)))
+ },
+ },
sidebar: {
opened: createMemo(() => store.sidebar.opened),
open() {
diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx
index cef6c5555..4b7ad7cc2 100644
--- a/packages/desktop/src/context/local.tsx
+++ b/packages/desktop/src/context/local.tsx
@@ -5,6 +5,7 @@ import type { FileContent, FileNode, Model, Provider, File as FileStatus } from
import { createSimpleContext } from "./helper"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
+import { base64Encode } from "@/utils"
export type LocalFile = FileNode &
Partial<{
@@ -457,6 +458,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})()
const result = {
+ slug: createMemo(() => base64Encode(sdk.directory)),
model,
agent,
file,
diff --git a/packages/desktop/src/context/sdk.tsx b/packages/desktop/src/context/sdk.tsx
index b7b753dbc..40165b1ba 100644
--- a/packages/desktop/src/context/sdk.tsx
+++ b/packages/desktop/src/context/sdk.tsx
@@ -2,34 +2,31 @@ import { createOpencodeClient, type Event } from "@opencode-ai/sdk/client"
import { createSimpleContext } from "./helper"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"
+import { useGlobalSDK } from "./global-sdk"
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
name: "SDK",
- init: (props: { url: string; directory?: string }) => {
+ init: (props: { directory: string }) => {
+ const globalSDK = useGlobalSDK()
const abort = new AbortController()
- const sdk = createOpencodeClient(
- {
- baseUrl: props.url,
- signal: abort.signal,
- },
- { directory: props.directory },
- )
+ const sdk = createOpencodeClient({
+ baseUrl: globalSDK.url,
+ signal: abort.signal,
+ directory: props.directory,
+ })
const emitter = createGlobalEmitter<{
[key in Event["type"]]: Extract<Event, { type: key }>
}>()
- sdk.event.subscribe().then(async (events) => {
- for await (const event of events.stream) {
- console.log("event", event.type)
- emitter.emit(event.type, event)
- }
+ globalSDK.event.on(props.directory, async (event) => {
+ emitter.emit(event.type, event)
})
onCleanup(() => {
abort.abort()
})
- return { url: props.url, directory: props.directory, client: sdk, event: emitter }
+ return { directory: props.directory, client: sdk, event: emitter }
},
})
diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx
index a468f4673..36319b0c5 100644
--- a/packages/desktop/src/context/session.tsx
+++ b/packages/desktop/src/context/session.tsx
@@ -6,11 +6,17 @@ import { makePersisted } from "@solid-primitives/storage"
import { TextSelection } from "./local"
import { pipe, sumBy } from "remeda"
import { AssistantMessage } from "@opencode-ai/sdk"
+import { useParams } from "@solidjs/router"
+import { base64Encode } from "@/utils"
export const { use: useSession, provider: SessionProvider } = createSimpleContext({
name: "Session",
- init: (props: { sessionId?: string }) => {
+ init: () => {
+ const params = useParams()
const sync = useSync()
+ const name = createMemo(
+ () => `${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}`,
+ )
const [store, setStore] = makePersisted(
createStore<{
@@ -29,17 +35,17 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
cursor: undefined,
}),
{
- name: props.sessionId ?? "new-session",
+ name: name(),
},
)
createEffect(() => {
- if (!props.sessionId) return
- sync.session.sync(props.sessionId)
+ if (!params.id) return
+ sync.session.sync(params.id)
})
- const info = createMemo(() => (props.sessionId ? sync.session.get(props.sessionId) : undefined))
- const messages = createMemo(() => (props.sessionId ? (sync.data.message[props.sessionId] ?? []) : []))
+ const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
+ const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
const userMessages = createMemo(() =>
messages()
.filter((m) => m.role === "user")
@@ -53,10 +59,10 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
return userMessages()?.find((m) => m.id === store.messageId)
})
const working = createMemo(() => {
- if (!props.sessionId) return false
+ if (!params.id) return false
const last = lastUserMessage()
if (!last) return false
- const assistantMessages = sync.data.message[props.sessionId]?.filter(
+ const assistantMessages = sync.data.message[params.id]?.filter(
(m) => m.role === "assistant" && m.parentID == last?.id,
) as AssistantMessage[]
const error = assistantMessages?.find((m) => m?.error)?.error
@@ -80,7 +86,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
const model = createMemo(() =>
last() ? sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined,
)
- const diffs = createMemo(() => (props.sessionId ? (sync.data.session_diff[props.sessionId] ?? []) : []))
+ const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
const tokens = createMemo(() => {
if (!last()) return
@@ -96,7 +102,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
})
return {
- id: props.sessionId,
+ id: params.id,
info,
working,
diffs,
diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx
index 11b2c36b8..14c0309c4 100644
--- a/packages/desktop/src/context/sync.tsx
+++ b/packages/desktop/src/context/sync.tsx
@@ -1,135 +1,17 @@
-import type {
- Message,
- Agent,
- Provider,
- Session,
- Part,
- Config,
- Path,
- File,
- FileNode,
- Project,
- FileDiff,
- Todo,
-} from "@opencode-ai/sdk"
-import { createStore, produce, reconcile } from "solid-js/store"
+import type { Part } from "@opencode-ai/sdk"
+import { produce } from "solid-js/store"
import { createMemo } from "solid-js"
import { Binary } from "@/utils/binary"
import { createSimpleContext } from "./helper"
+import { useGlobalSync } from "./global-sync"
import { useSDK } from "./sdk"
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync",
init: () => {
- const [store, setStore] = createStore<{
- ready: boolean
- provider: Provider[]
- agent: Agent[]
- project: Project
- config: Config
- path: Path
- session: Session[]
- session_diff: {
- [sessionID: string]: FileDiff[]
- }
- todo: {
- [sessionID: string]: Todo[]
- }
- limit: number
- message: {
- [sessionID: string]: Message[]
- }
- part: {
- [messageID: string]: Part[]
- }
- node: FileNode[]
- changes: File[]
- }>({
- project: { id: "", worktree: "", time: { created: 0, initialized: 0 } },
- config: {},
- path: { state: "", config: "", worktree: "", directory: "" },
- ready: false,
- agent: [],
- provider: [],
- session: [],
- session_diff: {},
- todo: {},
- limit: 10,
- message: {},
- part: {},
- node: [],
- changes: [],
- })
-
+ const globalSync = useGlobalSync()
const sdk = useSDK()
- sdk.event.listen((e) => {
- // fetch the child store
- // make a set store function that always rights to the child store
- const event = e.details
- switch (event.type) {
- case "session.updated": {
- const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
- if (result.found) {
- setStore("session", result.index, reconcile(event.properties.info))
- break
- }
- setStore(
- "session",
- produce((draft) => {
- draft.splice(result.index, 0, event.properties.info)
- }),
- )
- break
- }
- case "session.diff":
- setStore("session_diff", event.properties.sessionID, event.properties.diff)
- break
- case "todo.updated":
- setStore("todo", event.properties.sessionID, event.properties.todos)
- break
- case "message.updated": {
- const messages = store.message[event.properties.info.sessionID]
- if (!messages) {
- setStore("message", event.properties.info.sessionID, [event.properties.info])
- break
- }
- const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
- if (result.found) {
- setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
- break
- }
- setStore(
- "message",
- event.properties.info.sessionID,
- produce((draft) => {
- draft.splice(result.index, 0, event.properties.info)
- }),
- )
- break
- }
- case "message.part.updated": {
- const part = sanitizePart(event.properties.part)
- const parts = store.part[part.messageID]
- if (!parts) {
- setStore("part", part.messageID, [part])
- break
- }
- const result = Binary.search(parts, part.id, (p) => p.id)
- if (result.found) {
- setStore("part", part.messageID, result.index, reconcile(part))
- break
- }
- setStore(
- "part",
- part.messageID,
- produce((draft) => {
- draft.splice(result.index, 0, part)
- }),
- )
- break
- }
- }
- })
+ const [store, setStore] = globalSync.child(sdk.directory)
const load = {
project: () => sdk.client.project.current().then((x) => setStore("project", x.data!)),