diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 215696d7de62..03924ec973d6 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -17,6 +17,11 @@ import { } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" import { createStore, produce } from "solid-js/store" +import { DragDropProvider, PointerSensor } from "@dnd-kit/solid" +import { isSortable, useSortable } from "@dnd-kit/solid/sortable" +import { AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom" +import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers" +import { RestrictToElement } from "@dnd-kit/dom/modifiers" import { useQuery } from "@tanstack/solid-query" import { Button } from "@opencode-ai/ui/button" import { Logo } from "@opencode-ai/ui/logo" @@ -1037,7 +1042,7 @@ function HomeServerRow(props: { ) } -function HomeProjectList(props: { +type HomeProjectListProps = { server: ServerConnection.Any projects: LocalProject[] selected: HomeProjectSelection @@ -1048,29 +1053,82 @@ function HomeProjectList(props: { clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void unseenCount: (server: ServerConnection.Any, project: LocalProject) => number language: ReturnType -}) { +} + +function HomeProjectList(props: HomeProjectListProps) { + const global = useGlobal() + let listRef!: HTMLDivElement + const projects = () => global.ensureServerCtx(props.server).projects + return ( -
- - {(project) => ( - - )} - -
+ [ + ...defaults.filter((sensor) => sensor !== PointerSensor), + PointerSensor.configure({ + activationConstraints: (event) => + event.pointerType === "touch" + ? [new PointerActivationConstraints.Delay({ value: 250, tolerance: 5 })] + : [new PointerActivationConstraints.Distance({ value: 4 })], + preventActivation: (event) => event.target instanceof Element && !!event.target.closest("[data-action]"), + }), + ]} + modifiers={[RestrictToVerticalAxis, RestrictToElement.configure({ element: () => listRef })]} + plugins={(defaults) => [ + ...defaults.filter((plugin) => plugin !== AutoScroller && plugin !== Feedback), + AutoScroller.configure({ acceleration: 8, threshold: { x: 0, y: 0.05 } }), + Feedback.configure({ dropAnimation: null }), + ]} + onDragEnd={(event) => { + const source = event.operation.source + if (event.canceled || !isSortable(source)) return + if (source.initialIndex !== source.index) projects().move(source.id.toString(), source.index) + if (props.selected.server !== ServerConnection.key(props.server)) + props.selectProject(props.server, source.id.toString()) + }} + > +
+ {/* Keyed on worktree strings: the enriched project objects are + recreated on every store or sync update, so iterating them directly + remounts all rows — killing any in-flight drag activation (the + row's sortable unregisters on unmount) and discarding animations. + String keys keep row elements alive and move them on reorder. */} + project.worktree)}> + {(worktree, index) => } + +
+
+ ) +} + +function HomeProjectSlot( + props: HomeProjectListProps & { + worktree: string + index: () => number + }, +) { + const project = createMemo(() => props.projects.find((item) => item.worktree === props.worktree)) + + return ( + + {(item) => ( + + )} + ) } @@ -1150,6 +1208,8 @@ function HomeRecentlyClosedRow(props: { function HomeProjectRow(props: { project: LocalProject server: ServerConnection.Any + index: () => number + serverSelected: boolean selected: boolean unseenCount: number selectProject: (server: ServerConnection.Any, directory: string) => void @@ -1163,6 +1223,15 @@ function HomeProjectRow(props: { const platform = usePlatform() const serverUnreachable = () => global.servers.health[ServerConnection.key(props.server)]?.healthy === false const [state, setState] = createStore({ menuOpen: false }) + const sortable = useSortable({ + get id() { + return props.project.worktree + }, + get index() { + return props.index() + }, + }) + let pointerDownSelected: boolean | undefined const canRevealInFileManager = () => platform.platform === "desktop" && !!platform.openPath && ServerConnection.local(props.server) const fileManagerActionLabel = () => @@ -1179,15 +1248,49 @@ function HomeProjectRow(props: { ) } return ( -
+