@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
@@ -1,168 +1,25 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect, useCallback } from 'react';
3
2
  import styles from '../../Taskforce.module.css';
4
3
  import { DocumentIndex } from './DocumentIndex';
5
4
  import { DocumentViewer } from './DocumentViewer';
6
- import { fetchTaskforceApi } from '../../utils/taskforceApiClient';
7
- import { logDocumentManagerDebug, resolveDocumentManagerApiBase } from '../../utils/documentManagerApi';
8
- import { getReadableDocumentName } from '../../utils/documentNames';
9
- import { DOCUMENT_ATTACHMENT_FILTER_OPTIONS, DOCUMENT_TYPE_FILTER_OPTIONS, } from './documentWorkspaceFilters';
5
+ import { useDocumentWorkspaceController } from './useDocumentWorkspaceController';
10
6
  export { DOCUMENT_TYPE_FILTER_OPTIONS, DOCUMENT_ATTACHMENT_FILTER_OPTIONS } from './documentWorkspaceFilters';
11
- function normalizeDocPath(value) {
12
- let decoded = value;
13
- try {
14
- decoded = decodeURIComponent(value);
15
- }
16
- catch {
17
- decoded = value;
18
- }
19
- const normalized = decoded
20
- .trim()
21
- .replace(/\\/g, '/')
22
- .replace(/^\/+/, '')
23
- .replace(/^\.\//, '')
24
- .replace(/^\.taskforce\//, '');
25
- return normalized;
26
- }
27
- function docPathMatches(candidate, requested) {
28
- if (candidate === requested)
29
- return true;
30
- if (candidate.endsWith(`/${requested}`))
31
- return true;
32
- if (requested.endsWith(`/${candidate}`))
33
- return true;
34
- return false;
35
- }
36
- function extractAssetIdFromPath(value) {
37
- const normalized = normalizeDocPath(String(value || ''));
38
- if (!normalized)
39
- return null;
40
- const match = normalized.match(/(^|\/)(asset-[a-f0-9]{32})(?=[^/]*$)/i);
41
- return match?.[2] ? match[2].toLowerCase() : null;
42
- }
43
- export function getEffectiveAttachmentState(doc) {
44
- if (doc.attachmentState === 'attached' || doc.attachmentState === 'unattached') {
45
- return doc.attachmentState;
46
- }
47
- const attachmentCount = typeof doc.attachmentCount === 'number' && Number.isFinite(doc.attachmentCount)
48
- ? doc.attachmentCount
49
- : 0;
50
- if (attachmentCount > 0 || !!doc.taskId) {
51
- return 'attached';
52
- }
53
- return 'unattached';
54
- }
55
- export function DocumentWorkspace({ tasks, apiBaseUrl = '', documents, loading, error, selectedDoc, searchQuery, typeFilters, attachmentFilters, onSearchChange, onSelectDoc, onRefresh, enableTaskGeneration = false }) {
56
- const filteredDocs = documents.filter(doc => {
57
- const readableName = getReadableDocumentName(doc);
58
- const haystack = `${readableName} ${doc.originalFilename || ''}`.toLowerCase();
59
- const matchesSearch = !searchQuery || haystack.includes(searchQuery.toLowerCase());
60
- const matchesType = typeFilters.length === 0 || typeFilters.includes(doc.docType);
61
- const attachmentState = getEffectiveAttachmentState(doc);
62
- const matchesAttachment = attachmentFilters.length === 0 || attachmentFilters.includes(attachmentState);
63
- return matchesSearch && matchesType && matchesAttachment;
64
- });
65
- const hasActiveFilters = searchQuery.trim().length > 0 || typeFilters.length > 0 || attachmentFilters.length > 0;
7
+ export function DocumentWorkspace({ tasks, apiBaseUrl = '', documents, loading, error, selectedDoc, searchQuery, hasActiveFilters, onSearchChange, onSelectDoc, onRefresh, onDeleteSelected, enableTaskGeneration = false }) {
66
8
  const taskById = new Map(tasks.map(t => [t.id, t]));
67
- return (_jsxs("div", { className: styles.docWorkspace, children: [_jsx("div", { className: styles.docIndexPanel, children: _jsx(DocumentIndex, { documents: filteredDocs, selectedDocId: selectedDoc?.id ?? null, loading: loading, error: error, searchQuery: searchQuery, onSearchChange: onSearchChange, onSelect: (doc) => onSelectDoc(doc), onRefresh: onRefresh, taskById: taskById, hasActiveFilters: hasActiveFilters }) }), _jsx("div", { className: styles.docViewerPanel, children: selectedDoc ? (_jsx(DocumentViewer, { doc: selectedDoc, taskTitle: selectedDoc.taskId ? (taskById.get(selectedDoc.taskId)?.title ?? null) : null, apiBaseUrl: apiBaseUrl, onSaved: (_updatedAt) => onRefresh(), onDeleted: () => {
68
- onSelectDoc(null);
69
- void onRefresh();
70
- }, enableTaskGeneration: enableTaskGeneration })) : !loading && (_jsx("div", { className: styles.docViewerEmpty, children: _jsx("p", { className: "tf-empty-copy", children: filteredDocs.length === 0 ? 'No documents match your current filters.' : 'Select a document to view it.' }) })) })] }));
9
+ return (_jsxs("div", { className: styles.docWorkspace, children: [_jsx("div", { className: styles.docIndexPanel, children: _jsx(DocumentIndex, { documents: documents, selectedDocId: selectedDoc?.id ?? null, loading: loading, error: error, searchQuery: searchQuery, onSearchChange: onSearchChange, onSelect: (doc) => onSelectDoc(doc), onRefresh: onRefresh, taskById: taskById, hasActiveFilters: hasActiveFilters }) }), _jsx("div", { className: styles.docViewerPanel, children: selectedDoc ? (_jsx(DocumentViewer, { doc: selectedDoc, taskTitle: selectedDoc.taskId ? (taskById.get(selectedDoc.taskId)?.title ?? null) : null, tasks: tasks, apiBaseUrl: apiBaseUrl, onSaved: (_updatedAt) => onRefresh(), onDeleted: () => void onDeleteSelected(), enableTaskGeneration: enableTaskGeneration })) : !loading && (_jsx("div", { className: styles.docViewerEmpty, children: _jsx("p", { className: "tf-empty-copy", children: documents.length === 0 ? 'No documents match your current filters.' : 'Select a document to view it.' }) })) })] }));
71
10
  }
72
11
  export function DocumentWorkspaceShell({ tasks, runtimeMode = 'local', apiBaseUrl = '', cloudAuthBaseUrl = '', typeFilters: controlledTypeFilters, attachmentFilters: controlledAttachmentFilters, onTypeFiltersChange, onAttachmentFiltersChange, requestedDocPath = null, requestedDocAssetId = null, onRequestedDocHandled, enableTaskGeneration = false }) {
73
- const [documents, setDocuments] = useState([]);
74
- const [loading, setLoading] = useState(true);
75
- const [error, setError] = useState(null);
76
- const [selectedDoc, setSelectedDoc] = useState(null);
77
- const [searchQuery, setSearchQuery] = useState('');
78
- const [internalTypeFilters, setInternalTypeFilters] = useState(() => DOCUMENT_TYPE_FILTER_OPTIONS.map((option) => option.value));
79
- const [internalAttachmentFilters, setInternalAttachmentFilters] = useState(() => DOCUMENT_ATTACHMENT_FILTER_OPTIONS.map((option) => option.value));
80
- const documentApiBaseUrl = resolveDocumentManagerApiBase({
12
+ const { documentApiBaseUrl, filteredDocs, loading, error, selectedDoc, searchQuery, hasActiveFilters, setSearchQuery, fetchDocuments, selectDoc, handleDeleteSelected, } = useDocumentWorkspaceController({
81
13
  runtimeMode,
82
14
  apiBaseUrl,
83
15
  cloudAuthBaseUrl,
16
+ typeFilters: controlledTypeFilters,
17
+ attachmentFilters: controlledAttachmentFilters,
18
+ onTypeFiltersChange,
19
+ onAttachmentFiltersChange,
20
+ requestedDocPath,
21
+ requestedDocAssetId,
22
+ onRequestedDocHandled,
84
23
  });
85
- const typeFilters = controlledTypeFilters ?? internalTypeFilters;
86
- const attachmentFilters = controlledAttachmentFilters ?? internalAttachmentFilters;
87
- const handleTypeFiltersChange = onTypeFiltersChange ?? setInternalTypeFilters;
88
- const handleAttachmentFiltersChange = onAttachmentFiltersChange ?? setInternalAttachmentFilters;
89
- const fetchDocuments = useCallback(async () => {
90
- setLoading(true);
91
- setError(null);
92
- try {
93
- const res = await fetchTaskforceApi('/api/taskforce/documents', {
94
- credentials: 'include',
95
- }, documentApiBaseUrl);
96
- if (!res.ok)
97
- throw new Error(`Failed to load documents (${res.status})`);
98
- const data = await res.json();
99
- const nextDocuments = data.documents || [];
100
- logDocumentManagerDebug('list.loaded', {
101
- runtimeMode,
102
- apiBaseUrl: documentApiBaseUrl || '(same-origin)',
103
- count: Array.isArray(nextDocuments) ? nextDocuments.length : 0,
104
- });
105
- setDocuments(nextDocuments);
106
- }
107
- catch (e) {
108
- setError(e?.message || 'Failed to load documents');
109
- logDocumentManagerDebug('list.error', {
110
- runtimeMode,
111
- apiBaseUrl: documentApiBaseUrl || '(same-origin)',
112
- message: e?.message || 'Failed to load documents',
113
- });
114
- }
115
- finally {
116
- setLoading(false);
117
- }
118
- }, [documentApiBaseUrl, runtimeMode]);
119
- useEffect(() => {
120
- fetchDocuments();
121
- }, [fetchDocuments]);
122
- // Auto-select first doc when list loads
123
- useEffect(() => {
124
- if (requestedDocPath || requestedDocAssetId)
125
- return;
126
- if (!selectedDoc && documents.length > 0) {
127
- setSelectedDoc(documents[0]);
128
- }
129
- }, [documents, selectedDoc, requestedDocAssetId, requestedDocPath]);
130
- useEffect(() => {
131
- if ((!requestedDocPath && !requestedDocAssetId) || documents.length === 0)
132
- return;
133
- const requested = requestedDocPath ? normalizeDocPath(requestedDocPath) : '';
134
- const requestedAssetId = String(requestedDocAssetId || '').trim().toLowerCase()
135
- || extractAssetIdFromPath(requestedDocPath);
136
- const matched = documents.find((doc) => {
137
- const candidateAssetId = extractAssetIdFromPath(doc.fsPath) || String(doc.assetId || '').trim().toLowerCase() || null;
138
- if (requestedAssetId && candidateAssetId && candidateAssetId === requestedAssetId)
139
- return true;
140
- if (!requested)
141
- return false;
142
- const candidate = normalizeDocPath(doc.fsPath);
143
- return docPathMatches(candidate, requested);
144
- });
145
- if (matched) {
146
- setSelectedDoc(matched);
147
- onRequestedDocHandled?.();
148
- return;
149
- }
150
- }, [requestedDocAssetId, requestedDocPath, documents, onRequestedDocHandled]);
151
- useEffect(() => {
152
- if ((requestedDocPath || requestedDocAssetId) && !selectedDoc)
153
- return;
154
- const filtered = documents.filter((doc) => {
155
- const readableName = getReadableDocumentName(doc);
156
- const haystack = `${readableName} ${doc.originalFilename || ''}`.toLowerCase();
157
- const matchesSearch = !searchQuery || haystack.includes(searchQuery.toLowerCase());
158
- const matchesType = typeFilters.length === 0 || typeFilters.includes(doc.docType);
159
- const attachmentState = getEffectiveAttachmentState(doc);
160
- const matchesAttachment = attachmentFilters.length === 0 || attachmentFilters.includes(attachmentState);
161
- return matchesSearch && matchesType && matchesAttachment;
162
- });
163
- if (selectedDoc && filtered.some((doc) => doc.id === selectedDoc.id))
164
- return;
165
- setSelectedDoc(filtered[0] ?? null);
166
- }, [attachmentFilters, documents, requestedDocAssetId, requestedDocPath, searchQuery, selectedDoc, typeFilters]);
167
- return (_jsx(DocumentWorkspace, { tasks: tasks, apiBaseUrl: documentApiBaseUrl, documents: documents, loading: loading, error: error, selectedDoc: selectedDoc, searchQuery: searchQuery, typeFilters: typeFilters, attachmentFilters: attachmentFilters, onSearchChange: setSearchQuery, onSelectDoc: setSelectedDoc, onRefresh: fetchDocuments, enableTaskGeneration: enableTaskGeneration }));
24
+ return (_jsx(DocumentWorkspace, { tasks: tasks, apiBaseUrl: documentApiBaseUrl, documents: filteredDocs, loading: loading, error: error, selectedDoc: selectedDoc, searchQuery: searchQuery, hasActiveFilters: hasActiveFilters, onSearchChange: setSearchQuery, onSelectDoc: selectDoc, onRefresh: fetchDocuments, onDeleteSelected: handleDeleteSelected, enableTaskGeneration: enableTaskGeneration }));
168
25
  }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
@@ -44,6 +44,9 @@ function createEmptyReviewSessionsResponse() {
44
44
  json: async () => ({ sessions: [] }),
45
45
  };
46
46
  }
47
+ function renderDocumentWorkspace(props) {
48
+ return render(_jsx(DocumentWorkspace, { tasks: props.tasks ?? [], documents: props.documents, loading: false, error: null, selectedDoc: props.selectedDoc ?? null, searchQuery: props.searchQuery ?? '', hasActiveFilters: props.hasActiveFilters ?? false, onSearchChange: props.onSearchChange ?? (() => { }), onSelectDoc: props.onSelectDoc ?? (() => { }), onRefresh: props.onRefresh ?? (() => { }), onDeleteSelected: props.onDeleteSelected ?? (() => { }) }));
49
+ }
47
50
  describe('Document workspace management', () => {
48
51
  beforeEach(() => {
49
52
  fetchTaskforceApi.mockReset();
@@ -87,7 +90,7 @@ describe('Document workspace management', () => {
87
90
  ];
88
91
  function Harness() {
89
92
  const [selectedDoc, setSelectedDoc] = useState(null);
90
- return (_jsx(DocumentWorkspace, { tasks: [{ id: 'task-1', title: 'Parent task' }], documents: docs, loading: false, error: null, selectedDoc: selectedDoc, searchQuery: "", typeFilters: [], attachmentFilters: ['unattached'], onSearchChange: () => { }, onSelectDoc: setSelectedDoc, onRefresh: () => { } }));
93
+ return (_jsx(DocumentWorkspace, { tasks: [{ id: 'task-1', title: 'Parent task' }], documents: docs.filter((doc) => doc.attachmentState === 'unattached'), loading: false, error: null, selectedDoc: selectedDoc, searchQuery: "", hasActiveFilters: true, onSearchChange: () => { }, onSelectDoc: setSelectedDoc, onRefresh: () => { }, onDeleteSelected: () => { } }));
91
94
  }
92
95
  render(_jsx(Harness, {}));
93
96
  expect(screen.getByRole('button', { name: /Orphan notes/i })).toBeInTheDocument();
@@ -95,13 +98,13 @@ describe('Document workspace management', () => {
95
98
  expect(screen.getByText('Unattached')).toBeInTheDocument();
96
99
  });
97
100
  it('keeps document search in the index without duplicate filter controls', () => {
98
- render(_jsx(DocumentWorkspace, { tasks: [], documents: [createDoc()], loading: false, error: null, selectedDoc: null, searchQuery: "", typeFilters: [], attachmentFilters: [], onSearchChange: () => { }, onSelectDoc: () => { }, onRefresh: () => { } }));
101
+ renderDocumentWorkspace({ documents: [createDoc()] });
99
102
  expect(screen.getByPlaceholderText('Search documents…')).toBeInTheDocument();
100
103
  expect(screen.queryByRole('button', { name: 'Attached' })).not.toBeInTheDocument();
101
104
  expect(screen.queryByRole('button', { name: 'Plans' })).not.toBeInTheDocument();
102
105
  });
103
106
  it('shows the document reference badge in the bottom metadata row of index cards', () => {
104
- render(_jsx(DocumentWorkspace, { tasks: [], documents: [createDoc({ referenceLabel: 'D-42' })], loading: false, error: null, selectedDoc: null, searchQuery: "", typeFilters: [], attachmentFilters: [], onSearchChange: () => { }, onSelectDoc: () => { }, onRefresh: () => { } }));
107
+ renderDocumentWorkspace({ documents: [createDoc({ referenceLabel: 'D-42' })] });
105
108
  expect(screen.getByText('D-42')).toBeInTheDocument();
106
109
  expect(screen.getByText(/64B/)).toBeInTheDocument();
107
110
  });
@@ -142,6 +145,82 @@ describe('Document workspace management', () => {
142
145
  confirmSpy.mockRestore();
143
146
  alertSpy.mockRestore();
144
147
  });
148
+ it('opens an attach modal from the document viewer and attaches the document to a task', async () => {
149
+ const user = userEvent.setup();
150
+ fetchTaskforceApi
151
+ .mockResolvedValueOnce({
152
+ ok: true,
153
+ text: async () => '# Example\n',
154
+ })
155
+ .mockResolvedValueOnce(createEmptyReviewSessionsResponse())
156
+ .mockResolvedValueOnce({
157
+ ok: true,
158
+ json: async () => ({ success: true, attached: true, alreadyAttached: false }),
159
+ });
160
+ const onSaved = vi.fn();
161
+ render(_jsx(DocumentViewer, { doc: createDoc({
162
+ id: 'doc-attach',
163
+ assetId: 'asset-doc-attach',
164
+ attachmentCount: 0,
165
+ attachmentState: 'unattached',
166
+ }), taskTitle: null, tasks: [
167
+ { id: 'task-1', title: 'First attach target' },
168
+ { id: 'task-2', title: 'Second attach target' },
169
+ ], onSaved: onSaved }));
170
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenCalledWith('/api/taskforce/documents/asset-doc-1/content', { credentials: 'same-origin' }, ''));
171
+ await user.click(screen.getByRole('button', { name: /^Attach$/i }));
172
+ expect(screen.getByText(/Attach this document to a task from the document manager/i)).toBeInTheDocument();
173
+ await user.type(screen.getByPlaceholderText('Search tasks…'), 'Second');
174
+ await user.click(screen.getByRole('button', { name: /Second attach target/i }));
175
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenLastCalledWith('/api/taskforce/documents/asset-doc-attach/attach', expect.objectContaining({
176
+ method: 'POST',
177
+ credentials: 'include',
178
+ }), ''));
179
+ expect(onSaved).toHaveBeenCalled();
180
+ });
181
+ it('shows delete and unlink for attached docs and sends forceDetach on delete', async () => {
182
+ const user = userEvent.setup();
183
+ fetchTaskforceApi
184
+ .mockResolvedValueOnce({
185
+ ok: true,
186
+ text: async () => '# Example\n',
187
+ })
188
+ .mockResolvedValueOnce(createEmptyReviewSessionsResponse())
189
+ .mockResolvedValueOnce({
190
+ ok: true,
191
+ json: async () => ({ success: true, softDeleted: true, detachedTaskCount: 2 }),
192
+ });
193
+ const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
194
+ const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => { });
195
+ const onDeleted = vi.fn();
196
+ render(_jsx(DocumentViewer, { doc: {
197
+ id: 'doc-attached',
198
+ assetId: 'asset-doc-attached',
199
+ fsPath: 'workspaces/default/assets/documents/doc-attached.md',
200
+ apiUrl: '/api/taskforce/documents/asset-doc-attached/content',
201
+ taskId: 'task-1',
202
+ displayName: 'Attached spec',
203
+ title: 'Attached spec',
204
+ docType: 'other',
205
+ sizeBytes: 64,
206
+ updatedAt: '2026-03-19T11:00:00.000Z',
207
+ editable: true,
208
+ attachmentCount: 2,
209
+ attachmentState: 'attached',
210
+ }, taskTitle: null, onDeleted: onDeleted }));
211
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenCalledWith('/api/taskforce/documents/asset-doc-attached/content', { credentials: 'same-origin' }, ''));
212
+ await user.click(screen.getByRole('button', { name: /Delete & Unlink/i }));
213
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenLastCalledWith('/api/taskforce/documents/asset-doc-attached', expect.objectContaining({
214
+ method: 'DELETE',
215
+ credentials: 'include',
216
+ headers: { 'Content-Type': 'application/json' },
217
+ body: JSON.stringify({ forceDetach: true })
218
+ }), ''));
219
+ expect(confirmSpy).toHaveBeenCalledWith(expect.stringContaining('remove it from 2 linked tasks'));
220
+ expect(onDeleted).toHaveBeenCalled();
221
+ confirmSpy.mockRestore();
222
+ alertSpy.mockRestore();
223
+ });
145
224
  it('uses same-origin document routes in local runtime even when an api base is configured', async () => {
146
225
  const user = userEvent.setup();
147
226
  fetchTaskforceApi
@@ -226,6 +305,46 @@ describe('Document workspace management', () => {
226
305
  expect(await screen.findByText('Please tighten the rollout section.')).toBeInTheDocument();
227
306
  expect(screen.getByText('Lines 18-24')).toBeInTheDocument();
228
307
  });
308
+ it('toggles the review sidebar from the document header', async () => {
309
+ const user = userEvent.setup();
310
+ fetchTaskforceApi
311
+ .mockResolvedValueOnce({
312
+ ok: true,
313
+ text: async () => '# Example\n',
314
+ })
315
+ .mockResolvedValueOnce({
316
+ ok: true,
317
+ json: async () => ({
318
+ sessions: [{
319
+ id: 'review-1',
320
+ tenantId: 'default',
321
+ workspaceId: 'default',
322
+ assetId: 'asset-doc-1',
323
+ documentId: 'doc-1',
324
+ documentVersion: 1,
325
+ title: 'Review 1',
326
+ status: 'open',
327
+ createdByActorId: 'user',
328
+ updatedByActorId: 'user',
329
+ createdByActor: null,
330
+ updatedByActor: null,
331
+ createdAt: '2026-03-19T12:00:00.000Z',
332
+ updatedAt: '2026-03-19T12:05:00.000Z',
333
+ deletedAt: null,
334
+ comments: [],
335
+ }],
336
+ }),
337
+ });
338
+ render(_jsx(DocumentViewer, { doc: createDoc(), taskTitle: null }));
339
+ expect(await screen.findByText('Review & Comments')).toBeInTheDocument();
340
+ expect(screen.getByPlaceholderText('Add a comment for this document review…')).toBeInTheDocument();
341
+ await user.click(screen.getByRole('button', { name: 'Hide Review' }));
342
+ expect(screen.queryByText('Review & Comments')).not.toBeInTheDocument();
343
+ expect(screen.queryByPlaceholderText('Add a comment for this document review…')).not.toBeInTheDocument();
344
+ await user.click(screen.getByRole('button', { name: 'Show Review' }));
345
+ expect(await screen.findByText('Review & Comments')).toBeInTheDocument();
346
+ expect(screen.getByPlaceholderText('Add a comment for this document review…')).toBeInTheDocument();
347
+ });
229
348
  it('creates a review session and posts comments from the document viewer', async () => {
230
349
  const user = userEvent.setup();
231
350
  fetchTaskforceApi
@@ -1087,13 +1206,114 @@ Body copy.
1087
1206
  attachmentCount: 0,
1088
1207
  },
1089
1208
  ];
1090
- const { rerender } = render(_jsx(DocumentWorkspace, { tasks: [{ id: 'task-1', title: 'Parent task' }], documents: docs, loading: false, error: null, selectedDoc: null, searchQuery: "", typeFilters: [], attachmentFilters: ['attached'], onSearchChange: () => { }, onSelectDoc: () => { }, onRefresh: () => { } }));
1209
+ const { rerender } = renderDocumentWorkspace({
1210
+ tasks: [{ id: 'task-1', title: 'Parent task' }],
1211
+ documents: docs.filter((doc) => doc.taskId),
1212
+ hasActiveFilters: true,
1213
+ });
1091
1214
  expect(screen.getByText('Attached fallback')).toBeInTheDocument();
1092
1215
  expect(screen.queryByText('Unattached fallback')).not.toBeInTheDocument();
1093
- rerender(_jsx(DocumentWorkspace, { tasks: [{ id: 'task-1', title: 'Parent task' }], documents: docs, loading: false, error: null, selectedDoc: null, searchQuery: "", typeFilters: [], attachmentFilters: ['unattached'], onSearchChange: () => { }, onSelectDoc: () => { }, onRefresh: () => { } }));
1216
+ rerender(_jsx(DocumentWorkspace, { tasks: [{ id: 'task-1', title: 'Parent task' }], documents: docs.filter((doc) => !doc.taskId), loading: false, error: null, selectedDoc: null, searchQuery: "", hasActiveFilters: true, onSearchChange: () => { }, onSelectDoc: () => { }, onRefresh: () => { }, onDeleteSelected: () => { } }));
1094
1217
  expect(screen.getByText('Unattached fallback')).toBeInTheDocument();
1095
1218
  expect(screen.queryByText('Attached fallback')).not.toBeInTheDocument();
1096
1219
  });
1220
+ it('switches the active document when a different index item is selected', async () => {
1221
+ const user = userEvent.setup();
1222
+ const docs = [
1223
+ createDoc({
1224
+ id: 'doc-first',
1225
+ assetId: 'asset-doc-first',
1226
+ apiUrl: '/api/taskforce/documents/asset-doc-first/content',
1227
+ displayName: 'First notes',
1228
+ title: 'First notes',
1229
+ }),
1230
+ createDoc({
1231
+ id: 'doc-second',
1232
+ assetId: 'asset-doc-second',
1233
+ apiUrl: '/api/taskforce/documents/asset-doc-second/content',
1234
+ displayName: 'Second notes',
1235
+ title: 'Second notes',
1236
+ fsPath: 'workspaces/default/assets/documents/second-notes.md',
1237
+ }),
1238
+ ];
1239
+ fetchTaskforceApi
1240
+ .mockResolvedValueOnce({
1241
+ ok: true,
1242
+ json: async () => ({ documents: docs }),
1243
+ })
1244
+ .mockResolvedValueOnce({
1245
+ ok: true,
1246
+ text: async () => '# First content\n',
1247
+ })
1248
+ .mockResolvedValueOnce(createEmptyReviewSessionsResponse())
1249
+ .mockResolvedValueOnce({
1250
+ ok: true,
1251
+ text: async () => '# Second content\n',
1252
+ })
1253
+ .mockResolvedValueOnce(createEmptyReviewSessionsResponse());
1254
+ render(_jsx(DocumentWorkspaceShell, { tasks: [], runtimeMode: "local" }));
1255
+ expect(await screen.findByText('First content')).toBeInTheDocument();
1256
+ await user.click(screen.getByRole('button', { name: /Second notes/i }));
1257
+ expect(await screen.findByText('Second content')).toBeInTheDocument();
1258
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(4, '/api/taskforce/documents/asset-doc-second/content', expect.objectContaining({ credentials: 'same-origin' }), ''));
1259
+ });
1260
+ it('returns to the remaining filtered document when the active selection is filtered out', async () => {
1261
+ const user = userEvent.setup();
1262
+ const docs = [
1263
+ createDoc({
1264
+ id: 'doc-attached-return',
1265
+ assetId: 'asset-doc-attached-return',
1266
+ apiUrl: '/api/taskforce/documents/asset-doc-attached-return/content',
1267
+ displayName: 'Attached return doc',
1268
+ title: 'Attached return doc',
1269
+ attachmentCount: 1,
1270
+ attachmentState: 'attached',
1271
+ taskId: 'task-1',
1272
+ }),
1273
+ createDoc({
1274
+ id: 'doc-unattached-return',
1275
+ assetId: 'asset-doc-unattached-return',
1276
+ apiUrl: '/api/taskforce/documents/asset-doc-unattached-return/content',
1277
+ displayName: 'Unattached return doc',
1278
+ title: 'Unattached return doc',
1279
+ fsPath: 'workspaces/default/assets/documents/unattached-return-doc.md',
1280
+ attachmentCount: 0,
1281
+ attachmentState: 'unattached',
1282
+ taskId: null,
1283
+ }),
1284
+ ];
1285
+ function Harness() {
1286
+ const [attachmentFilters, setAttachmentFilters] = useState([]);
1287
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: () => setAttachmentFilters(['attached']), children: "Show attached only" }), _jsx(DocumentWorkspaceShell, { tasks: [{ id: 'task-1', title: 'Parent task' }], runtimeMode: "local", attachmentFilters: attachmentFilters })] }));
1288
+ }
1289
+ fetchTaskforceApi
1290
+ .mockResolvedValueOnce({
1291
+ ok: true,
1292
+ json: async () => ({ documents: docs }),
1293
+ })
1294
+ .mockResolvedValueOnce({
1295
+ ok: true,
1296
+ text: async () => '# Attached return content\n',
1297
+ })
1298
+ .mockResolvedValueOnce(createEmptyReviewSessionsResponse())
1299
+ .mockResolvedValueOnce({
1300
+ ok: true,
1301
+ text: async () => '# Unattached return content\n',
1302
+ })
1303
+ .mockResolvedValueOnce(createEmptyReviewSessionsResponse())
1304
+ .mockResolvedValueOnce({
1305
+ ok: true,
1306
+ text: async () => '# Attached return content\n',
1307
+ })
1308
+ .mockResolvedValueOnce(createEmptyReviewSessionsResponse());
1309
+ render(_jsx(Harness, {}));
1310
+ expect(await screen.findByText('Attached return content')).toBeInTheDocument();
1311
+ await user.click(screen.getByRole('button', { name: /Unattached return doc/i }));
1312
+ expect(await screen.findByText('Unattached return content')).toBeInTheDocument();
1313
+ await user.click(screen.getByRole('button', { name: /Show attached only/i }));
1314
+ expect(await screen.findByText('Attached return content')).toBeInTheDocument();
1315
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(6, '/api/taskforce/documents/asset-doc-attached-return/content', expect.objectContaining({ credentials: 'same-origin' }), ''));
1316
+ });
1097
1317
  it('selects a requested document by asset id when normalized paths differ', async () => {
1098
1318
  const docs = [
1099
1319
  {
@@ -534,13 +534,36 @@ export function TaskSettings(props) {
534
534
  const workspaceName = typeof details?.tokenWorkspaceId === 'string'
535
535
  ? String(details.tokenWorkspaceId)
536
536
  : undefined;
537
- const message = payload?.code === 'MCP_TOKEN_MISSING_SCOPES' && missingScopes.length > 0
537
+ const endpoint = typeof details?.endpoint === 'string'
538
+ ? String(details.endpoint)
539
+ : cloudMcpEndpoint;
540
+ let message = payload?.code === 'MCP_TOKEN_MISSING_SCOPES' && missingScopes.length > 0
538
541
  ? `Token is missing required scopes: ${missingScopes.join(', ')}.`
539
542
  : String(payload?.error || 'Failed to test MCP connection.');
543
+ let followup;
544
+ if (payload?.code === 'MCP_ENDPOINT_GATEWAY_ERROR') {
545
+ message = 'The dedicated MCP host is responding with a gateway error.';
546
+ followup = 'This usually means the cloud MCP service is degraded. Retry shortly, and if it keeps failing treat it as an environment incident rather than a bad token.';
547
+ }
548
+ else if (payload?.code === 'MCP_ENDPOINT_TIMEOUT') {
549
+ message = 'The dedicated MCP host timed out during MCP initialization.';
550
+ followup = 'This usually points to a degraded or overloaded cloud MCP service.';
551
+ }
552
+ else if (payload?.code === 'MCP_ENDPOINT_UNREACHABLE') {
553
+ message = 'The dedicated MCP host could not be reached.';
554
+ followup = 'Check the cloud MCP hostname and environment health before rotating credentials.';
555
+ }
556
+ else if (payload?.code === 'MCP_ENDPOINT_REJECTED_VALIDATED_TOKEN') {
557
+ message = 'The dedicated MCP host rejected a token that validated locally.';
558
+ followup = 'This suggests the MCP host is stale, routed incorrectly, or out of sync with auth data.';
559
+ }
540
560
  setMcpConnectionResult({
541
561
  type: 'error',
542
562
  message,
543
563
  workspaceName,
564
+ endpoint,
565
+ errorCode: typeof payload?.code === 'string' ? String(payload.code) : undefined,
566
+ followup,
544
567
  });
545
568
  return;
546
569
  }
@@ -551,12 +574,15 @@ export function TaskSettings(props) {
551
574
  scopes: Array.isArray(result?.scopes) ? result.scopes.map((scope) => String(scope)) : [],
552
575
  workspaceName: typeof result?.workspaceName === 'string' ? String(result.workspaceName) : '',
553
576
  workspaceId: typeof result?.workspaceId === 'string' ? String(result.workspaceId) : '',
577
+ endpoint: typeof result?.endpoint === 'string' ? String(result.endpoint) : cloudMcpEndpoint,
554
578
  });
555
579
  }
556
580
  catch {
557
581
  setMcpConnectionResult({
558
582
  type: 'error',
559
- message: 'Failed to test MCP connection.'
583
+ message: 'Failed to test MCP connection.',
584
+ endpoint: cloudMcpEndpoint,
585
+ followup: 'The settings page could not complete the connection probe. Check your network connection and the selected cloud environment.'
560
586
  });
561
587
  }
562
588
  finally {
@@ -893,7 +919,7 @@ export function TaskSettings(props) {
893
919
  } })] }), _jsxs("div", { className: styles.marginTop12, style: { display: 'flex', gap: '8px', flexWrap: 'wrap', justifyContent: 'flex-end' }, children: [generatedTokenValue && (_jsx("button", { className: styles.copyBtn, onClick: () => {
894
920
  setMcpConnectionToken(generatedTokenValue);
895
921
  setMcpConnectionResult(null);
896
- }, children: "Use Generated Token" })), _jsxs("button", { className: styles.copyBtn, onClick: () => void handleTestMcpConnection(), disabled: mcpConnectionBusy, children: [mcpConnectionBusy ? _jsx(Loader2, { size: 14, className: styles.spinIcon }) : _jsx(Zap, { size: 14 }), "Test Connection"] })] }), mcpConnectionResult && (_jsxs("div", { className: `${mcpConnectionResult.type === 'success' ? styles.successMessage : styles.errorMessage} ${styles.marginTop12}`, children: [mcpConnectionResult.type === 'success' ? _jsx(Check, { size: 14 }) : _jsx(AlertTriangle, { size: 14 }), _jsxs("div", { children: [_jsx("div", { children: mcpConnectionResult.message }), mcpConnectionResult.type === 'success' && (mcpConnectionResult.workspaceName || mcpConnectionResult.workspaceId) && (_jsxs("div", { className: styles.settingHelper, children: ["Workspace: ", mcpConnectionResult.workspaceName || mcpConnectionResult.workspaceId, mcpConnectionResult.workspaceId ? ` (${mcpConnectionResult.workspaceId})` : ''] })), mcpConnectionResult.type === 'success' && Array.isArray(mcpConnectionResult.scopes) && mcpConnectionResult.scopes.length > 0 && (_jsxs("div", { className: styles.settingHelper, children: ["Granted scopes: ", mcpConnectionResult.scopes.join(', ')] }))] })] }))] }))] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => setShowMcpTokensList((prev) => !prev), onKeyDown: (e) => {
922
+ }, children: "Use Generated Token" })), _jsxs("button", { className: styles.copyBtn, onClick: () => void handleTestMcpConnection(), disabled: mcpConnectionBusy, children: [mcpConnectionBusy ? _jsx(Loader2, { size: 14, className: styles.spinIcon }) : _jsx(Zap, { size: 14 }), "Test Connection"] })] }), mcpConnectionResult && (_jsxs("div", { className: `${mcpConnectionResult.type === 'success' ? styles.successMessage : styles.errorMessage} ${styles.marginTop12}`, children: [mcpConnectionResult.type === 'success' ? _jsx(Check, { size: 14 }) : _jsx(AlertTriangle, { size: 14 }), _jsxs("div", { children: [_jsx("div", { children: mcpConnectionResult.message }), mcpConnectionResult.type === 'success' && (mcpConnectionResult.workspaceName || mcpConnectionResult.workspaceId) && (_jsxs("div", { className: styles.settingHelper, children: ["Workspace: ", mcpConnectionResult.workspaceName || mcpConnectionResult.workspaceId, mcpConnectionResult.workspaceId ? ` (${mcpConnectionResult.workspaceId})` : ''] })), mcpConnectionResult.type === 'success' && Array.isArray(mcpConnectionResult.scopes) && mcpConnectionResult.scopes.length > 0 && (_jsxs("div", { className: styles.settingHelper, children: ["Granted scopes: ", mcpConnectionResult.scopes.join(', ')] })), mcpConnectionResult.endpoint && (_jsxs("div", { className: styles.settingHelper, children: ["Endpoint: ", mcpConnectionResult.endpoint] })), mcpConnectionResult.type === 'error' && mcpConnectionResult.errorCode && (_jsxs("div", { className: styles.settingHelper, children: ["Error code: ", mcpConnectionResult.errorCode] })), mcpConnectionResult.type === 'error' && mcpConnectionResult.followup && (_jsx("div", { className: styles.settingHelper, children: mcpConnectionResult.followup }))] })] }))] }))] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => setShowMcpTokensList((prev) => !prev), onKeyDown: (e) => {
897
923
  if (e.key === 'Enter' || e.key === ' ') {
898
924
  e.preventDefault();
899
925
  setShowMcpTokensList((prev) => !prev);
@@ -970,7 +970,8 @@ describe('TaskSettings Component', () => {
970
970
  result: {
971
971
  workspaceName: 'Workspace Alpha',
972
972
  workspaceId: 'workspace-alpha',
973
- scopes: ['tasks:read', 'tasks:write', 'workspace:read']
973
+ scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
974
+ endpoint: 'https://performance-mcp.taskforcehq.ai/mcp'
974
975
  }
975
976
  })
976
977
  };
@@ -999,6 +1000,46 @@ describe('TaskSettings Component', () => {
999
1000
  }));
1000
1001
  expect(await screen.findByText('Connection looks good for this workspace.')).toBeInTheDocument();
1001
1002
  expect(screen.getByText(/Granted scopes: tasks:read, tasks:write, workspace:read/i)).toBeInTheDocument();
1003
+ expect(screen.getByText(/Endpoint: https:\/\/performance-mcp\.taskforcehq\.ai\/mcp/i)).toBeInTheDocument();
1004
+ }, 10000);
1005
+ it('4.23d2 MCP test connection shows a dedicated-host incident message when the endpoint returns a gateway failure', async () => {
1006
+ const fetchCloudAuthApi = vi.fn(async (path) => {
1007
+ if (path === '/api/taskforce/settings/mcp/test-connection') {
1008
+ return {
1009
+ ok: false,
1010
+ json: async () => ({
1011
+ code: 'MCP_ENDPOINT_GATEWAY_ERROR',
1012
+ error: 'Cloud MCP endpoint https://performance-mcp.taskforcehq.ai/mcp returned 502.',
1013
+ details: {
1014
+ endpoint: 'https://performance-mcp.taskforcehq.ai/mcp',
1015
+ upstreamStatus: 502
1016
+ }
1017
+ })
1018
+ };
1019
+ }
1020
+ return {
1021
+ ok: true,
1022
+ json: async () => ({ items: [] })
1023
+ };
1024
+ });
1025
+ const user = userEvent.setup();
1026
+ renderComponent({
1027
+ settingsModel: {
1028
+ runtimeMode: 'local',
1029
+ isAuthenticated: true,
1030
+ currentWorkspaceName: 'Workspace Alpha',
1031
+ cloudMcpBaseUrl: 'https://performance-mcp.taskforcehq.ai',
1032
+ fetchCloudAuthApi
1033
+ }
1034
+ });
1035
+ await openCloudMcpSettings(user);
1036
+ await user.click(screen.getByRole('button', { name: 'Test Connection' }));
1037
+ await user.type(await screen.findByPlaceholderText('tfmcp_xxxxx_replace-me'), 'tfmcp_example_secret');
1038
+ await user.click(screen.getAllByRole('button', { name: 'Test Connection' })[1]);
1039
+ expect(await screen.findByText('The dedicated MCP host is responding with a gateway error.')).toBeInTheDocument();
1040
+ expect(screen.getByText(/Endpoint: https:\/\/performance-mcp\.taskforcehq\.ai\/mcp/i)).toBeInTheDocument();
1041
+ expect(screen.getByText(/Error code: MCP_ENDPOINT_GATEWAY_ERROR/i)).toBeInTheDocument();
1042
+ expect(screen.getByText(/This usually means the cloud MCP service is degraded/i)).toBeInTheDocument();
1002
1043
  }, 10000);
1003
1044
  it('4.23e Antigravity uses serverUrl instead of transport in the cloud snippet', async () => {
1004
1045
  const fetchCloudAuthApi = vi.fn(async () => ({
@@ -0,0 +1,24 @@
1
+ import type { DocumentAttachmentFilter, DocumentTypeFilter } from './documentWorkspaceFilters';
2
+ export interface DocMeta {
3
+ id: string;
4
+ assetId?: string;
5
+ referenceNumber?: number | null;
6
+ referenceLabel?: string | null;
7
+ fsPath: string;
8
+ apiUrl: string;
9
+ taskId: string | null;
10
+ displayName: string;
11
+ originalFilename?: string | null;
12
+ title: string;
13
+ docType: 'implementation-plan' | 'review' | 'walkthrough' | 'planning' | 'other';
14
+ sizeBytes: number;
15
+ updatedAt: string | null;
16
+ editable?: boolean;
17
+ attachmentCount?: number;
18
+ attachmentState?: 'attached' | 'unattached';
19
+ }
20
+ export declare function normalizeDocPath(value: string): string;
21
+ export declare function docPathMatches(candidate: string, requested: string): boolean;
22
+ export declare function extractAssetIdFromPath(value: string | null | undefined): string | null;
23
+ export declare function getEffectiveAttachmentState(doc: Pick<DocMeta, 'attachmentState' | 'attachmentCount' | 'taskId'>): 'attached' | 'unattached';
24
+ export declare function filterDocuments(documents: DocMeta[], searchQuery: string, typeFilters: DocumentTypeFilter[], attachmentFilters: DocumentAttachmentFilter[]): DocMeta[];