mirror of
https://github.com/stablyai/orca.git
synced 2026-10-15 08:03:08 +00:00
feat(worktree): allow adopting external worktrees as children via CLI and UI drag-and-drop
- Mints an `instanceId` automatically when a lineage relationship is created with an external worktree. - Adds drag-and-drop support to the sidebar to nest a worktree directly by dropping it onto a parent card. - Implements descendant cycle validation and visual drop state indicators during drag. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -9924,6 +9924,56 @@ export class OrcaRuntimeService {
|
||||
return { base, behind: drift.behind, recentSubjects }
|
||||
}
|
||||
|
||||
private async ensureWorktreeInstanceId(worktree: Worktree): Promise<void> {
|
||||
if (worktree.instanceId) {
|
||||
return
|
||||
}
|
||||
|
||||
const instanceId = randomUUID()
|
||||
const now = Date.now()
|
||||
|
||||
const metaUpdates: Partial<WorktreeMeta> = {
|
||||
instanceId,
|
||||
createdAt: now,
|
||||
orcaCreatedAt: now
|
||||
}
|
||||
|
||||
try {
|
||||
const dotOrcaPath = join(worktree.path, '.orca')
|
||||
const jsonPath = join(dotOrcaPath, 'worktree.json')
|
||||
const jsonContent = JSON.stringify(metaUpdates, null, 2)
|
||||
|
||||
const repo = this.store?.getRepos().find((r) => r.id === worktree.repoId)
|
||||
if (repo?.connectionId) {
|
||||
const fsProvider = getSshFilesystemProvider(repo.connectionId)
|
||||
if (!fsProvider) {
|
||||
throw new Error('SSH filesystem provider not found')
|
||||
}
|
||||
try {
|
||||
await fsProvider.createDir(dotOrcaPath)
|
||||
} catch {
|
||||
// ignore if exists
|
||||
}
|
||||
await fsProvider.writeFile(jsonPath, jsonContent)
|
||||
} else {
|
||||
const { writeFile } = require('fs/promises')
|
||||
await mkdir(dotOrcaPath, { recursive: true })
|
||||
await writeFile(jsonPath, jsonContent, 'utf-8')
|
||||
}
|
||||
} catch (err) {
|
||||
throw new RuntimeLineageError(
|
||||
'LINEAGE_PARENT_CONTEXT_MISSING',
|
||||
'Workspace directory is not writable. Cannot manage this external worktree.',
|
||||
err
|
||||
)
|
||||
}
|
||||
|
||||
if (this.store) {
|
||||
this.store.setWorktreeMeta(worktree.id, metaUpdates)
|
||||
}
|
||||
worktree.instanceId = instanceId
|
||||
}
|
||||
|
||||
async updateManagedWorktreeMeta(
|
||||
worktreeSelector: string,
|
||||
updates: Partial<WorktreeMeta> & {
|
||||
@@ -9942,6 +9992,10 @@ export class OrcaRuntimeService {
|
||||
this.store.removeWorktreeLineage?.(worktree.id)
|
||||
} else if (lineage?.parentWorktree) {
|
||||
const parent = await this.resolveWorktreeSelector(lineage.parentWorktree)
|
||||
|
||||
await this.ensureWorktreeInstanceId(worktree)
|
||||
await this.ensureWorktreeInstanceId(parent)
|
||||
|
||||
this.validateLineageParent(worktree, parent)
|
||||
if (!worktree.instanceId || !parent.instanceId) {
|
||||
throw new RuntimeLineageError(
|
||||
|
||||
@@ -76,6 +76,7 @@ type WorktreeCardProps = {
|
||||
lineageCollapsed?: boolean
|
||||
lineageChildren?: React.ReactNode
|
||||
onLineageToggle?: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||
isLineageDropTarget?: boolean
|
||||
onActivate?: () => void
|
||||
onImmediateActivate?: (worktreeId: string) => void
|
||||
onSelectionGesture?: (event: React.MouseEvent<HTMLElement>, worktreeId: string) => boolean
|
||||
@@ -152,7 +153,8 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
lineageChildCount = 0,
|
||||
lineageCollapsed = false,
|
||||
lineageChildren,
|
||||
onLineageToggle
|
||||
onLineageToggle,
|
||||
isLineageDropTarget = false
|
||||
}: WorktreeCardProps) {
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
const openTaskPage = useAppStore((s) => s.openTaskPage)
|
||||
@@ -801,11 +803,13 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
'group relative flex items-start gap-0.5 pl-0 pr-1.5 pt-1.5 pb-2 cursor-pointer transition-[background-color,border-color,opacity,box-shadow] duration-200 outline-none select-none',
|
||||
flushSurface ? 'ml-1 w-[calc(100%-0.25rem)]' : 'ml-1',
|
||||
isMultiSelected ? 'rounded-sm' : 'rounded-lg',
|
||||
isActiveSurface
|
||||
? 'bg-black/[0.08] shadow-[0_1px_2px_rgba(0,0,0,0.04)] border border-black/[0.015] dark:bg-white/[0.10] dark:border-border/40 dark:shadow-[0_1px_2px_rgba(0,0,0,0.03)]'
|
||||
: isMultiSelected
|
||||
? 'border border-worktree-sidebar-ring/35 bg-worktree-sidebar-accent/70 ring-1 ring-worktree-sidebar-ring/30'
|
||||
: 'border border-transparent worktree-sidebar-card-hover',
|
||||
isLineageDropTarget
|
||||
? 'bg-accent/80 border-accent-foreground/20'
|
||||
: isActiveSurface
|
||||
? 'bg-black/[0.08] shadow-[0_1px_2px_rgba(0,0,0,0.04)] border border-black/[0.015] dark:bg-white/[0.10] dark:border-border/40 dark:shadow-[0_1px_2px_rgba(0,0,0,0.03)]'
|
||||
: isMultiSelected
|
||||
? 'border border-worktree-sidebar-ring/35 bg-worktree-sidebar-accent/70 ring-1 ring-worktree-sidebar-ring/30'
|
||||
: 'border border-transparent worktree-sidebar-card-hover',
|
||||
isActiveSurface && isMultiSelected && 'ring-1 ring-worktree-sidebar-ring/35',
|
||||
revealHighlight && [
|
||||
'scroll-to-current-workspace-reveal-highlight',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable max-lines */
|
||||
import React, { useMemo, useCallback, useRef, useState, useEffect, useLayoutEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
measureElement as measureVirtualElementSize,
|
||||
useVirtualizer
|
||||
@@ -521,11 +522,11 @@ function areWorktreeDragPreviewOffsetsEqual(
|
||||
|
||||
function updateLatestWorktreeStatusDropTarget(
|
||||
drag: WorktreePointerDrag,
|
||||
target: WorktreeSidebarStatusDropTarget,
|
||||
target: WorktreeSidebarStatusDropTarget & { lineageParentId: string | null },
|
||||
preview: WorktreeSidebarDropPreview | null
|
||||
): void {
|
||||
drag.latestStatusDropTarget =
|
||||
target.status || target.isPinDrop
|
||||
target.status || target.isPinDrop || target.lineageParentId
|
||||
? {
|
||||
target,
|
||||
preview,
|
||||
@@ -544,14 +545,19 @@ function getPointerDropStatusTarget(args: {
|
||||
container: HTMLElement
|
||||
x: number
|
||||
y: number
|
||||
}): WorktreeSidebarStatusDropTarget {
|
||||
}): WorktreeSidebarStatusDropTarget & { lineageParentId: string | null } {
|
||||
const target = document.elementFromPoint(args.x, args.y)
|
||||
if (!(target instanceof Element) || !args.container.contains(target)) {
|
||||
return { status: null, isPinDrop: false }
|
||||
return { status: null, isPinDrop: false, lineageParentId: null }
|
||||
}
|
||||
const pinTarget = target.closest<HTMLElement>('[data-workspace-pin-drop-target]')
|
||||
if (pinTarget && args.container.contains(pinTarget)) {
|
||||
return { status: null, isPinDrop: true }
|
||||
return { status: null, isPinDrop: true, lineageParentId: null }
|
||||
}
|
||||
const rowTarget = target.closest<HTMLElement>('[data-worktree-drag-id]')
|
||||
let lineageParentId: string | null = null
|
||||
if (rowTarget && args.container.contains(rowTarget)) {
|
||||
lineageParentId = rowTarget.dataset.worktreeDragId ?? null
|
||||
}
|
||||
const statusTarget = target.closest<HTMLElement>('[data-workspace-status-drop-target]')
|
||||
return {
|
||||
@@ -559,7 +565,8 @@ function getPointerDropStatusTarget(args: {
|
||||
statusTarget && args.container.contains(statusTarget)
|
||||
? ((statusTarget.dataset.workspaceStatus as WorkspaceStatus | undefined) ?? null)
|
||||
: null,
|
||||
isPinDrop: false
|
||||
isPinDrop: false,
|
||||
lineageParentId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -764,6 +771,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
const renamingWorktreeId = useAppStore((s) => s.renamingWorktreeId)
|
||||
const setRenamingWorktreeId = useAppStore((s) => s.setRenamingWorktreeId)
|
||||
const updateWorktreeMeta = useAppStore((s) => s.updateWorktreeMeta)
|
||||
const updateWorktreeLineage = useAppStore((s) => s.updateWorktreeLineage)
|
||||
const worktreeDragSessionRef = useRef<WorktreeSidebarDragSession | null>(null)
|
||||
const worktreePointerDragRef = useRef<WorktreePointerDrag | null>(null)
|
||||
const worktreePointerAutoscrollFrameIdRef = useRef<number | null>(null)
|
||||
@@ -1690,7 +1698,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
x: drag.currentX,
|
||||
y: drag.currentY
|
||||
})
|
||||
: { status: null, isPinDrop: false }
|
||||
: { status: null, isPinDrop: false, lineageParentId: null }
|
||||
if (
|
||||
shouldPreferSidebarStatusDropTarget({
|
||||
sourceGroupKey: drag.sourceGroupKey,
|
||||
@@ -2036,7 +2044,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
})
|
||||
: { status: null, isPinDrop: false }
|
||||
: { status: null, isPinDrop: false, lineageParentId: null }
|
||||
if (
|
||||
shouldPreferSidebarStatusDropTarget({
|
||||
sourceGroupKey: drag.sourceGroupKey,
|
||||
@@ -2315,13 +2323,47 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
clearWorktreeDrag()
|
||||
return
|
||||
}
|
||||
const target = getPointerDropStatusTarget({
|
||||
container: event.currentTarget,
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
})
|
||||
if (target.lineageParentId && !session.draggedIds.includes(target.lineageParentId)) {
|
||||
const parentId = target.lineageParentId
|
||||
const isDescendantDrop = session.draggedIds.some((draggedId) => {
|
||||
let current: string | undefined = parentId
|
||||
while (current) {
|
||||
if (current === draggedId) {
|
||||
return true
|
||||
}
|
||||
const lineage = worktreeLineageById[current]
|
||||
current = lineage?.parentWorktreeId
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (!isDescendantDrop) {
|
||||
event.preventDefault()
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
setWorktreeDragState((prev) =>
|
||||
prev.dropIndex === null &&
|
||||
prev.dropIndicatorY === null &&
|
||||
prev.previewOffsetsByWorktreeId.size === 0
|
||||
? prev
|
||||
: {
|
||||
...prev,
|
||||
dropIndex: null,
|
||||
dropIndicatorY: null,
|
||||
previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS,
|
||||
pointerY: event.clientY
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const drop = computeWorktreeDrop(event.clientY)
|
||||
if (!drop) {
|
||||
const target = getPointerDropStatusTarget({
|
||||
container: event.currentTarget,
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
})
|
||||
const statusDrop = target.status
|
||||
? computeWorktreeStatusDrop({
|
||||
pointerY: event.clientY,
|
||||
@@ -2377,7 +2419,8 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
computeWorktreeDrop,
|
||||
computeWorktreeStatusDrop,
|
||||
refreshWorktreeDragSession,
|
||||
startWorktreeNativeAutoscroll
|
||||
startWorktreeNativeAutoscroll,
|
||||
worktreeLineageById
|
||||
]
|
||||
)
|
||||
|
||||
@@ -2396,16 +2439,49 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
clearWorktreeDrag()
|
||||
return
|
||||
}
|
||||
|
||||
const container = scrollRef.current
|
||||
const target = container
|
||||
? getPointerDropStatusTarget({
|
||||
container,
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
})
|
||||
: { status: null, isPinDrop: false, lineageParentId: null }
|
||||
|
||||
if (target.lineageParentId && !session.draggedIds.includes(target.lineageParentId)) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const parentId = target.lineageParentId
|
||||
const isDescendantDrop = session.draggedIds.some((draggedId) => {
|
||||
let current: string | undefined = parentId
|
||||
while (current) {
|
||||
if (current === draggedId) {
|
||||
return true
|
||||
}
|
||||
const lineage = worktreeLineageById[current]
|
||||
current = lineage?.parentWorktreeId
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (isDescendantDrop) {
|
||||
clearWorktreeDrag()
|
||||
return
|
||||
}
|
||||
|
||||
void Promise.all(
|
||||
session.draggedIds.map((id) => updateWorktreeLineage(id, { parentWorktreeId: parentId }))
|
||||
).catch((err) => {
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to nest workspace')
|
||||
})
|
||||
clearWorktreeDrag()
|
||||
return
|
||||
}
|
||||
|
||||
const drop = computeWorktreeDrop(event.clientY)
|
||||
if (!drop) {
|
||||
const container = scrollRef.current
|
||||
const target = container
|
||||
? getPointerDropStatusTarget({
|
||||
container,
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
})
|
||||
: { status: null, isPinDrop: false }
|
||||
const statusDrop = target.status
|
||||
? computeWorktreeStatusDrop({
|
||||
pointerY: event.clientY,
|
||||
@@ -2448,8 +2524,10 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
onMoveWorktreesToStatusAtIndex,
|
||||
onReorderWorktrees,
|
||||
refreshWorktreeDragSession,
|
||||
updateWorktreeLineage,
|
||||
worktreeDragGroups,
|
||||
worktreeDragUnitGroups
|
||||
worktreeDragUnitGroups,
|
||||
worktreeLineageById
|
||||
]
|
||||
)
|
||||
|
||||
@@ -2635,7 +2713,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
})
|
||||
: { status: null, isPinDrop: false }
|
||||
: { status: null, isPinDrop: false, lineageParentId: null }
|
||||
const statusDrop = target.status
|
||||
? computeWorktreeStatusDrop({
|
||||
pointerY: event.clientY,
|
||||
@@ -3216,6 +3294,11 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
const worktreeDragGroupIndex = groupIndexByWorktreeId.get(itemRow.worktree.id)
|
||||
const revealHighlightTone =
|
||||
agentSendTargetWorktreeId === itemRow.worktree.id ? 'ai' : 'default'
|
||||
const isLineageDropTarget =
|
||||
worktreeDragState.draggingWorktreeId &&
|
||||
worktreePointerDragRef.current?.latestStatusDropTarget?.target.lineageParentId ===
|
||||
itemRow.worktree.id
|
||||
|
||||
return (
|
||||
<div
|
||||
key={itemRow.worktree.id}
|
||||
@@ -3266,6 +3349,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
nativeDragEnabled={false}
|
||||
contentIndent={nested ? 0 : paddingLeft}
|
||||
flushSurface={!nested}
|
||||
isLineageDropTarget={Boolean(isLineageDropTarget)}
|
||||
onImmediateActivate={onImmediateWorktreeActivate}
|
||||
onSelectionGesture={onSelectionGesture}
|
||||
onContextMenuSelect={onContextMenuSelect}
|
||||
@@ -3334,6 +3418,11 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
const lineageToggleGroupKey = child.lineageGroupKey
|
||||
const childCardIndent = Math.max(0, child.depth) * LINEAGE_INDENT
|
||||
const childContentIndent = Math.max(0, child.depth - 1) * LINEAGE_INDENT
|
||||
const isLineageDropTarget =
|
||||
worktreeDragState.draggingWorktreeId &&
|
||||
worktreePointerDragRef.current?.latestStatusDropTarget?.target.lineageParentId ===
|
||||
child.worktree.id
|
||||
|
||||
return (
|
||||
<div
|
||||
key={child.worktree.id}
|
||||
@@ -3361,9 +3450,11 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
||||
revealHighlightTone === 'ai' &&
|
||||
'scroll-to-current-workspace-reveal-highlight--ai'
|
||||
],
|
||||
isActive
|
||||
? 'border-black/[0.015] bg-black/[0.08] shadow-[0_1px_2px_rgba(0,0,0,0.04)] dark:border-border/40 dark:bg-white/[0.10] dark:shadow-[0_1px_2px_rgba(0,0,0,0.03)]'
|
||||
: 'worktree-sidebar-card-hover',
|
||||
isLineageDropTarget
|
||||
? 'bg-accent/80 border-accent-foreground/20'
|
||||
: isActive
|
||||
? 'border-black/[0.015] bg-black/[0.08] shadow-[0_1px_2px_rgba(0,0,0,0.04)] dark:border-border/40 dark:bg-white/[0.10] dark:shadow-[0_1px_2px_rgba(0,0,0,0.03)]'
|
||||
: 'worktree-sidebar-card-hover',
|
||||
isDeleting && 'cursor-not-allowed opacity-50 grayscale'
|
||||
)}
|
||||
data-scroll-reveal-highlight={
|
||||
|
||||
@@ -50,10 +50,10 @@ describe('resolveWorktreeSidebarStatusDropCommitTarget', () => {
|
||||
it('uses the current status target when pointerup hit-testing succeeds', () => {
|
||||
expect(
|
||||
resolveWorktreeSidebarStatusDropCommitTarget({
|
||||
currentTarget: { status: 'completed', isPinDrop: false },
|
||||
currentTarget: { status: 'completed', isPinDrop: false, lineageParentId: null },
|
||||
currentPreview: preview,
|
||||
latestTrackedTarget: {
|
||||
target: { status: 'in-progress', isPinDrop: false },
|
||||
target: { status: 'in-progress', isPinDrop: false, lineageParentId: null },
|
||||
preview: null,
|
||||
x: 100,
|
||||
y: 100
|
||||
@@ -62,7 +62,7 @@ describe('resolveWorktreeSidebarStatusDropCommitTarget', () => {
|
||||
y: 100
|
||||
})
|
||||
).toEqual({
|
||||
target: { status: 'completed', isPinDrop: false },
|
||||
target: { status: 'completed', isPinDrop: false, lineageParentId: null },
|
||||
preview
|
||||
})
|
||||
})
|
||||
@@ -70,10 +70,10 @@ describe('resolveWorktreeSidebarStatusDropCommitTarget', () => {
|
||||
it('reuses the latest status target when pointerup hit-testing blanks at the same point', () => {
|
||||
expect(
|
||||
resolveWorktreeSidebarStatusDropCommitTarget({
|
||||
currentTarget: { status: null, isPinDrop: false },
|
||||
currentTarget: { status: null, isPinDrop: false, lineageParentId: null },
|
||||
currentPreview: null,
|
||||
latestTrackedTarget: {
|
||||
target: { status: 'completed', isPinDrop: false },
|
||||
target: { status: 'completed', isPinDrop: false, lineageParentId: null },
|
||||
preview,
|
||||
x: 100,
|
||||
y: 100
|
||||
@@ -82,7 +82,7 @@ describe('resolveWorktreeSidebarStatusDropCommitTarget', () => {
|
||||
y: 101
|
||||
})
|
||||
).toEqual({
|
||||
target: { status: 'completed', isPinDrop: false },
|
||||
target: { status: 'completed', isPinDrop: false, lineageParentId: null },
|
||||
preview
|
||||
})
|
||||
})
|
||||
@@ -90,10 +90,10 @@ describe('resolveWorktreeSidebarStatusDropCommitTarget', () => {
|
||||
it('does not reuse a stale status target after the pointer has moved away', () => {
|
||||
expect(
|
||||
resolveWorktreeSidebarStatusDropCommitTarget({
|
||||
currentTarget: { status: null, isPinDrop: false },
|
||||
currentTarget: { status: null, isPinDrop: false, lineageParentId: null },
|
||||
currentPreview: null,
|
||||
latestTrackedTarget: {
|
||||
target: { status: 'completed', isPinDrop: false },
|
||||
target: { status: 'completed', isPinDrop: false, lineageParentId: null },
|
||||
preview,
|
||||
x: 100,
|
||||
y: 100
|
||||
@@ -102,7 +102,7 @@ describe('resolveWorktreeSidebarStatusDropCommitTarget', () => {
|
||||
y: 100
|
||||
})
|
||||
).toEqual({
|
||||
target: { status: null, isPinDrop: false },
|
||||
target: { status: null, isPinDrop: false, lineageParentId: null },
|
||||
preview: null
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ export type WorktreeSidebarDropPreview = {
|
||||
dropIndex: number
|
||||
dropIndicatorY: number
|
||||
previewOffsetsByWorktreeId: ReadonlyMap<string, number>
|
||||
lineageParentId?: string
|
||||
}
|
||||
|
||||
export type WorktreeSidebarStatusDropTarget = {
|
||||
@@ -16,7 +17,7 @@ export type WorktreeSidebarStatusDropTarget = {
|
||||
}
|
||||
|
||||
export type WorktreeSidebarTrackedStatusDropTarget = {
|
||||
target: WorktreeSidebarStatusDropTarget
|
||||
target: WorktreeSidebarStatusDropTarget & { lineageParentId: string | null }
|
||||
preview: WorktreeSidebarDropPreview | null
|
||||
x: number
|
||||
y: number
|
||||
@@ -24,18 +25,20 @@ export type WorktreeSidebarTrackedStatusDropTarget = {
|
||||
|
||||
const STATUS_DROP_TARGET_FALLBACK_TOLERANCE_PX = 6
|
||||
|
||||
function hasWorktreeSidebarStatusDropTarget(target: WorktreeSidebarStatusDropTarget): boolean {
|
||||
return target.isPinDrop || target.status !== null
|
||||
function hasWorktreeSidebarStatusDropTarget(
|
||||
target: WorktreeSidebarStatusDropTarget & { lineageParentId?: string | null }
|
||||
): boolean {
|
||||
return target.isPinDrop || target.status !== null || (target.lineageParentId ?? null) !== null
|
||||
}
|
||||
|
||||
export function resolveWorktreeSidebarStatusDropCommitTarget(args: {
|
||||
currentTarget: WorktreeSidebarStatusDropTarget
|
||||
currentTarget: WorktreeSidebarStatusDropTarget & { lineageParentId?: string | null }
|
||||
currentPreview: WorktreeSidebarDropPreview | null
|
||||
latestTrackedTarget: WorktreeSidebarTrackedStatusDropTarget | null
|
||||
x: number
|
||||
y: number
|
||||
}): {
|
||||
target: WorktreeSidebarStatusDropTarget
|
||||
target: WorktreeSidebarStatusDropTarget & { lineageParentId?: string | null }
|
||||
preview: WorktreeSidebarDropPreview | null
|
||||
} {
|
||||
if (hasWorktreeSidebarStatusDropTarget(args.currentTarget)) {
|
||||
|
||||
Reference in New Issue
Block a user