@parhelia/core 0.3.12815 → 0.3.12816

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/agents-view/AgentsView.js +10 -11
  2. package/dist/agents-view/AgentsView.js.map +1 -1
  3. package/dist/components/ui/LanguageSelector.js +12 -1
  4. package/dist/components/ui/LanguageSelector.js.map +1 -1
  5. package/dist/config/config.js +11 -1
  6. package/dist/config/config.js.map +1 -1
  7. package/dist/editor/ContentTree.js +8 -3
  8. package/dist/editor/ContentTree.js.map +1 -1
  9. package/dist/editor/FieldListFieldWithFallbacks.js +1 -1
  10. package/dist/editor/FieldListFieldWithFallbacks.js.map +1 -1
  11. package/dist/editor/LinkEditorDialog.js +1 -5
  12. package/dist/editor/LinkEditorDialog.js.map +1 -1
  13. package/dist/editor/ai-image-editor/AiImageEditorDialog.js +57 -39
  14. package/dist/editor/ai-image-editor/AiImageEditorDialog.js.map +1 -1
  15. package/dist/editor/client/EditorShell.js +1 -0
  16. package/dist/editor/client/EditorShell.js.map +1 -1
  17. package/dist/editor/client/editContext.d.ts +6 -0
  18. package/dist/editor/client/editContext.js.map +1 -1
  19. package/dist/editor/client/itemsRepository.d.ts +2 -0
  20. package/dist/editor/client/itemsRepository.js +64 -0
  21. package/dist/editor/client/itemsRepository.js.map +1 -1
  22. package/dist/editor/client/operations.js +48 -5
  23. package/dist/editor/client/operations.js.map +1 -1
  24. package/dist/editor/commands/deleteVersionCommand.js +1 -0
  25. package/dist/editor/commands/deleteVersionCommand.js.map +1 -1
  26. package/dist/editor/field-types/richtext/components/ReactSlate.js +48 -3
  27. package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
  28. package/dist/editor/media-selector/TreeSelector.js +39 -20
  29. package/dist/editor/media-selector/TreeSelector.js.map +1 -1
  30. package/dist/editor/menubar/PageSelector.js +2 -8
  31. package/dist/editor/menubar/PageSelector.js.map +1 -1
  32. package/dist/editor/menubar/pageSelectorHistoryFilter.d.ts +9 -0
  33. package/dist/editor/menubar/pageSelectorHistoryFilter.js +40 -6
  34. package/dist/editor/menubar/pageSelectorHistoryFilter.js.map +1 -1
  35. package/dist/editor/page-viewer/PageViewerFrame.js +17 -6
  36. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  37. package/dist/editor/page-viewer/internalLinkNavigation.d.ts +2 -0
  38. package/dist/editor/page-viewer/internalLinkNavigation.js +10 -0
  39. package/dist/editor/page-viewer/internalLinkNavigation.js.map +1 -1
  40. package/dist/editor/reviews/ReviewersPanel.js +1 -1
  41. package/dist/editor/reviews/ReviewersPanel.js.map +1 -1
  42. package/dist/editor/services/aiService.d.ts +20 -0
  43. package/dist/editor/services/aiService.js +12 -0
  44. package/dist/editor/services/aiService.js.map +1 -1
  45. package/dist/editor/settings/UsageLeaderboard.d.ts +2 -0
  46. package/dist/editor/settings/UsageLeaderboard.js +108 -0
  47. package/dist/editor/settings/UsageLeaderboard.js.map +1 -0
  48. package/dist/editor/setup-wizard/steps/CompleteStep.js +2 -2
  49. package/dist/editor/setup-wizard/steps/CompleteStep.js.map +1 -1
  50. package/dist/editor/setup-wizard/steps/ModelSelectionStep.d.ts +1 -1
  51. package/dist/editor/setup-wizard/steps/ModelSelectionStep.js +6 -6
  52. package/dist/editor/setup-wizard/steps/ModelSelectionStep.js.map +1 -1
  53. package/dist/editor/setup-wizard/steps/ProviderStep.js +7 -7
  54. package/dist/editor/setup-wizard/steps/ProviderStep.js.map +1 -1
  55. package/dist/editor/sidebar/ComponentPalette.js +15 -11
  56. package/dist/editor/sidebar/ComponentPalette.js.map +1 -1
  57. package/dist/editor/template-wizard/TemplateBuilderDialog.js +6 -2
  58. package/dist/editor/template-wizard/TemplateBuilderDialog.js.map +1 -1
  59. package/dist/editor/template-wizard/TemplateStructureInlineEditor.js +6 -6
  60. package/dist/editor/template-wizard/TemplateStructureInlineEditor.js.map +1 -1
  61. package/dist/editor/ui/ItemSearch.d.ts +1 -1
  62. package/dist/editor/ui/ItemSearch.js +4 -0
  63. package/dist/editor/ui/ItemSearch.js.map +1 -1
  64. package/dist/editor/ui/PerfectTree.d.ts +3 -1
  65. package/dist/editor/ui/PerfectTree.js +8 -7
  66. package/dist/editor/ui/PerfectTree.js.map +1 -1
  67. package/dist/licensing/LicenseContext.js +14 -7
  68. package/dist/licensing/LicenseContext.js.map +1 -1
  69. package/dist/revision.d.ts +2 -2
  70. package/dist/revision.js +2 -2
  71. package/dist/task-board/types.d.ts +1 -0
  72. package/dist/task-board/views/DependencyGraphView.d.ts +42 -4
  73. package/dist/task-board/views/DependencyGraphView.js +467 -95
  74. package/dist/task-board/views/DependencyGraphView.js.map +1 -1
  75. package/dist/types.d.ts +2 -0
  76. package/package.json +1 -1
@@ -10,7 +10,8 @@ import { getTaskExecutionDisplayFromTask } from "../taskExecutionStatus";
10
10
  import { cn } from "../../lib/utils";
11
11
  import { saveGraphLayout } from "../services/taskService";
12
12
  import { Button } from "../../components/ui/button";
13
- import { Badge } from "../../components/ui/badge";
13
+ import { Badge, CountBadge } from "../../components/ui/badge";
14
+ import { Popover, PopoverContent, PopoverTrigger, } from "../../components/ui/popover";
14
15
  import { MultiSwitchButton } from "../../components/ui/multi-switch-button";
15
16
  import { Tooltip, TooltipContent, TooltipTrigger, } from "../../components/ui/tooltip";
16
17
  import { Circle, PlayCircle, Clock3, Eye, CheckCircle2, GitBranch, RotateCcw, Loader2, Plus, Maximize2, Minimize2, SquareSquare, AlertCircle, MoveHorizontal, MoveVertical, } from "lucide-react";
@@ -18,7 +19,13 @@ import { DeleteIcon } from "../../components/ui/delete-icon";
18
19
  import { TaskAssigneeBadge } from "../components/TaskAssigneeBadge";
19
20
  const NODE_WIDTH = 220;
20
21
  const NODE_HEIGHT = 96;
22
+ const GROUP_NODE_HEIGHT = 120;
21
23
  const SAVE_DEBOUNCE_MS = 500;
24
+ const TASK_GROUP_NODE_ID_PREFIX = "taskgroup:";
25
+ const TEMPLATE_GROUP_MIN_SIZE = 4;
26
+ function isTaskGroupNodeId(nodeId) {
27
+ return nodeId.startsWith(TASK_GROUP_NODE_ID_PREFIX);
28
+ }
22
29
  const EDGE_BLOCKED_BY = "var(--color-component-shared)";
23
30
  const EDGE_CHILD_TASK = "var(--color-component-suggestions)";
24
31
  const EDGE_RELATED = "var(--color-neutral-grey-50)";
@@ -62,6 +69,140 @@ const PRIORITY_HOVER_STYLES = {
62
69
  };
63
70
  const DEFAULT_HOVER_STYLE = "hover:border-neutral-grey-10/50";
64
71
  const HANDLE_CLASS = "bg-neutral-grey-10! border-border-default! w-2! h-2!";
72
+ function getTaskGroupLabel(members) {
73
+ const titles = members.map((member) => member.title.trim()).filter(Boolean);
74
+ if (titles.length === 0)
75
+ return `${members.length} tasks`;
76
+ if (titles.every((title) => title === titles[0]))
77
+ return titles[0];
78
+ let prefix = titles[0];
79
+ for (const title of titles.slice(1)) {
80
+ let prefixLength = 0;
81
+ const maxLength = Math.min(prefix.length, title.length);
82
+ while (prefixLength < maxLength &&
83
+ prefix[prefixLength] === title[prefixLength]) {
84
+ prefixLength += 1;
85
+ }
86
+ prefix = prefix.slice(0, prefixLength);
87
+ if (!prefix)
88
+ break;
89
+ }
90
+ const trimmedPrefix = prefix.replace(/[\s\p{P}\p{S}]+$/gu, "").trim();
91
+ return trimmedPrefix || `${members.length} tasks`;
92
+ }
93
+ function collectBlockedTaskIds(tasks, dependencies) {
94
+ const taskMap = new Map(tasks.map((task) => [task.taskId, task]));
95
+ const blocked = new Set();
96
+ for (const task of tasks) {
97
+ if ((task.executionBlockedByProjectIds?.length ?? 0) > 0) {
98
+ blocked.add(task.taskId);
99
+ }
100
+ }
101
+ for (const dependency of dependencies) {
102
+ if (dependency.dependencyType !== "BlockedBy")
103
+ continue;
104
+ const blocker = taskMap.get(dependency.dependsOnTaskId);
105
+ if (!blocker ||
106
+ normalizeTaskStatus(blocker.status, blocker.executionState) !== "Done") {
107
+ blocked.add(dependency.taskId);
108
+ }
109
+ }
110
+ return blocked;
111
+ }
112
+ function collectTaskGroups(tasks, selectedTaskId, blockedTaskIds) {
113
+ const groupedTasks = new Map();
114
+ const taskIdsWithChildren = new Set(tasks
115
+ .map((task) => task.parentTaskId?.trim())
116
+ .filter((taskId) => !!taskId));
117
+ for (const task of tasks) {
118
+ const parentTaskId = task.parentTaskId?.trim();
119
+ const batchId = task.batchId?.trim();
120
+ if (!parentTaskId || !batchId)
121
+ continue;
122
+ const groupId = `${TASK_GROUP_NODE_ID_PREFIX}${parentTaskId}:${batchId}`;
123
+ const members = groupedTasks.get(groupId) ?? [];
124
+ members.push(task);
125
+ groupedTasks.set(groupId, members);
126
+ }
127
+ const groups = new Map();
128
+ const collapsedTargetByTaskId = new Map();
129
+ for (const [groupId, tasksInGroup] of groupedTasks) {
130
+ if (tasksInGroup.length < TEMPLATE_GROUP_MIN_SIZE)
131
+ continue;
132
+ const members = tasksInGroup.map((task) => {
133
+ const status = normalizeTaskStatus(task.status, task.executionState);
134
+ const isBlocked = blockedTaskIds.has(task.taskId) ||
135
+ task.executionState === "WaitingForDependency";
136
+ const needsAttention = Boolean(getTaskExecutionDisplayFromTask({
137
+ taskStatus: status,
138
+ executionState: task.executionState,
139
+ isBlocked,
140
+ })?.needsAttention);
141
+ return {
142
+ taskId: task.taskId,
143
+ taskKey: task.taskKey ?? null,
144
+ title: task.title,
145
+ status,
146
+ executionState: task.executionState,
147
+ isBlocked,
148
+ isOutlier: task.taskId === selectedTaskId ||
149
+ (!isBlocked && needsAttention) ||
150
+ taskIdsWithChildren.has(task.taskId),
151
+ };
152
+ });
153
+ const collapsedMembers = members.filter((member) => !member.isOutlier);
154
+ if (collapsedMembers.length < 2)
155
+ continue;
156
+ const statusCounts = {
157
+ done: 0,
158
+ inProgress: 0,
159
+ needsAttention: 0,
160
+ todo: 0,
161
+ };
162
+ for (const member of members) {
163
+ if (member.status === "Done") {
164
+ statusCounts.done += 1;
165
+ }
166
+ else if (member.isBlocked) {
167
+ statusCounts.todo += 1;
168
+ }
169
+ else if (getTaskExecutionDisplayFromTask({
170
+ taskStatus: member.status,
171
+ executionState: member.executionState,
172
+ isBlocked: member.isBlocked,
173
+ })?.needsAttention) {
174
+ statusCounts.needsAttention += 1;
175
+ }
176
+ else if (member.status === "InProgress" || member.status === "Review") {
177
+ statusCounts.inProgress += 1;
178
+ }
179
+ else {
180
+ statusCounts.todo += 1;
181
+ }
182
+ }
183
+ const firstTask = tasksInGroup[0];
184
+ const status = members.every((member) => member.status === "Done")
185
+ ? "Done"
186
+ : members.some((member) => member.status === "InProgress" || member.status === "Review")
187
+ ? "InProgress"
188
+ : "Todo";
189
+ groups.set(groupId, {
190
+ groupId,
191
+ parentTaskId: firstTask.parentTaskId.trim(),
192
+ batchId: firstTask.batchId.trim(),
193
+ label: getTaskGroupLabel(members),
194
+ memberCount: members.length,
195
+ statusCounts,
196
+ members,
197
+ status,
198
+ isBlocked: members.some((member) => member.isBlocked),
199
+ });
200
+ for (const member of collapsedMembers) {
201
+ collapsedTargetByTaskId.set(member.taskId, groupId);
202
+ }
203
+ }
204
+ return { groups, collapsedTargetByTaskId };
205
+ }
65
206
  function TaskGraphNode({ data }) {
66
207
  const status = data.status;
67
208
  const statusStyle = STATUS_STYLES[status] || STATUS_STYLES.Todo;
@@ -114,8 +255,61 @@ function TaskGraphNode({ data }) {
114
255
  event.stopPropagation();
115
256
  }, children: _jsx(Plus, { className: "h-3 w-3", strokeWidth: 1 }) }) }), _jsx(TooltipContent, { side: subtaskTooltipSide, sideOffset: 8, children: "Add subtask" })] })), _jsx(Handle, { type: "source", position: Position.Right, id: "right", className: HANDLE_CLASS, "data-testid": `dependency-graph-source-right-${data.taskId}` }), _jsx(Handle, { type: "source", position: Position.Bottom, id: "bottom", className: HANDLE_CLASS, "data-testid": `dependency-graph-source-bottom-${data.taskId}` })] }));
116
257
  }
258
+ function TaskGroupGraphNode({ data }) {
259
+ const [isOpen, setIsOpen] = useState(false);
260
+ const breakdown = [
261
+ {
262
+ key: "done",
263
+ label: "Done",
264
+ count: data.statusCounts.done,
265
+ icon: STATUS_STYLES.Done.icon,
266
+ className: STATUS_STYLES.Done.text,
267
+ },
268
+ {
269
+ key: "inProgress",
270
+ label: "In progress",
271
+ count: data.statusCounts.inProgress,
272
+ icon: STATUS_STYLES.InProgress.icon,
273
+ className: STATUS_STYLES.InProgress.text,
274
+ },
275
+ {
276
+ key: "needsAttention",
277
+ label: "Needs attention",
278
+ count: data.statusCounts.needsAttention,
279
+ icon: AlertCircle,
280
+ className: "text-feedback-red",
281
+ },
282
+ {
283
+ key: "todo",
284
+ label: "To do",
285
+ count: data.statusCounts.todo,
286
+ icon: STATUS_STYLES.Todo.icon,
287
+ className: STATUS_STYLES.Todo.text,
288
+ },
289
+ ].filter((item) => item.count > 0);
290
+ return (_jsxs(Popover, { open: isOpen, onOpenChange: setIsOpen, children: [_jsxs("div", { className: "relative overflow-visible", style: { width: NODE_WIDTH }, "data-testid": `dependency-graph-group-node-${data.groupId}`, children: [_jsx("div", { className: "border-border-default bg-neutral-grey-5 absolute inset-0 translate-x-2 translate-y-2 rounded-lg border shadow-sm" }), _jsx("div", { className: "border-border-default absolute inset-0 translate-x-1 translate-y-1 rounded-lg border bg-white shadow-sm" }), _jsx(Handle, { type: "target", position: Position.Left, id: "left", className: HANDLE_CLASS, "data-testid": `dependency-graph-group-target-left-${data.groupId}` }), _jsx(Handle, { type: "target", position: Position.Top, id: "top", className: HANDLE_CLASS, "data-testid": `dependency-graph-group-target-top-${data.groupId}` }), _jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "border-border-default relative z-10 flex min-h-30 w-full cursor-pointer flex-col gap-2.5 rounded-lg border bg-white p-3 text-left shadow-sm transition-all hover:shadow-md", "aria-label": `Show ${data.memberCount} tasks in ${data.label}`, children: [_jsxs("div", { className: "flex w-full min-w-0 items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-muted-foreground mb-1 text-[10px] font-medium tracking-widest uppercase", children: "Batch" }), _jsx("div", { className: "line-clamp-2 text-sm leading-snug font-medium", children: data.label })] }), _jsx(CountBadge, { children: data.memberCount })] }), _jsxs("div", { className: "flex w-full flex-wrap items-center gap-x-2.5 gap-y-1", children: [breakdown.map((item) => {
291
+ const Icon = item.icon;
292
+ return (_jsxs("span", { className: cn("inline-flex items-center gap-1 text-[10px] font-medium tabular-nums", item.className), title: `${item.count} ${item.label.toLowerCase()}`, children: [_jsx(Icon, { className: "h-3 w-3" }), item.count] }, item.key));
293
+ }), data.isBlocked ? (_jsx(Badge, { variant: "outline", size: "sm", className: "bg-feedback-orange-light text-feedback-orange ml-auto border-transparent", children: "Blocked" })) : null] }), _jsxs("div", { className: "text-neutral-grey-50 text-[10px]", children: [data.memberCount, " tasks"] })] }) }), _jsx(Handle, { type: "source", position: Position.Right, id: "right", className: HANDLE_CLASS, "data-testid": `dependency-graph-group-source-right-${data.groupId}` }), _jsx(Handle, { type: "source", position: Position.Bottom, id: "bottom", className: HANDLE_CLASS, "data-testid": `dependency-graph-group-source-bottom-${data.groupId}` })] }), _jsx(PopoverContent, { className: "nodrag nopan max-h-64 w-72 overflow-y-auto p-1.5", onClick: (event) => event.stopPropagation(), children: _jsx("div", { className: "flex flex-col gap-0.5", children: data.members.map((member) => {
294
+ const executionDisplay = member.isBlocked
295
+ ? null
296
+ : getTaskExecutionDisplayFromTask({
297
+ taskStatus: member.status,
298
+ executionState: member.executionState,
299
+ isBlocked: member.isBlocked,
300
+ });
301
+ const statusStyle = STATUS_STYLES[member.status];
302
+ const StatusIcon = statusStyle.icon;
303
+ return (_jsxs("button", { type: "button", className: "hover:bg-neutral-grey-5 flex w-full items-center gap-2 rounded px-2 py-2 text-left transition-colors", "data-testid": `dependency-graph-group-member-${member.taskId}`, onClick: (event) => {
304
+ event.stopPropagation();
305
+ data.onSelect(member.taskId);
306
+ setIsOpen(false);
307
+ }, children: [_jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-xs font-medium", children: member.title }), _jsxs("span", { className: "text-neutral-grey-50 flex items-center gap-1 text-[10px]", children: [member.taskKey || member.taskId.slice(0, 8).toUpperCase(), member.isOutlier ? (_jsx("span", { className: "text-feedback-blue", children: "\u2022 shown in graph" })) : null] })] }), member.isBlocked ? (_jsx(Badge, { variant: "outline", size: "sm", className: "bg-feedback-orange-light text-feedback-orange border-transparent", children: "Blocked" })) : executionDisplay ? (_jsxs(Badge, { variant: executionDisplay.variant, size: "sm", className: cn("max-w-28 font-bold", executionDisplay.className), children: [executionDisplay.showSpinner ? (_jsx(Loader2, { className: "h-2.5 w-2.5 animate-spin" })) : null, executionDisplay.label] })) : (_jsxs(Badge, { variant: "outline", size: "sm", className: cn("border-transparent font-bold", statusStyle.bg, statusStyle.text), children: [_jsx(StatusIcon, {}), getTaskStatusLabel(member.status)] }))] }, member.taskId));
308
+ }) }) })] }));
309
+ }
117
310
  const nodeTypes = {
118
311
  taskNode: TaskGraphNode,
312
+ taskGroupNode: TaskGroupGraphNode,
119
313
  };
120
314
  function getHierarchyOrthogonalPath({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, }) {
121
315
  const sameColumn = Math.abs(sourceX - targetX) < 1;
@@ -173,7 +367,7 @@ function normalizeGraphNodeSize(size) {
173
367
  function getGraphNodeSize(nodeId, nodeSizeOverrides) {
174
368
  return (normalizeGraphNodeSize(nodeSizeOverrides?.get(nodeId)) ?? {
175
369
  width: NODE_WIDTH,
176
- height: NODE_HEIGHT,
370
+ height: isTaskGroupNodeId(nodeId) ? GROUP_NODE_HEIGHT : NODE_HEIGHT,
177
371
  });
178
372
  }
179
373
  function getMeasuredNodeSizeOverrides(nodes) {
@@ -473,13 +667,27 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
473
667
  const taskMap = new Map();
474
668
  for (const t of tasks)
475
669
  taskMap.set(t.taskId, t);
476
- const visibleTasks = tasks;
477
- const childRelationshipKeys = new Set(visibleTasks
478
- .filter((task) => !!task.parentTaskId)
479
- .map((task) => `${task.parentTaskId}->${task.taskId}`));
670
+ const { groups, collapsedTargetByTaskId } = collectTaskGroups(tasks, selectedTaskId, blockedTaskIds);
671
+ const mapTaskId = (taskId) => collapsedTargetByTaskId.get(taskId) ?? taskId;
672
+ const visibleTasks = tasks.filter((task) => !collapsedTargetByTaskId.has(task.taskId));
673
+ const childRelationshipKeys = new Set();
674
+ for (const task of visibleTasks) {
675
+ if (!task.parentTaskId)
676
+ continue;
677
+ const source = mapTaskId(task.parentTaskId);
678
+ if (source !== task.taskId) {
679
+ childRelationshipKeys.add(`${source}->${task.taskId}`);
680
+ }
681
+ }
682
+ for (const group of groups.values()) {
683
+ const source = mapTaskId(group.parentTaskId);
684
+ if (source !== group.groupId) {
685
+ childRelationshipKeys.add(`${source}->${group.groupId}`);
686
+ }
687
+ }
480
688
  const dependencyHandles = getDependencyHandles(orientation);
481
689
  const hierarchyHandles = getHierarchyHandles(orientation, autoLayoutStrategy);
482
- const nodes = visibleTasks.map((task) => ({
690
+ const taskNodes = visibleTasks.map((task) => ({
483
691
  id: task.taskId,
484
692
  type: "taskNode",
485
693
  position: { x: 0, y: 0 },
@@ -495,7 +703,8 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
495
703
  assigneeType: task.assigneeType ?? null,
496
704
  assigneeDisplayName: task.assigneeDisplayName ?? null,
497
705
  isSelected: task.taskId === selectedTaskId,
498
- isBlocked: blockedTaskIds.has(task.taskId),
706
+ isBlocked: blockedTaskIds.has(task.taskId) ||
707
+ task.executionState === "WaitingForDependency",
499
708
  orientation,
500
709
  dependencySourceHandle: dependencyHandles.source,
501
710
  hierarchySourceHandle: hierarchyHandles.source,
@@ -505,27 +714,33 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
505
714
  onRemoveTask,
506
715
  },
507
716
  }));
717
+ const groupNodes = Array.from(groups.values()).map((group) => ({
718
+ id: group.groupId,
719
+ type: "taskGroupNode",
720
+ position: { x: 0, y: 0 },
721
+ data: {
722
+ ...group,
723
+ orientation,
724
+ dependencySourceHandle: dependencyHandles.source,
725
+ hierarchySourceHandle: hierarchyHandles.source,
726
+ onSelect,
727
+ },
728
+ }));
729
+ const nodes = [...taskNodes, ...groupNodes];
508
730
  const depSourceHandle = dependencyHandles.source;
509
731
  const depTargetHandle = dependencyHandles.target;
510
732
  const hierSourceHandle = hierarchyHandles.source;
511
733
  const hierTargetHandle = hierarchyHandles.target;
512
734
  const dependencyEdgeType = autoLayoutStrategy === "hierarchy" ? "step" : "smoothstep";
513
735
  const hierarchyEdgeType = autoLayoutStrategy === "hierarchy" ? "hierarchyOrthogonal" : "smoothstep";
514
- const dependencyEdges = dependencies
515
- .filter((dep) => taskMap.has(dep.taskId) &&
516
- taskMap.has(dep.dependsOnTaskId) &&
517
- !childRelationshipKeys.has(`${dep.dependsOnTaskId}->${dep.taskId}`))
518
- .map((dep) => {
519
- const isBlockedBy = dep.dependencyType === "BlockedBy";
736
+ const buildDependencyEdge = (args) => {
737
+ const isBlockedBy = args.dependencyType === "BlockedBy";
738
+ const isSelected = args.isSelected === true;
520
739
  const strokeColor = isBlockedBy ? EDGE_BLOCKED_BY : EDGE_RELATED;
521
- const sourceTaskType = String(taskMap.get(dep.dependsOnTaskId)?.taskType ?? "").toLowerCase();
522
- const isDecisionEdge = isBlockedBy && sourceTaskType === "decisionpoint";
523
- const isSelected = dep.taskId === selectedTaskId &&
524
- dep.dependsOnTaskId === selectedDependencyId;
525
740
  return {
526
- id: dep.dependencyId,
527
- source: dep.dependsOnTaskId,
528
- target: dep.taskId,
741
+ id: args.id,
742
+ source: args.source,
743
+ target: args.target,
529
744
  sourceHandle: depSourceHandle,
530
745
  targetHandle: depTargetHandle,
531
746
  type: dependencyEdgeType,
@@ -533,7 +748,7 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
533
748
  style: {
534
749
  stroke: isSelected ? "var(--color-neutral-grey-100)" : strokeColor,
535
750
  strokeWidth: isSelected ? 3 : isBlockedBy ? 2 : 1.5,
536
- strokeDasharray: isDecisionEdge ? "6 4" : undefined,
751
+ strokeDasharray: args.isDecisionEdge ? "6 4" : undefined,
537
752
  },
538
753
  markerEnd: {
539
754
  type: MarkerType.ArrowClosed,
@@ -541,28 +756,88 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
541
756
  width: 16,
542
757
  height: 16,
543
758
  },
544
- label: isBlockedBy
545
- ? isDecisionEdge
546
- ? "decision"
547
- : undefined
548
- : "related",
759
+ label: args.count && args.count > 1
760
+ ? `×${args.count}`
761
+ : isBlockedBy
762
+ ? args.isDecisionEdge
763
+ ? "decision"
764
+ : undefined
765
+ : "related",
549
766
  labelStyle: {
550
767
  fontSize: 10,
551
- fill: isDecisionEdge ? EDGE_BLOCKED_BY : EDGE_RELATED,
768
+ fill: args.isDecisionEdge ? EDGE_BLOCKED_BY : EDGE_RELATED,
552
769
  },
553
770
  data: {
554
771
  relationship: "dependency",
555
- taskId: dep.taskId,
556
- dependsOnTaskId: dep.dependsOnTaskId,
772
+ dependencyType: args.dependencyType,
773
+ count: args.count,
774
+ taskId: args.taskId,
775
+ dependsOnTaskId: args.dependsOnTaskId,
557
776
  },
558
- selectable: true,
777
+ selectable: args.selectable,
559
778
  };
560
- });
779
+ };
780
+ const dependencyEdges = [];
781
+ const syntheticDependencies = new Map();
782
+ for (const dep of dependencies) {
783
+ if (!taskMap.has(dep.taskId) || !taskMap.has(dep.dependsOnTaskId))
784
+ continue;
785
+ const source = mapTaskId(dep.dependsOnTaskId);
786
+ const target = mapTaskId(dep.taskId);
787
+ if (source === target ||
788
+ childRelationshipKeys.has(`${source}->${target}`)) {
789
+ continue;
790
+ }
791
+ const isBlockedBy = dep.dependencyType === "BlockedBy";
792
+ const sourceTaskType = String(taskMap.get(dep.dependsOnTaskId)?.taskType ?? "").toLowerCase();
793
+ const isDecisionEdge = isBlockedBy && sourceTaskType === "decisionpoint";
794
+ const wasRemapped = source !== dep.dependsOnTaskId || target !== dep.taskId;
795
+ if (!wasRemapped) {
796
+ dependencyEdges.push(buildDependencyEdge({
797
+ id: dep.dependencyId,
798
+ source,
799
+ target,
800
+ dependencyType: dep.dependencyType,
801
+ isDecisionEdge,
802
+ isSelected: dep.taskId === selectedTaskId &&
803
+ dep.dependsOnTaskId === selectedDependencyId,
804
+ taskId: dep.taskId,
805
+ dependsOnTaskId: dep.dependsOnTaskId,
806
+ selectable: true,
807
+ }));
808
+ continue;
809
+ }
810
+ const key = `${source}->${target}`;
811
+ const existing = syntheticDependencies.get(key);
812
+ if (existing) {
813
+ existing.count += 1;
814
+ if (isBlockedBy)
815
+ existing.dependencyType = "BlockedBy";
816
+ if (isDecisionEdge)
817
+ existing.isDecisionEdge = true;
818
+ }
819
+ else {
820
+ syntheticDependencies.set(key, {
821
+ source,
822
+ target,
823
+ dependencyType: isBlockedBy ? "BlockedBy" : "RelatedTo",
824
+ isDecisionEdge,
825
+ count: 1,
826
+ });
827
+ }
828
+ }
829
+ for (const [key, summary] of Array.from(syntheticDependencies.entries()).sort(([a], [b]) => a.localeCompare(b))) {
830
+ dependencyEdges.push(buildDependencyEdge({
831
+ id: `groupdep:${key}`,
832
+ ...summary,
833
+ selectable: false,
834
+ }));
835
+ }
561
836
  const hierarchyEdges = visibleTasks
562
837
  .filter((task) => !!task.parentTaskId && taskMap.has(task.parentTaskId))
563
838
  .map((task) => ({
564
839
  id: `parent:${task.parentTaskId}:${task.taskId}`,
565
- source: task.parentTaskId,
840
+ source: mapTaskId(task.parentTaskId),
566
841
  target: task.taskId,
567
842
  sourceHandle: hierSourceHandle,
568
843
  targetHandle: hierTargetHandle,
@@ -581,13 +856,35 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
581
856
  relationship: "hierarchy",
582
857
  },
583
858
  }));
859
+ for (const group of groups.values()) {
860
+ hierarchyEdges.push({
861
+ id: `parent:${group.parentTaskId}:${group.groupId}`,
862
+ source: mapTaskId(group.parentTaskId),
863
+ target: group.groupId,
864
+ sourceHandle: hierSourceHandle,
865
+ targetHandle: hierTargetHandle,
866
+ type: hierarchyEdgeType,
867
+ style: {
868
+ stroke: "var(--color-component-default)",
869
+ strokeWidth: 1.5,
870
+ },
871
+ markerEnd: {
872
+ type: MarkerType.ArrowClosed,
873
+ color: "var(--color-component-default)",
874
+ width: 16,
875
+ height: 16,
876
+ },
877
+ data: { relationship: "hierarchy" },
878
+ });
879
+ }
584
880
  const edges = [...dependencyEdges, ...hierarchyEdges];
585
881
  if (nodes.length === 0)
586
882
  return { nodes: [], edges: [] };
587
883
  return layoutGraph(nodes, edges, orientation, autoLayoutStrategy, nodeSizeOverrides);
588
884
  }
589
885
  export function buildDependencyGraphLayout(args) {
590
- return buildGraph(args.tasks, args.dependencies, null, null, new Set(), args.orientation ?? "horizontal", false, args.autoLayoutStrategy ?? "hierarchy", () => undefined);
886
+ const blockedTaskIds = collectBlockedTaskIds(args.tasks, args.dependencies);
887
+ return buildGraph(args.tasks, args.dependencies, args.selectedTaskId ?? null, null, blockedTaskIds, args.orientation ?? "horizontal", false, args.autoLayoutStrategy ?? "hierarchy", () => undefined);
591
888
  }
592
889
  function applySavedLayout(nodes, savedLayout) {
593
890
  if (!savedLayout?.nodes?.length) {
@@ -600,10 +897,30 @@ function applySavedLayout(nodes, savedLayout) {
600
897
  y: node.y,
601
898
  },
602
899
  ]));
603
- return nodes.map((node) => ({
604
- ...node,
605
- position: positionsByTaskId.get(node.id) ?? node.position,
606
- }));
900
+ return nodes.map((node) => {
901
+ if (node.type === "taskGroupNode") {
902
+ const memberPositions = node.data.members
903
+ .filter((member) => !member.isOutlier)
904
+ .map((member) => positionsByTaskId.get(member.taskId))
905
+ .filter((position) => !!position);
906
+ if (memberPositions.length > 0) {
907
+ return {
908
+ ...node,
909
+ position: {
910
+ x: memberPositions.reduce((sum, position) => sum + position.x, 0) /
911
+ memberPositions.length,
912
+ y: memberPositions.reduce((sum, position) => sum + position.y, 0) /
913
+ memberPositions.length,
914
+ },
915
+ };
916
+ }
917
+ return node;
918
+ }
919
+ return {
920
+ ...node,
921
+ position: positionsByTaskId.get(node.id) ?? node.position,
922
+ };
923
+ });
607
924
  }
608
925
  function preserveNodePositions(previousNodes, nextNodes) {
609
926
  const previousPositions = new Map(previousNodes.map((node) => [node.id, node.position]));
@@ -716,17 +1033,11 @@ function isGraphConnectionValid(connection, tasks, dependencies, orientation, au
716
1033
  }
717
1034
  return isDependencyConnectionValid(connection, tasks, dependencies, orientation);
718
1035
  }
719
- function createLayoutSignature(layoutKey, nodes, viewport, orientation) {
1036
+ function createLayoutSignature(layoutKey, nodes, viewport, orientation, savedLayout, collapsedMemberIds = new Set()) {
720
1037
  return JSON.stringify({
721
1038
  layoutKey,
722
1039
  orientation,
723
- nodes: nodes
724
- .map((node) => ({
725
- taskId: node.id,
726
- x: node.position.x,
727
- y: node.position.y,
728
- }))
729
- .sort((a, b) => a.taskId.localeCompare(b.taskId)),
1040
+ nodes: buildPersistableLayoutNodes(nodes, savedLayout, collapsedMemberIds).sort((a, b) => a.taskId.localeCompare(b.taskId)),
730
1041
  viewport: viewport
731
1042
  ? {
732
1043
  x: viewport.x,
@@ -736,22 +1047,76 @@ function createLayoutSignature(layoutKey, nodes, viewport, orientation) {
736
1047
  : null,
737
1048
  });
738
1049
  }
1050
+ function buildPersistableLayoutNodes(nodes, savedLayout, collapsedMemberIds) {
1051
+ const renderedTaskIds = new Set();
1052
+ const persistableNodes = [];
1053
+ for (const node of nodes) {
1054
+ if (node.type === "taskGroupNode")
1055
+ continue;
1056
+ renderedTaskIds.add(node.id);
1057
+ persistableNodes.push({
1058
+ taskId: node.id,
1059
+ x: node.position.x,
1060
+ y: node.position.y,
1061
+ });
1062
+ }
1063
+ for (const savedNode of savedLayout?.nodes ?? []) {
1064
+ if (collapsedMemberIds.has(savedNode.taskId) &&
1065
+ !renderedTaskIds.has(savedNode.taskId)) {
1066
+ persistableNodes.push({
1067
+ taskId: savedNode.taskId,
1068
+ x: savedNode.x,
1069
+ y: savedNode.y,
1070
+ });
1071
+ renderedTaskIds.add(savedNode.taskId);
1072
+ }
1073
+ }
1074
+ return persistableNodes;
1075
+ }
739
1076
  function createGraphSyncSignature(nodes, edges) {
740
1077
  return JSON.stringify({
741
1078
  nodes: nodes
742
- .map((node) => ({
743
- id: node.id,
744
- x: node.position.x,
745
- y: node.position.y,
746
- selected: node.data.isSelected,
747
- status: node.data.status,
748
- executionState: node.data.executionState ?? null,
749
- priority: node.data.priority ?? null,
750
- blocked: node.data.isBlocked,
751
- taskType: node.data.taskType ?? null,
752
- assigneeType: node.data.assigneeType ?? null,
753
- assignee: node.data.assigneeDisplayName ?? null,
754
- }))
1079
+ .map((node) => {
1080
+ if (node.type === "taskGroupNode") {
1081
+ return {
1082
+ id: node.id,
1083
+ type: node.type,
1084
+ x: node.position.x,
1085
+ y: node.position.y,
1086
+ memberCount: node.data.memberCount,
1087
+ statusCounts: node.data.statusCounts,
1088
+ label: node.data.label,
1089
+ status: node.data.status,
1090
+ blocked: node.data.isBlocked,
1091
+ members: node.data.members
1092
+ .map((member) => ({
1093
+ taskId: member.taskId,
1094
+ taskKey: member.taskKey,
1095
+ title: member.title,
1096
+ status: member.status,
1097
+ executionState: member.executionState ?? null,
1098
+ isBlocked: member.isBlocked,
1099
+ isOutlier: member.isOutlier,
1100
+ }))
1101
+ .sort((a, b) => a.taskId.localeCompare(b.taskId)),
1102
+ };
1103
+ }
1104
+ return {
1105
+ id: node.id,
1106
+ type: node.type,
1107
+ x: node.position.x,
1108
+ y: node.position.y,
1109
+ title: node.data.title,
1110
+ selected: node.data.isSelected,
1111
+ status: node.data.status,
1112
+ executionState: node.data.executionState ?? null,
1113
+ priority: node.data.priority ?? null,
1114
+ blocked: node.data.isBlocked,
1115
+ taskType: node.data.taskType ?? null,
1116
+ assigneeType: node.data.assigneeType ?? null,
1117
+ assignee: node.data.assigneeDisplayName ?? null,
1118
+ };
1119
+ })
755
1120
  .sort((a, b) => a.id.localeCompare(b.id)),
756
1121
  edges: edges
757
1122
  .map((edge) => ({
@@ -760,6 +1125,9 @@ function createGraphSyncSignature(nodes, edges) {
760
1125
  target: edge.target,
761
1126
  sourceHandle: edge.sourceHandle ?? null,
762
1127
  targetHandle: edge.targetHandle ?? null,
1128
+ relationship: edge.data?.relationship ?? null,
1129
+ dependencyType: edge.data?.dependencyType ?? null,
1130
+ count: edge.data?.count ?? null,
763
1131
  }))
764
1132
  .sort((a, b) => a.id.localeCompare(b.id)),
765
1133
  });
@@ -776,33 +1144,12 @@ export function DependencyGraphView(props) {
776
1144
  : null;
777
1145
  const [transientOrientation, setTransientOrientation] = useState(null);
778
1146
  const effectiveOrientation = transientOrientation ?? savedLayoutOrientation ?? fallbackOrientation;
779
- const taskMap = useMemo(() => {
780
- const m = new Map();
781
- for (const t of tasks)
782
- m.set(t.taskId, t);
783
- return m;
784
- }, [tasks]);
785
1147
  const blockedTaskIds = useMemo(() => {
786
1148
  if (!highlightBlockedTasks) {
787
1149
  return new Set();
788
1150
  }
789
- const blocked = new Set();
790
- for (const task of tasks) {
791
- if ((task.executionBlockedByProjectIds?.length ?? 0) > 0) {
792
- blocked.add(task.taskId);
793
- }
794
- }
795
- for (const dep of dependencies) {
796
- if (dep.dependencyType !== "BlockedBy")
797
- continue;
798
- const blocker = taskMap.get(dep.dependsOnTaskId);
799
- if (!blocker ||
800
- normalizeTaskStatus(blocker.status, blocker.executionState) !== "Done") {
801
- blocked.add(dep.taskId);
802
- }
803
- }
804
- return blocked;
805
- }, [dependencies, highlightBlockedTasks, taskMap]);
1151
+ return collectBlockedTaskIds(tasks, dependencies);
1152
+ }, [dependencies, highlightBlockedTasks, tasks]);
806
1153
  const onSelect = useCallback((taskId) => onSelectTask(taskId), [onSelectTask]);
807
1154
  const { nodes: initialNodes, edges: initialEdges } = useMemo(() => buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, effectiveOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask), [
808
1155
  tasks,
@@ -818,6 +1165,18 @@ export function DependencyGraphView(props) {
818
1165
  onAddChildTaskFromNode,
819
1166
  onRemoveTask,
820
1167
  ]);
1168
+ const collapsedMemberIds = useMemo(() => {
1169
+ const ids = new Set();
1170
+ for (const node of initialNodes) {
1171
+ if (node.type !== "taskGroupNode")
1172
+ continue;
1173
+ for (const member of node.data.members) {
1174
+ if (!member.isOutlier)
1175
+ ids.add(member.taskId);
1176
+ }
1177
+ }
1178
+ return ids;
1179
+ }, [initialNodes]);
821
1180
  const [nodes, setNodes, onNodesChangeBase] = useNodesState(initialNodes);
822
1181
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
823
1182
  const nodesRef = useRef(nodes);
@@ -959,17 +1318,13 @@ export function DependencyGraphView(props) {
959
1318
  if (!canPersistLayout) {
960
1319
  return;
961
1320
  }
962
- const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation);
1321
+ const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation, savedLayout, collapsedMemberIds);
963
1322
  if (signature === lastSavedSignatureRef.current) {
964
1323
  return;
965
1324
  }
966
1325
  try {
967
1326
  const layoutPayload = {
968
- nodes: nextNodes.map((node) => ({
969
- taskId: node.id,
970
- x: node.position.x,
971
- y: node.position.y,
972
- })),
1327
+ nodes: buildPersistableLayoutNodes(nextNodes, savedLayout, collapsedMemberIds),
973
1328
  orientation: nextOrientation,
974
1329
  viewport: viewport == null
975
1330
  ? null
@@ -1015,17 +1370,18 @@ export function DependencyGraphView(props) {
1015
1370
  }
1016
1371
  }, [
1017
1372
  canPersistLayout,
1373
+ collapsedMemberIds,
1018
1374
  effectiveLayoutKey,
1019
- effectiveOrientation,
1020
1375
  onLayoutSaved,
1021
1376
  onPersistLayout,
1022
1377
  projectId,
1378
+ savedLayout,
1023
1379
  ]);
1024
1380
  const queueLayoutSave = useCallback((nextNodes, viewport, nextOrientation = effectiveOrientation) => {
1025
1381
  if (!canPersistLayout) {
1026
1382
  return;
1027
1383
  }
1028
- const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation);
1384
+ const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation, savedLayout, collapsedMemberIds);
1029
1385
  if (signature === lastSavedSignatureRef.current) {
1030
1386
  return;
1031
1387
  }
@@ -1043,10 +1399,12 @@ export function DependencyGraphView(props) {
1043
1399
  }, layoutSaveDebounceMs);
1044
1400
  }, [
1045
1401
  canPersistLayout,
1402
+ collapsedMemberIds,
1046
1403
  effectiveLayoutKey,
1047
1404
  effectiveOrientation,
1048
1405
  layoutSaveDebounceMs,
1049
1406
  persistLayout,
1407
+ savedLayout,
1050
1408
  ]);
1051
1409
  const restoreViewport = useCallback(() => {
1052
1410
  const instance = reactFlowRef.current;
@@ -1059,7 +1417,7 @@ export function DependencyGraphView(props) {
1059
1417
  skipNextMoveEndSaveRef.current = true;
1060
1418
  window.requestAnimationFrame(() => {
1061
1419
  reactFlowRef.current?.setViewport(viewport, { duration: 0 });
1062
- lastSavedSignatureRef.current = createLayoutSignature(effectiveLayoutKey, nodesRef.current, viewport, effectiveOrientation);
1420
+ lastSavedSignatureRef.current = createLayoutSignature(effectiveLayoutKey, nodesRef.current, viewport, effectiveOrientation, savedLayout, collapsedMemberIds);
1063
1421
  });
1064
1422
  return;
1065
1423
  }
@@ -1078,14 +1436,21 @@ export function DependencyGraphView(props) {
1078
1436
  const viewport = reactFlowRef.current?.getViewport() ?? null;
1079
1437
  queueLayoutSave(nodesRef.current, viewport, nextOrientation);
1080
1438
  });
1081
- }, [effectiveLayoutKey, effectiveOrientation, queueLayoutSave]);
1439
+ }, [
1440
+ collapsedMemberIds,
1441
+ effectiveLayoutKey,
1442
+ effectiveOrientation,
1443
+ queueLayoutSave,
1444
+ savedLayout,
1445
+ ]);
1082
1446
  useEffect(() => {
1083
1447
  const shouldApplySavedLayout = lastAppliedLayoutKeyRef.current !== effectiveLayoutKey ||
1084
1448
  lastAppliedLayoutSignatureRef.current !== savedLayoutSignature;
1085
1449
  const nextNodeIdSignature = initialNodes.map((node) => node.id).join("|");
1086
- const savedLayoutNodeCount = savedLayout?.nodes?.length ?? 0;
1450
+ const savedLayoutTaskIds = new Set((savedLayout?.nodes ?? []).map((node) => node.taskId));
1451
+ const hasRenderedNodeMissingFromSavedLayout = initialNodes.some((node) => node.type !== "taskGroupNode" && !savedLayoutTaskIds.has(node.id));
1087
1452
  const shouldAutoLayoutForHierarchy = autoLayoutStrategy === "hierarchy" &&
1088
- (savedLayoutNodeCount < initialNodes.length ||
1453
+ (hasRenderedNodeMissingFromSavedLayout ||
1089
1454
  (lastAppliedNodeIdSignatureRef.current !== null &&
1090
1455
  lastAppliedNodeIdSignatureRef.current !== nextNodeIdSignature));
1091
1456
  const positionedNodes = shouldApplySavedLayout
@@ -1117,7 +1482,7 @@ export function DependencyGraphView(props) {
1117
1482
  shouldAutoLayoutForHierarchy || !savedLayout?.viewport;
1118
1483
  if (!savedLayout?.viewport || shouldAutoLayoutForHierarchy) {
1119
1484
  lastSavedSignatureRef.current = savedLayout?.nodes?.length
1120
- ? createLayoutSignature(effectiveLayoutKey, nextNodes, null, effectiveOrientation)
1485
+ ? createLayoutSignature(effectiveLayoutKey, nextNodes, null, effectiveOrientation, savedLayout, collapsedMemberIds)
1121
1486
  : null;
1122
1487
  }
1123
1488
  if (shouldAutoLayoutForHierarchy) {
@@ -1134,6 +1499,7 @@ export function DependencyGraphView(props) {
1134
1499
  }
1135
1500
  }, [
1136
1501
  autoLayoutStrategy,
1502
+ collapsedMemberIds,
1137
1503
  getLatestGraphNodes,
1138
1504
  initialEdges,
1139
1505
  initialNodes,
@@ -1305,6 +1671,9 @@ export function DependencyGraphView(props) {
1305
1671
  }
1306
1672
  }, onNodeClick: (_event, node) => {
1307
1673
  focusGraphContainer();
1674
+ if (node.type === "taskGroupNode") {
1675
+ return;
1676
+ }
1308
1677
  if (performance.now() < suppressNodeSelectionUntilRef.current) {
1309
1678
  return;
1310
1679
  }
@@ -1312,6 +1681,9 @@ export function DependencyGraphView(props) {
1312
1681
  onSelectTask(node.id);
1313
1682
  }, onNodeDragStart: (_event, node) => {
1314
1683
  focusGraphContainer();
1684
+ if (node.type === "taskGroupNode") {
1685
+ return;
1686
+ }
1315
1687
  if (performance.now() < suppressNodeSelectionUntilRef.current) {
1316
1688
  return;
1317
1689
  }