@parhelia/core 0.1.12436 → 0.1.12447

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 (43) hide show
  1. package/dist/config/types.d.ts +4 -0
  2. package/dist/config/types.js.map +1 -1
  3. package/dist/editor/ContentTree.js +19 -7
  4. package/dist/editor/ContentTree.js.map +1 -1
  5. package/dist/editor/Editor.js.map +1 -1
  6. package/dist/editor/ai/AgentTerminal.js +59 -1
  7. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  8. package/dist/editor/services/serviceHelper.js +5 -2
  9. package/dist/editor/services/serviceHelper.js.map +1 -1
  10. package/dist/editor/settings/panels/PersistentLogsPanel.js +25 -2
  11. package/dist/editor/settings/panels/PersistentLogsPanel.js.map +1 -1
  12. package/dist/editor/settings/panels/ProjectTemplateAgentPanel.d.ts +7 -1
  13. package/dist/editor/settings/panels/ProjectTemplateAgentPanel.js +3 -3
  14. package/dist/editor/settings/panels/ProjectTemplateAgentPanel.js.map +1 -1
  15. package/dist/editor/settings/panels/ProjectTemplatesPanel.js +143 -84
  16. package/dist/editor/settings/panels/ProjectTemplatesPanel.js.map +1 -1
  17. package/dist/revision.d.ts +2 -2
  18. package/dist/revision.js +2 -2
  19. package/dist/task-board/TaskBoardWorkspace.js +1 -1
  20. package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
  21. package/dist/task-board/assigneeDisplay.js +1 -3
  22. package/dist/task-board/assigneeDisplay.js.map +1 -1
  23. package/dist/task-board/components/CreateProjectDialog.js +2 -1
  24. package/dist/task-board/components/CreateProjectDialog.js.map +1 -1
  25. package/dist/task-board/components/TaskboardPersistentLogPanel.js +32 -5
  26. package/dist/task-board/components/TaskboardPersistentLogPanel.js.map +1 -1
  27. package/dist/task-board/persistentLogCopy.d.ts +7 -0
  28. package/dist/task-board/persistentLogCopy.js +80 -0
  29. package/dist/task-board/persistentLogCopy.js.map +1 -0
  30. package/dist/task-board/types.d.ts +3 -0
  31. package/dist/task-board/utils/taskDependencyOrdering.d.ts +6 -0
  32. package/dist/task-board/utils/taskDependencyOrdering.js +138 -1
  33. package/dist/task-board/utils/taskDependencyOrdering.js.map +1 -1
  34. package/dist/task-board/views/DependencyGraphView.d.ts +5 -2
  35. package/dist/task-board/views/DependencyGraphView.js +178 -61
  36. package/dist/task-board/views/DependencyGraphView.js.map +1 -1
  37. package/dist/task-board/views/WizardView.js +26 -24
  38. package/dist/task-board/views/WizardView.js.map +1 -1
  39. package/dist/tour/Tour.js +63 -0
  40. package/dist/tour/Tour.js.map +1 -1
  41. package/dist/tour/default-tour.js +7 -0
  42. package/dist/tour/default-tour.js.map +1 -1
  43. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useRef } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { ReactFlow, Controls, MiniMap, useNodesState, useEdgesState, Handle, Position, MarkerType, } from "@xyflow/react";
4
4
  import Dagre from "@dagrejs/dagre";
5
5
  import "@xyflow/react/dist/style.css";
@@ -10,7 +10,7 @@ import { cn } from "../../lib/utils";
10
10
  import { saveGraphLayout } from "../services/taskService";
11
11
  import { Button } from "../../components/ui/button";
12
12
  import { Badge } from "../../components/ui/badge";
13
- import { Tooltip, TooltipContent, TooltipTrigger } from "../../components/ui/tooltip";
13
+ import { Tooltip, TooltipContent, TooltipTrigger, } from "../../components/ui/tooltip";
14
14
  import { Circle, PlayCircle, Clock3, Eye, CheckCircle2, ArrowUp, ArrowDown, Minus, Flame, GitBranch, RotateCcw, Loader2, Plus, Trash2, } from "lucide-react";
15
15
  const NODE_WIDTH = 220;
16
16
  const NODE_HEIGHT = 80;
@@ -46,24 +46,37 @@ function TaskGraphNode({ data }) {
46
46
  const priorityCfg = data.priority ? PRIORITY_CONFIG[data.priority] : null;
47
47
  const PriorityIcon = priorityCfg?.icon;
48
48
  const actionButtonClass = "nodrag nopan absolute z-20 flex h-5 w-5 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 shadow-sm transition-colors hover:border-blue-200 hover:bg-blue-50 hover:text-blue-600";
49
- return (_jsxs("div", { className: cn("relative cursor-pointer rounded-lg border bg-white px-3 py-2 shadow-sm transition-all", data.isSelected
50
- ? "ring-primary border-primary/50 ring-2"
51
- : "border-slate-200 hover:border-slate-300 hover:shadow-md", data.isBlocked && !data.isSelected && "border-red-200 bg-red-50/40"), style: { width: NODE_WIDTH }, "data-testid": `dependency-graph-node-${data.taskId}`, children: [_jsx(Handle, { type: "target", position: Position.Left, id: "left", className: HANDLE_CLASS, "data-testid": `dependency-graph-target-left-${data.taskId}` }), _jsx(Handle, { type: "target", position: Position.Top, id: "top", className: HANDLE_CLASS, "data-testid": `dependency-graph-target-top-${data.taskId}` }), _jsxs("div", { className: "flex items-start justify-between gap-1", children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center justify-between gap-1", children: [_jsx("span", { className: "truncate text-[10px] font-medium text-slate-400", children: data.taskKey || data.taskId.slice(0, 8).toUpperCase() }), PriorityIcon && (_jsx(PriorityIcon, { className: cn("h-3 w-3 shrink-0", priorityCfg.color) }))] }), data.onRemoveTask && (_jsx("button", { type: "button", className: "text-slate-400 hover:bg-red-50 hover:text-red-600 -mr-0.5 -mt-px shrink-0 rounded p-0.5 transition-colors", "aria-label": "Remove task", "data-testid": `dependency-graph-remove-task-${data.taskId}`, onClick: (event) => {
49
+ const dependentButtonClass = data.dependencySourceHandle === "right"
50
+ ? "top-1/2 right-0 translate-x-[calc(100%+6px)] -translate-y-1/2"
51
+ : "bottom-0 left-1/2 -translate-x-1/2 translate-y-[calc(100%+6px)]";
52
+ const dependentTooltipSide = data.dependencySourceHandle === "right" ? "right" : "bottom";
53
+ const subtaskButtonClass = data.hierarchySourceHandle === "right"
54
+ ? "top-1/2 right-0 translate-x-[calc(100%+6px)] -translate-y-1/2"
55
+ : "bottom-0 left-1/2 -translate-x-1/2 translate-y-[calc(100%+6px)]";
56
+ const subtaskTooltipSide = data.hierarchySourceHandle === "right" ? "right" : "bottom";
57
+ const nodeSurfaceClass = data.isSelected
58
+ ? "ring-primary border-primary/50 ring-2 bg-white"
59
+ : data.isBlocked
60
+ ? "border-red-200 bg-red-50/70"
61
+ : data.status === "Done"
62
+ ? "border-emerald-200 bg-emerald-50/80"
63
+ : "border-slate-200 bg-white hover:border-slate-300 hover:shadow-md";
64
+ return (_jsxs("div", { className: cn("relative cursor-pointer rounded-lg border px-3 py-2 shadow-sm transition-all", nodeSurfaceClass), style: { width: NODE_WIDTH }, "data-testid": `dependency-graph-node-${data.taskId}`, children: [_jsx(Handle, { type: "target", position: Position.Left, id: "left", className: HANDLE_CLASS, "data-testid": `dependency-graph-target-left-${data.taskId}` }), _jsx(Handle, { type: "target", position: Position.Top, id: "top", className: HANDLE_CLASS, "data-testid": `dependency-graph-target-top-${data.taskId}` }), _jsxs("div", { className: "flex items-start justify-between gap-1", children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center justify-between gap-1", children: [_jsx("span", { className: "truncate text-[10px] font-medium text-slate-400", children: data.taskKey || data.taskId.slice(0, 8).toUpperCase() }), PriorityIcon && (_jsx(PriorityIcon, { className: cn("h-3 w-3 shrink-0", priorityCfg.color) }))] }), data.onRemoveTask && (_jsx("button", { type: "button", className: "-mt-px -mr-0.5 shrink-0 rounded p-0.5 text-slate-400 transition-colors hover:bg-red-50 hover:text-red-600", "aria-label": "Remove task", "data-testid": `dependency-graph-remove-task-${data.taskId}`, onClick: (event) => {
52
65
  event.stopPropagation();
53
66
  data.onRemoveTask?.(data.taskId);
54
- }, children: _jsx(Trash2, { className: "h-2.5 w-2.5", strokeWidth: 1.5 }) }))] }), _jsx("div", { className: "mt-0.5 truncate text-xs font-medium text-slate-800", children: data.title }), _jsxs("div", { className: "mt-1 flex items-center gap-1.5", children: [executionDisplay ? (_jsxs(Badge, { variant: executionDisplay.variant, className: cn("h-5 max-w-full px-1.5 text-[10px] font-bold uppercase tracking-wider", executionDisplay.className), children: [executionDisplay.showSpinner && (_jsx(Loader2, { className: "mr-1 h-2.5 w-2.5 animate-spin" })), executionDisplay.label] })) : (_jsxs("span", { className: cn("inline-flex items-center gap-0.5 rounded-full px-1.5 py-0 text-[10px] font-medium", statusStyle.bg, statusStyle.text), children: [_jsx(StatusIcon, { className: "h-2.5 w-2.5" }), getTaskStatusLabel(status)] })), data.assigneeDisplayName && (_jsxs("span", { className: "truncate text-[10px] text-slate-400", children: ["@", data.assigneeDisplayName] }))] }), data.onAddDependentTaskFromNode && (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: cn(actionButtonClass, "top-1/2 right-0 translate-x-[calc(100%+6px)] -translate-y-1/2"), "aria-label": "Add dependent task", "data-testid": `dependency-graph-add-dependent-task-${data.taskId}`, onClick: (event) => {
67
+ }, children: _jsx(Trash2, { className: "h-2.5 w-2.5", strokeWidth: 1.5 }) }))] }), _jsx("div", { className: "mt-0.5 truncate text-xs font-medium text-slate-800", children: data.title }), _jsxs("div", { className: "mt-1 flex items-center gap-1.5", children: [executionDisplay ? (_jsxs(Badge, { variant: executionDisplay.variant, className: cn("h-5 max-w-full px-1.5 text-[10px] font-bold tracking-wider uppercase", executionDisplay.className), children: [executionDisplay.showSpinner && (_jsx(Loader2, { className: "mr-1 h-2.5 w-2.5 animate-spin" })), executionDisplay.label] })) : (_jsxs("span", { className: cn("inline-flex items-center gap-0.5 rounded-full px-1.5 py-0 text-[10px] font-medium", statusStyle.bg, statusStyle.text), children: [_jsx(StatusIcon, { className: "h-2.5 w-2.5" }), getTaskStatusLabel(status)] })), data.assigneeDisplayName && (_jsxs("span", { className: "truncate text-[10px] text-slate-400", children: ["@", data.assigneeDisplayName] }))] }), data.onAddDependentTaskFromNode && (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: cn(actionButtonClass, dependentButtonClass), "aria-label": "Add dependent task", "data-testid": `dependency-graph-add-dependent-task-${data.taskId}`, onClick: (event) => {
55
68
  event.stopPropagation();
56
69
  data.onSelect(data.taskId);
57
70
  data.onAddDependentTaskFromNode?.(data.taskId);
58
71
  }, onMouseDown: (event) => {
59
72
  event.stopPropagation();
60
- }, children: _jsx(Plus, { className: "h-3 w-3", strokeWidth: 1.75 }) }) }), _jsx(TooltipContent, { side: "right", sideOffset: 8, children: "Add dependent task" })] })), data.onAddChildTaskFromNode && (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: cn(actionButtonClass, "bottom-0 left-1/2 -translate-x-1/2 translate-y-[calc(100%+6px)]"), "aria-label": "Add subtask", "data-testid": `dependency-graph-add-subtask-${data.taskId}`, onClick: (event) => {
73
+ }, children: _jsx(Plus, { className: "h-3 w-3", strokeWidth: 1.75 }) }) }), _jsx(TooltipContent, { side: dependentTooltipSide, sideOffset: 8, children: "Add dependent task" })] })), data.onAddChildTaskFromNode && (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: cn(actionButtonClass, subtaskButtonClass), "aria-label": "Add subtask", "data-testid": `dependency-graph-add-subtask-${data.taskId}`, onClick: (event) => {
61
74
  event.stopPropagation();
62
75
  data.onSelect(data.taskId);
63
76
  data.onAddChildTaskFromNode?.(data.taskId);
64
77
  }, onMouseDown: (event) => {
65
78
  event.stopPropagation();
66
- }, children: _jsx(Plus, { className: "h-3 w-3", strokeWidth: 1.75 }) }) }), _jsx(TooltipContent, { side: "bottom", 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}` })] }));
79
+ }, children: _jsx(Plus, { className: "h-3 w-3", strokeWidth: 1.75 }) }) }), _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}` })] }));
67
80
  }
68
81
  const nodeTypes = {
69
82
  taskNode: TaskGraphNode,
@@ -71,13 +84,30 @@ const nodeTypes = {
71
84
  const HIERARCHY_CHILD_INDENT = 24;
72
85
  const HIERARCHY_CHILD_GAP = 32;
73
86
  const HIERARCHY_SUBTREE_GAP = 80;
87
+ function normalizeGraphOrientation(orientation) {
88
+ return orientation === "vertical" ? "vertical" : "horizontal";
89
+ }
90
+ function getDependencyHandles(orientation) {
91
+ return orientation === "horizontal"
92
+ ? { source: "right", target: "left" }
93
+ : { source: "bottom", target: "top" };
94
+ }
95
+ function getHierarchyHandles(orientation, autoLayoutStrategy) {
96
+ if (autoLayoutStrategy !== "hierarchy") {
97
+ return getDependencyHandles(orientation);
98
+ }
99
+ return orientation === "horizontal"
100
+ ? { source: "bottom", target: "left" }
101
+ : { source: "right", target: "left" };
102
+ }
74
103
  /**
75
104
  * Compound layout for the hierarchy strategy: each parent–children group is
76
105
  * laid out vertically (parent on top, children stacked below with a slight
77
106
  * indent), and then the resulting subtree "super-nodes" are arranged
78
- * left-to-right via Dagre using the dependency edges between them.
107
+ * in the selected orientation via Dagre using the dependency edges between
108
+ * them.
79
109
  */
80
- function layoutGraphHierarchy(nodes, edges) {
110
+ function layoutGraphHierarchy(nodes, edges, orientation) {
81
111
  if (nodes.length === 0)
82
112
  return { nodes, edges };
83
113
  const childrenOf = new Map();
@@ -122,7 +152,11 @@ function layoutGraphHierarchy(nodes, edges) {
122
152
  }
123
153
  }
124
154
  const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
125
- g.setGraph({ rankdir: "LR", nodesep: 40, ranksep: HIERARCHY_SUBTREE_GAP });
155
+ g.setGraph({
156
+ rankdir: orientation === "horizontal" ? "LR" : "TB",
157
+ nodesep: 40,
158
+ ranksep: HIERARCHY_SUBTREE_GAP,
159
+ });
126
160
  for (const [rootId, data] of subtrees) {
127
161
  g.setNode(rootId, { width: data.width, height: data.height });
128
162
  }
@@ -164,7 +198,7 @@ function layoutGraphHierarchy(nodes, edges) {
164
198
  function layoutGraph(nodes, edges, orientation, autoLayoutStrategy) {
165
199
  if (autoLayoutStrategy === "hierarchy" &&
166
200
  edges.some((e) => e.data?.relationship === "hierarchy")) {
167
- return layoutGraphHierarchy(nodes, edges);
201
+ return layoutGraphHierarchy(nodes, edges, orientation);
168
202
  }
169
203
  const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
170
204
  g.setGraph({
@@ -199,6 +233,8 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
199
233
  const childRelationshipKeys = new Set(visibleTasks
200
234
  .filter((task) => !!task.parentTaskId)
201
235
  .map((task) => `${task.parentTaskId}->${task.taskId}`));
236
+ const dependencyHandles = getDependencyHandles(orientation);
237
+ const hierarchyHandles = getHierarchyHandles(orientation, autoLayoutStrategy);
202
238
  const nodes = visibleTasks.map((task) => ({
203
239
  id: task.taskId,
204
240
  type: "taskNode",
@@ -215,26 +251,27 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
215
251
  isSelected: task.taskId === selectedTaskId,
216
252
  isBlocked: blockedTaskIds.has(task.taskId),
217
253
  orientation,
254
+ dependencySourceHandle: dependencyHandles.source,
255
+ hierarchySourceHandle: hierarchyHandles.source,
218
256
  onSelect,
219
257
  onAddDependentTaskFromNode,
220
258
  onAddChildTaskFromNode,
221
259
  onRemoveTask,
222
260
  },
223
261
  }));
224
- const depSourceHandle = orientation === "horizontal" ? "right" : "bottom";
225
- const depTargetHandle = orientation === "horizontal" ? "left" : "top";
226
- const hierSourceHandle = autoLayoutStrategy === "hierarchy" ? "bottom" : depSourceHandle;
227
- const hierTargetHandle = autoLayoutStrategy === "hierarchy" ? "left" : depTargetHandle;
262
+ const depSourceHandle = dependencyHandles.source;
263
+ const depTargetHandle = dependencyHandles.target;
264
+ const hierSourceHandle = hierarchyHandles.source;
265
+ const hierTargetHandle = hierarchyHandles.target;
228
266
  const dependencyEdges = dependencies
229
267
  .filter((dep) => taskMap.has(dep.taskId) &&
230
268
  taskMap.has(dep.dependsOnTaskId) &&
231
269
  !childRelationshipKeys.has(`${dep.dependsOnTaskId}->${dep.taskId}`))
232
270
  .map((dep) => {
233
271
  const isBlockedBy = dep.dependencyType === "BlockedBy";
234
- const strokeColor = isBlockedBy
235
- ? EDGE_BLOCKED_BY
236
- : EDGE_RELATED;
237
- const isSelected = dep.taskId === selectedTaskId && dep.dependsOnTaskId === selectedDependencyId;
272
+ const strokeColor = isBlockedBy ? EDGE_BLOCKED_BY : EDGE_RELATED;
273
+ const isSelected = dep.taskId === selectedTaskId &&
274
+ dep.dependsOnTaskId === selectedDependencyId;
238
275
  return {
239
276
  id: dep.dependencyId,
240
277
  source: dep.dependsOnTaskId,
@@ -264,8 +301,7 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
264
301
  };
265
302
  });
266
303
  const hierarchyEdges = visibleTasks
267
- .filter((task) => !!task.parentTaskId &&
268
- taskMap.has(task.parentTaskId))
304
+ .filter((task) => !!task.parentTaskId && taskMap.has(task.parentTaskId))
269
305
  .map((task) => ({
270
306
  id: `parent:${task.parentTaskId}:${task.taskId}`,
271
307
  source: task.parentTaskId,
@@ -320,12 +356,15 @@ function buildChildRelationshipKeys(tasks) {
320
356
  .filter((task) => !!task.parentTaskId)
321
357
  .map((task) => `${task.parentTaskId}->${task.taskId}`));
322
358
  }
323
- function isHierarchyConnectionValid(connection, tasks) {
359
+ function isHierarchyConnectionValid(connection, tasks, orientation, autoLayoutStrategy) {
360
+ if (autoLayoutStrategy !== "hierarchy") {
361
+ return false;
362
+ }
324
363
  const { source, sourceHandle, target, targetHandle } = connection;
325
364
  if (!source || !target || source === target) {
326
365
  return false;
327
366
  }
328
- if (sourceHandle !== "bottom") {
367
+ if (sourceHandle !== getHierarchyHandles(orientation, autoLayoutStrategy).source) {
329
368
  return false;
330
369
  }
331
370
  if (targetHandle && targetHandle !== "left" && targetHandle !== "top") {
@@ -345,12 +384,12 @@ function isHierarchyConnectionValid(connection, tasks) {
345
384
  }
346
385
  return !buildChildRelationshipKeys(tasks).has(`${source}->${target}`);
347
386
  }
348
- function isDependencyConnectionValid(connection, tasks, dependencies) {
387
+ function isDependencyConnectionValid(connection, tasks, dependencies, orientation) {
349
388
  const { source, sourceHandle, target, targetHandle } = connection;
350
389
  if (!source || !target || source === target) {
351
390
  return false;
352
391
  }
353
- if (sourceHandle !== "right") {
392
+ if (sourceHandle !== getDependencyHandles(orientation).source) {
354
393
  return false;
355
394
  }
356
395
  if (targetHandle && targetHandle !== "left" && targetHandle !== "top") {
@@ -365,15 +404,18 @@ function isDependencyConnectionValid(connection, tasks, dependencies) {
365
404
  }
366
405
  return !buildChildRelationshipKeys(tasks).has(`${source}->${target}`);
367
406
  }
368
- function isGraphConnectionValid(connection, tasks, dependencies) {
369
- if (connection.sourceHandle === "bottom") {
370
- return isHierarchyConnectionValid(connection, tasks);
407
+ function isGraphConnectionValid(connection, tasks, dependencies, orientation, autoLayoutStrategy) {
408
+ const hierarchySourceHandle = getHierarchyHandles(orientation, "hierarchy").source;
409
+ if (autoLayoutStrategy === "hierarchy" &&
410
+ connection.sourceHandle === hierarchySourceHandle) {
411
+ return isHierarchyConnectionValid(connection, tasks, orientation, autoLayoutStrategy);
371
412
  }
372
- return isDependencyConnectionValid(connection, tasks, dependencies);
413
+ return isDependencyConnectionValid(connection, tasks, dependencies, orientation);
373
414
  }
374
- function createLayoutSignature(layoutKey, nodes, viewport) {
415
+ function createLayoutSignature(layoutKey, nodes, viewport, orientation) {
375
416
  return JSON.stringify({
376
417
  layoutKey,
418
+ orientation,
377
419
  nodes: nodes
378
420
  .map((node) => ({
379
421
  taskId: node.id,
@@ -391,11 +433,17 @@ function createLayoutSignature(layoutKey, nodes, viewport) {
391
433
  });
392
434
  }
393
435
  export function DependencyGraphView(props) {
394
- const { projectId, layoutKey, tasks, dependencies, savedLayout, selectedTaskId, selectedDependencyId = null, onSelectTask, onSelectDependency, onClearDependencySelection, permission, canPersistLayout: canPersistLayoutOverride, onPersistLayout, onLayoutSaved, highlightBlockedTasks = true, showExecutionStateBadges = true, emptyStateTitle = "No tasks to visualize", emptyStateDescription = "Create tasks in this project to see them in the dependency graph.", orientation = "vertical", autoLayoutStrategy = "allEdges", layoutSaveDebounceMs = SAVE_DEBOUNCE_MS, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask, allowDependencyConnect = false, onCreateDependency, onCreateChildRelationship, miniMapWidth, miniMapHeight, } = props;
436
+ const { projectId, layoutKey, tasks, dependencies, savedLayout, selectedTaskId, selectedDependencyId = null, onSelectTask, onSelectDependency, onClearDependencySelection, permission, canPersistLayout: canPersistLayoutOverride, onPersistLayout, onLayoutSaved, highlightBlockedTasks = true, showExecutionStateBadges = true, emptyStateTitle = "No tasks to visualize", emptyStateDescription = "Create tasks in this project to see them in the dependency graph.", orientation = "vertical", autoLayoutStrategy = "allEdges", showOrientationToggle = false, layoutSaveDebounceMs = SAVE_DEBOUNCE_MS, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask, allowDependencyConnect = false, onCreateDependency, onCreateChildRelationship, miniMapWidth, miniMapHeight, showMiniMap = true, } = props;
395
437
  const canPersistLayout = typeof canPersistLayoutOverride === "boolean"
396
438
  ? canPersistLayoutOverride
397
439
  : permission === "Owner" || permission === "Editor";
398
440
  const effectiveLayoutKey = layoutKey ?? projectId ?? "graph";
441
+ const fallbackOrientation = normalizeGraphOrientation(orientation);
442
+ const savedLayoutOrientation = savedLayout
443
+ ? normalizeGraphOrientation(savedLayout.orientation ?? fallbackOrientation)
444
+ : null;
445
+ const [transientOrientation, setTransientOrientation] = useState(null);
446
+ const effectiveOrientation = transientOrientation ?? savedLayoutOrientation ?? fallbackOrientation;
399
447
  const taskMap = useMemo(() => {
400
448
  const m = new Map();
401
449
  for (const t of tasks)
@@ -424,13 +472,13 @@ export function DependencyGraphView(props) {
424
472
  return blocked;
425
473
  }, [dependencies, highlightBlockedTasks, taskMap]);
426
474
  const onSelect = useCallback((taskId) => onSelectTask(taskId), [onSelectTask]);
427
- const { nodes: initialNodes, edges: initialEdges } = useMemo(() => buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, orientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask), [
475
+ const { nodes: initialNodes, edges: initialEdges } = useMemo(() => buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, effectiveOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask), [
428
476
  tasks,
429
477
  dependencies,
430
478
  selectedTaskId,
431
479
  selectedDependencyId,
432
480
  blockedTaskIds,
433
- orientation,
481
+ effectiveOrientation,
434
482
  showExecutionStateBadges,
435
483
  autoLayoutStrategy,
436
484
  onSelect,
@@ -458,22 +506,34 @@ export function DependencyGraphView(props) {
458
506
  const pendingViewportRef = useRef(null);
459
507
  const fitViewOnNextSyncRef = useRef(false);
460
508
  const skipNextMoveEndSaveRef = useRef(false);
461
- const saveAfterViewportRestoreRef = useRef(false);
509
+ const pendingSaveOrientationRef = useRef(null);
462
510
  const lastSavedSignatureRef = useRef(null);
463
511
  const lastAppliedLayoutKeyRef = useRef(null);
464
512
  const lastAppliedLayoutSignatureRef = useRef(null);
465
513
  const savedLayoutSignature = useMemo(() => savedLayout
466
514
  ? JSON.stringify({
515
+ orientation: savedLayoutOrientation,
467
516
  updatedAt: savedLayout.updatedAt ?? null,
468
517
  nodes: savedLayout.nodes ?? [],
469
518
  viewport: savedLayout.viewport ?? null,
470
519
  })
471
- : "null", [savedLayout]);
472
- const persistLayout = useCallback(async (nextNodes, viewport) => {
520
+ : "null", [savedLayout, savedLayoutOrientation]);
521
+ useEffect(() => {
522
+ setTransientOrientation(null);
523
+ pendingSaveOrientationRef.current = null;
524
+ }, [effectiveLayoutKey]);
525
+ useEffect(() => {
526
+ if (transientOrientation &&
527
+ savedLayoutOrientation &&
528
+ transientOrientation === savedLayoutOrientation) {
529
+ setTransientOrientation(null);
530
+ }
531
+ }, [savedLayoutOrientation, transientOrientation]);
532
+ const persistLayout = useCallback(async (nextNodes, viewport, nextOrientation) => {
473
533
  if (!canPersistLayout) {
474
534
  return;
475
535
  }
476
- const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport);
536
+ const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation);
477
537
  if (signature === lastSavedSignatureRef.current) {
478
538
  return;
479
539
  }
@@ -484,6 +544,7 @@ export function DependencyGraphView(props) {
484
544
  x: node.position.x,
485
545
  y: node.position.y,
486
546
  })),
547
+ orientation: nextOrientation,
487
548
  viewport: viewport == null
488
549
  ? null
489
550
  : {
@@ -500,6 +561,7 @@ export function DependencyGraphView(props) {
500
561
  projectId,
501
562
  nodes: layoutPayload.nodes,
502
563
  viewport: layoutPayload.viewport,
564
+ orientation: layoutPayload.orientation,
503
565
  });
504
566
  if (result.type !== "success") {
505
567
  toast.error(result.summary || "Failed to save graph layout");
@@ -511,6 +573,7 @@ export function DependencyGraphView(props) {
511
573
  lastSavedSignatureRef.current = signature;
512
574
  if (nextLayout) {
513
575
  const incomingSignature = JSON.stringify({
576
+ orientation: normalizeGraphOrientation(nextLayout.orientation ?? nextOrientation),
514
577
  updatedAt: nextLayout.updatedAt ?? null,
515
578
  nodes: nextLayout.nodes ?? [],
516
579
  viewport: nextLayout.viewport ?? null,
@@ -527,15 +590,16 @@ export function DependencyGraphView(props) {
527
590
  }, [
528
591
  canPersistLayout,
529
592
  effectiveLayoutKey,
593
+ effectiveOrientation,
530
594
  onLayoutSaved,
531
595
  onPersistLayout,
532
596
  projectId,
533
597
  ]);
534
- const queueLayoutSave = useCallback((nextNodes, viewport) => {
598
+ const queueLayoutSave = useCallback((nextNodes, viewport, nextOrientation = effectiveOrientation) => {
535
599
  if (!canPersistLayout) {
536
600
  return;
537
601
  }
538
- const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport);
602
+ const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation);
539
603
  if (signature === lastSavedSignatureRef.current) {
540
604
  return;
541
605
  }
@@ -544,14 +608,20 @@ export function DependencyGraphView(props) {
544
608
  saveTimeoutRef.current = null;
545
609
  }
546
610
  if (layoutSaveDebounceMs === 0) {
547
- void persistLayout(nextNodes, viewport);
611
+ void persistLayout(nextNodes, viewport, nextOrientation);
548
612
  return;
549
613
  }
550
614
  saveTimeoutRef.current = window.setTimeout(() => {
551
615
  saveTimeoutRef.current = null;
552
- void persistLayout(nextNodes, viewport);
616
+ void persistLayout(nextNodes, viewport, nextOrientation);
553
617
  }, layoutSaveDebounceMs);
554
- }, [canPersistLayout, effectiveLayoutKey, layoutSaveDebounceMs, persistLayout]);
618
+ }, [
619
+ canPersistLayout,
620
+ effectiveLayoutKey,
621
+ effectiveOrientation,
622
+ layoutSaveDebounceMs,
623
+ persistLayout,
624
+ ]);
555
625
  const restoreViewport = useCallback(() => {
556
626
  const instance = reactFlowRef.current;
557
627
  if (!instance || nodesRef.current.length === 0) {
@@ -563,7 +633,7 @@ export function DependencyGraphView(props) {
563
633
  skipNextMoveEndSaveRef.current = true;
564
634
  window.requestAnimationFrame(() => {
565
635
  reactFlowRef.current?.setViewport(viewport, { duration: 0 });
566
- lastSavedSignatureRef.current = createLayoutSignature(effectiveLayoutKey, nodesRef.current, viewport);
636
+ lastSavedSignatureRef.current = createLayoutSignature(effectiveLayoutKey, nodesRef.current, viewport, effectiveOrientation);
567
637
  });
568
638
  return;
569
639
  }
@@ -574,14 +644,15 @@ export function DependencyGraphView(props) {
574
644
  skipNextMoveEndSaveRef.current = true;
575
645
  window.requestAnimationFrame(() => {
576
646
  reactFlowRef.current?.fitView({ padding: 0.2 });
577
- if (!saveAfterViewportRestoreRef.current) {
647
+ if (!pendingSaveOrientationRef.current) {
578
648
  return;
579
649
  }
580
- saveAfterViewportRestoreRef.current = false;
650
+ const nextOrientation = pendingSaveOrientationRef.current;
651
+ pendingSaveOrientationRef.current = null;
581
652
  const viewport = reactFlowRef.current?.getViewport() ?? null;
582
- queueLayoutSave(nodesRef.current, viewport);
653
+ queueLayoutSave(nodesRef.current, viewport, nextOrientation);
583
654
  });
584
- }, [effectiveLayoutKey, queueLayoutSave]);
655
+ }, [effectiveLayoutKey, effectiveOrientation, queueLayoutSave]);
585
656
  useEffect(() => {
586
657
  const shouldApplySavedLayout = lastAppliedLayoutKeyRef.current !== effectiveLayoutKey ||
587
658
  lastAppliedLayoutSignatureRef.current !== savedLayoutSignature;
@@ -598,7 +669,7 @@ export function DependencyGraphView(props) {
598
669
  fitViewOnNextSyncRef.current = !savedLayout?.viewport;
599
670
  if (!savedLayout?.viewport) {
600
671
  lastSavedSignatureRef.current = savedLayout?.nodes?.length
601
- ? createLayoutSignature(effectiveLayoutKey, nextNodes, null)
672
+ ? createLayoutSignature(effectiveLayoutKey, nextNodes, null, effectiveOrientation)
602
673
  : null;
603
674
  }
604
675
  restoreViewport();
@@ -607,6 +678,7 @@ export function DependencyGraphView(props) {
607
678
  initialEdges,
608
679
  initialNodes,
609
680
  effectiveLayoutKey,
681
+ effectiveOrientation,
610
682
  restoreViewport,
611
683
  savedLayout,
612
684
  savedLayoutSignature,
@@ -623,18 +695,18 @@ export function DependencyGraphView(props) {
623
695
  }, []);
624
696
  const hasVisibleNodes = initialNodes.length > 0;
625
697
  const handleAutoLayout = useCallback(() => {
626
- const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, orientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask);
698
+ const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, effectiveOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask);
627
699
  setNodes(autoLayout.nodes);
628
700
  setEdges(autoLayout.edges);
629
701
  nodesRef.current = autoLayout.nodes;
630
702
  fitViewOnNextSyncRef.current = true;
631
- saveAfterViewportRestoreRef.current = true;
703
+ pendingSaveOrientationRef.current = effectiveOrientation;
632
704
  restoreViewport();
633
705
  }, [
634
706
  blockedTaskIds,
635
707
  dependencies,
708
+ effectiveOrientation,
636
709
  onSelect,
637
- orientation,
638
710
  showExecutionStateBadges,
639
711
  autoLayoutStrategy,
640
712
  onRemoveTask,
@@ -648,8 +720,14 @@ export function DependencyGraphView(props) {
648
720
  tasks,
649
721
  ]);
650
722
  const isValidConnection = useCallback((connection) => allowDependencyConnect
651
- ? isGraphConnectionValid(connection, tasks, dependencies)
652
- : false, [allowDependencyConnect, dependencies, tasks]);
723
+ ? isGraphConnectionValid(connection, tasks, dependencies, effectiveOrientation, autoLayoutStrategy)
724
+ : false, [
725
+ allowDependencyConnect,
726
+ autoLayoutStrategy,
727
+ dependencies,
728
+ effectiveOrientation,
729
+ tasks,
730
+ ]);
653
731
  const handleConnect = useCallback((connection) => {
654
732
  if (!allowDependencyConnect) {
655
733
  return;
@@ -658,24 +736,60 @@ export function DependencyGraphView(props) {
658
736
  if (!source || !target) {
659
737
  return;
660
738
  }
661
- if (!isGraphConnectionValid(connection, tasks, dependencies)) {
739
+ if (!isGraphConnectionValid(connection, tasks, dependencies, effectiveOrientation, autoLayoutStrategy)) {
662
740
  return;
663
741
  }
664
742
  onSelectTask(target);
665
- if (sourceHandle === "bottom") {
743
+ if (autoLayoutStrategy === "hierarchy" &&
744
+ sourceHandle === getHierarchyHandles(effectiveOrientation, "hierarchy").source) {
666
745
  onCreateChildRelationship?.(source, target);
667
746
  return;
668
747
  }
669
748
  onCreateDependency?.(source, target);
670
749
  }, [
671
750
  allowDependencyConnect,
751
+ autoLayoutStrategy,
672
752
  dependencies,
753
+ effectiveOrientation,
673
754
  onCreateChildRelationship,
674
755
  onCreateDependency,
675
756
  onSelectTask,
676
757
  tasks,
677
758
  ]);
678
- return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsxs("div", { className: "flex shrink-0 items-center justify-between gap-3 px-3 pt-2", children: [hasVisibleNodes && highlightBlockedTasks ? (_jsxs("div", { className: "flex items-center gap-2 text-[11px] text-slate-400", children: [_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "inline-block h-2 w-4 rounded-sm border border-orange-300 bg-orange-400" }), "Blocked by"] }), _jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "inline-block h-0.5 w-4 rounded-sm bg-slate-400" }), "Related"] }), _jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "inline-block h-0.5 w-4 rounded-sm bg-blue-500" }), "Child task"] })] })) : (_jsx("div", {})), hasVisibleNodes && canPersistLayout && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "h-8 px-3", onClick: handleAutoLayout, children: [_jsx(RotateCcw, { className: "mr-1.5 h-4 w-4" }), "Auto layout"] }))] }), _jsx("div", { className: "min-h-0 flex-1", children: !hasVisibleNodes ? (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsxs("div", { className: "flex max-w-sm flex-col items-center gap-3 text-center", children: [_jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-slate-100", children: _jsx(GitBranch, { className: "h-6 w-6 text-slate-400" }) }), _jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium text-slate-600", children: emptyStateTitle }), _jsx("p", { className: "mt-1 text-xs text-slate-400", children: emptyStateDescription })] })] }) })) : (_jsxs(ReactFlow, { nodes: nodes, edges: edges, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, onConnect: handleConnect, onPaneClick: () => {
759
+ const handleOrientationChange = useCallback((nextOrientation) => {
760
+ if (nextOrientation === effectiveOrientation) {
761
+ return;
762
+ }
763
+ setTransientOrientation(nextOrientation);
764
+ const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, nextOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask);
765
+ setNodes(autoLayout.nodes);
766
+ setEdges(autoLayout.edges);
767
+ nodesRef.current = autoLayout.nodes;
768
+ fitViewOnNextSyncRef.current = true;
769
+ pendingSaveOrientationRef.current = nextOrientation;
770
+ restoreViewport();
771
+ }, [
772
+ autoLayoutStrategy,
773
+ blockedTaskIds,
774
+ dependencies,
775
+ effectiveOrientation,
776
+ onAddChildTaskFromNode,
777
+ onAddDependentTaskFromNode,
778
+ onRemoveTask,
779
+ onSelect,
780
+ restoreViewport,
781
+ selectedDependencyId,
782
+ selectedTaskId,
783
+ setEdges,
784
+ setNodes,
785
+ showExecutionStateBadges,
786
+ tasks,
787
+ ]);
788
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsxs("div", { className: "flex shrink-0 items-center justify-between gap-3 px-3 pt-2", children: [hasVisibleNodes && highlightBlockedTasks ? (_jsxs("div", { className: "flex items-center gap-2 text-[11px] text-slate-400", children: [_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "inline-block h-0.5 w-4 rounded-sm bg-orange-400" }), "Blocked by"] }), _jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "inline-block h-0.5 w-4 rounded-sm bg-slate-400" }), "Related"] }), _jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "inline-block h-0.5 w-4 rounded-sm bg-blue-500" }), "Child task"] })] })) : (_jsx("div", {})), hasVisibleNodes && (_jsxs("div", { className: "flex items-center gap-2", children: [showOrientationToggle && (_jsxs("div", { className: "inline-flex rounded-md border border-slate-200 bg-white p-0.5", "data-testid": "dependency-graph-orientation-toggle", children: [_jsx("button", { type: "button", className: cn("rounded px-2.5 py-1 text-xs font-medium transition-colors", effectiveOrientation === "horizontal"
789
+ ? "bg-slate-900 text-white"
790
+ : "text-slate-500 hover:bg-slate-100 hover:text-slate-700"), "data-testid": "dependency-graph-orientation-horizontal", onClick: () => handleOrientationChange("horizontal"), children: "Horizontal" }), _jsx("button", { type: "button", className: cn("rounded px-2.5 py-1 text-xs font-medium transition-colors", effectiveOrientation === "vertical"
791
+ ? "bg-slate-900 text-white"
792
+ : "text-slate-500 hover:bg-slate-100 hover:text-slate-700"), "data-testid": "dependency-graph-orientation-vertical", onClick: () => handleOrientationChange("vertical"), children: "Vertical" })] })), canPersistLayout && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "h-8 px-3", onClick: handleAutoLayout, children: [_jsx(RotateCcw, { className: "mr-1.5 h-4 w-4" }), "Auto layout"] }))] }))] }), _jsx("div", { className: "min-h-0 flex-1", children: !hasVisibleNodes ? (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsxs("div", { className: "flex max-w-sm flex-col items-center gap-3 text-center", children: [_jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-slate-100", children: _jsx(GitBranch, { className: "h-6 w-6 text-slate-400" }) }), _jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium text-slate-600", children: emptyStateTitle }), _jsx("p", { className: "mt-1 text-xs text-slate-400", children: emptyStateDescription })] })] }) })) : (_jsxs(ReactFlow, { nodes: nodes, edges: edges, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, onConnect: handleConnect, onPaneClick: () => {
679
793
  onClearDependencySelection?.();
680
794
  }, onEdgeClick: (_event, edge) => {
681
795
  if (edge.data?.relationship === "dependency" &&
@@ -702,13 +816,16 @@ export function DependencyGraphView(props) {
702
816
  return;
703
817
  }
704
818
  queueLayoutSave(nodesRef.current, viewport);
705
- }, nodeTypes: nodeTypes, minZoom: 0.2, maxZoom: 2, proOptions: { hideAttribution: true }, nodesDraggable: canPersistLayout, nodesConnectable: allowDependencyConnect, isValidConnection: isValidConnection, elementsSelectable: true, children: [_jsx(Controls, { showInteractive: false, className: "rounded-lg! border-slate-200! shadow-md!" }), _jsx(MiniMap, { ...(miniMapWidth != null && miniMapHeight != null
819
+ }, nodeTypes: nodeTypes, minZoom: 0.2, maxZoom: 2, proOptions: { hideAttribution: true }, nodesDraggable: canPersistLayout, nodesConnectable: allowDependencyConnect, isValidConnection: isValidConnection, elementsSelectable: true, children: [_jsx(Controls, { showInteractive: false, className: "rounded-lg! border-slate-200! shadow-md!" }), showMiniMap ? (_jsx(MiniMap, { ...(miniMapWidth != null && miniMapHeight != null
706
820
  ? { width: miniMapWidth, height: miniMapHeight }
707
821
  : {}), nodeColor: (node) => {
708
- const status = node.data?.status;
822
+ const d = node.data;
823
+ if (d?.isBlocked)
824
+ return "#fecaca";
825
+ const status = d?.status;
709
826
  switch (status) {
710
827
  case "Done":
711
- return "#86efac";
828
+ return "#bbf7d0";
712
829
  case "InProgress":
713
830
  return "#93c5fd";
714
831
  case "Review":
@@ -718,6 +835,6 @@ export function DependencyGraphView(props) {
718
835
  default:
719
836
  return "#cbd5e1";
720
837
  }
721
- }, maskColor: "rgba(0, 0, 0, 0.08)", className: "rounded-lg! border-slate-200! shadow-md!" })] })) })] }));
838
+ }, maskColor: "rgba(0, 0, 0, 0.08)", className: "rounded-lg! border-slate-200! shadow-md!" })) : null] })) })] }));
722
839
  }
723
840
  //# sourceMappingURL=DependencyGraphView.js.map