Kaynağa Gözat

wip(desktop): progress

Adam 2 ay önce
ebeveyn
işleme
5442adb517

+ 1 - 1
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: [],

+ 26 - 8
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),

+ 1 - 1
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]

+ 11 - 2
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)

+ 16 - 17
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>
   )
 }

+ 28 - 6
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>

+ 58 - 30
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

+ 1 - 4
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
             }}

+ 3 - 0
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"] {

+ 0 - 4
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(() =>