From 1bc1e56da379fdd9040dc40caac1a57ffe8d1197 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:52:39 -0600 Subject: [PATCH] wip(desktop): progress --- packages/desktop/src/context/layout.tsx | 10 + packages/desktop/src/context/local.tsx | 4 +- packages/desktop/src/context/session.tsx | 13 +- packages/desktop/src/pages/layout.tsx | 354 ++++++++++++++--------- packages/desktop/src/pages/session.tsx | 2 +- packages/util/src/path.ts | 8 +- 6 files changed, 240 insertions(+), 151 deletions(-) diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx index 5ef41c1f4..58d947af4 100644 --- a/packages/desktop/src/context/layout.tsx +++ b/packages/desktop/src/context/layout.tsx @@ -66,6 +66,16 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( collapse(directory: string) { setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: false } : x))) }, + move(directory: string, toIndex: number) { + setStore("projects", (projects) => { + const fromIndex = projects.findIndex((x) => x.directory === directory) + if (fromIndex === -1 || fromIndex === toIndex) return projects + const result = [...projects] + const [item] = result.splice(fromIndex, 1) + result.splice(toIndex, 0, item) + return result + }) + }, }, sidebar: { opened: createMemo(() => store.sidebar.opened), diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx index 0d8303b45..8223a36b9 100644 --- a/packages/desktop/src/context/local.tsx +++ b/packages/desktop/src/context/local.tsx @@ -257,7 +257,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const load = async (path: string) => { const relativePath = relative(path) - sdk.client.file.read({ path: relativePath }).then((x) => { + await sdk.client.file.read({ path: relativePath }).then((x) => { setStore( "node", relativePath, @@ -335,7 +335,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ return { node: async (path: string) => { - if (!store.node[path] || store.node[path].loaded === false) { + if (!store.node[path] || !store.node[path].loaded) { await init(path) } return store.node[path] diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx index 5335422f7..2a0391d6b 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, onMount } from "solid-js" +import { batch, createEffect, createMemo } from "solid-js" import { useSync } from "./sync" import { makePersisted } from "@solid-primitives/storage" -import { TextSelection, useLocal } from "./local" +import { TextSelection } from "./local" import { pipe, sumBy } from "remeda" import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2" import { useParams } from "@solidjs/router" @@ -25,7 +25,6 @@ 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`, ) @@ -56,14 +55,6 @@ 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/layout.tsx b/packages/desktop/src/pages/layout.tsx index 6831fdf31..61fa6c766 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, For, Match, ParentProps, Show, Switch } from "solid-js" +import { createEffect, createMemo, For, Match, ParentProps, Show, Switch, type JSX } from "solid-js" import { DateTime } from "luxon" import { A, useNavigate, useParams } from "@solidjs/router" import { useLayout } from "@/context/layout" @@ -19,10 +19,21 @@ 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" +import { + DragDropProvider, + DragDropSensors, + DragOverlay, + SortableProvider, + closestCenter, + createSortable, + useDragDropContext, +} from "@thisbeyond/solid-dnd" +import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd" export default function Layout(props: ParentProps) { const [store, setStore] = createStore({ lastSession: {} as { [directory: string]: string }, + activeDraggable: undefined as string | undefined, }) const params = useParams() @@ -52,6 +63,7 @@ export default function Layout(props: ParentProps) { function closeProject(directory: string) { layout.projects.close(directory) + // TODO: more intelligent navigation navigate("/") } @@ -76,6 +88,192 @@ export default function Layout(props: ParentProps) { setStore("lastSession", directory, params.id) }) + function getDraggableId(event: unknown): string | undefined { + if (typeof event !== "object" || event === null) return undefined + if (!("draggable" in event)) return undefined + const draggable = (event as { draggable?: { id?: unknown } }).draggable + if (!draggable) return undefined + return typeof draggable.id === "string" ? draggable.id : undefined + } + + function handleDragStart(event: unknown) { + const id = getDraggableId(event) + if (!id) return + setStore("activeDraggable", id) + } + + function handleDragOver(event: DragEvent) { + const { draggable, droppable } = event + if (draggable && droppable) { + const projects = layout.projects.list() + const fromIndex = projects.findIndex((p) => p.directory === draggable.id.toString()) + const toIndex = projects.findIndex((p) => p.directory === droppable.id.toString()) + if (fromIndex !== toIndex && toIndex !== -1) { + layout.projects.move(draggable.id.toString(), toIndex) + } + } + } + + function handleDragEnd() { + setStore("activeDraggable", undefined) + } + + const ConstrainDragXAxis = (): JSX.Element => { + const context = useDragDropContext() + if (!context) return <> + const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context + const transformer: Transformer = { + id: "constrain-x-axis", + order: 100, + callback: (transform) => ({ ...transform, x: 0 }), + } + onDragStart((event) => { + const id = getDraggableId(event) + if (!id) return + addTransformer("draggables", id, transformer) + }) + onDragEnd((event) => { + const id = getDraggableId(event) + if (!id) return + removeTransformer("draggables", id, transformer.id) + }) + return <> + } + + const SortableProject = (props: { project: { directory: string; expanded: boolean } }): JSX.Element => { + const sortable = createSortable(props.project.directory) + const [projectStore] = globalSync.child(props.project.directory) + const slug = createMemo(() => base64Encode(props.project.directory)) + const name = createMemo(() => getFilename(props.project.directory)) + return ( + // @ts-ignore +
+ + + + + + + + + + + + + + + +
+ ) + } + + const ProjectDragOverlay = (): JSX.Element => { + const activeName = createMemo(() => { + if (!store.activeDraggable) return undefined + return getFilename(store.activeDraggable) + }) + return ( + + {(name) => ( +
+ + {name()} +
+ )} +
+ ) + } + return (
@@ -96,8 +294,9 @@ export default function Layout(props: ParentProps) {
@@ -214,136 +413,23 @@ export default function Layout(props: ParentProps) { -
- - {(project) => { - const [store] = globalSync.child(project.directory) - const slug = createMemo(() => base64Encode(project.directory)) - const name = createMemo(() => getFilename(project.directory)) - return ( - - - - - - - {/* */} - {/* */} - {/* */} - - - - - - - - - - ) - }} - -
+ + + +
+ p.directory)}> + {(project) => } + +
+ + + +
diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 90f339798..68a2418f4 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -574,7 +574,7 @@ export default function Page() { onOpenChange={(open) => setStore("fileSelectOpen", open)} onSelect={(x) => { if (x) { - return local.file.open(x).then(() => session.layout.openTab("file://" + x)) + return session.layout.openTab("file://" + x) } return undefined }} diff --git a/packages/util/src/path.ts b/packages/util/src/path.ts index fbb84878d..f7c46d4ef 100644 --- a/packages/util/src/path.ts +++ b/packages/util/src/path.ts @@ -1,16 +1,18 @@ -export function getFilename(path: string) { +export function getFilename(path: string | undefined) { if (!path) return "" const trimmed = path.replace(/[\/]+$/, "") const parts = trimmed.split("/") return parts[parts.length - 1] ?? "" } -export function getDirectory(path: string) { +export function getDirectory(path: string | undefined) { + if (!path) return "" const parts = path.split("/") return parts.slice(0, parts.length - 1).join("/") + "/" } -export function getFileExtension(path: string) { +export function getFileExtension(path: string | undefined) { + if (!path) return "" const parts = path.split(".") return parts[parts.length - 1] }