@parhelia/core 0.1.12439 → 0.1.12451

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 (46) hide show
  1. package/dist/config/types.d.ts +4 -0
  2. package/dist/config/types.js.map +1 -1
  3. package/dist/editor/ConfirmationDialog.js +20 -4
  4. package/dist/editor/ConfirmationDialog.js.map +1 -1
  5. package/dist/editor/ContentTree.js +7 -2
  6. package/dist/editor/ContentTree.js.map +1 -1
  7. package/dist/editor/Editor.js.map +1 -1
  8. package/dist/editor/ai/AgentTerminal.js +65 -3
  9. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  10. package/dist/editor/ai/AiResponseMessage.js +5 -0
  11. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  12. package/dist/editor/ai/ToolCallDisplay.d.ts +2 -0
  13. package/dist/editor/ai/ToolCallDisplay.js +54 -11
  14. package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
  15. package/dist/editor/ai/types.d.ts +2 -0
  16. package/dist/editor/services/serviceHelper.js +5 -2
  17. package/dist/editor/services/serviceHelper.js.map +1 -1
  18. package/dist/editor/settings/panels/ProjectTemplateAgentPanel.d.ts +7 -1
  19. package/dist/editor/settings/panels/ProjectTemplateAgentPanel.js +3 -3
  20. package/dist/editor/settings/panels/ProjectTemplateAgentPanel.js.map +1 -1
  21. package/dist/editor/settings/panels/ProjectTemplatesPanel.js +143 -84
  22. package/dist/editor/settings/panels/ProjectTemplatesPanel.js.map +1 -1
  23. package/dist/editor/sidebar/NavigationPanelItem.js +1 -1
  24. package/dist/editor/sidebar/NavigationPanelItem.js.map +1 -1
  25. package/dist/editor/sidebar/WorkspaceButton.js +1 -1
  26. package/dist/editor/sidebar/WorkspaceButton.js.map +1 -1
  27. package/dist/revision.d.ts +2 -2
  28. package/dist/revision.js +2 -2
  29. package/dist/task-board/TaskBoardWorkspace.js +1 -1
  30. package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
  31. package/dist/task-board/components/CreateProjectDialog.js +2 -1
  32. package/dist/task-board/components/CreateProjectDialog.js.map +1 -1
  33. package/dist/task-board/types.d.ts +3 -0
  34. package/dist/task-board/utils/taskDependencyOrdering.d.ts +6 -0
  35. package/dist/task-board/utils/taskDependencyOrdering.js +138 -1
  36. package/dist/task-board/utils/taskDependencyOrdering.js.map +1 -1
  37. package/dist/task-board/views/DependencyGraphView.d.ts +5 -2
  38. package/dist/task-board/views/DependencyGraphView.js +174 -52
  39. package/dist/task-board/views/DependencyGraphView.js.map +1 -1
  40. package/dist/task-board/views/WizardView.js +26 -24
  41. package/dist/task-board/views/WizardView.js.map +1 -1
  42. package/dist/tour/Tour.js +63 -0
  43. package/dist/tour/Tour.js.map +1 -1
  44. package/dist/tour/default-tour.js +7 -0
  45. package/dist/tour/default-tour.js.map +1 -1
  46. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import "@xyflow/react/dist/style.css";
2
- import type { GraphLayoutSnapshot, TaskDependency, TaskItem, ProjectPermission } from "../types";
2
+ import type { GraphOrientation, GraphLayoutSnapshot, TaskDependency, TaskItem, ProjectPermission } from "../types";
3
3
  export declare function DependencyGraphView(props: {
4
4
  projectId?: string;
5
5
  layoutKey?: string;
@@ -19,8 +19,9 @@ export declare function DependencyGraphView(props: {
19
19
  showExecutionStateBadges?: boolean;
20
20
  emptyStateTitle?: string;
21
21
  emptyStateDescription?: string;
22
- orientation?: "vertical" | "horizontal";
22
+ orientation?: GraphOrientation;
23
23
  autoLayoutStrategy?: "allEdges" | "hierarchy";
24
+ showOrientationToggle?: boolean;
24
25
  /** Debounce before persisting layout (API or parent callback). Use 0 for immediate sync (e.g. project template draft). */
25
26
  layoutSaveDebounceMs?: number;
26
27
  /** When set, shows a create-dependent-task action near the right handle of each node. */
@@ -39,4 +40,6 @@ export declare function DependencyGraphView(props: {
39
40
  miniMapWidth?: number;
40
41
  /** MiniMap (overview) height in px. */
41
42
  miniMapHeight?: number;
43
+ /** When false, the overview minimap is not rendered (e.g. narrow mobile layouts). */
44
+ showMiniMap?: boolean;
42
45
  }): import("react/jsx-runtime").JSX.Element;
@@ -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";
@@ -46,6 +46,14 @@ 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
+ 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";
49
57
  const nodeSurfaceClass = data.isSelected
50
58
  ? "ring-primary border-primary/50 ring-2 bg-white"
51
59
  : data.isBlocked
@@ -56,19 +64,19 @@ function TaskGraphNode({ data }) {
56
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) => {
57
65
  event.stopPropagation();
58
66
  data.onRemoveTask?.(data.taskId);
59
- }, 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, "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) => {
60
68
  event.stopPropagation();
61
69
  data.onSelect(data.taskId);
62
70
  data.onAddDependentTaskFromNode?.(data.taskId);
63
71
  }, onMouseDown: (event) => {
64
72
  event.stopPropagation();
65
- }, 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) => {
66
74
  event.stopPropagation();
67
75
  data.onSelect(data.taskId);
68
76
  data.onAddChildTaskFromNode?.(data.taskId);
69
77
  }, onMouseDown: (event) => {
70
78
  event.stopPropagation();
71
- }, 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}` })] }));
72
80
  }
73
81
  const nodeTypes = {
74
82
  taskNode: TaskGraphNode,
@@ -76,13 +84,30 @@ const nodeTypes = {
76
84
  const HIERARCHY_CHILD_INDENT = 24;
77
85
  const HIERARCHY_CHILD_GAP = 32;
78
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
+ }
79
103
  /**
80
104
  * Compound layout for the hierarchy strategy: each parent–children group is
81
105
  * laid out vertically (parent on top, children stacked below with a slight
82
106
  * indent), and then the resulting subtree "super-nodes" are arranged
83
- * 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.
84
109
  */
85
- function layoutGraphHierarchy(nodes, edges) {
110
+ function layoutGraphHierarchy(nodes, edges, orientation) {
86
111
  if (nodes.length === 0)
87
112
  return { nodes, edges };
88
113
  const childrenOf = new Map();
@@ -127,7 +152,11 @@ function layoutGraphHierarchy(nodes, edges) {
127
152
  }
128
153
  }
129
154
  const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
130
- 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
+ });
131
160
  for (const [rootId, data] of subtrees) {
132
161
  g.setNode(rootId, { width: data.width, height: data.height });
133
162
  }
@@ -169,7 +198,7 @@ function layoutGraphHierarchy(nodes, edges) {
169
198
  function layoutGraph(nodes, edges, orientation, autoLayoutStrategy) {
170
199
  if (autoLayoutStrategy === "hierarchy" &&
171
200
  edges.some((e) => e.data?.relationship === "hierarchy")) {
172
- return layoutGraphHierarchy(nodes, edges);
201
+ return layoutGraphHierarchy(nodes, edges, orientation);
173
202
  }
174
203
  const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
175
204
  g.setGraph({
@@ -204,6 +233,8 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
204
233
  const childRelationshipKeys = new Set(visibleTasks
205
234
  .filter((task) => !!task.parentTaskId)
206
235
  .map((task) => `${task.parentTaskId}->${task.taskId}`));
236
+ const dependencyHandles = getDependencyHandles(orientation);
237
+ const hierarchyHandles = getHierarchyHandles(orientation, autoLayoutStrategy);
207
238
  const nodes = visibleTasks.map((task) => ({
208
239
  id: task.taskId,
209
240
  type: "taskNode",
@@ -220,16 +251,18 @@ function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, b
220
251
  isSelected: task.taskId === selectedTaskId,
221
252
  isBlocked: blockedTaskIds.has(task.taskId),
222
253
  orientation,
254
+ dependencySourceHandle: dependencyHandles.source,
255
+ hierarchySourceHandle: hierarchyHandles.source,
223
256
  onSelect,
224
257
  onAddDependentTaskFromNode,
225
258
  onAddChildTaskFromNode,
226
259
  onRemoveTask,
227
260
  },
228
261
  }));
229
- const depSourceHandle = orientation === "horizontal" ? "right" : "bottom";
230
- const depTargetHandle = orientation === "horizontal" ? "left" : "top";
231
- const hierSourceHandle = autoLayoutStrategy === "hierarchy" ? "bottom" : depSourceHandle;
232
- 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;
233
266
  const dependencyEdges = dependencies
234
267
  .filter((dep) => taskMap.has(dep.taskId) &&
235
268
  taskMap.has(dep.dependsOnTaskId) &&
@@ -323,12 +356,15 @@ function buildChildRelationshipKeys(tasks) {
323
356
  .filter((task) => !!task.parentTaskId)
324
357
  .map((task) => `${task.parentTaskId}->${task.taskId}`));
325
358
  }
326
- function isHierarchyConnectionValid(connection, tasks) {
359
+ function isHierarchyConnectionValid(connection, tasks, orientation, autoLayoutStrategy) {
360
+ if (autoLayoutStrategy !== "hierarchy") {
361
+ return false;
362
+ }
327
363
  const { source, sourceHandle, target, targetHandle } = connection;
328
364
  if (!source || !target || source === target) {
329
365
  return false;
330
366
  }
331
- if (sourceHandle !== "bottom") {
367
+ if (sourceHandle !== getHierarchyHandles(orientation, autoLayoutStrategy).source) {
332
368
  return false;
333
369
  }
334
370
  if (targetHandle && targetHandle !== "left" && targetHandle !== "top") {
@@ -348,12 +384,12 @@ function isHierarchyConnectionValid(connection, tasks) {
348
384
  }
349
385
  return !buildChildRelationshipKeys(tasks).has(`${source}->${target}`);
350
386
  }
351
- function isDependencyConnectionValid(connection, tasks, dependencies) {
387
+ function isDependencyConnectionValid(connection, tasks, dependencies, orientation) {
352
388
  const { source, sourceHandle, target, targetHandle } = connection;
353
389
  if (!source || !target || source === target) {
354
390
  return false;
355
391
  }
356
- if (sourceHandle !== "right") {
392
+ if (sourceHandle !== getDependencyHandles(orientation).source) {
357
393
  return false;
358
394
  }
359
395
  if (targetHandle && targetHandle !== "left" && targetHandle !== "top") {
@@ -368,15 +404,18 @@ function isDependencyConnectionValid(connection, tasks, dependencies) {
368
404
  }
369
405
  return !buildChildRelationshipKeys(tasks).has(`${source}->${target}`);
370
406
  }
371
- function isGraphConnectionValid(connection, tasks, dependencies) {
372
- if (connection.sourceHandle === "bottom") {
373
- 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);
374
412
  }
375
- return isDependencyConnectionValid(connection, tasks, dependencies);
413
+ return isDependencyConnectionValid(connection, tasks, dependencies, orientation);
376
414
  }
377
- function createLayoutSignature(layoutKey, nodes, viewport) {
415
+ function createLayoutSignature(layoutKey, nodes, viewport, orientation) {
378
416
  return JSON.stringify({
379
417
  layoutKey,
418
+ orientation,
380
419
  nodes: nodes
381
420
  .map((node) => ({
382
421
  taskId: node.id,
@@ -394,11 +433,17 @@ function createLayoutSignature(layoutKey, nodes, viewport) {
394
433
  });
395
434
  }
396
435
  export function DependencyGraphView(props) {
397
- 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;
398
437
  const canPersistLayout = typeof canPersistLayoutOverride === "boolean"
399
438
  ? canPersistLayoutOverride
400
439
  : permission === "Owner" || permission === "Editor";
401
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;
402
447
  const taskMap = useMemo(() => {
403
448
  const m = new Map();
404
449
  for (const t of tasks)
@@ -427,13 +472,13 @@ export function DependencyGraphView(props) {
427
472
  return blocked;
428
473
  }, [dependencies, highlightBlockedTasks, taskMap]);
429
474
  const onSelect = useCallback((taskId) => onSelectTask(taskId), [onSelectTask]);
430
- 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), [
431
476
  tasks,
432
477
  dependencies,
433
478
  selectedTaskId,
434
479
  selectedDependencyId,
435
480
  blockedTaskIds,
436
- orientation,
481
+ effectiveOrientation,
437
482
  showExecutionStateBadges,
438
483
  autoLayoutStrategy,
439
484
  onSelect,
@@ -444,6 +489,7 @@ export function DependencyGraphView(props) {
444
489
  const [nodes, setNodes, onNodesChangeBase] = useNodesState(initialNodes);
445
490
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
446
491
  const nodesRef = useRef(nodes);
492
+ const suppressNodeSelectionUntilRef = useRef(0);
447
493
  const onNodesChange = useCallback((changes) => {
448
494
  onNodesChangeBase(changes);
449
495
  const hasPositionChange = changes.some((c) => c.type === "position" && c.position);
@@ -461,22 +507,34 @@ export function DependencyGraphView(props) {
461
507
  const pendingViewportRef = useRef(null);
462
508
  const fitViewOnNextSyncRef = useRef(false);
463
509
  const skipNextMoveEndSaveRef = useRef(false);
464
- const saveAfterViewportRestoreRef = useRef(false);
510
+ const pendingSaveOrientationRef = useRef(null);
465
511
  const lastSavedSignatureRef = useRef(null);
466
512
  const lastAppliedLayoutKeyRef = useRef(null);
467
513
  const lastAppliedLayoutSignatureRef = useRef(null);
468
514
  const savedLayoutSignature = useMemo(() => savedLayout
469
515
  ? JSON.stringify({
516
+ orientation: savedLayoutOrientation,
470
517
  updatedAt: savedLayout.updatedAt ?? null,
471
518
  nodes: savedLayout.nodes ?? [],
472
519
  viewport: savedLayout.viewport ?? null,
473
520
  })
474
- : "null", [savedLayout]);
475
- const persistLayout = useCallback(async (nextNodes, viewport) => {
521
+ : "null", [savedLayout, savedLayoutOrientation]);
522
+ useEffect(() => {
523
+ setTransientOrientation(null);
524
+ pendingSaveOrientationRef.current = null;
525
+ }, [effectiveLayoutKey]);
526
+ useEffect(() => {
527
+ if (transientOrientation &&
528
+ savedLayoutOrientation &&
529
+ transientOrientation === savedLayoutOrientation) {
530
+ setTransientOrientation(null);
531
+ }
532
+ }, [savedLayoutOrientation, transientOrientation]);
533
+ const persistLayout = useCallback(async (nextNodes, viewport, nextOrientation) => {
476
534
  if (!canPersistLayout) {
477
535
  return;
478
536
  }
479
- const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport);
537
+ const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation);
480
538
  if (signature === lastSavedSignatureRef.current) {
481
539
  return;
482
540
  }
@@ -487,6 +545,7 @@ export function DependencyGraphView(props) {
487
545
  x: node.position.x,
488
546
  y: node.position.y,
489
547
  })),
548
+ orientation: nextOrientation,
490
549
  viewport: viewport == null
491
550
  ? null
492
551
  : {
@@ -503,6 +562,7 @@ export function DependencyGraphView(props) {
503
562
  projectId,
504
563
  nodes: layoutPayload.nodes,
505
564
  viewport: layoutPayload.viewport,
565
+ orientation: layoutPayload.orientation,
506
566
  });
507
567
  if (result.type !== "success") {
508
568
  toast.error(result.summary || "Failed to save graph layout");
@@ -514,6 +574,7 @@ export function DependencyGraphView(props) {
514
574
  lastSavedSignatureRef.current = signature;
515
575
  if (nextLayout) {
516
576
  const incomingSignature = JSON.stringify({
577
+ orientation: normalizeGraphOrientation(nextLayout.orientation ?? nextOrientation),
517
578
  updatedAt: nextLayout.updatedAt ?? null,
518
579
  nodes: nextLayout.nodes ?? [],
519
580
  viewport: nextLayout.viewport ?? null,
@@ -530,15 +591,16 @@ export function DependencyGraphView(props) {
530
591
  }, [
531
592
  canPersistLayout,
532
593
  effectiveLayoutKey,
594
+ effectiveOrientation,
533
595
  onLayoutSaved,
534
596
  onPersistLayout,
535
597
  projectId,
536
598
  ]);
537
- const queueLayoutSave = useCallback((nextNodes, viewport) => {
599
+ const queueLayoutSave = useCallback((nextNodes, viewport, nextOrientation = effectiveOrientation) => {
538
600
  if (!canPersistLayout) {
539
601
  return;
540
602
  }
541
- const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport);
603
+ const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation);
542
604
  if (signature === lastSavedSignatureRef.current) {
543
605
  return;
544
606
  }
@@ -547,14 +609,20 @@ export function DependencyGraphView(props) {
547
609
  saveTimeoutRef.current = null;
548
610
  }
549
611
  if (layoutSaveDebounceMs === 0) {
550
- void persistLayout(nextNodes, viewport);
612
+ void persistLayout(nextNodes, viewport, nextOrientation);
551
613
  return;
552
614
  }
553
615
  saveTimeoutRef.current = window.setTimeout(() => {
554
616
  saveTimeoutRef.current = null;
555
- void persistLayout(nextNodes, viewport);
617
+ void persistLayout(nextNodes, viewport, nextOrientation);
556
618
  }, layoutSaveDebounceMs);
557
- }, [canPersistLayout, effectiveLayoutKey, layoutSaveDebounceMs, persistLayout]);
619
+ }, [
620
+ canPersistLayout,
621
+ effectiveLayoutKey,
622
+ effectiveOrientation,
623
+ layoutSaveDebounceMs,
624
+ persistLayout,
625
+ ]);
558
626
  const restoreViewport = useCallback(() => {
559
627
  const instance = reactFlowRef.current;
560
628
  if (!instance || nodesRef.current.length === 0) {
@@ -566,7 +634,7 @@ export function DependencyGraphView(props) {
566
634
  skipNextMoveEndSaveRef.current = true;
567
635
  window.requestAnimationFrame(() => {
568
636
  reactFlowRef.current?.setViewport(viewport, { duration: 0 });
569
- lastSavedSignatureRef.current = createLayoutSignature(effectiveLayoutKey, nodesRef.current, viewport);
637
+ lastSavedSignatureRef.current = createLayoutSignature(effectiveLayoutKey, nodesRef.current, viewport, effectiveOrientation);
570
638
  });
571
639
  return;
572
640
  }
@@ -577,14 +645,15 @@ export function DependencyGraphView(props) {
577
645
  skipNextMoveEndSaveRef.current = true;
578
646
  window.requestAnimationFrame(() => {
579
647
  reactFlowRef.current?.fitView({ padding: 0.2 });
580
- if (!saveAfterViewportRestoreRef.current) {
648
+ if (!pendingSaveOrientationRef.current) {
581
649
  return;
582
650
  }
583
- saveAfterViewportRestoreRef.current = false;
651
+ const nextOrientation = pendingSaveOrientationRef.current;
652
+ pendingSaveOrientationRef.current = null;
584
653
  const viewport = reactFlowRef.current?.getViewport() ?? null;
585
- queueLayoutSave(nodesRef.current, viewport);
654
+ queueLayoutSave(nodesRef.current, viewport, nextOrientation);
586
655
  });
587
- }, [effectiveLayoutKey, queueLayoutSave]);
656
+ }, [effectiveLayoutKey, effectiveOrientation, queueLayoutSave]);
588
657
  useEffect(() => {
589
658
  const shouldApplySavedLayout = lastAppliedLayoutKeyRef.current !== effectiveLayoutKey ||
590
659
  lastAppliedLayoutSignatureRef.current !== savedLayoutSignature;
@@ -601,7 +670,7 @@ export function DependencyGraphView(props) {
601
670
  fitViewOnNextSyncRef.current = !savedLayout?.viewport;
602
671
  if (!savedLayout?.viewport) {
603
672
  lastSavedSignatureRef.current = savedLayout?.nodes?.length
604
- ? createLayoutSignature(effectiveLayoutKey, nextNodes, null)
673
+ ? createLayoutSignature(effectiveLayoutKey, nextNodes, null, effectiveOrientation)
605
674
  : null;
606
675
  }
607
676
  restoreViewport();
@@ -610,6 +679,7 @@ export function DependencyGraphView(props) {
610
679
  initialEdges,
611
680
  initialNodes,
612
681
  effectiveLayoutKey,
682
+ effectiveOrientation,
613
683
  restoreViewport,
614
684
  savedLayout,
615
685
  savedLayoutSignature,
@@ -626,18 +696,18 @@ export function DependencyGraphView(props) {
626
696
  }, []);
627
697
  const hasVisibleNodes = initialNodes.length > 0;
628
698
  const handleAutoLayout = useCallback(() => {
629
- const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, orientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask);
699
+ const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, effectiveOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask);
630
700
  setNodes(autoLayout.nodes);
631
701
  setEdges(autoLayout.edges);
632
702
  nodesRef.current = autoLayout.nodes;
633
703
  fitViewOnNextSyncRef.current = true;
634
- saveAfterViewportRestoreRef.current = true;
704
+ pendingSaveOrientationRef.current = effectiveOrientation;
635
705
  restoreViewport();
636
706
  }, [
637
707
  blockedTaskIds,
638
708
  dependencies,
709
+ effectiveOrientation,
639
710
  onSelect,
640
- orientation,
641
711
  showExecutionStateBadges,
642
712
  autoLayoutStrategy,
643
713
  onRemoveTask,
@@ -651,8 +721,14 @@ export function DependencyGraphView(props) {
651
721
  tasks,
652
722
  ]);
653
723
  const isValidConnection = useCallback((connection) => allowDependencyConnect
654
- ? isGraphConnectionValid(connection, tasks, dependencies)
655
- : false, [allowDependencyConnect, dependencies, tasks]);
724
+ ? isGraphConnectionValid(connection, tasks, dependencies, effectiveOrientation, autoLayoutStrategy)
725
+ : false, [
726
+ allowDependencyConnect,
727
+ autoLayoutStrategy,
728
+ dependencies,
729
+ effectiveOrientation,
730
+ tasks,
731
+ ]);
656
732
  const handleConnect = useCallback((connection) => {
657
733
  if (!allowDependencyConnect) {
658
734
  return;
@@ -661,24 +737,64 @@ export function DependencyGraphView(props) {
661
737
  if (!source || !target) {
662
738
  return;
663
739
  }
664
- if (!isGraphConnectionValid(connection, tasks, dependencies)) {
740
+ if (!isGraphConnectionValid(connection, tasks, dependencies, effectiveOrientation, autoLayoutStrategy)) {
665
741
  return;
666
742
  }
667
- onSelectTask(target);
668
- if (sourceHandle === "bottom") {
743
+ suppressNodeSelectionUntilRef.current = performance.now() + 150;
744
+ if (autoLayoutStrategy === "hierarchy" &&
745
+ sourceHandle === getHierarchyHandles(effectiveOrientation, "hierarchy").source) {
669
746
  onCreateChildRelationship?.(source, target);
670
- return;
671
747
  }
672
- onCreateDependency?.(source, target);
748
+ else {
749
+ onCreateDependency?.(source, target);
750
+ }
751
+ window.requestAnimationFrame(() => {
752
+ onSelectTask(target);
753
+ });
673
754
  }, [
674
755
  allowDependencyConnect,
756
+ autoLayoutStrategy,
675
757
  dependencies,
758
+ effectiveOrientation,
676
759
  onCreateChildRelationship,
677
760
  onCreateDependency,
678
761
  onSelectTask,
679
762
  tasks,
680
763
  ]);
681
- 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 && 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: () => {
764
+ const handleOrientationChange = useCallback((nextOrientation) => {
765
+ if (nextOrientation === effectiveOrientation) {
766
+ return;
767
+ }
768
+ setTransientOrientation(nextOrientation);
769
+ const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, nextOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask);
770
+ setNodes(autoLayout.nodes);
771
+ setEdges(autoLayout.edges);
772
+ nodesRef.current = autoLayout.nodes;
773
+ fitViewOnNextSyncRef.current = true;
774
+ pendingSaveOrientationRef.current = nextOrientation;
775
+ restoreViewport();
776
+ }, [
777
+ autoLayoutStrategy,
778
+ blockedTaskIds,
779
+ dependencies,
780
+ effectiveOrientation,
781
+ onAddChildTaskFromNode,
782
+ onAddDependentTaskFromNode,
783
+ onRemoveTask,
784
+ onSelect,
785
+ restoreViewport,
786
+ selectedDependencyId,
787
+ selectedTaskId,
788
+ setEdges,
789
+ setNodes,
790
+ showExecutionStateBadges,
791
+ tasks,
792
+ ]);
793
+ 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"
794
+ ? "bg-slate-900 text-white"
795
+ : "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"
796
+ ? "bg-slate-900 text-white"
797
+ : "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: () => {
682
798
  onClearDependencySelection?.();
683
799
  }, onEdgeClick: (_event, edge) => {
684
800
  if (edge.data?.relationship === "dependency" &&
@@ -687,9 +803,15 @@ export function DependencyGraphView(props) {
687
803
  onSelectDependency?.(edge.data.taskId, edge.data.dependsOnTaskId);
688
804
  }
689
805
  }, onNodeClick: (_event, node) => {
806
+ if (performance.now() < suppressNodeSelectionUntilRef.current) {
807
+ return;
808
+ }
690
809
  onClearDependencySelection?.();
691
810
  onSelectTask(node.id);
692
811
  }, onNodeDragStart: (_event, node) => {
812
+ if (performance.now() < suppressNodeSelectionUntilRef.current) {
813
+ return;
814
+ }
693
815
  onClearDependencySelection?.();
694
816
  onSelectTask(node.id);
695
817
  }, onInit: (instance) => {
@@ -705,7 +827,7 @@ export function DependencyGraphView(props) {
705
827
  return;
706
828
  }
707
829
  queueLayoutSave(nodesRef.current, viewport);
708
- }, 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
830
+ }, 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
709
831
  ? { width: miniMapWidth, height: miniMapHeight }
710
832
  : {}), nodeColor: (node) => {
711
833
  const d = node.data;
@@ -724,6 +846,6 @@ export function DependencyGraphView(props) {
724
846
  default:
725
847
  return "#cbd5e1";
726
848
  }
727
- }, maskColor: "rgba(0, 0, 0, 0.08)", className: "rounded-lg! border-slate-200! shadow-md!" })] })) })] }));
849
+ }, maskColor: "rgba(0, 0, 0, 0.08)", className: "rounded-lg! border-slate-200! shadow-md!" })) : null] })) })] }));
728
850
  }
729
851
  //# sourceMappingURL=DependencyGraphView.js.map