summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-11 03:13:08 -0600
committerAdam <[email protected]>2025-12-11 06:48:59 -0600
commit1980113ee4305844803f866aef05d742f7cffd47 (patch)
treeda957fc920b24ab9b35cbcdc1b41d525824a833e
parent85c0311d38c47c41e89f6b9cd7d3a0cfc4348841 (diff)
downloadopencode-1980113ee4305844803f866aef05d742f7cffd47.tar.gz
opencode-1980113ee4305844803f866aef05d742f7cffd47.zip
wip(desktop): progress
-rw-r--r--packages/desktop/src/context/global-sync.tsx75
-rw-r--r--packages/desktop/src/context/layout.tsx32
-rw-r--r--packages/desktop/src/context/sync.tsx50
-rw-r--r--packages/desktop/src/pages/layout.tsx5
-rw-r--r--packages/ui/src/components/list.tsx11
5 files changed, 101 insertions, 72 deletions
diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx
index 770275a5c..2a24a845c 100644
--- a/packages/desktop/src/context/global-sync.tsx
+++ b/packages/desktop/src/context/global-sync.tsx
@@ -1,19 +1,20 @@
-import type {
- Message,
- Agent,
- Session,
- Part,
- Config,
- Path,
- File,
- FileNode,
- Project,
- FileDiff,
- Todo,
- SessionStatus,
- ProviderListResponse,
- ProviderAuthResponse,
-} from "@opencode-ai/sdk/v2"
+import {
+ type Message,
+ type Agent,
+ type Session,
+ type Part,
+ type Config,
+ type Path,
+ type File,
+ type FileNode,
+ type Project,
+ type FileDiff,
+ type Todo,
+ type SessionStatus,
+ type ProviderListResponse,
+ type ProviderAuthResponse,
+ createOpencodeClient,
+} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile } from "solid-js/store"
import { Binary } from "@opencode-ai/util/binary"
import { createSimpleContext } from "@opencode-ai/ui/context"
@@ -51,7 +52,7 @@ type State = {
export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({
name: "GlobalSync",
init: () => {
- const sdk = useGlobalSDK()
+ const globalSDK = useGlobalSDK()
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
project: Project[]
@@ -66,6 +67,33 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
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]) {
@@ -87,11 +115,12 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
changes: [],
})
children[directory] = createStore(globalStore.children[directory])
+ bootstrapInstance(directory)
}
return children[directory]
}
- sdk.event.listen((e) => {
+ globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
@@ -121,6 +150,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
const [store, setStore] = child(directory)
switch (event.type) {
+ case "server.instance.disposed": {
+ bootstrapInstance(directory)
+ break
+ }
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (result.found) {
@@ -191,7 +224,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
async function bootstrap() {
return Promise.all([
- sdk.client.project.list().then(async (x) => {
+ globalSDK.client.project.list().then(async (x) => {
setGlobalStore(
"project",
x
@@ -199,10 +232,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
.sort((a, b) => a.id.localeCompare(b.id)),
)
}),
- sdk.client.provider.list().then((x) => {
+ globalSDK.client.provider.list().then((x) => {
setGlobalStore("provider", x.data ?? {})
}),
- sdk.client.provider.auth().then((x) => {
+ globalSDK.client.provider.auth().then((x) => {
setGlobalStore("provider_auth", x.data ?? {})
}),
]).then(() => setGlobalStore("ready", true))
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index d00e101b8..d4a8875f7 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -1,4 +1,4 @@
-import { createStore } from "solid-js/store"
+import { createStore, produce } from "solid-js/store"
import { batch, createMemo, onMount } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { makePersisted } from "@solid-primitives/storage"
@@ -48,6 +48,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const [ephemeral, setEphemeral] = createStore({
connect: {
provider: undefined as undefined | string,
+ state: undefined as undefined | "pending" | "complete" | "error",
+ error: undefined as undefined | string,
},
dialog: {
open: undefined as undefined | Dialog,
@@ -176,21 +178,47 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
opened: createMemo(() => ephemeral.dialog?.open),
open(dialog: Dialog) {
setEphemeral("dialog", "open", dialog)
+ if (dialog !== "connect") {
+ setEphemeral("connect", {})
+ }
},
close(dialog: Dialog) {
if (ephemeral.dialog?.open === dialog) {
setEphemeral("dialog", "open", undefined)
+ if (dialog === "connect") {
+ setEphemeral("connect", {})
+ }
}
},
connect(provider: string) {
batch(() => {
setEphemeral("dialog", "open", "connect")
- setEphemeral("connect", "provider", provider)
+ setEphemeral("connect", { provider, state: "pending" })
})
},
},
connect: {
provider: createMemo(() => ephemeral.connect.provider),
+ state: createMemo(() => ephemeral.connect.state),
+ complete() {
+ setEphemeral(
+ produce((state) => {
+ state.dialog.open = "model"
+ state.connect.state = "complete"
+ }),
+ )
+ },
+ error(message: string) {
+ setEphemeral(
+ produce((state) => {
+ state.connect.state = "error"
+ state.connect.error = message
+ }),
+ )
+ },
+ clear() {
+ setEphemeral("connect", {})
+ },
},
}
},
diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx
index 9c3abd731..85758c5b6 100644
--- a/packages/desktop/src/context/sync.tsx
+++ b/packages/desktop/src/context/sync.tsx
@@ -1,5 +1,5 @@
import { produce } from "solid-js/store"
-import { createMemo, onMount } from "solid-js"
+import { createMemo } from "solid-js"
import { Binary } from "@opencode-ai/util/binary"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSync } from "./global-sync"
@@ -11,45 +11,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const globalSync = useGlobalSync()
const sdk = useSDK()
const [store, setStore] = globalSync.child(sdk.directory)
-
- const load = {
- project: () => sdk.client.project.current().then((x) => setStore("project", x.data!.id)),
- provider: () => sdk.client.provider.list().then((x) => setStore("provider", x.data!)),
- path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
- agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
- session: () =>
- sdk.client.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.client.session.status().then((x) => setStore("session_status", x.data!)),
- config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)),
- changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)),
- node: () => sdk.client.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
- }
-
- async function bootstrap() {
- return Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
- }
-
- onMount(() => {
- bootstrap()
- })
-
- sdk.event.listen((e) => {
- const event = e.details
- console.log(event)
- switch (event.type) {
- case "server.instance.disposed": {
- bootstrap()
- break
- }
- }
- })
-
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
return {
@@ -95,11 +56,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
},
fetch: async (count = 10) => {
setStore("limit", (x) => x + count)
- await load.session()
+ await sdk.client.session.list().then((x) => {
+ const sessions = (x.data ?? [])
+ .slice()
+ .sort((a, b) => a.id.localeCompare(b.id))
+ .slice(0, store.limit)
+ setStore("session", sessions)
+ })
},
more: createMemo(() => store.session.length >= store.limit),
},
- bootstrap,
absolute,
get directory() {
return store.path.directory
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index 0bc6e9e09..5f0b26f6b 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -440,7 +440,7 @@ export default function Layout(props: ParentProps) {
<Button
variant="ghost"
size="large"
- class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg"
+ class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
onClick={layout.sidebar.toggle}
>
<div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
@@ -615,7 +615,7 @@ export default function Layout(props: ParentProps) {
)}
</SelectDialog>
</Show>
- <Show when={layout.dialog?.opened() === "connect"}>
+ <Show when={layout.dialog.opened() === "connect"}>
{iife(() => {
const [store, setStore] = createStore({
method: undefined as undefined | ProviderAuthMethod,
@@ -753,6 +753,7 @@ export default function Layout(props: ParentProps) {
},
})
await globalSDK.client.global.dispose()
+ layout.connect.complete()
}
return (
diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx
index a7f2db9ef..013767e60 100644
--- a/packages/ui/src/components/list.tsx
+++ b/packages/ui/src/components/list.tsx
@@ -65,8 +65,8 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
})
- const handleSelect = (item: T | undefined) => {
- props.onSelect?.(item)
+ const handleSelect = (item: T | undefined, index: number) => {
+ props.onSelect?.(item, index)
}
const handleKey = (e: KeyboardEvent) => {
@@ -75,11 +75,12 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
const all = flat()
const selected = all.find((x) => props.key(x) === active())
+ const index = selected ? all.indexOf(selected) : -1
props.onKeyEvent?.(e, selected)
if (e.key === "Enter") {
e.preventDefault()
- if (selected) handleSelect(selected)
+ if (selected) handleSelect(selected, index)
} else {
onKeyDown(e)
}
@@ -110,13 +111,13 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
</Show>
<div data-slot="list-items">
<For each={group.items}>
- {(item) => (
+ {(item, i) => (
<button
data-slot="list-item"
data-key={props.key(item)}
data-active={props.key(item) === active()}
data-selected={item === props.current}
- onClick={() => handleSelect(item)}
+ onClick={() => handleSelect(item, i())}
onMouseMove={() => {
setStore("mouseActive", true)
setActive(props.key(item))