@parhelia/core 0.1.12368 → 0.1.12393

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 (258) 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 +37 -15
  14. package/dist/editor/Editor.js.map +1 -1
  15. package/dist/editor/SetupWizard.js +20 -2
  16. package/dist/editor/SetupWizard.js.map +1 -1
  17. package/dist/editor/ai/AgentCostDisplay.d.ts +1 -0
  18. package/dist/editor/ai/AgentCostDisplay.js +1 -1
  19. package/dist/editor/ai/AgentCostDisplay.js.map +1 -1
  20. package/dist/editor/ai/AgentTerminal.js +158 -39
  21. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  22. package/dist/editor/ai/AgentTerminalStatusBar.d.ts +2 -0
  23. package/dist/editor/ai/AgentTerminalStatusBar.js +22 -37
  24. package/dist/editor/ai/AgentTerminalStatusBar.js.map +1 -1
  25. package/dist/editor/ai/AiResponseMessage.js +0 -1
  26. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  27. package/dist/editor/ai/ContentInspectorPopover.d.ts +17 -0
  28. package/dist/editor/ai/ContentInspectorPopover.js +136 -0
  29. package/dist/editor/ai/ContentInspectorPopover.js.map +1 -0
  30. package/dist/editor/ai/ContextInfoBar.js +55 -2
  31. package/dist/editor/ai/ContextInfoBar.js.map +1 -1
  32. package/dist/editor/ai/InlineAiDialog.js +1 -7
  33. package/dist/editor/ai/InlineAiDialog.js.map +1 -1
  34. package/dist/editor/ai/ToolCallDisplay.d.ts +4 -0
  35. package/dist/editor/ai/ToolCallDisplay.js +43 -8
  36. package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
  37. package/dist/editor/ai/dialogs/AgentDialogHandler.js +46 -22
  38. package/dist/editor/ai/dialogs/AgentDialogHandler.js.map +1 -1
  39. package/dist/editor/client/EditorShell.js +69 -26
  40. package/dist/editor/client/EditorShell.js.map +1 -1
  41. package/dist/editor/client/editContext.d.ts +3 -2
  42. package/dist/editor/client/hooks/useQuota.d.ts +2 -1
  43. package/dist/editor/client/hooks/useQuota.js.map +1 -1
  44. package/dist/editor/client/hooks/useSocketMessageHandler.js +28 -0
  45. package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
  46. package/dist/editor/client/operations.d.ts +1 -0
  47. package/dist/editor/client/operations.js +67 -15
  48. package/dist/editor/client/operations.js.map +1 -1
  49. package/dist/editor/client/waitForEditOperationTerminal.d.ts +11 -0
  50. package/dist/editor/client/waitForEditOperationTerminal.js +40 -0
  51. package/dist/editor/client/waitForEditOperationTerminal.js.map +1 -0
  52. package/dist/editor/commands/commands.d.ts +11 -1
  53. package/dist/editor/commands/commands.js +12 -1
  54. package/dist/editor/commands/commands.js.map +1 -1
  55. package/dist/editor/commands/customCommandConverter.d.ts +8 -1
  56. package/dist/editor/commands/customCommandConverter.js +33 -4
  57. package/dist/editor/commands/customCommandConverter.js.map +1 -1
  58. package/dist/editor/commands/handlers/uiActionHandlers.d.ts +6 -0
  59. package/dist/editor/commands/handlers/uiActionHandlers.js +84 -0
  60. package/dist/editor/commands/handlers/uiActionHandlers.js.map +1 -0
  61. package/dist/editor/commands/itemCommands.js +6 -2
  62. package/dist/editor/commands/itemCommands.js.map +1 -1
  63. package/dist/editor/commands/keyboardCommands.d.ts +10 -0
  64. package/dist/editor/commands/keyboardCommands.js +142 -0
  65. package/dist/editor/commands/keyboardCommands.js.map +1 -0
  66. package/dist/editor/commands/undo.d.ts +9 -15
  67. package/dist/editor/commands/undo.js +24 -0
  68. package/dist/editor/commands/undo.js.map +1 -1
  69. package/dist/editor/menubar/PageSelector.js +1 -3
  70. package/dist/editor/menubar/PageSelector.js.map +1 -1
  71. package/dist/editor/menubar/VersionSelector.js +1 -3
  72. package/dist/editor/menubar/VersionSelector.js.map +1 -1
  73. package/dist/editor/menubar/toolbar-sections/CustomCommandsToolbar.js +7 -36
  74. package/dist/editor/menubar/toolbar-sections/CustomCommandsToolbar.js.map +1 -1
  75. package/dist/editor/notifications/notificationRoutes.js +1 -0
  76. package/dist/editor/notifications/notificationRoutes.js.map +1 -1
  77. package/dist/editor/page-editor-chrome/InlineEditor.js +53 -36
  78. package/dist/editor/page-editor-chrome/InlineEditor.js.map +1 -1
  79. package/dist/editor/page-editor-chrome/useInlineAICompletion.js +283 -298
  80. package/dist/editor/page-editor-chrome/useInlineAICompletion.js.map +1 -1
  81. package/dist/editor/page-viewer/PageViewer.js +60 -6
  82. package/dist/editor/page-viewer/PageViewer.js.map +1 -1
  83. package/dist/editor/reviews/Comment.js +12 -10
  84. package/dist/editor/reviews/Comment.js.map +1 -1
  85. package/dist/editor/reviews/CommentDisplayPopover.js +1 -3
  86. package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -1
  87. package/dist/editor/reviews/PreviewInfo.js +1 -4
  88. package/dist/editor/reviews/PreviewInfo.js.map +1 -1
  89. package/dist/editor/reviews/reviewCommands.js +4 -1
  90. package/dist/editor/reviews/reviewCommands.js.map +1 -1
  91. package/dist/editor/reviews/useReviews.d.ts +2 -2
  92. package/dist/editor/reviews/useReviews.js +12 -30
  93. package/dist/editor/reviews/useReviews.js.map +1 -1
  94. package/dist/editor/services/agentService.d.ts +26 -0
  95. package/dist/editor/services/agentService.js +41 -0
  96. package/dist/editor/services/agentService.js.map +1 -1
  97. package/dist/editor/services/aiService.d.ts +7 -1
  98. package/dist/editor/services/aiService.js +13 -1
  99. package/dist/editor/services/aiService.js.map +1 -1
  100. package/dist/editor/services/notificationService.d.ts +1 -0
  101. package/dist/editor/services/notificationService.js +1 -0
  102. package/dist/editor/services/notificationService.js.map +1 -1
  103. package/dist/editor/services/reviewsService.d.ts +2 -5
  104. package/dist/editor/services/reviewsService.js +0 -10
  105. package/dist/editor/services/reviewsService.js.map +1 -1
  106. package/dist/editor/services/systemService.d.ts +2 -1
  107. package/dist/editor/services/systemService.js +3 -0
  108. package/dist/editor/services/systemService.js.map +1 -1
  109. package/dist/editor/services/templateBuilderService.d.ts +7 -0
  110. package/dist/editor/services/templateBuilderService.js +7 -1
  111. package/dist/editor/services/templateBuilderService.js.map +1 -1
  112. package/dist/editor/settings/About.js +25 -19
  113. package/dist/editor/settings/About.js.map +1 -1
  114. package/dist/editor/settings/QuotaInfo.js +15 -7
  115. package/dist/editor/settings/QuotaInfo.js.map +1 -1
  116. package/dist/editor/settings/index/useIndexStatus.js +1 -1
  117. package/dist/editor/settings/index/useIndexStatus.js.map +1 -1
  118. package/dist/editor/settings/panels/AgentProfileConfigPanel.d.ts +10 -0
  119. package/dist/editor/settings/panels/AgentProfileConfigPanel.js +61 -0
  120. package/dist/editor/settings/panels/AgentProfileConfigPanel.js.map +1 -0
  121. package/dist/editor/settings/panels/AgentsPanel.d.ts +0 -4
  122. package/dist/editor/settings/panels/AgentsPanel.js +101 -109
  123. package/dist/editor/settings/panels/AgentsPanel.js.map +1 -1
  124. package/dist/editor/settings/panels/CreateAgentProfileDialog.d.ts +7 -0
  125. package/dist/editor/settings/panels/CreateAgentProfileDialog.js +48 -0
  126. package/dist/editor/settings/panels/CreateAgentProfileDialog.js.map +1 -0
  127. package/dist/editor/settings/panels/GroupedFieldConfigPanel.d.ts +33 -0
  128. package/dist/editor/settings/panels/GroupedFieldConfigPanel.js +91 -0
  129. package/dist/editor/settings/panels/GroupedFieldConfigPanel.js.map +1 -0
  130. package/dist/editor/settings/panels/ModelConfigPanel.d.ts +10 -0
  131. package/dist/editor/settings/panels/ModelConfigPanel.js +51 -0
  132. package/dist/editor/settings/panels/ModelConfigPanel.js.map +1 -0
  133. package/dist/editor/settings/panels/ModelsPanel.js +201 -70
  134. package/dist/editor/settings/panels/ModelsPanel.js.map +1 -1
  135. package/dist/editor/settings/panels/ProjectTemplateAgentPanel.d.ts +10 -0
  136. package/dist/editor/settings/panels/ProjectTemplateAgentPanel.js +46 -0
  137. package/dist/editor/settings/panels/ProjectTemplateAgentPanel.js.map +1 -0
  138. package/dist/editor/settings/panels/ProjectTemplatesPanel.d.ts +2 -0
  139. package/dist/editor/settings/panels/ProjectTemplatesPanel.js +1340 -0
  140. package/dist/editor/settings/panels/ProjectTemplatesPanel.js.map +1 -0
  141. package/dist/editor/settings/panels/ProviderConfigPanel.d.ts +10 -0
  142. package/dist/editor/settings/panels/ProviderConfigPanel.js +32 -0
  143. package/dist/editor/settings/panels/ProviderConfigPanel.js.map +1 -0
  144. package/dist/editor/settings/panels/ProvidersPanel.js +46 -4
  145. package/dist/editor/settings/panels/ProvidersPanel.js.map +1 -1
  146. package/dist/editor/settings/panels/SearchConfigPanel.js +3 -3
  147. package/dist/editor/settings/panels/SearchConfigPanel.js.map +1 -1
  148. package/dist/editor/settings/panels/index.d.ts +1 -2
  149. package/dist/editor/settings/panels/index.js +1 -2
  150. package/dist/editor/settings/panels/index.js.map +1 -1
  151. package/dist/editor/setup-wizard/steps/CompleteStep.d.ts +2 -1
  152. package/dist/editor/setup-wizard/steps/CompleteStep.js +2 -1
  153. package/dist/editor/setup-wizard/steps/CompleteStep.js.map +1 -1
  154. package/dist/editor/setup-wizard/steps/LicenseActivationStep.d.ts +9 -0
  155. package/dist/editor/setup-wizard/steps/LicenseActivationStep.js +160 -0
  156. package/dist/editor/setup-wizard/steps/LicenseActivationStep.js.map +1 -0
  157. package/dist/editor/setup-wizard/steps/LicenseEmailStep.d.ts +10 -0
  158. package/dist/editor/setup-wizard/steps/LicenseEmailStep.js +101 -0
  159. package/dist/editor/setup-wizard/steps/LicenseEmailStep.js.map +1 -0
  160. package/dist/editor/template-wizard/TemplateStructureInlineEditor.js +422 -65
  161. package/dist/editor/template-wizard/TemplateStructureInlineEditor.js.map +1 -1
  162. package/dist/editor/ui/ItemNameDialogNew.js +15 -9
  163. package/dist/editor/ui/ItemNameDialogNew.js.map +1 -1
  164. package/dist/editor/utils/keyboardNavigation.d.ts +6 -20
  165. package/dist/editor/utils/keyboardNavigation.js +48 -139
  166. package/dist/editor/utils/keyboardNavigation.js.map +1 -1
  167. package/dist/licensing/EmailEntry.js +1 -1
  168. package/dist/licensing/EmailEntry.js.map +1 -1
  169. package/dist/licensing/LicenseActivationForm.js +1 -1
  170. package/dist/licensing/LicenseActivationForm.js.map +1 -1
  171. package/dist/licensing/LicenseCodeEntry.js +2 -2
  172. package/dist/licensing/LicenseCodeEntry.js.map +1 -1
  173. package/dist/licensing/LicenseContext.js +18 -9
  174. package/dist/licensing/LicenseContext.js.map +1 -1
  175. package/dist/licensing/LicenseOverlay.js +2 -1
  176. package/dist/licensing/LicenseOverlay.js.map +1 -1
  177. package/dist/licensing/licenseService.d.ts +10 -0
  178. package/dist/licensing/licenseService.js +28 -0
  179. package/dist/licensing/licenseService.js.map +1 -1
  180. package/dist/revision.d.ts +2 -2
  181. package/dist/revision.js +2 -2
  182. package/dist/setup/services/setupWizardService.d.ts +8 -10
  183. package/dist/setup/services/setupWizardService.js +4 -17
  184. package/dist/setup/services/setupWizardService.js.map +1 -1
  185. package/dist/splash-screen/ModernSplashScreen.js +100 -18
  186. package/dist/splash-screen/ModernSplashScreen.js.map +1 -1
  187. package/dist/splash-screen/ParheliaAssistantChat.js +3 -22
  188. package/dist/splash-screen/ParheliaAssistantChat.js.map +1 -1
  189. package/dist/task-board/TaskBoardWorkspace.js +70 -3
  190. package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
  191. package/dist/task-board/components/AssignAgentDialog.js +9 -4
  192. package/dist/task-board/components/AssignAgentDialog.js.map +1 -1
  193. package/dist/task-board/components/CreateProjectDialog.js +32 -34
  194. package/dist/task-board/components/CreateProjectDialog.js.map +1 -1
  195. package/dist/task-board/components/CreateTaskDialog.d.ts +2 -0
  196. package/dist/task-board/components/CreateTaskDialog.js +35 -11
  197. package/dist/task-board/components/CreateTaskDialog.js.map +1 -1
  198. package/dist/task-board/components/ProjectPropertiesPanel.js +4 -1
  199. package/dist/task-board/components/ProjectPropertiesPanel.js.map +1 -1
  200. package/dist/task-board/components/TaskAgentPanel.js +13 -4
  201. package/dist/task-board/components/TaskAgentPanel.js.map +1 -1
  202. package/dist/task-board/components/TaskAssigneePicker.d.ts +2 -2
  203. package/dist/task-board/components/TaskAssigneePicker.js +12 -4
  204. package/dist/task-board/components/TaskAssigneePicker.js.map +1 -1
  205. package/dist/task-board/components/TaskBoardProjectListSidebar.js.map +1 -1
  206. package/dist/task-board/components/TaskCard.js +2 -2
  207. package/dist/task-board/components/TaskCard.js.map +1 -1
  208. package/dist/task-board/components/TaskDetailPanel.js +2 -2
  209. package/dist/task-board/components/TaskDetailPanel.js.map +1 -1
  210. package/dist/task-board/components/TaskRow.js +2 -2
  211. package/dist/task-board/components/TaskRow.js.map +1 -1
  212. package/dist/task-board/components/WizardCommunicationCenter.js +10 -4
  213. package/dist/task-board/components/WizardCommunicationCenter.js.map +1 -1
  214. package/dist/task-board/services/taskService.d.ts +11 -2
  215. package/dist/task-board/services/taskService.js +20 -2
  216. package/dist/task-board/services/taskService.js.map +1 -1
  217. package/dist/task-board/types.d.ts +52 -7
  218. package/dist/task-board/views/DependencyGraphView.d.ts +31 -4
  219. package/dist/task-board/views/DependencyGraphView.js +383 -64
  220. package/dist/task-board/views/DependencyGraphView.js.map +1 -1
  221. package/dist/types.d.ts +23 -15
  222. package/package.json +7 -7
  223. package/dist/editor/settings/Setup.d.ts +0 -1
  224. package/dist/editor/settings/Setup.js +0 -211
  225. package/dist/editor/settings/Setup.js.map +0 -1
  226. package/dist/editor/settings/panels/DatabasePanel.d.ts +0 -6
  227. package/dist/editor/settings/panels/DatabasePanel.js +0 -50
  228. package/dist/editor/settings/panels/DatabasePanel.js.map +0 -1
  229. package/dist/editor/settings/setup-steps/AiSetupStep/EmbeddingsModelSection.d.ts +0 -2
  230. package/dist/editor/settings/setup-steps/AiSetupStep/EmbeddingsModelSection.js +0 -195
  231. package/dist/editor/settings/setup-steps/AiSetupStep/EmbeddingsModelSection.js.map +0 -1
  232. package/dist/editor/settings/setup-steps/AiSetupStep/index.d.ts +0 -2
  233. package/dist/editor/settings/setup-steps/AiSetupStep/index.js +0 -21
  234. package/dist/editor/settings/setup-steps/AiSetupStep/index.js.map +0 -1
  235. package/dist/editor/settings/setup-steps/AiSetupStep/provider/ProviderSection.d.ts +0 -1
  236. package/dist/editor/settings/setup-steps/AiSetupStep/provider/ProviderSection.js +0 -233
  237. package/dist/editor/settings/setup-steps/AiSetupStep/provider/ProviderSection.js.map +0 -1
  238. package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersList.d.ts +0 -15
  239. package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersList.js +0 -14
  240. package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersList.js.map +0 -1
  241. package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersSection.d.ts +0 -1
  242. package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersSection.js +0 -94
  243. package/dist/editor/settings/setup-steps/AiSetupStep/required-containers/RequiredContainersSection.js.map +0 -1
  244. package/dist/editor/settings/setup-steps/AiSetupStep/types.d.ts +0 -1
  245. package/dist/editor/settings/setup-steps/AiSetupStep/types.js +0 -2
  246. package/dist/editor/settings/setup-steps/AiSetupStep/types.js.map +0 -1
  247. package/dist/editor/settings/setup-steps/AiSetupStep/utils.d.ts +0 -5
  248. package/dist/editor/settings/setup-steps/AiSetupStep/utils.js +0 -44
  249. package/dist/editor/settings/setup-steps/AiSetupStep/utils.js.map +0 -1
  250. package/dist/editor/settings/setup-steps/IndexSetupStep.d.ts +0 -2
  251. package/dist/editor/settings/setup-steps/IndexSetupStep.js +0 -36
  252. package/dist/editor/settings/setup-steps/IndexSetupStep.js.map +0 -1
  253. package/dist/editor/settings/setup-steps/SettingsSetupStep.d.ts +0 -2
  254. package/dist/editor/settings/setup-steps/SettingsSetupStep.js +0 -111
  255. package/dist/editor/settings/setup-steps/SettingsSetupStep.js.map +0 -1
  256. package/dist/editor/settings/setup-steps/SetupOverview.d.ts +0 -14
  257. package/dist/editor/settings/setup-steps/SetupOverview.js +0 -38
  258. package/dist/editor/settings/setup-steps/SetupOverview.js.map +0 -1
@@ -0,0 +1,1340 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
3
+ import { AlertCircle, ChevronDown, CornerDownRight, GitBranch, Link2, Plus, RefreshCw, Search, Settings2, Shield, Trash2, X, } from "lucide-react";
4
+ import { toast } from "sonner";
5
+ import { Button } from "../../../components/ui/button";
6
+ import { Dialog, DialogContent, DialogFooter, } from "../../../components/ui/dialog";
7
+ import { Input } from "../../../components/ui/input";
8
+ import { Label } from "../../../components/ui/label";
9
+ import { StyledDialogTitle } from "../../../components/ui/styled-dialog-title";
10
+ import { Textarea } from "../../../components/ui/textarea";
11
+ import { Select } from "../../../components/ui/select";
12
+ import { Switch } from "../../../components/ui/switch";
13
+ import { Badge } from "../../../components/ui/badge";
14
+ import { Popover, PopoverContent, PopoverTrigger, } from "../../../components/ui/popover";
15
+ import { Splitter } from "../../ui/Splitter";
16
+ import { DependencyGraphView } from "../../../task-board/views/DependencyGraphView";
17
+ import { deleteProjectTemplate, getProjectTemplates, getProjectTemplateAgent, resetProjectTemplateAgent, upsertProjectTemplate, } from "../../../task-board/services/taskService";
18
+ import { getAiProfilesErrorMessage, loadAiProfiles, } from "../../services/aiService";
19
+ import { updateAgentContext } from "../../services/agentService";
20
+ import { useEditContext } from "../../client/editContext";
21
+ import { isTypingEventTarget } from "../../utils/keyboardNavigation";
22
+ import { cn } from "../../../lib/utils";
23
+ import { ProjectTemplateAgentPanel } from "./ProjectTemplateAgentPanel";
24
+ import { useSearchParams } from "next/navigation";
25
+ /** Query param for deep-linking the selected project template (settings reload). */
26
+ const SELECTED_PROJECT_TEMPLATE_QUERY_PARAM = "projectTemplateId";
27
+ const PRIORITY_OPTIONS = [
28
+ { value: "", label: "No priority" },
29
+ { value: "Low", label: "Low" },
30
+ { value: "Medium", label: "Medium" },
31
+ { value: "High", label: "High" },
32
+ { value: "Critical", label: "Critical" },
33
+ ];
34
+ const ASSIGNEE_TYPE_OPTIONS = [
35
+ { value: "", label: "Unassigned" },
36
+ { value: "Agent", label: "Agent profile" },
37
+ { value: "Human", label: "Human user" },
38
+ { value: "Role", label: "Role" },
39
+ ];
40
+ const PROJECT_TEMPLATE_AUTOSAVE_DEBOUNCE_MS = 1200;
41
+ function cloneTemplate(template) {
42
+ return normalizeProjectTemplate(JSON.parse(JSON.stringify(template)));
43
+ }
44
+ function normalizeTaskTemplate(task) {
45
+ const rawAssigneeId = task.assigneeId?.trim() || null;
46
+ const rawAgentProfileId = task.agentProfileId?.trim() || null;
47
+ const rawParentTaskId = task.parentTaskId?.trim() || null;
48
+ const assigneeType = task.assigneeType ?? (rawAgentProfileId ? "Agent" : null);
49
+ if (assigneeType === "Agent") {
50
+ const canonicalAgentId = rawAgentProfileId || (isValidGuid(rawAssigneeId) ? rawAssigneeId : null);
51
+ return {
52
+ ...task,
53
+ assigneeType: "Agent",
54
+ assigneeId: canonicalAgentId,
55
+ agentProfileId: canonicalAgentId,
56
+ parentTaskId: rawParentTaskId,
57
+ };
58
+ }
59
+ if (assigneeType === "Human" || assigneeType === "Role") {
60
+ return {
61
+ ...task,
62
+ assigneeType,
63
+ assigneeId: rawAssigneeId,
64
+ agentProfileId: null,
65
+ parentTaskId: rawParentTaskId,
66
+ };
67
+ }
68
+ return {
69
+ ...task,
70
+ assigneeType: null,
71
+ assigneeId: rawAssigneeId,
72
+ agentProfileId: null,
73
+ parentTaskId: rawParentTaskId,
74
+ };
75
+ }
76
+ function normalizeProjectTemplate(template) {
77
+ return {
78
+ ...template,
79
+ hideFromCreateDialog: template.hideFromCreateDialog === true,
80
+ openInWizardMode: template.openInWizardMode === true,
81
+ taskTemplates: (template.taskTemplates ?? []).map(normalizeTaskTemplate),
82
+ };
83
+ }
84
+ function createEmptyTemplate() {
85
+ return {
86
+ id: crypto.randomUUID(),
87
+ name: "New Project Template",
88
+ description: "",
89
+ disabled: false,
90
+ hideFromCreateDialog: false,
91
+ openInWizardMode: false,
92
+ isSystem: false,
93
+ defaultCostLimit: null,
94
+ graphLayout: null,
95
+ taskTemplates: [],
96
+ };
97
+ }
98
+ function createEmptyTaskTemplate(taskCount, title) {
99
+ return {
100
+ id: crypto.randomUUID(),
101
+ title: title?.trim() || `Task ${taskCount + 1}`,
102
+ description: "",
103
+ agentProfileId: null,
104
+ assigneeType: null,
105
+ assigneeId: null,
106
+ priority: null,
107
+ sortOrder: taskCount * 100,
108
+ parentTaskId: null,
109
+ dependencyIds: [],
110
+ };
111
+ }
112
+ function sortTemplates(templates) {
113
+ return [...templates].sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
114
+ }
115
+ function buildUpsertTemplateRequest(template) {
116
+ return {
117
+ projectTemplateId: template.id,
118
+ name: template.name.trim(),
119
+ description: template.description?.trim() || null,
120
+ disabled: template.disabled === true,
121
+ hideFromCreateDialog: template.hideFromCreateDialog === true,
122
+ openInWizardMode: template.openInWizardMode === true,
123
+ defaultCostLimit: template.defaultCostLimit == null ||
124
+ Number.isNaN(template.defaultCostLimit)
125
+ ? null
126
+ : template.defaultCostLimit,
127
+ graphLayout: template.graphLayout
128
+ ? {
129
+ projectTemplateId: template.id,
130
+ nodes: template.graphLayout.nodes ?? [],
131
+ viewport: template.graphLayout.viewport ?? null,
132
+ }
133
+ : null,
134
+ taskTemplates: template.taskTemplates.map((task, index) => {
135
+ const normalizedTask = normalizeTaskTemplate(task);
136
+ return {
137
+ ...normalizedTask,
138
+ title: normalizedTask.title.trim(),
139
+ description: normalizedTask.description?.trim() || null,
140
+ priority: normalizedTask.priority || null,
141
+ sortOrder: normalizedTask.sortOrder || index * 100,
142
+ parentTaskId: normalizedTask.parentTaskId || null,
143
+ agentProfileId: normalizedTask.agentProfileId || null,
144
+ assigneeType: normalizedTask.assigneeType || null,
145
+ assigneeId: normalizedTask.assigneeId || null,
146
+ dependencyIds: Array.from(new Set(normalizedTask.dependencyIds || [])),
147
+ };
148
+ }),
149
+ };
150
+ }
151
+ function mergeTemplateResponseWithRequest(serverTemplate, request) {
152
+ const parentTaskIdByTaskId = new Map((request.taskTemplates ?? []).map((task) => [
153
+ task.id,
154
+ task.parentTaskId ?? null,
155
+ ]));
156
+ const mergedGraphLayout = request.graphLayout === undefined
157
+ ? serverTemplate.graphLayout
158
+ : request.graphLayout === null
159
+ ? null
160
+ : {
161
+ projectTemplateId: serverTemplate.id,
162
+ nodes: request.graphLayout.nodes ?? [],
163
+ viewport: request.graphLayout.viewport ?? null,
164
+ };
165
+ return normalizeProjectTemplate({
166
+ ...serverTemplate,
167
+ graphLayout: mergedGraphLayout,
168
+ taskTemplates: (serverTemplate.taskTemplates ?? []).map((task) => ({
169
+ ...task,
170
+ parentTaskId: task.parentTaskId ?? parentTaskIdByTaskId.get(task.id) ?? null,
171
+ })),
172
+ });
173
+ }
174
+ function isValidGuid(value) {
175
+ return (!!value &&
176
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value));
177
+ }
178
+ function readProjectTemplateIdFromUrl(searchParams) {
179
+ const raw = searchParams.get(SELECTED_PROJECT_TEMPLATE_QUERY_PARAM) ||
180
+ (typeof window !== "undefined"
181
+ ? new URLSearchParams(window.location.search).get(SELECTED_PROJECT_TEMPLATE_QUERY_PARAM)
182
+ : null);
183
+ return raw && isValidGuid(raw) ? raw : null;
184
+ }
185
+ function readProjectTemplateIdFromWindow() {
186
+ const raw = typeof window !== "undefined"
187
+ ? new URLSearchParams(window.location.search).get(SELECTED_PROJECT_TEMPLATE_QUERY_PARAM)
188
+ : null;
189
+ return raw && isValidGuid(raw) ? raw : null;
190
+ }
191
+ function getSelectedTaskIdForTemplate(template, preferredTaskId) {
192
+ const taskTemplates = template?.taskTemplates ?? [];
193
+ if (!taskTemplates.length) {
194
+ return null;
195
+ }
196
+ if (preferredTaskId &&
197
+ taskTemplates.some((task) => task.id === preferredTaskId)) {
198
+ return preferredTaskId;
199
+ }
200
+ return taskTemplates[0]?.id ?? null;
201
+ }
202
+ function normalizeTaskPriority(priority) {
203
+ if (priority === "Low" ||
204
+ priority === "Medium" ||
205
+ priority === "High" ||
206
+ priority === "Critical") {
207
+ return priority;
208
+ }
209
+ return null;
210
+ }
211
+ function buildTemplateTaskItems(template, aiProfilesById) {
212
+ if (!template)
213
+ return [];
214
+ const now = new Date().toISOString();
215
+ return template.taskTemplates.map((task) => {
216
+ const normalizedTask = normalizeTaskTemplate(task);
217
+ const assigneeType = normalizedTask.assigneeType;
218
+ const assigneeId = normalizedTask.assigneeId;
219
+ const assigneeDisplayName = assigneeType === "Agent" && assigneeId
220
+ ? aiProfilesById[assigneeId.toLowerCase()]?.displayTitle ||
221
+ aiProfilesById[assigneeId.toLowerCase()]?.name ||
222
+ assigneeId
223
+ : assigneeId;
224
+ return {
225
+ taskId: task.id,
226
+ projectId: template.id,
227
+ parentTaskId: normalizedTask.parentTaskId ?? null,
228
+ taskType: "Task",
229
+ title: task.title || "Untitled Task",
230
+ description: normalizedTask.description ?? null,
231
+ status: "Todo",
232
+ assigneeType,
233
+ assigneeId,
234
+ assigneeDisplayName,
235
+ priority: normalizeTaskPriority(normalizedTask.priority),
236
+ sortOrder: normalizedTask.sortOrder,
237
+ executionState: "Unassigned",
238
+ assignedAgentId: null,
239
+ createdBy: "template",
240
+ createdAt: now,
241
+ updatedAt: now,
242
+ };
243
+ });
244
+ }
245
+ function buildTemplateDependencies(template) {
246
+ if (!template)
247
+ return [];
248
+ return template.taskTemplates.flatMap((task) => (task.dependencyIds ?? []).map((dependencyId) => ({
249
+ dependencyId: `${task.id}:${dependencyId}`,
250
+ taskId: task.id,
251
+ dependsOnTaskId: dependencyId,
252
+ dependencyType: "BlockedBy",
253
+ })));
254
+ }
255
+ function canAddTemplateDependency(template, taskId, dependencyId) {
256
+ if (!template || taskId === dependencyId) {
257
+ return false;
258
+ }
259
+ const tasksById = new Map(template.taskTemplates.map((task) => [task.id, task]));
260
+ const task = tasksById.get(taskId);
261
+ const dependencyTask = tasksById.get(dependencyId);
262
+ if (!task || !dependencyTask) {
263
+ return false;
264
+ }
265
+ if ((task.dependencyIds ?? []).includes(dependencyId)) {
266
+ return false;
267
+ }
268
+ return task.parentTaskId !== dependencyId;
269
+ }
270
+ function buildProjectTemplateAgentContext(template, selectedTaskTemplateId) {
271
+ const dependencyCount = template.taskTemplates.reduce((count, task) => count + (task.dependencyIds?.length ?? 0), 0);
272
+ const agentAssignedTaskCount = template.taskTemplates.filter((task) => task.agentProfileId || (task.assigneeType === "Agent" && task.assigneeId)).length;
273
+ const selectedTask = selectedTaskTemplateId
274
+ ? template.taskTemplates.find((task) => task.id === selectedTaskTemplateId) ??
275
+ null
276
+ : null;
277
+ return {
278
+ activeWorkspace: "settings",
279
+ additionalData: {
280
+ activeWorkspace: "settings",
281
+ projectTemplateEditor: true,
282
+ projectTemplateId: template.id,
283
+ projectTemplateName: template.name,
284
+ projectTemplateDescription: template.description ?? null,
285
+ projectTemplateDisabled: template.disabled === true,
286
+ projectTemplateDefaultCostLimit: template.defaultCostLimit ?? null,
287
+ selectedTaskTemplateId: selectedTask ? selectedTask.id : null,
288
+ selectedTaskTemplateTitle: selectedTask?.title?.trim() || null,
289
+ selectedTaskTemplate: selectedTask
290
+ ? normalizeTaskTemplate(selectedTask)
291
+ : null,
292
+ projectTemplateSummary: {
293
+ taskCount: template.taskTemplates.length,
294
+ dependencyCount,
295
+ agentAssignedTaskCount,
296
+ defaultCostLimit: template.defaultCostLimit ?? null,
297
+ disabled: template.disabled === true,
298
+ },
299
+ projectTemplate: cloneTemplate(template),
300
+ projectTemplateSavePayload: buildUpsertTemplateRequest(template),
301
+ },
302
+ };
303
+ }
304
+ export function ProjectTemplatesPanel() {
305
+ const editContext = useEditContext();
306
+ const searchParams = useSearchParams();
307
+ const [state, setState] = useState("loading");
308
+ const [templates, setTemplates] = useState([]);
309
+ const [error, setError] = useState(null);
310
+ const [searchQuery, setSearchQuery] = useState("");
311
+ const [selectedTemplateId, setSelectedTemplateId] = useState(null);
312
+ const [draftTemplate, setDraftTemplate] = useState(null);
313
+ const [selectedTaskId, setSelectedTaskId] = useState(null);
314
+ const [saving, setSaving] = useState(false);
315
+ const [deleting, setDeleting] = useState(false);
316
+ const [isDirty, setIsDirty] = useState(false);
317
+ const [saveError, setSaveError] = useState(null);
318
+ const [aiProfiles, setAiProfiles] = useState([]);
319
+ const [profilesError, setProfilesError] = useState(null);
320
+ const [isCreateTaskDialogOpen, setIsCreateTaskDialogOpen] = useState(false);
321
+ const [newTaskTitle, setNewTaskTitle] = useState("");
322
+ const [newTaskDependencySourceId, setNewTaskDependencySourceId] = useState(null);
323
+ const [creatingTask, setCreatingTask] = useState(false);
324
+ const [isAddChildTaskDialogOpen, setIsAddChildTaskDialogOpen] = useState(false);
325
+ const [newChildTaskTitle, setNewChildTaskTitle] = useState("");
326
+ const [creatingChildTask, setCreatingChildTask] = useState(false);
327
+ const [dependencyPickerOpen, setDependencyPickerOpen] = useState(false);
328
+ const [isBasicSettingsExpanded, setIsBasicSettingsExpanded] = useState(false);
329
+ const [templateAgentId, setTemplateAgentId] = useState(null);
330
+ const [templateAgentLoading, setTemplateAgentLoading] = useState(false);
331
+ const [templateAgentResetting, setTemplateAgentResetting] = useState(false);
332
+ const [templateAgentError, setTemplateAgentError] = useState(null);
333
+ const autosaveTimeoutRef = useRef(null);
334
+ const lastPersistedSignatureRef = useRef(null);
335
+ const latestDraftSignatureRef = useRef(null);
336
+ const saveRunIdRef = useRef(0);
337
+ const draftTemplateRef = useRef(null);
338
+ const selectedTemplateIdRef = useRef(selectedTemplateId);
339
+ const selectedTaskIdRef = useRef(selectedTaskId);
340
+ const templateAgentLoadRunIdRef = useRef(0);
341
+ useEffect(() => {
342
+ let cancelled = false;
343
+ loadAiProfiles()
344
+ .then((items) => {
345
+ if (!cancelled) {
346
+ setAiProfiles(items || []);
347
+ setProfilesError(null);
348
+ }
349
+ })
350
+ .catch((loadError) => {
351
+ if (!cancelled) {
352
+ setAiProfiles([]);
353
+ setProfilesError(getAiProfilesErrorMessage(loadError));
354
+ }
355
+ });
356
+ return () => {
357
+ cancelled = true;
358
+ };
359
+ }, []);
360
+ useEffect(() => {
361
+ draftTemplateRef.current = draftTemplate;
362
+ }, [draftTemplate]);
363
+ const selectTemplate = useCallback((template) => {
364
+ const clonedTemplate = template ? cloneTemplate(template) : null;
365
+ const preferredTaskId = clonedTemplate?.id &&
366
+ clonedTemplate.id === selectedTemplateIdRef.current
367
+ ? selectedTaskIdRef.current
368
+ : null;
369
+ const persistedSignature = clonedTemplate && clonedTemplate.name.trim()
370
+ ? JSON.stringify(buildUpsertTemplateRequest(clonedTemplate))
371
+ : null;
372
+ selectedTemplateIdRef.current = template?.id ?? null;
373
+ draftTemplateRef.current = clonedTemplate;
374
+ setSelectedTemplateId(template?.id ?? null);
375
+ setDraftTemplate(clonedTemplate);
376
+ setSelectedTaskId(getSelectedTaskIdForTemplate(clonedTemplate, preferredTaskId));
377
+ setIsDirty(false);
378
+ setSaveError(null);
379
+ lastPersistedSignatureRef.current = persistedSignature;
380
+ latestDraftSignatureRef.current = persistedSignature;
381
+ const nextId = template?.id ?? null;
382
+ const currentUrlId = readProjectTemplateIdFromUrl(searchParams);
383
+ if (nextId !== currentUrlId) {
384
+ editContext?.updateUrl({
385
+ [SELECTED_PROJECT_TEMPLATE_QUERY_PARAM]: nextId ?? undefined,
386
+ });
387
+ }
388
+ }, [editContext, searchParams]);
389
+ const loadTemplates = useCallback(async (preferredTemplateId) => {
390
+ try {
391
+ setState("loading");
392
+ setError(null);
393
+ const result = await getProjectTemplates({ includeDisabled: true });
394
+ if (result.type !== "success") {
395
+ throw new Error(result.summary || "Failed to load project templates");
396
+ }
397
+ const loadedTemplates = result.data ?? [];
398
+ setTemplates(loadedTemplates);
399
+ setState("success");
400
+ const preferredTemplateExists = !!preferredTemplateId &&
401
+ loadedTemplates.some((template) => template.id === preferredTemplateId);
402
+ const nextTemplateId = preferredTemplateExists
403
+ ? preferredTemplateId
404
+ : preferredTemplateId
405
+ ? null
406
+ : loadedTemplates[0]?.id || null;
407
+ const nextTemplate = loadedTemplates.find((template) => template.id === nextTemplateId);
408
+ selectTemplate(nextTemplate ?? null);
409
+ }
410
+ catch (loadError) {
411
+ setState("error");
412
+ setError(loadError instanceof Error
413
+ ? loadError.message
414
+ : "Failed to load project templates");
415
+ }
416
+ }, [selectTemplate]);
417
+ useEffect(() => {
418
+ void loadTemplates(readProjectTemplateIdFromWindow());
419
+ // Run once on mount. `loadTemplates` is recreated whenever `selectTemplate` changes
420
+ // (e.g. searchParams / editContext). Re-running on that identity change refetches from
421
+ // the server and calls selectTemplate, which resets the draft and drops unsaved edits
422
+ // before autosave can persist.
423
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional mount-only bootstrap
424
+ }, []);
425
+ useEffect(() => {
426
+ if (state !== "success" || !templates.length)
427
+ return;
428
+ const fromUrl = readProjectTemplateIdFromUrl(searchParams);
429
+ if (fromUrl === selectedTemplateId)
430
+ return;
431
+ if (!fromUrl && selectedTemplateId) {
432
+ editContext?.updateUrl({
433
+ [SELECTED_PROJECT_TEMPLATE_QUERY_PARAM]: selectedTemplateId,
434
+ });
435
+ return;
436
+ }
437
+ const match = fromUrl
438
+ ? templates.find((template) => template.id === fromUrl)
439
+ : null;
440
+ if (fromUrl && !match) {
441
+ return;
442
+ }
443
+ selectTemplate(match ?? null);
444
+ }, [
445
+ editContext,
446
+ searchParams,
447
+ selectTemplate,
448
+ selectedTemplateId,
449
+ state,
450
+ templates,
451
+ ]);
452
+ useEffect(() => {
453
+ if (!selectedTemplateId)
454
+ return;
455
+ const matchingTemplate = templates.find((template) => template.id === selectedTemplateId);
456
+ if (!matchingTemplate)
457
+ return;
458
+ if (draftTemplate?.id === matchingTemplate.id)
459
+ return;
460
+ setDraftTemplate(cloneTemplate(matchingTemplate));
461
+ setSelectedTaskId((currentSelectedTaskId) => getSelectedTaskIdForTemplate(matchingTemplate, currentSelectedTaskId));
462
+ setIsDirty(false);
463
+ }, [draftTemplate?.id, selectedTemplateId, templates]);
464
+ const aiProfilesById = useMemo(() => {
465
+ const byId = {};
466
+ for (const profile of aiProfiles) {
467
+ if (!profile.id)
468
+ continue;
469
+ byId[profile.id.toLowerCase()] = profile;
470
+ }
471
+ return byId;
472
+ }, [aiProfiles]);
473
+ const filteredTemplates = useMemo(() => {
474
+ const query = searchQuery.trim().toLowerCase();
475
+ if (!query)
476
+ return templates;
477
+ return templates.filter((template) => {
478
+ return (template.name.toLowerCase().includes(query) ||
479
+ template.description?.toLowerCase().includes(query) ||
480
+ template.taskTemplates.some((task) => task.title.toLowerCase().includes(query)));
481
+ });
482
+ }, [searchQuery, templates]);
483
+ const selectedTask = useMemo(() => draftTemplate?.taskTemplates.find((task) => task.id === selectedTaskId) ??
484
+ null, [draftTemplate, selectedTaskId]);
485
+ const dependencyTasks = useMemo(() => {
486
+ if (!draftTemplate || !selectedTask)
487
+ return [];
488
+ const byId = new Map(draftTemplate.taskTemplates.map((t) => [t.id, t]));
489
+ return selectedTask.dependencyIds
490
+ .map((id) => byId.get(id))
491
+ .filter((t) => t != null);
492
+ }, [draftTemplate, selectedTask]);
493
+ const childTaskTemplates = useMemo(() => {
494
+ if (!draftTemplate || !selectedTaskId)
495
+ return [];
496
+ return draftTemplate.taskTemplates.filter((task) => task.id !== selectedTaskId && task.parentTaskId === selectedTaskId);
497
+ }, [draftTemplate, selectedTaskId]);
498
+ const dependencyAddCandidates = useMemo(() => {
499
+ if (!draftTemplate || !selectedTaskId || !selectedTask)
500
+ return [];
501
+ return draftTemplate.taskTemplates.filter((task) => task.id !== selectedTaskId &&
502
+ !selectedTask.dependencyIds.includes(task.id));
503
+ }, [draftTemplate, selectedTaskId, selectedTask]);
504
+ const parentTaskSelectOptions = useMemo(() => {
505
+ if (!draftTemplate || !selectedTaskId) {
506
+ return [{ value: "", label: "None" }];
507
+ }
508
+ const others = draftTemplate.taskTemplates.filter((task) => task.id !== selectedTaskId);
509
+ return [
510
+ { value: "", label: "None" },
511
+ ...[...others]
512
+ .sort((a, b) => (a.title || "").localeCompare(b.title || "", undefined, {
513
+ sensitivity: "base",
514
+ }))
515
+ .map((task) => ({
516
+ value: task.id,
517
+ label: task.title?.trim() || "Untitled Task",
518
+ })),
519
+ ];
520
+ }, [draftTemplate, selectedTaskId]);
521
+ const headerStatusBadge = useMemo(() => {
522
+ if (saveError) {
523
+ return (_jsx("span", { "data-testid": "project-template-save-status", "data-state": "error", className: "inline-flex min-w-[84px] justify-center rounded bg-red-100 px-1.5 py-0.5 whitespace-nowrap text-red-700", children: "Save failed" }));
524
+ }
525
+ if (isDirty || saving) {
526
+ return (_jsx("span", { "data-testid": "project-template-save-status", "data-state": "saving", className: "inline-flex min-w-[84px] justify-center rounded bg-amber-100 px-1.5 py-0.5 whitespace-nowrap text-amber-700", children: "Saving..." }));
527
+ }
528
+ return (_jsx("span", { "aria-hidden": "true", "data-testid": "project-template-save-status", "data-state": "saved", className: "inline-flex min-w-[84px] justify-center rounded px-1.5 py-0.5 whitespace-nowrap opacity-0", children: "Saving..." }));
529
+ }, [isDirty, saveError, saving]);
530
+ const graphTasks = useMemo(() => buildTemplateTaskItems(draftTemplate, aiProfilesById), [aiProfilesById, draftTemplate]);
531
+ const graphDependencies = useMemo(() => buildTemplateDependencies(draftTemplate), [draftTemplate]);
532
+ const profileOptions = useMemo(() => aiProfiles.map((profile) => ({
533
+ value: profile.id,
534
+ label: profile.displayTitle || profile.name,
535
+ description: profile.description || undefined,
536
+ })), [aiProfiles]);
537
+ const syncProjectTemplateAgentContext = useCallback(async (agentId, template, selectedTaskTemplateId) => {
538
+ try {
539
+ await updateAgentContext(agentId, buildProjectTemplateAgentContext(template, selectedTaskTemplateId));
540
+ }
541
+ catch (error) {
542
+ setTemplateAgentError(error instanceof Error
543
+ ? `Failed to update template assistant context: ${error.message}`
544
+ : "Failed to update template assistant context");
545
+ }
546
+ }, []);
547
+ useEffect(() => {
548
+ const runId = ++templateAgentLoadRunIdRef.current;
549
+ if (!selectedTemplateId) {
550
+ setTemplateAgentId(null);
551
+ setTemplateAgentLoading(false);
552
+ setTemplateAgentError(null);
553
+ return;
554
+ }
555
+ const selectedTemplate = draftTemplateRef.current;
556
+ if (!selectedTemplate || selectedTemplate.id !== selectedTemplateId) {
557
+ setTemplateAgentId(null);
558
+ setTemplateAgentLoading(true);
559
+ setTemplateAgentError(null);
560
+ return;
561
+ }
562
+ setTemplateAgentId(null);
563
+ setTemplateAgentLoading(true);
564
+ setTemplateAgentError(null);
565
+ void getProjectTemplateAgent(selectedTemplateId)
566
+ .then(async (result) => {
567
+ if (runId !== templateAgentLoadRunIdRef.current) {
568
+ return;
569
+ }
570
+ if (result.type !== "success" || !result.data) {
571
+ throw new Error(result.summary || "Failed to load project template assistant");
572
+ }
573
+ setTemplateAgentId(result.data.agentId);
574
+ setTemplateAgentLoading(false);
575
+ })
576
+ .catch((loadError) => {
577
+ if (runId !== templateAgentLoadRunIdRef.current)
578
+ return;
579
+ setTemplateAgentId(null);
580
+ setTemplateAgentLoading(false);
581
+ setTemplateAgentError(loadError instanceof Error
582
+ ? loadError.message
583
+ : "Failed to load project template assistant");
584
+ });
585
+ }, [draftTemplate?.id, selectedTemplateId, syncProjectTemplateAgentContext]);
586
+ useEffect(() => {
587
+ if (!templateAgentId || templateAgentLoading)
588
+ return;
589
+ const currentTemplate = draftTemplateRef.current;
590
+ if (!currentTemplate || currentTemplate.id !== selectedTemplateId)
591
+ return;
592
+ void syncProjectTemplateAgentContext(templateAgentId, currentTemplate, selectedTaskId);
593
+ }, [
594
+ selectedTaskId,
595
+ templateAgentId,
596
+ templateAgentLoading,
597
+ selectedTemplateId,
598
+ syncProjectTemplateAgentContext,
599
+ ]);
600
+ const applyDraftChange = useCallback((updater) => {
601
+ setDraftTemplate((current) => {
602
+ if (!current)
603
+ return current;
604
+ const next = normalizeProjectTemplate(updater(cloneTemplate(current)));
605
+ const nextSignature = next.name.trim()
606
+ ? JSON.stringify(buildUpsertTemplateRequest(next))
607
+ : null;
608
+ draftTemplateRef.current = next;
609
+ latestDraftSignatureRef.current = nextSignature;
610
+ setTemplates((existingTemplates) => existingTemplates.map((template) => template.id === next.id ? cloneTemplate(next) : template));
611
+ return next;
612
+ });
613
+ setIsDirty(true);
614
+ setSaveError(null);
615
+ }, []);
616
+ const updateSelectedTask = useCallback((updater) => {
617
+ if (!selectedTaskId)
618
+ return;
619
+ applyDraftChange((currentTemplate) => ({
620
+ ...currentTemplate,
621
+ taskTemplates: currentTemplate.taskTemplates.map((task) => task.id === selectedTaskId ? updater({ ...task }) : task),
622
+ }));
623
+ }, [applyDraftChange, selectedTaskId]);
624
+ const currentSaveRequest = useMemo(() => {
625
+ if (!draftTemplate || !draftTemplate.name.trim()) {
626
+ return null;
627
+ }
628
+ return buildUpsertTemplateRequest(draftTemplate);
629
+ }, [draftTemplate]);
630
+ const currentSaveSignature = useMemo(() => (currentSaveRequest ? JSON.stringify(currentSaveRequest) : null), [currentSaveRequest]);
631
+ useEffect(() => {
632
+ latestDraftSignatureRef.current = currentSaveSignature;
633
+ }, [currentSaveSignature]);
634
+ const persistTemplate = useCallback(async (request, signature, options) => {
635
+ const runId = ++saveRunIdRef.current;
636
+ setSaving(true);
637
+ setSaveError(null);
638
+ try {
639
+ const result = await upsertProjectTemplate(request);
640
+ if (result.type !== "success" || !result.data) {
641
+ const message = result.summary || "Failed to save project template";
642
+ if (latestDraftSignatureRef.current === signature) {
643
+ setSaveError(message);
644
+ }
645
+ if (options?.showErrorToast) {
646
+ toast.error(message);
647
+ }
648
+ return false;
649
+ }
650
+ const savedTemplate = mergeTemplateResponseWithRequest(result.data, request);
651
+ lastPersistedSignatureRef.current = signature;
652
+ setTemplates((existingTemplates) => sortTemplates([
653
+ ...existingTemplates.filter((template) => template.id !== savedTemplate.id),
654
+ savedTemplate,
655
+ ]));
656
+ if (latestDraftSignatureRef.current === signature) {
657
+ setDraftTemplate((current) => current?.id === savedTemplate.id
658
+ ? cloneTemplate(savedTemplate)
659
+ : current);
660
+ if (selectedTemplateIdRef.current === savedTemplate.id) {
661
+ setSelectedTaskId((currentSelectedTaskId) => getSelectedTaskIdForTemplate(savedTemplate, currentSelectedTaskId));
662
+ }
663
+ setIsDirty(false);
664
+ setSaveError(null);
665
+ }
666
+ if (templateAgentId && savedTemplate.id === selectedTemplateId) {
667
+ const nextSelectedTaskId = getSelectedTaskIdForTemplate(savedTemplate, selectedTaskIdRef.current);
668
+ void syncProjectTemplateAgentContext(templateAgentId, savedTemplate, nextSelectedTaskId);
669
+ }
670
+ return true;
671
+ }
672
+ finally {
673
+ if (runId === saveRunIdRef.current) {
674
+ setSaving(false);
675
+ }
676
+ }
677
+ }, [selectedTemplateId, syncProjectTemplateAgentContext, templateAgentId]);
678
+ const flushPendingSave = useCallback(async (options) => {
679
+ if (autosaveTimeoutRef.current !== null) {
680
+ window.clearTimeout(autosaveTimeoutRef.current);
681
+ autosaveTimeoutRef.current = null;
682
+ }
683
+ if (!currentSaveRequest || !currentSaveSignature) {
684
+ if (draftTemplate && !draftTemplate.name.trim()) {
685
+ const message = "Project template name is required before it can be saved";
686
+ setSaveError(message);
687
+ if (options?.showErrorToast) {
688
+ toast.error(message);
689
+ }
690
+ return false;
691
+ }
692
+ return true;
693
+ }
694
+ if (currentSaveSignature === lastPersistedSignatureRef.current) {
695
+ return true;
696
+ }
697
+ return persistTemplate(currentSaveRequest, currentSaveSignature, options);
698
+ }, [currentSaveRequest, currentSaveSignature, draftTemplate, persistTemplate]);
699
+ useEffect(() => {
700
+ if (autosaveTimeoutRef.current !== null) {
701
+ window.clearTimeout(autosaveTimeoutRef.current);
702
+ autosaveTimeoutRef.current = null;
703
+ }
704
+ if (!draftTemplate) {
705
+ setIsDirty(false);
706
+ return;
707
+ }
708
+ if (saving) {
709
+ return;
710
+ }
711
+ if (!currentSaveRequest || !currentSaveSignature) {
712
+ if (!draftTemplate.name.trim()) {
713
+ setIsDirty(true);
714
+ setSaveError("Project template name is required before it can be saved");
715
+ }
716
+ return;
717
+ }
718
+ if (currentSaveSignature === lastPersistedSignatureRef.current) {
719
+ setIsDirty(false);
720
+ return;
721
+ }
722
+ setIsDirty(true);
723
+ autosaveTimeoutRef.current = window.setTimeout(() => {
724
+ autosaveTimeoutRef.current = null;
725
+ void persistTemplate(currentSaveRequest, currentSaveSignature);
726
+ }, PROJECT_TEMPLATE_AUTOSAVE_DEBOUNCE_MS);
727
+ return () => {
728
+ if (autosaveTimeoutRef.current !== null) {
729
+ window.clearTimeout(autosaveTimeoutRef.current);
730
+ autosaveTimeoutRef.current = null;
731
+ }
732
+ };
733
+ }, [
734
+ currentSaveRequest,
735
+ currentSaveSignature,
736
+ draftTemplate,
737
+ persistTemplate,
738
+ saving,
739
+ ]);
740
+ useEffect(() => {
741
+ return () => {
742
+ if (autosaveTimeoutRef.current !== null) {
743
+ window.clearTimeout(autosaveTimeoutRef.current);
744
+ }
745
+ };
746
+ }, []);
747
+ const loadTemplatesRef = useRef(loadTemplates);
748
+ useEffect(() => {
749
+ loadTemplatesRef.current = loadTemplates;
750
+ }, [loadTemplates]);
751
+ useEffect(() => {
752
+ selectedTemplateIdRef.current = selectedTemplateId;
753
+ }, [selectedTemplateId]);
754
+ useEffect(() => {
755
+ selectedTaskIdRef.current = selectedTaskId;
756
+ }, [selectedTaskId]);
757
+ const handleCreateTemplate = useCallback(async () => {
758
+ const canContinue = await flushPendingSave({ showErrorToast: true });
759
+ if (!canContinue) {
760
+ return;
761
+ }
762
+ const nextTemplate = createEmptyTemplate();
763
+ selectedTemplateIdRef.current = nextTemplate.id;
764
+ draftTemplateRef.current = nextTemplate;
765
+ setTemplates((current) => sortTemplates([nextTemplate, ...current]));
766
+ setSelectedTemplateId(nextTemplate.id);
767
+ setDraftTemplate(cloneTemplate(nextTemplate));
768
+ setSelectedTaskId(null);
769
+ setIsDirty(true);
770
+ setSaveError(null);
771
+ lastPersistedSignatureRef.current = null;
772
+ latestDraftSignatureRef.current = null;
773
+ editContext?.updateUrl({
774
+ [SELECTED_PROJECT_TEMPLATE_QUERY_PARAM]: nextTemplate.id,
775
+ });
776
+ }, [editContext, flushPendingSave]);
777
+ const handleDeleteTemplate = useCallback(() => {
778
+ if (!draftTemplate)
779
+ return;
780
+ const templateId = draftTemplate.id;
781
+ const templateName = draftTemplate.name.trim() || "Untitled template";
782
+ const runDelete = async () => {
783
+ if (autosaveTimeoutRef.current !== null) {
784
+ window.clearTimeout(autosaveTimeoutRef.current);
785
+ autosaveTimeoutRef.current = null;
786
+ }
787
+ const existsOnServer = templates.some((template) => template.id === templateId);
788
+ if (!existsOnServer) {
789
+ const remainingTemplates = templates.filter((template) => template.id !== templateId);
790
+ setTemplates(remainingTemplates);
791
+ selectTemplate(remainingTemplates[0] ?? null);
792
+ return;
793
+ }
794
+ setDeleting(true);
795
+ try {
796
+ const result = await deleteProjectTemplate(templateId);
797
+ if (result.type !== "success") {
798
+ toast.error(result.summary || "Failed to delete project template");
799
+ return;
800
+ }
801
+ toast.success("Project template deleted");
802
+ const fallbackId = templates.find((template) => template.id !== templateId)?.id;
803
+ await loadTemplates(fallbackId ?? null);
804
+ }
805
+ finally {
806
+ setDeleting(false);
807
+ }
808
+ };
809
+ if (editContext?.confirm) {
810
+ const existsOnServer = templates.some((template) => template.id === templateId);
811
+ editContext.confirm({
812
+ header: "Delete project template",
813
+ message: existsOnServer
814
+ ? `Are you sure you want to delete "${templateName}"?\n\nThis action cannot be undone.`
815
+ : `Remove "${templateName}" from the list?\n\nThis template has not been saved yet.`,
816
+ acceptLabel: "Delete",
817
+ showCancel: true,
818
+ accept: () => {
819
+ void runDelete();
820
+ },
821
+ });
822
+ return;
823
+ }
824
+ void runDelete();
825
+ }, [draftTemplate, editContext, loadTemplates, selectTemplate, templates]);
826
+ const handleSelectTemplate = useCallback(async (template) => {
827
+ const canContinue = await flushPendingSave({ showErrorToast: true });
828
+ if (!canContinue) {
829
+ return;
830
+ }
831
+ selectTemplate(template);
832
+ }, [flushPendingSave, selectTemplate]);
833
+ const handleRefreshTemplates = useCallback(async () => {
834
+ const canContinue = await flushPendingSave({ showErrorToast: true });
835
+ if (!canContinue) {
836
+ return;
837
+ }
838
+ await loadTemplates(selectedTemplateId);
839
+ }, [flushPendingSave, loadTemplates, selectedTemplateId]);
840
+ const handleResetTemplateAgent = useCallback(async () => {
841
+ if (!selectedTemplateId)
842
+ return;
843
+ const canContinue = await flushPendingSave({ showErrorToast: true });
844
+ if (!canContinue) {
845
+ return;
846
+ }
847
+ setTemplateAgentResetting(true);
848
+ setTemplateAgentError(null);
849
+ try {
850
+ const result = await resetProjectTemplateAgent(selectedTemplateId);
851
+ if (result.type !== "success" || !result.data) {
852
+ const message = result.summary || "Failed to restart the template assistant";
853
+ setTemplateAgentError(message);
854
+ toast.error(message);
855
+ return;
856
+ }
857
+ setTemplateAgentId(result.data.agentId);
858
+ const currentTemplate = draftTemplateRef.current;
859
+ if (currentTemplate?.id === selectedTemplateId) {
860
+ await syncProjectTemplateAgentContext(result.data.agentId, currentTemplate, selectedTaskIdRef.current);
861
+ }
862
+ toast.success("Template assistant restarted");
863
+ }
864
+ catch (error) {
865
+ const message = error instanceof Error
866
+ ? error.message
867
+ : "Failed to restart the template assistant";
868
+ setTemplateAgentError(message);
869
+ toast.error(message);
870
+ }
871
+ finally {
872
+ setTemplateAgentResetting(false);
873
+ }
874
+ }, [flushPendingSave, selectedTemplateId, syncProjectTemplateAgentContext]);
875
+ const handleOpenCreateTaskDialog = useCallback(async () => {
876
+ if (!draftTemplate)
877
+ return;
878
+ const canContinue = await flushPendingSave({ showErrorToast: true });
879
+ if (!canContinue) {
880
+ return;
881
+ }
882
+ setNewTaskDependencySourceId(null);
883
+ setNewTaskTitle("");
884
+ setIsCreateTaskDialogOpen(true);
885
+ }, [draftTemplate, flushPendingSave]);
886
+ const handleOpenCreateDependentTaskDialogForTask = useCallback(async (taskId) => {
887
+ if (!draftTemplate)
888
+ return;
889
+ const canContinue = await flushPendingSave({ showErrorToast: true });
890
+ if (!canContinue) {
891
+ return;
892
+ }
893
+ setSelectedTaskId(taskId);
894
+ setNewTaskDependencySourceId(taskId);
895
+ setNewTaskTitle("");
896
+ setIsCreateTaskDialogOpen(true);
897
+ }, [draftTemplate, flushPendingSave]);
898
+ const handleOpenAddChildTaskDialogForTask = useCallback(async (taskId) => {
899
+ if (!draftTemplate)
900
+ return;
901
+ const canContinue = await flushPendingSave({ showErrorToast: true });
902
+ if (!canContinue) {
903
+ return;
904
+ }
905
+ setSelectedTaskId(taskId);
906
+ setNewChildTaskTitle("");
907
+ setIsAddChildTaskDialogOpen(true);
908
+ }, [draftTemplate, flushPendingSave]);
909
+ const handleConfirmCreateTask = useCallback(async () => {
910
+ if (!draftTemplate)
911
+ return;
912
+ const trimmedTitle = newTaskTitle.trim();
913
+ if (!trimmedTitle) {
914
+ toast.error("Task title is required");
915
+ return;
916
+ }
917
+ if (!draftTemplate.name.trim()) {
918
+ const message = "Project template name is required before it can be saved";
919
+ setSaveError(message);
920
+ toast.error(message);
921
+ return;
922
+ }
923
+ if (autosaveTimeoutRef.current !== null) {
924
+ window.clearTimeout(autosaveTimeoutRef.current);
925
+ autosaveTimeoutRef.current = null;
926
+ }
927
+ const nextTask = createEmptyTaskTemplate(draftTemplate.taskTemplates.length, trimmedTitle);
928
+ if (newTaskDependencySourceId) {
929
+ nextTask.dependencyIds = [newTaskDependencySourceId];
930
+ }
931
+ const nextTemplate = cloneTemplate(draftTemplate);
932
+ nextTemplate.taskTemplates = [...nextTemplate.taskTemplates, nextTask];
933
+ const request = buildUpsertTemplateRequest(nextTemplate);
934
+ const signature = JSON.stringify(request);
935
+ latestDraftSignatureRef.current = signature;
936
+ setDraftTemplate(cloneTemplate(nextTemplate));
937
+ setTemplates((existingTemplates) => sortTemplates([
938
+ ...existingTemplates.filter((template) => template.id !== nextTemplate.id),
939
+ cloneTemplate(nextTemplate),
940
+ ]));
941
+ setSelectedTaskId(nextTask.id);
942
+ setIsDirty(true);
943
+ setSaveError(null);
944
+ setCreatingTask(true);
945
+ try {
946
+ const saved = await persistTemplate(request, signature, {
947
+ showErrorToast: true,
948
+ });
949
+ if (saved) {
950
+ setIsCreateTaskDialogOpen(false);
951
+ setNewTaskDependencySourceId(null);
952
+ setNewTaskTitle("");
953
+ toast.success(newTaskDependencySourceId
954
+ ? "Dependent task template created"
955
+ : "Task template created");
956
+ }
957
+ }
958
+ finally {
959
+ setCreatingTask(false);
960
+ }
961
+ }, [
962
+ draftTemplate,
963
+ newTaskDependencySourceId,
964
+ newTaskTitle,
965
+ persistTemplate,
966
+ ]);
967
+ const handleOpenAddChildTaskDialog = useCallback(async () => {
968
+ if (!draftTemplate || !selectedTaskId)
969
+ return;
970
+ await handleOpenAddChildTaskDialogForTask(selectedTaskId);
971
+ }, [draftTemplate, handleOpenAddChildTaskDialogForTask, selectedTaskId]);
972
+ const handleConfirmCreateChildTask = useCallback(async () => {
973
+ if (!draftTemplate || !selectedTaskId)
974
+ return;
975
+ const trimmedTitle = newChildTaskTitle.trim();
976
+ if (!trimmedTitle) {
977
+ toast.error("Task title is required");
978
+ return;
979
+ }
980
+ if (!draftTemplate.name.trim()) {
981
+ const message = "Project template name is required before it can be saved";
982
+ setSaveError(message);
983
+ toast.error(message);
984
+ return;
985
+ }
986
+ if (autosaveTimeoutRef.current !== null) {
987
+ window.clearTimeout(autosaveTimeoutRef.current);
988
+ autosaveTimeoutRef.current = null;
989
+ }
990
+ const nextTask = createEmptyTaskTemplate(draftTemplate.taskTemplates.length, trimmedTitle);
991
+ nextTask.parentTaskId = selectedTaskId;
992
+ nextTask.dependencyIds = [selectedTaskId];
993
+ const nextTemplate = cloneTemplate(draftTemplate);
994
+ nextTemplate.taskTemplates = [...nextTemplate.taskTemplates, nextTask];
995
+ const request = buildUpsertTemplateRequest(nextTemplate);
996
+ const signature = JSON.stringify(request);
997
+ latestDraftSignatureRef.current = signature;
998
+ setDraftTemplate(cloneTemplate(nextTemplate));
999
+ setTemplates((existingTemplates) => sortTemplates([
1000
+ ...existingTemplates.filter((template) => template.id !== nextTemplate.id),
1001
+ cloneTemplate(nextTemplate),
1002
+ ]));
1003
+ // Keep the parent task selected so the new child appears in "Child tasks" immediately.
1004
+ setIsDirty(true);
1005
+ setSaveError(null);
1006
+ setCreatingChildTask(true);
1007
+ try {
1008
+ const saved = await persistTemplate(request, signature, {
1009
+ showErrorToast: true,
1010
+ });
1011
+ if (saved) {
1012
+ setIsAddChildTaskDialogOpen(false);
1013
+ setNewChildTaskTitle("");
1014
+ toast.success("Child task template created");
1015
+ }
1016
+ }
1017
+ finally {
1018
+ setCreatingChildTask(false);
1019
+ }
1020
+ }, [draftTemplate, newChildTaskTitle, persistTemplate, selectedTaskId]);
1021
+ const handleRemoveDependency = useCallback((dependencyId) => {
1022
+ updateSelectedTask((currentTask) => ({
1023
+ ...currentTask,
1024
+ parentTaskId: currentTask.parentTaskId === dependencyId
1025
+ ? null
1026
+ : currentTask.parentTaskId,
1027
+ dependencyIds: currentTask.dependencyIds.filter((id) => id !== dependencyId),
1028
+ }));
1029
+ }, [updateSelectedTask]);
1030
+ const handleAddDependencyToTask = useCallback((taskId, dependencyId) => {
1031
+ const currentTemplate = draftTemplateRef.current;
1032
+ if (!canAddTemplateDependency(currentTemplate, taskId, dependencyId)) {
1033
+ return false;
1034
+ }
1035
+ applyDraftChange((template) => ({
1036
+ ...template,
1037
+ taskTemplates: template.taskTemplates.map((task) => task.id === taskId
1038
+ ? {
1039
+ ...task,
1040
+ dependencyIds: Array.from(new Set([...(task.dependencyIds ?? []), dependencyId])),
1041
+ }
1042
+ : task),
1043
+ }));
1044
+ return true;
1045
+ }, [applyDraftChange]);
1046
+ const handleAddDependency = useCallback((dependencyId) => {
1047
+ if (!selectedTaskId)
1048
+ return;
1049
+ handleAddDependencyToTask(selectedTaskId, dependencyId);
1050
+ setDependencyPickerOpen(false);
1051
+ }, [handleAddDependencyToTask, selectedTaskId]);
1052
+ const handleCreateDependencyFromGraph = useCallback((dependsOnTaskId, taskId) => {
1053
+ const didAddDependency = handleAddDependencyToTask(taskId, dependsOnTaskId);
1054
+ if (!didAddDependency) {
1055
+ return;
1056
+ }
1057
+ setSelectedTaskId(taskId);
1058
+ }, [handleAddDependencyToTask]);
1059
+ const handleCreateChildRelationshipFromGraph = useCallback((parentTaskId, childTaskId) => {
1060
+ const currentTemplate = draftTemplateRef.current;
1061
+ const taskTemplates = currentTemplate?.taskTemplates ?? [];
1062
+ const childTask = taskTemplates.find((task) => task.id === childTaskId);
1063
+ if (!childTask || childTaskId === parentTaskId) {
1064
+ return;
1065
+ }
1066
+ const parentByTaskId = new Map(taskTemplates.map((task) => [task.id, task.parentTaskId ?? null]));
1067
+ let ancestorTaskId = parentByTaskId.get(parentTaskId) ?? null;
1068
+ while (ancestorTaskId) {
1069
+ if (ancestorTaskId === childTaskId) {
1070
+ return;
1071
+ }
1072
+ ancestorTaskId = parentByTaskId.get(ancestorTaskId) ?? null;
1073
+ }
1074
+ applyDraftChange((template) => ({
1075
+ ...template,
1076
+ taskTemplates: template.taskTemplates.map((task) => {
1077
+ if (task.id !== childTaskId) {
1078
+ return task;
1079
+ }
1080
+ const existingParentTaskId = task.parentTaskId ?? null;
1081
+ return {
1082
+ ...task,
1083
+ parentTaskId,
1084
+ dependencyIds: Array.from(new Set([
1085
+ ...task.dependencyIds.filter((id) => id !== existingParentTaskId),
1086
+ parentTaskId,
1087
+ ])),
1088
+ };
1089
+ }),
1090
+ }));
1091
+ setSelectedTaskId(childTaskId);
1092
+ }, [applyDraftChange]);
1093
+ const handleUnlinkChildTask = useCallback((childTaskId) => {
1094
+ if (!selectedTaskId)
1095
+ return;
1096
+ applyDraftChange((currentTemplate) => ({
1097
+ ...currentTemplate,
1098
+ taskTemplates: currentTemplate.taskTemplates.map((task) => task.id === childTaskId
1099
+ ? {
1100
+ ...task,
1101
+ parentTaskId: null,
1102
+ dependencyIds: task.dependencyIds.filter((id) => id !== selectedTaskId),
1103
+ }
1104
+ : task),
1105
+ }));
1106
+ }, [applyDraftChange, selectedTaskId]);
1107
+ const removeTaskTemplateById = useCallback((taskId) => {
1108
+ const template = draftTemplateRef.current;
1109
+ if (!template?.taskTemplates.some((t) => t.id === taskId))
1110
+ return;
1111
+ const taskTitle = template.taskTemplates.find((t) => t.id === taskId)?.title ??
1112
+ "Untitled task";
1113
+ const runDelete = () => {
1114
+ const current = draftTemplateRef.current;
1115
+ if (!current?.taskTemplates.some((t) => t.id === taskId))
1116
+ return;
1117
+ const remainingTasks = current.taskTemplates.filter((task) => task.id !== taskId);
1118
+ applyDraftChange((currentTemplate) => ({
1119
+ ...currentTemplate,
1120
+ graphLayout: currentTemplate.graphLayout
1121
+ ? {
1122
+ ...currentTemplate.graphLayout,
1123
+ nodes: currentTemplate.graphLayout.nodes.filter((node) => node.taskId !== taskId),
1124
+ }
1125
+ : null,
1126
+ taskTemplates: currentTemplate.taskTemplates
1127
+ .filter((task) => task.id !== taskId)
1128
+ .map((task) => ({
1129
+ ...task,
1130
+ parentTaskId: task.parentTaskId === taskId ? null : task.parentTaskId,
1131
+ dependencyIds: task.dependencyIds.filter((id) => id !== taskId),
1132
+ })),
1133
+ }));
1134
+ setSelectedTaskId((prev) => {
1135
+ if (prev === taskId) {
1136
+ return remainingTasks[0]?.id ?? null;
1137
+ }
1138
+ return remainingTasks.some((t) => t.id === prev)
1139
+ ? prev
1140
+ : (remainingTasks[0]?.id ?? null);
1141
+ });
1142
+ };
1143
+ if (editContext?.confirm) {
1144
+ editContext.confirm({
1145
+ header: "Remove task template",
1146
+ message: `Are you sure you want to remove "${taskTitle}"?\n\nOther tasks will no longer depend on this task.`,
1147
+ acceptLabel: "Remove",
1148
+ showCancel: true,
1149
+ accept: runDelete,
1150
+ });
1151
+ return;
1152
+ }
1153
+ runDelete();
1154
+ }, [applyDraftChange, editContext]);
1155
+ const handleDeleteSelectedTask = useCallback(() => {
1156
+ if (!selectedTaskId)
1157
+ return;
1158
+ removeTaskTemplateById(selectedTaskId);
1159
+ }, [removeTaskTemplateById, selectedTaskId]);
1160
+ const handleProjectTemplateEditorKeyDownCapture = useCallback((event) => {
1161
+ if (event.key !== "Delete")
1162
+ return;
1163
+ if (!selectedTaskId)
1164
+ return;
1165
+ if (isTypingEventTarget(event.nativeEvent))
1166
+ return;
1167
+ event.preventDefault();
1168
+ event.stopPropagation();
1169
+ handleDeleteSelectedTask();
1170
+ }, [handleDeleteSelectedTask, selectedTaskId]);
1171
+ const listContent = (_jsx("div", { className: "h-full overflow-auto p-4", "data-testid": "project-template-list-pane", children: _jsxs("div", { className: "flex h-full flex-col overflow-hidden", children: [_jsxs("div", { className: "shrink-0 space-y-4 pb-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("div", { className: "flex min-h-5 items-center gap-2 text-xs text-gray-700", children: state === "loading" ? (_jsx(RefreshCw, { className: "h-4 w-4 animate-spin text-amber-600", strokeWidth: 1.5 })) : state === "error" ? (_jsx(AlertCircle, { className: "h-4 w-4 text-red-600", strokeWidth: 1.5 })) : null }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { size: "sm", variant: "outline", onClick: () => void handleRefreshTemplates(), disabled: state === "loading" || deleting, "data-testid": "project-template-refresh-button", children: _jsx(RefreshCw, { className: cn("h-4 w-4", state === "loading" && "animate-spin"), strokeWidth: 1.5 }) }), _jsxs(Button, { size: "sm", onClick: () => void handleCreateTemplate(), "data-testid": "project-template-create-button", children: [_jsx(Plus, { className: "h-4 w-4", strokeWidth: 1.5 }), "Create Template"] })] })] }), _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400", strokeWidth: 1.5 }), _jsx(Input, { type: "text", placeholder: "Search templates...", value: searchQuery, onChange: (event) => setSearchQuery(event.target.value), className: "pl-9 text-xs md:text-xs", "data-testid": "project-template-search-input" })] }), error && (_jsx("div", { className: "rounded border border-red-200 bg-red-50 p-2 text-xs whitespace-pre-wrap text-red-700", children: error }))] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: filteredTemplates.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center", children: [_jsx(GitBranch, { className: "mb-4 h-10 w-10 text-gray-300", strokeWidth: 1.2 }), _jsx("p", { className: "text-xs text-gray-500", children: searchQuery
1172
+ ? "No templates match your search"
1173
+ : "No project templates found" })] })) : (_jsx("div", { className: "space-y-3", children: filteredTemplates.map((template) => {
1174
+ const isSelected = selectedTemplateId === template.id;
1175
+ return (_jsx("button", { type: "button", onClick: () => void handleSelectTemplate(template), "data-testid": `project-template-list-item-${template.id}`, className: cn("w-full rounded-lg border p-4 text-left transition-shadow hover:shadow-sm", isSelected
1176
+ ? "border-blue-400 bg-blue-50"
1177
+ : "border-gray-200 bg-white"), children: _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [template.isSystem && (_jsx("span", { title: "System template", className: "inline-flex", children: _jsx(Shield, { className: "h-3.5 w-3.5 shrink-0 text-amber-600", strokeWidth: 1.5, "aria-label": "System template" }) })), _jsx("h4", { className: "font-medium text-gray-900", children: template.name }), template.disabled && (_jsx(Badge, { variant: "outline", className: "text-[10px] uppercase", children: "Disabled" })), template.hideFromCreateDialog && (_jsx(Badge, { variant: "outline", className: "text-[10px] uppercase", children: "Hidden" })), template.openInWizardMode && (_jsx(Badge, { variant: "outline", className: "text-[10px] uppercase", children: "Wizard" }))] }), template.description && (_jsx("div", { className: "mt-2 line-clamp-2 text-xs text-gray-600", children: template.description })), _jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-2 text-xs text-gray-500", children: [_jsxs("span", { children: [template.taskTemplates.length, " task templates"] }), template.defaultCostLimit != null && (_jsxs("span", { children: ["Default budget: ", template.defaultCostLimit] }))] })] }) }, template.id));
1178
+ }) })) })] }) }));
1179
+ const detailContent = draftTemplate ? (_jsxs("div", { className: "flex h-full flex-col bg-gray-50/40", "data-testid": "project-template-detail-pane", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-gray-200 bg-white px-5 py-3.5", children: [_jsx("div", { className: "min-w-0", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-gray-900 text-white", children: _jsx(GitBranch, { className: "h-4 w-4", strokeWidth: 1.5 }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "truncate text-xs font-semibold text-gray-900", children: draftTemplate.name || "Project Template" }), _jsxs("div", { className: "mt-0.5 flex min-h-[20px] items-center gap-2 text-[11px] text-gray-500", children: [_jsxs("span", { children: [draftTemplate.taskTemplates.length, " task templates"] }), headerStatusBadge] })] })] }) }), _jsx("div", { className: "flex shrink-0 items-center gap-2", children: _jsxs(Button, { variant: "outline", size: "sm", onClick: handleDeleteTemplate, disabled: deleting, "data-testid": "project-template-delete-button", children: [_jsx(Trash2, { className: "h-4 w-4", strokeWidth: 1.5 }), deleting ? "Deleting..." : "Delete"] }) })] }), _jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-5 overflow-hidden p-5", children: [saveError && (_jsx("div", { className: "rounded border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-700", children: saveError })), _jsxs("section", { className: "shrink-0 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm", children: [_jsxs("button", { type: "button", className: "flex w-full items-center justify-between gap-3 px-4 py-3 text-left", onClick: () => setIsBasicSettingsExpanded((currentValue) => !currentValue), "aria-expanded": isBasicSettingsExpanded, "aria-controls": "project-template-basic-settings", "data-testid": "project-template-basic-settings-toggle", children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx("span", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-gray-100 text-gray-500", children: _jsx(Settings2, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-xs font-semibold tracking-wide text-gray-800", children: "Basic Settings" }), _jsx("p", { className: "text-[11px] leading-tight text-gray-400", children: "Configure the shared defaults for this project template." })] })] }), _jsx(ChevronDown, { className: cn("h-4 w-4 shrink-0 text-gray-400 transition-transform", isBasicSettingsExpanded ? "rotate-180" : "rotate-0"), strokeWidth: 1.5 })] }), isBasicSettingsExpanded && (_jsxs("div", { id: "project-template-basic-settings", className: "grid gap-4 border-t border-gray-100 p-4 md:grid-cols-2", children: [_jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Template name" }), _jsx(Input, { value: draftTemplate.name, onChange: (event) => applyDraftChange((currentTemplate) => ({
1180
+ ...currentTemplate,
1181
+ name: event.target.value,
1182
+ })), placeholder: "Project template name", className: "text-xs md:text-xs", "data-testid": "project-template-name-input" })] }), _jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Description" }), _jsx(Textarea, { className: "text-xs", value: draftTemplate.description ?? "", onChange: (event) => applyDraftChange((currentTemplate) => ({
1183
+ ...currentTemplate,
1184
+ description: event.target.value,
1185
+ })), rows: 4, placeholder: "Describe when to use this template", "data-testid": "project-template-description-input" })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { className: "text-xs", children: "Default cost limit" }), _jsx(Input, { type: "number", value: draftTemplate.defaultCostLimit ?? "", onChange: (event) => applyDraftChange((currentTemplate) => ({
1186
+ ...currentTemplate,
1187
+ defaultCostLimit: event.target.value.trim() === ""
1188
+ ? null
1189
+ : Number(event.target.value),
1190
+ })), placeholder: "0", className: "text-xs md:text-xs", "data-testid": "project-template-default-cost-limit-input" })] }), _jsxs("div", { className: "flex items-center justify-between gap-4 md:col-span-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-xs font-medium text-zinc-900", children: "Disabled" }), _jsx("div", { className: "mt-0.5 text-xs text-zinc-500", children: "Makes the template unavailable for project creation and template resolution." })] }), _jsx(Switch, { checked: draftTemplate.disabled === true, onCheckedChange: (checked) => applyDraftChange((currentTemplate) => ({
1191
+ ...currentTemplate,
1192
+ disabled: checked,
1193
+ })), className: "data-[state=checked]:bg-amber-600 data-[state=checked]:shadow-inner dark:data-[state=checked]:bg-amber-600", "aria-label": "Disable this project template", "data-testid": "project-template-disabled-switch" })] }), _jsxs("div", { className: "flex items-center justify-between gap-4 md:col-span-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-xs font-medium text-zinc-900", children: "Hide from create dialog" }), _jsx("div", { className: "mt-0.5 text-xs text-zinc-500", children: "Keeps the template active, but removes it from the create project dialog." })] }), _jsx(Switch, { checked: draftTemplate.hideFromCreateDialog === true, onCheckedChange: (checked) => applyDraftChange((currentTemplate) => ({
1194
+ ...currentTemplate,
1195
+ hideFromCreateDialog: checked,
1196
+ })), "aria-label": "Hide this project template from the create dialog", "data-testid": "project-template-hide-from-create-dialog-switch" })] }), _jsxs("div", { className: "flex items-center justify-between gap-4 md:col-span-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-xs font-medium text-zinc-900", children: "Open in wizard mode" }), _jsx("div", { className: "mt-0.5 text-xs text-zinc-500", children: "New projects created from this template start in taskboard wizard mode." })] }), _jsx(Switch, { checked: draftTemplate.openInWizardMode === true, onCheckedChange: (checked) => applyDraftChange((currentTemplate) => ({
1197
+ ...currentTemplate,
1198
+ openInWizardMode: checked,
1199
+ })), "aria-label": "Open new projects from this template in wizard mode", "data-testid": "project-template-open-in-wizard-mode-switch" })] })] }))] }), _jsxs("section", { className: "flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm", children: [_jsxs("div", { className: "flex shrink-0 items-center justify-between gap-3 border-b border-gray-100 px-4 py-3", children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx("span", { className: "flex h-6 w-6 items-center justify-center rounded-md bg-gray-100 text-gray-500", children: _jsx(GitBranch, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-xs font-semibold tracking-wide text-gray-800", children: "Template Task Editor" }), _jsx("p", { className: "text-[11px] leading-tight text-gray-400", children: "Add task templates, arrange them in the graph, and define dependencies." })] })] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => void handleOpenCreateTaskDialog(), disabled: creatingTask, "data-testid": "project-template-add-task-button", children: [_jsx(Plus, { className: "h-4 w-4", strokeWidth: 1.5 }), "Add Task"] })] }), _jsx("div", { className: "min-h-[360px] flex-1 overflow-hidden border-b border-gray-100", children: _jsx(Splitter, { direction: "vertical", localStorageKey: "settings-project-template-graph-task-splitter-v1", className: "h-full", panels: [
1200
+ {
1201
+ name: "project-template-graph",
1202
+ defaultSize: 400,
1203
+ className: "min-h-0",
1204
+ content: (_jsx("div", { className: "h-full min-h-0 overflow-hidden bg-slate-50/40", "data-testid": "project-template-graph", children: _jsx(DependencyGraphView, { projectId: draftTemplate.id, layoutKey: `project-template:${draftTemplate.id}`, tasks: graphTasks, dependencies: graphDependencies, miniMapWidth: 112, miniMapHeight: 84, orientation: "horizontal", autoLayoutStrategy: "hierarchy", savedLayout: draftTemplate.graphLayout, selectedTaskId: selectedTaskId, onSelectTask: setSelectedTaskId, onAddDependentTaskFromNode: (taskId) => {
1205
+ void handleOpenCreateDependentTaskDialogForTask(taskId);
1206
+ }, onAddChildTaskFromNode: (taskId) => {
1207
+ void handleOpenAddChildTaskDialogForTask(taskId);
1208
+ }, onRemoveTask: removeTaskTemplateById, allowDependencyConnect: true, onCreateDependency: handleCreateDependencyFromGraph, onCreateChildRelationship: handleCreateChildRelationshipFromGraph, canPersistLayout: true, layoutSaveDebounceMs: 0, highlightBlockedTasks: false, showExecutionStateBadges: false, emptyStateTitle: "No task templates yet", emptyStateDescription: "Add task templates to start designing the project flow.", onPersistLayout: async (layout) => {
1209
+ const nextLayout = {
1210
+ ...layout,
1211
+ projectTemplateId: draftTemplate.id,
1212
+ };
1213
+ applyDraftChange((currentTemplate) => ({
1214
+ ...currentTemplate,
1215
+ graphLayout: nextLayout,
1216
+ }));
1217
+ return nextLayout;
1218
+ } }) })),
1219
+ },
1220
+ {
1221
+ name: "project-template-task-details",
1222
+ defaultSize: "auto",
1223
+ className: "min-h-0",
1224
+ content: (_jsx("div", { className: "flex h-full min-h-0 flex-col overflow-hidden", "data-testid": "project-template-task-detail-pane", children: !selectedTask ? (_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-4", children: _jsx("div", { className: "rounded-lg border border-dashed border-gray-200 bg-gray-50 p-6 text-center text-xs text-gray-500", children: "Select a task node to edit its details." }) })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "shrink-0 border-b border-gray-100 bg-white px-4 pb-3 pt-4", children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-xs font-semibold text-gray-900", "data-testid": "project-template-selected-task-title", children: selectedTask.title || "Untitled Task" }), _jsx("div", { className: "text-xs text-gray-500", children: "Configure task details, assignment, and dependencies." })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: handleDeleteSelectedTask, "data-testid": "project-template-remove-task-button", children: [_jsx(Trash2, { className: "h-4 w-4", strokeWidth: 1.5 }), "Remove Task"] })] }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-4 pb-4 pt-3", children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Task title" }), _jsx(Input, { value: selectedTask.title, onChange: (event) => updateSelectedTask((currentTask) => ({
1225
+ ...currentTask,
1226
+ title: event.target.value,
1227
+ })), placeholder: "Task title", className: "text-xs md:text-xs", "data-testid": "project-template-task-title-input" })] }), _jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Parent task" }), _jsx(Select, { className: "text-xs", value: selectedTask.parentTaskId ?? "", onValueChange: (value) => {
1228
+ const newParentId = value.trim() || null;
1229
+ updateSelectedTask((currentTask) => {
1230
+ const oldParent = currentTask.parentTaskId ?? null;
1231
+ let deps = [
1232
+ ...(currentTask.dependencyIds ?? []),
1233
+ ];
1234
+ if (oldParent &&
1235
+ oldParent !== newParentId) {
1236
+ deps = deps.filter((id) => id !== oldParent);
1237
+ }
1238
+ if (newParentId) {
1239
+ if (!deps.includes(newParentId)) {
1240
+ deps = [...deps, newParentId];
1241
+ }
1242
+ }
1243
+ else if (oldParent) {
1244
+ deps = deps.filter((id) => id !== oldParent);
1245
+ }
1246
+ return {
1247
+ ...currentTask,
1248
+ parentTaskId: newParentId,
1249
+ dependencyIds: deps,
1250
+ };
1251
+ });
1252
+ }, options: parentTaskSelectOptions, placeholder: "No parent", "data-testid": "project-template-task-parent-select" }), _jsx("p", { className: "text-[11px] leading-snug text-gray-500", children: "Marks this task as a child in the template; the parent is also added as a blocking dependency unless already listed." })] }), _jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Description" }), _jsx(Textarea, { className: "text-xs", value: selectedTask.description ?? "", onChange: (event) => updateSelectedTask((currentTask) => ({
1253
+ ...currentTask,
1254
+ description: event.target.value,
1255
+ })), rows: 4, placeholder: "Describe what this task should accomplish", "data-testid": "project-template-task-description-input" })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { className: "text-xs", children: "Priority" }), _jsx(Select, { className: "text-xs", value: selectedTask.priority ?? "", onValueChange: (value) => updateSelectedTask((currentTask) => ({
1256
+ ...currentTask,
1257
+ priority: value || null,
1258
+ })), options: PRIORITY_OPTIONS, "data-testid": "project-template-task-priority-select" })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { className: "text-xs", children: "Default assignment type" }), _jsx(Select, { className: "text-xs", value: selectedTask.assigneeType ?? "", onValueChange: (value) => updateSelectedTask((currentTask) => ({
1259
+ ...currentTask,
1260
+ assigneeType: (value ||
1261
+ null),
1262
+ assigneeId: value
1263
+ ? (currentTask.assigneeId ?? null)
1264
+ : null,
1265
+ agentProfileId: value === "Agent"
1266
+ ? (currentTask.agentProfileId ?? null)
1267
+ : null,
1268
+ })), options: ASSIGNEE_TYPE_OPTIONS, "data-testid": "project-template-task-assignee-type-select" })] }), selectedTask.assigneeType === "Agent" ? (_jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Agent profile" }), _jsx(Select, { className: "text-xs", value: selectedTask.assigneeId ??
1269
+ selectedTask.agentProfileId ??
1270
+ "", onValueChange: (value) => updateSelectedTask((currentTask) => ({
1271
+ ...currentTask,
1272
+ assigneeType: "Agent",
1273
+ assigneeId: value || null,
1274
+ agentProfileId: value || null,
1275
+ })), options: profileOptions, searchable: true, placeholder: "Select an agent profile", "data-testid": "project-template-task-agent-profile-select" }), profilesError && (_jsx("div", { className: "text-xs text-amber-700", children: profilesError }))] })) : selectedTask.assigneeType === "Human" ? (_jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Default user identifier" }), _jsx(Input, { value: selectedTask.assigneeId ?? "", onChange: (event) => updateSelectedTask((currentTask) => ({
1276
+ ...currentTask,
1277
+ assigneeType: "Human",
1278
+ assigneeId: event.target.value,
1279
+ agentProfileId: null,
1280
+ })), placeholder: "sitecore\\\\john", className: "text-xs md:text-xs", "data-testid": "project-template-task-user-assignee-input" })] })) : selectedTask.assigneeType === "Role" ? (_jsxs("div", { className: "grid gap-1.5 md:col-span-2", children: [_jsx(Label, { className: "text-xs", children: "Default role identifier" }), _jsx(Input, { value: selectedTask.assigneeId ?? "", onChange: (event) => updateSelectedTask((currentTask) => ({
1281
+ ...currentTask,
1282
+ assigneeType: "Role",
1283
+ assigneeId: event.target.value,
1284
+ agentProfileId: null,
1285
+ })), placeholder: "sitecore\\\\Author", className: "text-xs md:text-xs", "data-testid": "project-template-task-role-assignee-input" })] })) : null] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "rounded-lg border border-gray-200 bg-gray-50 p-3", "data-testid": "project-template-dependencies-section", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-gray-800", children: [_jsx(Shield, { className: "h-4 w-4 text-gray-500", strokeWidth: 1.5 }), "Dependencies"] }), _jsxs(Popover, { open: dependencyPickerOpen, onOpenChange: setDependencyPickerOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { type: "button", size: "sm", variant: "outline", className: "h-7 gap-1 px-2", disabled: dependencyAddCandidates.length === 0, "aria-label": "Add dependency", "data-testid": "project-template-add-dependency-button", children: _jsx(Plus, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }) }) }), _jsxs(PopoverContent, { align: "end", className: "w-72 p-0", sideOffset: 6, "data-testid": "project-template-dependency-popover", children: [_jsx("div", { className: "border-b border-gray-100 px-3 py-2 text-xs font-medium text-gray-700", children: "Depends on" }), _jsx("div", { className: "max-h-56 overflow-y-auto p-1", children: dependencyAddCandidates.length ===
1286
+ 0 ? (_jsx("div", { className: "px-2 py-3 text-center text-xs text-gray-500", children: "All other tasks are already linked." })) : (dependencyAddCandidates.map((task) => (_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-xs hover:bg-gray-100", onClick: () => handleAddDependency(task.id), "data-testid": `project-template-dependency-option-${task.id}`, children: [_jsx(Link2, { className: "h-3.5 w-3.5 shrink-0 text-gray-400", strokeWidth: 1.5 }), _jsx("span", { className: "min-w-0 truncate font-medium text-gray-900", children: task.title ||
1287
+ "Untitled Task" })] }, task.id)))) })] })] })] }), _jsx("p", { className: "mb-2 text-[11px] leading-snug text-gray-500", children: "This task must wait for these tasks to finish first." }), dependencyTasks.length === 0 ? (_jsx("div", { className: "text-xs text-gray-500", children: draftTemplate.taskTemplates.filter((t) => t.id !== selectedTask.id).length === 0
1288
+ ? "Add more task templates to create dependencies."
1289
+ : "No dependencies yet. Use + to add one." })) : (_jsx("ul", { className: "space-y-1.5", children: dependencyTasks.map((task) => (_jsxs("li", { className: "flex items-center justify-between gap-2 rounded-md border border-gray-200 bg-white px-3 py-2 text-xs", "data-testid": `project-template-dependency-row-${task.id}`, children: [_jsx("span", { className: "min-w-0 truncate font-medium text-gray-800", children: task.title || "Untitled Task" }), _jsx("button", { type: "button", className: "shrink-0 rounded p-0.5 text-gray-400 hover:bg-gray-100 hover:text-gray-700", "aria-label": `Remove dependency on ${task.title}`, onClick: () => handleRemoveDependency(task.id), "data-testid": `project-template-remove-dependency-${task.id}`, children: _jsx(X, { className: "h-4 w-4", strokeWidth: 1.5 }) })] }, task.id))) }))] }), _jsxs("div", { className: "rounded-lg border border-gray-200 bg-gray-50 p-3", "data-testid": "project-template-child-tasks-section", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-gray-800", children: [_jsx(CornerDownRight, { className: "h-4 w-4 text-gray-500", strokeWidth: 1.5 }), "Child tasks"] }), _jsx(Button, { type: "button", size: "sm", variant: "outline", className: "h-7 gap-1 px-2", onClick: () => void handleOpenAddChildTaskDialog(), disabled: creatingChildTask, "aria-label": "Add child task", "data-testid": "project-template-add-child-task-button", children: _jsx(Plus, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }) })] }), _jsx("p", { className: "mb-2 text-[11px] leading-snug text-gray-500", children: "Tasks that run after this one (they depend on this task)." }), childTaskTemplates.length === 0 ? (_jsx("div", { className: "text-xs text-gray-500", children: "No child tasks yet. Use + to add one." })) : (_jsx("ul", { className: "space-y-1.5", children: childTaskTemplates.map((task) => (_jsxs("li", { className: "flex items-center justify-between gap-2 rounded-md border border-gray-200 bg-white px-3 py-2 text-xs", "data-testid": `project-template-child-task-row-${task.id}`, children: [_jsx("span", { className: "min-w-0 truncate font-medium text-gray-800", children: task.title || "Untitled Task" }), _jsx("button", { type: "button", className: "shrink-0 rounded p-0.5 text-gray-400 hover:bg-gray-100 hover:text-gray-700", "aria-label": `Unlink child task ${task.title}`, onClick: () => handleUnlinkChildTask(task.id), "data-testid": `project-template-unlink-child-task-${task.id}`, children: _jsx(X, { className: "h-4 w-4", strokeWidth: 1.5 }) })] }, task.id))) }))] })] })] }) })] })) })),
1290
+ },
1291
+ ] }) })] })] })] })) : (_jsx("div", { className: "flex h-full items-center justify-center bg-gray-50/40", children: _jsxs("div", { className: "max-w-sm rounded-lg border border-dashed border-gray-300 bg-white p-8 text-center", children: [_jsx(GitBranch, { className: "mx-auto mb-3 h-10 w-10 text-gray-300", strokeWidth: 1.2 }), _jsx("h3", { className: "text-xs font-semibold text-gray-900", children: "Select a project template" }), _jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Choose a template from the list or create a new one to start editing." })] }) }));
1292
+ const agentPanelContent = (_jsx(ProjectTemplateAgentPanel, { templateName: draftTemplate?.name ?? null, agentId: templateAgentId, loading: templateAgentLoading, resetting: templateAgentResetting, error: templateAgentError, onStartOver: () => void handleResetTemplateAgent() }));
1293
+ const panels = [
1294
+ {
1295
+ name: "project-template-list",
1296
+ defaultSize: 360,
1297
+ content: listContent,
1298
+ className: "overflow-hidden",
1299
+ },
1300
+ {
1301
+ name: "project-template-detail",
1302
+ defaultSize: "auto",
1303
+ content: detailContent,
1304
+ className: "overflow-hidden",
1305
+ },
1306
+ {
1307
+ name: "project-template-agent",
1308
+ defaultSize: 460,
1309
+ content: agentPanelContent,
1310
+ className: "overflow-hidden",
1311
+ collapsible: true,
1312
+ },
1313
+ ];
1314
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "h-full", "data-testid": "project-template-editor", onKeyDownCapture: handleProjectTemplateEditorKeyDownCapture, children: _jsx(Splitter, { panels: panels, localStorageKey: "settings-project-templates-panel-splitter-v2", direction: "horizontal", className: "h-full" }) }), _jsx(Dialog, { open: isCreateTaskDialogOpen, onOpenChange: (open) => {
1315
+ setIsCreateTaskDialogOpen(open);
1316
+ if (!open) {
1317
+ setNewTaskDependencySourceId(null);
1318
+ }
1319
+ }, children: _jsxs(DialogContent, { className: "max-w-md", "data-testid": "project-template-create-task-dialog", children: [_jsx(StyledDialogTitle, { icon: _jsx(Plus, { strokeWidth: 1.5 }), title: newTaskDependencySourceId
1320
+ ? "Add Dependent Task"
1321
+ : "Add Task Template", subtitle: newTaskDependencySourceId
1322
+ ? "Creates a new task that depends on the selected task."
1323
+ : "Create the task and save it immediately." }), _jsxs("div", { className: "grid gap-2 p-6", children: [_jsx(Label, { className: "text-xs", htmlFor: "project-template-new-task-title", children: "Task title" }), _jsx(Input, { id: "project-template-new-task-title", value: newTaskTitle, onChange: (event) => setNewTaskTitle(event.target.value), placeholder: "e.g. Discover Sitecore Setup", className: "text-xs md:text-xs", autoFocus: true, onKeyDown: (event) => {
1324
+ if (event.key === "Enter" && !event.shiftKey) {
1325
+ event.preventDefault();
1326
+ void handleConfirmCreateTask();
1327
+ }
1328
+ } })] }), _jsxs(DialogFooter, { className: "px-6 pt-0 pb-6", children: [_jsx(Button, { variant: "outline", onClick: () => setIsCreateTaskDialogOpen(false), disabled: creatingTask, "data-testid": "project-template-create-task-cancel-button", children: "Cancel" }), _jsx(Button, { onClick: () => void handleConfirmCreateTask(), disabled: creatingTask || !newTaskTitle.trim(), "data-testid": "project-template-create-task-submit-button", children: creatingTask
1329
+ ? "Creating..."
1330
+ : newTaskDependencySourceId
1331
+ ? "Create Dependent Task"
1332
+ : "Create Task" })] })] }) }), _jsx(Dialog, { open: isAddChildTaskDialogOpen, onOpenChange: setIsAddChildTaskDialogOpen, children: _jsxs(DialogContent, { className: "max-w-md", "data-testid": "project-template-create-child-task-dialog", children: [_jsx(StyledDialogTitle, { icon: _jsx(Plus, { strokeWidth: 1.5 }), title: "Add Child Task", subtitle: "Creates a new task that depends on the current task." }), _jsxs("div", { className: "grid gap-2 p-6", children: [_jsx(Label, { className: "text-xs", htmlFor: "project-template-new-child-task-title", children: "Task title" }), _jsx(Input, { id: "project-template-new-child-task-title", value: newChildTaskTitle, onChange: (event) => setNewChildTaskTitle(event.target.value), placeholder: "e.g. Review content", className: "text-xs md:text-xs", autoFocus: true, onKeyDown: (event) => {
1333
+ if (event.key === "Enter" && !event.shiftKey) {
1334
+ event.preventDefault();
1335
+ void handleConfirmCreateChildTask();
1336
+ }
1337
+ } })] }), _jsxs(DialogFooter, { className: "px-6 pt-0 pb-6", children: [_jsx(Button, { variant: "outline", onClick: () => setIsAddChildTaskDialogOpen(false), disabled: creatingChildTask, "data-testid": "project-template-create-child-task-cancel-button", children: "Cancel" }), _jsx(Button, { onClick: () => void handleConfirmCreateChildTask(), disabled: creatingChildTask || !newChildTaskTitle.trim(), "data-testid": "project-template-create-child-task-submit-button", children: creatingChildTask ? "Creating..." : "Create Task" })] })] }) })] }));
1338
+ }
1339
+ export default ProjectTemplatesPanel;
1340
+ //# sourceMappingURL=ProjectTemplatesPanel.js.map