@parhelia/core 0.1.12368 → 0.1.12390

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 (225) hide show
  1. package/dist/agents-view/AgentsView.js +1 -1
  2. package/dist/agents-view/AgentsView.js.map +1 -1
  3. package/dist/components/ui/LanguageSelector.js +1 -3
  4. package/dist/components/ui/LanguageSelector.js.map +1 -1
  5. package/dist/components/ui/dialog.js +1 -1
  6. package/dist/components/ui/dialog.js.map +1 -1
  7. package/dist/config/config.js +53 -16
  8. package/dist/config/config.js.map +1 -1
  9. package/dist/config/notificationRoutes.js +10 -0
  10. package/dist/config/notificationRoutes.js.map +1 -1
  11. package/dist/config/types/workspace.d.ts +6 -0
  12. package/dist/config/types.d.ts +2 -5
  13. package/dist/editor/Editor.js +11 -4
  14. package/dist/editor/Editor.js.map +1 -1
  15. package/dist/editor/ai/AgentCostDisplay.d.ts +1 -0
  16. package/dist/editor/ai/AgentCostDisplay.js +1 -1
  17. package/dist/editor/ai/AgentCostDisplay.js.map +1 -1
  18. package/dist/editor/ai/AgentTerminal.js +144 -36
  19. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  20. package/dist/editor/ai/AgentTerminalStatusBar.d.ts +2 -0
  21. package/dist/editor/ai/AgentTerminalStatusBar.js +22 -37
  22. package/dist/editor/ai/AgentTerminalStatusBar.js.map +1 -1
  23. package/dist/editor/ai/AiResponseMessage.js +0 -1
  24. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  25. package/dist/editor/ai/ContentInspectorPopover.d.ts +17 -0
  26. package/dist/editor/ai/ContentInspectorPopover.js +136 -0
  27. package/dist/editor/ai/ContentInspectorPopover.js.map +1 -0
  28. package/dist/editor/ai/ContextInfoBar.js +55 -2
  29. package/dist/editor/ai/ContextInfoBar.js.map +1 -1
  30. package/dist/editor/ai/InlineAiDialog.js +1 -7
  31. package/dist/editor/ai/InlineAiDialog.js.map +1 -1
  32. package/dist/editor/ai/ToolCallDisplay.d.ts +4 -0
  33. package/dist/editor/ai/ToolCallDisplay.js +43 -8
  34. package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
  35. package/dist/editor/ai/dialogs/AgentDialogHandler.js +46 -22
  36. package/dist/editor/ai/dialogs/AgentDialogHandler.js.map +1 -1
  37. package/dist/editor/client/EditorShell.js +69 -26
  38. package/dist/editor/client/EditorShell.js.map +1 -1
  39. package/dist/editor/client/editContext.d.ts +3 -2
  40. package/dist/editor/client/hooks/useQuota.d.ts +2 -1
  41. package/dist/editor/client/hooks/useQuota.js.map +1 -1
  42. package/dist/editor/client/hooks/useSocketMessageHandler.js +28 -0
  43. package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
  44. package/dist/editor/client/operations.d.ts +1 -0
  45. package/dist/editor/client/operations.js +67 -15
  46. package/dist/editor/client/operations.js.map +1 -1
  47. package/dist/editor/client/waitForEditOperationTerminal.d.ts +11 -0
  48. package/dist/editor/client/waitForEditOperationTerminal.js +40 -0
  49. package/dist/editor/client/waitForEditOperationTerminal.js.map +1 -0
  50. package/dist/editor/commands/commands.d.ts +11 -1
  51. package/dist/editor/commands/commands.js +12 -1
  52. package/dist/editor/commands/commands.js.map +1 -1
  53. package/dist/editor/commands/customCommandConverter.d.ts +8 -1
  54. package/dist/editor/commands/customCommandConverter.js +33 -4
  55. package/dist/editor/commands/customCommandConverter.js.map +1 -1
  56. package/dist/editor/commands/handlers/uiActionHandlers.d.ts +6 -0
  57. package/dist/editor/commands/handlers/uiActionHandlers.js +84 -0
  58. package/dist/editor/commands/handlers/uiActionHandlers.js.map +1 -0
  59. package/dist/editor/commands/itemCommands.js +6 -2
  60. package/dist/editor/commands/itemCommands.js.map +1 -1
  61. package/dist/editor/commands/keyboardCommands.d.ts +10 -0
  62. package/dist/editor/commands/keyboardCommands.js +142 -0
  63. package/dist/editor/commands/keyboardCommands.js.map +1 -0
  64. package/dist/editor/commands/undo.d.ts +9 -15
  65. package/dist/editor/commands/undo.js +24 -0
  66. package/dist/editor/commands/undo.js.map +1 -1
  67. package/dist/editor/menubar/PageSelector.js +1 -3
  68. package/dist/editor/menubar/PageSelector.js.map +1 -1
  69. package/dist/editor/menubar/VersionSelector.js +1 -3
  70. package/dist/editor/menubar/VersionSelector.js.map +1 -1
  71. package/dist/editor/menubar/toolbar-sections/CustomCommandsToolbar.js +7 -36
  72. package/dist/editor/menubar/toolbar-sections/CustomCommandsToolbar.js.map +1 -1
  73. package/dist/editor/notifications/notificationRoutes.js +1 -0
  74. package/dist/editor/notifications/notificationRoutes.js.map +1 -1
  75. package/dist/editor/page-editor-chrome/InlineEditor.js +53 -36
  76. package/dist/editor/page-editor-chrome/InlineEditor.js.map +1 -1
  77. package/dist/editor/page-viewer/PageViewer.js +60 -6
  78. package/dist/editor/page-viewer/PageViewer.js.map +1 -1
  79. package/dist/editor/reviews/Comment.js +12 -10
  80. package/dist/editor/reviews/Comment.js.map +1 -1
  81. package/dist/editor/reviews/CommentDisplayPopover.js +1 -3
  82. package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -1
  83. package/dist/editor/reviews/PreviewInfo.js +1 -4
  84. package/dist/editor/reviews/PreviewInfo.js.map +1 -1
  85. package/dist/editor/reviews/reviewCommands.js +4 -1
  86. package/dist/editor/reviews/reviewCommands.js.map +1 -1
  87. package/dist/editor/reviews/useReviews.d.ts +2 -2
  88. package/dist/editor/reviews/useReviews.js +12 -30
  89. package/dist/editor/reviews/useReviews.js.map +1 -1
  90. package/dist/editor/services/agentService.d.ts +26 -0
  91. package/dist/editor/services/agentService.js +41 -0
  92. package/dist/editor/services/agentService.js.map +1 -1
  93. package/dist/editor/services/aiService.d.ts +7 -1
  94. package/dist/editor/services/aiService.js +13 -1
  95. package/dist/editor/services/aiService.js.map +1 -1
  96. package/dist/editor/services/notificationService.d.ts +1 -0
  97. package/dist/editor/services/notificationService.js +1 -0
  98. package/dist/editor/services/notificationService.js.map +1 -1
  99. package/dist/editor/services/reviewsService.d.ts +2 -5
  100. package/dist/editor/services/reviewsService.js +0 -10
  101. package/dist/editor/services/reviewsService.js.map +1 -1
  102. package/dist/editor/services/systemService.d.ts +2 -1
  103. package/dist/editor/services/systemService.js +3 -0
  104. package/dist/editor/services/systemService.js.map +1 -1
  105. package/dist/editor/settings/QuotaInfo.js +15 -7
  106. package/dist/editor/settings/QuotaInfo.js.map +1 -1
  107. package/dist/editor/settings/index/useIndexStatus.js +1 -1
  108. package/dist/editor/settings/index/useIndexStatus.js.map +1 -1
  109. package/dist/editor/settings/panels/AgentProfileConfigPanel.d.ts +10 -0
  110. package/dist/editor/settings/panels/AgentProfileConfigPanel.js +61 -0
  111. package/dist/editor/settings/panels/AgentProfileConfigPanel.js.map +1 -0
  112. package/dist/editor/settings/panels/AgentsPanel.d.ts +0 -4
  113. package/dist/editor/settings/panels/AgentsPanel.js +101 -109
  114. package/dist/editor/settings/panels/AgentsPanel.js.map +1 -1
  115. package/dist/editor/settings/panels/CreateAgentProfileDialog.d.ts +7 -0
  116. package/dist/editor/settings/panels/CreateAgentProfileDialog.js +48 -0
  117. package/dist/editor/settings/panels/CreateAgentProfileDialog.js.map +1 -0
  118. package/dist/editor/settings/panels/GroupedFieldConfigPanel.d.ts +33 -0
  119. package/dist/editor/settings/panels/GroupedFieldConfigPanel.js +91 -0
  120. package/dist/editor/settings/panels/GroupedFieldConfigPanel.js.map +1 -0
  121. package/dist/editor/settings/panels/ModelConfigPanel.d.ts +10 -0
  122. package/dist/editor/settings/panels/ModelConfigPanel.js +51 -0
  123. package/dist/editor/settings/panels/ModelConfigPanel.js.map +1 -0
  124. package/dist/editor/settings/panels/ModelsPanel.js +201 -70
  125. package/dist/editor/settings/panels/ModelsPanel.js.map +1 -1
  126. package/dist/editor/settings/panels/ProjectTemplateAgentPanel.d.ts +10 -0
  127. package/dist/editor/settings/panels/ProjectTemplateAgentPanel.js +46 -0
  128. package/dist/editor/settings/panels/ProjectTemplateAgentPanel.js.map +1 -0
  129. package/dist/editor/settings/panels/ProjectTemplatesPanel.d.ts +2 -0
  130. package/dist/editor/settings/panels/ProjectTemplatesPanel.js +1340 -0
  131. package/dist/editor/settings/panels/ProjectTemplatesPanel.js.map +1 -0
  132. package/dist/editor/settings/panels/ProviderConfigPanel.d.ts +10 -0
  133. package/dist/editor/settings/panels/ProviderConfigPanel.js +32 -0
  134. package/dist/editor/settings/panels/ProviderConfigPanel.js.map +1 -0
  135. package/dist/editor/settings/panels/ProvidersPanel.js +46 -4
  136. package/dist/editor/settings/panels/ProvidersPanel.js.map +1 -1
  137. package/dist/editor/settings/panels/SearchConfigPanel.js +3 -3
  138. package/dist/editor/settings/panels/SearchConfigPanel.js.map +1 -1
  139. package/dist/editor/settings/panels/index.d.ts +1 -2
  140. package/dist/editor/settings/panels/index.js +1 -2
  141. package/dist/editor/settings/panels/index.js.map +1 -1
  142. package/dist/editor/ui/ItemNameDialogNew.js +15 -9
  143. package/dist/editor/ui/ItemNameDialogNew.js.map +1 -1
  144. package/dist/editor/utils/keyboardNavigation.d.ts +6 -20
  145. package/dist/editor/utils/keyboardNavigation.js +48 -139
  146. package/dist/editor/utils/keyboardNavigation.js.map +1 -1
  147. package/dist/revision.d.ts +2 -2
  148. package/dist/revision.js +2 -2
  149. package/dist/setup/services/setupWizardService.d.ts +8 -10
  150. package/dist/setup/services/setupWizardService.js +4 -17
  151. package/dist/setup/services/setupWizardService.js.map +1 -1
  152. package/dist/splash-screen/ModernSplashScreen.js +100 -18
  153. package/dist/splash-screen/ModernSplashScreen.js.map +1 -1
  154. package/dist/splash-screen/ParheliaAssistantChat.js +3 -22
  155. package/dist/splash-screen/ParheliaAssistantChat.js.map +1 -1
  156. package/dist/task-board/TaskBoardWorkspace.js +70 -3
  157. package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
  158. package/dist/task-board/components/AssignAgentDialog.js +9 -4
  159. package/dist/task-board/components/AssignAgentDialog.js.map +1 -1
  160. package/dist/task-board/components/CreateProjectDialog.js +32 -34
  161. package/dist/task-board/components/CreateProjectDialog.js.map +1 -1
  162. package/dist/task-board/components/CreateTaskDialog.d.ts +2 -0
  163. package/dist/task-board/components/CreateTaskDialog.js +35 -11
  164. package/dist/task-board/components/CreateTaskDialog.js.map +1 -1
  165. package/dist/task-board/components/ProjectPropertiesPanel.js +4 -1
  166. package/dist/task-board/components/ProjectPropertiesPanel.js.map +1 -1
  167. package/dist/task-board/components/TaskAgentPanel.js +13 -4
  168. package/dist/task-board/components/TaskAgentPanel.js.map +1 -1
  169. package/dist/task-board/components/TaskAssigneePicker.d.ts +2 -2
  170. package/dist/task-board/components/TaskAssigneePicker.js +12 -4
  171. package/dist/task-board/components/TaskAssigneePicker.js.map +1 -1
  172. package/dist/task-board/components/TaskBoardProjectListSidebar.js.map +1 -1
  173. package/dist/task-board/components/TaskCard.js +2 -2
  174. package/dist/task-board/components/TaskCard.js.map +1 -1
  175. package/dist/task-board/components/TaskDetailPanel.js +2 -2
  176. package/dist/task-board/components/TaskDetailPanel.js.map +1 -1
  177. package/dist/task-board/components/TaskRow.js +2 -2
  178. package/dist/task-board/components/TaskRow.js.map +1 -1
  179. package/dist/task-board/components/WizardCommunicationCenter.js +10 -4
  180. package/dist/task-board/components/WizardCommunicationCenter.js.map +1 -1
  181. package/dist/task-board/services/taskService.d.ts +11 -2
  182. package/dist/task-board/services/taskService.js +20 -2
  183. package/dist/task-board/services/taskService.js.map +1 -1
  184. package/dist/task-board/types.d.ts +52 -7
  185. package/dist/task-board/views/DependencyGraphView.d.ts +31 -4
  186. package/dist/task-board/views/DependencyGraphView.js +383 -64
  187. package/dist/task-board/views/DependencyGraphView.js.map +1 -1
  188. package/dist/types.d.ts +23 -15
  189. package/package.json +7 -7
  190. package/dist/editor/settings/Setup.d.ts +0 -1
  191. package/dist/editor/settings/Setup.js +0 -211
  192. package/dist/editor/settings/Setup.js.map +0 -1
  193. package/dist/editor/settings/panels/DatabasePanel.d.ts +0 -6
  194. package/dist/editor/settings/panels/DatabasePanel.js +0 -50
  195. package/dist/editor/settings/panels/DatabasePanel.js.map +0 -1
  196. package/dist/editor/settings/setup-steps/AiSetupStep/EmbeddingsModelSection.d.ts +0 -2
  197. package/dist/editor/settings/setup-steps/AiSetupStep/EmbeddingsModelSection.js +0 -195
  198. package/dist/editor/settings/setup-steps/AiSetupStep/EmbeddingsModelSection.js.map +0 -1
  199. package/dist/editor/settings/setup-steps/AiSetupStep/index.d.ts +0 -2
  200. package/dist/editor/settings/setup-steps/AiSetupStep/index.js +0 -21
  201. package/dist/editor/settings/setup-steps/AiSetupStep/index.js.map +0 -1
  202. package/dist/editor/settings/setup-steps/AiSetupStep/provider/ProviderSection.d.ts +0 -1
  203. package/dist/editor/settings/setup-steps/AiSetupStep/provider/ProviderSection.js +0 -233
  204. package/dist/editor/settings/setup-steps/AiSetupStep/provider/ProviderSection.js.map +0 -1
  205. package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersList.d.ts +0 -15
  206. package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersList.js +0 -14
  207. package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersList.js.map +0 -1
  208. package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersSection.d.ts +0 -1
  209. package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersSection.js +0 -94
  210. package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersSection.js.map +0 -1
  211. package/dist/editor/settings/setup-steps/AiSetupStep/types.d.ts +0 -1
  212. package/dist/editor/settings/setup-steps/AiSetupStep/types.js +0 -2
  213. package/dist/editor/settings/setup-steps/AiSetupStep/types.js.map +0 -1
  214. package/dist/editor/settings/setup-steps/AiSetupStep/utils.d.ts +0 -5
  215. package/dist/editor/settings/setup-steps/AiSetupStep/utils.js +0 -44
  216. package/dist/editor/settings/setup-steps/AiSetupStep/utils.js.map +0 -1
  217. package/dist/editor/settings/setup-steps/IndexSetupStep.d.ts +0 -2
  218. package/dist/editor/settings/setup-steps/IndexSetupStep.js +0 -36
  219. package/dist/editor/settings/setup-steps/IndexSetupStep.js.map +0 -1
  220. package/dist/editor/settings/setup-steps/SettingsSetupStep.d.ts +0 -2
  221. package/dist/editor/settings/setup-steps/SettingsSetupStep.js +0 -111
  222. package/dist/editor/settings/setup-steps/SettingsSetupStep.js.map +0 -1
  223. package/dist/editor/settings/setup-steps/SetupOverview.d.ts +0 -14
  224. package/dist/editor/settings/setup-steps/SetupOverview.js +0 -38
  225. package/dist/editor/settings/setup-steps/SetupOverview.js.map +0 -1
@@ -5,13 +5,19 @@ import Dagre from "@dagrejs/dagre";
5
5
  import "@xyflow/react/dist/style.css";
6
6
  import { toast } from "sonner";
7
7
  import { normalizeTaskStatus, getTaskStatusLabel } from "../taskStatus";
8
+ import { getTaskExecutionDisplayFromTask } from "../taskExecutionStatus";
8
9
  import { cn } from "../../lib/utils";
9
10
  import { saveGraphLayout } from "../services/taskService";
10
11
  import { Button } from "../../components/ui/button";
11
- import { Circle, PlayCircle, Clock3, Eye, CheckCircle2, ArrowUp, ArrowDown, Minus, Flame, GitBranch, RotateCcw, } from "lucide-react";
12
+ import { Badge } from "../../components/ui/badge";
13
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../../components/ui/tooltip";
14
+ import { Circle, PlayCircle, Clock3, Eye, CheckCircle2, ArrowUp, ArrowDown, Minus, Flame, GitBranch, RotateCcw, Loader2, Plus, Trash2, } from "lucide-react";
12
15
  const NODE_WIDTH = 220;
13
16
  const NODE_HEIGHT = 80;
14
17
  const SAVE_DEBOUNCE_MS = 500;
18
+ const EDGE_BLOCKED_BY = "#f97316";
19
+ const EDGE_CHILD_TASK = "#0d9488";
20
+ const EDGE_RELATED = "#94a3b8";
15
21
  const STATUS_STYLES = {
16
22
  Todo: { bg: "bg-slate-100", text: "text-slate-600", icon: Circle },
17
23
  InProgress: { bg: "bg-blue-100", text: "text-blue-700", icon: PlayCircle },
@@ -25,22 +31,147 @@ const PRIORITY_CONFIG = {
25
31
  Medium: { icon: Minus, color: "text-yellow-500", label: "Medium" },
26
32
  Low: { icon: ArrowDown, color: "text-slate-400", label: "Low" },
27
33
  };
34
+ const HANDLE_CLASS = "bg-slate-300! border-slate-400! w-2! h-2!";
28
35
  function TaskGraphNode({ data }) {
29
36
  const status = data.status;
30
37
  const statusStyle = STATUS_STYLES[status] || STATUS_STYLES.Todo;
31
38
  const StatusIcon = statusStyle.icon;
39
+ const executionDisplay = data.showExecutionStateBadge
40
+ ? getTaskExecutionDisplayFromTask({
41
+ taskStatus: status,
42
+ executionState: data.executionState,
43
+ isBlocked: data.isBlocked,
44
+ })
45
+ : null;
32
46
  const priorityCfg = data.priority ? PRIORITY_CONFIG[data.priority] : null;
33
47
  const PriorityIcon = priorityCfg?.icon;
34
- return (_jsxs("div", { className: cn("cursor-pointer rounded-lg border bg-white px-3 py-2 shadow-sm transition-all", data.isSelected
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
35
50
  ? "ring-primary border-primary/50 ring-2"
36
- : "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 }, onClick: () => data.onSelect(data.taskId), children: [_jsx(Handle, { type: "target", position: Position.Top, className: "bg-slate-300! border-slate-400! w-2! h-2!" }), _jsxs("div", { className: "flex 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) }))] }), _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: [_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] }))] }), _jsx(Handle, { type: "source", position: Position.Bottom, className: "bg-slate-300! border-slate-400! w-2! h-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) => {
52
+ event.stopPropagation();
53
+ 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) => {
55
+ event.stopPropagation();
56
+ data.onSelect(data.taskId);
57
+ data.onAddDependentTaskFromNode?.(data.taskId);
58
+ }, onMouseDown: (event) => {
59
+ 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) => {
61
+ event.stopPropagation();
62
+ data.onSelect(data.taskId);
63
+ data.onAddChildTaskFromNode?.(data.taskId);
64
+ }, onMouseDown: (event) => {
65
+ 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}` })] }));
37
67
  }
38
68
  const nodeTypes = {
39
69
  taskNode: TaskGraphNode,
40
70
  };
41
- function layoutGraph(nodes, edges) {
71
+ const HIERARCHY_CHILD_INDENT = 24;
72
+ const HIERARCHY_CHILD_GAP = 32;
73
+ const HIERARCHY_SUBTREE_GAP = 80;
74
+ /**
75
+ * Compound layout for the hierarchy strategy: each parent–children group is
76
+ * laid out vertically (parent on top, children stacked below with a slight
77
+ * indent), and then the resulting subtree "super-nodes" are arranged
78
+ * left-to-right via Dagre using the dependency edges between them.
79
+ */
80
+ function layoutGraphHierarchy(nodes, edges) {
81
+ if (nodes.length === 0)
82
+ return { nodes, edges };
83
+ const childrenOf = new Map();
84
+ const parentOf = new Map();
85
+ for (const edge of edges) {
86
+ if (edge.data?.relationship !== "hierarchy")
87
+ continue;
88
+ parentOf.set(edge.target, edge.source);
89
+ const kids = childrenOf.get(edge.source) ?? [];
90
+ kids.push(edge.target);
91
+ childrenOf.set(edge.source, kids);
92
+ }
93
+ const roots = nodes.filter((n) => !parentOf.has(n.id));
94
+ function measureSubtree(nodeId, x, y, positions) {
95
+ positions.set(nodeId, { x, y });
96
+ const children = childrenOf.get(nodeId) ?? [];
97
+ if (children.length === 0) {
98
+ return { width: NODE_WIDTH, height: NODE_HEIGHT };
99
+ }
100
+ let childY = y + NODE_HEIGHT + HIERARCHY_CHILD_GAP;
101
+ let maxRight = x + NODE_WIDTH;
102
+ for (const childId of children) {
103
+ const result = measureSubtree(childId, x + HIERARCHY_CHILD_INDENT, childY, positions);
104
+ childY += result.height + HIERARCHY_CHILD_GAP;
105
+ maxRight = Math.max(maxRight, x + HIERARCHY_CHILD_INDENT + result.width);
106
+ }
107
+ return {
108
+ width: maxRight - x,
109
+ height: childY - HIERARCHY_CHILD_GAP - y,
110
+ };
111
+ }
112
+ const subtrees = new Map();
113
+ for (const root of roots) {
114
+ const positions = new Map();
115
+ const dims = measureSubtree(root.id, 0, 0, positions);
116
+ subtrees.set(root.id, { positions, ...dims });
117
+ }
118
+ const nodeToRoot = new Map();
119
+ for (const [rootId, data] of subtrees) {
120
+ for (const nodeId of data.positions.keys()) {
121
+ nodeToRoot.set(nodeId, rootId);
122
+ }
123
+ }
42
124
  const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
43
- g.setGraph({ rankdir: "TB", nodesep: 40, ranksep: 60 });
125
+ g.setGraph({ rankdir: "LR", nodesep: 40, ranksep: HIERARCHY_SUBTREE_GAP });
126
+ for (const [rootId, data] of subtrees) {
127
+ g.setNode(rootId, { width: data.width, height: data.height });
128
+ }
129
+ const addedSuperEdges = new Set();
130
+ for (const edge of edges) {
131
+ if (edge.data?.relationship !== "dependency")
132
+ continue;
133
+ const srcRoot = nodeToRoot.get(edge.source);
134
+ const tgtRoot = nodeToRoot.get(edge.target);
135
+ if (srcRoot && tgtRoot && srcRoot !== tgtRoot) {
136
+ const key = `${srcRoot}->${tgtRoot}`;
137
+ if (!addedSuperEdges.has(key)) {
138
+ addedSuperEdges.add(key);
139
+ g.setEdge(srcRoot, tgtRoot);
140
+ }
141
+ }
142
+ }
143
+ Dagre.layout(g);
144
+ const finalPositions = new Map();
145
+ for (const [rootId, data] of subtrees) {
146
+ const superNode = g.node(rootId);
147
+ const offsetX = superNode.x - data.width / 2;
148
+ const offsetY = superNode.y - data.height / 2;
149
+ for (const [nodeId, localPos] of data.positions) {
150
+ finalPositions.set(nodeId, {
151
+ x: offsetX + localPos.x,
152
+ y: offsetY + localPos.y,
153
+ });
154
+ }
155
+ }
156
+ return {
157
+ nodes: nodes.map((node) => ({
158
+ ...node,
159
+ position: finalPositions.get(node.id) ?? node.position,
160
+ })),
161
+ edges,
162
+ };
163
+ }
164
+ function layoutGraph(nodes, edges, orientation, autoLayoutStrategy) {
165
+ if (autoLayoutStrategy === "hierarchy" &&
166
+ edges.some((e) => e.data?.relationship === "hierarchy")) {
167
+ return layoutGraphHierarchy(nodes, edges);
168
+ }
169
+ const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
170
+ g.setGraph({
171
+ rankdir: orientation === "horizontal" ? "LR" : "TB",
172
+ nodesep: 40,
173
+ ranksep: 60,
174
+ });
44
175
  for (const node of nodes) {
45
176
  g.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
46
177
  }
@@ -60,11 +191,14 @@ function layoutGraph(nodes, edges) {
60
191
  });
61
192
  return { nodes: layoutedNodes, edges };
62
193
  }
63
- function buildGraph(tasks, dependencies, selectedTaskId, blockedTaskIds, onSelect) {
194
+ function buildGraph(tasks, dependencies, selectedTaskId, blockedTaskIds, orientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask) {
64
195
  const taskMap = new Map();
65
196
  for (const t of tasks)
66
197
  taskMap.set(t.taskId, t);
67
198
  const visibleTasks = tasks;
199
+ const childRelationshipKeys = new Set(visibleTasks
200
+ .filter((task) => !!task.parentTaskId)
201
+ .map((task) => `${task.parentTaskId}->${task.taskId}`));
68
202
  const nodes = visibleTasks.map((task) => ({
69
203
  id: task.taskId,
70
204
  type: "taskNode",
@@ -74,41 +208,85 @@ function buildGraph(tasks, dependencies, selectedTaskId, blockedTaskIds, onSelec
74
208
  taskKey: task.taskKey ?? null,
75
209
  title: task.title,
76
210
  status: normalizeTaskStatus(task.status, task.executionState),
211
+ executionState: task.executionState,
212
+ showExecutionStateBadge: showExecutionStateBadges,
77
213
  priority: task.priority ?? null,
78
214
  assigneeDisplayName: task.assigneeDisplayName ?? null,
79
215
  isSelected: task.taskId === selectedTaskId,
80
216
  isBlocked: blockedTaskIds.has(task.taskId),
217
+ orientation,
81
218
  onSelect,
219
+ onAddDependentTaskFromNode,
220
+ onAddChildTaskFromNode,
221
+ onRemoveTask,
82
222
  },
83
223
  }));
84
- const edges = dependencies
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;
228
+ const dependencyEdges = dependencies
85
229
  .filter((dep) => taskMap.has(dep.taskId) &&
86
- taskMap.has(dep.dependsOnTaskId))
230
+ taskMap.has(dep.dependsOnTaskId) &&
231
+ !childRelationshipKeys.has(`${dep.dependsOnTaskId}->${dep.taskId}`))
87
232
  .map((dep) => {
88
233
  const isBlockedBy = dep.dependencyType === "BlockedBy";
234
+ const strokeColor = isBlockedBy
235
+ ? EDGE_BLOCKED_BY
236
+ : EDGE_RELATED;
89
237
  return {
90
238
  id: dep.dependencyId,
91
239
  source: dep.dependsOnTaskId,
92
240
  target: dep.taskId,
93
- animated: isBlockedBy,
241
+ sourceHandle: depSourceHandle,
242
+ targetHandle: depTargetHandle,
243
+ type: "smoothstep",
244
+ animated: false,
94
245
  style: {
95
- stroke: isBlockedBy ? "#f97316" : "#94a3b8",
246
+ stroke: strokeColor,
96
247
  strokeWidth: isBlockedBy ? 2 : 1.5,
97
- strokeDasharray: isBlockedBy ? undefined : "5 5",
98
248
  },
99
249
  markerEnd: {
100
250
  type: MarkerType.ArrowClosed,
101
- color: isBlockedBy ? "#f97316" : "#94a3b8",
251
+ color: strokeColor,
102
252
  width: 16,
103
253
  height: 16,
104
254
  },
105
255
  label: isBlockedBy ? undefined : "related",
106
- labelStyle: { fontSize: 10, fill: "#94a3b8" },
256
+ labelStyle: { fontSize: 10, fill: EDGE_RELATED },
257
+ data: {
258
+ relationship: "dependency",
259
+ },
107
260
  };
108
261
  });
262
+ const hierarchyEdges = visibleTasks
263
+ .filter((task) => !!task.parentTaskId &&
264
+ taskMap.has(task.parentTaskId))
265
+ .map((task) => ({
266
+ id: `parent:${task.parentTaskId}:${task.taskId}`,
267
+ source: task.parentTaskId,
268
+ target: task.taskId,
269
+ sourceHandle: hierSourceHandle,
270
+ targetHandle: hierTargetHandle,
271
+ type: "smoothstep",
272
+ style: {
273
+ stroke: "#3b82f6",
274
+ strokeWidth: 1.5,
275
+ },
276
+ markerEnd: {
277
+ type: MarkerType.ArrowClosed,
278
+ color: "#3b82f6",
279
+ width: 16,
280
+ height: 16,
281
+ },
282
+ data: {
283
+ relationship: "hierarchy",
284
+ },
285
+ }));
286
+ const edges = [...dependencyEdges, ...hierarchyEdges];
109
287
  if (nodes.length === 0)
110
288
  return { nodes: [], edges: [] };
111
- return layoutGraph(nodes, edges);
289
+ return layoutGraph(nodes, edges, orientation, autoLayoutStrategy);
112
290
  }
113
291
  function applySavedLayout(nodes, savedLayout) {
114
292
  if (!savedLayout?.nodes?.length) {
@@ -133,9 +311,65 @@ function preserveNodePositions(previousNodes, nextNodes) {
133
311
  position: previousPositions.get(node.id) ?? node.position,
134
312
  }));
135
313
  }
136
- function createLayoutSignature(projectId, nodes, viewport) {
314
+ function buildChildRelationshipKeys(tasks) {
315
+ return new Set(tasks
316
+ .filter((task) => !!task.parentTaskId)
317
+ .map((task) => `${task.parentTaskId}->${task.taskId}`));
318
+ }
319
+ function isHierarchyConnectionValid(connection, tasks) {
320
+ const { source, sourceHandle, target, targetHandle } = connection;
321
+ if (!source || !target || source === target) {
322
+ return false;
323
+ }
324
+ if (sourceHandle !== "bottom") {
325
+ return false;
326
+ }
327
+ if (targetHandle && targetHandle !== "left" && targetHandle !== "top") {
328
+ return false;
329
+ }
330
+ const validTaskIds = new Set(tasks.map((task) => task.taskId));
331
+ if (!validTaskIds.has(source) || !validTaskIds.has(target)) {
332
+ return false;
333
+ }
334
+ const parentByTaskId = new Map(tasks.map((task) => [task.taskId, task.parentTaskId ?? null]));
335
+ let ancestorTaskId = parentByTaskId.get(source) ?? null;
336
+ while (ancestorTaskId) {
337
+ if (ancestorTaskId === target) {
338
+ return false;
339
+ }
340
+ ancestorTaskId = parentByTaskId.get(ancestorTaskId) ?? null;
341
+ }
342
+ return !buildChildRelationshipKeys(tasks).has(`${source}->${target}`);
343
+ }
344
+ function isDependencyConnectionValid(connection, tasks, dependencies) {
345
+ const { source, sourceHandle, target, targetHandle } = connection;
346
+ if (!source || !target || source === target) {
347
+ return false;
348
+ }
349
+ if (sourceHandle !== "right") {
350
+ return false;
351
+ }
352
+ if (targetHandle && targetHandle !== "left" && targetHandle !== "top") {
353
+ return false;
354
+ }
355
+ const validTaskIds = new Set(tasks.map((task) => task.taskId));
356
+ if (!validTaskIds.has(source) || !validTaskIds.has(target)) {
357
+ return false;
358
+ }
359
+ if (dependencies.some((dependency) => dependency.dependsOnTaskId === source && dependency.taskId === target)) {
360
+ return false;
361
+ }
362
+ return !buildChildRelationshipKeys(tasks).has(`${source}->${target}`);
363
+ }
364
+ function isGraphConnectionValid(connection, tasks, dependencies) {
365
+ if (connection.sourceHandle === "bottom") {
366
+ return isHierarchyConnectionValid(connection, tasks);
367
+ }
368
+ return isDependencyConnectionValid(connection, tasks, dependencies);
369
+ }
370
+ function createLayoutSignature(layoutKey, nodes, viewport) {
137
371
  return JSON.stringify({
138
- projectId,
372
+ layoutKey,
139
373
  nodes: nodes
140
374
  .map((node) => ({
141
375
  taskId: node.id,
@@ -153,8 +387,11 @@ function createLayoutSignature(projectId, nodes, viewport) {
153
387
  });
154
388
  }
155
389
  export function DependencyGraphView(props) {
156
- const { projectId, tasks, dependencies, savedLayout, selectedTaskId, onSelectTask, permission, onLayoutSaved, } = props;
157
- const canPersistLayout = permission === "Owner" || permission === "Editor";
390
+ const { projectId, layoutKey, tasks, dependencies, savedLayout, selectedTaskId, onSelectTask, 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;
391
+ const canPersistLayout = typeof canPersistLayoutOverride === "boolean"
392
+ ? canPersistLayoutOverride
393
+ : permission === "Owner" || permission === "Editor";
394
+ const effectiveLayoutKey = layoutKey ?? projectId ?? "graph";
158
395
  const taskMap = useMemo(() => {
159
396
  const m = new Map();
160
397
  for (const t of tasks)
@@ -162,6 +399,9 @@ export function DependencyGraphView(props) {
162
399
  return m;
163
400
  }, [tasks]);
164
401
  const blockedTaskIds = useMemo(() => {
402
+ if (!highlightBlockedTasks) {
403
+ return new Set();
404
+ }
165
405
  const blocked = new Set();
166
406
  for (const dep of dependencies) {
167
407
  if (dep.dependencyType !== "BlockedBy")
@@ -173,9 +413,21 @@ export function DependencyGraphView(props) {
173
413
  }
174
414
  }
175
415
  return blocked;
176
- }, [dependencies, taskMap]);
416
+ }, [dependencies, highlightBlockedTasks, taskMap]);
177
417
  const onSelect = useCallback((taskId) => onSelectTask(taskId), [onSelectTask]);
178
- const { nodes: initialNodes, edges: initialEdges } = useMemo(() => buildGraph(tasks, dependencies, selectedTaskId, blockedTaskIds, onSelect), [tasks, dependencies, selectedTaskId, blockedTaskIds, onSelect]);
418
+ const { nodes: initialNodes, edges: initialEdges } = useMemo(() => buildGraph(tasks, dependencies, selectedTaskId, blockedTaskIds, orientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask), [
419
+ tasks,
420
+ dependencies,
421
+ selectedTaskId,
422
+ blockedTaskIds,
423
+ orientation,
424
+ showExecutionStateBadges,
425
+ autoLayoutStrategy,
426
+ onSelect,
427
+ onAddDependentTaskFromNode,
428
+ onAddChildTaskFromNode,
429
+ onRemoveTask,
430
+ ]);
179
431
  const [nodes, setNodes, onNodesChangeBase] = useNodesState(initialNodes);
180
432
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
181
433
  const nodesRef = useRef(nodes);
@@ -198,7 +450,7 @@ export function DependencyGraphView(props) {
198
450
  const skipNextMoveEndSaveRef = useRef(false);
199
451
  const saveAfterViewportRestoreRef = useRef(false);
200
452
  const lastSavedSignatureRef = useRef(null);
201
- const lastAppliedProjectIdRef = useRef(null);
453
+ const lastAppliedLayoutKeyRef = useRef(null);
202
454
  const lastAppliedLayoutSignatureRef = useRef(null);
203
455
  const savedLayoutSignature = useMemo(() => savedLayout
204
456
  ? JSON.stringify({
@@ -211,56 +463,85 @@ export function DependencyGraphView(props) {
211
463
  if (!canPersistLayout) {
212
464
  return;
213
465
  }
214
- const signature = createLayoutSignature(projectId, nextNodes, viewport);
466
+ const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport);
215
467
  if (signature === lastSavedSignatureRef.current) {
216
468
  return;
217
469
  }
218
- const result = await saveGraphLayout({
219
- projectId,
220
- nodes: nextNodes.map((node) => ({
221
- taskId: node.id,
222
- x: node.position.x,
223
- y: node.position.y,
224
- })),
225
- viewport: viewport == null
226
- ? null
227
- : {
228
- x: viewport.x,
229
- y: viewport.y,
230
- zoom: viewport.zoom,
231
- },
232
- });
233
- if (result.type !== "success") {
234
- toast.error(result.summary || "Failed to save graph layout");
235
- return;
470
+ try {
471
+ const layoutPayload = {
472
+ nodes: nextNodes.map((node) => ({
473
+ taskId: node.id,
474
+ x: node.position.x,
475
+ y: node.position.y,
476
+ })),
477
+ viewport: viewport == null
478
+ ? null
479
+ : {
480
+ x: viewport.x,
481
+ y: viewport.y,
482
+ zoom: viewport.zoom,
483
+ },
484
+ };
485
+ const nextLayout = onPersistLayout
486
+ ? await onPersistLayout(layoutPayload)
487
+ : projectId
488
+ ? await (async () => {
489
+ const result = await saveGraphLayout({
490
+ projectId,
491
+ nodes: layoutPayload.nodes,
492
+ viewport: layoutPayload.viewport,
493
+ });
494
+ if (result.type !== "success") {
495
+ toast.error(result.summary || "Failed to save graph layout");
496
+ return null;
497
+ }
498
+ return result.data ?? null;
499
+ })()
500
+ : layoutPayload;
501
+ lastSavedSignatureRef.current = signature;
502
+ if (nextLayout) {
503
+ const incomingSignature = JSON.stringify({
504
+ updatedAt: nextLayout.updatedAt ?? null,
505
+ nodes: nextLayout.nodes ?? [],
506
+ viewport: nextLayout.viewport ?? null,
507
+ });
508
+ lastAppliedLayoutSignatureRef.current = incomingSignature;
509
+ }
510
+ onLayoutSaved?.(nextLayout ?? null);
236
511
  }
237
- lastSavedSignatureRef.current = signature;
238
- if (result.data) {
239
- const incomingSignature = JSON.stringify({
240
- updatedAt: result.data.updatedAt ?? null,
241
- nodes: result.data.nodes ?? [],
242
- viewport: result.data.viewport ?? null,
243
- });
244
- lastAppliedLayoutSignatureRef.current = incomingSignature;
512
+ catch (error) {
513
+ toast.error(error instanceof Error
514
+ ? error.message
515
+ : "Failed to save graph layout");
245
516
  }
246
- onLayoutSaved?.(result.data ?? null);
247
- }, [canPersistLayout, onLayoutSaved, projectId]);
517
+ }, [
518
+ canPersistLayout,
519
+ effectiveLayoutKey,
520
+ onLayoutSaved,
521
+ onPersistLayout,
522
+ projectId,
523
+ ]);
248
524
  const queueLayoutSave = useCallback((nextNodes, viewport) => {
249
525
  if (!canPersistLayout) {
250
526
  return;
251
527
  }
252
- const signature = createLayoutSignature(projectId, nextNodes, viewport);
528
+ const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport);
253
529
  if (signature === lastSavedSignatureRef.current) {
254
530
  return;
255
531
  }
256
532
  if (saveTimeoutRef.current !== null) {
257
533
  window.clearTimeout(saveTimeoutRef.current);
534
+ saveTimeoutRef.current = null;
535
+ }
536
+ if (layoutSaveDebounceMs === 0) {
537
+ void persistLayout(nextNodes, viewport);
538
+ return;
258
539
  }
259
540
  saveTimeoutRef.current = window.setTimeout(() => {
260
541
  saveTimeoutRef.current = null;
261
542
  void persistLayout(nextNodes, viewport);
262
- }, SAVE_DEBOUNCE_MS);
263
- }, [canPersistLayout, persistLayout, projectId]);
543
+ }, layoutSaveDebounceMs);
544
+ }, [canPersistLayout, effectiveLayoutKey, layoutSaveDebounceMs, persistLayout]);
264
545
  const restoreViewport = useCallback(() => {
265
546
  const instance = reactFlowRef.current;
266
547
  if (!instance || nodesRef.current.length === 0) {
@@ -272,7 +553,7 @@ export function DependencyGraphView(props) {
272
553
  skipNextMoveEndSaveRef.current = true;
273
554
  window.requestAnimationFrame(() => {
274
555
  reactFlowRef.current?.setViewport(viewport, { duration: 0 });
275
- lastSavedSignatureRef.current = createLayoutSignature(projectId, nodesRef.current, viewport);
556
+ lastSavedSignatureRef.current = createLayoutSignature(effectiveLayoutKey, nodesRef.current, viewport);
276
557
  });
277
558
  return;
278
559
  }
@@ -290,9 +571,9 @@ export function DependencyGraphView(props) {
290
571
  const viewport = reactFlowRef.current?.getViewport() ?? null;
291
572
  queueLayoutSave(nodesRef.current, viewport);
292
573
  });
293
- }, [projectId, queueLayoutSave]);
574
+ }, [effectiveLayoutKey, queueLayoutSave]);
294
575
  useEffect(() => {
295
- const shouldApplySavedLayout = lastAppliedProjectIdRef.current !== projectId ||
576
+ const shouldApplySavedLayout = lastAppliedLayoutKeyRef.current !== effectiveLayoutKey ||
296
577
  lastAppliedLayoutSignatureRef.current !== savedLayoutSignature;
297
578
  const nextNodes = shouldApplySavedLayout
298
579
  ? applySavedLayout(initialNodes, savedLayout)
@@ -301,13 +582,13 @@ export function DependencyGraphView(props) {
301
582
  setEdges(initialEdges);
302
583
  nodesRef.current = nextNodes;
303
584
  if (shouldApplySavedLayout) {
304
- lastAppliedProjectIdRef.current = projectId;
585
+ lastAppliedLayoutKeyRef.current = effectiveLayoutKey;
305
586
  lastAppliedLayoutSignatureRef.current = savedLayoutSignature;
306
587
  pendingViewportRef.current = savedLayout?.viewport ?? null;
307
588
  fitViewOnNextSyncRef.current = !savedLayout?.viewport;
308
589
  if (!savedLayout?.viewport) {
309
590
  lastSavedSignatureRef.current = savedLayout?.nodes?.length
310
- ? createLayoutSignature(projectId, nextNodes, null)
591
+ ? createLayoutSignature(effectiveLayoutKey, nextNodes, null)
311
592
  : null;
312
593
  }
313
594
  restoreViewport();
@@ -315,7 +596,7 @@ export function DependencyGraphView(props) {
315
596
  }, [
316
597
  initialEdges,
317
598
  initialNodes,
318
- projectId,
599
+ effectiveLayoutKey,
319
600
  restoreViewport,
320
601
  savedLayout,
321
602
  savedLayoutSignature,
@@ -332,7 +613,7 @@ export function DependencyGraphView(props) {
332
613
  }, []);
333
614
  const hasVisibleNodes = initialNodes.length > 0;
334
615
  const handleAutoLayout = useCallback(() => {
335
- const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, blockedTaskIds, onSelect);
616
+ const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, blockedTaskIds, orientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask);
336
617
  setNodes(autoLayout.nodes);
337
618
  setEdges(autoLayout.edges);
338
619
  nodesRef.current = autoLayout.nodes;
@@ -343,15 +624,51 @@ export function DependencyGraphView(props) {
343
624
  blockedTaskIds,
344
625
  dependencies,
345
626
  onSelect,
627
+ orientation,
628
+ showExecutionStateBadges,
629
+ autoLayoutStrategy,
630
+ onRemoveTask,
631
+ onAddDependentTaskFromNode,
632
+ onAddChildTaskFromNode,
346
633
  restoreViewport,
347
634
  selectedTaskId,
348
635
  setEdges,
349
636
  setNodes,
350
637
  tasks,
351
638
  ]);
352
- 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 ? (_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-2 w-4 rounded-sm border border-slate-300 bg-slate-400", style: {
353
- backgroundImage: "repeating-linear-gradient(90deg, transparent, transparent 2px, white 2px, white 4px)",
354
- } }), "Related"] })] })) : (_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: "No tasks to visualize" }), _jsx("p", { className: "mt-1 text-xs text-slate-400", children: "Create tasks in this project to see them in the dependency graph." })] })] }) })) : (_jsxs(ReactFlow, { nodes: nodes, edges: edges, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, onInit: (instance) => {
639
+ const isValidConnection = useCallback((connection) => allowDependencyConnect
640
+ ? isGraphConnectionValid(connection, tasks, dependencies)
641
+ : false, [allowDependencyConnect, dependencies, tasks]);
642
+ const handleConnect = useCallback((connection) => {
643
+ if (!allowDependencyConnect) {
644
+ return;
645
+ }
646
+ const { source, sourceHandle, target } = connection;
647
+ if (!source || !target) {
648
+ return;
649
+ }
650
+ if (!isGraphConnectionValid(connection, tasks, dependencies)) {
651
+ return;
652
+ }
653
+ onSelectTask(target);
654
+ if (sourceHandle === "bottom") {
655
+ onCreateChildRelationship?.(source, target);
656
+ return;
657
+ }
658
+ onCreateDependency?.(source, target);
659
+ }, [
660
+ allowDependencyConnect,
661
+ dependencies,
662
+ onCreateChildRelationship,
663
+ onCreateDependency,
664
+ onSelectTask,
665
+ tasks,
666
+ ]);
667
+ 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, onNodeClick: (_event, node) => {
668
+ onSelectTask(node.id);
669
+ }, onNodeDragStart: (_event, node) => {
670
+ onSelectTask(node.id);
671
+ }, onInit: (instance) => {
355
672
  reactFlowRef.current = instance;
356
673
  restoreViewport();
357
674
  }, onNodeDragStop: () => {
@@ -364,7 +681,9 @@ export function DependencyGraphView(props) {
364
681
  return;
365
682
  }
366
683
  queueLayoutSave(nodesRef.current, viewport);
367
- }, nodeTypes: nodeTypes, minZoom: 0.2, maxZoom: 2, proOptions: { hideAttribution: true }, nodesDraggable: canPersistLayout, nodesConnectable: false, elementsSelectable: false, children: [_jsx(Controls, { showInteractive: false, className: "rounded-lg! border-slate-200! shadow-md!" }), _jsx(MiniMap, { nodeColor: (node) => {
684
+ }, nodeTypes: nodeTypes, minZoom: 0.2, maxZoom: 2, proOptions: { hideAttribution: true }, nodesDraggable: canPersistLayout, nodesConnectable: allowDependencyConnect, isValidConnection: isValidConnection, elementsSelectable: false, children: [_jsx(Controls, { showInteractive: false, className: "rounded-lg! border-slate-200! shadow-md!" }), _jsx(MiniMap, { ...(miniMapWidth != null && miniMapHeight != null
685
+ ? { width: miniMapWidth, height: miniMapHeight }
686
+ : {}), nodeColor: (node) => {
368
687
  const status = node.data?.status;
369
688
  switch (status) {
370
689
  case "Done":