summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-09 11:08:55 -0600
committerAdam <[email protected]>2025-12-09 11:09:00 -0600
commit5442adb5174cee717b9a301d0cce8be8f3338388 (patch)
treee6b74870c33ad5501fe202595af3d510930c0084
parent6b2ac20abc19d3678b251cc730b3faf56e916bb5 (diff)
downloadopencode-5442adb5174cee717b9a301d0cce8be8f3338388.tar.gz
opencode-5442adb5174cee717b9a301d0cce8be8f3338388.zip
wip(desktop): progress
-rw-r--r--packages/desktop/src/context/global-sync.tsx2
-rw-r--r--packages/desktop/src/context/layout.tsx34
-rw-r--r--packages/desktop/src/context/local.tsx2
-rw-r--r--packages/desktop/src/context/session.tsx13
-rw-r--r--packages/desktop/src/pages/directory-layout.tsx33
-rw-r--r--packages/desktop/src/pages/home.tsx34
-rw-r--r--packages/desktop/src/pages/layout.tsx88
-rw-r--r--packages/desktop/src/pages/session.tsx5
-rw-r--r--packages/ui/src/components/button.css3
-rw-r--r--packages/ui/src/components/session-turn.tsx4
10 files changed, 145 insertions, 73 deletions
diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx
index 188db1e69..2c5c05125 100644
--- a/packages/desktop/src/context/global-sync.tsx
+++ b/packages/desktop/src/context/global-sync.tsx
@@ -74,7 +74,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
session_status: {},
session_diff: {},
todo: {},
- limit: 10,
+ limit: 5,
message: {},
part: {},
node: [],
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index 1b43cf511..5ef41c1f4 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -1,14 +1,18 @@
import { createStore } from "solid-js/store"
-import { createMemo } from "solid-js"
+import { createMemo, onMount } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { makePersisted } from "@solid-primitives/storage"
+import { useGlobalSync } from "./global-sync"
+import { useGlobalSDK } from "./global-sdk"
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
init: () => {
+ const globalSdk = useGlobalSDK()
+ const globalSync = useGlobalSync()
const [store, setStore] = makePersisted(
createStore({
- projects: [] as { directory: string; expanded: boolean; lastSession?: string }[],
+ projects: [] as { directory: string; expanded: boolean }[],
sidebar: {
opened: false,
width: 280,
@@ -26,11 +30,31 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
)
+ 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(({ directory }) => {
+ return loadProjectSessions(directory)
+ }),
+ )
+ })
+
return {
projects: {
list: createMemo(() => store.projects),
open(directory: string) {
if (store.projects.find((x) => x.directory === directory)) return
+ loadProjectSessions(directory)
setStore("projects", (x) => [...x, { directory, expanded: true }])
},
close(directory: string) {
@@ -42,12 +66,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
collapse(directory: string) {
setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: false } : x)))
},
- lastSession(directory: string) {
- return store.projects.find((x) => x.directory === directory)?.lastSession
- },
- setLastSession(directory: string, session: string | undefined) {
- setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, lastSession: session } : x)))
- },
},
sidebar: {
opened: createMemo(() => store.sidebar.opened),
diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx
index a9f48ec67..0d8303b45 100644
--- a/packages/desktop/src/context/local.tsx
+++ b/packages/desktop/src/context/local.tsx
@@ -335,7 +335,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return {
node: async (path: string) => {
- if (!store.node[path]) {
+ if (!store.node[path] || store.node[path].loaded === false) {
await init(path)
}
return store.node[path]
diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx
index 2a0391d6b..5335422f7 100644
--- a/packages/desktop/src/context/session.tsx
+++ b/packages/desktop/src/context/session.tsx
@@ -1,9 +1,9 @@
import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
-import { batch, createEffect, createMemo } from "solid-js"
+import { batch, createEffect, createMemo, onMount } from "solid-js"
import { useSync } from "./sync"
import { makePersisted } from "@solid-primitives/storage"
-import { TextSelection } from "./local"
+import { TextSelection, useLocal } from "./local"
import { pipe, sumBy } from "remeda"
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
import { useParams } from "@solidjs/router"
@@ -25,6 +25,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
const sdk = useSDK()
const params = useParams()
const sync = useSync()
+ const local = useLocal()
const name = createMemo(
() => `${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}.v2`,
)
@@ -55,6 +56,14 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
},
)
+ onMount(() => {
+ store.tabs.all.forEach((tab) => {
+ if (tab.startsWith("file://")) {
+ local.file.open(tab.replace("file://", ""))
+ }
+ })
+ })
+
createEffect(() => {
if (!params.id) return
sync.session.sync(params.id)
diff --git a/packages/desktop/src/pages/directory-layout.tsx b/packages/desktop/src/pages/directory-layout.tsx
index 20467c066..c909a373d 100644
--- a/packages/desktop/src/pages/directory-layout.tsx
+++ b/packages/desktop/src/pages/directory-layout.tsx
@@ -1,32 +1,31 @@
-import { createMemo, type ParentProps } from "solid-js"
+import { createMemo, Show, type ParentProps } from "solid-js"
import { useParams } from "@solidjs/router"
import { SDKProvider } from "@/context/sdk"
import { SyncProvider, useSync } from "@/context/sync"
import { LocalProvider } from "@/context/local"
-import { useGlobalSync } from "@/context/global-sync"
import { base64Decode } from "@opencode-ai/util/encode"
import { DataProvider } from "@opencode-ai/ui/context"
import { iife } from "@opencode-ai/util/iife"
export default function Layout(props: ParentProps) {
const params = useParams()
- const sync = useGlobalSync()
const directory = createMemo(() => {
- const decoded = base64Decode(params.dir!)
- return sync.data.projects.find((x) => x.worktree === decoded)?.worktree ?? "/"
+ return base64Decode(params.dir!)
})
return (
- <SDKProvider directory={directory()}>
- <SyncProvider>
- {iife(() => {
- const sync = useSync()
- return (
- <DataProvider data={sync.data} directory={directory()}>
- <LocalProvider>{props.children}</LocalProvider>
- </DataProvider>
- )
- })}
- </SyncProvider>
- </SDKProvider>
+ <Show when={params.dir} keyed>
+ <SDKProvider directory={directory()}>
+ <SyncProvider>
+ {iife(() => {
+ const sync = useSync()
+ return (
+ <DataProvider data={sync.data} directory={directory()}>
+ <LocalProvider>{props.children}</LocalProvider>
+ </DataProvider>
+ )
+ })}
+ </SyncProvider>
+ </SDKProvider>
+ </Show>
)
}
diff --git a/packages/desktop/src/pages/home.tsx b/packages/desktop/src/pages/home.tsx
index 469e2b7f7..517926613 100644
--- a/packages/desktop/src/pages/home.tsx
+++ b/packages/desktop/src/pages/home.tsx
@@ -1,22 +1,38 @@
import { useGlobalSync } from "@/context/global-sync"
-import { For, Match, Switch } from "solid-js"
+import { For, Match, Show, Switch } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { Logo } from "@opencode-ai/ui/logo"
import { useLayout } from "@/context/layout"
import { useNavigate } from "@solidjs/router"
import { base64Encode } from "@opencode-ai/util/encode"
import { Icon } from "@opencode-ai/ui/icon"
+import { usePlatform } from "@/context/platform"
export default function Home() {
- const navigate = useNavigate()
const sync = useGlobalSync()
const layout = useLayout()
+ const platform = usePlatform()
+ const navigate = useNavigate()
function openProject(directory: string) {
layout.projects.open(directory)
navigate(`/${base64Encode(directory)}`)
}
+ async function chooseProject() {
+ const result = await platform.openDirectoryPickerDialog?.({
+ title: "Open project",
+ multiple: true,
+ })
+ if (Array.isArray(result)) {
+ for (const directory of result) {
+ openProject(directory)
+ }
+ } else if (result) {
+ openProject(result)
+ }
+ }
+
return (
<div class="mx-auto mt-55">
<Logo class="w-xl opacity-12" />
@@ -25,9 +41,11 @@ export default function Home() {
<div class="mt-20 w-full flex flex-col gap-4">
<div class="flex gap-2 items-center justify-between pl-3">
<div class="text-14-medium text-text-strong">Recent projects</div>
- <Button icon="folder-add-left" size="normal" class="pl-2 pr-3">
- Open project
- </Button>
+ <Show when={platform.openDirectoryPickerDialog}>
+ <Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
+ Open project
+ </Button>
+ </Show>
</div>
<ol class="flex flex-col gap-2">
<For each={sync.data.projects.slice(0, 5)}>
@@ -54,7 +72,11 @@ export default function Home() {
<div class="text-12-regular text-text-weak">Get started by opening a local project</div>
</div>
<div />
- <Button class="px-3">Open project</Button>
+ <Show when={platform.openDirectoryPickerDialog}>
+ <Button class="px-3" onClick={chooseProject}>
+ Open project
+ </Button>
+ </Show>
</div>
</Match>
</Switch>
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index 03eeedfb1..6831fdf31 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -17,19 +17,27 @@ import { getFilename } from "@opencode-ai/util/path"
import { Select } from "@opencode-ai/ui/select"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Session } from "@opencode-ai/sdk/v2/client"
+import { usePlatform } from "@/context/platform"
+import { createStore } from "solid-js/store"
export default function Layout(props: ParentProps) {
- const navigate = useNavigate()
+ const [store, setStore] = createStore({
+ lastSession: {} as { [directory: string]: string },
+ })
+
const params = useParams()
const globalSync = useGlobalSync()
const layout = useLayout()
+ const platform = usePlatform()
+ const navigate = useNavigate()
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
function navigateToProject(directory: string | undefined) {
if (!directory) return
- navigate(`/${base64Encode(directory)}`)
+ const lastSession = store.lastSession[directory]
+ navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
}
function navigateToSession(session: Session | undefined) {
@@ -37,19 +45,36 @@ export default function Layout(props: ParentProps) {
navigate(`/${params.dir}/session/${session?.id}`)
}
+ function openProject(directory: string, navigate = true) {
+ layout.projects.open(directory)
+ if (navigate) navigateToProject(directory)
+ }
+
function closeProject(directory: string) {
layout.projects.close(directory)
navigate("/")
}
- const handleOpenProject = async () => {
- // layout.projects.open(dir.)
+ async function chooseProject() {
+ const result = await platform.openDirectoryPickerDialog?.({
+ title: "Open project",
+ multiple: true,
+ })
+ if (Array.isArray(result)) {
+ for (const directory of result) {
+ openProject(directory, false)
+ }
+ navigateToProject(result[0])
+ } else if (result) {
+ openProject(result)
+ }
}
- // createEffect(() => {
- // if (!params.dir) return
- // layout.projects.setLastSession(base64Decode(params.dir), params.id)
- // })
+ createEffect(() => {
+ if (!params.dir || !params.id) return
+ const directory = base64Decode(params.dir)
+ setStore("lastSession", directory, params.id)
+ })
return (
<div class="relative h-screen flex flex-col">
@@ -89,7 +114,7 @@ export default function Layout(props: ParentProps) {
<Select
options={sessions()}
current={currentSession()}
- placeholder="Select session"
+ placeholder="New session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={navigateToSession}
@@ -97,9 +122,11 @@ export default function Layout(props: ParentProps) {
variant="ghost"
/>
</div>
- <Button as={A} href={`/${params.dir}/session`} icon="plus-small">
- New session
- </Button>
+ <Show when={currentSession()}>
+ <Button as={A} href={`/${params.dir}/session`} icon="plus-small">
+ New session
+ </Button>
+ </Show>
</div>
<div class="flex items-center gap-4">
<Tooltip
@@ -155,7 +182,7 @@ export default function Layout(props: ParentProps) {
onCollapse={layout.sidebar.close}
/>
</Show>
- <div class="grow flex flex-col items-start self-stretch gap-4 p-2 min-h-0">
+ <div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
<Tooltip class="shrink-0" placement="right" value="Toggle sidebar" inactive={layout.sidebar.opened()}>
<Button
variant="ghost"
@@ -187,7 +214,7 @@ export default function Layout(props: ParentProps) {
</Show>
</Button>
</Tooltip>
- <div class="size-full min-w-8 flex flex-col gap-2 grow min-h-0 overflow-y-auto no-scrollbar">
+ <div class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar">
<For each={layout.projects.list()}>
{(project) => {
const [store] = globalSync.child(project.directory)
@@ -196,7 +223,7 @@ export default function Layout(props: ParentProps) {
return (
<Switch>
<Match when={layout.sidebar.opened()}>
- <Collapsible variant="ghost" defaultOpen class="gap-2">
+ <Collapsible variant="ghost" defaultOpen class="gap-2 shrink-0">
<Button
as={"div"}
variant="ghost"
@@ -232,7 +259,7 @@ export default function Layout(props: ParentProps) {
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
- <Tooltip placement="bottom" value="New session">
+ <Tooltip placement="top" value="New session">
<IconButton as={A} href={`${slug()}/session`} icon="plus-small" variant="ghost" />
</Tooltip>
</div>
@@ -300,11 +327,11 @@ export default function Layout(props: ParentProps) {
<Match when={true}>
<Tooltip placement="right" value={project.directory}>
<Button
- as={A}
- href={`${slug()}/session`}
variant="ghost"
size="large"
class="flex items-center justify-center p-0 aspect-square border-none"
+ data-selected={project.directory === currentDirectory()}
+ onClick={() => navigateToProject(project.directory)}
>
<div class="size-6 shrink-0 inset-0">
<Avatar fallback={name()} background="var(--surface-info-base)" class="size-full" />
@@ -319,18 +346,19 @@ export default function Layout(props: ParentProps) {
</div>
</div>
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
- <Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
- <Button
- disabled
- class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
- variant="ghost"
- size="large"
- icon="folder-add-left"
- onClick={handleOpenProject}
- >
- <Show when={layout.sidebar.opened()}>Open project</Show>
- </Button>
- </Tooltip>
+ <Show when={platform.openDirectoryPickerDialog}>
+ <Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
+ <Button
+ class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
+ variant="ghost"
+ size="large"
+ icon="folder-add-left"
+ onClick={chooseProject}
+ >
+ <Show when={layout.sidebar.opened()}>Open project</Show>
+ </Button>
+ </Tooltip>
+ </Show>
<Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
<Button
disabled
diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx
index f9e717674..90f339798 100644
--- a/packages/desktop/src/pages/session.tsx
+++ b/packages/desktop/src/pages/session.tsx
@@ -220,7 +220,6 @@ export default function Page() {
onTabClose: (tab: string) => void
}): JSX.Element => {
const sortable = createSortable(props.tab)
-
const [file] = createResource(
() => props.tab,
async (tab) => {
@@ -230,7 +229,6 @@ export default function Page() {
return undefined
},
)
-
return (
// @ts-ignore
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
@@ -576,8 +574,7 @@ export default function Page() {
onOpenChange={(open) => setStore("fileSelectOpen", open)}
onSelect={(x) => {
if (x) {
- local.file.open(x)
- return session.layout.openTab("file://" + x)
+ return local.file.open(x).then(() => session.layout.openTab("file://" + x))
}
return undefined
}}
diff --git a/packages/ui/src/components/button.css b/packages/ui/src/components/button.css
index 8fa582cde..a557fccb0 100644
--- a/packages/ui/src/components/button.css
+++ b/packages/ui/src/components/button.css
@@ -54,6 +54,9 @@
opacity: 0.7;
cursor: not-allowed;
}
+ &[data-selected="true"]:not(:disabled) {
+ background-color: var(--surface-raised-base-hover);
+ }
}
&[data-variant="secondary"] {
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
index 47d4cd22d..5ca50a5cc 100644
--- a/packages/ui/src/components/session-turn.tsx
+++ b/packages/ui/src/components/session-turn.tsx
@@ -1,7 +1,6 @@
import { AssistantMessage } from "@opencode-ai/sdk"
import { useData } from "../context"
import { useDiffComponent } from "../context/diff"
-import { Binary } from "@opencode-ai/util/binary"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { checksum } from "@opencode-ai/util/encode"
import { createEffect, createMemo, createSignal, For, Match, onMount, ParentProps, Show, Switch } from "solid-js"
@@ -31,9 +30,6 @@ export function SessionTurn(
) {
const data = useData()
const diffComponent = useDiffComponent()
- const match = Binary.search(data.store.session, props.sessionID, (s) => s.id)
- if (!match.found) throw new Error(`Session ${props.sessionID} not found`)
-
const sanitizer = createMemo(() => (data.directory ? new RegExp(`${data.directory}/`, "g") : undefined))
const messages = createMemo(() => (props.sessionID ? (data.store.message[props.sessionID] ?? []) : []))
const userMessages = createMemo(() =>