@taskforcehq/taskforce 0.3.328 → 0.3.329

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 (264) hide show
  1. package/dist/Taskforce.module.css +233 -34
  2. package/dist/TaskforceCore.js +237 -230
  3. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +1 -1
  4. package/dist/cli.js +34 -0
  5. package/dist/cliCloudTargetGuard.d.ts +5 -0
  6. package/dist/cliCloudTargetGuard.js +11 -0
  7. package/dist/components/features/AnnotatedAttachmentWorkspace.js +24 -8
  8. package/dist/components/features/TaskSettings.d.ts +12 -1
  9. package/dist/components/features/TaskSettings.js +147 -40
  10. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +1 -1
  11. package/dist/components/features/planning/PlanningModule.js +2 -5
  12. package/dist/components/features/planning/PlanningTaskOrderList.d.ts +1 -1
  13. package/dist/components/features/planning/PlanningTaskOrderList.js +17 -6
  14. package/dist/components/features/planning/planningTaskArrangement.d.ts +1 -0
  15. package/dist/components/features/planning/planningTaskArrangement.js +5 -5
  16. package/dist/components/features/taskforceAgentEditorModel.js +3 -1
  17. package/dist/components/task/TaskCard.d.ts +5 -0
  18. package/dist/components/task/TaskCard.js +65 -10
  19. package/dist/components/task/TaskImageReviews.js +14 -5
  20. package/dist/components/task/TaskKanban.d.ts +6 -1
  21. package/dist/components/task/TaskKanban.js +152 -6
  22. package/dist/components/task/TaskList.d.ts +5 -0
  23. package/dist/components/task/TaskList.js +3 -3
  24. package/dist/components/ui/PasswordInput.d.ts +6 -0
  25. package/dist/components/ui/PasswordInput.js +11 -0
  26. package/dist/components/ui/TopNoticeLayer.js +1 -1
  27. package/dist/components/views/AppShellHeader.js +32 -11
  28. package/dist/components/views/PlanComparisonPage.js +12 -0
  29. package/dist/components/views/StandaloneLayout.js +7 -31
  30. package/dist/components/views/WidgetView.js +3 -3
  31. package/dist/components/views/panels/PlanningDrawer.js +5 -12
  32. package/dist/components/views/standalone/modals/AccountSettingsModal.js +26 -17
  33. package/dist/config/envSchema.js +12 -0
  34. package/dist/config/localServiceRuntimeContract.d.ts +13 -0
  35. package/dist/config/localServiceRuntimeContract.js +135 -0
  36. package/dist/core/AiProfileService.js +46 -8
  37. package/dist/core/DeletedTaskLifecycleService.d.ts +1 -0
  38. package/dist/core/DeletedTaskLifecycleService.js +37 -6
  39. package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +3 -2
  40. package/dist/core/TaskAttachmentLinksDurableMutationService.js +33 -9
  41. package/dist/core/Taskforce.d.ts +62 -2
  42. package/dist/core/Taskforce.js +635 -32
  43. package/dist/core/types.d.ts +6 -0
  44. package/dist/hooks/sync/useDurableLocalOutboxDrain.d.ts +1 -1
  45. package/dist/hooks/sync/useDurableTaskCreateMutation.js +1 -1
  46. package/dist/hooks/sync/useDurableTaskMetadataMutation.d.ts +1 -0
  47. package/dist/hooks/sync/useDurableTaskMetadataMutation.js +1 -0
  48. package/dist/hooks/sync/useDurableTaskStatusMutation.d.ts +1 -0
  49. package/dist/hooks/tasks/taskHttpMutation.js +1 -0
  50. package/dist/hooks/tasks/useCloudTaskSearch.d.ts +12 -0
  51. package/dist/hooks/tasks/useCloudTaskSearch.js +71 -0
  52. package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.d.ts +16 -0
  53. package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.js +197 -0
  54. package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +16 -3
  55. package/dist/hooks/tasks/useTaskEditorNavigation.js +72 -13
  56. package/dist/hooks/tasks/useTaskFormActions.d.ts +6 -0
  57. package/dist/hooks/tasks/useTaskFormActions.js +65 -12
  58. package/dist/hooks/ui/useSettingsModel.d.ts +24 -2
  59. package/dist/hooks/ui/useSettingsModel.js +13 -2
  60. package/dist/hooks/ui/useSettingsPersistence.d.ts +12 -1
  61. package/dist/hooks/ui/useSettingsPersistence.js +74 -47
  62. package/dist/hooks/useFilterSort.d.ts +7 -3
  63. package/dist/hooks/useFilterSort.js +15 -5
  64. package/dist/hooks/useTaskData.d.ts +10 -1
  65. package/dist/hooks/useTaskData.js +216 -7
  66. package/dist/hooks/useTaskMutations.d.ts +10 -2
  67. package/dist/hooks/useTaskMutations.js +223 -43
  68. package/dist/hooks/useTaskforce.d.ts +37 -9
  69. package/dist/hooks/useTaskforce.js +349 -39
  70. package/dist/hooks/useUiNotice.d.ts +4 -0
  71. package/dist/hooks/useUiNotice.js +4 -0
  72. package/dist/mcp/adminWorkspaceRegistrar.js +3 -3
  73. package/dist/mcp/documentAssetRegistrar.js +44 -35
  74. package/dist/mcp/documentHelpers.d.ts +1 -0
  75. package/dist/mcp/documentHelpers.js +7 -3
  76. package/dist/mcp/runtime.js +172 -42
  77. package/dist/mcp/taskPlanningRegistrar.js +41 -38
  78. package/dist/mcp/toolCallCompatibility.js +38 -1
  79. package/dist/mcp/toolCatalog.d.ts +1 -0
  80. package/dist/migrations/taskSchemaMigrations.js +12 -0
  81. package/dist/runtime/appGateDefinitions.d.ts +30 -6
  82. package/dist/runtime/appGateDefinitions.js +6 -6
  83. package/dist/runtime/appGates.d.ts +12 -13
  84. package/dist/runtime/appGates.js +29 -13
  85. package/dist/runtime/nodeAppGates.d.ts +8 -0
  86. package/dist/runtime/nodeAppGates.js +27 -0
  87. package/dist/server/cloudCollectionRead.d.ts +1 -1
  88. package/dist/server/cloudCollectionRead.js +2 -2
  89. package/dist/server/index.d.ts +3 -0
  90. package/dist/server/index.js +15 -3
  91. package/dist/server/localServiceLease.d.ts +3 -0
  92. package/dist/server/localServiceLease.js +9 -1
  93. package/dist/server/localWorkspaceSyncServerRuntime.js +10 -3
  94. package/dist/server/rateLimit.d.ts +1 -0
  95. package/dist/server/rateLimit.js +4 -0
  96. package/dist/server/realtimeSyncWs.js +36 -12
  97. package/dist/server/routes/admin.js +1 -0
  98. package/dist/server/routes/agentRoles.js +3 -2
  99. package/dist/server/routes/agentSkills.js +3 -2
  100. package/dist/server/routes/agents.js +118 -16
  101. package/dist/server/routes/annotatedAttachments.js +3 -2
  102. package/dist/server/routes/auth.js +3 -1
  103. package/dist/server/routes/durableTaskHttpRouters.d.ts +9 -2
  104. package/dist/server/routes/localService.js +2 -0
  105. package/dist/server/routes/primitives.d.ts +2 -0
  106. package/dist/server/routes/syncAuxRoutes.js +5 -2
  107. package/dist/server/routes/syncPullApplyRoutes.js +23 -4
  108. package/dist/server/routes/syncV3FeedRoutes.js +18 -16
  109. package/dist/server/routes/syncV3LocalCaptureRoutes.js +28 -2
  110. package/dist/server/routes/syncV3LocalFeedApplyRoutes.d.ts +1 -0
  111. package/dist/server/routes/syncV3LocalFeedApplyRoutes.js +26 -0
  112. package/dist/server/routes/syncV3OperationRoutes.js +10 -0
  113. package/dist/server/routes/tasks.js +103 -11
  114. package/dist/server/routes/workspaces.js +43 -1
  115. package/dist/server/workspaceDeletionAdmission.d.ts +23 -0
  116. package/dist/server/workspaceDeletionAdmission.js +50 -0
  117. package/dist/service/localServiceCli.js +16 -0
  118. package/dist/service/localServiceClientLifecycle.js +36 -10
  119. package/dist/services/codexAppServerModelGateway.d.ts +112 -0
  120. package/dist/services/codexAppServerModelGateway.js +438 -0
  121. package/dist/services/taskforceAgentMcpBridge.d.ts +7 -1
  122. package/dist/services/taskforceAgentMcpBridge.js +23 -3
  123. package/dist/shared/taskforceAgentDefinition.js +7 -1
  124. package/dist/shared/taskforceAgentModels.d.ts +1 -0
  125. package/dist/shared/taskforceAgentModels.js +30 -1
  126. package/dist/storage/postgresAdapter.d.ts +20 -3
  127. package/dist/storage/postgresAdapter.js +106 -14
  128. package/dist/storage/postgresWorker.js +13 -4
  129. package/dist/storage/postgresWorkerProtocol.d.ts +29 -0
  130. package/dist/storage/postgresWorkerProtocol.js +30 -0
  131. package/dist/sync/cloudSyncApi.d.ts +4 -1
  132. package/dist/sync/cloudSyncApi.js +7 -2
  133. package/dist/sync/contentSyncState.d.ts +1 -1
  134. package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +1 -0
  135. package/dist/sync/coordinator/localSyncExecutionPermitStore.js +7 -1
  136. package/dist/sync/coordinator/localWorkspaceSyncRunner.js +6 -2
  137. package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +20 -2
  138. package/dist/sync/engine/workspaceSyncEngineLocalServices.d.ts +4 -0
  139. package/dist/sync/engine/workspaceSyncEngineLocalServices.js +11 -0
  140. package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +11 -0
  141. package/dist/sync/engine/workspaceSyncEngineRemoteServices.js +2 -1
  142. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +31 -4
  143. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.d.ts +3 -1
  144. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +25 -8
  145. package/dist/sync/engine/workspaceSyncEngineTransfers.d.ts +1 -1
  146. package/dist/sync/engine/workspaceSyncEngineTransfers.js +8 -3
  147. package/dist/sync/syncService.d.ts +2 -0
  148. package/dist/sync/syncService.js +4 -1
  149. package/dist/sync/v3/combinedFeedCapability.js +4 -0
  150. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +2 -0
  151. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +11 -1
  152. package/dist/sync/v3/durableTaskChecklistMutationRouter.d.ts +2 -0
  153. package/dist/sync/v3/durableTaskChecklistMutationRouter.js +4 -0
  154. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.d.ts +1 -0
  155. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +30 -8
  156. package/dist/sync/v3/durableTaskHttpRootMutationRouter.d.ts +5 -3
  157. package/dist/sync/v3/durableTaskHttpRootMutationRouter.js +27 -2
  158. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +2 -0
  159. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +47 -7
  160. package/dist/sync/v3/localTaskCreateOutboxService.js +15 -6
  161. package/dist/sync/v3/localTaskMetadataClient.d.ts +1 -0
  162. package/dist/sync/v3/localTaskMetadataClient.js +1 -0
  163. package/dist/sync/v3/localTaskMetadataOutboxHandler.js +5 -1
  164. package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -0
  165. package/dist/sync/v3/localTaskMetadataOutboxService.js +7 -0
  166. package/dist/sync/v3/localTaskRestoreOutboxService.js +14 -6
  167. package/dist/sync/v3/localTaskStatusClient.d.ts +1 -0
  168. package/dist/sync/v3/localTaskStatusClient.js +6 -1
  169. package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -0
  170. package/dist/sync/v3/localTaskStatusOutboxService.js +54 -13
  171. package/dist/sync/v3/syncDurabilityStore.d.ts +7 -0
  172. package/dist/sync/v3/syncDurabilityStore.js +31 -11
  173. package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +6 -4
  174. package/dist/sync/v3/taskAttachmentLinksMutationService.js +19 -10
  175. package/dist/sync/v3/taskChecklistMutationService.d.ts +4 -2
  176. package/dist/sync/v3/taskChecklistMutationService.js +4 -0
  177. package/dist/sync/v3/taskCreateMutationService.d.ts +2 -2
  178. package/dist/sync/v3/taskCreateMutationService.js +7 -4
  179. package/dist/sync/v3/taskMetadataMutationService.d.ts +3 -0
  180. package/dist/sync/v3/taskMetadataMutationService.js +4 -0
  181. package/dist/sync/v3/taskRestoreMutationService.d.ts +2 -2
  182. package/dist/sync/v3/taskRestoreMutationService.js +7 -4
  183. package/dist/sync/v3/taskStatusMutationService.d.ts +2 -0
  184. package/dist/sync/v3/taskStatusMutationService.js +4 -0
  185. package/dist/sync/v3/workflowAssignmentSync.d.ts +1 -0
  186. package/dist/sync/v3/workflowAssignmentSync.js +3 -0
  187. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.d.ts +1 -1
  188. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +13 -3
  189. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.d.ts +11 -0
  190. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.js +27 -10
  191. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.d.ts +2 -1
  192. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.js +52 -10
  193. package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +9 -1
  194. package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +8 -3
  195. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +51 -10
  196. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +7 -0
  197. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +30 -18
  198. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +9 -4
  199. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +98 -47
  200. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.d.ts +10 -0
  201. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +58 -21
  202. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.d.ts +3 -1
  203. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.js +18 -0
  204. package/dist/sync/v3/workspaceSyncV3CoverageRegistry.js +6 -0
  205. package/dist/sync/v3/workspaceSyncV3FeedProfile.d.ts +1 -1
  206. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.d.ts +1 -0
  207. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.js +23 -10
  208. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.d.ts +3 -0
  209. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.js +64 -13
  210. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.d.ts +9 -0
  211. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.js +26 -0
  212. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +15 -5
  213. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +50 -14
  214. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.d.ts +2 -1
  215. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.js +11 -3
  216. package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +2 -0
  217. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.d.ts +2 -0
  218. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.js +19 -7
  219. package/dist/sync/v3/workspaceSyncV3TaskCommentAudit.js +9 -2
  220. package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.d.ts +7 -0
  221. package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.js +37 -0
  222. package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.d.ts +6 -0
  223. package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.js +9 -0
  224. package/dist/sync/workspacePullFeed.js +2 -1
  225. package/dist/sync/workspaceRepair.d.ts +3 -0
  226. package/dist/sync/workspaceRepair.js +67 -6
  227. package/dist/types.d.ts +47 -1
  228. package/dist/types.js +6 -0
  229. package/dist/ui/assets/{AiIdentityRosterCard-MZs7lVED.js → AiIdentityRosterCard-BZM4aQnM.js} +1 -1
  230. package/dist/ui/assets/AiProfilesModule-BbAEWtHH.js +1 -0
  231. package/dist/ui/assets/AnnotatedAttachmentWorkspace-B68Wg-RV.js +3 -0
  232. package/dist/ui/assets/AnnotatedAttachmentWorkspace-TzYu7pkG.css +1 -0
  233. package/dist/ui/assets/{AssetTraySortControl-Kf1pelSy.js → AssetTraySortControl-C8Rztyal.js} +1 -1
  234. package/dist/ui/assets/{ContextAttachmentManager-B8boOs6Q.js → ContextAttachmentManager-Cj3c7X9y.js} +2 -2
  235. package/dist/ui/assets/{DocumentWorkspace-DXH4XUsB.js → DocumentWorkspace-B5uvzems.js} +1 -1
  236. package/dist/ui/assets/{EntityActivityTimeline-BtSTq0ON.js → EntityActivityTimeline-BVFvCB5R.js} +1 -1
  237. package/dist/ui/assets/PlanningModule-B5DWQOGz.js +1 -0
  238. package/dist/ui/assets/PlansPage-AY4cGWco.js +1 -0
  239. package/dist/ui/assets/TaskContextUpload-DZxTVesW.js +1 -0
  240. package/dist/ui/assets/TaskSettings-BeWAuyZj.js +12 -0
  241. package/dist/ui/assets/{TaskforceAgentsModule-BazdUwxZ.js → TaskforceAgentsModule-BifCYtjR.js} +6 -6
  242. package/dist/ui/assets/{WorkflowManagerModule-DWFspC-o.js → WorkflowManagerModule-DEr5di_r.js} +1 -1
  243. package/dist/ui/assets/index-I5zhdtgt.js +7 -0
  244. package/dist/ui/assets/index-VRFsx3wz.css +1 -0
  245. package/dist/ui/assets/{vendor-icons-BkFLXavV.js → vendor-icons-D9Lpw-j4.js} +1 -1
  246. package/dist/ui/index.html +3 -3
  247. package/dist/utils/taskActivity.d.ts +3 -2
  248. package/dist/utils/taskActivity.js +19 -2
  249. package/dist/utils/taskEditorReconciliation.d.ts +40 -0
  250. package/dist/utils/taskEditorReconciliation.js +217 -0
  251. package/dist/utils/taskNormalization.d.ts +1 -0
  252. package/dist/utils/taskNormalization.js +39 -0
  253. package/dist/utils/taskSearch.js +2 -2
  254. package/dist/utils/uiNotice.d.ts +4 -0
  255. package/package.json +16 -4
  256. package/dist/ui/assets/AiProfilesModule-C3i0LOxe.js +0 -1
  257. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CBRZ8rca.css +0 -1
  258. package/dist/ui/assets/AnnotatedAttachmentWorkspace-DVZk5Tln.js +0 -3
  259. package/dist/ui/assets/PlanningModule-BPVyEa-f.js +0 -1
  260. package/dist/ui/assets/PlansPage-ChqgxALc.js +0 -1
  261. package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +0 -1
  262. package/dist/ui/assets/TaskSettings-g4ECKNOz.js +0 -12
  263. package/dist/ui/assets/index-BUplSsv_.js +0 -7
  264. package/dist/ui/assets/index-gz2EXuoK.css +0 -1
@@ -4,10 +4,14 @@ import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, us
4
4
  import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable';
5
5
  import { CSS } from '@dnd-kit/utilities';
6
6
  import { GripVertical } from 'lucide-react';
7
+ import { isPlanningTaskTerminal } from './planningTaskArrangement';
7
8
  import styles from './PlanningTaskOrderList.module.css';
8
9
  function SortableTask(props) {
9
10
  const sortable = useSortable({ id: props.task.id, disabled: props.disabled });
10
- return (_jsxs("div", { ref: sortable.setNodeRef, className: styles.item, style: { transform: CSS.Transform.toString(sortable.transform), transition: sortable.transition }, children: [_jsx("button", { type: "button", className: styles.grip, disabled: props.disabled, "aria-label": `Reorder ${props.task.title}`, title: "Drag to change execution order", ...sortable.attributes, ...sortable.listeners, children: _jsx(GripVertical, { size: 15, "aria-hidden": "true" }) }), _jsx("div", { className: styles.card, children: props.children })] }));
11
+ const reorderLabel = props.disabledReason
12
+ ? `Reorder ${props.task.title} — ${props.disabledReason}`
13
+ : `Reorder ${props.task.title}`;
14
+ return (_jsxs("div", { ref: sortable.setNodeRef, className: styles.item, style: { transform: CSS.Transform.toString(sortable.transform), transition: sortable.transition }, children: [_jsx("button", { ref: sortable.setActivatorNodeRef, type: "button", className: styles.grip, disabled: props.disabled, "aria-label": reorderLabel, title: props.disabledReason || 'Drag to change execution order', ...sortable.attributes, ...sortable.listeners, children: _jsx(GripVertical, { size: 15, "aria-hidden": "true" }) }), _jsx("div", { className: styles.card, children: props.children })] }));
11
15
  }
12
16
  export function PlanningTaskOrderList(props) {
13
17
  const [orderedIds, setOrderedIds] = React.useState(() => props.tasks.map((task) => task.id));
@@ -19,7 +23,8 @@ export function PlanningTaskOrderList(props) {
19
23
  const byId = new Map(props.tasks.map((task) => [task.id, task]));
20
24
  return orderedIds.map((id) => byId.get(id)).filter((task) => Boolean(task));
21
25
  }, [orderedIds, props.tasks]);
22
- const draggable = props.mode === 'execution' && Boolean(props.onReorder);
26
+ const hasReorderAction = Boolean(props.onReorder);
27
+ const draggable = props.mode === 'execution' && hasReorderAction;
23
28
  const handleDragEnd = React.useCallback(async (event) => {
24
29
  if (!draggable || saving || !event.over || event.active.id === event.over.id)
25
30
  return;
@@ -29,8 +34,11 @@ export function PlanningTaskOrderList(props) {
29
34
  if (from < 0 || to < 0)
30
35
  return;
31
36
  const byId = new Map(orderedTasks.map((task) => [task.id, task]));
32
- const terminal = (task) => task?.status === 'done' || task?.status === 'cancelled';
33
- if (terminal(byId.get(previous[from])) !== terminal(byId.get(previous[to])))
37
+ const sourceTask = byId.get(previous[from]);
38
+ const targetTask = byId.get(previous[to]);
39
+ if (!sourceTask || !targetTask)
40
+ return;
41
+ if (isPlanningTaskTerminal(sourceTask) !== isPlanningTaskTerminal(targetTask))
34
42
  return;
35
43
  const next = arrayMove(previous, from, to);
36
44
  setOrderedIds(next);
@@ -45,8 +53,11 @@ export function PlanningTaskOrderList(props) {
45
53
  setSaving(false);
46
54
  }
47
55
  }, [draggable, orderedIds, orderedTasks, props, saving]);
48
- if (!draggable) {
56
+ if (!hasReorderAction) {
49
57
  return _jsx("div", { className: `${styles.list} ${props.className || ''}`.trim(), children: props.tasks.map((task) => (_jsx(React.Fragment, { children: props.children(task) }, task.id))) });
50
58
  }
51
- return (_jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: (event) => void handleDragEnd(event), children: _jsx(SortableContext, { items: orderedIds, strategy: verticalListSortingStrategy, children: _jsx("div", { className: `${styles.list} ${props.className || ''}`.trim(), "aria-busy": saving || undefined, children: orderedTasks.map((task) => (_jsx(SortableTask, { task: task, disabled: saving, children: props.children(task) }, task.id))) }) }) }));
59
+ const disabledReason = props.mode === 'execution'
60
+ ? undefined
61
+ : 'Switch to Execution order to reorder tasks';
62
+ return (_jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: (event) => void handleDragEnd(event), children: _jsx(SortableContext, { items: orderedIds, strategy: verticalListSortingStrategy, children: _jsx("div", { className: `${styles.list} ${props.className || ''}`.trim(), "aria-busy": saving || undefined, children: orderedTasks.map((task) => (_jsx(SortableTask, { task: task, disabled: !draggable || saving, disabledReason: disabledReason || (saving ? 'Saving execution order' : undefined), children: props.children(task) }, task.id))) }) }) }));
52
63
  }
@@ -15,4 +15,5 @@ export declare const PLANNING_TASK_ARRANGE_OPTIONS: readonly ({
15
15
  })[];
16
16
  export declare function normalizePlanningTaskArrangeMode(value: unknown): PlanningTaskArrangeMode;
17
17
  export declare function mergeVisiblePlanningTaskOrder(allTasks: readonly PlanningTaskPreview[], reorderedVisibleTaskIds: readonly string[]): string[];
18
+ export declare function isPlanningTaskTerminal(task: PlanningTaskPreview): boolean;
18
19
  export declare function arrangePlanningTasks(tasks: readonly PlanningTaskPreview[], mode: PlanningTaskArrangeMode): PlanningTaskPreview[];
@@ -19,7 +19,7 @@ export function mergeVisiblePlanningTaskOrder(allTasks, reorderedVisibleTaskIds)
19
19
  let visibleIndex = 0;
20
20
  return allTasks.map((task) => (visible.has(task.id) ? reorderedVisibleTaskIds[visibleIndex++] : task.id));
21
21
  }
22
- function isTerminal(task) {
22
+ export function isPlanningTaskTerminal(task) {
23
23
  const status = getStatusDefinition(task.status).value;
24
24
  return status === 'done' || status === 'cancelled';
25
25
  }
@@ -50,7 +50,7 @@ function arrangeBlockedTasks(tasks, allTasksById) {
50
50
  for (const task of tasks) {
51
51
  for (const prerequisiteId of task.prerequisiteTaskIds || []) {
52
52
  const prerequisite = allTasksById.get(prerequisiteId);
53
- if (!prerequisite || isTerminal(prerequisite) || !taskIds.has(prerequisiteId))
53
+ if (!prerequisite || isPlanningTaskTerminal(prerequisite) || !taskIds.has(prerequisiteId))
54
54
  continue;
55
55
  dependentsByPrerequisite.set(prerequisiteId, [
56
56
  ...(dependentsByPrerequisite.get(prerequisiteId) || []),
@@ -85,15 +85,15 @@ function arrangeBlockedTasks(tasks, allTasksById) {
85
85
  export function arrangePlanningTasks(tasks, mode) {
86
86
  const taskById = new Map(tasks.map((task) => [task.id, task]));
87
87
  if (mode === 'ready') {
88
- const active = tasks.filter((task) => !isTerminal(task));
88
+ const active = tasks.filter((task) => !isPlanningTaskTerminal(task));
89
89
  const ready = sortByExecutionOrder(active.filter((task) => isReady(task, taskById)));
90
90
  const blocked = arrangeBlockedTasks(active.filter((task) => !isReady(task, taskById)), taskById);
91
- return [...ready, ...blocked, ...sortByExecutionOrder(tasks.filter(isTerminal))];
91
+ return [...ready, ...blocked, ...sortByExecutionOrder(tasks.filter(isPlanningTaskTerminal))];
92
92
  }
93
93
  return tasks.map((task, index) => ({ task, index })).sort((leftEntry, rightEntry) => {
94
94
  const left = leftEntry.task;
95
95
  const right = rightEntry.task;
96
- const terminalComparison = Number(isTerminal(left)) - Number(isTerminal(right));
96
+ const terminalComparison = Number(isPlanningTaskTerminal(left)) - Number(isPlanningTaskTerminal(right));
97
97
  if (terminalComparison)
98
98
  return terminalComparison;
99
99
  if (mode === 'priority') {
@@ -96,6 +96,7 @@ export function buildTaskforceAgentDefinitionFromDraft(input) {
96
96
  const artStyle = selectedAvatarArtStyle(draft.avatarArtStyleSelection, draft.avatarArtStyleCustom);
97
97
  const requestedCapabilities = draft.requestedCapabilities
98
98
  .filter((capability) => DEFAULT_AGENT_TOOL_CAPABILITY_SET.has(capability));
99
+ const selectedModel = getTaskforceAgentModel(input.modelKey);
99
100
  return {
100
101
  ...(baseDefinition || {}),
101
102
  schemaVersion: AGENT_DEFINITION_SCHEMA_VERSION,
@@ -104,8 +105,9 @@ export function buildTaskforceAgentDefinitionFromDraft(input) {
104
105
  model: {
105
106
  providerKey: input.providerKey,
106
107
  modelKey: input.modelKey,
107
- modelSource: getTaskforceAgentModel(input.modelKey)?.source || 'bedrock',
108
+ modelSource: selectedModel?.source || 'bedrock',
108
109
  tier: input.tier,
110
+ ...(selectedModel?.source === 'subscription_dev' ? { connectionScope: 'local_dev' } : {}),
109
111
  },
110
112
  behavior: {
111
113
  ...(personality ? { personality } : {}),
@@ -48,6 +48,11 @@ interface TaskCardProps {
48
48
  onWorkstreamChange?: (task: TaskItem, workstreamId: string | null) => boolean | void | Promise<boolean | void>;
49
49
  compressed?: boolean;
50
50
  showStatusLabel?: boolean;
51
+ showDescription?: boolean;
52
+ showAttachments?: boolean;
53
+ showChecklistProgress?: boolean;
54
+ showLatestActivity?: boolean;
55
+ tintActivityBackground?: boolean;
51
56
  }
52
57
  export declare const TaskCard: React.NamedExoticComponent<TaskCardProps>;
53
58
  export {};
@@ -23,7 +23,55 @@ import { TaxonomyDropdown } from '../ui/TaxonomyDropdown';
23
23
  import { AssigneeAvatar, AssigneeIdentity } from '../ui/AssigneeIdentity';
24
24
  import { TaskWorkstreamPicker } from './TaskWorkstreamPicker';
25
25
  import { DocumentTypeIcon } from '../ui/DocumentTypeIcon';
26
- const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [], types = [], priorities = [], categories = [], assigneeOptions = [], workstreams = [], taskWorkstream = null, taskInitiative = null, workspaceId = null, isOverlay = false, isArchived = false, readOnlyMode = null, isRecentlyChanged = false, selectable = false, selected = false, onSelectionChange, onClick, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onDelete, deleteActionTitle, taskReferences, onAssigneeChange, onWorkstreamChange, compressed = false, showStatusLabel = false }) => {
26
+ function resolveLatestActivityActor(item, assigneeOptions) {
27
+ if (!item)
28
+ return null;
29
+ const actorRef = String(item.type === 'comment' ? item.comment.author : item.event.actor).trim();
30
+ const actorProfile = item.type === 'comment' ? item.comment.actor : item.event.actorProfile;
31
+ if (!actorRef && !actorProfile)
32
+ return null;
33
+ const normalizedActorRef = actorRef.toLowerCase();
34
+ const option = assigneeOptions.find((candidate) => (String(candidate.value || '').trim().toLowerCase() === normalizedActorRef));
35
+ const actorType = item.type === 'comment'
36
+ ? actorProfile?.kind === 'ai'
37
+ ? 'ai'
38
+ : actorProfile?.kind === 'human'
39
+ ? 'human'
40
+ : option?.kind === 'agent'
41
+ ? 'ai'
42
+ : option?.kind === 'member'
43
+ ? 'human'
44
+ : null
45
+ : item.event.actorType;
46
+ const kind = actorType === 'system' || normalizedActorRef === 'system'
47
+ ? 'system'
48
+ : actorType === 'ai' || actorProfile?.kind === 'ai' || option?.kind === 'agent'
49
+ ? 'agent'
50
+ : 'member';
51
+ if (kind === 'system')
52
+ return { label: 'System' };
53
+ const profileLabel = String(actorProfile?.label || '').trim();
54
+ const optionLabel = String(option?.label || '').trim();
55
+ const isGenericActor = !normalizedActorRef
56
+ || normalizedActorRef === 'user'
57
+ || normalizedActorRef === 'human'
58
+ || normalizedActorRef === 'ai';
59
+ const isOpaqueActor = /^(?:ai-profile|user|member)-[0-9a-f-]{8,}$/i.test(actorRef)
60
+ || /^[0-9a-f]{16,}$/i.test(actorRef);
61
+ const readableActorRef = actorRef
62
+ .replace(/[-_]+/g, ' ')
63
+ .replace(/\s+/g, ' ')
64
+ .trim()
65
+ .replace(/\b\w/g, (character) => character.toUpperCase());
66
+ const label = profileLabel
67
+ || optionLabel
68
+ || (kind === 'agent' && (isGenericActor || isOpaqueActor) ? 'AI Agent' : '')
69
+ || (isGenericActor || isOpaqueActor ? 'Workspace member' : readableActorRef)
70
+ || (kind === 'agent' ? 'AI Agent' : 'Workspace member');
71
+ const color = resolveColor(actorProfile?.color || option?.color);
72
+ return { label, color };
73
+ }
74
+ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [], types = [], priorities = [], categories = [], assigneeOptions = [], workstreams = [], taskWorkstream = null, taskInitiative = null, workspaceId = null, isOverlay = false, isArchived = false, readOnlyMode = null, isRecentlyChanged = false, selectable = false, selected = false, onSelectionChange, onClick, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onDelete, deleteActionTitle, taskReferences, onAssigneeChange, onWorkstreamChange, compressed = false, showStatusLabel = false, showDescription = true, showAttachments = true, showChecklistProgress = true, showLatestActivity = true, tintActivityBackground = false }) => {
27
75
  const taskReference = getTaskReferenceDisplayState(task);
28
76
  const taskReferenceLabel = taskReference.label || (taskReference.isProvisional ? 'Pending' : '');
29
77
  const isProvisionalReference = taskReference.isProvisional;
@@ -71,7 +119,8 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
71
119
  kind,
72
120
  };
73
121
  }, [assigneeLabel, assigneeOptions, task.assignee]);
74
- const latestActivity = React.useMemo(() => getLatestTaskActivity(task), [task]);
122
+ const latestActivity = React.useMemo(() => task.latestActivity ?? getLatestTaskActivity(task), [task]);
123
+ const latestActivityActor = React.useMemo(() => resolveLatestActivityActor(latestActivity, assigneeOptions), [assigneeOptions, latestActivity]);
75
124
  const resolvedReadOnlyMode = readOnlyMode || (task.isDeleted ? 'deleted' : (isArchived ? 'archived' : null));
76
125
  const isReadOnly = resolvedReadOnlyMode !== null;
77
126
  const deletedExpiryLabel = React.useMemo(() => {
@@ -99,7 +148,7 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
99
148
  return undefined;
100
149
  }, [assigneeOptions, initiativeReferenceLabel, taskInitiative, taskWorkstream, workstreamReferenceLabel]);
101
150
  const latestActivityText = React.useMemo(() => summarizeTaskActivity(latestActivity, formatActivityFieldValue), [formatActivityFieldValue, latestActivity]);
102
- const commentCount = React.useMemo(() => getTaskActivityItems(task).filter((item) => item.type === 'comment').length, [task]);
151
+ const commentCount = React.useMemo(() => task.commentCount ?? getTaskActivityItems(task).filter((item) => item.type === 'comment').length, [task]);
103
152
  const cardClasses = [
104
153
  cardStyles.taskItem,
105
154
  task.status === 'on-hold' ? cardStyles.onHold : '',
@@ -196,10 +245,9 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
196
245
  const toTitleCase = (str) => str.replace(/\b\w/g, t => t.toUpperCase());
197
246
  const visibleTaxonomies = getVisibleTaxonomiesForTasks(taxonomies, [task]);
198
247
  const checklistItems = task.checklistItems || [];
199
- const checklistTotal = checklistItems.length;
200
- const checklistCompleted = checklistTotal > 0
201
- ? checklistItems.filter(item => item.isCompleted).length
202
- : 0;
248
+ const checklistTotal = task.checklistCount ?? checklistItems.length;
249
+ const checklistCompleted = task.completedChecklistCount
250
+ ?? (checklistTotal > 0 ? checklistItems.filter(item => item.isCompleted).length : 0);
203
251
  const handleReferenceCopy = (event, label) => {
204
252
  event.stopPropagation();
205
253
  onCopyId?.(event, label);
@@ -232,6 +280,11 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
232
280
  const cardCoverAttachment = (() => {
233
281
  if (compressed || isOverlay || !task.cardCoverAssetId)
234
282
  return null;
283
+ if (task.cardCoverAttachment
284
+ && typeof task.cardCoverAttachment !== 'string'
285
+ && task.cardCoverAttachment.assetId === task.cardCoverAssetId) {
286
+ return task.cardCoverAttachment;
287
+ }
235
288
  return (task.attachments || []).find((attachment) => (typeof attachment !== 'string'
236
289
  && String(attachment.assetId || '').trim() === task.cardCoverAssetId
237
290
  && attachment.linkRole === 'image'
@@ -288,7 +341,7 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
288
341
  }, children: _jsx("img", { src: cardCoverAttachment.path, alt: cardCoverAttachment.caption
289
342
  || cardCoverAttachment.displayName
290
343
  || cardCoverAttachment.originalFilename
291
- || task.title, loading: "lazy", decoding: "async", onError: () => setCardCoverLoadFailed(true) }) })), _jsxs("div", { className: cardStyles.taskContent, children: [/* Meta Badges moved to bottom */ isReadOnly && (_jsx("div", { className: `${cardStyles.taskMeta} ${cardStyles.taskMetaCompact}` })), _jsxs("div", { children: [_jsxs("div", { className: cardStyles.taskTitle, style: compressed ? { fontSize: '13px', lineHeight: '1.4' } : {}, children: [resolvedReadOnlyMode === 'archived' && '✓ ', resolvedReadOnlyMode === 'deleted' && 'Deleted: ', renderHighlighted(task.title, searchQuery)] }), deletedExpiryLabel && (_jsx("div", { className: cardStyles.deletedExpiry, title: "Trash items are permanently deleted 90 days after deletion.", children: deletedExpiryLabel })), task.description && !isReadOnly && !compressed && (_jsx("div", { className: cardStyles.taskDescription, children: _jsx(Markdown, { variant: "compact", taskReferences: taskReferences, children: task.description }) }))] }), cardAttachments.length > 0 && !isReadOnly && !compressed && (_jsxs("div", { className: cardStyles.attachmentThumbnails, children: [_jsx("div", { className: cardStyles.attachmentImageRow, children: cardAttachments.map((attachment, idx) => {
344
+ || task.title, loading: "lazy", decoding: "async", onError: () => setCardCoverLoadFailed(true) }) })), _jsxs("div", { className: cardStyles.taskContent, children: [/* Meta Badges moved to bottom */ isReadOnly && (_jsx("div", { className: `${cardStyles.taskMeta} ${cardStyles.taskMetaCompact}` })), _jsxs("div", { children: [_jsxs("div", { className: cardStyles.taskTitle, style: compressed ? { fontSize: '13px', lineHeight: '1.4' } : {}, children: [resolvedReadOnlyMode === 'archived' && '✓ ', resolvedReadOnlyMode === 'deleted' && 'Deleted: ', renderHighlighted(task.title, searchQuery)] }), deletedExpiryLabel && (_jsx("div", { className: cardStyles.deletedExpiry, title: "Trash items are permanently deleted 90 days after deletion.", children: deletedExpiryLabel })), showDescription && task.description && !isReadOnly && !compressed && (_jsx("div", { className: cardStyles.taskDescription, children: _jsx(Markdown, { variant: "compact", taskReferences: taskReferences, children: task.description }) }))] }), showAttachments && cardAttachments.length > 0 && !isReadOnly && !compressed && (_jsxs("div", { className: cardStyles.attachmentThumbnails, children: [_jsx("div", { className: cardStyles.attachmentImageRow, children: cardAttachments.map((attachment, idx) => {
292
345
  const path = typeof attachment === 'string' ? attachment : attachment.path;
293
346
  if (!isImageAttachment(attachment))
294
347
  return null;
@@ -319,7 +372,9 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
319
372
  return;
320
373
  window.open(path, '_blank');
321
374
  }, title: name, "aria-label": `${name} ${fileType}`, style: { '--context-doc-accent': fileAccentColor }, children: [_jsx(DocumentTypeIcon, { label: fileType, accentColor: fileAccentColor, compact: true }), _jsx("span", { className: cardStyles.contextDocMain, children: _jsx("span", { className: cardStyles.contextDocName, children: name }) })] }, idx));
322
- }) })] })), task.status === 'cancelled' && task.canceledReason && !compressed && (_jsxs("div", { className: cardStyles.taskCancellationReason, children: [_jsx("strong", { children: "Cancellation Reason:" }), " ", renderHighlighted(task.canceledReason, searchQuery)] })), (latestActivityText || commentCount > 0) && !isReadOnly && !compressed && (_jsxs("div", { className: cardStyles.taskLatestComment, "data-task-activity-summary": "true", children: [_jsxs("div", { className: cardStyles.taskLatestCommentHeader, children: [_jsx("strong", { children: latestActivityText ? 'Latest Activity:' : 'Activity:' }), commentCount > 0 && (_jsxs("span", { className: `${styles.metaBadge} ${cardStyles.taskCommentCount}`, title: `${commentCount} ${commentCount === 1 ? 'comment' : 'comments'}`, "aria-label": `${commentCount} ${commentCount === 1 ? 'comment' : 'comments'}`, children: [_jsx(MessageSquare, { size: 12, "aria-hidden": "true" }), _jsx("span", { children: commentCount })] }))] }), latestActivityText && (_jsx("div", { className: cardStyles.taskLatestCommentText, title: latestActivityText, children: renderHighlighted(latestActivityText, searchQuery) }))] })), !isReadOnly && visibleTaxonomies.length > 0 && !compressed && (_jsx("div", { className: `${cardStyles.taskSpecialists} ${cardStyles.taxonomiesList}`, children: visibleTaxonomies.map(tax => {
375
+ }) })] })), task.status === 'cancelled' && task.canceledReason && !compressed && (_jsxs("div", { className: cardStyles.taskCancellationReason, children: [_jsx("strong", { children: "Cancellation Reason:" }), " ", renderHighlighted(task.canceledReason, searchQuery)] })), showLatestActivity && (latestActivityText || commentCount > 0) && !isReadOnly && !compressed && (_jsxs("div", { className: `${cardStyles.taskLatestComment} ${tintActivityBackground && latestActivityActor?.color ? cardStyles.taskLatestCommentTinted : ''}`, "data-task-activity-summary": "true", style: latestActivityActor?.color
376
+ ? { '--task-activity-accent': latestActivityActor.color }
377
+ : undefined, children: [_jsxs("div", { className: cardStyles.taskLatestCommentHeader, children: [_jsxs("div", { className: cardStyles.taskLatestActivityHeading, children: [_jsx("strong", { children: latestActivityText ? 'Latest Activity:' : 'Activity:' }), latestActivityText && latestActivityActor ? (_jsx("span", { className: cardStyles.taskLatestActivityActor, title: latestActivityActor.label, children: latestActivityActor.label })) : null] }), commentCount > 0 && (_jsxs("span", { className: `${styles.metaBadge} ${cardStyles.taskCommentCount}`, title: `${commentCount} ${commentCount === 1 ? 'comment' : 'comments'}`, "aria-label": `${commentCount} ${commentCount === 1 ? 'comment' : 'comments'}`, children: [_jsx(MessageSquare, { size: 12, "aria-hidden": "true" }), _jsx("span", { children: commentCount })] }))] }), latestActivityText && (_jsx("div", { className: cardStyles.taskLatestCommentText, title: latestActivityText, children: renderHighlighted(latestActivityText, searchQuery) }))] })), !isReadOnly && visibleTaxonomies.length > 0 && !compressed && (_jsx("div", { className: `${cardStyles.taskSpecialists} ${cardStyles.taxonomiesList}`, children: visibleTaxonomies.map(tax => {
323
378
  const val = task.taxonomies?.[tax.id];
324
379
  if (!val)
325
380
  return null;
@@ -364,7 +419,7 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
364
419
  const iconName = priorityConfig.icon || 'AlertCircle';
365
420
  const IconComp = Icons[iconName] || Icons.AlertCircle;
366
421
  return _jsx(IconComp, { size: 14 });
367
- })() })), checklistTotal > 0 && (_jsxs("span", { className: styles.metaBadge, title: `${checklistCompleted}/${checklistTotal} checklist items complete`, style: {
422
+ })() })), showChecklistProgress && checklistTotal > 0 && (_jsxs("span", { className: styles.metaBadge, title: `${checklistCompleted}/${checklistTotal} checklist items complete`, style: {
368
423
  color: 'var(--text-muted)',
369
424
  backgroundColor: 'var(--bg-tertiary)',
370
425
  borderColor: 'var(--border-primary)',
@@ -3,33 +3,42 @@ import { useEffect, useState } from 'react';
3
3
  import { Image, Loader2, MessageSquareText } from 'lucide-react';
4
4
  import { fetchTaskforceApi } from '../../utils/taskforceApiClient';
5
5
  import { dispatchOpenAnnotatedAttachment } from '../../utils/annotatedAttachments';
6
+ import { formatDate } from '../../utils/formatting';
6
7
  import styles from './TaskImageReviews.module.css';
7
8
  function reviewTitle(review) {
8
9
  return String(review.title || '').trim() || review.image?.displayName || 'Image review';
9
10
  }
10
11
  export function TaskImageReviews({ taskId, taskReferenceLabel, workspaceId, apiBaseUrl }) {
11
12
  const [reviews, setReviews] = useState(null);
13
+ const [loadError, setLoadError] = useState(false);
12
14
  useEffect(() => {
13
15
  let active = true;
14
16
  setReviews(null);
17
+ setLoadError(false);
15
18
  const params = new URLSearchParams({ workspaceId, taskId });
16
19
  void fetchTaskforceApi(`/api/taskforce/annotated-attachments/sessions?${params.toString()}`, {
17
20
  credentials: 'include',
18
21
  }, apiBaseUrl)
19
- .then(async (response) => response.ok ? response.json() : { sessions: [] })
22
+ .then(async (response) => {
23
+ if (!response.ok)
24
+ throw new Error('Failed to load image reviews.');
25
+ return response.json();
26
+ })
20
27
  .then((data) => {
21
28
  if (active)
22
29
  setReviews(Array.isArray(data?.sessions) ? data.sessions : []);
23
30
  })
24
31
  .catch(() => {
25
- if (active)
32
+ if (active) {
26
33
  setReviews([]);
34
+ setLoadError(true);
35
+ }
27
36
  });
28
37
  return () => { active = false; };
29
38
  }, [apiBaseUrl, taskId, workspaceId]);
30
- if (reviews !== null && reviews.length === 0)
39
+ if (reviews !== null && reviews.length === 0 && !loadError)
31
40
  return null;
32
- return (_jsx("section", { className: styles.group, "aria-label": "Image reviews", children: reviews === null ? (_jsxs("div", { className: styles.loading, "aria-live": "polite", children: [_jsx(Loader2, { size: 14 }), " Loading image reviews"] })) : (_jsx("div", { className: styles.list, children: reviews.map((review) => {
41
+ return (_jsx("section", { className: styles.group, "aria-label": "Image reviews", children: reviews === null ? (_jsxs("div", { className: styles.loading, "aria-live": "polite", children: [_jsx(Loader2, { size: 14 }), " Loading image reviews"] })) : loadError ? (_jsx("div", { className: styles.loading, role: "status", children: "Unable to load image reviews." })) : (_jsx("div", { className: styles.list, children: reviews.map((review) => {
33
42
  const image = review.image;
34
43
  const markerCount = review.annotations.length;
35
44
  const canOpen = Boolean(image?.assetId && image.apiUrl);
@@ -42,6 +51,6 @@ export function TaskImageReviews({ taskId, taskReferenceLabel, workspaceId, apiB
42
51
  displayName: image.displayName,
43
52
  referenceLabel: image.referenceLabel || undefined,
44
53
  }, { taskId, taskReferenceLabel, sessionId: review.id });
45
- }, children: [_jsx("span", { className: styles.icon, children: _jsx(Image, { size: 14 }) }), _jsxs("span", { className: styles.copy, children: [_jsx("span", { className: styles.kindLabel, children: "Image review session" }), _jsx("span", { className: `${styles.title} tf-heading-card`, children: reviewTitle(review) }), _jsxs("span", { className: "tf-text-meta", children: [markerCount, " ", markerCount === 1 ? 'marker' : 'markers'] })] }), _jsx(MessageSquareText, { className: styles.openIcon, size: 14, "aria-hidden": "true" })] }, review.id));
54
+ }, children: [_jsx("span", { className: styles.icon, children: _jsx(Image, { size: 14 }) }), _jsxs("span", { className: styles.copy, children: [_jsx("span", { className: styles.kindLabel, children: "Image review session" }), _jsx("span", { className: `${styles.title} tf-heading-card`, children: reviewTitle(review) }), _jsxs("span", { className: "tf-text-meta", children: [[image?.referenceLabel, image?.displayName].filter(Boolean).join(' · ') || 'Image', review.updatedAt ? ` · Updated ${formatDate(review.updatedAt, { month: 'short', day: 'numeric', year: 'numeric' })}` : ''] }), _jsxs("span", { className: "tf-text-meta", children: [markerCount, " ", markerCount === 1 ? 'marker' : 'markers'] })] }), _jsx(MessageSquareText, { className: styles.openIcon, size: 14, "aria-hidden": "true" })] }, review.id));
46
55
  }) })) }));
47
56
  }
@@ -64,6 +64,11 @@ interface TaskKanbanProps {
64
64
  onAssignTaskToWorkstream?: (taskId: string, workstreamId: string | null) => boolean | void | Promise<boolean | void>;
65
65
  onAssignWorkstreamToInitiative?: (workstreamId: string, initiativeId: string | null) => boolean | void | Promise<boolean | void>;
66
66
  showTaskCardStatusLabel?: boolean;
67
+ showTaskCardDescription?: boolean;
68
+ showTaskCardAttachments?: boolean;
69
+ showTaskCardChecklistProgress?: boolean;
70
+ showTaskCardLatestActivity?: boolean;
71
+ tintTaskCardActivityBackground?: boolean;
67
72
  workstreams?: WorkstreamItem[];
68
73
  initiatives?: InitiativeItem[];
69
74
  workspaceId?: string | null;
@@ -71,5 +76,5 @@ interface TaskKanbanProps {
71
76
  selectedDeletedRecordIds?: Set<string>;
72
77
  onDeletedSelectionChange?: (deletedRecordId: string, selected: boolean) => void;
73
78
  }
74
- export declare function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery, copiedId, taxonomies, types, priorities, assigneeOptions, onUpdateTask, onTaskClick, taskReferences, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onDelete, allTasks, onAddTaskToColumn, emptyColumnMode, filterCategories, filterTypes, filterPriorities, filterStatus, filterAssignees, filtersReady, scheduleFilteredTaskIds, categories, compressed, readOnlyMode, recentlyChangedTaskIds, scheduleDates, onScheduleDaySelected, persistedScrollLeft, onScrollLeftChange, sortBy, sortOrder, planningDropTargets, onAssignTaskToWorkstream, onAssignWorkstreamToInitiative, showTaskCardStatusLabel, workstreams, initiatives, workspaceId, deletedTaskRecordByTaskId, selectedDeletedRecordIds, onDeletedSelectionChange, }: TaskKanbanProps): import("react/jsx-runtime").JSX.Element;
79
+ export declare function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery, copiedId, taxonomies, types, priorities, assigneeOptions, onUpdateTask, onTaskClick, taskReferences, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onDelete, allTasks, onAddTaskToColumn, emptyColumnMode, filterCategories, filterTypes, filterPriorities, filterStatus, filterAssignees, filtersReady, scheduleFilteredTaskIds, categories, compressed, readOnlyMode, recentlyChangedTaskIds, scheduleDates, onScheduleDaySelected, persistedScrollLeft, onScrollLeftChange, sortBy, sortOrder, planningDropTargets, onAssignTaskToWorkstream, onAssignWorkstreamToInitiative, showTaskCardStatusLabel, showTaskCardDescription, showTaskCardAttachments, showTaskCardChecklistProgress, showTaskCardLatestActivity, tintTaskCardActivityBackground, workstreams, initiatives, workspaceId, deletedTaskRecordByTaskId, selectedDeletedRecordIds, onDeletedSelectionChange, }: TaskKanbanProps): import("react/jsx-runtime").JSX.Element;
75
80
  export {};
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useMemo, useState, useRef, useEffect, useCallback } from 'react';
2
+ import React, { startTransition, useMemo, useState, useRef, useEffect, useCallback } from 'react';
3
3
  import { DndContext, DragOverlay, closestCorners, KeyboardSensor, PointerSensor, useSensor, useSensors, pointerWithin, defaultDropAnimationSideEffects, useDroppable } from '@dnd-kit/core';
4
4
  import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
5
5
  import { CSS } from '@dnd-kit/utilities';
@@ -14,7 +14,107 @@ import { AssigneeAvatar } from '../ui/AssigneeIdentity';
14
14
  const EXPIRED_CARD_ID_PREFIX = 'expired::';
15
15
  const EMPTY_DELETED_RECORD_MAP = new Map();
16
16
  const EMPTY_SELECTED_DELETED_IDS = new Set();
17
- export function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery, copiedId, taxonomies, types, priorities, assigneeOptions = [], onUpdateTask, onTaskClick, taskReferences, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onDelete, allTasks = [], onAddTaskToColumn, emptyColumnMode = 'show', filterCategories = [], filterTypes = [], filterPriorities = [], filterStatus = [], filterAssignees = [], filtersReady = true, scheduleFilteredTaskIds, categories = [], compressed = false, readOnlyMode = null, recentlyChangedTaskIds = [], scheduleDates, onScheduleDaySelected, persistedScrollLeft, onScrollLeftChange, sortBy = 'created', sortOrder = 'desc', planningDropTargets = null, onAssignTaskToWorkstream, onAssignWorkstreamToInitiative, showTaskCardStatusLabel = false, workstreams = [], initiatives = [], workspaceId = null, deletedTaskRecordByTaskId = EMPTY_DELETED_RECORD_MAP, selectedDeletedRecordIds = EMPTY_SELECTED_DELETED_IDS, onDeletedSelectionChange, }) {
17
+ const KANBAN_VIRTUALIZATION_TASK_THRESHOLD = 250;
18
+ const KANBAN_VIRTUALIZATION_COLUMN_THRESHOLD = 24;
19
+ const KANBAN_VIRTUAL_CARD_ESTIMATE_PX = 180;
20
+ const KANBAN_VIRTUAL_CARD_GAP_PX = 8;
21
+ const KANBAN_VIRTUAL_OVERSCAN_PX = 600;
22
+ const KANBAN_VIRTUAL_FALLBACK_VIEWPORT_PX = 720;
23
+ function useVirtualKanbanRows({ tasks, enabled, scrollRef, initialScrollTop, onScrollTopChange, }) {
24
+ const measuredHeightsRef = useRef(new Map());
25
+ const [measurementVersion, setMeasurementVersion] = useState(0);
26
+ const [viewport, setViewport] = useState(() => ({
27
+ scrollTop: initialScrollTop,
28
+ height: KANBAN_VIRTUAL_FALLBACK_VIEWPORT_PX,
29
+ }));
30
+ const measureRow = useCallback((taskId, height) => {
31
+ if (!Number.isFinite(height) || height <= 0)
32
+ return;
33
+ const normalizedHeight = Math.ceil(height);
34
+ const previousHeight = measuredHeightsRef.current.get(taskId) ?? KANBAN_VIRTUAL_CARD_ESTIMATE_PX;
35
+ if (previousHeight === normalizedHeight)
36
+ return;
37
+ const taskIndex = tasks.findIndex((task) => task.id === taskId);
38
+ let rowTop = 0;
39
+ for (let index = 0; index < taskIndex; index += 1) {
40
+ rowTop += measuredHeightsRef.current.get(tasks[index].id) ?? KANBAN_VIRTUAL_CARD_ESTIMATE_PX;
41
+ rowTop += KANBAN_VIRTUAL_CARD_GAP_PX;
42
+ }
43
+ measuredHeightsRef.current.set(taskId, normalizedHeight);
44
+ const element = scrollRef.current;
45
+ const heightDelta = normalizedHeight - previousHeight;
46
+ const previousRowBottom = rowTop + previousHeight;
47
+ if (element && taskIndex >= 0 && previousRowBottom <= element.scrollTop && heightDelta !== 0) {
48
+ const scrollTop = Math.max(0, element.scrollTop + heightDelta);
49
+ element.scrollTop = scrollTop;
50
+ setViewport((current) => ({ ...current, scrollTop }));
51
+ onScrollTopChange(scrollTop);
52
+ }
53
+ setMeasurementVersion((version) => version + 1);
54
+ }, [onScrollTopChange, scrollRef, tasks]);
55
+ const layout = useMemo(() => {
56
+ let top = 0;
57
+ const rows = tasks.map((task, index) => {
58
+ const row = { task, index, top };
59
+ const height = measuredHeightsRef.current.get(task.id) ?? KANBAN_VIRTUAL_CARD_ESTIMATE_PX;
60
+ top += height;
61
+ if (index < tasks.length - 1)
62
+ top += KANBAN_VIRTUAL_CARD_GAP_PX;
63
+ return row;
64
+ });
65
+ return { rows, totalHeight: top };
66
+ }, [measurementVersion, tasks]);
67
+ useEffect(() => {
68
+ const element = scrollRef.current;
69
+ if (!element)
70
+ return;
71
+ if (initialScrollTop > 0 && element.scrollTop !== initialScrollTop) {
72
+ element.scrollTop = initialScrollTop;
73
+ }
74
+ const updateViewport = (deferred = false) => {
75
+ const scrollTop = Math.max(0, element.scrollTop);
76
+ const height = element.clientHeight || KANBAN_VIRTUAL_FALLBACK_VIEWPORT_PX;
77
+ const commitViewport = () => setViewport((current) => (current.scrollTop === scrollTop && current.height === height
78
+ ? current
79
+ : { scrollTop, height }));
80
+ if (deferred)
81
+ startTransition(commitViewport);
82
+ else
83
+ commitViewport();
84
+ onScrollTopChange(scrollTop);
85
+ };
86
+ const handleScroll = () => updateViewport(true);
87
+ const handleLayoutChange = () => updateViewport(false);
88
+ handleLayoutChange();
89
+ element.addEventListener('scroll', handleScroll, { passive: true });
90
+ const resizeObserver = typeof ResizeObserver === 'undefined'
91
+ ? null
92
+ : new ResizeObserver(handleLayoutChange);
93
+ resizeObserver?.observe(element);
94
+ window.addEventListener('resize', handleLayoutChange);
95
+ return () => {
96
+ element.removeEventListener('scroll', handleScroll);
97
+ resizeObserver?.disconnect();
98
+ window.removeEventListener('resize', handleLayoutChange);
99
+ };
100
+ }, [initialScrollTop, onScrollTopChange, scrollRef]);
101
+ const visibleRows = useMemo(() => {
102
+ if (!enabled)
103
+ return layout.rows;
104
+ const minimumTop = Math.max(0, viewport.scrollTop - KANBAN_VIRTUAL_OVERSCAN_PX);
105
+ const maximumBottom = viewport.scrollTop + viewport.height + KANBAN_VIRTUAL_OVERSCAN_PX;
106
+ return layout.rows.filter((row) => {
107
+ const height = measuredHeightsRef.current.get(row.task.id) ?? KANBAN_VIRTUAL_CARD_ESTIMATE_PX;
108
+ return row.top + height >= minimumTop && row.top <= maximumBottom;
109
+ });
110
+ }, [enabled, layout.rows, measurementVersion, viewport.height, viewport.scrollTop]);
111
+ return {
112
+ measureRow,
113
+ rows: visibleRows,
114
+ totalHeight: layout.totalHeight,
115
+ };
116
+ }
117
+ export function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery, copiedId, taxonomies, types, priorities, assigneeOptions = [], onUpdateTask, onTaskClick, taskReferences, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onDelete, allTasks = [], onAddTaskToColumn, emptyColumnMode = 'show', filterCategories = [], filterTypes = [], filterPriorities = [], filterStatus = [], filterAssignees = [], filtersReady = true, scheduleFilteredTaskIds, categories = [], compressed = false, readOnlyMode = null, recentlyChangedTaskIds = [], scheduleDates, onScheduleDaySelected, persistedScrollLeft, onScrollLeftChange, sortBy = 'created', sortOrder = 'desc', planningDropTargets = null, onAssignTaskToWorkstream, onAssignWorkstreamToInitiative, showTaskCardStatusLabel = false, showTaskCardDescription = true, showTaskCardAttachments = true, showTaskCardChecklistProgress = true, showTaskCardLatestActivity = true, tintTaskCardActivityBackground = false, workstreams = [], initiatives = [], workspaceId = null, deletedTaskRecordByTaskId = EMPTY_DELETED_RECORD_MAP, selectedDeletedRecordIds = EMPTY_SELECTED_DELETED_IDS, onDeletedSelectionChange, }) {
18
118
  const [activeId, setActiveId] = useState(null);
19
119
  const [isHeaderPanning, setIsHeaderPanning] = useState(false);
20
120
  const [headerPanEnabled, setHeaderPanEnabled] = useState(false);
@@ -33,6 +133,10 @@ export function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery
33
133
  const isTaskCardId = useCallback((id) => taskIdSet.has(getTaskIdFromCardId(id)), [taskIdSet, getTaskIdFromCardId]);
34
134
  const scheduleFilteredTaskIdSet = useMemo(() => new Set(scheduleFilteredTaskIds || []), [scheduleFilteredTaskIds]);
35
135
  const lastPlanningOverRef = useRef(null);
136
+ const columnScrollTopRef = useRef(new Map());
137
+ const rememberColumnScrollTop = useCallback((columnId, scrollTop) => {
138
+ columnScrollTopRef.current.set(columnId, scrollTop);
139
+ }, []);
36
140
  const resolveActiveDragMeta = useCallback((activeLike) => {
37
141
  const current = activeLike?.data?.current || {};
38
142
  const activeId = String(activeLike?.id || '');
@@ -317,6 +421,9 @@ export function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery
317
421
  }, [tasks, effectiveKanbanColumns, groupBy, columnLookup, scheduleFilteredTaskIdSet, todayDateOnly, sortBy, sortOrder, taxonomies]);
318
422
  const activeTaskId = activeId ? getTaskIdFromCardId(activeId) : null;
319
423
  const activeTaskOverlay = activeTaskId ? tasks.find(t => t.id === activeTaskId) : null;
424
+ const virtualizeLargeBoard = tasks.length > KANBAN_VIRTUALIZATION_TASK_THRESHOLD
425
+ && groupBy !== 'schedule'
426
+ && !readOnlyMode;
320
427
  // Scroll Syncing Refs and Logic
321
428
  const containerRef = useRef(null);
322
429
  const topScrollRef = useRef(null);
@@ -762,6 +869,11 @@ export function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery
762
869
  resolveTaskHierarchy,
763
870
  groupBy,
764
871
  showStatusLabel: showTaskCardStatusLabel,
872
+ showDescription: showTaskCardDescription,
873
+ showAttachments: showTaskCardAttachments,
874
+ showChecklistProgress: showTaskCardChecklistProgress,
875
+ showLatestActivity: showTaskCardLatestActivity,
876
+ tintActivityBackground: tintTaskCardActivityBackground,
765
877
  deletedTaskRecordByTaskId,
766
878
  selectedDeletedRecordIds,
767
879
  onDeletedSelectionChange,
@@ -788,7 +900,12 @@ export function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery
788
900
  resolveTaskHierarchy,
789
901
  searchQuery,
790
902
  selectedDeletedRecordIds,
903
+ showTaskCardAttachments,
904
+ showTaskCardChecklistProgress,
905
+ showTaskCardDescription,
791
906
  showTaskCardStatusLabel,
907
+ showTaskCardLatestActivity,
908
+ tintTaskCardActivityBackground,
792
909
  taskReferences,
793
910
  taxonomies,
794
911
  types,
@@ -815,13 +932,13 @@ export function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery
815
932
  return null;
816
933
  return (_jsx(KanbanColumn, { id: String(column.value), title: column.label, color: column.color, icon: column.icon, assigneeOption: groupBy === 'assignee'
817
934
  ? assigneeOptions.find((option) => String(option.value) === String(column.value))
818
- : undefined, tasks: columnTasks, activeId: activeId, activeTaskId: activeTaskId, totalCount: columnTotals[String(column.value)], onAddTask: () => onAddTaskToColumn?.(groupBy, column.value), onTaskClick: onTaskClick, commonCardProps: commonCardProps, changedTaskIdSet: changedTaskIdSet, collapseEmpty: emptyColumnMode === 'collapse', compressed: compressed, onHeaderPointerDown: handleHeaderPointerDown, isHeaderPanning: isHeaderPanning, headerPanEnabled: headerPanEnabled, stickyLeft: stickyLeft, stickyZIndex: stickyZIndex, isPast: !!column.isPast, isSelected: !!column.isSelected, isWeekend: !!column.isWeekend, disableSorting: false, scheduleDate: groupBy === 'schedule' ? scheduleContext.dates[String(column.value)] : undefined, onScheduleDaySelected: onScheduleDaySelected }, String(column.value)));
935
+ : undefined, tasks: columnTasks, activeId: activeId, activeTaskId: activeTaskId, totalCount: columnTotals[String(column.value)], onAddTask: () => onAddTaskToColumn?.(groupBy, column.value), onTaskClick: onTaskClick, commonCardProps: commonCardProps, changedTaskIdSet: changedTaskIdSet, collapseEmpty: emptyColumnMode === 'collapse', compressed: compressed, onHeaderPointerDown: handleHeaderPointerDown, isHeaderPanning: isHeaderPanning, headerPanEnabled: headerPanEnabled, stickyLeft: stickyLeft, stickyZIndex: stickyZIndex, isPast: !!column.isPast, isSelected: !!column.isSelected, isWeekend: !!column.isWeekend, disableSorting: false, scheduleDate: groupBy === 'schedule' ? scheduleContext.dates[String(column.value)] : undefined, onScheduleDaySelected: onScheduleDaySelected, virtualize: virtualizeLargeBoard, initialScrollTop: columnScrollTopRef.current.get(String(column.value)) || 0, onScrollTopChange: rememberColumnScrollTop }, String(column.value)));
819
936
  }) })] }), createPortal(_jsx(DragOverlay, { dropAnimation: dropAnimation, children: activeTaskOverlay ? ((() => {
820
937
  const { taskWorkstream, taskInitiative } = resolveTaskHierarchy(activeTaskOverlay);
821
938
  return (_jsx(TaskCard, { task: activeTaskOverlay, taskWorkstream: taskWorkstream, taskInitiative: taskInitiative, isOverlay: true, ...commonCardProps, isArchived: activeTaskOverlay.isArchived, categories: categories, compressed: compressed, isRecentlyChanged: changedTaskIdSet.has(activeTaskOverlay.id) }));
822
939
  })()) : null }), document.body)] }));
823
940
  }
824
- const KanbanColumn = React.memo(function KanbanColumn({ id, title, color, icon, assigneeOption, tasks, activeId = null, activeTaskId = null, totalCount, onAddTask, onTaskClick, commonCardProps, changedTaskIdSet, collapseEmpty, compressed, onHeaderPointerDown, isHeaderPanning = false, headerPanEnabled = false, stickyLeft, stickyZIndex, isPast = false, isSelected = false, isWeekend = false, disableSorting = false, scheduleDate, onScheduleDaySelected }) {
941
+ const KanbanColumn = React.memo(function KanbanColumn({ id, title, color, icon, assigneeOption, tasks, activeId = null, activeTaskId = null, totalCount, onAddTask, onTaskClick, commonCardProps, changedTaskIdSet, collapseEmpty, compressed, onHeaderPointerDown, isHeaderPanning = false, headerPanEnabled = false, stickyLeft, stickyZIndex, isPast = false, isSelected = false, isWeekend = false, disableSorting = false, scheduleDate, onScheduleDaySelected, virtualize = false, initialScrollTop = 0, onScrollTopChange, }) {
825
942
  const { setNodeRef } = useDroppable({
826
943
  id: id,
827
944
  data: {
@@ -832,6 +949,19 @@ const KanbanColumn = React.memo(function KanbanColumn({ id, title, color, icon,
832
949
  const visibleTasks = activeTaskId ? tasks.filter((t) => t.id !== activeTaskId) : tasks;
833
950
  const isSticky = typeof stickyLeft === 'number';
834
951
  const scrollRef = useRef(null);
952
+ const virtualizationEnabled = virtualize
953
+ && !disableSorting
954
+ && visibleTasks.length > KANBAN_VIRTUALIZATION_COLUMN_THRESHOLD;
955
+ const handleScrollTopChange = useCallback((scrollTop) => {
956
+ onScrollTopChange?.(id, scrollTop);
957
+ }, [id, onScrollTopChange]);
958
+ const virtualRows = useVirtualKanbanRows({
959
+ tasks: visibleTasks,
960
+ enabled: virtualizationEnabled,
961
+ scrollRef,
962
+ initialScrollTop,
963
+ onScrollTopChange: handleScrollTopChange,
964
+ });
835
965
  const toSortableId = (taskId) => (id === 'expired' ? `${EXPIRED_CARD_ID_PREFIX}${taskId}` : taskId);
836
966
  const handleColumnClick = (event) => {
837
967
  if (!scheduleDate || !onScheduleDaySelected)
@@ -847,13 +977,29 @@ const KanbanColumn = React.memo(function KanbanColumn({ id, title, color, icon,
847
977
  const IconComp = icon && IconMap[icon] ? IconMap[icon] : Icons.Folder;
848
978
  const resolvedColor = resolveColor(color) || 'var(--color-purple)';
849
979
  return _jsx(IconComp, { size: 16, style: { color: resolvedColor } });
850
- })(), !isCollapsed && _jsx("span", { style: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, children: title })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }, children: [(!isCollapsed || totalCount > 0) && (_jsx("span", { className: styles.kanbanCount, children: isCollapsed ? totalCount : `${tasks.length} / ${totalCount}` })), !commonCardProps.readOnlyMode && (_jsx("button", { className: "tf-control-icon tf-control-icon-compact", onClick: (e) => { e.stopPropagation(); onAddTask(); }, title: `Add task to ${title}`, "aria-label": `Add task to ${title}`, "data-no-header-pan": "true", children: _jsx(Icons.Plus, { size: 14 }) }))] })] }), _jsx("div", { className: styles.kanbanDroppable, children: _jsx("div", { className: `${styles.kanbanDroppableScroll} ${styles.appScrollbar} tf-scrollbar`, ref: scrollRef, children: disableSorting ? (visibleTasks.map((task) => (_jsx("div", { className: styles.kanbanCardWrapper, children: (() => {
980
+ })(), !isCollapsed && _jsx("span", { style: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, children: title })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }, children: [(!isCollapsed || totalCount > 0) && (_jsx("span", { className: styles.kanbanCount, children: isCollapsed ? totalCount : `${tasks.length} / ${totalCount}` })), !commonCardProps.readOnlyMode && (_jsx("button", { className: "tf-control-icon tf-control-icon-compact", onClick: (e) => { e.stopPropagation(); onAddTask(); }, title: `Add task to ${title}`, "aria-label": `Add task to ${title}`, "data-no-header-pan": "true", children: _jsx(Icons.Plus, { size: 14 }) }))] })] }), _jsx("div", { className: styles.kanbanDroppable, children: _jsx("div", { className: `${styles.kanbanDroppableScroll} ${virtualizationEnabled ? styles.kanbanDroppableScrollVirtualized : ''} ${styles.appScrollbar} tf-scrollbar`, ref: scrollRef, "data-kanban-column-scroll": id, children: disableSorting ? (visibleTasks.map((task) => (_jsx("div", { className: styles.kanbanCardWrapper, children: (() => {
851
981
  const { taskWorkstream, taskInitiative } = commonCardProps.resolveTaskHierarchy?.(task) || {};
852
982
  const deletedRecord = commonCardProps.deletedTaskRecordByTaskId?.get(task.id);
853
983
  return (_jsx(TaskCard, { task: task, taskWorkstream: taskWorkstream, taskInitiative: taskInitiative, onClick: onTaskClick, ...commonCardProps, isArchived: task.isArchived, readOnlyMode: commonCardProps.readOnlyMode || null, compressed: compressed, isRecentlyChanged: false, selectable: Boolean(deletedRecord && commonCardProps.onDeletedSelectionChange), selected: Boolean(deletedRecord && commonCardProps.selectedDeletedRecordIds?.has(deletedRecord.id)), onSelectionChange: deletedRecord
854
984
  ? (selected) => commonCardProps.onDeletedSelectionChange?.(deletedRecord.id, selected)
855
985
  : undefined }));
856
- })() }, task.id)))) : (_jsx(SortableContext, { id: id, items: visibleTasks.map((t) => toSortableId(t.id)), strategy: verticalListSortingStrategy, children: visibleTasks.map((task) => (_jsx(SortableTaskCard, { sortableId: toSortableId(task.id), task: task, onClick: onTaskClick, commonCardProps: commonCardProps, compressed: compressed, isRecentlyChanged: changedTaskIdSet.has(task.id) }, toSortableId(task.id)))) })) }) })] }));
986
+ })() }, task.id)))) : (_jsx(SortableContext, { id: id, items: visibleTasks.map((t) => toSortableId(t.id)), strategy: verticalListSortingStrategy, children: virtualizationEnabled ? (_jsx("div", { className: styles.kanbanVirtualCanvas, style: { height: `${virtualRows.totalHeight}px` }, children: virtualRows.rows.map((row) => (_jsx(VirtualizedSortableTaskCard, { top: row.top, sortableId: toSortableId(row.task.id), task: row.task, onClick: onTaskClick, commonCardProps: commonCardProps, compressed: compressed, isRecentlyChanged: changedTaskIdSet.has(row.task.id), onMeasure: virtualRows.measureRow }, toSortableId(row.task.id)))) })) : visibleTasks.map((task) => (_jsx(SortableTaskCard, { sortableId: toSortableId(task.id), task: task, onClick: onTaskClick, commonCardProps: commonCardProps, compressed: compressed, isRecentlyChanged: changedTaskIdSet.has(task.id) }, toSortableId(task.id)))) })) }) })] }));
987
+ });
988
+ const VirtualizedSortableTaskCard = React.memo(function VirtualizedSortableTaskCard({ top, onMeasure, ...cardProps }) {
989
+ const rowRef = useRef(null);
990
+ useEffect(() => {
991
+ const element = rowRef.current;
992
+ if (!element)
993
+ return;
994
+ const reportHeight = () => onMeasure(cardProps.task.id, element.getBoundingClientRect().height);
995
+ reportHeight();
996
+ if (typeof ResizeObserver === 'undefined')
997
+ return;
998
+ const observer = new ResizeObserver(reportHeight);
999
+ observer.observe(element);
1000
+ return () => observer.disconnect();
1001
+ }, [cardProps.task.id, onMeasure]);
1002
+ return (_jsx("div", { ref: rowRef, className: styles.kanbanVirtualRow, style: { top: `${top}px` }, "data-kanban-virtual-index": cardProps.task.id, children: _jsx(SortableTaskCard, { ...cardProps }) }));
857
1003
  });
858
1004
  const SortableTaskCard = React.memo(function SortableTaskCard({ sortableId, task, onClick, commonCardProps, compressed, isRecentlyChanged = false }) {
859
1005
  const dragDisabled = Boolean(commonCardProps?.readOnlyMode);
@@ -45,6 +45,11 @@ interface TaskListProps {
45
45
  copiedId: string | null;
46
46
  recentlyChangedTaskIds?: string[];
47
47
  showTaskCardStatusLabel?: boolean;
48
+ showTaskCardDescription?: boolean;
49
+ showTaskCardAttachments?: boolean;
50
+ showTaskCardChecklistProgress?: boolean;
51
+ showTaskCardLatestActivity?: boolean;
52
+ tintTaskCardActivityBackground?: boolean;
48
53
  deletedTaskRecordByTaskId?: Map<string, DeletedTaskRecord>;
49
54
  selectedDeletedRecordIds?: Set<string>;
50
55
  onDeletedSelectionChange?: (deletedRecordId: string, selected: boolean) => void;