diff options
| author | Adam <[email protected]> | 2025-12-09 11:08:55 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-09 11:09:00 -0600 |
| commit | 5442adb5174cee717b9a301d0cce8be8f3338388 (patch) | |
| tree | e6b74870c33ad5501fe202595af3d510930c0084 | |
| parent | 6b2ac20abc19d3678b251cc730b3faf56e916bb5 (diff) | |
| download | opencode-5442adb5174cee717b9a301d0cce8be8f3338388.tar.gz opencode-5442adb5174cee717b9a301d0cce8be8f3338388.zip | |
wip(desktop): progress
| -rw-r--r-- | packages/desktop/src/context/global-sync.tsx | 2 | ||||
| -rw-r--r-- | packages/desktop/src/context/layout.tsx | 34 | ||||
| -rw-r--r-- | packages/desktop/src/context/local.tsx | 2 | ||||
| -rw-r--r-- | packages/desktop/src/context/session.tsx | 13 | ||||
| -rw-r--r-- | packages/desktop/src/pages/directory-layout.tsx | 33 | ||||
| -rw-r--r-- | packages/desktop/src/pages/home.tsx | 34 | ||||
| -rw-r--r-- | packages/desktop/src/pages/layout.tsx | 88 | ||||
| -rw-r--r-- | packages/desktop/src/pages/session.tsx | 5 | ||||
| -rw-r--r-- | packages/ui/src/components/button.css | 3 | ||||
| -rw-r--r-- | packages/ui/src/components/session-turn.tsx | 4 |
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(() => |
