Skip to content

Commit 90a5194

Browse files
committed
fix: correct sidebar drag index translation with coordinator children
1 parent 1f17546 commit 90a5194

3 files changed

Lines changed: 71 additions & 14 deletions

File tree

src/components/Sidebar.tsx

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
toggleNewTaskDialog,
88
setActiveTask,
99
toggleSidebar,
10-
reorderTask,
10+
reorderTaskVisually,
1111
getTaskDotStatus,
1212
getTaskAttentionState,
1313
getTaskViewportVisibility,
@@ -126,18 +126,29 @@ export function Sidebar() {
126126
ImportableWorktree[] | null
127127
>(null);
128128
const [dragFromIndex, setDragFromIndex] = createSignal<number | null>(null);
129+
const [dragFromTaskId, setDragFromTaskId] = createSignal<string | null>(null);
129130
const [dropTargetIndex, setDropTargetIndex] = createSignal<number | null>(null);
130131
const [resizing, setResizing] = createSignal(false);
131132
let taskListRef: HTMLDivElement | undefined;
132133

133134
const sidebarWidth = () => getPanelUserSize(SIDEBAR_SIZE_KEY) ?? SIDEBAR_DEFAULT_WIDTH;
134135

136+
// Maps each visible draggable task ID to its visual position (0-based, excluding coordinated children).
137+
// This keeps drag signals, drop indicators, and data-task-index in the same coordinate space.
135138
const taskIndexById = createMemo(() => {
136139
const map = new Map<string, number>();
137-
store.taskOrder.forEach((taskId, idx) => map.set(taskId, idx));
140+
let visIdx = 0;
141+
for (const taskId of store.taskOrder) {
142+
if (!isCoordinatedChild(taskId)) {
143+
map.set(taskId, visIdx++);
144+
}
145+
}
138146
return map;
139147
});
140148

149+
// Number of visible draggable items (used for the end-of-list drop indicator).
150+
const draggableTaskCount = createMemo(() => taskIndexById().size);
151+
141152
const groupedTasks = createMemo(() => computeGroupedTasks());
142153

143154
function handleResizeMouseDown(e: MouseEvent) {
@@ -170,12 +181,12 @@ export function Sidebar() {
170181
const handler = (e: MouseEvent) => {
171182
const target = (e.target as HTMLElement).closest<HTMLElement>('[data-task-index]');
172183
if (!target) return;
173-
const index = Number(target.dataset.taskIndex);
174-
const taskId = store.taskOrder[index];
184+
const visibleIndex = Number(target.dataset.taskIndex);
185+
// data-task-index is now the visible draggable index; look up the task ID from the visible order
186+
const draggableOrder = store.taskOrder.filter((id) => !isCoordinatedChild(id));
187+
const taskId = draggableOrder[visibleIndex];
175188
if (taskId === undefined || taskId === null) return;
176-
// Don't allow dragging coordinated children
177-
if (isCoordinatedChild(taskId)) return;
178-
handleTaskMouseDown(e, taskId, index);
189+
handleTaskMouseDown(e, taskId, visibleIndex);
179190
};
180191
el.addEventListener('mousedown', handler);
181192
onCleanup(() => el.removeEventListener('mousedown', handler));
@@ -262,7 +273,7 @@ export function Sidebar() {
262273
return items.length;
263274
}
264275

265-
function handleTaskMouseDown(e: MouseEvent, taskId: string, index: number) {
276+
function handleTaskMouseDown(e: MouseEvent, taskId: string, visibleIndex: number) {
266277
if (e.button !== 0) return;
267278
e.preventDefault();
268279
const startX = e.clientX;
@@ -276,11 +287,12 @@ export function Sidebar() {
276287

277288
if (!dragging) {
278289
dragging = true;
279-
setDragFromIndex(index);
290+
setDragFromIndex(visibleIndex);
291+
setDragFromTaskId(taskId);
280292
document.body.classList.add('dragging-task');
281293
}
282294

283-
setDropTargetIndex(computeDropIndex(ev.clientY, index));
295+
setDropTargetIndex(computeDropIndex(ev.clientY, visibleIndex));
284296
}
285297

286298
function onUp() {
@@ -291,12 +303,14 @@ export function Sidebar() {
291303
document.body.classList.remove('dragging-task');
292304
const from = dragFromIndex();
293305
const to = dropTargetIndex();
306+
const fromTaskId = dragFromTaskId();
294307
setDragFromIndex(null);
308+
setDragFromTaskId(null);
295309
setDropTargetIndex(null);
296310

297-
if (from !== null && to !== null && from !== to) {
311+
if (from !== null && to !== null && from !== to && fromTaskId !== null) {
298312
const adjustedTo = to > from ? to - 1 : to;
299-
reorderTask(from, adjustedTo);
313+
reorderTaskVisually(fromTaskId, adjustedTo);
300314
}
301315
} else {
302316
setActiveTask(taskId);
@@ -739,7 +753,7 @@ export function Sidebar() {
739753
</For>
740754
</Show>
741755

742-
<Show when={dropTargetIndex() === store.taskOrder.length}>
756+
<Show when={dropTargetIndex() === draggableTaskCount()}>
743757
<div class="drop-indicator" />
744758
</Show>
745759
</div>

src/store/store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export {
4141
clearPrefillPrompt,
4242
setPrefillPrompt,
4343
reorderTask,
44+
reorderTaskVisually,
4445
spawnShellForTask,
4546
runBookmarkInTask,
4647
closeShell,

src/store/tasks.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { parseGitHubUrl, taskNameFromGitHubUrl } from '../lib/github-url';
2727
import type { Agent, Task, GitIsolationMode } from './types';
2828
import type { DockerSource } from '../lib/docker';
2929
import { COORDINATOR_PREAMBLE } from './coordinator-preamble';
30-
import { getCoordinatorChildren } from './sidebar-order';
30+
import { getCoordinatorChildren, isCoordinatedChild } from './sidebar-order';
3131

3232
function initTaskInStore(
3333
taskId: string,
@@ -607,6 +607,48 @@ export function reorderTask(fromIndex: number, toIndex: number): void {
607607
);
608608
}
609609

610+
/**
611+
* Reorder a task using visible sidebar indices (excluding hidden coordinated children).
612+
* Keeps coordinator+children blocks contiguous in taskOrder.
613+
*
614+
* @param movedId - ID of the task being dragged
615+
* @param targetVisibleIdx - target position in the visible draggable order (after removal of movedId)
616+
*/
617+
export function reorderTaskVisually(movedId: string, targetVisibleIdx: number): void {
618+
// Visible draggable order: active tasks excluding coordinated children
619+
const draggableOrder = store.taskOrder.filter((id) => !isCoordinatedChild(id));
620+
621+
// After removing the moved item, find what task should come after it
622+
const remainingDraggable = draggableOrder.filter((id) => id !== movedId);
623+
const insertBeforeId = remainingDraggable[targetVisibleIdx] ?? null;
624+
625+
// Build the block to move: movedId + its active children in taskOrder sequence
626+
const { active: activeChildren } = getCoordinatorChildren(movedId);
627+
const childSet = new Set(activeChildren);
628+
const block = [movedId, ...store.taskOrder.filter((id) => childSet.has(id))];
629+
630+
// Remove the block from taskOrder
631+
const blockSet = new Set(block);
632+
const remaining = store.taskOrder.filter((id) => !blockSet.has(id));
633+
634+
// Find where to insert in the remaining raw order
635+
const rawInsertAt =
636+
insertBeforeId !== null ? remaining.indexOf(insertBeforeId) : remaining.length;
637+
const finalInsertAt = rawInsertAt === -1 ? remaining.length : rawInsertAt;
638+
639+
const newOrder = [
640+
...remaining.slice(0, finalInsertAt),
641+
...block,
642+
...remaining.slice(finalInsertAt),
643+
];
644+
645+
setStore(
646+
produce((s) => {
647+
s.taskOrder = newOrder;
648+
}),
649+
);
650+
}
651+
610652
export function spawnShellForTask(taskId: string, initialCommand?: string): string {
611653
const shellId = crypto.randomUUID();
612654
if (initialCommand) setPendingShellCommand(shellId, initialCommand);

0 commit comments

Comments
 (0)