diff options
| author | Adam <[email protected]> | 2025-11-18 11:00:27 -0600 |
|---|---|---|
| committer | opencode <[email protected]> | 2025-11-18 17:07:34 +0000 |
| commit | fc5fc2c570e6ac5a6f9647080b80f7fec0c4b605 (patch) | |
| tree | a3971bfbe17308df9f2d3e1771c0ab544900c099 /packages/desktop/src/context | |
| parent | 4069999b782cc00d4e707f5eca32082bdfad45bc (diff) | |
| download | opencode-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.tsx | 33 | ||||
| -rw-r--r-- | packages/desktop/src/context/global-sync.tsx | 174 | ||||
| -rw-r--r-- | packages/desktop/src/context/layout.tsx | 25 | ||||
| -rw-r--r-- | packages/desktop/src/context/local.tsx | 2 | ||||
| -rw-r--r-- | packages/desktop/src/context/sdk.tsx | 25 | ||||
| -rw-r--r-- | packages/desktop/src/context/session.tsx | 26 | ||||
| -rw-r--r-- | packages/desktop/src/context/sync.tsx | 128 |
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!)), |
