@taskforcehq/taskforce 0.3.304 → 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.
Files changed (383) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +295 -89
  3. package/dist/TaskforceCore.js +378 -111
  4. package/dist/TaskforceCore.test.js +1221 -107
  5. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  8. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  10. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  11. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  12. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  13. package/dist/compat/workspaceSyncCompat.js +52 -2
  14. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  15. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  16. package/dist/components/context/ContextAttachmentManager.js +549 -0
  17. package/dist/components/features/AgentsModule.d.ts +10 -1
  18. package/dist/components/features/AgentsModule.js +260 -15
  19. package/dist/components/features/AgentsModule.test.js +255 -5
  20. package/dist/components/features/DocumentIndex.d.ts +1 -1
  21. package/dist/components/features/DocumentIndex.js +1 -1
  22. package/dist/components/features/DocumentViewer.d.ts +6 -2
  23. package/dist/components/features/DocumentViewer.js +90 -10
  24. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  25. package/dist/components/features/DocumentWorkspace.js +12 -155
  26. package/dist/components/features/DocumentWorkspace.test.js +226 -6
  27. package/dist/components/features/TaskSettings.js +29 -3
  28. package/dist/components/features/TaskSettings.test.js +42 -1
  29. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  30. package/dist/components/features/documentWorkspaceModel.js +57 -0
  31. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  32. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  33. package/dist/components/task/TaskCard.js +19 -28
  34. package/dist/components/task/TaskCard.test.js +38 -3
  35. package/dist/components/task/TaskContextUpload.js +4 -526
  36. package/dist/components/task/TaskForm.d.ts +8 -8
  37. package/dist/components/task/TaskForm.js +362 -230
  38. package/dist/components/task/TaskForm.test.js +558 -381
  39. package/dist/components/task/TaskKanban.d.ts +2 -1
  40. package/dist/components/task/TaskKanban.js +96 -14
  41. package/dist/components/task/TaskKanban.test.js +36 -0
  42. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  43. package/dist/components/ui/EditableAvatarButton.js +18 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  45. package/dist/components/views/PlanComparisonPage.js +70 -17
  46. package/dist/components/views/PlanComparisonPage.test.js +289 -41
  47. package/dist/components/views/PlansPage.js +239 -106
  48. package/dist/components/views/PlansPage.test.js +593 -42
  49. package/dist/components/views/StandaloneLayout.js +310 -554
  50. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  51. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  52. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  53. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  54. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  55. package/dist/components/views/planningScope.d.ts +20 -0
  56. package/dist/components/views/planningScope.js +25 -0
  57. package/dist/components/views/planningScope.test.d.ts +1 -0
  58. package/dist/components/views/planningScope.test.js +60 -0
  59. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +27 -14
  60. package/dist/components/views/standalone/modals/AccountHubModal.js +87 -15
  61. package/dist/components/views/standalone/modals/AccountHubModal.test.js +42 -2
  62. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  63. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  64. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  65. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  66. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  67. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  68. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  69. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  70. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  71. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  72. package/dist/config/envSchema.js +1 -1
  73. package/dist/core/AiProfileService.d.ts +45 -3
  74. package/dist/core/AiProfileService.js +774 -63
  75. package/dist/core/AiProfileService.test.js +22 -2
  76. package/dist/core/AiProfiles.test.js +761 -2
  77. package/dist/core/AttachmentLinkService.js +57 -15
  78. package/dist/core/AuthIdentityService.d.ts +107 -0
  79. package/dist/core/AuthIdentityService.js +284 -0
  80. package/dist/core/AuthTokenService.d.ts +4 -0
  81. package/dist/core/AuthTokenService.js +34 -6
  82. package/dist/core/AuthTokenService.test.d.ts +1 -0
  83. package/dist/core/AuthTokenService.test.js +78 -0
  84. package/dist/core/EntitlementsPolicy.test.js +77 -15
  85. package/dist/core/GlobalSettingsService.js +115 -6
  86. package/dist/core/PlanEntitlementService.d.ts +30 -3
  87. package/dist/core/PlanEntitlementService.js +454 -93
  88. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  89. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  90. package/dist/core/PlanVersionPolicy.test.js +54 -28
  91. package/dist/core/PlanningEntities.test.js +52 -0
  92. package/dist/core/SignupMonetizationSettings.test.js +241 -44
  93. package/dist/core/SyncReconciliationService.js +2 -65
  94. package/dist/core/SystemAdmin.test.js +244 -88
  95. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  96. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  97. package/dist/core/Taskforce.d.ts +228 -14
  98. package/dist/core/Taskforce.js +814 -201
  99. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  100. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  101. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  102. package/dist/core/WorkspaceLifecycleService.js +0 -5
  103. package/dist/core/WorkspacePermissions.test.js +25 -1
  104. package/dist/core/WorkspacePlanMode.test.js +128 -27
  105. package/dist/core/shared.d.ts +3 -1
  106. package/dist/core/shared.js +16 -1
  107. package/dist/core/types.d.ts +68 -2
  108. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  109. package/dist/hooks/sync/auth.js +3 -1
  110. package/dist/hooks/sync/bootstrap.js +2 -0
  111. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  112. package/dist/hooks/sync/controlPlane.js +78 -0
  113. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  114. package/dist/hooks/sync/controlPlane.test.js +121 -0
  115. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  116. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  117. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  118. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  119. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  120. package/dist/hooks/sync/orchestratorShared.js +68 -47
  121. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  122. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  123. package/dist/hooks/sync/pullLifecycle.js +24 -0
  124. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  125. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  126. package/dist/hooks/sync/recovery.d.ts +16 -2
  127. package/dist/hooks/sync/recovery.js +171 -53
  128. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  129. package/dist/hooks/sync/recovery.test.js +292 -0
  130. package/dist/hooks/sync/transfers.d.ts +16 -3
  131. package/dist/hooks/sync/transfers.js +186 -65
  132. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  133. package/dist/hooks/sync/transfers.test.js +396 -0
  134. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  135. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  136. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  137. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  138. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  139. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  140. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  141. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  142. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  143. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  144. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  145. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  146. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  147. package/dist/hooks/useSyncOrchestrator.d.ts +16 -6
  148. package/dist/hooks/useSyncOrchestrator.js +424 -175
  149. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +487 -10
  150. package/dist/hooks/useTaskData.js +8 -3
  151. package/dist/hooks/useTaskData.test.js +45 -0
  152. package/dist/hooks/useTaskMutations.d.ts +2 -1
  153. package/dist/hooks/useTaskMutations.js +12 -1
  154. package/dist/hooks/useTaskMutations.test.js +30 -1
  155. package/dist/hooks/useTaskforce.d.ts +65 -4
  156. package/dist/hooks/useTaskforce.js +564 -243
  157. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  158. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  159. package/dist/hooks/useTaskforce.sync-behavior.test.js +2454 -208
  160. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  161. package/dist/hooks/useWorkspaceSyncController.js +17 -2
  162. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  163. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  164. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  165. package/dist/localization/locales/en-US.d.ts +0 -1
  166. package/dist/localization/locales/en-US.js +0 -1
  167. package/dist/localization/locales/es-419.js +0 -1
  168. package/dist/localization/locales/pt-BR.js +0 -1
  169. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  170. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  171. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  172. package/dist/mcp/collaborationRegistrar.js +71 -0
  173. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  174. package/dist/mcp/documentAssetRegistrar.js +346 -0
  175. package/dist/mcp/runtime.d.ts +2 -0
  176. package/dist/mcp/runtime.js +2433 -3534
  177. package/dist/mcp/runtime.test.js +440 -2
  178. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  179. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  180. package/dist/mcp/toolCatalog.d.ts +35 -0
  181. package/dist/mcp/toolCatalog.js +3 -0
  182. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  183. package/dist/migrations/taskSchemaMigrations.js +171 -61
  184. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  185. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  186. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  187. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  188. package/dist/resources/templates/workflows/plan.yaml +1 -1
  189. package/dist/resources/templates/workflows/review.yaml +2 -2
  190. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  191. package/dist/server/auth/providers/apple.d.ts +33 -0
  192. package/dist/server/auth/providers/apple.js +82 -0
  193. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  194. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  195. package/dist/server/auth/providers/github.d.ts +26 -0
  196. package/dist/server/auth/providers/github.js +86 -0
  197. package/dist/server/auth/providers/google.d.ts +21 -0
  198. package/dist/server/auth/providers/google.js +56 -0
  199. package/dist/server/auth/providers/types.d.ts +21 -0
  200. package/dist/server/auth/providers/types.js +1 -0
  201. package/dist/server/auth.d.ts +2 -0
  202. package/dist/server/auth.js +6 -3
  203. package/dist/server/documentReviewRoutes.test.js +36 -3
  204. package/dist/server/index.cookieProxy.test.js +1 -0
  205. package/dist/server/index.d.ts +12 -0
  206. package/dist/server/index.js +120 -9
  207. package/dist/server/index.rateLimit.test.js +3 -0
  208. package/dist/server/index.test.js +106 -3
  209. package/dist/server/routes/admin.d.ts +4 -5
  210. package/dist/server/routes/admin.js +410 -80
  211. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  212. package/dist/server/routes/annotatedAttachments.js +1 -1
  213. package/dist/server/routes/auth.d.ts +16 -6
  214. package/dist/server/routes/auth.js +1015 -118
  215. package/dist/server/routes/authSupport.d.ts +30 -0
  216. package/dist/server/routes/authSupport.js +81 -0
  217. package/dist/server/routes/billing.d.ts +1 -1
  218. package/dist/server/routes/billing.js +276 -34
  219. package/dist/server/routes/billing.test.js +760 -52
  220. package/dist/server/routes/documentReviews.d.ts +1 -1
  221. package/dist/server/routes/documentReviews.js +1 -1
  222. package/dist/server/routes/documents.d.ts +5 -2
  223. package/dist/server/routes/documents.js +242 -74
  224. package/dist/server/routes/primitives.d.ts +11 -0
  225. package/dist/server/routes/primitives.js +73 -0
  226. package/dist/server/routes/resources.d.ts +1 -1
  227. package/dist/server/routes/resources.js +1 -1
  228. package/dist/server/routes/shared.d.ts +29 -3
  229. package/dist/server/routes/shared.js +69 -4
  230. package/dist/server/routes/sync.d.ts +1 -1
  231. package/dist/server/routes/sync.integration.test.js +437 -39
  232. package/dist/server/routes/sync.js +62 -606
  233. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  234. package/dist/server/routes/syncAuxRoutes.js +91 -0
  235. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  236. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  237. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  238. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  239. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  240. package/dist/server/routes/syncPushRoutes.js +212 -0
  241. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  242. package/dist/server/routes/syncRouteGuards.js +67 -0
  243. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  244. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  245. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  246. package/dist/server/routes/syncRouteTypes.js +1 -0
  247. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  248. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  249. package/dist/server/routes/tasks.d.ts +1 -1
  250. package/dist/server/routes/tasks.js +44 -2
  251. package/dist/server/routes/workspaces.d.ts +1 -1
  252. package/dist/server/routes/workspaces.js +37 -6
  253. package/dist/server/routes.d.ts +2 -9
  254. package/dist/server/routes.js +136 -165
  255. package/dist/server/routes.test.js +2457 -162
  256. package/dist/services/workflowExportSnapshots.test.js +2 -0
  257. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  258. package/dist/shared/aiProfileSeatScope.js +21 -0
  259. package/dist/shared/passwordPolicy.d.ts +13 -0
  260. package/dist/shared/passwordPolicy.js +84 -0
  261. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  262. package/dist/shared/passwordPolicy.test.js +34 -0
  263. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  264. package/dist/storage/workspaceAssetStore.js +132 -44
  265. package/dist/storage/workspaceAssetStore.test.js +93 -6
  266. package/dist/styles/fonts.css +70 -0
  267. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  268. package/dist/sync/collaborationSyncPayload.js +17 -11
  269. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  270. package/dist/sync/contentSyncState.d.ts +4 -8
  271. package/dist/sync/contentSyncState.js +33 -77
  272. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  273. package/dist/sync/syncApplyHandlers.js +28 -13
  274. package/dist/sync/syncApplyHandlers.test.js +72 -3
  275. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  276. package/dist/sync/syncFieldSemantics.js +55 -0
  277. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  278. package/dist/sync/syncResourceAdapters.js +77 -0
  279. package/dist/sync/syncService.d.ts +6 -0
  280. package/dist/sync/syncService.js +60 -0
  281. package/dist/sync/syncService.test.js +71 -3
  282. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  283. package/dist/sync/taskSyncChangeKey.js +143 -0
  284. package/dist/sync/taskSyncPayload.js +3 -8
  285. package/dist/sync/workspacePullFeed.js +4 -3
  286. package/dist/sync/workspacePullFeed.test.js +43 -0
  287. package/dist/sync/workspaceRepair.js +5 -2
  288. package/dist/sync/workspaceSyncModel.d.ts +48 -0
  289. package/dist/sync/workspaceSyncModel.js +195 -31
  290. package/dist/sync/workspaceSyncModel.test.js +325 -12
  291. package/dist/sync/workspaceSyncState.d.ts +19 -0
  292. package/dist/sync/workspaceSyncState.js +3 -1
  293. package/dist/sync/workspaceSyncSurface.js +3 -8
  294. package/dist/types.d.ts +17 -0
  295. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  296. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  297. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  298. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  299. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  300. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  301. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  302. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  303. package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-9E6ParrY.js} +1 -1
  304. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  305. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  306. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  307. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  308. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  309. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  310. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +9 -0
  311. package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  312. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  313. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  314. package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-DuEd_65c.js} +1 -1
  315. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  316. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  317. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  318. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  319. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  320. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  321. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  322. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  323. package/dist/ui/fonts/Noir_medium.otf +0 -0
  324. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  325. package/dist/ui/fonts/Noir_regular.otf +0 -0
  326. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  327. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  328. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  329. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  330. package/dist/ui/index.html +3 -3
  331. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  332. package/dist/utils/assignees.d.ts +2 -0
  333. package/dist/utils/assignees.js +2 -0
  334. package/dist/utils/avatarUpload.d.ts +27 -0
  335. package/dist/utils/avatarUpload.js +132 -0
  336. package/dist/utils/avatarUpload.test.d.ts +1 -0
  337. package/dist/utils/avatarUpload.test.js +40 -0
  338. package/dist/utils/bootstrapTrace.d.ts +7 -0
  339. package/dist/utils/bootstrapTrace.js +29 -0
  340. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  341. package/dist/utils/commercialLifecyclePresentation.js +199 -0
  342. package/dist/utils/contextAssetEvents.d.ts +2 -0
  343. package/dist/utils/syncEventDetails.d.ts +3 -0
  344. package/dist/utils/syncEventDetails.js +64 -0
  345. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  346. package/dist/utils/syncEventDetails.test.js +23 -0
  347. package/dist/utils/syncEventPresentation.d.ts +15 -0
  348. package/dist/utils/syncEventPresentation.js +129 -0
  349. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  350. package/dist/utils/syncEventPresentation.test.js +64 -0
  351. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  352. package/dist/utils/syncStatusPresentation.js +217 -0
  353. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  354. package/dist/utils/syncStatusPresentation.test.js +164 -0
  355. package/dist/utils/taskActivity.d.ts +2 -0
  356. package/dist/utils/taskActivity.js +228 -22
  357. package/dist/utils/taskActivity.test.js +119 -8
  358. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  359. package/dist/utils/workspaceSyncPresentation.js +68 -2
  360. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  361. package/package.json +10 -2
  362. package/scripts/check-cloud-mcp.mjs +83 -0
  363. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  364. package/scripts/playwright-auth.sh +5 -1
  365. package/scripts/run-billing-performance-smoke.sh +24 -0
  366. package/scripts/run-e2e-realtime.sh +17 -3
  367. package/scripts/run-smoke.sh +17 -5
  368. package/scripts/run-soak.sh +17 -5
  369. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  370. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  371. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  372. package/src/resources/templates/workflows/plan.yaml +1 -1
  373. package/src/resources/templates/workflows/review.yaml +2 -2
  374. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +0 -1
  375. package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
  376. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  377. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  378. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
  379. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  380. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
  381. package/dist/ui/assets/index-DXUdtH1B.js +0 -6
  382. package/dist/ui/assets/index-Ii0B0rTO.css +0 -1
  383. package/scripts/migrate-planning-model.ts +0 -233
@@ -0,0 +1,164 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildCopySyncDetailsText, buildReferenceMismatchSummaries, buildSyncDiagnosticsSummary, buildSyncEventRows, getActiveReferenceMismatchCount, resolveSyncStageLabel, resolveSyncStatusMeta, } from './syncStatusPresentation';
3
+ describe('syncStatusPresentation', () => {
4
+ it('resolves sync status badges for visible header states', () => {
5
+ expect(resolveSyncStatusMeta('healthy')).toMatchObject({
6
+ label: 'Healthy',
7
+ icon: 'cloud',
8
+ });
9
+ expect(resolveSyncStatusMeta('attention')).toMatchObject({
10
+ label: 'Needs Attention',
11
+ icon: 'cloud',
12
+ });
13
+ expect(resolveSyncStatusMeta('off')).toMatchObject({
14
+ label: 'Off',
15
+ icon: 'off',
16
+ });
17
+ });
18
+ it('describes startup and repair stages for the sync manager', () => {
19
+ expect(resolveSyncStageLabel('attach-cloud', false, true)).toBe('Initial cloud pull');
20
+ expect(resolveSyncStageLabel('error', true, true)).toBe('Repairing');
21
+ expect(resolveSyncStageLabel('error', false, true)).toBe('Recovering');
22
+ });
23
+ it('builds the diagnostics summary from sync snapshot counts', () => {
24
+ const summary = buildSyncDiagnosticsSummary({
25
+ aiProfileSnapshotCount: 2,
26
+ aiProfileSnapshotRawCount: 4,
27
+ lastPushedAiProfileCount: 3,
28
+ lastPushedAiProfileWatermarkCount: 5,
29
+ documentSnapshotCount: 1,
30
+ assetSnapshotCount: 2,
31
+ forceFullAiProfilePushQueued: true,
32
+ aiProfileSnapshotLastFetchAt: null,
33
+ aiProfileSnapshotLastFetchError: '',
34
+ aiProfileSnapshotLastSkipReason: null,
35
+ });
36
+ expect(summary).toContainEqual({
37
+ label: 'AI profile snapshot',
38
+ value: '2 local profiles',
39
+ });
40
+ expect(summary).toContainEqual({
41
+ label: 'Document snapshot',
42
+ value: '1 local doc',
43
+ });
44
+ expect(summary).toContainEqual({
45
+ label: 'Queued full AI sync',
46
+ value: 'Yes',
47
+ });
48
+ });
49
+ it('keeps only active reference mismatches until the next successful sync event', () => {
50
+ const events = [
51
+ {
52
+ occurredAt: '2026-04-18T10:00:00.000Z',
53
+ status: 'error',
54
+ errorMessage: 'Detected task reference number mismatch during normal sync.',
55
+ details: {
56
+ path: 'docs/spec.md',
57
+ existingReferenceNumber: 14,
58
+ incomingReferenceNumber: 8,
59
+ },
60
+ },
61
+ {
62
+ occurredAt: '2026-04-18T09:59:00.000Z',
63
+ status: 'success',
64
+ errorMessage: '',
65
+ },
66
+ {
67
+ occurredAt: '2026-04-18T09:58:00.000Z',
68
+ status: 'error',
69
+ errorMessage: 'Detected task reference number mismatch during normal sync.',
70
+ },
71
+ ];
72
+ expect(getActiveReferenceMismatchCount(events)).toBe(1);
73
+ expect(buildReferenceMismatchSummaries(events)).toEqual([
74
+ expect.objectContaining({
75
+ pathLabel: 'docs/spec.md',
76
+ refsLabel: 'Existing 14 vs incoming 8',
77
+ }),
78
+ ]);
79
+ });
80
+ it('builds readable sync event rows for loading, errors, and structured events', () => {
81
+ expect(buildSyncEventRows({
82
+ syncRecentEvents: [],
83
+ syncRecentEventsError: null,
84
+ syncRecentEventsLoading: true,
85
+ })).toEqual([{
86
+ key: 'loading',
87
+ text: 'Loading recent sync events...',
88
+ tone: 'muted',
89
+ }]);
90
+ expect(buildSyncEventRows({
91
+ syncRecentEvents: [],
92
+ syncRecentEventsError: 'Unable to load events.',
93
+ syncRecentEventsLoading: false,
94
+ })).toEqual([{
95
+ key: 'error',
96
+ text: 'Unable to load events.',
97
+ tone: 'error',
98
+ }]);
99
+ expect(buildSyncEventRows({
100
+ syncRecentEvents: [{
101
+ occurredAt: '2026-04-18T10:00:00.000Z',
102
+ status: 'error',
103
+ details: {
104
+ source: 'retry.blocked',
105
+ reason: 'lease-held',
106
+ phase: 'active',
107
+ },
108
+ }],
109
+ syncRecentEventsError: null,
110
+ syncRecentEventsLoading: false,
111
+ })[0]).toMatchObject({
112
+ tone: 'error',
113
+ text: '2026-04-18T10:00:00.000Z manual retry blocked: another window is already syncing during active sync',
114
+ });
115
+ });
116
+ it('assembles the sync manager report text from the current view model', () => {
117
+ const report = buildCopySyncDetailsText({
118
+ currentWorkspaceId: 'workspace-123',
119
+ syncStatusLabel: 'Needs Attention',
120
+ workspaceSyncSummary: 'Sync is paused until you sign in again.',
121
+ workspaceSyncRecommendedAction: 'Sign in, then press Sync to retry.',
122
+ formattedLastSyncTime: '4/17/2026, 10:32:47 PM',
123
+ formattedLastPullTime: '4/17/2026, 10:32:47 PM',
124
+ formattedLastPushTime: '4/17/2026, 10:31:45 PM',
125
+ workspaceSyncDiagnostics: {
126
+ lastErrorAt: '2026-04-18T01:33:05.980Z',
127
+ pushBlockedReason: 'attach-cloud',
128
+ pullBlockedReason: null,
129
+ retryBlockedReason: 'auth-required',
130
+ repairActive: false,
131
+ pushInFlight: false,
132
+ pullInFlight: true,
133
+ retryPending: false,
134
+ aiProfileSnapshotCount: 14,
135
+ aiProfileSnapshotRawCount: 14,
136
+ lastPushedAiProfileCount: 14,
137
+ lastPushedAiProfileWatermarkCount: 14,
138
+ aiProfileSnapshotLastFetchAt: '2026-04-18T11:15:52.000Z',
139
+ aiProfileSnapshotLastFetchError: null,
140
+ aiProfileSnapshotLastSkipReason: null,
141
+ documentSnapshotCount: 127,
142
+ assetSnapshotCount: 56,
143
+ forceFullAiProfilePushQueued: false,
144
+ },
145
+ workspaceSyncPendingChanges: 31,
146
+ syncLastError: 'Detected task reference number mismatch during normal sync.',
147
+ syncStageLabel: 'Active',
148
+ referenceMismatchCount: 3,
149
+ syncRecentEvents: [{
150
+ occurredAt: '2026-04-18T01:33:06.044Z',
151
+ eventType: 'apply',
152
+ status: 'success',
153
+ changeCount: 200,
154
+ requestMs: 167,
155
+ statusCode: 0,
156
+ }],
157
+ });
158
+ expect(report).toContain('Workspace ID: workspace-123');
159
+ expect(report).toContain('Push blocked reason: attach-cloud');
160
+ expect(report).toContain('Retry blocked reason: auth-required');
161
+ expect(report).toContain('Reference mismatches detected: 3');
162
+ expect(report).toContain('2026-04-18T01:33:06.044Z apply succeeded (200 changes) in 167ms [0]');
163
+ });
164
+ });
@@ -5,5 +5,7 @@ export declare function summarizeTaskChange(field: string, change: {
5
5
  to?: unknown;
6
6
  } | undefined, formatter?: TaskEventValueFormatter): string;
7
7
  export declare function summarizeTaskEvent(event: TaskEvent, formatter?: TaskEventValueFormatter): string;
8
+ export declare function getTaskEventPrimaryChangeField(event: TaskEvent): string | null;
9
+ export declare function getTaskActivityItems(task: Pick<TaskItem, 'activity' | 'comments'>): TaskActivityItem[];
8
10
  export declare function getLatestTaskActivity(task: Pick<TaskItem, 'activity' | 'comments'>): TaskActivityItem | null;
9
11
  export declare function summarizeTaskActivity(item: TaskActivityItem | null, formatter?: TaskEventValueFormatter): string;
@@ -1,16 +1,31 @@
1
1
  function getActivityFieldLabel(field) {
2
2
  if (field === 'title')
3
- return 'TITLE';
3
+ return 'Title';
4
4
  if (field === 'description')
5
- return 'DESCRIPTION';
5
+ return 'Description';
6
6
  if (field === 'assignee')
7
- return 'ASSIGNED TO';
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 'WORKSTREAM';
15
+ return 'Workstream';
10
16
  if (field === 'initiativeId')
11
- return 'INITIATIVE';
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
- export function getLatestTaskActivity(task) {
137
- const items = Array.isArray(task.activity) && task.activity.length > 0
138
- ? task.activity
139
- : Array.isArray(task.comments)
140
- ? task.comments.map((comment) => ({
141
- id: `comment:${comment.id}`,
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 = Date.parse(String(left.timestamp || ''));
151
- const rightTs = Date.parse(String(right.timestamp || ''));
152
- if (Number.isFinite(leftTs) && Number.isFinite(rightTs) && leftTs !== rightTs) {
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('TITLE: updated');
8
+ })).toBe('Title: updated');
9
9
  });
10
- it('summarizes assignee changes with uppercase labels and formatted values', () => {
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('ASSIGNED TO: Unassigned -> Emma Peel');
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('DESCRIPTION: added');
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('DESCRIPTION: cleared');
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('WORKSTREAM: none -> WS-12');
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;