@taskforcehq/taskforce 0.3.305 → 0.3.306
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.
- package/dist/Taskforce.module.css +295 -89
- package/dist/TaskforceCore.js +215 -43
- package/dist/TaskforceCore.test.js +433 -28
- package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/assets/fonts/Noir_medium.woff2 +0 -0
- package/dist/assets/fonts/Noir_regular.woff2 +0 -0
- package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/compat/workspaceSyncCompat.js +46 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +549 -0
- package/dist/components/features/AgentsModule.d.ts +10 -1
- package/dist/components/features/AgentsModule.js +260 -15
- package/dist/components/features/AgentsModule.test.js +255 -5
- package/dist/components/features/DocumentViewer.d.ts +5 -1
- package/dist/components/features/DocumentViewer.js +60 -3
- package/dist/components/features/DocumentWorkspace.js +1 -1
- package/dist/components/features/DocumentWorkspace.test.js +33 -0
- package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
- package/dist/components/task/TaskCard.js +19 -28
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskForm.js +85 -90
- package/dist/components/task/TaskForm.test.js +110 -8
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +94 -13
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +70 -14
- package/dist/components/views/PlanComparisonPage.test.js +238 -0
- package/dist/components/views/PlansPage.js +127 -36
- package/dist/components/views/PlansPage.test.js +294 -12
- package/dist/components/views/StandaloneLayout.js +283 -533
- package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
- package/dist/components/views/panels/PlanningDrawer.js +18 -11
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
- package/dist/components/views/planningScope.d.ts +20 -0
- package/dist/components/views/planningScope.js +25 -0
- package/dist/components/views/planningScope.test.d.ts +1 -0
- package/dist/components/views/planningScope.test.js +60 -0
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
- package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
- package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
- package/dist/core/AiProfileService.d.ts +39 -2
- package/dist/core/AiProfileService.js +639 -73
- package/dist/core/AiProfileService.test.js +19 -1
- package/dist/core/AiProfiles.test.js +566 -7
- package/dist/core/AttachmentLinkService.js +57 -15
- package/dist/core/AuthIdentityService.d.ts +107 -0
- package/dist/core/AuthIdentityService.js +284 -0
- package/dist/core/AuthTokenService.d.ts +4 -0
- package/dist/core/AuthTokenService.js +34 -6
- package/dist/core/AuthTokenService.test.js +3 -3
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +115 -6
- package/dist/core/PlanEntitlementService.d.ts +23 -2
- package/dist/core/PlanEntitlementService.js +343 -57
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +35 -2
- package/dist/core/PlanningEntities.test.js +52 -0
- package/dist/core/SignupMonetizationSettings.test.js +199 -27
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +42 -42
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/Taskforce.d.ts +225 -14
- package/dist/core/Taskforce.js +765 -192
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
- package/dist/core/WorkspaceLifecycleService.js +0 -5
- package/dist/core/WorkspacePermissions.test.js +25 -1
- package/dist/core/WorkspacePlanMode.test.js +117 -16
- package/dist/core/shared.d.ts +1 -1
- package/dist/core/shared.js +5 -1
- package/dist/core/types.d.ts +68 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
- package/dist/hooks/sync/bootstrap.js +2 -0
- package/dist/hooks/sync/controlPlane.d.ts +37 -0
- package/dist/hooks/sync/controlPlane.js +78 -0
- package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
- package/dist/hooks/sync/controlPlane.test.js +121 -0
- package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
- package/dist/hooks/sync/lifecyclePolicy.js +93 -0
- package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
- package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
- package/dist/hooks/sync/orchestratorShared.js +68 -47
- package/dist/hooks/sync/orchestratorShared.test.js +50 -1
- package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
- package/dist/hooks/sync/pullLifecycle.js +24 -0
- package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
- package/dist/hooks/sync/pullLifecycle.test.js +80 -0
- package/dist/hooks/sync/recovery.d.ts +16 -2
- package/dist/hooks/sync/recovery.js +171 -53
- package/dist/hooks/sync/recovery.test.d.ts +1 -0
- package/dist/hooks/sync/recovery.test.js +292 -0
- package/dist/hooks/sync/transfers.d.ts +15 -3
- package/dist/hooks/sync/transfers.js +179 -62
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +396 -0
- package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
- package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
- package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
- package/dist/hooks/sync/useSyncStatusActions.js +88 -0
- package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
- package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
- package/dist/hooks/sync/useSyncStatusControls.js +60 -0
- package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
- package/dist/hooks/useSyncOrchestrator.js +359 -178
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskforce.d.ts +60 -2
- package/dist/hooks/useTaskforce.js +549 -240
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +6 -1
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
- package/dist/localization/locales/en-US.d.ts +0 -1
- package/dist/localization/locales/en-US.js +0 -1
- package/dist/localization/locales/es-419.js +0 -1
- package/dist/localization/locales/pt-BR.js +0 -1
- package/dist/mcp/documentAssetRegistrar.js +3 -3
- package/dist/mcp/runtime.d.ts +2 -0
- package/dist/mcp/runtime.js +131 -64
- package/dist/mcp/runtime.test.js +291 -12
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +171 -61
- package/dist/migrations/taskSchemaMigrations.test.js +15 -0
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/dist/resources/templates/workflows/collaborate.yaml +1 -1
- package/dist/resources/templates/workflows/evaluate.yaml +2 -2
- package/dist/resources/templates/workflows/plan.yaml +1 -1
- package/dist/resources/templates/workflows/review.yaml +2 -2
- package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
- package/dist/server/auth/providers/apple.d.ts +33 -0
- package/dist/server/auth/providers/apple.js +82 -0
- package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
- package/dist/server/auth/providers/appleClientSecret.js +65 -0
- package/dist/server/auth/providers/github.d.ts +26 -0
- package/dist/server/auth/providers/github.js +86 -0
- package/dist/server/auth/providers/google.d.ts +21 -0
- package/dist/server/auth/providers/google.js +56 -0
- package/dist/server/auth/providers/types.d.ts +21 -0
- package/dist/server/auth/providers/types.js +1 -0
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.js +6 -3
- package/dist/server/documentReviewRoutes.test.js +36 -3
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +120 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +106 -3
- package/dist/server/routes/admin.d.ts +3 -0
- package/dist/server/routes/admin.js +272 -19
- package/dist/server/routes/auth.d.ts +15 -2
- package/dist/server/routes/auth.js +994 -57
- package/dist/server/routes/authSupport.d.ts +1 -1
- package/dist/server/routes/authSupport.js +1 -1
- package/dist/server/routes/billing.js +361 -35
- package/dist/server/routes/billing.test.js +592 -12
- package/dist/server/routes/documents.d.ts +1 -0
- package/dist/server/routes/documents.js +172 -70
- package/dist/server/routes/shared.d.ts +28 -2
- package/dist/server/routes/shared.js +69 -4
- package/dist/server/routes/sync.integration.test.js +437 -39
- package/dist/server/routes/sync.js +61 -605
- package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
- package/dist/server/routes/syncAuxRoutes.js +91 -0
- package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
- package/dist/server/routes/syncAuxRoutes.test.js +158 -0
- package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
- package/dist/server/routes/syncPullApplyRoutes.js +204 -0
- package/dist/server/routes/syncPushRoutes.d.ts +24 -0
- package/dist/server/routes/syncPushRoutes.js +212 -0
- package/dist/server/routes/syncRouteGuards.d.ts +36 -0
- package/dist/server/routes/syncRouteGuards.js +67 -0
- package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
- package/dist/server/routes/syncRouteGuards.test.js +94 -0
- package/dist/server/routes/syncRouteTypes.d.ts +13 -0
- package/dist/server/routes/syncRouteTypes.js +1 -0
- package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
- package/dist/server/routes/syncSnapshotRoutes.js +180 -0
- package/dist/server/routes/tasks.js +43 -1
- package/dist/server/routes/workspaces.js +36 -5
- package/dist/server/routes.js +60 -3
- package/dist/server/routes.test.js +1824 -171
- package/dist/services/workflowExportSnapshots.test.js +2 -0
- package/dist/shared/aiProfileSeatScope.d.ts +5 -0
- package/dist/shared/aiProfileSeatScope.js +21 -0
- package/dist/shared/passwordPolicy.d.ts +13 -0
- package/dist/shared/passwordPolicy.js +84 -0
- package/dist/shared/passwordPolicy.test.d.ts +1 -0
- package/dist/shared/passwordPolicy.test.js +34 -0
- package/dist/storage/workspaceAssetStore.d.ts +15 -3
- package/dist/storage/workspaceAssetStore.js +132 -44
- package/dist/storage/workspaceAssetStore.test.js +93 -6
- package/dist/styles/fonts.css +70 -0
- package/dist/sync/collaborationSyncPayload.d.ts +6 -1
- package/dist/sync/collaborationSyncPayload.js +17 -11
- package/dist/sync/collaborationSyncPayload.test.js +13 -1
- package/dist/sync/contentSyncState.d.ts +4 -8
- package/dist/sync/contentSyncState.js +17 -70
- package/dist/sync/syncApplyHandlers.d.ts +7 -0
- package/dist/sync/syncApplyHandlers.js +28 -13
- package/dist/sync/syncApplyHandlers.test.js +70 -3
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +44 -0
- package/dist/sync/syncResourceAdapters.js +77 -0
- package/dist/sync/syncService.d.ts +5 -0
- package/dist/sync/syncService.js +52 -0
- package/dist/sync/syncService.test.js +57 -3
- package/dist/sync/taskSyncChangeKey.d.ts +2 -0
- package/dist/sync/taskSyncChangeKey.js +143 -0
- package/dist/sync/taskSyncPayload.js +3 -8
- package/dist/sync/workspacePullFeed.js +4 -3
- package/dist/sync/workspacePullFeed.test.js +43 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +45 -0
- package/dist/sync/workspaceSyncModel.js +183 -28
- package/dist/sync/workspaceSyncModel.test.js +291 -12
- package/dist/sync/workspaceSyncState.d.ts +17 -0
- package/dist/sync/workspaceSyncState.js +2 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +17 -0
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
- package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
- package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
- package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
- package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
- package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
- package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
- package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
- package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
- package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
- package/dist/ui/assets/index-C6k75jr8.js +6 -0
- package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
- package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/ui/fonts/Noir_medium.otf +0 -0
- package/dist/ui/fonts/Noir_medium.woff2 +0 -0
- package/dist/ui/fonts/Noir_regular.otf +0 -0
- package/dist/ui/fonts/Noir_regular.woff2 +0 -0
- package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/assignees.d.ts +2 -0
- package/dist/utils/assignees.js +2 -0
- package/dist/utils/avatarUpload.d.ts +27 -0
- package/dist/utils/avatarUpload.js +132 -0
- package/dist/utils/avatarUpload.test.d.ts +1 -0
- package/dist/utils/avatarUpload.test.js +40 -0
- package/dist/utils/bootstrapTrace.d.ts +7 -0
- package/dist/utils/bootstrapTrace.js +29 -0
- package/dist/utils/commercialLifecyclePresentation.js +3 -0
- package/dist/utils/contextAssetEvents.d.ts +2 -0
- package/dist/utils/syncEventDetails.d.ts +3 -0
- package/dist/utils/syncEventDetails.js +64 -0
- package/dist/utils/syncEventDetails.test.d.ts +1 -0
- package/dist/utils/syncEventDetails.test.js +23 -0
- package/dist/utils/syncEventPresentation.d.ts +15 -0
- package/dist/utils/syncEventPresentation.js +129 -0
- package/dist/utils/syncEventPresentation.test.d.ts +1 -0
- package/dist/utils/syncEventPresentation.test.js +64 -0
- package/dist/utils/syncStatusPresentation.d.ts +72 -0
- package/dist/utils/syncStatusPresentation.js +217 -0
- package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
- package/dist/utils/syncStatusPresentation.test.js +164 -0
- package/dist/utils/taskActivity.d.ts +2 -0
- package/dist/utils/taskActivity.js +228 -22
- package/dist/utils/taskActivity.test.js +119 -8
- package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
- package/dist/utils/workspaceSyncPresentation.js +68 -2
- package/dist/utils/workspaceSyncPresentation.test.js +36 -1
- package/package.json +3 -2
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/run-e2e-realtime.sh +17 -3
- package/scripts/run-smoke.sh +17 -5
- package/scripts/run-soak.sh +17 -5
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/src/resources/templates/workflows/collaborate.yaml +1 -1
- package/src/resources/templates/workflows/evaluate.yaml +2 -2
- package/src/resources/templates/workflows/plan.yaml +1 -1
- package/src/resources/templates/workflows/review.yaml +2 -2
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/index-C2-OXZV7.css +0 -1
- package/dist/ui/assets/index-DcSI5F86.js +0 -6
- package/scripts/migrate-planning-model.ts +0 -233
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
function getActivityFieldLabel(field) {
|
|
2
2
|
if (field === 'title')
|
|
3
|
-
return '
|
|
3
|
+
return 'Title';
|
|
4
4
|
if (field === 'description')
|
|
5
|
-
return '
|
|
5
|
+
return 'Description';
|
|
6
6
|
if (field === 'assignee')
|
|
7
|
-
return '
|
|
7
|
+
return 'Assigned to';
|
|
8
|
+
if (field === 'status')
|
|
9
|
+
return 'Status';
|
|
10
|
+
if (field === 'scheduledDate')
|
|
11
|
+
return 'Scheduled date';
|
|
12
|
+
if (field === 'dueDate')
|
|
13
|
+
return 'Due date';
|
|
8
14
|
if (field === 'workstreamId')
|
|
9
|
-
return '
|
|
15
|
+
return 'Workstream';
|
|
10
16
|
if (field === 'initiativeId')
|
|
11
|
-
return '
|
|
17
|
+
return 'Initiative';
|
|
12
18
|
return field;
|
|
13
19
|
}
|
|
20
|
+
function formatStatusValue(value) {
|
|
21
|
+
if (value === 'task')
|
|
22
|
+
return 'Task';
|
|
23
|
+
return value
|
|
24
|
+
.replace(/[-_]+/g, ' ')
|
|
25
|
+
.replace(/\s+/g, ' ')
|
|
26
|
+
.trim()
|
|
27
|
+
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
28
|
+
}
|
|
14
29
|
function formatValue(value, field, formatter) {
|
|
15
30
|
const formatted = formatter?.(field, value);
|
|
16
31
|
if (typeof formatted === 'string')
|
|
@@ -23,6 +38,8 @@ function formatValue(value, field, formatter) {
|
|
|
23
38
|
}
|
|
24
39
|
if (typeof value === 'object')
|
|
25
40
|
return 'updated';
|
|
41
|
+
if (field === 'status')
|
|
42
|
+
return formatStatusValue(String(value));
|
|
26
43
|
return String(value);
|
|
27
44
|
}
|
|
28
45
|
function normalizeChecklistItems(value) {
|
|
@@ -75,10 +92,131 @@ function summarizeChecklistChange(change) {
|
|
|
75
92
|
}
|
|
76
93
|
return 'Checklist updated';
|
|
77
94
|
}
|
|
95
|
+
function stripQueryAndHash(value) {
|
|
96
|
+
return value.split(/[?#]/, 1)[0] || value;
|
|
97
|
+
}
|
|
98
|
+
function basenameFromPath(value) {
|
|
99
|
+
const normalized = stripQueryAndHash(value).replace(/\\/g, '/');
|
|
100
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
101
|
+
return segments[segments.length - 1] || normalized;
|
|
102
|
+
}
|
|
103
|
+
function isImageAttachmentCandidate(value) {
|
|
104
|
+
return /\.(png|jpe?g|webp|gif)$/i.test(stripQueryAndHash(value));
|
|
105
|
+
}
|
|
106
|
+
function isDocumentAttachmentCandidate(value) {
|
|
107
|
+
if (value.includes('/api/taskforce/documents/'))
|
|
108
|
+
return true;
|
|
109
|
+
return /\.(pdf|txt|md|markdown|csv|json|docx?|html?|js|jsx|ts|tsx|css|py|java|go|rs|sh)$/i.test(stripQueryAndHash(value));
|
|
110
|
+
}
|
|
111
|
+
function normalizeAttachmentList(value) {
|
|
112
|
+
if (!Array.isArray(value))
|
|
113
|
+
return [];
|
|
114
|
+
return value.flatMap((entry, index) => {
|
|
115
|
+
const attachment = (entry && typeof entry === 'object' ? entry : null);
|
|
116
|
+
const path = typeof entry === 'string'
|
|
117
|
+
? entry
|
|
118
|
+
: String(attachment?.path || '').trim();
|
|
119
|
+
const fsPath = String(attachment?.fsPath || '').trim();
|
|
120
|
+
const assetId = String(attachment?.assetId || '').trim();
|
|
121
|
+
const referenceLabel = String(attachment?.referenceLabel || '').trim();
|
|
122
|
+
const label = [
|
|
123
|
+
attachment?.displayName,
|
|
124
|
+
attachment?.originalFilename,
|
|
125
|
+
attachment?.caption,
|
|
126
|
+
referenceLabel,
|
|
127
|
+
fsPath ? basenameFromPath(fsPath) : '',
|
|
128
|
+
path ? basenameFromPath(path) : '',
|
|
129
|
+
]
|
|
130
|
+
.map((candidate) => String(candidate || '').trim())
|
|
131
|
+
.find(Boolean) || `Attachment ${index + 1}`;
|
|
132
|
+
const key = assetId || fsPath || path || `${label}:${index}`;
|
|
133
|
+
const kindCandidates = [
|
|
134
|
+
path,
|
|
135
|
+
fsPath,
|
|
136
|
+
String(attachment?.originalFilename || '').trim(),
|
|
137
|
+
String(attachment?.displayName || '').trim(),
|
|
138
|
+
referenceLabel,
|
|
139
|
+
].filter(Boolean);
|
|
140
|
+
const kind = kindCandidates.some((candidate) => isImageAttachmentCandidate(candidate))
|
|
141
|
+
? 'image'
|
|
142
|
+
: (referenceLabel.startsWith('D-') || kindCandidates.some((candidate) => isDocumentAttachmentCandidate(candidate)))
|
|
143
|
+
? 'document'
|
|
144
|
+
: 'attachment';
|
|
145
|
+
return [{ key, label, kind }];
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function pluralizeAttachmentKind(kind, count) {
|
|
149
|
+
if (count === 1)
|
|
150
|
+
return kind;
|
|
151
|
+
if (kind === 'image')
|
|
152
|
+
return 'images';
|
|
153
|
+
if (kind === 'document')
|
|
154
|
+
return 'documents';
|
|
155
|
+
return 'attachments';
|
|
156
|
+
}
|
|
157
|
+
function describeAttachmentCollection(entries) {
|
|
158
|
+
if (entries.length === 0)
|
|
159
|
+
return 'attachments';
|
|
160
|
+
const counts = entries.reduce((acc, entry) => {
|
|
161
|
+
acc[entry.kind] += 1;
|
|
162
|
+
return acc;
|
|
163
|
+
}, { image: 0, document: 0, attachment: 0 });
|
|
164
|
+
const nonZeroParts = Object.entries(counts)
|
|
165
|
+
.filter(([, count]) => count > 0)
|
|
166
|
+
.map(([kind, count]) => count === 1 ? kind : `${count} ${pluralizeAttachmentKind(kind, count)}`);
|
|
167
|
+
if (nonZeroParts.length === 1)
|
|
168
|
+
return nonZeroParts[0];
|
|
169
|
+
if (entries.length <= 3 && nonZeroParts.length <= 2)
|
|
170
|
+
return nonZeroParts.join(' and ');
|
|
171
|
+
return `${entries.length} attachments`;
|
|
172
|
+
}
|
|
173
|
+
function capitalizeLeading(value) {
|
|
174
|
+
if (!value)
|
|
175
|
+
return value;
|
|
176
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
177
|
+
}
|
|
178
|
+
function summarizeAttachmentChange(change) {
|
|
179
|
+
const before = normalizeAttachmentList(change?.from);
|
|
180
|
+
const after = normalizeAttachmentList(change?.to);
|
|
181
|
+
const beforeByKey = new Map(before.map((entry) => [entry.key, entry]));
|
|
182
|
+
const afterByKey = new Map(after.map((entry) => [entry.key, entry]));
|
|
183
|
+
const added = after.filter((entry) => !beforeByKey.has(entry.key));
|
|
184
|
+
const removed = before.filter((entry) => !afterByKey.has(entry.key));
|
|
185
|
+
if (added.length === 1 && removed.length === 0) {
|
|
186
|
+
return `${capitalizeLeading(added[0].kind)} attached: ${added[0].label}`;
|
|
187
|
+
}
|
|
188
|
+
if (removed.length === 1 && added.length === 0) {
|
|
189
|
+
return `${capitalizeLeading(removed[0].kind)} removed: ${removed[0].label}`;
|
|
190
|
+
}
|
|
191
|
+
if (added.length > 0 && removed.length === 0) {
|
|
192
|
+
return `${capitalizeLeading(describeAttachmentCollection(added))} attached`;
|
|
193
|
+
}
|
|
194
|
+
if (removed.length > 0 && added.length === 0) {
|
|
195
|
+
return `${capitalizeLeading(describeAttachmentCollection(removed))} removed`;
|
|
196
|
+
}
|
|
197
|
+
if (added.length === 1 && removed.length === 1) {
|
|
198
|
+
return `${capitalizeLeading(added[0].kind)} replaced: ${removed[0].label} -> ${added[0].label}`;
|
|
199
|
+
}
|
|
200
|
+
if (added.length > 0 || removed.length > 0) {
|
|
201
|
+
return `${capitalizeLeading(describeAttachmentCollection(added))} attached, ${describeAttachmentCollection(removed)} removed`;
|
|
202
|
+
}
|
|
203
|
+
const relabeled = after.filter((entry) => {
|
|
204
|
+
const previous = beforeByKey.get(entry.key);
|
|
205
|
+
return previous && previous.label !== entry.label;
|
|
206
|
+
});
|
|
207
|
+
if (relabeled.length === 1 && before.length === 1 && after.length === 1) {
|
|
208
|
+
const previous = beforeByKey.get(relabeled[0].key);
|
|
209
|
+
return `${capitalizeLeading(relabeled[0].kind)} renamed: ${previous?.label || 'Attachment'} -> ${relabeled[0].label}`;
|
|
210
|
+
}
|
|
211
|
+
return 'Attachment details updated';
|
|
212
|
+
}
|
|
78
213
|
export function summarizeTaskChange(field, change, formatter) {
|
|
79
214
|
if (field === 'checklistItems') {
|
|
80
215
|
return summarizeChecklistChange(change);
|
|
81
216
|
}
|
|
217
|
+
if (field === 'attachments') {
|
|
218
|
+
return summarizeAttachmentChange(change);
|
|
219
|
+
}
|
|
82
220
|
const label = getActivityFieldLabel(field);
|
|
83
221
|
if (field === 'title') {
|
|
84
222
|
return `${label}: updated`;
|
|
@@ -108,22 +246,28 @@ export function summarizeTaskEvent(event, formatter) {
|
|
|
108
246
|
if (event.action === 'task-unarchived')
|
|
109
247
|
return 'Task restored';
|
|
110
248
|
if (event.action === 'task-status-changed' && changes.status) {
|
|
111
|
-
return `Status: ${formatValue(changes.status.from, 'status', formatter)} -> ${formatValue(changes.status.to, 'status', formatter)}`;
|
|
249
|
+
return `Status changed: ${formatValue(changes.status.from, 'status', formatter)} -> ${formatValue(changes.status.to, 'status', formatter)}`;
|
|
112
250
|
}
|
|
113
251
|
if (event.action === 'task-schedule-changed') {
|
|
114
252
|
if (changes.scheduledDate) {
|
|
115
|
-
return `Scheduled: ${formatValue(changes.scheduledDate.from, 'scheduledDate', formatter)} -> ${formatValue(changes.scheduledDate.to, 'scheduledDate', formatter)}`;
|
|
253
|
+
return `Scheduled date changed: ${formatValue(changes.scheduledDate.from, 'scheduledDate', formatter)} -> ${formatValue(changes.scheduledDate.to, 'scheduledDate', formatter)}`;
|
|
116
254
|
}
|
|
117
255
|
if (changes.dueDate) {
|
|
118
|
-
return `Due: ${formatValue(changes.dueDate.from, 'dueDate', formatter)} -> ${formatValue(changes.dueDate.to, 'dueDate', formatter)}`;
|
|
256
|
+
return `Due date changed: ${formatValue(changes.dueDate.from, 'dueDate', formatter)} -> ${formatValue(changes.dueDate.to, 'dueDate', formatter)}`;
|
|
119
257
|
}
|
|
120
258
|
return 'Schedule updated';
|
|
121
259
|
}
|
|
122
260
|
if (event.action === 'task-relationship-changed' && changes.workstreamId) {
|
|
123
|
-
return `Workstream: ${formatValue(changes.workstreamId.from, 'workstreamId', formatter)} -> ${formatValue(changes.workstreamId.to, 'workstreamId', formatter)}`;
|
|
261
|
+
return `Workstream changed: ${formatValue(changes.workstreamId.from, 'workstreamId', formatter)} -> ${formatValue(changes.workstreamId.to, 'workstreamId', formatter)}`;
|
|
262
|
+
}
|
|
263
|
+
if (event.action === 'task-relationship-changed' && changes.initiativeId) {
|
|
264
|
+
return `Initiative changed: ${formatValue(changes.initiativeId.from, 'initiativeId', formatter)} -> ${formatValue(changes.initiativeId.to, 'initiativeId', formatter)}`;
|
|
265
|
+
}
|
|
266
|
+
if (event.action === 'task-attachments-changed') {
|
|
267
|
+
return changes.attachments
|
|
268
|
+
? summarizeTaskChange('attachments', changes.attachments, formatter)
|
|
269
|
+
: 'Attachments updated';
|
|
124
270
|
}
|
|
125
|
-
if (event.action === 'task-attachments-changed')
|
|
126
|
-
return 'Attachments updated';
|
|
127
271
|
if (fields.length === 1) {
|
|
128
272
|
const field = fields[0];
|
|
129
273
|
const change = changes[field];
|
|
@@ -133,25 +277,87 @@ export function summarizeTaskEvent(event, formatter) {
|
|
|
133
277
|
return `${fields.length} fields updated`;
|
|
134
278
|
return 'Task updated';
|
|
135
279
|
}
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
280
|
+
function activityTimestamp(item) {
|
|
281
|
+
const parsed = Date.parse(String(item.timestamp || ''));
|
|
282
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
283
|
+
}
|
|
284
|
+
export function getTaskEventPrimaryChangeField(event) {
|
|
285
|
+
const changes = event.details?.changes || {};
|
|
286
|
+
const fields = Object.keys(changes);
|
|
287
|
+
if (event.action === 'task-status-changed' && changes.status)
|
|
288
|
+
return 'status';
|
|
289
|
+
if (event.action === 'task-schedule-changed') {
|
|
290
|
+
if (changes.scheduledDate)
|
|
291
|
+
return 'scheduledDate';
|
|
292
|
+
if (changes.dueDate)
|
|
293
|
+
return 'dueDate';
|
|
294
|
+
}
|
|
295
|
+
if (event.action === 'task-relationship-changed') {
|
|
296
|
+
if (changes.workstreamId)
|
|
297
|
+
return 'workstreamId';
|
|
298
|
+
if (changes.initiativeId)
|
|
299
|
+
return 'initiativeId';
|
|
300
|
+
}
|
|
301
|
+
if (event.action === 'task-attachments-changed' && changes.attachments)
|
|
302
|
+
return 'attachments';
|
|
303
|
+
return fields.length === 1 ? fields[0] : null;
|
|
304
|
+
}
|
|
305
|
+
export function getTaskActivityItems(task) {
|
|
306
|
+
const deduped = new Map();
|
|
307
|
+
const commentIds = new Set();
|
|
308
|
+
const addItem = (item) => {
|
|
309
|
+
if (item.type === 'event' && item.event.action === 'task-comment-added')
|
|
310
|
+
return;
|
|
311
|
+
const key = item.type === 'comment'
|
|
312
|
+
? `comment:${String(item.comment.id || item.id).replace(/^comment:/, '')}`
|
|
313
|
+
: `event:${String(item.event.id || item.id).replace(/^event:/, '')}`;
|
|
314
|
+
deduped.set(key, { ...item, id: key });
|
|
315
|
+
if (item.type === 'comment') {
|
|
316
|
+
commentIds.add(String(item.comment.id || item.id).replace(/^comment:/, ''));
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
if (Array.isArray(task.activity)) {
|
|
320
|
+
task.activity.forEach(addItem);
|
|
321
|
+
}
|
|
322
|
+
if (Array.isArray(task.comments)) {
|
|
323
|
+
task.comments.forEach((comment) => {
|
|
324
|
+
const commentId = String(comment.id || '').trim();
|
|
325
|
+
if (!commentId || commentIds.has(commentId))
|
|
326
|
+
return;
|
|
327
|
+
addItem({
|
|
328
|
+
id: `comment:${commentId}`,
|
|
142
329
|
type: 'comment',
|
|
143
330
|
timestamp: comment.timestamp,
|
|
144
331
|
comment
|
|
145
|
-
})
|
|
146
|
-
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return [...deduped.values()].sort((left, right) => {
|
|
336
|
+
const leftTs = activityTimestamp(left);
|
|
337
|
+
const rightTs = activityTimestamp(right);
|
|
338
|
+
if (leftTs !== null && rightTs !== null && leftTs !== rightTs)
|
|
339
|
+
return leftTs - rightTs;
|
|
340
|
+
if (leftTs !== null && rightTs === null)
|
|
341
|
+
return -1;
|
|
342
|
+
if (leftTs === null && rightTs !== null)
|
|
343
|
+
return 1;
|
|
344
|
+
return String(left.id || '').localeCompare(String(right.id || ''));
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
export function getLatestTaskActivity(task) {
|
|
348
|
+
const items = getTaskActivityItems(task);
|
|
147
349
|
if (items.length === 0)
|
|
148
350
|
return null;
|
|
149
351
|
return [...items].sort((left, right) => {
|
|
150
|
-
const leftTs =
|
|
151
|
-
const rightTs =
|
|
152
|
-
if (
|
|
352
|
+
const leftTs = activityTimestamp(left);
|
|
353
|
+
const rightTs = activityTimestamp(right);
|
|
354
|
+
if (leftTs !== null && rightTs !== null && leftTs !== rightTs) {
|
|
153
355
|
return rightTs - leftTs;
|
|
154
356
|
}
|
|
357
|
+
if (leftTs !== null && rightTs === null)
|
|
358
|
+
return -1;
|
|
359
|
+
if (leftTs === null && rightTs !== null)
|
|
360
|
+
return 1;
|
|
155
361
|
return String(right.id || '').localeCompare(String(left.id || ''));
|
|
156
362
|
})[0] || null;
|
|
157
363
|
}
|
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { summarizeTaskChange, summarizeTaskEvent } from './taskActivity';
|
|
2
|
+
import { getTaskActivityItems, summarizeTaskChange, summarizeTaskEvent } from './taskActivity';
|
|
3
3
|
describe('summarizeTaskChange', () => {
|
|
4
4
|
it('summarizes title edits without dumping before and after text', () => {
|
|
5
5
|
expect(summarizeTaskChange('title', {
|
|
6
6
|
from: 'Old title',
|
|
7
7
|
to: 'New title',
|
|
8
|
-
})).toBe('
|
|
8
|
+
})).toBe('Title: updated');
|
|
9
9
|
});
|
|
10
|
-
it('summarizes assignee changes with
|
|
10
|
+
it('summarizes assignee changes with sentence-case labels and formatted values', () => {
|
|
11
11
|
expect(summarizeTaskChange('assignee', { from: 'unassigned', to: 'member-1' }, (_field, value) => {
|
|
12
12
|
if (value === 'unassigned')
|
|
13
13
|
return 'Unassigned';
|
|
14
14
|
if (value === 'member-1')
|
|
15
15
|
return 'Emma Peel';
|
|
16
16
|
return undefined;
|
|
17
|
-
})).toBe('
|
|
17
|
+
})).toBe('Assigned to: Unassigned -> Emma Peel');
|
|
18
18
|
});
|
|
19
19
|
it('summarizes description additions without rendering the body text', () => {
|
|
20
20
|
expect(summarizeTaskChange('description', {
|
|
21
21
|
from: '',
|
|
22
22
|
to: 'Line one\n\nLine two',
|
|
23
|
-
})).toBe('
|
|
23
|
+
})).toBe('Description: added');
|
|
24
24
|
});
|
|
25
25
|
it('summarizes description clears without rendering the old body text', () => {
|
|
26
26
|
expect(summarizeTaskChange('description', {
|
|
27
27
|
from: 'Old body',
|
|
28
28
|
to: '',
|
|
29
|
-
})).toBe('
|
|
29
|
+
})).toBe('Description: cleared');
|
|
30
30
|
});
|
|
31
31
|
it('summarizes checklist completion clearly', () => {
|
|
32
32
|
expect(summarizeTaskChange('checklistItems', {
|
|
@@ -59,7 +59,49 @@ describe('summarizeTaskChange', () => {
|
|
|
59
59
|
})).toBe('Checklist reordered');
|
|
60
60
|
});
|
|
61
61
|
it('uses readable labels for workstream relationship changes', () => {
|
|
62
|
-
expect(summarizeTaskChange('workstreamId', { from: null, to: 'workstream-1' }, (_field, value) => value === 'workstream-1' ? 'WS-12' : undefined)).toBe('
|
|
62
|
+
expect(summarizeTaskChange('workstreamId', { from: null, to: 'workstream-1' }, (_field, value) => value === 'workstream-1' ? 'WS-12' : undefined)).toBe('Workstream: none -> WS-12');
|
|
63
|
+
});
|
|
64
|
+
it('summarizes image attachment additions clearly', () => {
|
|
65
|
+
expect(summarizeTaskChange('attachments', {
|
|
66
|
+
from: [],
|
|
67
|
+
to: [{
|
|
68
|
+
path: '/api/taskforce/context/workspaces/default/assets/images/homepage-proof.png',
|
|
69
|
+
caption: 'Homepage proof',
|
|
70
|
+
}],
|
|
71
|
+
})).toBe('Image attached: Homepage proof');
|
|
72
|
+
});
|
|
73
|
+
it('summarizes document attachment removals clearly', () => {
|
|
74
|
+
expect(summarizeTaskChange('attachments', {
|
|
75
|
+
from: [{
|
|
76
|
+
path: '/api/taskforce/documents/asset-doc-1/content',
|
|
77
|
+
originalFilename: 'spec.md',
|
|
78
|
+
assetId: 'asset-doc-1',
|
|
79
|
+
}],
|
|
80
|
+
to: [],
|
|
81
|
+
})).toBe('Document removed: spec.md');
|
|
82
|
+
});
|
|
83
|
+
it('summarizes multiple document attachments concisely', () => {
|
|
84
|
+
expect(summarizeTaskChange('attachments', {
|
|
85
|
+
from: [],
|
|
86
|
+
to: [
|
|
87
|
+
{ path: '/api/taskforce/documents/asset-doc-1/content', originalFilename: 'spec.md', assetId: 'asset-doc-1' },
|
|
88
|
+
{ path: '/api/taskforce/documents/asset-doc-2/content', originalFilename: 'notes.pdf', assetId: 'asset-doc-2' },
|
|
89
|
+
],
|
|
90
|
+
})).toBe('2 documents attached');
|
|
91
|
+
});
|
|
92
|
+
it('summarizes attachment renames when the same asset remains attached', () => {
|
|
93
|
+
expect(summarizeTaskChange('attachments', {
|
|
94
|
+
from: [{
|
|
95
|
+
path: '/api/taskforce/documents/asset-doc-1/content',
|
|
96
|
+
assetId: 'asset-doc-1',
|
|
97
|
+
caption: 'Draft spec',
|
|
98
|
+
}],
|
|
99
|
+
to: [{
|
|
100
|
+
path: '/api/taskforce/documents/asset-doc-1/content',
|
|
101
|
+
assetId: 'asset-doc-1',
|
|
102
|
+
caption: 'Final spec',
|
|
103
|
+
}],
|
|
104
|
+
})).toBe('Document renamed: Draft spec -> Final spec');
|
|
63
105
|
});
|
|
64
106
|
});
|
|
65
107
|
describe('summarizeTaskEvent', () => {
|
|
@@ -80,6 +122,75 @@ describe('summarizeTaskEvent', () => {
|
|
|
80
122
|
},
|
|
81
123
|
},
|
|
82
124
|
},
|
|
83
|
-
}, (_field, value) => value === 'workstream-1' ? 'WS-12' : undefined)).toBe('Workstream: none -> WS-12');
|
|
125
|
+
}, (_field, value) => value === 'workstream-1' ? 'WS-12' : undefined)).toBe('Workstream changed: none -> WS-12');
|
|
126
|
+
});
|
|
127
|
+
it('uses attachment-specific summaries for attachment activity events', () => {
|
|
128
|
+
expect(summarizeTaskEvent({
|
|
129
|
+
id: 'event-2',
|
|
130
|
+
taskId: 'task-1',
|
|
131
|
+
workspaceId: 'default',
|
|
132
|
+
action: 'task-attachments-changed',
|
|
133
|
+
actor: 'user',
|
|
134
|
+
actorType: 'human',
|
|
135
|
+
createdAt: '2026-04-12T21:42:00.000Z',
|
|
136
|
+
details: {
|
|
137
|
+
changes: {
|
|
138
|
+
attachments: {
|
|
139
|
+
from: [{
|
|
140
|
+
path: '/api/taskforce/context/workspaces/default/assets/images/old-proof.png',
|
|
141
|
+
caption: 'Old proof',
|
|
142
|
+
}],
|
|
143
|
+
to: [{
|
|
144
|
+
path: '/api/taskforce/context/workspaces/default/assets/images/new-proof.png',
|
|
145
|
+
caption: 'New proof',
|
|
146
|
+
}],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
})).toBe('Image replaced: Old proof -> New proof');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe('getTaskActivityItems', () => {
|
|
154
|
+
it('combines comments and audit events while hiding duplicate comment-added events', () => {
|
|
155
|
+
const items = getTaskActivityItems({
|
|
156
|
+
comments: [{
|
|
157
|
+
id: 'comment-1',
|
|
158
|
+
author: 'user',
|
|
159
|
+
text: 'Captured comment',
|
|
160
|
+
timestamp: '2026-03-22T10:00:00.000Z',
|
|
161
|
+
}],
|
|
162
|
+
activity: [
|
|
163
|
+
{
|
|
164
|
+
id: 'event:event-1',
|
|
165
|
+
type: 'event',
|
|
166
|
+
timestamp: '2026-03-22T09:00:00.000Z',
|
|
167
|
+
event: {
|
|
168
|
+
id: 'event-1',
|
|
169
|
+
taskId: 'task-1',
|
|
170
|
+
workspaceId: 'default',
|
|
171
|
+
action: 'task-status-changed',
|
|
172
|
+
actor: 'user',
|
|
173
|
+
actorType: 'human',
|
|
174
|
+
createdAt: '2026-03-22T09:00:00.000Z',
|
|
175
|
+
details: { changes: { status: { from: 'task', to: 'in-progress' } } },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'event:comment-event-1',
|
|
180
|
+
type: 'event',
|
|
181
|
+
timestamp: '2026-03-22T10:00:00.000Z',
|
|
182
|
+
event: {
|
|
183
|
+
id: 'comment-event-1',
|
|
184
|
+
taskId: 'task-1',
|
|
185
|
+
workspaceId: 'default',
|
|
186
|
+
action: 'task-comment-added',
|
|
187
|
+
actor: 'user',
|
|
188
|
+
actorType: 'human',
|
|
189
|
+
createdAt: '2026-03-22T10:00:00.000Z',
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
expect(items.map((item) => item.id)).toEqual(['event:event-1', 'comment:comment-1']);
|
|
84
195
|
});
|
|
85
196
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type HeaderWorkspaceSyncStatus = 'off' | 'syncing' | 'attention' | 'healthy';
|
|
2
|
+
type WorkspaceSyncBlockedReason = 'cloud-auth-unconfigured' | 'runtime-not-local' | 'sync-disabled' | 'invalid-workspace' | 'repair-active' | 'attach-cloud' | 'provision-local' | 'auth-required' | 'push-in-flight' | 'pull-in-flight' | 'retry-pending' | 'lease-held';
|
|
2
3
|
interface ResolveHeaderWorkspaceSyncPresentationInput {
|
|
3
4
|
runtimeMode: 'local' | 'cloud';
|
|
4
5
|
isAuthenticated: boolean;
|
|
@@ -7,6 +8,9 @@ interface ResolveHeaderWorkspaceSyncPresentationInput {
|
|
|
7
8
|
workspaceSyncSummary: string;
|
|
8
9
|
workspaceSyncRecommendedAction: string;
|
|
9
10
|
workspaceSyncError: string;
|
|
11
|
+
pushBlockedReason?: WorkspaceSyncBlockedReason | null;
|
|
12
|
+
pullBlockedReason?: WorkspaceSyncBlockedReason | null;
|
|
13
|
+
retryBlockedReason?: WorkspaceSyncBlockedReason | null;
|
|
10
14
|
}
|
|
11
15
|
export interface HeaderWorkspaceSyncPresentation {
|
|
12
16
|
actionable: boolean;
|
|
@@ -1,12 +1,78 @@
|
|
|
1
|
+
function resolveBlockedReasonPresentation(reason) {
|
|
2
|
+
switch (reason) {
|
|
3
|
+
case 'auth-required':
|
|
4
|
+
return {
|
|
5
|
+
summary: 'Sync is paused until you sign in again.',
|
|
6
|
+
recommendedAction: 'Sign in, then press Sync to retry.'
|
|
7
|
+
};
|
|
8
|
+
case 'attach-cloud':
|
|
9
|
+
return {
|
|
10
|
+
summary: 'This workspace still needs its first cloud pull before local pushes can run.',
|
|
11
|
+
recommendedAction: 'Keep this window open for the first cloud pull. If it seems stuck, press Repair.'
|
|
12
|
+
};
|
|
13
|
+
case 'provision-local':
|
|
14
|
+
return {
|
|
15
|
+
summary: 'This workspace is still doing its first cloud upload before normal pulls can run.',
|
|
16
|
+
recommendedAction: 'Keep this window open until the first cloud upload finishes.'
|
|
17
|
+
};
|
|
18
|
+
case 'repair-active':
|
|
19
|
+
return {
|
|
20
|
+
summary: 'Repair is already running for this workspace.',
|
|
21
|
+
recommendedAction: 'Wait for repair to finish before retrying sync.'
|
|
22
|
+
};
|
|
23
|
+
case 'retry-pending':
|
|
24
|
+
return {
|
|
25
|
+
summary: 'Sync hit a temporary problem and is waiting for its scheduled retry.',
|
|
26
|
+
recommendedAction: 'Wait for the automatic retry, or press Sync to retry now.'
|
|
27
|
+
};
|
|
28
|
+
case 'push-in-flight':
|
|
29
|
+
case 'pull-in-flight':
|
|
30
|
+
return {
|
|
31
|
+
summary: 'A sync request is already running for this workspace.',
|
|
32
|
+
recommendedAction: 'Wait for the current sync request to finish.'
|
|
33
|
+
};
|
|
34
|
+
case 'lease-held':
|
|
35
|
+
return {
|
|
36
|
+
summary: 'Another local window is already syncing this workspace.',
|
|
37
|
+
recommendedAction: 'Use a single local window for sync, or close the other syncing window.'
|
|
38
|
+
};
|
|
39
|
+
case 'invalid-workspace':
|
|
40
|
+
return {
|
|
41
|
+
summary: 'Workspace setup is required before sync can run.',
|
|
42
|
+
recommendedAction: 'Open or create a named workspace before retrying sync.'
|
|
43
|
+
};
|
|
44
|
+
case 'cloud-auth-unconfigured':
|
|
45
|
+
return {
|
|
46
|
+
summary: 'Cloud sync is unavailable because cloud authentication is not configured.',
|
|
47
|
+
recommendedAction: 'Configure cloud authentication before turning on sync.'
|
|
48
|
+
};
|
|
49
|
+
case 'runtime-not-local':
|
|
50
|
+
return {
|
|
51
|
+
summary: 'Workspace sync controls are only available in the local runtime.',
|
|
52
|
+
recommendedAction: 'Open this workspace in the local app to manage sync.'
|
|
53
|
+
};
|
|
54
|
+
case 'sync-disabled':
|
|
55
|
+
return {
|
|
56
|
+
summary: 'Workspace sync is currently turned off.',
|
|
57
|
+
recommendedAction: 'Turn on sync when you are ready to connect this workspace to cloud.'
|
|
58
|
+
};
|
|
59
|
+
default:
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
1
63
|
export function resolveHeaderWorkspaceSyncPresentation(input) {
|
|
2
64
|
const actionable = input.runtimeMode === 'local' && input.isAuthenticated;
|
|
3
65
|
if (actionable) {
|
|
66
|
+
const blockedReasonPresentation = resolveBlockedReasonPresentation(input.retryBlockedReason
|
|
67
|
+
|| input.pullBlockedReason
|
|
68
|
+
|| input.pushBlockedReason
|
|
69
|
+
|| null);
|
|
4
70
|
return {
|
|
5
71
|
actionable,
|
|
6
72
|
status: input.workspaceSyncStatus,
|
|
7
73
|
syncEnabledLabel: input.workspaceCloudSyncEnabled ? 'yes' : 'no',
|
|
8
|
-
summary: input.workspaceSyncSummary,
|
|
9
|
-
recommendedAction: input.workspaceSyncRecommendedAction,
|
|
74
|
+
summary: blockedReasonPresentation?.summary || input.workspaceSyncSummary,
|
|
75
|
+
recommendedAction: blockedReasonPresentation?.recommendedAction || input.workspaceSyncRecommendedAction,
|
|
10
76
|
lastError: input.workspaceSyncError
|
|
11
77
|
};
|
|
12
78
|
}
|
|
@@ -25,7 +25,10 @@ describe('resolveHeaderWorkspaceSyncPresentation', () => {
|
|
|
25
25
|
workspaceCloudSyncEnabled: true,
|
|
26
26
|
workspaceSyncSummary: 'Sync needs attention before it can continue.',
|
|
27
27
|
workspaceSyncRecommendedAction: 'Press Sync to retry.',
|
|
28
|
-
workspaceSyncError: 'Retry later'
|
|
28
|
+
workspaceSyncError: 'Retry later',
|
|
29
|
+
pushBlockedReason: null,
|
|
30
|
+
pullBlockedReason: null,
|
|
31
|
+
retryBlockedReason: null
|
|
29
32
|
});
|
|
30
33
|
expect(result.actionable).toBe(true);
|
|
31
34
|
expect(result.status).toBe('attention');
|
|
@@ -33,4 +36,36 @@ describe('resolveHeaderWorkspaceSyncPresentation', () => {
|
|
|
33
36
|
expect(result.summary).toBe('Sync needs attention before it can continue.');
|
|
34
37
|
expect(result.lastError).toBe('Retry later');
|
|
35
38
|
});
|
|
39
|
+
it('surfaces the highest-priority blocked sync reason in the visible summary', () => {
|
|
40
|
+
const result = resolveHeaderWorkspaceSyncPresentation({
|
|
41
|
+
runtimeMode: 'local',
|
|
42
|
+
isAuthenticated: true,
|
|
43
|
+
workspaceSyncStatus: 'attention',
|
|
44
|
+
workspaceCloudSyncEnabled: true,
|
|
45
|
+
workspaceSyncSummary: 'Sync needs attention before it can continue.',
|
|
46
|
+
workspaceSyncRecommendedAction: 'Press Sync to retry.',
|
|
47
|
+
workspaceSyncError: 'Retry later',
|
|
48
|
+
pushBlockedReason: 'attach-cloud',
|
|
49
|
+
pullBlockedReason: 'retry-pending',
|
|
50
|
+
retryBlockedReason: 'auth-required'
|
|
51
|
+
});
|
|
52
|
+
expect(result.summary).toBe('Sync is paused until you sign in again.');
|
|
53
|
+
expect(result.recommendedAction).toBe('Sign in, then press Sync to retry.');
|
|
54
|
+
});
|
|
55
|
+
it('explains when another local window holds the sync lease', () => {
|
|
56
|
+
const result = resolveHeaderWorkspaceSyncPresentation({
|
|
57
|
+
runtimeMode: 'local',
|
|
58
|
+
isAuthenticated: true,
|
|
59
|
+
workspaceSyncStatus: 'attention',
|
|
60
|
+
workspaceCloudSyncEnabled: true,
|
|
61
|
+
workspaceSyncSummary: 'Sync needs attention before it can continue.',
|
|
62
|
+
workspaceSyncRecommendedAction: 'Press Sync to retry.',
|
|
63
|
+
workspaceSyncError: 'Retry later',
|
|
64
|
+
pushBlockedReason: 'lease-held',
|
|
65
|
+
pullBlockedReason: null,
|
|
66
|
+
retryBlockedReason: null
|
|
67
|
+
});
|
|
68
|
+
expect(result.summary).toBe('Another local window is already syncing this workspace.');
|
|
69
|
+
expect(result.recommendedAction).toBe('Use a single local window for sync, or close the other syncing window.');
|
|
70
|
+
});
|
|
36
71
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taskforcehq/taskforce",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.306",
|
|
4
4
|
"description": "Mission Command center for AI-powered task management (beta - under active development)",
|
|
5
5
|
"author": "Taskforce HQ <hello@taskforcehq.ai> (https://taskforcehq.ai)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"build:ui": "npm run build:app",
|
|
55
55
|
"check:deploy-preflight": "npm run build",
|
|
56
56
|
"hooks:install": "git config core.hooksPath .githooks",
|
|
57
|
-
"build": "shx rm -rf dist && tsc && shx cp src/Taskforce.module.css dist/Taskforce.module.css && shx mkdir -p dist/resources && shx cp -r src/resources/* dist/resources/ && shx rm -f dist/resources/templates/workflows/release.yaml && shx mkdir -p dist/public && shx cp -r src/assets/images/* dist/public/ && shx cp -r src/assets/favicon/* dist/public/ && shx chmod +x dist/cli.js && npm run build:ui",
|
|
57
|
+
"build": "shx rm -rf dist && tsc && shx cp src/Taskforce.module.css dist/Taskforce.module.css && shx mkdir -p dist/styles && shx cp src/styles/fonts.css dist/styles/fonts.css && shx mkdir -p dist/assets/fonts && shx cp src/assets/fonts/*.woff2 dist/assets/fonts/ && shx mkdir -p dist/resources && shx cp -r src/resources/* dist/resources/ && shx rm -f dist/resources/templates/workflows/release.yaml && shx mkdir -p dist/public && shx cp -r src/assets/images/* dist/public/ && shx cp -r src/assets/favicon/* dist/public/ && shx chmod +x dist/cli.js && npm run build:ui",
|
|
58
58
|
"mcp": "node dist/mcp/server.js",
|
|
59
59
|
"gen:workflows": "node scripts/generate-workflow-templates.mjs",
|
|
60
60
|
"check:workflows": "node scripts/generate-workflow-templates.mjs --check",
|
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
"js-yaml": "^4.1.1",
|
|
112
112
|
"nodemailer": "^6.10.1",
|
|
113
113
|
"pg": "^8.18.0",
|
|
114
|
+
"react-avatar-editor": "^15.1.0",
|
|
114
115
|
"rehype-sanitize": "^6.0.0",
|
|
115
116
|
"stripe": "^20.3.1",
|
|
116
117
|
"ws": "^8.18.0"
|