@parhelia/core 0.1.11873 → 0.1.11955

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 (247) hide show
  1. package/dist/agents-view/AgentsInbox.d.ts +1 -1
  2. package/dist/agents-view/AgentsInbox.js +15 -2
  3. package/dist/agents-view/AgentsInbox.js.map +1 -1
  4. package/dist/agents-view/AgentsSidebar.d.ts +20 -0
  5. package/dist/agents-view/AgentsSidebar.js +21 -0
  6. package/dist/agents-view/AgentsSidebar.js.map +1 -0
  7. package/dist/agents-view/AgentsView.d.ts +6 -7
  8. package/dist/agents-view/AgentsView.js +63 -25
  9. package/dist/agents-view/AgentsView.js.map +1 -1
  10. package/dist/agents-view/AgentsWorkspaceView.d.ts +2 -6
  11. package/dist/agents-view/AgentsWorkspaceView.js +242 -112
  12. package/dist/agents-view/AgentsWorkspaceView.js.map +1 -1
  13. package/dist/components/ui/context-menu.js +24 -9
  14. package/dist/components/ui/context-menu.js.map +1 -1
  15. package/dist/components/ui/select.js +1 -1
  16. package/dist/components/ui/select.js.map +1 -1
  17. package/dist/config/config.js +15 -11
  18. package/dist/config/config.js.map +1 -1
  19. package/dist/editor/ContentTree.js +2 -2
  20. package/dist/editor/ContentTree.js.map +1 -1
  21. package/dist/editor/ContextMenu.js +11 -5
  22. package/dist/editor/ContextMenu.js.map +1 -1
  23. package/dist/editor/FieldListField.js +1 -1
  24. package/dist/editor/FieldListField.js.map +1 -1
  25. package/dist/editor/MainLayout.js.map +1 -1
  26. package/dist/editor/MobileLayout.js +19 -9
  27. package/dist/editor/MobileLayout.js.map +1 -1
  28. package/dist/editor/ai/AgentStatusBadge.d.ts +1 -1
  29. package/dist/editor/ai/AgentStatusBadge.js +18 -2
  30. package/dist/editor/ai/AgentStatusBadge.js.map +1 -1
  31. package/dist/editor/ai/AgentTerminal.js +342 -55
  32. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  33. package/dist/editor/ai/AiResponseMessage.js +46 -4
  34. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  35. package/dist/editor/ai/ContextInfoBar.js +151 -5
  36. package/dist/editor/ai/ContextInfoBar.js.map +1 -1
  37. package/dist/editor/ai/EditOperationsPanel.d.ts +2 -1
  38. package/dist/editor/ai/EditOperationsPanel.js +6 -1
  39. package/dist/editor/ai/EditOperationsPanel.js.map +1 -1
  40. package/dist/editor/ai/dialogs/AgentDialogHandler.js +64 -15
  41. package/dist/editor/ai/dialogs/AgentDialogHandler.js.map +1 -1
  42. package/dist/editor/ai/dialogs/QuestionnaireInline.js +111 -20
  43. package/dist/editor/ai/dialogs/QuestionnaireInline.js.map +1 -1
  44. package/dist/editor/ai/dialogs/agentDialogTypes.d.ts +24 -0
  45. package/dist/editor/ai/dialogs/agentDialogTypes.js.map +1 -1
  46. package/dist/editor/ai/useAgentStatus.d.ts +1 -0
  47. package/dist/editor/ai/useAgentStatus.js +74 -29
  48. package/dist/editor/ai/useAgentStatus.js.map +1 -1
  49. package/dist/editor/client/EditorShell.js +72 -8
  50. package/dist/editor/client/EditorShell.js.map +1 -1
  51. package/dist/editor/client/hooks/useQuota.d.ts +7 -0
  52. package/dist/editor/client/hooks/useQuota.js.map +1 -1
  53. package/dist/editor/client/hooks/useSocketMessageHandler.js +10 -1
  54. package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
  55. package/dist/editor/client/pageModelBuilder.js +3 -30
  56. package/dist/editor/client/pageModelBuilder.js.map +1 -1
  57. package/dist/editor/client/ui/EditorChrome.js +31 -1
  58. package/dist/editor/client/ui/EditorChrome.js.map +1 -1
  59. package/dist/editor/commands/componentCommands.js +106 -6
  60. package/dist/editor/commands/componentCommands.js.map +1 -1
  61. package/dist/editor/commands/itemCommands.d.ts +1 -0
  62. package/dist/editor/commands/itemCommands.js +28 -1
  63. package/dist/editor/commands/itemCommands.js.map +1 -1
  64. package/dist/editor/componentTreeHelper.js +22 -2
  65. package/dist/editor/componentTreeHelper.js.map +1 -1
  66. package/dist/editor/insertMenuItems.d.ts +4 -0
  67. package/dist/editor/insertMenuItems.js +66 -0
  68. package/dist/editor/insertMenuItems.js.map +1 -0
  69. package/dist/editor/menubar/toolbar-sections/UtilityControls.js +1 -1
  70. package/dist/editor/menubar/toolbar-sections/UtilityControls.js.map +1 -1
  71. package/dist/editor/page-editor-chrome/FrameMenus.js +8 -1
  72. package/dist/editor/page-editor-chrome/FrameMenus.js.map +1 -1
  73. package/dist/editor/page-editor-chrome/InlineEditor.js +25 -11
  74. package/dist/editor/page-editor-chrome/InlineEditor.js.map +1 -1
  75. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +32 -17
  76. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
  77. package/dist/editor/page-editor-chrome/PlaceholderDropZones.js +17 -11
  78. package/dist/editor/page-editor-chrome/PlaceholderDropZones.js.map +1 -1
  79. package/dist/editor/page-viewer/EditorForm.js +6 -5
  80. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  81. package/dist/editor/page-viewer/PageViewerFrame.js +49 -1
  82. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  83. package/dist/editor/page-viewer/pageModelSkeletonBuilder.js +5 -0
  84. package/dist/editor/page-viewer/pageModelSkeletonBuilder.js.map +1 -1
  85. package/dist/editor/pageModel.d.ts +2 -0
  86. package/dist/editor/reviews/CreateReviewConfirmStep.d.ts +16 -0
  87. package/dist/editor/reviews/CreateReviewConfirmStep.js +37 -0
  88. package/dist/editor/reviews/CreateReviewConfirmStep.js.map +1 -0
  89. package/dist/editor/reviews/CreateReviewDetailsStep.d.ts +51 -0
  90. package/dist/editor/reviews/CreateReviewDetailsStep.js +121 -0
  91. package/dist/editor/reviews/CreateReviewDetailsStep.js.map +1 -0
  92. package/dist/editor/reviews/CreateReviewDialog.js +260 -173
  93. package/dist/editor/reviews/CreateReviewDialog.js.map +1 -1
  94. package/dist/editor/reviews/CreateReviewSuccessStep.d.ts +6 -0
  95. package/dist/editor/reviews/CreateReviewSuccessStep.js +8 -0
  96. package/dist/editor/reviews/CreateReviewSuccessStep.js.map +1 -0
  97. package/dist/editor/reviews/DecisionsMatrix.js +96 -25
  98. package/dist/editor/reviews/DecisionsMatrix.js.map +1 -1
  99. package/dist/editor/reviews/MultiReviewManager.js +25 -3
  100. package/dist/editor/reviews/MultiReviewManager.js.map +1 -1
  101. package/dist/editor/reviews/PagesPanel.js +31 -15
  102. package/dist/editor/reviews/PagesPanel.js.map +1 -1
  103. package/dist/editor/reviews/ReviewCard.js +13 -7
  104. package/dist/editor/reviews/ReviewCard.js.map +1 -1
  105. package/dist/editor/reviews/ReviewDetail.js +2 -2
  106. package/dist/editor/reviews/ReviewDetail.js.map +1 -1
  107. package/dist/editor/reviews/ReviewsList.js +7 -3
  108. package/dist/editor/reviews/ReviewsList.js.map +1 -1
  109. package/dist/editor/services/agentService.d.ts +14 -1
  110. package/dist/editor/services/agentService.js +27 -1
  111. package/dist/editor/services/agentService.js.map +1 -1
  112. package/dist/editor/services/aiService.d.ts +39 -1
  113. package/dist/editor/services/aiService.js +12 -2
  114. package/dist/editor/services/aiService.js.map +1 -1
  115. package/dist/editor/services/serviceHelper.d.ts +1 -1
  116. package/dist/editor/services/serviceHelper.js +2 -1
  117. package/dist/editor/services/serviceHelper.js.map +1 -1
  118. package/dist/editor/settings/About.js +1 -1
  119. package/dist/editor/settings/About.js.map +1 -1
  120. package/dist/editor/settings/QuotaInfo.js +202 -4
  121. package/dist/editor/settings/QuotaInfo.js.map +1 -1
  122. package/dist/editor/settings/panels/SearchConfigPanel.js +11 -13
  123. package/dist/editor/settings/panels/SearchConfigPanel.js.map +1 -1
  124. package/dist/editor/settings/status/useStartupChecks.js +2 -1
  125. package/dist/editor/settings/status/useStartupChecks.js.map +1 -1
  126. package/dist/editor/sidebar/ComponentTree.js +26 -20
  127. package/dist/editor/sidebar/ComponentTree.js.map +1 -1
  128. package/dist/editor/sidebar/MobileWorkspacePopover.d.ts +16 -0
  129. package/dist/editor/sidebar/MobileWorkspacePopover.js +35 -0
  130. package/dist/editor/sidebar/MobileWorkspacePopover.js.map +1 -0
  131. package/dist/editor/sidebar/NavigationSidebar.js +30 -14
  132. package/dist/editor/sidebar/NavigationSidebar.js.map +1 -1
  133. package/dist/editor/ui/ItemNameDialogNew.js +8 -8
  134. package/dist/editor/ui/ItemNameDialogNew.js.map +1 -1
  135. package/dist/editor/ui/SharedFolderSelectorDialog.d.ts +11 -0
  136. package/dist/editor/ui/SharedFolderSelectorDialog.js +79 -0
  137. package/dist/editor/ui/SharedFolderSelectorDialog.js.map +1 -0
  138. package/dist/editor/ui/SimpleTabs.js +1 -1
  139. package/dist/editor/ui/SimpleTabs.js.map +1 -1
  140. package/dist/editor/ui/Splitter.js +4 -2
  141. package/dist/editor/ui/Splitter.js.map +1 -1
  142. package/dist/editor/ui/TreeListSelector.d.ts +2 -1
  143. package/dist/editor/ui/TreeListSelector.js +2 -2
  144. package/dist/editor/ui/TreeListSelector.js.map +1 -1
  145. package/dist/editor/utils.js +42 -0
  146. package/dist/editor/utils.js.map +1 -1
  147. package/dist/editor/views/EditorSlot.js +7 -8
  148. package/dist/editor/views/EditorSlot.js.map +1 -1
  149. package/dist/index.d.ts +1 -0
  150. package/dist/index.js +1 -0
  151. package/dist/index.js.map +1 -1
  152. package/dist/revision.d.ts +2 -2
  153. package/dist/revision.js +2 -2
  154. package/dist/setup/wizard/steps/ImportModelDialog.js +24 -22
  155. package/dist/setup/wizard/steps/ImportModelDialog.js.map +1 -1
  156. package/dist/splash-screen/NewPage.js +2 -2
  157. package/dist/splash-screen/NewPage.js.map +1 -1
  158. package/dist/splash-screen/RecentPages.js +1 -1
  159. package/dist/splash-screen/RecentPages.js.map +1 -1
  160. package/dist/task-board/TaskBoardWorkspace.d.ts +1 -0
  161. package/dist/task-board/TaskBoardWorkspace.js +1094 -0
  162. package/dist/task-board/TaskBoardWorkspace.js.map +1 -0
  163. package/dist/task-board/components/AddDependencyDialog.d.ts +8 -0
  164. package/dist/task-board/components/AddDependencyDialog.js +53 -0
  165. package/dist/task-board/components/AddDependencyDialog.js.map +1 -0
  166. package/dist/task-board/components/AssignAgentDialog.d.ts +7 -0
  167. package/dist/task-board/components/AssignAgentDialog.js +96 -0
  168. package/dist/task-board/components/AssignAgentDialog.js.map +1 -0
  169. package/dist/task-board/components/CommentsList.d.ts +3 -0
  170. package/dist/task-board/components/CommentsList.js +36 -0
  171. package/dist/task-board/components/CommentsList.js.map +1 -0
  172. package/dist/task-board/components/CreateProjectDialog.d.ts +7 -0
  173. package/dist/task-board/components/CreateProjectDialog.js +175 -0
  174. package/dist/task-board/components/CreateProjectDialog.js.map +1 -0
  175. package/dist/task-board/components/CreateTaskDialog.d.ts +7 -0
  176. package/dist/task-board/components/CreateTaskDialog.js +76 -0
  177. package/dist/task-board/components/CreateTaskDialog.js.map +1 -0
  178. package/dist/task-board/components/ProjectAgentsPanel.d.ts +4 -0
  179. package/dist/task-board/components/ProjectAgentsPanel.js +159 -0
  180. package/dist/task-board/components/ProjectAgentsPanel.js.map +1 -0
  181. package/dist/task-board/components/ProjectDashboard.d.ts +25 -0
  182. package/dist/task-board/components/ProjectDashboard.js +91 -0
  183. package/dist/task-board/components/ProjectDashboard.js.map +1 -0
  184. package/dist/task-board/components/ProjectList.d.ts +7 -0
  185. package/dist/task-board/components/ProjectList.js +74 -0
  186. package/dist/task-board/components/ProjectList.js.map +1 -0
  187. package/dist/task-board/components/ProjectSettingsDialog.d.ts +8 -0
  188. package/dist/task-board/components/ProjectSettingsDialog.js +146 -0
  189. package/dist/task-board/components/ProjectSettingsDialog.js.map +1 -0
  190. package/dist/task-board/components/TaskAgentPanel.d.ts +10 -0
  191. package/dist/task-board/components/TaskAgentPanel.js +42 -0
  192. package/dist/task-board/components/TaskAgentPanel.js.map +1 -0
  193. package/dist/task-board/components/TaskAssigneePicker.d.ts +12 -0
  194. package/dist/task-board/components/TaskAssigneePicker.js +115 -0
  195. package/dist/task-board/components/TaskAssigneePicker.js.map +1 -0
  196. package/dist/task-board/components/TaskBoardTitlebar.d.ts +1 -0
  197. package/dist/task-board/components/TaskBoardTitlebar.js +60 -0
  198. package/dist/task-board/components/TaskBoardTitlebar.js.map +1 -0
  199. package/dist/task-board/components/TaskCard.d.ts +9 -0
  200. package/dist/task-board/components/TaskCard.js +24 -0
  201. package/dist/task-board/components/TaskCard.js.map +1 -0
  202. package/dist/task-board/components/TaskDetailDialog.d.ts +11 -0
  203. package/dist/task-board/components/TaskDetailDialog.js +8 -0
  204. package/dist/task-board/components/TaskDetailDialog.js.map +1 -0
  205. package/dist/task-board/components/TaskDetailPanel.d.ts +13 -0
  206. package/dist/task-board/components/TaskDetailPanel.js +322 -0
  207. package/dist/task-board/components/TaskDetailPanel.js.map +1 -0
  208. package/dist/task-board/components/TaskRow.d.ts +9 -0
  209. package/dist/task-board/components/TaskRow.js +25 -0
  210. package/dist/task-board/components/TaskRow.js.map +1 -0
  211. package/dist/task-board/index.d.ts +15 -0
  212. package/dist/task-board/index.js +18 -0
  213. package/dist/task-board/index.js.map +1 -0
  214. package/dist/task-board/services/taskService.d.ts +52 -0
  215. package/dist/task-board/services/taskService.js +74 -0
  216. package/dist/task-board/services/taskService.js.map +1 -0
  217. package/dist/task-board/taskAgentConfig.d.ts +7 -0
  218. package/dist/task-board/taskAgentConfig.js +43 -0
  219. package/dist/task-board/taskAgentConfig.js.map +1 -0
  220. package/dist/task-board/taskAgentLink.d.ts +2 -0
  221. package/dist/task-board/taskAgentLink.js +35 -0
  222. package/dist/task-board/taskAgentLink.js.map +1 -0
  223. package/dist/task-board/taskBoardNavStore.d.ts +35 -0
  224. package/dist/task-board/taskBoardNavStore.js +42 -0
  225. package/dist/task-board/taskBoardNavStore.js.map +1 -0
  226. package/dist/task-board/taskExecutionStatus.d.ts +12 -0
  227. package/dist/task-board/taskExecutionStatus.js +96 -0
  228. package/dist/task-board/taskExecutionStatus.js.map +1 -0
  229. package/dist/task-board/taskStatus.d.ts +2 -0
  230. package/dist/task-board/taskStatus.js +26 -0
  231. package/dist/task-board/taskStatus.js.map +1 -0
  232. package/dist/task-board/types.d.ts +169 -0
  233. package/dist/task-board/types.js +2 -0
  234. package/dist/task-board/types.js.map +1 -0
  235. package/dist/task-board/utils/projectHierarchy.d.ts +13 -0
  236. package/dist/task-board/utils/projectHierarchy.js +34 -0
  237. package/dist/task-board/utils/projectHierarchy.js.map +1 -0
  238. package/dist/task-board/views/KanbanView.d.ts +14 -0
  239. package/dist/task-board/views/KanbanView.js +136 -0
  240. package/dist/task-board/views/KanbanView.js.map +1 -0
  241. package/dist/task-board/views/ListView.d.ts +13 -0
  242. package/dist/task-board/views/ListView.js +74 -0
  243. package/dist/task-board/views/ListView.js.map +1 -0
  244. package/dist/task-board/views/PlanningView.d.ts +7 -0
  245. package/dist/task-board/views/PlanningView.js +112 -0
  246. package/dist/task-board/views/PlanningView.js.map +1 -0
  247. package/package.json +1 -1
@@ -0,0 +1,1094 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { toast } from "sonner";
4
+ import { Splitter } from "../editor/ui/Splitter";
5
+ import { deleteProject, getProjects, getTasks, getDependencies, runOrchestrator, triggerPlanning, updateProject } from "./services/taskService";
6
+ import { getActiveAgents, getAgent, } from "../editor/services/agentService";
7
+ import { loadAiProfiles } from "../editor/services/aiService";
8
+ import { useEditContext } from "../editor/client/editContext";
9
+ import { KanbanView } from "./views/KanbanView";
10
+ import { ListView } from "./views/ListView";
11
+ import { TaskDetailPanel } from "./components/TaskDetailPanel";
12
+ import { TaskAgentPanel } from "./components/TaskAgentPanel";
13
+ import { ProjectDashboard } from "./components/ProjectDashboard";
14
+ import { CreateTaskDialog } from "./components/CreateTaskDialog";
15
+ import { AssignAgentDialog } from "./components/AssignAgentDialog";
16
+ import { CreateProjectDialog } from "./components/CreateProjectDialog";
17
+ import { ProjectSettingsDialog } from "./components/ProjectSettingsDialog";
18
+ import { EditorSlotContextProvider } from "../editor/views/editorSlotContext";
19
+ import { SingleEditView } from "../editor/views/SingleEditView";
20
+ import { Select } from "../components/ui/select";
21
+ import { getLinkedAgentId } from "./taskAgentLink";
22
+ import { setTaskBoardNavState, resetTaskBoardNavState, } from "./taskBoardNavStore";
23
+ import { flattenProjectsHierarchy } from "./utils/projectHierarchy";
24
+ import { normalizeTaskStatus } from "./taskStatus";
25
+ function normalizeAgentStatus(status) {
26
+ if (status === undefined || status === null)
27
+ return undefined;
28
+ if (typeof status === "number") {
29
+ return status;
30
+ }
31
+ switch (status) {
32
+ case "Running":
33
+ return "running";
34
+ case "WaitingForApproval":
35
+ return "waitingForApproval";
36
+ case "WaitingForInput":
37
+ return "waitingForInput";
38
+ case "CostLimitReached":
39
+ return "costLimitReached";
40
+ case "Error":
41
+ return "error";
42
+ case "Idle":
43
+ return "idle";
44
+ case "Completed":
45
+ return "completed";
46
+ case "Closed":
47
+ return "closed";
48
+ case "Cancelled":
49
+ return "cancelled";
50
+ case "New":
51
+ return "new";
52
+ default:
53
+ return status;
54
+ }
55
+ }
56
+ // ── component ────────────────────────────────────────────────────────
57
+ export function TaskBoardWorkspace() {
58
+ const editContext = useEditContext();
59
+ const showEditorPanel = editContext?.showAgentsWorkspaceEditor ?? true;
60
+ const currentItemDescriptor = editContext?.currentItemDescriptor;
61
+ const [projects, setProjects] = useState([]);
62
+ const [selectedProjectId, setSelectedProjectId] = useState(null);
63
+ const [tasks, setTasks] = useState([]);
64
+ const [dependencies, setDependencies] = useState([]);
65
+ const [selectedTaskId, setSelectedTaskId] = useState(null);
66
+ const [activeTab, setActiveTab] = useState(0);
67
+ const [createDialogOpen, setCreateDialogOpen] = useState(false);
68
+ const [createProjectOpen, setCreateProjectOpen] = useState(false);
69
+ const [settingsProjectId, setSettingsProjectId] = useState(null);
70
+ const [savingProjectStatus, setSavingProjectStatus] = useState(false);
71
+ const [assignAgentDialogOpen, setAssignAgentDialogOpen] = useState(false);
72
+ const [runningOrchestrator, setRunningOrchestrator] = useState(false);
73
+ const [agentStatusesById, setAgentStatusesById] = useState({});
74
+ const [agentProfileTitlesById, setAgentProfileTitlesById] = useState({});
75
+ const [subprojectTaskCounts, setSubprojectTaskCounts] = useState({});
76
+ const [subprojectCountsLoading, setSubprojectCountsLoading] = useState(false);
77
+ const [previewItemName, setPreviewItemName] = useState("");
78
+ const [previewItemPath, setPreviewItemPath] = useState("");
79
+ const [agentContextItems, setAgentContextItems] = useState([]);
80
+ const [contextItemNamesByKey, setContextItemNamesByKey] = useState({});
81
+ const [contextItemPathsByKey, setContextItemPathsByKey] = useState({});
82
+ const wsRefreshTimeoutRef = useRef(null);
83
+ const wsSubprojectRefreshTimeoutRef = useRef(null);
84
+ const wsProjectRefreshTimeoutRef = useRef(null);
85
+ const refreshProjectsRef = useRef(null);
86
+ const refreshTasksRef = useRef(null);
87
+ const refreshDependenciesRef = useRef(null);
88
+ const refreshSubprojectTaskCountsRef = useRef(null);
89
+ const selectedProjectIdRef = useRef(null);
90
+ const directSubprojectIdsRef = useRef(new Set());
91
+ const tasksRef = useRef([]);
92
+ const autoSelectedProjectIdsRef = useRef(new Set());
93
+ useEffect(() => {
94
+ let cancelled = false;
95
+ loadAiProfiles()
96
+ .then((profiles) => {
97
+ if (cancelled)
98
+ return;
99
+ const byId = {};
100
+ for (const profile of profiles || []) {
101
+ if (!profile?.id)
102
+ continue;
103
+ byId[profile.id.toLowerCase()] = profile.displayTitle || profile.name || profile.id;
104
+ }
105
+ setAgentProfileTitlesById(byId);
106
+ })
107
+ .catch(() => {
108
+ // best-effort only
109
+ });
110
+ return () => {
111
+ cancelled = true;
112
+ };
113
+ }, []);
114
+ const tasksWithDisplayAssignees = useMemo(() => {
115
+ return tasks.map((task) => {
116
+ if (task.assigneeType !== "Agent" ||
117
+ !task.assigneeId ||
118
+ task.assigneeDisplayName) {
119
+ return task;
120
+ }
121
+ const profileTitle = agentProfileTitlesById[task.assigneeId.toLowerCase()];
122
+ if (!profileTitle)
123
+ return task;
124
+ return {
125
+ ...task,
126
+ assigneeDisplayName: profileTitle,
127
+ };
128
+ });
129
+ }, [tasks, agentProfileTitlesById]);
130
+ const selectedProject = useMemo(() => projects.find((p) => p.project.projectId === selectedProjectId) ?? null, [projects, selectedProjectId]);
131
+ const settingsProject = useMemo(() => projects.find((p) => p.project.projectId === settingsProjectId) ?? null, [projects, settingsProjectId]);
132
+ const selectedProjectTaskCounts = useMemo(() => {
133
+ const counts = {
134
+ total: tasks.length,
135
+ todo: 0,
136
+ inProgress: 0,
137
+ review: 0,
138
+ done: 0,
139
+ };
140
+ for (const task of tasks) {
141
+ const normalizedStatus = normalizeTaskStatus(task.status, task.executionStatus);
142
+ if (normalizedStatus === "Todo")
143
+ counts.todo += 1;
144
+ else if (normalizedStatus === "InProgress")
145
+ counts.inProgress += 1;
146
+ else if (normalizedStatus === "Review")
147
+ counts.review += 1;
148
+ else if (normalizedStatus === "Done")
149
+ counts.done += 1;
150
+ }
151
+ return counts;
152
+ }, [tasks]);
153
+ const directSubprojects = useMemo(() => selectedProjectId
154
+ ? projects
155
+ .filter((project) => project.project.parentProjectId === selectedProjectId)
156
+ .sort((a, b) => a.project.title.localeCompare(b.project.title))
157
+ : [], [projects, selectedProjectId]);
158
+ const selectedProjectCumulativeCostUsed = useMemo(() => {
159
+ if (!selectedProjectId)
160
+ return 0;
161
+ const childrenByParent = new Map();
162
+ for (const project of projects) {
163
+ const parentId = project.project.parentProjectId;
164
+ if (!parentId)
165
+ continue;
166
+ const children = childrenByParent.get(parentId) || [];
167
+ children.push(project.project.projectId);
168
+ childrenByParent.set(parentId, children);
169
+ }
170
+ const stack = [selectedProjectId];
171
+ const visited = new Set();
172
+ let total = 0;
173
+ while (stack.length > 0) {
174
+ const currentId = stack.pop();
175
+ if (!currentId || visited.has(currentId))
176
+ continue;
177
+ visited.add(currentId);
178
+ const currentProject = projects.find((project) => project.project.projectId === currentId);
179
+ const cost = currentProject?.project.costUsed;
180
+ if (typeof cost === "number" && Number.isFinite(cost)) {
181
+ total += cost;
182
+ }
183
+ const children = childrenByParent.get(currentId) || [];
184
+ for (const childId of children) {
185
+ if (!visited.has(childId))
186
+ stack.push(childId);
187
+ }
188
+ }
189
+ return total;
190
+ }, [projects, selectedProjectId]);
191
+ const parentProject = useMemo(() => {
192
+ const parentId = selectedProject?.project.parentProjectId;
193
+ if (!parentId)
194
+ return null;
195
+ return projects.find((project) => project.project.projectId === parentId) ?? null;
196
+ }, [projects, selectedProject]);
197
+ useEffect(() => {
198
+ const childrenByParent = new Map();
199
+ for (const project of projects) {
200
+ const parentId = project.project.parentProjectId;
201
+ if (!parentId)
202
+ continue;
203
+ const children = childrenByParent.get(parentId) || [];
204
+ children.push(project.project.projectId);
205
+ childrenByParent.set(parentId, children);
206
+ }
207
+ const includedProjects = [];
208
+ if (selectedProjectId) {
209
+ const stack = [selectedProjectId];
210
+ const visited = new Set();
211
+ while (stack.length > 0) {
212
+ const currentId = stack.pop();
213
+ if (!currentId || visited.has(currentId))
214
+ continue;
215
+ visited.add(currentId);
216
+ const currentProject = projects.find((project) => project.project.projectId === currentId);
217
+ includedProjects.push({
218
+ projectId: currentId,
219
+ costUsed: typeof currentProject?.project.costUsed === "number"
220
+ ? currentProject.project.costUsed
221
+ : null,
222
+ });
223
+ const children = childrenByParent.get(currentId) || [];
224
+ for (const childId of children) {
225
+ if (!visited.has(childId))
226
+ stack.push(childId);
227
+ }
228
+ }
229
+ }
230
+ }, [selectedProjectCumulativeCostUsed, selectedProjectId, selectedProject?.project.costUsed, projects]);
231
+ const canCreate = selectedProject?.permission === "Owner" || selectedProject?.permission === "Editor";
232
+ const canEditTasks = canCreate;
233
+ const viewTabIds = useMemo(() => [
234
+ "kanban",
235
+ "list",
236
+ ], []);
237
+ const isListView = viewTabIds[activeTab] === "list";
238
+ const isPlanning = selectedProject?.project.status === "Planning" ||
239
+ selectedProject?.project.status === "PlanPending";
240
+ const selectedTask = useMemo(() => tasksWithDisplayAssignees.find((t) => t.taskId === selectedTaskId) ?? null, [tasksWithDisplayAssignees, selectedTaskId]);
241
+ const planningTask = useMemo(() => tasksWithDisplayAssignees.find((t) => String(t.taskType ?? "").toLowerCase() === "plan") ??
242
+ tasksWithDisplayAssignees.find((t) => t.title === "Project Plan") ??
243
+ null, [tasksWithDisplayAssignees]);
244
+ const selectedTaskIsPlan = useMemo(() => {
245
+ const taskType = String(selectedTask?.taskType ?? "").toLowerCase();
246
+ return taskType === "plan" || selectedTask?.title === "Project Plan";
247
+ }, [selectedTask]);
248
+ const selectedTaskIsBlocked = useMemo(() => {
249
+ if (!selectedTask)
250
+ return false;
251
+ const blockerIds = dependencies
252
+ .filter((dependency) => dependency.taskId === selectedTask.taskId &&
253
+ dependency.dependencyType === "BlockedBy")
254
+ .map((dependency) => dependency.dependsOnTaskId);
255
+ if (blockerIds.length === 0)
256
+ return false;
257
+ const taskById = new Map(tasksWithDisplayAssignees.map((task) => [task.taskId, task]));
258
+ return blockerIds.some((blockerId) => {
259
+ const blockerTask = taskById.get(blockerId);
260
+ return !blockerTask || blockerTask.status !== "Done";
261
+ });
262
+ }, [selectedTask, dependencies, tasksWithDisplayAssignees]);
263
+ // ── current agent ID and panel mode for the right panel ──
264
+ const currentAgentId = useMemo(() => {
265
+ return getLinkedAgentId(selectedTask);
266
+ }, [selectedTask]);
267
+ const currentItemKey = useMemo(() => {
268
+ if (!currentItemDescriptor)
269
+ return "";
270
+ return `${currentItemDescriptor.id}|${currentItemDescriptor.language}|${currentItemDescriptor.version}`;
271
+ }, [currentItemDescriptor]);
272
+ const agentPanelMode = useMemo(() => {
273
+ if (!selectedTask)
274
+ return "no-task-selected";
275
+ if (currentAgentId)
276
+ return "agent";
277
+ return "no-agent";
278
+ }, [selectedTask, currentAgentId]);
279
+ // ── data fetching ──
280
+ const refreshProjects = useCallback(async () => {
281
+ const result = await getProjects();
282
+ if (result.type !== "success") {
283
+ toast.error(result.summary || "Failed to load projects");
284
+ return;
285
+ }
286
+ const data = result.data || [];
287
+ const selectedProject = selectedProjectIdRef.current
288
+ ? data.find((p) => p.project.projectId === selectedProjectIdRef.current)
289
+ : null;
290
+ setProjects(data);
291
+ setSelectedProjectId((prev) => {
292
+ if (!prev)
293
+ return data[0]?.project.projectId ?? null;
294
+ const stillExists = data.some((p) => p.project.projectId === prev);
295
+ return stillExists ? prev : data[0]?.project.projectId ?? null;
296
+ });
297
+ }, []);
298
+ const refreshTasks = useCallback(async (projectId) => {
299
+ const result = await getTasks(projectId);
300
+ if (result.type !== "success") {
301
+ toast.error(result.summary || "Failed to load tasks");
302
+ return;
303
+ }
304
+ setTasks(result.data || []);
305
+ }, []);
306
+ const refreshSubprojectTaskCounts = useCallback(async () => {
307
+ if (directSubprojects.length === 0) {
308
+ setSubprojectTaskCounts({});
309
+ return;
310
+ }
311
+ setSubprojectCountsLoading(true);
312
+ try {
313
+ const results = await Promise.all(directSubprojects.map(async (subproject) => {
314
+ const projectId = subproject.project.projectId;
315
+ const response = await getTasks(projectId);
316
+ if (response.type !== "success") {
317
+ return [projectId, null];
318
+ }
319
+ const tasksForProject = response.data || [];
320
+ const counts = {
321
+ total: tasksForProject.length,
322
+ todo: 0,
323
+ inProgress: 0,
324
+ review: 0,
325
+ done: 0,
326
+ };
327
+ for (const task of tasksForProject) {
328
+ const normalizedStatus = normalizeTaskStatus(task.status, task.executionStatus);
329
+ if (normalizedStatus === "Todo")
330
+ counts.todo += 1;
331
+ else if (normalizedStatus === "InProgress")
332
+ counts.inProgress += 1;
333
+ else if (normalizedStatus === "Review")
334
+ counts.review += 1;
335
+ else if (normalizedStatus === "Done")
336
+ counts.done += 1;
337
+ }
338
+ return [projectId, counts];
339
+ }));
340
+ const nextCounts = {};
341
+ for (const [projectId, counts] of results) {
342
+ if (counts)
343
+ nextCounts[projectId] = counts;
344
+ }
345
+ setSubprojectTaskCounts(nextCounts);
346
+ }
347
+ finally {
348
+ setSubprojectCountsLoading(false);
349
+ }
350
+ }, [directSubprojects]);
351
+ // If a project only has a single task, auto-select it once when loading.
352
+ // Do not auto-reselect after the user explicitly closes task details.
353
+ useEffect(() => {
354
+ if (!selectedProjectId)
355
+ return;
356
+ if (selectedTaskId)
357
+ return;
358
+ if (tasks.length !== 1)
359
+ return;
360
+ if (autoSelectedProjectIdsRef.current.has(selectedProjectId))
361
+ return;
362
+ const firstTask = tasks[0];
363
+ if (!firstTask)
364
+ return;
365
+ autoSelectedProjectIdsRef.current.add(selectedProjectId);
366
+ setSelectedTaskId(firstTask.taskId);
367
+ }, [selectedProjectId, selectedTaskId, tasks]);
368
+ const refreshDependencies = useCallback(async (projectId) => {
369
+ const result = await getDependencies(projectId);
370
+ if (result.type !== "success") {
371
+ toast.error(result.summary || "Failed to load dependencies");
372
+ return;
373
+ }
374
+ setDependencies(result.data || []);
375
+ }, []);
376
+ const refreshAgentStatuses = useCallback(async (taskList) => {
377
+ const agentIds = Array.from(new Set(taskList
378
+ .map((task) => getLinkedAgentId(task))
379
+ .filter((id) => typeof id === "string" && id.length > 0)));
380
+ if (agentIds.length === 0) {
381
+ setAgentStatusesById({});
382
+ return;
383
+ }
384
+ try {
385
+ const response = await getActiveAgents({
386
+ limit: 1000,
387
+ includeOwned: true,
388
+ includeShared: true,
389
+ excludeClosed: false,
390
+ });
391
+ const wanted = new Set(agentIds.map((id) => id.toLowerCase()));
392
+ const statuses = {};
393
+ for (const agent of response.agents || []) {
394
+ const normalizedAgentId = agent.id.toLowerCase();
395
+ if (wanted.has(normalizedAgentId)) {
396
+ statuses[normalizedAgentId] = agent.status;
397
+ }
398
+ }
399
+ setAgentStatusesById(statuses);
400
+ }
401
+ catch {
402
+ // best-effort only
403
+ }
404
+ }, []);
405
+ // Sync refs with latest function/state values for use in WebSocket timeout callback
406
+ useEffect(() => {
407
+ refreshProjectsRef.current = refreshProjects;
408
+ refreshTasksRef.current = refreshTasks;
409
+ refreshDependenciesRef.current = refreshDependencies;
410
+ refreshSubprojectTaskCountsRef.current = refreshSubprojectTaskCounts;
411
+ selectedProjectIdRef.current = selectedProjectId;
412
+ directSubprojectIdsRef.current = new Set(directSubprojects.map((subproject) => subproject.project.projectId));
413
+ tasksRef.current = tasks;
414
+ }, [refreshProjects, refreshTasks, refreshDependencies, refreshSubprojectTaskCounts, selectedProjectId, directSubprojects, tasks]);
415
+ const handleRunOrchestrator = useCallback(async () => {
416
+ if (!selectedProjectId)
417
+ return;
418
+ setRunningOrchestrator(true);
419
+ try {
420
+ const result = await runOrchestrator(selectedProjectId);
421
+ if (result.type !== "success") {
422
+ toast.error(result.summary || "Failed to run orchestrator");
423
+ return;
424
+ }
425
+ const data = result.data;
426
+ if (data?.errors?.length) {
427
+ toast.error(data.errors.join(", "));
428
+ }
429
+ else {
430
+ const dispatched = data?.tasksDispatched?.length || 0;
431
+ const launched = data?.tasksLaunched?.length || 0;
432
+ if (launched > 0) {
433
+ if (dispatched > 0) {
434
+ toast.success(`Launched ${launched} task(s). ${dispatched} task(s) still need planner assignment.`);
435
+ }
436
+ else {
437
+ toast.success(`Launched ${launched} task(s).`);
438
+ }
439
+ }
440
+ else if (dispatched > 0) {
441
+ toast.info(`${dispatched} task(s) are runnable but still need planner assignment.`);
442
+ }
443
+ else {
444
+ toast.info("No queued tasks ready to launch");
445
+ }
446
+ }
447
+ // Refresh tasks after orchestrator runs
448
+ await refreshTasks(selectedProjectId);
449
+ await refreshDependencies(selectedProjectId);
450
+ }
451
+ finally {
452
+ setRunningOrchestrator(false);
453
+ }
454
+ }, [selectedProjectId, refreshTasks, refreshDependencies]);
455
+ const handleProjectStatusChange = useCallback(async (status) => {
456
+ if (!selectedProject)
457
+ return;
458
+ if (selectedProject.permission !== "Owner")
459
+ return;
460
+ if (selectedProject.project.status === status)
461
+ return;
462
+ setSavingProjectStatus(true);
463
+ try {
464
+ const result = await updateProject({
465
+ projectId: selectedProject.project.projectId,
466
+ title: selectedProject.project.title,
467
+ description: selectedProject.project.description ?? "",
468
+ costLimit: selectedProject.project.costLimit ?? null,
469
+ status,
470
+ });
471
+ if (result.type !== "success") {
472
+ toast.error(result.summary || "Failed to update project status");
473
+ return;
474
+ }
475
+ await refreshProjects();
476
+ }
477
+ finally {
478
+ setSavingProjectStatus(false);
479
+ }
480
+ }, [selectedProject, refreshProjects]);
481
+ useEffect(() => {
482
+ setTaskBoardNavState({
483
+ projectTitle: selectedProject?.project.title ?? "Tasks",
484
+ projectSubtitle: selectedProject?.project.description?.trim() ||
485
+ "Select a project to get started",
486
+ permissionLabel: selectedProject?.permission ?? "No project selected",
487
+ activeTab,
488
+ selectedProjectId,
489
+ projectOptions: flattenProjectsHierarchy(projects.map((project) => ({
490
+ projectId: project.project.projectId,
491
+ parentProjectId: project.project.parentProjectId ?? null,
492
+ title: project.project.title,
493
+ description: project.project.description,
494
+ status: project.project.status,
495
+ permission: project.permission,
496
+ }))),
497
+ hasSelectedProject: !!selectedProjectId,
498
+ canCreateTask: canCreate,
499
+ canRunOrchestrator: canCreate && !isPlanning,
500
+ runningOrchestrator,
501
+ canManageProject: selectedProject?.permission === "Owner",
502
+ onSelectProject: setSelectedProjectId,
503
+ onSetActiveTab: setActiveTab,
504
+ onCreateProject: () => setCreateProjectOpen(true),
505
+ onOpenProjectSettings: (projectId) => setSettingsProjectId(projectId),
506
+ onDeleteProject: (projectId, title) => {
507
+ const runDelete = async () => {
508
+ const result = await deleteProject(projectId);
509
+ if (result.type !== "success") {
510
+ toast.error(result.summary || "Failed to delete project");
511
+ return;
512
+ }
513
+ toast.success("Project deleted");
514
+ const fallbackProjectId = projects.find((p) => p.project.projectId !== projectId)?.project.projectId ??
515
+ null;
516
+ await refreshProjects();
517
+ if (selectedProjectId === projectId) {
518
+ setSelectedProjectId(fallbackProjectId);
519
+ }
520
+ };
521
+ if (editContext?.confirm) {
522
+ editContext.confirm({
523
+ header: "Delete project",
524
+ message: `Are you sure you want to delete \"${title}\"?\n\nThis action cannot be undone.`,
525
+ acceptLabel: "Delete",
526
+ showCancel: true,
527
+ accept: () => {
528
+ void runDelete();
529
+ },
530
+ });
531
+ return;
532
+ }
533
+ void runDelete();
534
+ },
535
+ onCreateTask: () => setCreateDialogOpen(true),
536
+ onRunOrchestrator: handleRunOrchestrator,
537
+ onRefresh: () => {
538
+ if (selectedProjectId) {
539
+ void refreshTasks(selectedProjectId);
540
+ void refreshDependencies(selectedProjectId);
541
+ }
542
+ },
543
+ });
544
+ }, [
545
+ selectedProject?.project.title,
546
+ selectedProject?.project.description,
547
+ selectedProject?.permission,
548
+ activeTab,
549
+ selectedProjectId,
550
+ projects,
551
+ canCreate,
552
+ isPlanning,
553
+ runningOrchestrator,
554
+ editContext,
555
+ refreshProjects,
556
+ handleRunOrchestrator,
557
+ refreshTasks,
558
+ refreshDependencies,
559
+ ]);
560
+ useEffect(() => {
561
+ return () => {
562
+ resetTaskBoardNavState();
563
+ };
564
+ }, []);
565
+ const handleStartPlanning = useCallback(async () => {
566
+ if (!selectedProjectId)
567
+ return;
568
+ try {
569
+ const result = await triggerPlanning(selectedProjectId);
570
+ if (result.type !== "success") {
571
+ toast.error(result.summary || "Failed to start planning");
572
+ return;
573
+ }
574
+ toast.success("Planning agent started");
575
+ // Open the planner terminal by selecting the Plan task.
576
+ // The backend links the Plan task to the planner agent (assigneeId),
577
+ // so we don't need any special "planner discovery" logic on the frontend.
578
+ await refreshTasks(selectedProjectId);
579
+ await refreshDependencies(selectedProjectId);
580
+ const plan = tasks.find((t) => String(t.taskType ?? "").toLowerCase() === "plan") ??
581
+ tasks.find((t) => t.title === "Project Plan") ??
582
+ null;
583
+ if (plan)
584
+ setSelectedTaskId(plan.taskId);
585
+ }
586
+ catch {
587
+ toast.error("Failed to start planning");
588
+ }
589
+ }, [selectedProjectId, refreshTasks, refreshDependencies, tasks]);
590
+ useEffect(() => {
591
+ void refreshProjects();
592
+ }, [refreshProjects]);
593
+ useEffect(() => {
594
+ if (!selectedProjectId)
595
+ return;
596
+ void refreshTasks(selectedProjectId);
597
+ void refreshDependencies(selectedProjectId);
598
+ }, [refreshTasks, refreshDependencies, selectedProjectId]);
599
+ useEffect(() => {
600
+ void refreshSubprojectTaskCounts();
601
+ }, [refreshSubprojectTaskCounts]);
602
+ useEffect(() => {
603
+ if (!selectedProjectId) {
604
+ setAgentStatusesById({});
605
+ return;
606
+ }
607
+ void refreshAgentStatuses(tasks);
608
+ }, [selectedProjectId, tasks, refreshAgentStatuses]);
609
+ // Keep task board in sync with backend task updates pushed over WebSocket.
610
+ // Uses refs for callbacks and selectedProjectId so the effect only depends on
611
+ // editContext (stable), preventing cleanup from canceling pending refresh timeouts.
612
+ useEffect(() => {
613
+ const addListener = editContext?.addSocketMessageListener;
614
+ if (!addListener)
615
+ return;
616
+ const unsubscribe = addListener((message) => {
617
+ const messageType = message?.type;
618
+ if (messageType !== "task:updated" && messageType !== "task:created" && messageType !== "task:deleted") {
619
+ return;
620
+ }
621
+ const payload = message?.payload;
622
+ const projectId = payload?.projectId ?? payload?.ProjectId;
623
+ const currentProjectId = selectedProjectIdRef.current;
624
+ if (!projectId || !currentProjectId)
625
+ return;
626
+ if (projectId !== currentProjectId) {
627
+ if (!directSubprojectIdsRef.current.has(projectId))
628
+ return;
629
+ if (wsSubprojectRefreshTimeoutRef.current !== null) {
630
+ window.clearTimeout(wsSubprojectRefreshTimeoutRef.current);
631
+ wsSubprojectRefreshTimeoutRef.current = null;
632
+ }
633
+ wsSubprojectRefreshTimeoutRef.current = window.setTimeout(() => {
634
+ wsSubprojectRefreshTimeoutRef.current = null;
635
+ const refreshSubprojectsFn = refreshSubprojectTaskCountsRef.current;
636
+ const refreshProjectsFn = refreshProjectsRef.current;
637
+ if (!refreshSubprojectsFn)
638
+ return;
639
+ void refreshSubprojectsFn();
640
+ if (refreshProjectsFn)
641
+ void refreshProjectsFn();
642
+ }, 250);
643
+ return;
644
+ }
645
+ if (wsRefreshTimeoutRef.current !== null) {
646
+ window.clearTimeout(wsRefreshTimeoutRef.current);
647
+ wsRefreshTimeoutRef.current = null;
648
+ }
649
+ wsRefreshTimeoutRef.current = window.setTimeout(() => {
650
+ wsRefreshTimeoutRef.current = null;
651
+ const pid = selectedProjectIdRef.current;
652
+ const refreshProjectsFn = refreshProjectsRef.current;
653
+ const refreshTasksFn = refreshTasksRef.current;
654
+ const refreshDepsFn = refreshDependenciesRef.current;
655
+ if (!pid || !refreshTasksFn || !refreshDepsFn)
656
+ return;
657
+ void refreshTasksFn(pid);
658
+ void refreshDepsFn(pid);
659
+ if (refreshProjectsFn)
660
+ void refreshProjectsFn();
661
+ }, 250);
662
+ });
663
+ return () => {
664
+ unsubscribe();
665
+ // Do NOT clear wsRefreshTimeoutRef here — the ref persists across effect
666
+ // re-runs and the timeout callback reads the latest values via refs.
667
+ // Clearing it here was the bug: editContext changes caused cleanup to
668
+ // cancel the pending debounced refresh before it could fire.
669
+ };
670
+ }, [editContext]);
671
+ // Keep project dropdown in sync when projects are created elsewhere (e.g. by agents).
672
+ useEffect(() => {
673
+ const addListener = editContext?.addSocketMessageListener;
674
+ if (!addListener)
675
+ return;
676
+ const unsubscribe = addListener((message) => {
677
+ if (message?.type !== "project:created")
678
+ return;
679
+ void refreshProjects();
680
+ });
681
+ return () => unsubscribe();
682
+ }, [editContext, refreshProjects]);
683
+ useEffect(() => {
684
+ const addListener = editContext?.addSocketMessageListener;
685
+ if (!addListener)
686
+ return;
687
+ const unsubscribe = addListener((message) => {
688
+ const type = message?.type;
689
+ if (!type)
690
+ return;
691
+ const payload = message?.payload ?? {};
692
+ const agentId = payload?.agentId;
693
+ if (!agentId || typeof agentId !== "string")
694
+ return;
695
+ const trackedAgentIds = new Set(tasksRef.current
696
+ .map((task) => task.assignedAgentId)
697
+ .filter((id) => typeof id === "string" && id.length > 0)
698
+ .map((id) => id.toLowerCase()));
699
+ if (!trackedAgentIds.has(agentId.toLowerCase()))
700
+ return;
701
+ const queueProjectsRefresh = () => {
702
+ if (wsProjectRefreshTimeoutRef.current !== null) {
703
+ window.clearTimeout(wsProjectRefreshTimeoutRef.current);
704
+ wsProjectRefreshTimeoutRef.current = null;
705
+ }
706
+ wsProjectRefreshTimeoutRef.current = window.setTimeout(() => {
707
+ wsProjectRefreshTimeoutRef.current = null;
708
+ const refreshProjectsFn = refreshProjectsRef.current;
709
+ if (!refreshProjectsFn)
710
+ return;
711
+ void refreshProjectsFn();
712
+ }, 500);
713
+ };
714
+ if (type === "agent:run:status") {
715
+ const status = normalizeAgentStatus(payload?.data?.state);
716
+ if (status === undefined)
717
+ return;
718
+ const normalizedAgentId = agentId.toLowerCase();
719
+ setAgentStatusesById((prev) => ({ ...prev, [normalizedAgentId]: status }));
720
+ queueProjectsRefresh();
721
+ return;
722
+ }
723
+ if (type === "agent:run:start") {
724
+ const normalizedAgentId = agentId.toLowerCase();
725
+ setAgentStatusesById((prev) => ({ ...prev, [normalizedAgentId]: "running" }));
726
+ queueProjectsRefresh();
727
+ return;
728
+ }
729
+ if (type === "agent:run:complete") {
730
+ const normalizedAgentId = agentId.toLowerCase();
731
+ setAgentStatusesById((prev) => ({ ...prev, [normalizedAgentId]: "completed" }));
732
+ queueProjectsRefresh();
733
+ return;
734
+ }
735
+ if (type === "agent:run:error") {
736
+ const normalizedAgentId = agentId.toLowerCase();
737
+ setAgentStatusesById((prev) => ({ ...prev, [normalizedAgentId]: "error" }));
738
+ queueProjectsRefresh();
739
+ return;
740
+ }
741
+ if (type === "agent:run:closed") {
742
+ const normalizedAgentId = agentId.toLowerCase();
743
+ setAgentStatusesById((prev) => ({ ...prev, [normalizedAgentId]: "closed" }));
744
+ queueProjectsRefresh();
745
+ }
746
+ });
747
+ return () => unsubscribe();
748
+ }, [editContext]);
749
+ // Clean up pending debounced refresh on unmount only.
750
+ useEffect(() => {
751
+ return () => {
752
+ if (wsRefreshTimeoutRef.current !== null) {
753
+ window.clearTimeout(wsRefreshTimeoutRef.current);
754
+ wsRefreshTimeoutRef.current = null;
755
+ }
756
+ if (wsSubprojectRefreshTimeoutRef.current !== null) {
757
+ window.clearTimeout(wsSubprojectRefreshTimeoutRef.current);
758
+ wsSubprojectRefreshTimeoutRef.current = null;
759
+ }
760
+ if (wsProjectRefreshTimeoutRef.current !== null) {
761
+ window.clearTimeout(wsProjectRefreshTimeoutRef.current);
762
+ wsProjectRefreshTimeoutRef.current = null;
763
+ }
764
+ };
765
+ }, []);
766
+ // Ensure selected task still exists after reload
767
+ useEffect(() => {
768
+ if (!selectedTaskId)
769
+ return;
770
+ if (!tasks.some((t) => t.taskId === selectedTaskId))
771
+ setSelectedTaskId(null);
772
+ }, [selectedTaskId, tasks]);
773
+ // No special auto-selection by project status — the agent panel is driven by
774
+ // whichever task the user selects.
775
+ // The backend now creates the Plan task and triggers the planner agent
776
+ // during project creation (TaskService.CreateProjectAsync), so we no longer
777
+ // need to auto-trigger planning from the frontend.
778
+ const taskBoardContent = useMemo(() => {
779
+ if (!selectedProjectId) {
780
+ return (_jsx("div", { className: "text-sm text-muted-foreground p-6", children: "Select a project to view tasks." }));
781
+ }
782
+ if (isListView) {
783
+ return (_jsxs("div", { className: "grid gap-3", children: [_jsx(ProjectDashboard, { selectedProjectTitle: selectedProject?.project.title ?? "Current project", selectedProjectDescription: selectedProject?.project.description, selectedProjectStatus: selectedProject?.project.status, selectedProjectCostUsed: selectedProjectCumulativeCostUsed, selectedProjectCostLimit: selectedProject?.project.costLimit ?? null, canEditProjectStatus: selectedProject?.permission === "Owner", savingProjectStatus: savingProjectStatus, onStatusChange: handleProjectStatusChange, selectedProjectTaskCounts: selectedProjectTaskCounts, subprojects: directSubprojects, taskCountsByProjectId: subprojectTaskCounts, loading: subprojectCountsLoading, parentProjectId: parentProject?.project.projectId ?? null, parentProjectTitle: parentProject?.project.title ?? null, onSelectProject: setSelectedProjectId }), _jsx(ListView, { tasks: tasksWithDisplayAssignees, onSelectTask: (id) => setSelectedTaskId(id), selectedTaskId: selectedTaskId, onTasksChanged: () => {
784
+ if (selectedProjectId) {
785
+ void refreshTasks(selectedProjectId);
786
+ void refreshDependencies(selectedProjectId);
787
+ void refreshSubprojectTaskCounts();
788
+ }
789
+ }, projectId: selectedProjectId, permission: selectedProject?.permission, agentStatusesById: agentStatusesById, activeTab: activeTab, onSetActiveTab: setActiveTab })] }));
790
+ }
791
+ return (_jsxs("div", { className: "grid gap-3", children: [_jsx(ProjectDashboard, { selectedProjectTitle: selectedProject?.project.title ?? "Current project", selectedProjectDescription: selectedProject?.project.description, selectedProjectStatus: selectedProject?.project.status, selectedProjectCostUsed: selectedProjectCumulativeCostUsed, selectedProjectCostLimit: selectedProject?.project.costLimit ?? null, canEditProjectStatus: selectedProject?.permission === "Owner", savingProjectStatus: savingProjectStatus, onStatusChange: handleProjectStatusChange, selectedProjectTaskCounts: selectedProjectTaskCounts, subprojects: directSubprojects, taskCountsByProjectId: subprojectTaskCounts, loading: subprojectCountsLoading, parentProjectId: parentProject?.project.projectId ?? null, parentProjectTitle: parentProject?.project.title ?? null, onSelectProject: setSelectedProjectId }), _jsx(KanbanView, { tasks: tasksWithDisplayAssignees, dependencies: dependencies, onSelectTask: (id) => setSelectedTaskId(id), selectedTaskId: selectedTaskId, onTasksChanged: () => {
792
+ if (selectedProjectId) {
793
+ void refreshTasks(selectedProjectId);
794
+ void refreshDependencies(selectedProjectId);
795
+ void refreshSubprojectTaskCounts();
796
+ }
797
+ }, projectId: selectedProjectId, permission: selectedProject?.permission, agentStatusesById: agentStatusesById, activeTab: activeTab, onSetActiveTab: setActiveTab })] }));
798
+ }, [
799
+ selectedProjectId,
800
+ isListView,
801
+ tasksWithDisplayAssignees,
802
+ selectedTaskId,
803
+ refreshTasks,
804
+ refreshDependencies,
805
+ selectedProject?.permission,
806
+ selectedProject?.project.description,
807
+ selectedProject?.project.status,
808
+ selectedProject?.project.costLimit,
809
+ selectedProjectCumulativeCostUsed,
810
+ agentStatusesById,
811
+ dependencies,
812
+ directSubprojects,
813
+ parentProject?.project.projectId,
814
+ parentProject?.project.title,
815
+ selectedProject?.project.title,
816
+ selectedProjectTaskCounts,
817
+ subprojectTaskCounts,
818
+ subprojectCountsLoading,
819
+ refreshSubprojectTaskCounts,
820
+ savingProjectStatus,
821
+ handleProjectStatusChange,
822
+ activeTab,
823
+ ]);
824
+ const taskDetailPanel = useMemo(() => (_jsx(TaskDetailPanel, { project: selectedProject, task: selectedTask, allTasks: tasksWithDisplayAssignees, dependencies: dependencies, agentStatusesById: agentStatusesById, onTaskChanged: () => {
825
+ if (selectedProjectId) {
826
+ void refreshTasks(selectedProjectId);
827
+ void refreshDependencies(selectedProjectId);
828
+ }
829
+ }, onSelectTask: (taskId) => setSelectedTaskId(taskId), onClose: () => setSelectedTaskId(null), variant: "panel" })), [selectedProject, selectedTask, tasksWithDisplayAssignees, dependencies, agentStatusesById, selectedProjectId, refreshTasks, refreshDependencies]);
830
+ const agentTerminalPanel = useMemo(() => (_jsx(TaskAgentPanel, { agentId: currentAgentId, mode: agentPanelMode, label: selectedTaskIsPlan ? "Planner" : "Task Agent", onStartPlanning: selectedTaskIsPlan && isPlanning ? handleStartPlanning : undefined, canAssignAgent: !selectedTaskIsBlocked, assignAgentDisabledReason: selectedTaskIsBlocked
831
+ ? "This task is blocked by unfinished dependencies."
832
+ : undefined, onAssignAgent: !selectedTaskIsPlan && canEditTasks && agentPanelMode === "no-agent"
833
+ ? () => setAssignAgentDialogOpen(true)
834
+ : undefined })), [currentAgentId, agentPanelMode, selectedTaskIsPlan, isPlanning, handleStartPlanning, selectedTaskIsBlocked, canEditTasks]);
835
+ const slotContext = editContext?.getActiveSlotContext();
836
+ const previewItemVersion = useMemo(() => {
837
+ if (!currentItemDescriptor)
838
+ return null;
839
+ return currentItemDescriptor.version === 0
840
+ ? (editContext?.item?.version ?? null)
841
+ : currentItemDescriptor.version;
842
+ }, [currentItemDescriptor, editContext?.item?.version]);
843
+ const contextItemOptions = useMemo(() => {
844
+ return agentContextItems.map((contextItem) => {
845
+ const key = `${contextItem.id}|${contextItem.language}|${contextItem.version}`;
846
+ const name = contextItemNamesByKey[key] || contextItem.name || contextItem.id;
847
+ const path = contextItemPathsByKey[key] || contextItem.path || "";
848
+ return {
849
+ value: key,
850
+ label: name,
851
+ description: `${path || contextItem.id} (${contextItem.language}/v${contextItem.version})`,
852
+ };
853
+ });
854
+ }, [agentContextItems, contextItemNamesByKey, contextItemPathsByKey]);
855
+ const selectedContextItemValue = useMemo(() => {
856
+ if (currentItemKey)
857
+ return currentItemKey;
858
+ const first = agentContextItems[0];
859
+ if (!first)
860
+ return "";
861
+ return `${first.id}|${first.language}|${first.version}`;
862
+ }, [currentItemKey, agentContextItems]);
863
+ const hasMultipleContextItems = contextItemOptions.length > 1;
864
+ useEffect(() => {
865
+ if (!currentAgentId) {
866
+ setAgentContextItems([]);
867
+ return;
868
+ }
869
+ let cancelled = false;
870
+ getAgent(currentAgentId)
871
+ .then((agent) => {
872
+ if (cancelled)
873
+ return;
874
+ const rawContext = agent?.agentContext;
875
+ if (!rawContext) {
876
+ setAgentContextItems([]);
877
+ return;
878
+ }
879
+ let parsed = null;
880
+ try {
881
+ parsed = JSON.parse(rawContext);
882
+ }
883
+ catch {
884
+ parsed = null;
885
+ }
886
+ const items = (parsed?.items || []).filter((x) => !!x?.id && !!x?.language && typeof x?.version === "number");
887
+ setAgentContextItems(items);
888
+ })
889
+ .catch(() => {
890
+ if (cancelled)
891
+ return;
892
+ setAgentContextItems([]);
893
+ });
894
+ return () => {
895
+ cancelled = true;
896
+ };
897
+ }, [currentAgentId]);
898
+ useEffect(() => {
899
+ if (!editContext || agentContextItems.length === 0) {
900
+ setContextItemNamesByKey({});
901
+ setContextItemPathsByKey({});
902
+ return;
903
+ }
904
+ let cancelled = false;
905
+ Promise.all(agentContextItems.map(async (contextItem) => {
906
+ const key = `${contextItem.id}|${contextItem.language}|${contextItem.version}`;
907
+ const descriptor = {
908
+ id: contextItem.id,
909
+ language: contextItem.language,
910
+ version: contextItem.version,
911
+ };
912
+ const item = await editContext.itemsRepository.getItem(descriptor);
913
+ return {
914
+ key,
915
+ name: item?.name || contextItem.name || "",
916
+ path: item?.path || contextItem.path || "",
917
+ };
918
+ }))
919
+ .then((results) => {
920
+ if (cancelled)
921
+ return;
922
+ const nextNames = {};
923
+ const nextPaths = {};
924
+ for (const result of results) {
925
+ nextNames[result.key] = result.name;
926
+ nextPaths[result.key] = result.path;
927
+ }
928
+ setContextItemNamesByKey(nextNames);
929
+ setContextItemPathsByKey(nextPaths);
930
+ })
931
+ .catch(() => {
932
+ if (cancelled)
933
+ return;
934
+ setContextItemNamesByKey({});
935
+ setContextItemPathsByKey({});
936
+ });
937
+ return () => {
938
+ cancelled = true;
939
+ };
940
+ }, [editContext, agentContextItems]);
941
+ const handleSelectContextItem = useCallback((value) => {
942
+ const [id, language, versionText] = value.split("|");
943
+ const version = Number(versionText);
944
+ if (!id || !language || Number.isNaN(version) || !editContext?.loadItem)
945
+ return;
946
+ void editContext.loadItem({ id, language, version });
947
+ }, [editContext]);
948
+ useEffect(() => {
949
+ if (!editContext || !currentItemDescriptor) {
950
+ setPreviewItemName("");
951
+ setPreviewItemPath("");
952
+ return;
953
+ }
954
+ const activeItem = editContext.item;
955
+ const itemMatchesDescriptor = activeItem?.id === currentItemDescriptor.id &&
956
+ activeItem?.language === currentItemDescriptor.language;
957
+ if (itemMatchesDescriptor) {
958
+ setPreviewItemName(activeItem?.name || "");
959
+ setPreviewItemPath(activeItem?.path || "");
960
+ return;
961
+ }
962
+ let cancelled = false;
963
+ editContext.itemsRepository
964
+ .getItem(currentItemDescriptor)
965
+ .then((item) => {
966
+ if (cancelled)
967
+ return;
968
+ setPreviewItemName(item?.name || "");
969
+ setPreviewItemPath(item?.path || "");
970
+ })
971
+ .catch(() => {
972
+ if (cancelled)
973
+ return;
974
+ setPreviewItemName("");
975
+ setPreviewItemPath("");
976
+ });
977
+ return () => {
978
+ cancelled = true;
979
+ };
980
+ }, [
981
+ editContext,
982
+ currentItemDescriptor,
983
+ currentItemDescriptor?.id,
984
+ currentItemDescriptor?.language,
985
+ currentItemDescriptor?.version,
986
+ editContext?.item?.id,
987
+ editContext?.item?.language,
988
+ editContext?.item?.name,
989
+ editContext?.item?.path,
990
+ ]);
991
+ const itemPreviewPanel = useMemo(() => {
992
+ const hasCurrentItem = !!currentItemDescriptor;
993
+ if (!slotContext) {
994
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx("div", { className: "border-b border-gray-200 bg-white px-3 py-2", children: hasCurrentItem ? (_jsxs("div", { className: "min-w-0", children: [hasMultipleContextItems ? (_jsx(Select, { value: selectedContextItemValue, onValueChange: handleSelectContextItem, options: contextItemOptions, placeholder: "Select context item", size: "xs", searchable: true, className: "h-7 rounded-md bg-white", maxWidth: 420, "data-testid": "taskboard-context-item-selector" })) : (_jsx("div", { className: "truncate text-sm font-medium text-slate-900", children: previewItemName || "Loading item..." })), _jsxs("div", { className: "mt-0.5 flex items-center gap-2", children: [_jsx("span", { className: "truncate text-xs text-slate-500", children: previewItemPath || "Path unavailable" }), _jsx("span", { className: "shrink-0 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium text-slate-600", children: currentItemDescriptor?.language }), previewItemVersion != null && (_jsxs("span", { className: "shrink-0 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium text-slate-600", children: ["v", previewItemVersion] }))] })] })) : (_jsx("div", { className: "text-xs text-slate-500", children: "No item loaded" })) }), _jsxs("div", { className: "flex min-h-0 flex-1 flex-col items-center justify-center gap-3 bg-gray-50 p-6 text-center", children: [_jsx("div", { className: "text-sm font-medium text-muted-foreground", children: "Editor preview unavailable" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Open the editor context item to preview and edit it here." })] })] }));
995
+ }
996
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx("div", { className: "border-b border-gray-200 bg-white px-3 py-2", children: hasCurrentItem ? (_jsxs("div", { className: "min-w-0", children: [hasMultipleContextItems ? (_jsx(Select, { value: selectedContextItemValue, onValueChange: handleSelectContextItem, options: contextItemOptions, placeholder: "Select context item", size: "xs", searchable: true, className: "h-7 rounded-md bg-white", maxWidth: 420, "data-testid": "taskboard-context-item-selector" })) : (_jsx("div", { className: "truncate text-sm font-medium text-slate-900", children: previewItemName || "Loading item..." })), _jsxs("div", { className: "mt-0.5 flex items-center gap-2", children: [_jsx("span", { className: "truncate text-xs text-slate-500", children: previewItemPath || "Path unavailable" }), _jsx("span", { className: "shrink-0 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium text-slate-600", children: currentItemDescriptor?.language }), previewItemVersion != null && (_jsxs("span", { className: "shrink-0 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium text-slate-600", children: ["v", previewItemVersion] }))] })] })) : (_jsx("div", { className: "text-xs text-slate-500", children: "No item loaded" })) }), _jsx("div", { className: "min-h-0 flex-1 overflow-hidden bg-gray-50", children: _jsx(EditorSlotContextProvider, { value: slotContext, children: _jsx(SingleEditView, { compareView: false, name: "taskboard-workspace-preview", view: "primary" }) }) })] }));
997
+ }, [
998
+ slotContext,
999
+ currentItemDescriptor,
1000
+ contextItemOptions,
1001
+ hasMultipleContextItems,
1002
+ handleSelectContextItem,
1003
+ selectedContextItemValue,
1004
+ previewItemName,
1005
+ previewItemPath,
1006
+ previewItemVersion,
1007
+ ]);
1008
+ useEffect(() => {
1009
+ if (!assignAgentDialogOpen)
1010
+ return;
1011
+ if (!selectedTask || agentPanelMode !== "no-agent" || selectedTaskIsBlocked) {
1012
+ setAssignAgentDialogOpen(false);
1013
+ }
1014
+ }, [assignAgentDialogOpen, selectedTask, agentPanelMode, selectedTaskIsBlocked]);
1015
+ const panels = useMemo(() => {
1016
+ if (isListView) {
1017
+ const listPanels = [
1018
+ {
1019
+ name: "task-list",
1020
+ defaultSize: "auto",
1021
+ content: (_jsx("div", { className: "h-full overflow-y-auto overflow-x-auto bg-slate-50/50 p-4 pt-3", children: taskBoardContent })),
1022
+ },
1023
+ {
1024
+ name: "task-details",
1025
+ defaultSize: 420,
1026
+ content: taskDetailPanel,
1027
+ hidden: !selectedTask,
1028
+ },
1029
+ {
1030
+ name: "agent-terminal",
1031
+ defaultSize: 560,
1032
+ content: agentTerminalPanel,
1033
+ },
1034
+ ];
1035
+ if (showEditorPanel) {
1036
+ listPanels.push({
1037
+ name: "editor-preview",
1038
+ defaultSize: 500,
1039
+ content: (_jsx("div", { className: "h-full overflow-hidden border-l border-gray-200 bg-gray-50", children: itemPreviewPanel })),
1040
+ });
1041
+ }
1042
+ return listPanels;
1043
+ }
1044
+ const kanbanPanels = [
1045
+ {
1046
+ name: "work-area",
1047
+ defaultSize: "auto",
1048
+ content: (_jsx(Splitter, { direction: "vertical", localStorageKey: "task-board-work-area-splitter", panels: [
1049
+ {
1050
+ name: "board",
1051
+ defaultSize: "auto",
1052
+ content: (_jsx("div", { className: "h-full overflow-y-auto overflow-x-hidden bg-slate-50/50 p-4 pt-3", children: taskBoardContent })),
1053
+ },
1054
+ {
1055
+ name: "task-details",
1056
+ defaultSize: 320,
1057
+ content: taskDetailPanel,
1058
+ hidden: !selectedTask,
1059
+ },
1060
+ ] })),
1061
+ },
1062
+ {
1063
+ name: "agent-terminal",
1064
+ defaultSize: 560,
1065
+ content: agentTerminalPanel,
1066
+ },
1067
+ ];
1068
+ if (showEditorPanel) {
1069
+ kanbanPanels.push({
1070
+ name: "editor-preview",
1071
+ defaultSize: 500,
1072
+ content: (_jsx("div", { className: "h-full overflow-hidden border-l border-gray-200 bg-gray-50", children: itemPreviewPanel })),
1073
+ });
1074
+ }
1075
+ return kanbanPanels;
1076
+ }, [isListView, taskBoardContent, taskDetailPanel, agentTerminalPanel, selectedTask, showEditorPanel, itemPreviewPanel]);
1077
+ return (_jsxs("div", { className: "flex h-full w-full flex-col bg-white text-foreground select-text", children: [_jsx("div", { className: "min-h-0 flex-1", children: _jsx(Splitter, { panels: panels, direction: "horizontal", localStorageKey: isListView ? "task-board-list-splitter" : "task-board-kanban-splitter" }) }), selectedProjectId && (_jsx(CreateTaskDialog, { open: createDialogOpen, onOpenChange: setCreateDialogOpen, projectId: selectedProjectId, onCreated: () => {
1078
+ void refreshTasks(selectedProjectId);
1079
+ void refreshDependencies(selectedProjectId);
1080
+ } })), _jsx(CreateProjectDialog, { open: createProjectOpen, onOpenChange: setCreateProjectOpen, projects: projects, onCreated: async (newProjectId) => {
1081
+ await refreshProjects();
1082
+ if (newProjectId)
1083
+ setSelectedProjectId(newProjectId);
1084
+ } }), _jsx(ProjectSettingsDialog, { open: !!settingsProjectId, onOpenChange: (open) => {
1085
+ if (!open)
1086
+ setSettingsProjectId(null);
1087
+ }, projectId: settingsProjectId, selectedProject: settingsProject, onChanged: refreshProjects }), _jsx(AssignAgentDialog, { open: assignAgentDialogOpen, task: selectedTask, onOpenChange: setAssignAgentDialogOpen, onAssigned: async () => {
1088
+ if (!selectedProjectId)
1089
+ return;
1090
+ await refreshTasks(selectedProjectId);
1091
+ await refreshDependencies(selectedProjectId);
1092
+ } })] }));
1093
+ }
1094
+ //# sourceMappingURL=TaskBoardWorkspace.js.map