@taskforcehq/taskforce 0.3.305 → 0.3.307

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 (440) hide show
  1. package/README.md +0 -2
  2. package/dist/Taskforce.module.css +603 -107
  3. package/dist/TaskforceCore.js +216 -43
  4. package/dist/TaskforceCore.test.js +611 -28
  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 +46 -2
  14. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  15. package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
  16. package/dist/components/activity/EntityActivityTimeline.js +81 -0
  17. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  18. package/dist/components/context/ContextAttachmentManager.js +561 -0
  19. package/dist/components/features/AgentsModule.d.ts +14 -1
  20. package/dist/components/features/AgentsModule.js +366 -18
  21. package/dist/components/features/AgentsModule.test.js +507 -8
  22. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
  23. package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
  24. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
  25. package/dist/components/features/DocumentIndex.d.ts +2 -2
  26. package/dist/components/features/DocumentIndex.js +3 -4
  27. package/dist/components/features/DocumentViewer.d.ts +7 -1
  28. package/dist/components/features/DocumentViewer.js +62 -4
  29. package/dist/components/features/DocumentWorkspace.d.ts +10 -2
  30. package/dist/components/features/DocumentWorkspace.js +15 -4
  31. package/dist/components/features/DocumentWorkspace.test.js +51 -1
  32. package/dist/components/features/TaskSettings.d.ts +2 -0
  33. package/dist/components/features/TaskSettings.js +83 -78
  34. package/dist/components/features/TaskSettings.test.js +171 -82
  35. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  36. package/dist/components/task/TaskActionHeader.js +11 -5
  37. package/dist/components/task/TaskActionHeader.test.js +10 -0
  38. package/dist/components/task/TaskCard.js +24 -29
  39. package/dist/components/task/TaskCard.test.js +38 -3
  40. package/dist/components/task/TaskContextUpload.js +4 -526
  41. package/dist/components/task/TaskContextUpload.test.js +3 -3
  42. package/dist/components/task/TaskForm.js +61 -116
  43. package/dist/components/task/TaskForm.test.js +110 -8
  44. package/dist/components/task/TaskKanban.d.ts +2 -1
  45. package/dist/components/task/TaskKanban.js +94 -21
  46. package/dist/components/task/TaskKanban.test.js +36 -0
  47. package/dist/components/task/TaskList.js +5 -1
  48. package/dist/components/task/TaskList.test.js +4 -0
  49. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  50. package/dist/components/ui/EditableAvatarButton.js +18 -0
  51. package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
  52. package/dist/components/ui/HierarchyActionBadge.js +5 -0
  53. package/dist/components/views/AppShellHeader.d.ts +3 -1
  54. package/dist/components/views/AppShellHeader.js +27 -11
  55. package/dist/components/views/AppShellHeader.test.js +18 -1
  56. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  57. package/dist/components/views/PlanComparisonPage.js +78 -14
  58. package/dist/components/views/PlanComparisonPage.test.js +257 -0
  59. package/dist/components/views/PlansPage.js +127 -36
  60. package/dist/components/views/PlansPage.test.js +294 -12
  61. package/dist/components/views/StandaloneLayout.js +529 -592
  62. package/dist/components/views/WidgetView.js +10 -8
  63. package/dist/components/views/panels/PlanningDrawer.d.ts +25 -5
  64. package/dist/components/views/panels/PlanningDrawer.js +270 -55
  65. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  66. package/dist/components/views/panels/PlanningDrawer.test.js +529 -0
  67. package/dist/components/views/planningScope.d.ts +20 -0
  68. package/dist/components/views/planningScope.js +25 -0
  69. package/dist/components/views/planningScope.test.d.ts +1 -0
  70. package/dist/components/views/planningScope.test.js +60 -0
  71. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  72. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  73. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  74. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  75. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  76. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  77. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  78. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  79. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  80. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  81. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  82. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  83. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  84. package/dist/config/envSchema.js +1 -0
  85. package/dist/core/AiProfileService.d.ts +46 -2
  86. package/dist/core/AiProfileService.js +1073 -82
  87. package/dist/core/AiProfileService.test.js +19 -1
  88. package/dist/core/AiProfiles.test.js +1249 -44
  89. package/dist/core/AttachmentLinkService.d.ts +3 -0
  90. package/dist/core/AttachmentLinkService.js +110 -23
  91. package/dist/core/AuthIdentityService.d.ts +107 -0
  92. package/dist/core/AuthIdentityService.js +284 -0
  93. package/dist/core/AuthTokenService.d.ts +4 -0
  94. package/dist/core/AuthTokenService.js +34 -6
  95. package/dist/core/AuthTokenService.test.js +3 -3
  96. package/dist/core/EntitlementsPolicy.test.js +2 -2
  97. package/dist/core/GlobalSettingsService.js +127 -6
  98. package/dist/core/McpTokenService.d.ts +12 -0
  99. package/dist/core/McpTokenService.js +44 -1
  100. package/dist/core/PlanEntitlementService.d.ts +23 -2
  101. package/dist/core/PlanEntitlementService.js +343 -57
  102. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  103. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  104. package/dist/core/PlanVersionPolicy.test.js +35 -2
  105. package/dist/core/PlanningEntities.test.js +418 -4
  106. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  107. package/dist/core/SyncReconciliationService.js +2 -65
  108. package/dist/core/SystemAdmin.test.js +42 -42
  109. package/dist/core/TaskActivityService.d.ts +28 -1
  110. package/dist/core/TaskActivityService.js +227 -46
  111. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  112. package/dist/core/TaskActivitySync.test.js +90 -0
  113. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  114. package/dist/core/TaskAuditTimeline.test.js +96 -0
  115. package/dist/core/Taskforce.d.ts +415 -40
  116. package/dist/core/Taskforce.ids.test.js +58 -0
  117. package/dist/core/Taskforce.js +1543 -266
  118. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  119. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  120. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  121. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  122. package/dist/core/WorkspaceLifecycleService.js +0 -5
  123. package/dist/core/WorkspacePermissions.test.js +25 -1
  124. package/dist/core/WorkspacePlanMode.test.js +117 -16
  125. package/dist/core/shared.d.ts +1 -1
  126. package/dist/core/shared.js +5 -1
  127. package/dist/core/types.d.ts +75 -2
  128. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +28 -2
  129. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  130. package/dist/hooks/bootstrapTimeouts.js +8 -0
  131. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  132. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  133. package/dist/hooks/sync/bootstrap.js +2 -0
  134. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  135. package/dist/hooks/sync/controlPlane.js +78 -0
  136. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  137. package/dist/hooks/sync/controlPlane.test.js +121 -0
  138. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  139. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  140. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  141. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  142. package/dist/hooks/sync/orchestratorShared.d.ts +13 -4
  143. package/dist/hooks/sync/orchestratorShared.js +129 -48
  144. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  145. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  146. package/dist/hooks/sync/pullLifecycle.js +24 -0
  147. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  148. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  149. package/dist/hooks/sync/recovery.d.ts +16 -2
  150. package/dist/hooks/sync/recovery.js +179 -53
  151. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  152. package/dist/hooks/sync/recovery.test.js +292 -0
  153. package/dist/hooks/sync/transfers.d.ts +16 -3
  154. package/dist/hooks/sync/transfers.js +210 -66
  155. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  156. package/dist/hooks/sync/transfers.test.js +928 -0
  157. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  158. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  159. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  160. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  161. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  162. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  163. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  164. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  165. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  166. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  167. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  168. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  169. package/dist/hooks/useFilterSort.js +13 -13
  170. package/dist/hooks/useFilterSort.test.js +87 -1
  171. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  172. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  173. package/dist/hooks/useSyncOrchestrator.js +409 -183
  174. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  175. package/dist/hooks/useTaskData.js +8 -3
  176. package/dist/hooks/useTaskData.test.js +45 -0
  177. package/dist/hooks/useTaskforce.d.ts +145 -52
  178. package/dist/hooks/useTaskforce.js +592 -253
  179. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  180. package/dist/hooks/useTaskforce.runtime-routing.test.js +246 -0
  181. package/dist/hooks/useTaskforce.sync-behavior.test.js +2468 -201
  182. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  183. package/dist/hooks/useWorkspaceSyncController.js +13 -2
  184. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  185. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  186. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +328 -40
  187. package/dist/localization/locales/en-US.d.ts +0 -1
  188. package/dist/localization/locales/en-US.js +0 -1
  189. package/dist/localization/locales/es-419.js +0 -1
  190. package/dist/localization/locales/pt-BR.js +0 -1
  191. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  192. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  193. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  194. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  195. package/dist/mcp/clientIntegrations.d.ts +1 -0
  196. package/dist/mcp/clientIntegrations.js +25 -0
  197. package/dist/mcp/clientIntegrations.test.js +14 -3
  198. package/dist/mcp/clientProfileDefaults.js +3 -3
  199. package/dist/mcp/collaborationRegistrar.js +7 -7
  200. package/dist/mcp/documentAssetRegistrar.js +116 -10
  201. package/dist/mcp/runtime.d.ts +3 -0
  202. package/dist/mcp/runtime.js +1099 -185
  203. package/dist/mcp/runtime.test.js +1491 -140
  204. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  205. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  206. package/dist/mcp/toolRegistry.d.ts +56 -0
  207. package/dist/mcp/toolRegistry.js +8 -0
  208. package/dist/mcp/toolRegistry.test.js +4 -0
  209. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  210. package/dist/migrations/taskSchemaMigrations.js +192 -61
  211. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  212. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  213. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  214. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  215. package/dist/resources/templates/workflows/plan.yaml +1 -1
  216. package/dist/resources/templates/workflows/review.yaml +2 -2
  217. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  218. package/dist/server/aiProfileAvatarAssets.js +319 -0
  219. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  220. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  221. package/dist/server/annotatedAttachmentsRoutes.test.js +69 -10
  222. package/dist/server/auth/providers/apple.d.ts +33 -0
  223. package/dist/server/auth/providers/apple.js +82 -0
  224. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  225. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  226. package/dist/server/auth/providers/github.d.ts +26 -0
  227. package/dist/server/auth/providers/github.js +86 -0
  228. package/dist/server/auth/providers/google.d.ts +21 -0
  229. package/dist/server/auth/providers/google.js +56 -0
  230. package/dist/server/auth/providers/types.d.ts +21 -0
  231. package/dist/server/auth/providers/types.js +1 -0
  232. package/dist/server/auth.d.ts +2 -0
  233. package/dist/server/auth.js +6 -3
  234. package/dist/server/documentReviewRoutes.test.js +36 -3
  235. package/dist/server/index.cookieProxy.test.js +1 -0
  236. package/dist/server/index.d.ts +12 -0
  237. package/dist/server/index.js +124 -9
  238. package/dist/server/index.rateLimit.test.js +3 -0
  239. package/dist/server/index.test.js +111 -3
  240. package/dist/server/routes/admin.d.ts +3 -0
  241. package/dist/server/routes/admin.js +459 -28
  242. package/dist/server/routes/annotatedAttachments.js +51 -0
  243. package/dist/server/routes/auth.d.ts +26 -2
  244. package/dist/server/routes/auth.js +1170 -71
  245. package/dist/server/routes/authSupport.d.ts +5 -1
  246. package/dist/server/routes/authSupport.js +13 -2
  247. package/dist/server/routes/billing.js +372 -35
  248. package/dist/server/routes/billing.test.js +595 -12
  249. package/dist/server/routes/documents.d.ts +1 -0
  250. package/dist/server/routes/documents.js +182 -70
  251. package/dist/server/routes/shared.d.ts +28 -2
  252. package/dist/server/routes/shared.js +69 -14
  253. package/dist/server/routes/sync.integration.test.js +658 -39
  254. package/dist/server/routes/sync.js +74 -608
  255. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  256. package/dist/server/routes/syncAuxRoutes.js +91 -0
  257. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  258. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  259. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  260. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  261. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  262. package/dist/server/routes/syncPushRoutes.js +212 -0
  263. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  264. package/dist/server/routes/syncRouteGuards.js +67 -0
  265. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  266. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  267. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  268. package/dist/server/routes/syncRouteTypes.js +1 -0
  269. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  270. package/dist/server/routes/syncSnapshotRoutes.js +182 -0
  271. package/dist/server/routes/tasks.js +55 -10
  272. package/dist/server/routes/workspaces.js +36 -5
  273. package/dist/server/routes.js +147 -11
  274. package/dist/server/routes.test.js +2704 -183
  275. package/dist/services/workflowExportSnapshots.test.js +2 -0
  276. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  277. package/dist/shared/aiProfileSeatScope.js +21 -0
  278. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  279. package/dist/shared/aiProfileSurfaceType.js +10 -8
  280. package/dist/shared/passwordPolicy.d.ts +13 -0
  281. package/dist/shared/passwordPolicy.js +84 -0
  282. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  283. package/dist/shared/passwordPolicy.test.js +34 -0
  284. package/dist/storage/objectStorage.d.ts +1 -0
  285. package/dist/storage/objectStorage.js +2 -1
  286. package/dist/storage/objectStorageClient.d.ts +1 -0
  287. package/dist/storage/objectStorageClient.js +41 -8
  288. package/dist/storage/objectStorageClient.test.js +26 -1
  289. package/dist/storage/objectStorageConfig.js +3 -1
  290. package/dist/storage/objectStorageConfig.test.js +13 -1
  291. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  292. package/dist/storage/workspaceAssetStore.js +132 -44
  293. package/dist/storage/workspaceAssetStore.test.js +93 -6
  294. package/dist/styles/fonts.css +70 -0
  295. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  296. package/dist/sync/collaborationSyncPayload.js +19 -11
  297. package/dist/sync/collaborationSyncPayload.test.js +14 -1
  298. package/dist/sync/collaborationSyncState.d.ts +3 -3
  299. package/dist/sync/collaborationSyncState.js +19 -12
  300. package/dist/sync/contentSyncPayload.d.ts +5 -1
  301. package/dist/sync/contentSyncPayload.js +9 -0
  302. package/dist/sync/contentSyncPayload.test.js +39 -0
  303. package/dist/sync/contentSyncState.d.ts +9 -9
  304. package/dist/sync/contentSyncState.js +248 -107
  305. package/dist/sync/planningSyncPayload.js +5 -0
  306. package/dist/sync/planningSyncPayload.test.js +24 -0
  307. package/dist/sync/syncApplyHandlers.d.ts +11 -4
  308. package/dist/sync/syncApplyHandlers.js +115 -42
  309. package/dist/sync/syncApplyHandlers.test.js +251 -4
  310. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  311. package/dist/sync/syncFieldSemantics.js +55 -0
  312. package/dist/sync/syncResourceAdapters.d.ts +46 -0
  313. package/dist/sync/syncResourceAdapters.js +79 -0
  314. package/dist/sync/syncService.d.ts +6 -0
  315. package/dist/sync/syncService.js +65 -1
  316. package/dist/sync/syncService.test.js +96 -3
  317. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  318. package/dist/sync/taskSyncChangeKey.js +143 -0
  319. package/dist/sync/taskSyncPayload.js +3 -8
  320. package/dist/sync/workspacePullFeed.d.ts +4 -0
  321. package/dist/sync/workspacePullFeed.js +132 -72
  322. package/dist/sync/workspacePullFeed.test.js +92 -0
  323. package/dist/sync/workspaceRepair.js +5 -2
  324. package/dist/sync/workspaceSyncModel.d.ts +73 -6
  325. package/dist/sync/workspaceSyncModel.js +342 -73
  326. package/dist/sync/workspaceSyncModel.test.js +559 -14
  327. package/dist/sync/workspaceSyncState.d.ts +17 -0
  328. package/dist/sync/workspaceSyncState.js +2 -1
  329. package/dist/sync/workspaceSyncSurface.js +3 -8
  330. package/dist/types.d.ts +50 -2
  331. package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
  332. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
  333. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  334. package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
  335. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  336. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  337. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  338. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  339. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  340. package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
  341. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  342. package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
  343. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-D4XseTwW.js} +1 -1
  344. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  345. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  346. package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
  347. package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
  348. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  349. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  350. package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
  351. package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
  352. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-oOA9bcdK.js} +4 -4
  353. package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
  354. package/dist/ui/assets/index-607WPo_X.js +4 -0
  355. package/dist/ui/assets/index-CYyodXsg.css +1 -0
  356. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-B32hGuF2.js} +1 -1
  357. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  358. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  359. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  360. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  361. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  362. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  363. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  364. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  365. package/dist/ui/fonts/Noir_medium.otf +0 -0
  366. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  367. package/dist/ui/fonts/Noir_regular.otf +0 -0
  368. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  369. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  370. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  371. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  372. package/dist/ui/index.html +3 -3
  373. package/dist/ui/product/agent manager.png +0 -0
  374. package/dist/ui/product/agent_manager_02.png +0 -0
  375. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  376. package/dist/utils/aiProfileEvents.d.ts +7 -0
  377. package/dist/utils/aiProfileEvents.js +6 -0
  378. package/dist/utils/assignees.d.ts +8 -0
  379. package/dist/utils/assignees.js +10 -1
  380. package/dist/utils/assignees.test.d.ts +1 -0
  381. package/dist/utils/assignees.test.js +25 -0
  382. package/dist/utils/avatarDisplay.d.ts +1 -0
  383. package/dist/utils/avatarDisplay.js +15 -0
  384. package/dist/utils/avatarUpload.d.ts +27 -0
  385. package/dist/utils/avatarUpload.js +132 -0
  386. package/dist/utils/avatarUpload.test.d.ts +1 -0
  387. package/dist/utils/avatarUpload.test.js +40 -0
  388. package/dist/utils/bootstrapTrace.d.ts +7 -0
  389. package/dist/utils/bootstrapTrace.js +29 -0
  390. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  391. package/dist/utils/contextAssetEvents.d.ts +2 -0
  392. package/dist/utils/contextFiles.d.ts +3 -1
  393. package/dist/utils/contextFiles.js +2 -1
  394. package/dist/utils/syncEventDetails.d.ts +3 -0
  395. package/dist/utils/syncEventDetails.js +64 -0
  396. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  397. package/dist/utils/syncEventDetails.test.js +23 -0
  398. package/dist/utils/syncEventPresentation.d.ts +15 -0
  399. package/dist/utils/syncEventPresentation.js +129 -0
  400. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  401. package/dist/utils/syncEventPresentation.test.js +64 -0
  402. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  403. package/dist/utils/syncStatusPresentation.js +217 -0
  404. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  405. package/dist/utils/syncStatusPresentation.test.js +164 -0
  406. package/dist/utils/taskActivity.d.ts +9 -1
  407. package/dist/utils/taskActivity.js +290 -22
  408. package/dist/utils/taskActivity.test.js +141 -8
  409. package/dist/utils/taskSearch.d.ts +22 -0
  410. package/dist/utils/taskSearch.js +87 -0
  411. package/dist/utils/taskSearch.test.d.ts +1 -0
  412. package/dist/utils/taskSearch.test.js +69 -0
  413. package/dist/utils/taskUtils.js +10 -3
  414. package/dist/utils/taskUtils.test.d.ts +1 -0
  415. package/dist/utils/taskUtils.test.js +19 -0
  416. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  417. package/dist/utils/workspaceSyncPresentation.js +68 -2
  418. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  419. package/package.json +3 -2
  420. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  421. package/scripts/run-e2e-realtime.sh +17 -3
  422. package/scripts/run-smoke.sh +17 -5
  423. package/scripts/run-soak.sh +17 -5
  424. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  425. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  426. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  427. package/src/resources/templates/workflows/plan.yaml +1 -1
  428. package/src/resources/templates/workflows/review.yaml +2 -2
  429. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  430. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BG6P_GVW.js +0 -3
  431. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  432. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  433. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  434. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  435. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  436. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  437. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +0 -9
  438. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  439. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  440. package/scripts/migrate-planning-model.ts +0 -233
@@ -27,7 +27,7 @@
27
27
  * === SECTION: Navigation === Line ~3410 — setActiveTab, handleNavigation
28
28
  * === SECTION: Form === Line ~3489 — resetForm, handleEdit, handleSubmit, saveTask
29
29
  * === SECTION: Task CRUD === Line ~3691 — handleDelete, handleUpdateTask, handleToggle*
30
- * === SECTION: Task Relationships === Line ~4129 — parent/child link handlers
30
+ * === SECTION: Planning Links === Line ~4129 — workstream and initiative link handlers
31
31
  * === SECTION: Category/Type Mgmt === Line ~4462 — handleSaveCategory, handleRemoveCategory
32
32
  * === SECTION: Settings Model === Line ~4625 — settingsModel object
33
33
  * === SECTION: Return Value === Line ~4817 — final return object (public API shape)
@@ -43,6 +43,7 @@ import { DEFAULT_TYPE_VALUE } from '../shared/taxonomyDefaults.js';
43
43
  import { cloneDefaultWorkflowDefinitions } from '../shared/workflowDefaults.js';
44
44
  import { resolveClientDeploymentConfig } from '../config/clientDeployment';
45
45
  import { resolveTaskforceMcpBaseUrlFromBaseUrl } from '../config/cloudEnvironment';
46
+ import { buildBootstrapTraceId, withBootstrapTraceHeader } from '../utils/bootstrapTrace';
46
47
  import { createDefaultAssigneeOptions } from '../utils/assignees';
47
48
  import { persistLocalMutationActorUserId, readLocalMutationActorUserId } from '../utils/taskforceApiClient';
48
49
  import { useSyncOrchestrator } from './useSyncOrchestrator';
@@ -67,6 +68,14 @@ import { useTaskforceAuthBootstrap } from './auth/useTaskforceAuthBootstrap';
67
68
  import { deriveAuthGuardPolicy } from './auth/useAuthGuardPolicy';
68
69
  import { useTaskforceWorkspaceBootstrap } from './workspace/useTaskforceWorkspaceBootstrap';
69
70
  import { useDataVersionRefresh } from './sync/useDataVersionRefresh';
71
+ import { BOOTSTRAP_REQUEST_TIMEOUT_MS } from './bootstrapTimeouts';
72
+ function upsertEntityById(items, nextItem) {
73
+ const existingIndex = items.findIndex((item) => item.id === nextItem.id);
74
+ if (existingIndex === -1) {
75
+ return [...items, nextItem];
76
+ }
77
+ return items.map((item, index) => (index === existingIndex ? nextItem : item));
78
+ }
70
79
  const ENV_DEPLOYMENT = resolveClientDeploymentConfig(import.meta.env);
71
80
  const DEFAULT_CLOUD_APP_ORIGIN = (() => {
72
81
  const configuredBase = ENV_DEPLOYMENT.baseUrl;
@@ -81,7 +90,75 @@ const DEFAULT_CLOUD_APP_ORIGIN = (() => {
81
90
  // Safety default for local runtime cloud auth/sync fallback.
82
91
  return 'https://app.taskforcehq.ai';
83
92
  })();
84
- const BOOTSTRAP_REQUEST_TIMEOUT_MS = 12_000;
93
+ const PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS = 20_000;
94
+ const EXPECTED_WORKSPACE_ABORT_REASONS = new Set([
95
+ 'external-abort',
96
+ 'superseded',
97
+ 'workspace-reset',
98
+ 'workspace-transition',
99
+ ]);
100
+ function hasAbortErrorName(error) {
101
+ if (typeof DOMException !== 'undefined' && error instanceof DOMException) {
102
+ return error.name === 'AbortError';
103
+ }
104
+ return String(error?.name || '').toLowerCase() === 'aborterror';
105
+ }
106
+ function isExpectedWorkspaceAbortReason(reason) {
107
+ return typeof reason === 'string' && EXPECTED_WORKSPACE_ABORT_REASONS.has(reason);
108
+ }
109
+ function isExpectedWorkspaceAbortFailure(error, signal) {
110
+ if (!signal.aborted)
111
+ return false;
112
+ if (error === signal.reason)
113
+ return true;
114
+ if (isExpectedWorkspaceAbortReason(error) || isExpectedWorkspaceAbortReason(signal.reason))
115
+ return true;
116
+ return hasAbortErrorName(error);
117
+ }
118
+ function createBootstrapTimeoutError(timeoutMs) {
119
+ const timeoutError = new Error(`Startup checks timed out after ${timeoutMs}ms.`);
120
+ timeoutError.name = 'BootstrapTimeoutError';
121
+ return timeoutError;
122
+ }
123
+ function sanitizePlanningBootstrapPayload(input) {
124
+ const payload = input && typeof input === 'object' ? input : {};
125
+ const initiatives = Array.isArray(payload.initiatives)
126
+ ? payload.initiatives.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
127
+ : [];
128
+ const workstreams = Array.isArray(payload.workstreams)
129
+ ? payload.workstreams.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
130
+ : [];
131
+ const workstreamTaskSummaries = Array.isArray(payload.workstreamTaskSummaries)
132
+ ? payload.workstreamTaskSummaries.flatMap((entry) => {
133
+ if (!entry || typeof entry !== 'object')
134
+ return [];
135
+ const normalizedWorkstreamId = String(entry.workstreamId || '').trim();
136
+ if (!normalizedWorkstreamId)
137
+ return [];
138
+ const rawTasks = Array.isArray(entry.tasks) ? entry.tasks : [];
139
+ const taskCount = Math.max(0, Number(entry.taskCount) || 0);
140
+ const completedTaskCount = Math.max(0, Number(entry.completedTaskCount) || 0);
141
+ return [{
142
+ workstreamId: normalizedWorkstreamId,
143
+ taskCount,
144
+ completedTaskCount,
145
+ tasks: rawTasks
146
+ .filter((task) => Boolean(task && typeof task === 'object' && typeof task.id === 'string'))
147
+ .map((task) => ({
148
+ id: String(task.id || '').trim(),
149
+ referenceNumber: typeof task.referenceNumber === 'number' ? task.referenceNumber : null,
150
+ title: String(task.title || '').trim() || 'Untitled Task',
151
+ status: typeof task.status === 'string' ? task.status : null,
152
+ })),
153
+ }];
154
+ })
155
+ : [];
156
+ return {
157
+ initiatives,
158
+ workstreams,
159
+ workstreamTaskSummaries,
160
+ };
161
+ }
85
162
  export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, onClose }) {
86
163
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
87
164
  const { categories, types, apiEndpoint, apiBaseUrl, cloudAuthBaseUrl, cloudMcpBaseUrl, wsBaseUrl, shortcut: configShortcut } = mergedConfig;
@@ -131,6 +208,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
131
208
  const workspaceMode = workspaceModeRaw === 'single-local' || workspaceModeRaw === 'multi-cloud'
132
209
  ? workspaceModeRaw
133
210
  : (runtimeModeHint === 'cloud' ? 'multi-cloud' : 'single-local');
211
+ setRuntimeMode(runtimeModeHint);
134
212
  setRuntimeConfigOverride({
135
213
  cloudEnvironment: typeof cfg.cloudEnvironment === 'string'
136
214
  ? String(cfg.cloudEnvironment).trim().toLowerCase()
@@ -175,10 +253,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
175
253
  const propApiBaseUrl = normalizeBaseUrl(apiBaseUrl);
176
254
  const propCloudAuthBaseUrl = normalizeBaseUrl(cloudAuthBaseUrl);
177
255
  const propCloudMcpBaseUrl = normalizeBaseUrl(cloudMcpBaseUrl);
178
- const explicitConfiguredApiBaseUrl = propApiBaseUrl || envApiBaseUrl || envBaseUrl;
179
256
  const runtimeResolvedApiBaseUrl = runtimeConfigOverride.apiBaseUrl || runtimeConfigOverride.baseUrl;
180
- const normalizedApiBaseUrl = explicitConfiguredApiBaseUrl
257
+ const envConfiguredApiBaseUrl = isLoopbackHost ? '' : (envApiBaseUrl || envBaseUrl);
258
+ const normalizedApiBaseUrl = propApiBaseUrl
181
259
  || runtimeResolvedApiBaseUrl
260
+ || envConfiguredApiBaseUrl
182
261
  || '';
183
262
  const explicitConfiguredCloudAuthBaseUrl = propCloudAuthBaseUrl || envCloudAuthBaseUrl || envBaseUrl;
184
263
  const runtimeResolvedCloudAuthBaseUrl = runtimeConfigOverride.cloudAuthBaseUrl || runtimeConfigOverride.baseUrl;
@@ -206,10 +285,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
206
285
  || normalizedCloudAuthBaseUrl
207
286
  || defaultCloudAuthBaseUrl).trim().replace(/\/+$/, '');
208
287
  const authOnlyCloudMode = !isCloudHost && Boolean(normalizedCloudAuthBaseUrl) && !normalizedApiBaseUrl;
209
- const shouldProbeCloudAuth = runtimeConfigReady && (Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl) || isCloudHost);
288
+ // Real cloud hosts must defer protected bootstrap calls immediately, even before
289
+ // runtime-config finishes, or the one-shot initial bootstrap can consume itself
290
+ // on unauthenticated /config, /taxonomy-state, and /planning/bootstrap reads.
291
+ const shouldProbeCloudAuth = isCloudHost || (runtimeConfigReady && Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl));
210
292
  const shouldGateProtectedApiCalls = shouldProbeCloudAuth && !authOnlyCloudMode;
211
293
  const cloudAuthConfigured = Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl || isCloudHost);
212
294
  const initialRuntimeMode = isCloudHost ? 'cloud' : 'local';
295
+ const [runtimeMode, setRuntimeMode] = useState(() => initialRuntimeMode);
213
296
  const optimisticLocalAuthUserId = initialRuntimeMode === 'local' && isLoopbackHost && cloudAuthConfigured
214
297
  ? readLocalMutationActorUserId()
215
298
  : '';
@@ -236,6 +319,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
236
319
  || path.startsWith('/api/taskforce/sync/')
237
320
  || path.startsWith('/api/taskforce/settings/mcp/');
238
321
  if (!isCloudHost && isCloudScopedPath) {
322
+ if (runtimeMode === 'cloud') {
323
+ return path;
324
+ }
239
325
  if (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy) {
240
326
  return path;
241
327
  }
@@ -263,7 +349,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
263
349
  if (!isCloudHost && isCloudScopedPath)
264
350
  return path;
265
351
  return path;
266
- }, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, runtimeConfigOverride.workspaceMode, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost]);
352
+ }, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, runtimeConfigOverride.workspaceMode, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost, runtimeMode]);
267
353
  const resolveWebSocketUrl = useCallback((path) => {
268
354
  const wsPath = String(path || '').trim() || '/taskforce-ws';
269
355
  const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`;
@@ -313,7 +399,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
313
399
  }), [projectRoot, initialRuntimeMode]);
314
400
  const [mcpScriptPath, setMcpScriptPath] = useState('');
315
401
  const [tenantId, setTenantId] = useState('');
316
- const [runtimeMode, setRuntimeMode] = useState(() => initialRuntimeMode);
317
402
  const runtimeContractFallback = resolveRuntimeModeContract(runtimeMode);
318
403
  const workspaceMode = runtimeConfigOverride.workspaceMode === 'single-local' || runtimeConfigOverride.workspaceMode === 'multi-cloud'
319
404
  ? runtimeConfigOverride.workspaceMode
@@ -328,6 +413,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
328
413
  const [authWorkspaceId, setAuthWorkspaceId] = useState('');
329
414
  const [authUserEmail, setAuthUserEmail] = useState('');
330
415
  const [authUserDisplayName, setAuthUserDisplayName] = useState('');
416
+ const [availableAuthProviders, setAvailableAuthProviders] = useState([]);
331
417
  const [authUserAvatarUrl, setAuthUserAvatarUrl] = useState('');
332
418
  const [hasBetaAccess, setHasBetaAccess] = useState(true);
333
419
  const [hydratedWorkspaceRole, setHydratedWorkspaceRole] = useState(null);
@@ -358,6 +444,12 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
358
444
  const currentWorkspaceIdRef = useRef(currentWorkspaceId);
359
445
  const explicitWorkspaceSelectionRef = useRef(null);
360
446
  const workspaceTransitionEpochRef = useRef(0);
447
+ const bootstrapTraceSeedRef = useRef((() => {
448
+ const randomPart = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
449
+ ? crypto.randomUUID()
450
+ : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
451
+ return randomPart.replace(/[^A-Za-z0-9_-]+/g, '').slice(0, 12) || 'session';
452
+ })());
361
453
  const [workspaceTransitionEpoch, setWorkspaceTransitionEpoch] = useState(0);
362
454
  const bootstrapConfigAbortRef = useRef(null);
363
455
  const referenceDataAbortRef = useRef(null);
@@ -365,6 +457,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
365
457
  const assigneeOptionsAbortRef = useRef(null);
366
458
  const deletedTasksAbortRef = useRef(null);
367
459
  const planningEntitiesAbortRef = useRef(null);
460
+ const planningEntitiesRetryTimerRef = useRef(null);
368
461
  const commitCurrentWorkspaceId = useCallback((nextWorkspaceIdRaw, options) => {
369
462
  const nextWorkspaceId = String(nextWorkspaceIdRaw || '').trim();
370
463
  if (!nextWorkspaceId)
@@ -409,9 +502,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
409
502
  return workspaceTransitionEpochRef.current !== requestState.epoch;
410
503
  }, []);
411
504
  const isAbortError = useCallback((error) => {
412
- return error instanceof DOMException
413
- ? error.name === 'AbortError'
414
- : String(error?.name || '').toLowerCase() === 'aborterror';
505
+ return hasAbortErrorName(error) || isExpectedWorkspaceAbortReason(error);
415
506
  }, []);
416
507
  const abortWorkspaceScopedRequests = useCallback(() => {
417
508
  [
@@ -425,6 +516,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
425
516
  ref.current?.abort('workspace-transition');
426
517
  ref.current = null;
427
518
  });
519
+ if (planningEntitiesRetryTimerRef.current !== null && typeof window !== 'undefined') {
520
+ window.clearTimeout(planningEntitiesRetryTimerRef.current);
521
+ planningEntitiesRetryTimerRef.current = null;
522
+ }
428
523
  referenceDataRequestRef.current = null;
429
524
  }, []);
430
525
  // Local runtime APIs must stay available immediately on app start, even when cloud
@@ -504,6 +599,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
504
599
  const [archivedTasks, setArchivedTasks] = useState([]);
505
600
  const [initiatives, setInitiatives] = useState([]);
506
601
  const [workstreams, setWorkstreams] = useState([]);
602
+ const [planningBootstrapTaskSummaries, setPlanningBootstrapTaskSummaries] = useState([]);
507
603
  const [deletedTasks, setDeletedTasks] = useState([]);
508
604
  const [loadingTasks, setLoadingTasks] = useState(false);
509
605
  const [editingTaskId, setEditingTaskId] = useState(null);
@@ -545,6 +641,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
545
641
  const abortController = new AbortController();
546
642
  const externalSignal = options?.signal;
547
643
  const timeoutLabel = Number(timeoutMs) > 0 ? Number(timeoutMs) : BOOTSTRAP_REQUEST_TIMEOUT_MS;
644
+ const requestState = getWorkspaceRequestState();
645
+ const tracedInit = url.startsWith('/api/taskforce/')
646
+ ? withBootstrapTraceHeader(init, buildBootstrapTraceId({
647
+ workspaceId: requestState.workspaceId,
648
+ epoch: requestState.epoch,
649
+ seed: bootstrapTraceSeedRef.current,
650
+ }))
651
+ : init;
548
652
  const timerId = typeof window !== 'undefined'
549
653
  ? window.setTimeout(() => abortController.abort('bootstrap-timeout'), timeoutLabel)
550
654
  : null;
@@ -561,18 +665,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
561
665
  }
562
666
  try {
563
667
  return await fetch(url, {
564
- ...(init || {}),
668
+ ...(tracedInit || {}),
565
669
  signal: abortController.signal
566
670
  });
567
671
  }
568
672
  catch (error) {
569
- const isAbortError = error instanceof DOMException
570
- ? error.name === 'AbortError'
571
- : String(error?.name || '').toLowerCase() === 'aborterror';
572
- if (isAbortError) {
573
- const timeoutError = new Error(`Startup checks timed out after ${timeoutLabel}ms.`);
574
- timeoutError.name = 'BootstrapTimeoutError';
575
- throw timeoutError;
673
+ if (externalSignal?.aborted && isExpectedWorkspaceAbortFailure(error, externalSignal)) {
674
+ throw externalSignal.reason || error;
675
+ }
676
+ if (hasAbortErrorName(error) || error === 'bootstrap-timeout') {
677
+ throw createBootstrapTimeoutError(timeoutLabel);
576
678
  }
577
679
  throw error;
578
680
  }
@@ -584,7 +686,50 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
584
686
  window.clearTimeout(timerId);
585
687
  }
586
688
  }
587
- }, []);
689
+ }, [getWorkspaceRequestState]);
690
+ const bootstrapState = useMemo(() => {
691
+ const authPending = !authSessionResolved;
692
+ const stalled = bootstrapPhase === 'stalled';
693
+ const appPending = authSessionResolved && (!configLoaded || workspaceBootstrapPending || stalled);
694
+ let subtitle = 'Checking account, workspace access, and setup status...';
695
+ if (bootstrapPhase === 'auth') {
696
+ subtitle = 'Verifying session...';
697
+ }
698
+ else if (bootstrapPhase === 'workspace') {
699
+ subtitle = 'Resolving workspace access...';
700
+ }
701
+ else if (bootstrapPhase === 'config') {
702
+ subtitle = 'Loading configuration...';
703
+ }
704
+ return {
705
+ phase: bootstrapPhase,
706
+ auth: {
707
+ resolved: authSessionResolved,
708
+ pending: authPending
709
+ },
710
+ config: {
711
+ loaded: configLoaded
712
+ },
713
+ workspace: {
714
+ pending: workspaceBootstrapPending
715
+ },
716
+ pending: authPending || appPending,
717
+ authPending,
718
+ appPending,
719
+ ready: authSessionResolved && configLoaded && !workspaceBootstrapPending && bootstrapPhase === 'ready',
720
+ stalled,
721
+ subtitle,
722
+ error: bootstrapError,
723
+ startedAt: bootstrapStartedAt
724
+ };
725
+ }, [
726
+ authSessionResolved,
727
+ bootstrapError,
728
+ bootstrapPhase,
729
+ bootstrapStartedAt,
730
+ configLoaded,
731
+ workspaceBootstrapPending
732
+ ]);
588
733
  const isBootstrapTimeoutError = useCallback((error) => {
589
734
  return String(error?.name || '') === 'BootstrapTimeoutError';
590
735
  }, []);
@@ -653,7 +798,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
653
798
  useEffect(() => {
654
799
  if (workspaceTransitionEpoch === 0)
655
800
  return;
656
- abortWorkspaceScopedRequests();
657
801
  referenceDataRequestRef.current = null;
658
802
  lastLoadedUiStateKeyRef.current = '';
659
803
  lastAssigneeOptionsLoadKeyRef.current = '';
@@ -668,8 +812,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
668
812
  setDeletedTasks([]);
669
813
  setTasks([]);
670
814
  setArchivedTasks([]);
671
- setInitiatives([]);
672
- setWorkstreams([]);
673
815
  setLoadingTasks(false);
674
816
  if (cloudRealtimeFollowUpRefreshTimerRef.current !== null && typeof window !== 'undefined') {
675
817
  window.clearTimeout(cloudRealtimeFollowUpRefreshTimerRef.current);
@@ -679,7 +821,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
679
821
  window.clearTimeout(archiveBootstrapTimerRef.current);
680
822
  archiveBootstrapTimerRef.current = null;
681
823
  }
682
- }, [abortWorkspaceScopedRequests, setArchivedTasks, setLoadingTasks, setTasks, workspaceTransitionEpoch]);
824
+ }, [setArchivedTasks, setLoadingTasks, setTasks, workspaceTransitionEpoch]);
683
825
  const fetchDeletedTasks = useCallback(async (isSilent = false, options) => {
684
826
  const ignoreAuthGuard = options?.ignoreAuthGuard === true;
685
827
  if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
@@ -727,7 +869,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
727
869
  }
728
870
  }
729
871
  catch (err) {
730
- if (isAbortError(err))
872
+ if (isExpectedWorkspaceAbortFailure(err, abortController.signal) || isAbortError(err))
731
873
  return;
732
874
  console.error('[Taskforce] Failed to fetch deleted tasks:', err);
733
875
  }
@@ -751,37 +893,119 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
751
893
  if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
752
894
  return;
753
895
  const requestState = getWorkspaceRequestState();
896
+ planningEntitiesAbortRef.current?.abort('superseded');
897
+ if (planningEntitiesRetryTimerRef.current !== null && typeof window !== 'undefined') {
898
+ window.clearTimeout(planningEntitiesRetryTimerRef.current);
899
+ planningEntitiesRetryTimerRef.current = null;
900
+ }
754
901
  const abortController = new AbortController();
755
902
  planningEntitiesAbortRef.current = abortController;
756
903
  try {
757
- const [initiativesRes, workstreamsRes] = await Promise.all([
758
- fetch('/api/taskforce/initiatives', { signal: abortController.signal }),
759
- fetch('/api/taskforce/workstreams', { signal: abortController.signal }),
760
- ]);
761
- if (initiativesRes.status === 401 || workstreamsRes.status === 401) {
904
+ const planningTraceId = buildBootstrapTraceId({
905
+ workspaceId: requestState.workspaceId,
906
+ epoch: requestState.epoch,
907
+ seed: bootstrapTraceSeedRef.current,
908
+ });
909
+ const fetchPlanningRoute = async (url) => {
910
+ if (typeof window === 'undefined') {
911
+ return fetch(url, withBootstrapTraceHeader({ signal: abortController.signal }, planningTraceId));
912
+ }
913
+ const timeoutController = new AbortController();
914
+ const timerId = window.setTimeout(() => timeoutController.abort('planning-timeout'), PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS);
915
+ const onAbort = () => timeoutController.abort(abortController.signal.reason || 'external-abort');
916
+ if (abortController.signal.aborted) {
917
+ onAbort();
918
+ }
919
+ else {
920
+ abortController.signal.addEventListener('abort', onAbort, { once: true });
921
+ }
922
+ try {
923
+ return await fetch(url, withBootstrapTraceHeader({ signal: timeoutController.signal }, planningTraceId));
924
+ }
925
+ catch (error) {
926
+ if (abortController.signal.aborted)
927
+ throw abortController.signal.reason || error;
928
+ if (hasAbortErrorName(error) || error === 'planning-timeout') {
929
+ return null;
930
+ }
931
+ throw error;
932
+ }
933
+ finally {
934
+ window.clearTimeout(timerId);
935
+ abortController.signal.removeEventListener('abort', onAbort);
936
+ }
937
+ };
938
+ let bootstrapPayload = null;
939
+ const bootstrapRes = await fetchPlanningRoute('/api/taskforce/planning/bootstrap');
940
+ if (bootstrapRes?.status === 401) {
762
941
  handleUnauthorized();
763
942
  setInitiatives([]);
764
943
  setWorkstreams([]);
944
+ setPlanningBootstrapTaskSummaries([]);
765
945
  return;
766
946
  }
767
- const [initiativesData, workstreamsData] = await Promise.all([
768
- initiativesRes.ok ? initiativesRes.json().catch(() => []) : [],
769
- workstreamsRes.ok ? workstreamsRes.json().catch(() => []) : [],
770
- ]);
947
+ if (bootstrapRes === null) {
948
+ throw createBootstrapTimeoutError(PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS);
949
+ }
950
+ if (bootstrapRes.ok) {
951
+ bootstrapPayload = sanitizePlanningBootstrapPayload(await bootstrapRes.json().catch(() => ({})));
952
+ }
953
+ else {
954
+ const [initiativesRes, workstreamsRes] = await Promise.all([
955
+ fetchPlanningRoute('/api/taskforce/initiatives'),
956
+ fetchPlanningRoute('/api/taskforce/workstreams'),
957
+ ]);
958
+ if (!initiativesRes || !workstreamsRes) {
959
+ throw new Error('Planning endpoints timed out during startup.');
960
+ }
961
+ if (initiativesRes.status === 401 || workstreamsRes.status === 401) {
962
+ handleUnauthorized();
963
+ setInitiatives([]);
964
+ setWorkstreams([]);
965
+ setPlanningBootstrapTaskSummaries([]);
966
+ return;
967
+ }
968
+ const [initiativesData, workstreamsData] = await Promise.all([
969
+ initiativesRes.ok ? initiativesRes.json().catch(() => []) : [],
970
+ workstreamsRes.ok ? workstreamsRes.json().catch(() => []) : [],
971
+ ]);
972
+ bootstrapPayload = sanitizePlanningBootstrapPayload({
973
+ initiatives: initiativesData,
974
+ workstreams: workstreamsData,
975
+ workstreamTaskSummaries: [],
976
+ });
977
+ }
771
978
  if (isWorkspaceRequestStale(requestState)) {
772
979
  return;
773
980
  }
774
- setInitiatives(Array.isArray(initiativesData)
775
- ? initiativesData.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
776
- : []);
777
- setWorkstreams(Array.isArray(workstreamsData)
778
- ? workstreamsData.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
779
- : []);
981
+ setInitiatives(bootstrapPayload.initiatives);
982
+ setWorkstreams(bootstrapPayload.workstreams);
983
+ setPlanningBootstrapTaskSummaries(bootstrapPayload.workstreamTaskSummaries);
780
984
  }
781
985
  catch (err) {
782
- if (isAbortError(err))
986
+ if (isExpectedWorkspaceAbortFailure(err, abortController.signal) || isAbortError(err))
783
987
  return;
784
- console.error('[Taskforce] Failed to fetch planning entities:', err);
988
+ const startupTimeout = isBootstrapTimeoutError(err);
989
+ if (startupTimeout) {
990
+ logBootstrapDebug('planning_bootstrap_timeout', {
991
+ workspaceId: requestState.workspaceId,
992
+ epoch: requestState.epoch,
993
+ timeoutMs: PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS,
994
+ });
995
+ console.warn('[Taskforce] Planning bootstrap timed out during startup; retrying in background.');
996
+ }
997
+ else {
998
+ console.error('[Taskforce] Failed to fetch planning entities:', err);
999
+ }
1000
+ if (typeof window !== 'undefined'
1001
+ && !isWorkspaceRequestStale(requestState)
1002
+ && initiatives.length === 0
1003
+ && workstreams.length === 0) {
1004
+ planningEntitiesRetryTimerRef.current = window.setTimeout(() => {
1005
+ planningEntitiesRetryTimerRef.current = null;
1006
+ void fetchPlanningEntities(options);
1007
+ }, 3000);
1008
+ }
785
1009
  }
786
1010
  finally {
787
1011
  if (planningEntitiesAbortRef.current === abortController) {
@@ -789,12 +1013,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
789
1013
  }
790
1014
  }
791
1015
  }, [
1016
+ BOOTSTRAP_REQUEST_TIMEOUT_MS,
1017
+ PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS,
1018
+ initiatives.length,
792
1019
  getWorkspaceRequestState,
793
1020
  handleUnauthorized,
794
1021
  isAbortError,
795
1022
  isWorkspaceRequestStale,
796
1023
  shouldDeferProtectedApiCalls,
797
1024
  shouldBlockProtectedApiCalls,
1025
+ workstreams.length,
798
1026
  ]);
799
1027
  const fetchTasks = useCallback(async (isSilent = false, options) => {
800
1028
  await fetchTasksBase(isSilent, options);
@@ -946,7 +1174,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
946
1174
  }
947
1175
  });
948
1176
  checkAuthSessionImplRef.current = checkAuthSessionImpl;
949
- const [activeWorkspaceModule, setActiveWorkspaceModule] = useState('tasks');
1177
+ const [activeWorkspaceModule, setActiveWorkspaceModuleState] = useState('tasks');
950
1178
  const [showComments, setShowComments] = useState(false);
951
1179
  const [descriptionFocused, setDescriptionFocused] = useState(false);
952
1180
  const [isCapturingScreenshot, setIsCapturingScreenshot] = useState(false);
@@ -967,10 +1195,17 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
967
1195
  const lastTaskSurfaceLoadKeyRef = useRef('');
968
1196
  const lastSettingsSurfaceLoadKeyRef = useRef('');
969
1197
  const skipNextUiStatePersistRef = useRef(false);
1198
+ const activeWorkspaceModuleOverridePendingRef = useRef(false);
970
1199
  const referenceDataRequestRef = useRef(null);
971
1200
  const workspaceListHydratedRef = useRef(false);
972
1201
  const hasBootstrappedDataRef = useRef(false);
973
1202
  const [uiStateReady, setUiStateReady] = useState(false);
1203
+ const setActiveWorkspaceModule = useCallback((nextModule) => {
1204
+ if (!uiStateReady) {
1205
+ activeWorkspaceModuleOverridePendingRef.current = true;
1206
+ }
1207
+ setActiveWorkspaceModuleState(nextModule);
1208
+ }, [uiStateReady]);
974
1209
  const { handleSaveSettings, handleSaveTheme, handleSaveGlobalTheme, handleJsonBackupEnabledChange, handleSaveGlobalJsonBackupEnabled, handleSaveGlobalWeekStartsOn, handleSaveLocale, handleManualComplexityEnabledChange, handleChecklistDropdownEnabledChange, handleShowTaskCardStatusLabelChange, handleResetProjectToGlobal } = useSettingsPersistence({
975
1210
  keyShortcut,
976
1211
  themeUseGlobalDefault,
@@ -1003,7 +1238,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1003
1238
  authRequiredForApiRef.current = authRequiredForApi;
1004
1239
  isAuthenticatedRef.current = isAuthenticated;
1005
1240
  }, [authSessionResolved, authRequiredForApi, isAuthenticated]);
1006
- const applyPersistedUiState = useCallback((state) => {
1241
+ const applyPersistedUiState = useCallback((state, options) => {
1007
1242
  if (!state)
1008
1243
  return;
1009
1244
  const hasPersistedFilterState = Array.isArray(state.filterCategories)
@@ -1013,11 +1248,13 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1013
1248
  || Array.isArray(state.filterAssignees)
1014
1249
  || typeof state.filterAssigneesAllSelected === 'boolean'
1015
1250
  || (state.filterTaxonomies && typeof state.filterTaxonomies === 'object');
1016
- if (typeof state.activeWorkspaceModule === 'string') {
1017
- setActiveWorkspaceModule(state.activeWorkspaceModule);
1018
- }
1019
- else if (state.groupBy === 'docs') {
1020
- setActiveWorkspaceModule('docs');
1251
+ if (!options?.skipActiveWorkspaceModule) {
1252
+ if (typeof state.activeWorkspaceModule === 'string') {
1253
+ setActiveWorkspaceModuleState(state.activeWorkspaceModule);
1254
+ }
1255
+ else if (state.groupBy === 'docs') {
1256
+ setActiveWorkspaceModuleState('docs');
1257
+ }
1021
1258
  }
1022
1259
  if (state.groupBy && state.groupBy !== 'docs')
1023
1260
  setGroupBy(state.groupBy);
@@ -1081,19 +1318,24 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1081
1318
  if (currentWorkspaceIdRef.current !== targetWorkspaceId) {
1082
1319
  return;
1083
1320
  }
1084
- applyPersistedUiState(state);
1321
+ applyPersistedUiState(state, {
1322
+ skipActiveWorkspaceModule: activeWorkspaceModuleOverridePendingRef.current
1323
+ });
1085
1324
  lastLoadedUiStateKeyRef.current = uiStateLoadKey;
1086
1325
  }
1087
1326
  catch {
1088
1327
  const localState = readPersistedAppUiState(workspaceUiStateScope);
1089
1328
  if (currentWorkspaceIdRef.current === targetWorkspaceId) {
1090
- applyPersistedUiState(localState);
1329
+ applyPersistedUiState(localState, {
1330
+ skipActiveWorkspaceModule: activeWorkspaceModuleOverridePendingRef.current
1331
+ });
1091
1332
  lastLoadedUiStateKeyRef.current = uiStateLoadKey;
1092
1333
  }
1093
1334
  }
1094
1335
  finally {
1095
1336
  if (currentWorkspaceIdRef.current !== targetWorkspaceId)
1096
1337
  return;
1338
+ activeWorkspaceModuleOverridePendingRef.current = false;
1097
1339
  skipNextUiStatePersistRef.current = true;
1098
1340
  setUiStateReady(true);
1099
1341
  }
@@ -1319,11 +1561,13 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1319
1561
  }
1320
1562
  catch (versionError) {
1321
1563
  if (isBootstrapTimeoutError(versionError)) {
1322
- markBootstrapStalled('Startup checks timed out.');
1564
+ logBootstrapDebug('build_info_timeout', {
1565
+ durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
1566
+ });
1323
1567
  }
1324
1568
  }
1325
- }, [fetchWithTimeout, isBootstrapTimeoutError, markBootstrapStalled]);
1326
- const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks } = useTaskforceWorkspaceBootstrap({
1569
+ }, [fetchWithTimeout, isBootstrapTimeoutError]);
1570
+ const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks, runCloudBootstrap } = useTaskforceWorkspaceBootstrap({
1327
1571
  shouldDeferProtectedApiCalls,
1328
1572
  shouldBlockProtectedApiCalls,
1329
1573
  getWorkspaceRequestState,
@@ -1363,6 +1607,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1363
1607
  authSessionResolved,
1364
1608
  isAuthenticated,
1365
1609
  authUserId,
1610
+ authUserAvatarUrl,
1366
1611
  setHydratedWorkspaceRole,
1367
1612
  setAssigneeOptions,
1368
1613
  setAssigneeOptionsLoaded,
@@ -1388,6 +1633,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1388
1633
  showArchive,
1389
1634
  taskScope,
1390
1635
  settingsSection,
1636
+ workspaceBootstrapPending,
1391
1637
  fetchTasks,
1392
1638
  fetchArchive,
1393
1639
  fetchPlanningEntities,
@@ -1403,12 +1649,35 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1403
1649
  useEffect(() => {
1404
1650
  if (!configLoaded || workspaceBootstrapPending)
1405
1651
  return;
1406
- const loadKey = `${runtimeMode}:${isAuthenticated ? 'auth' : 'guest'}:${currentWorkspaceId}`;
1652
+ const loadKey = `${runtimeMode}:${isAuthenticated ? 'auth' : 'guest'}:${currentWorkspaceId}:${authUserAvatarUrl || 'no-avatar'}`;
1407
1653
  if (assigneeOptionsLoaded && lastAssigneeOptionsLoadKeyRef.current === loadKey)
1408
1654
  return;
1409
1655
  lastAssigneeOptionsLoadKeyRef.current = loadKey;
1410
1656
  void fetchAssigneeOptions();
1411
- }, [assigneeOptionsLoaded, configLoaded, currentWorkspaceId, fetchAssigneeOptions, isAuthenticated, runtimeMode, workspaceBootstrapPending]);
1657
+ }, [assigneeOptionsLoaded, authUserAvatarUrl, configLoaded, currentWorkspaceId, fetchAssigneeOptions, isAuthenticated, runtimeMode, workspaceBootstrapPending]);
1658
+ useEffect(() => {
1659
+ if (!authSessionResolved || !isAuthenticated)
1660
+ return;
1661
+ const normalizedAuthUserId = String(authUserId || '').trim();
1662
+ if (!normalizedAuthUserId || normalizedAuthUserId === 'anonymous')
1663
+ return;
1664
+ const nextAvatarUrl = String(authUserAvatarUrl || '').trim() || null;
1665
+ setAssigneeOptions((previous) => {
1666
+ let changed = false;
1667
+ const next = previous.map((option) => {
1668
+ if (option.kind !== 'member' || String(option.value || '').trim() !== normalizedAuthUserId)
1669
+ return option;
1670
+ const currentAvatarUrl = typeof option.avatarUrl === 'string' && option.avatarUrl.trim().length > 0
1671
+ ? option.avatarUrl.trim()
1672
+ : null;
1673
+ if (currentAvatarUrl === nextAvatarUrl)
1674
+ return option;
1675
+ changed = true;
1676
+ return { ...option, avatarUrl: nextAvatarUrl };
1677
+ });
1678
+ return changed ? next : previous;
1679
+ });
1680
+ }, [authSessionResolved, authUserAvatarUrl, authUserId, isAuthenticated]);
1412
1681
  const currentWorkspaceRole = useMemo(() => {
1413
1682
  if (workspaceSwitchingEnabled) {
1414
1683
  const current = availableWorkspaces.find((workspace) => workspace.id === currentWorkspaceId);
@@ -1423,44 +1692,86 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1423
1692
  }, [isAuthenticated]);
1424
1693
  const continueAfterCommercialOnboarding = useCallback(async () => {
1425
1694
  setBootstrapError(null);
1426
- markBootstrapPhase('auth');
1427
- const authenticated = await checkAuthSession({ force: true });
1428
- if (!authenticated) {
1429
- await fetchConfig({ ignoreAuthGuard: true });
1430
- return { success: false, destination: 'login', error: 'Sign in required.', code: 'AUTH_REQUIRED' };
1695
+ const result = await runCloudBootstrap({
1696
+ reason: 'post-plans',
1697
+ ignoreAuthGuard: true,
1698
+ tasksSilent: true,
1699
+ includeDeferredArchive: false
1700
+ });
1701
+ if (!result.authenticated) {
1702
+ return { success: false, destination: 'login', error: result.error || 'Sign in required.', code: result.code || 'AUTH_REQUIRED' };
1431
1703
  }
1432
- if (workspaceSwitchingEnabled) {
1433
- const workspaceResolution = await resolveWorkspaceAfterAuth();
1704
+ if (!result.success) {
1705
+ return {
1706
+ success: false,
1707
+ destination: 'app',
1708
+ error: result.error || 'Unable to resolve workspace after plan selection.',
1709
+ code: result.code
1710
+ };
1711
+ }
1712
+ if (result.workspaceSetupRequired) {
1713
+ return { success: true, destination: 'setup' };
1714
+ }
1715
+ return { success: true, destination: 'app' };
1716
+ }, [
1717
+ runCloudBootstrap
1718
+ ]);
1719
+ const resolvePostAuthCloudEntry = useCallback(async (options) => {
1720
+ const reason = options?.reason || 'retry';
1721
+ const preferredWorkspaceId = typeof options?.preferredWorkspaceId === 'string' ? options.preferredWorkspaceId.trim() : '';
1722
+ const allowCommercialGate = options?.allowCommercialGate === true;
1723
+ if (preferredWorkspaceId && workspaceSwitchingEnabled) {
1724
+ const authenticated = await checkAuthSession({ force: true });
1725
+ if (!authenticated) {
1726
+ await fetchConfig({ ignoreAuthGuard: true });
1727
+ return { success: false, error: 'Sign in required.', code: 'AUTH_REQUIRED' };
1728
+ }
1729
+ const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
1434
1730
  if (!workspaceResolution.success) {
1435
1731
  await fetchConfig({ ignoreAuthGuard: true });
1436
1732
  return {
1437
1733
  success: false,
1438
- destination: 'app',
1439
- error: workspaceResolution.error || 'Unable to resolve workspace after plan selection.',
1734
+ error: workspaceResolution.error || 'Unable to resolve workspace access.',
1440
1735
  code: workspaceResolution.code
1441
1736
  };
1442
1737
  }
1443
1738
  if (workspaceResolution.workspaceSetupRequired) {
1444
1739
  await fetchConfig({ ignoreAuthGuard: true });
1445
- return { success: true, destination: 'setup' };
1740
+ return { success: true, workspaceSetupRequired: true };
1446
1741
  }
1447
1742
  }
1448
- await fetchBootstrapConfig();
1449
- await Promise.all([
1450
- fetchTasks(true, { ignoreAuthGuard: true }),
1451
- fetchReferenceData(),
1452
- fetchPlanningEntities()
1453
- ]);
1454
- return { success: true, destination: 'app' };
1743
+ const result = await runCloudBootstrap({
1744
+ reason,
1745
+ ignoreAuthGuard: true,
1746
+ tasksSilent: true,
1747
+ includeDeferredArchive: false
1748
+ });
1749
+ if (!result.authenticated) {
1750
+ return { success: false, error: result.error || 'Sign in required.', code: result.code || 'AUTH_REQUIRED' };
1751
+ }
1752
+ if (!result.success) {
1753
+ return {
1754
+ success: false,
1755
+ error: result.error || 'Unable to resolve workspace access.',
1756
+ code: result.code
1757
+ };
1758
+ }
1759
+ if (result.workspaceSetupRequired) {
1760
+ if (allowCommercialGate) {
1761
+ return { success: true, workspaceSetupRequired: false };
1762
+ }
1763
+ return { success: true, workspaceSetupRequired: true };
1764
+ }
1765
+ if (!allowCommercialGate) {
1766
+ void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
1767
+ }
1768
+ return { success: true, workspaceSetupRequired: false };
1455
1769
  }, [
1456
1770
  checkAuthSession,
1457
- fetchBootstrapConfig,
1458
1771
  fetchConfig,
1459
- fetchPlanningEntities,
1460
- fetchReferenceData,
1461
- fetchTasks,
1462
- markBootstrapPhase,
1463
1772
  resolveWorkspaceAfterAuth,
1773
+ runCloudBootstrap,
1774
+ syncUserGlobalSettings,
1464
1775
  workspaceSwitchingEnabled
1465
1776
  ]);
1466
1777
  const createWorkspace = useCallback(async (name, description) => {
@@ -1621,30 +1932,34 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1621
1932
  return { success: false, error: data?.error || 'Sign in failed.', code: data?.code };
1622
1933
  }
1623
1934
  setError('');
1624
- await checkAuthSession({ force: true });
1625
- const workspaceResolution = await resolveWorkspaceAfterAuth();
1626
- if (!workspaceResolution.success) {
1627
- return {
1628
- success: false,
1629
- error: workspaceResolution.error || 'Unable to resolve workspace after sign in.',
1630
- code: workspaceResolution.code
1631
- };
1632
- }
1633
- if (workspaceResolution.workspaceSetupRequired) {
1634
- await fetchConfig({ ignoreAuthGuard: true });
1635
- return { success: true, workspaceSetupRequired: true };
1935
+ const entry = await resolvePostAuthCloudEntry();
1936
+ if (!entry.success) {
1937
+ return { success: false, error: entry.error || 'Unable to resolve workspace after sign in.', code: entry.code };
1636
1938
  }
1637
- await Promise.all([
1638
- fetchConfig({ ignoreAuthGuard: true }),
1639
- fetchTasks(true, { ignoreAuthGuard: true })
1640
- ]);
1641
- void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
1642
- return { success: true };
1939
+ return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
1643
1940
  }
1644
1941
  catch {
1645
1942
  return { success: false, error: 'Sign in failed.' };
1646
1943
  }
1647
- }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
1944
+ }, [resolveCloudAuthUrl, cloudAuthConfigured, resolvePostAuthCloudEntry]);
1945
+ /**
1946
+ * Initiates an OAuth login (or invite-claim) flow by redirecting the browser
1947
+ * to the provider's authorisation URL via the server-side start route.
1948
+ *
1949
+ * @param provider - 'google' | 'github' | 'apple'
1950
+ * @param returnTo - Optional relative path to redirect back to after auth (defaults to '/').
1951
+ * @param inviteToken - Optional invite token; when provided the flow uses `invite_claim` mode.
1952
+ */
1953
+ const beginOAuthLogin = useCallback((provider, returnTo, inviteToken) => {
1954
+ const sanitizedReturnTo = returnTo && /^\/[A-Za-z0-9_\-./?=&%]*$/.test(returnTo)
1955
+ ? returnTo
1956
+ : '/';
1957
+ let path = `/api/taskforce/auth/oauth/${encodeURIComponent(provider)}/start?return_to=${encodeURIComponent(sanitizedReturnTo)}`;
1958
+ if (inviteToken) {
1959
+ path += `&invite_token=${encodeURIComponent(inviteToken)}`;
1960
+ }
1961
+ window.location.href = resolveCloudAuthUrl(path);
1962
+ }, [resolveCloudAuthUrl]);
1648
1963
  const registerWithCredentials = useCallback(async (email, password, options) => {
1649
1964
  if (!cloudAuthConfigured) {
1650
1965
  return { success: false, error: 'Cloud auth endpoint is not configured.' };
@@ -1694,32 +2009,42 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1694
2009
  const verificationRequired = Boolean(data?.verificationRequired);
1695
2010
  if (!verificationRequired) {
1696
2011
  setError('');
1697
- await checkAuthSession({ force: true });
1698
- const workspaceResolution = await resolveWorkspaceAfterAuth();
1699
- if (!workspaceResolution.success) {
1700
- return {
1701
- success: false,
1702
- error: workspaceResolution.error || 'Unable to resolve workspace after registration.',
1703
- code: workspaceResolution.code
1704
- };
1705
- }
1706
2012
  const hasCommercialOnboardingGate = Boolean(planSelectionRequired
1707
2013
  || checkoutPending
1708
2014
  || commercialState === 'pending_plan_selection'
1709
2015
  || commercialState === 'checkout_pending');
1710
- if (workspaceResolution.workspaceSetupRequired || hasCommercialOnboardingGate) {
1711
- await fetchConfig({ ignoreAuthGuard: true });
2016
+ if (hasCommercialOnboardingGate) {
2017
+ const authenticated = await checkAuthSession({ force: true });
2018
+ if (!authenticated) {
2019
+ return {
2020
+ success: false,
2021
+ error: 'Unable to resolve workspace after registration.',
2022
+ code: 'AUTH_REQUIRED'
2023
+ };
2024
+ }
2025
+ return {
2026
+ success: true,
2027
+ workspaceSetupRequired: false,
2028
+ planSelectionRequired,
2029
+ checkoutPending,
2030
+ commercialState,
2031
+ verificationRequired,
2032
+ verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
2033
+ emailSent: data?.emailSent !== false,
2034
+ emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
2035
+ };
1712
2036
  }
1713
- else {
1714
- await Promise.all([
1715
- fetchConfig({ ignoreAuthGuard: true }),
1716
- fetchTasks(true, { ignoreAuthGuard: true })
1717
- ]);
1718
- void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
2037
+ const entry = await resolvePostAuthCloudEntry({ allowCommercialGate: false });
2038
+ if (!entry.success) {
2039
+ return {
2040
+ success: false,
2041
+ error: entry.error || 'Unable to resolve workspace after registration.',
2042
+ code: entry.code
2043
+ };
1719
2044
  }
1720
2045
  return {
1721
2046
  success: true,
1722
- workspaceSetupRequired: workspaceResolution.workspaceSetupRequired,
2047
+ workspaceSetupRequired: entry.workspaceSetupRequired === true,
1723
2048
  planSelectionRequired,
1724
2049
  checkoutPending,
1725
2050
  commercialState,
@@ -1744,7 +2069,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1744
2069
  catch {
1745
2070
  return { success: false, error: 'Create account failed.' };
1746
2071
  }
1747
- }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
2072
+ }, [checkAuthSession, resolveCloudAuthUrl, cloudAuthConfigured, resolvePostAuthCloudEntry]);
1748
2073
  const updateCurrentUserProfile = useCallback(async (input) => {
1749
2074
  if (!cloudAuthConfigured) {
1750
2075
  return { success: false, error: 'Cloud auth endpoint is not configured.' };
@@ -1925,7 +2250,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1925
2250
  email: typeof data?.email === 'string' ? data.email : undefined,
1926
2251
  workspaceId: typeof data?.workspaceId === 'string' ? data.workspaceId : undefined,
1927
2252
  inviteeKind: data?.inviteeKind === 'existing_user' ? 'existing_user' : 'new_user',
1928
- passwordRequired: data?.passwordRequired === true
2253
+ passwordRequired: data?.passwordRequired === true,
2254
+ inviteeState: data?.inviteeState === 'existing_account' ? 'existing_account' : 'pending_setup',
2255
+ availableMethods: Array.isArray(data?.availableMethods) ? data.availableMethods : []
1929
2256
  };
1930
2257
  }
1931
2258
  catch {
@@ -1953,30 +2280,30 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1953
2280
  return { success: false, error: data?.error || 'Invite acceptance failed.', code: data?.code };
1954
2281
  }
1955
2282
  setError('');
1956
- await checkAuthSession({ force: true });
1957
2283
  const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
1958
- const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
1959
- if (!workspaceResolution.success) {
1960
- return {
1961
- success: false,
1962
- error: workspaceResolution.error || 'Unable to resolve workspace after invite acceptance.',
1963
- code: workspaceResolution.code
1964
- };
2284
+ if (preferredWorkspaceId && workspaceSwitchingEnabled) {
2285
+ const switched = await switchWorkspace(preferredWorkspaceId, { hydrate: false });
2286
+ if (!switched.success) {
2287
+ return {
2288
+ success: false,
2289
+ error: switched.error || 'Unable to resolve workspace after invite acceptance.',
2290
+ code: switched.code
2291
+ };
2292
+ }
1965
2293
  }
1966
- if (workspaceResolution.workspaceSetupRequired) {
1967
- return { success: true, workspaceSetupRequired: true };
2294
+ const entry = await resolvePostAuthCloudEntry();
2295
+ if (!entry.success) {
2296
+ return { success: false, error: entry.error || 'Unable to resolve workspace after invite acceptance.', code: entry.code };
1968
2297
  }
1969
- await Promise.all([
1970
- fetchConfig({ ignoreAuthGuard: true }),
1971
- fetchTasks(true, { ignoreAuthGuard: true })
1972
- ]);
1973
- void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
1974
- return { success: true };
2298
+ if (preferredWorkspaceId && currentWorkspaceIdRef.current !== preferredWorkspaceId) {
2299
+ commitCurrentWorkspaceId(preferredWorkspaceId, { clearExplicitSelection: false });
2300
+ }
2301
+ return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
1975
2302
  }
1976
2303
  catch {
1977
2304
  return { success: false, error: 'Invite acceptance failed.' };
1978
2305
  }
1979
- }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
2306
+ }, [commitCurrentWorkspaceId, resolveCloudAuthUrl, cloudAuthConfigured, currentWorkspaceIdRef, resolvePostAuthCloudEntry, switchWorkspace, workspaceSwitchingEnabled]);
1980
2307
  const joinInviteWithToken = useCallback(async (token) => {
1981
2308
  if (!cloudAuthConfigured) {
1982
2309
  return { success: false, error: 'Cloud auth endpoint is not configured.' };
@@ -1997,30 +2324,93 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1997
2324
  return { success: false, error: data?.error || 'Invite join failed.', code: data?.code };
1998
2325
  }
1999
2326
  setError('');
2000
- await checkAuthSession({ force: true });
2001
2327
  const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
2002
- const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
2003
- if (!workspaceResolution.success) {
2004
- return {
2005
- success: false,
2006
- error: workspaceResolution.error || 'Unable to resolve workspace after invite join.',
2007
- code: workspaceResolution.code
2008
- };
2328
+ if (preferredWorkspaceId && workspaceSwitchingEnabled) {
2329
+ const switched = await switchWorkspace(preferredWorkspaceId, { hydrate: false });
2330
+ if (!switched.success) {
2331
+ return {
2332
+ success: false,
2333
+ error: switched.error || 'Unable to resolve workspace after invite join.',
2334
+ code: switched.code
2335
+ };
2336
+ }
2009
2337
  }
2010
- if (workspaceResolution.workspaceSetupRequired) {
2011
- return { success: true, workspaceSetupRequired: true };
2338
+ const entry = await resolvePostAuthCloudEntry();
2339
+ if (!entry.success) {
2340
+ return { success: false, error: entry.error || 'Unable to resolve workspace after invite join.', code: entry.code };
2012
2341
  }
2013
- await Promise.all([
2014
- fetchConfig({ ignoreAuthGuard: true }),
2015
- fetchTasks(true, { ignoreAuthGuard: true })
2016
- ]);
2017
- void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
2018
- return { success: true };
2342
+ if (preferredWorkspaceId && currentWorkspaceIdRef.current !== preferredWorkspaceId) {
2343
+ commitCurrentWorkspaceId(preferredWorkspaceId, { clearExplicitSelection: false });
2344
+ }
2345
+ return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
2019
2346
  }
2020
2347
  catch {
2021
2348
  return { success: false, error: 'Invite join failed.' };
2022
2349
  }
2023
- }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
2350
+ }, [commitCurrentWorkspaceId, resolveCloudAuthUrl, cloudAuthConfigured, currentWorkspaceIdRef, resolvePostAuthCloudEntry, switchWorkspace, workspaceSwitchingEnabled]);
2351
+ // ─── Login method management ────────────────────────────────────────────────
2352
+ const fetchLoginMethods = useCallback(async () => {
2353
+ if (!cloudAuthConfigured)
2354
+ return { success: false, error: 'Cloud auth not configured.' };
2355
+ try {
2356
+ const res = await fetch(resolveCloudAuthUrl('/api/taskforce/account/login-methods'), {
2357
+ credentials: 'include'
2358
+ });
2359
+ const data = await res.json().catch(() => ({}));
2360
+ if (!res.ok || !data?.success) {
2361
+ return { success: false, error: data?.error || 'Failed to fetch login methods.' };
2362
+ }
2363
+ return { success: true, methods: Array.isArray(data?.methods) ? data.methods : [] };
2364
+ }
2365
+ catch {
2366
+ return { success: false, error: 'Failed to fetch login methods.' };
2367
+ }
2368
+ }, [resolveCloudAuthUrl, cloudAuthConfigured]);
2369
+ const unlinkLoginMethod = useCallback(async (provider) => {
2370
+ if (!cloudAuthConfigured)
2371
+ return { success: false, error: 'Cloud auth not configured.' };
2372
+ try {
2373
+ const res = await fetch(resolveCloudAuthUrl(`/api/taskforce/account/login-methods/${encodeURIComponent(provider)}`), {
2374
+ method: 'DELETE',
2375
+ credentials: 'include'
2376
+ });
2377
+ const data = await res.json().catch(() => ({}));
2378
+ if (!res.ok || !data?.success) {
2379
+ return { success: false, error: data?.error || 'Unlink failed.', code: data?.code };
2380
+ }
2381
+ return { success: true };
2382
+ }
2383
+ catch {
2384
+ return { success: false, error: 'Unlink failed.' };
2385
+ }
2386
+ }, [resolveCloudAuthUrl, cloudAuthConfigured]);
2387
+ const addPasswordToAccount = useCallback(async (password) => {
2388
+ if (!cloudAuthConfigured)
2389
+ return { success: false, error: 'Cloud auth not configured.' };
2390
+ try {
2391
+ const res = await fetch(resolveCloudAuthUrl('/api/taskforce/account/login-methods/password'), {
2392
+ method: 'POST',
2393
+ headers: { 'Content-Type': 'application/json' },
2394
+ credentials: 'include',
2395
+ body: JSON.stringify({ password })
2396
+ });
2397
+ const data = await res.json().catch(() => ({}));
2398
+ if (!res.ok || !data?.success) {
2399
+ return { success: false, error: data?.error || 'Failed to add password.', code: data?.code };
2400
+ }
2401
+ return { success: true };
2402
+ }
2403
+ catch {
2404
+ return { success: false, error: 'Failed to add password.' };
2405
+ }
2406
+ }, [resolveCloudAuthUrl, cloudAuthConfigured]);
2407
+ const beginOAuthLink = useCallback((provider, returnTo) => {
2408
+ const sanitizedReturnTo = returnTo && /^\/[A-Za-z0-9_\-./?=&%]*$/.test(returnTo)
2409
+ ? returnTo
2410
+ : '/';
2411
+ const path = `/api/taskforce/auth/oauth/${encodeURIComponent(provider)}/start?flow=link&return_to=${encodeURIComponent(sanitizedReturnTo)}`;
2412
+ window.location.href = resolveCloudAuthUrl(path);
2413
+ }, [resolveCloudAuthUrl]);
2024
2414
  const resetCloudLogoutClientState = useCallback(() => {
2025
2415
  abortWorkspaceScopedRequests();
2026
2416
  referenceDataRequestRef.current = null;
@@ -2039,6 +2429,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2039
2429
  setArchivedTasks([]);
2040
2430
  setInitiatives([]);
2041
2431
  setWorkstreams([]);
2432
+ setPlanningBootstrapTaskSummaries([]);
2042
2433
  setLoadingTasks(false);
2043
2434
  if (cloudRealtimeFollowUpRefreshTimerRef.current !== null && typeof window !== 'undefined') {
2044
2435
  window.clearTimeout(cloudRealtimeFollowUpRefreshTimerRef.current);
@@ -2149,6 +2540,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2149
2540
  throw new Error(data?.error || `Failed to create initiative (${res.status})`);
2150
2541
  }
2151
2542
  await fetchPlanningEntities();
2543
+ setInitiatives((current) => upsertEntityById(current, data));
2152
2544
  return data;
2153
2545
  }, [fetchPlanningEntities]);
2154
2546
  const updateInitiative = useCallback(async (id, payload) => {
@@ -2162,6 +2554,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2162
2554
  throw new Error(data?.error || `Failed to update initiative (${res.status})`);
2163
2555
  }
2164
2556
  await fetchPlanningEntities();
2557
+ setInitiatives((current) => upsertEntityById(current, data));
2165
2558
  return data;
2166
2559
  }, [fetchPlanningEntities]);
2167
2560
  const archiveInitiative = useCallback(async (id) => {
@@ -2173,6 +2566,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2173
2566
  throw new Error(data?.error || `Failed to archive initiative (${res.status})`);
2174
2567
  }
2175
2568
  await fetchPlanningEntities();
2569
+ setInitiatives((current) => upsertEntityById(current, data));
2176
2570
  return data;
2177
2571
  }, [fetchPlanningEntities]);
2178
2572
  const unarchiveInitiative = useCallback(async (id) => {
@@ -2184,6 +2578,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2184
2578
  throw new Error(data?.error || `Failed to unarchive initiative (${res.status})`);
2185
2579
  }
2186
2580
  await fetchPlanningEntities();
2581
+ setInitiatives((current) => upsertEntityById(current, data));
2187
2582
  return data;
2188
2583
  }, [fetchPlanningEntities]);
2189
2584
  const createWorkstream = useCallback(async (payload) => {
@@ -2197,6 +2592,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2197
2592
  throw new Error(data?.error || `Failed to create workstream (${res.status})`);
2198
2593
  }
2199
2594
  await fetchPlanningEntities();
2595
+ setWorkstreams((current) => upsertEntityById(current, data));
2200
2596
  return data;
2201
2597
  }, [fetchPlanningEntities]);
2202
2598
  const updateWorkstream = useCallback(async (id, payload) => {
@@ -2210,6 +2606,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2210
2606
  throw new Error(data?.error || `Failed to update workstream (${res.status})`);
2211
2607
  }
2212
2608
  await fetchPlanningEntities();
2609
+ setWorkstreams((current) => upsertEntityById(current, data));
2213
2610
  return data;
2214
2611
  }, [fetchPlanningEntities]);
2215
2612
  const archiveWorkstream = useCallback(async (id) => {
@@ -2221,6 +2618,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2221
2618
  throw new Error(data?.error || `Failed to archive workstream (${res.status})`);
2222
2619
  }
2223
2620
  await fetchPlanningEntities();
2621
+ setWorkstreams((current) => upsertEntityById(current, data));
2224
2622
  return data;
2225
2623
  }, [fetchPlanningEntities]);
2226
2624
  const unarchiveWorkstream = useCallback(async (id) => {
@@ -2232,6 +2630,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2232
2630
  throw new Error(data?.error || `Failed to unarchive workstream (${res.status})`);
2233
2631
  }
2234
2632
  await fetchPlanningEntities();
2633
+ setWorkstreams((current) => upsertEntityById(current, data));
2235
2634
  return data;
2236
2635
  }, [fetchPlanningEntities]);
2237
2636
  // Initialize auth once, then bootstrap protected data once auth gates are settled.
@@ -2248,6 +2647,38 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2248
2647
  return;
2249
2648
  void checkAuthSession();
2250
2649
  }, [runtimeConfigReady, checkAuthSession]);
2650
+ // Fetch available OAuth providers once we know the auth endpoint.
2651
+ // Uses a short timeout so an unreachable auth server doesn't hold open
2652
+ // browser connections and slow down subsequent requests on the same origin.
2653
+ useEffect(() => {
2654
+ if (!cloudAuthConfigured)
2655
+ return;
2656
+ let cancelled = false;
2657
+ const controller = new AbortController();
2658
+ const timerId = typeof window !== 'undefined'
2659
+ ? window.setTimeout(() => controller.abort(), 5000)
2660
+ : null;
2661
+ fetch(resolveCloudAuthUrl('/api/taskforce/auth/providers'), {
2662
+ credentials: 'include',
2663
+ signal: controller.signal
2664
+ })
2665
+ .then(r => r.ok ? r.json() : null)
2666
+ .then(data => {
2667
+ if (!cancelled && Array.isArray(data?.providers)) {
2668
+ setAvailableAuthProviders(data.providers);
2669
+ }
2670
+ })
2671
+ .catch(() => { })
2672
+ .finally(() => {
2673
+ if (timerId !== null && typeof window !== 'undefined') {
2674
+ window.clearTimeout(timerId);
2675
+ }
2676
+ });
2677
+ return () => {
2678
+ cancelled = true;
2679
+ controller.abort();
2680
+ };
2681
+ }, [cloudAuthConfigured, resolveCloudAuthUrl]);
2251
2682
  // Keep local runtime auth state aligned with cloud session changes
2252
2683
  // (e.g. user signs in/out in another tab or cloud-hosted app).
2253
2684
  useEffect(() => {
@@ -2271,109 +2702,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2271
2702
  window.clearInterval(intervalId);
2272
2703
  };
2273
2704
  }, [runtimeMode, shouldProbeCloudAuth, checkAuthSession]);
2274
- useEffect(() => {
2275
- if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
2276
- return;
2277
- if (hasBootstrappedDataRef.current)
2278
- return;
2279
- hasBootstrappedDataRef.current = true;
2280
- void (async () => {
2281
- if (runtimeMode === 'cloud' && authSessionResolved && isAuthenticated) {
2282
- const workspaceResolution = await resolveWorkspaceAfterAuth();
2283
- if (!workspaceResolution.success || workspaceResolution.workspaceSetupRequired) {
2284
- await fetchBootstrapConfig();
2285
- void fetchReferenceData();
2286
- return;
2287
- }
2288
- }
2289
- await fetchBootstrapConfig();
2290
- await Promise.all([
2291
- (async () => {
2292
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
2293
- await fetchTasks(false);
2294
- logBootstrapDebug('bootstrap_tasks_loaded', {
2295
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
2296
- });
2297
- })(),
2298
- fetchReferenceData(),
2299
- fetchPlanningEntities()
2300
- ]);
2301
- if (typeof window !== 'undefined') {
2302
- if (archiveBootstrapTimerRef.current !== null) {
2303
- window.clearTimeout(archiveBootstrapTimerRef.current);
2304
- }
2305
- archiveBootstrapTimerRef.current = window.setTimeout(() => {
2306
- archiveBootstrapTimerRef.current = null;
2307
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
2308
- void fetchArchive(true).then(() => {
2309
- logBootstrapDebug('bootstrap_archive_loaded_deferred', {
2310
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
2311
- });
2312
- });
2313
- }, 1500);
2314
- }
2315
- })();
2316
- }, [
2317
- shouldDeferProtectedApiCalls,
2318
- shouldBlockProtectedApiCalls,
2319
- runtimeMode,
2320
- authSessionResolved,
2321
- isAuthenticated,
2322
- resolveWorkspaceAfterAuth,
2323
- fetchBootstrapConfig,
2324
- fetchTasks,
2325
- fetchArchive,
2326
- fetchReferenceData,
2327
- fetchPlanningEntities
2328
- ]);
2329
- useEffect(() => {
2330
- if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
2331
- return;
2332
- if (workspaceBootstrapPending)
2333
- return;
2334
- if (workspaceSelectionScope === WORKSPACE_BOOTSTRAP_SCOPE)
2335
- return;
2336
- if (!hasBootstrappedDataRef.current)
2337
- return;
2338
- const loadKey = `${runtimeMode}:${currentWorkspaceId}:${workspaceSelectionScope}`;
2339
- if (lastConfigRefreshKeyRef.current === loadKey)
2340
- return;
2341
- lastConfigRefreshKeyRef.current = loadKey;
2342
- void fetchConfig();
2343
- }, [
2344
- currentWorkspaceId,
2345
- shouldDeferProtectedApiCalls,
2346
- shouldBlockProtectedApiCalls,
2347
- workspaceBootstrapPending,
2348
- runtimeMode,
2349
- workspaceSelectionScope,
2350
- fetchConfig
2351
- ]);
2352
- useEffect(() => {
2353
- if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
2354
- return;
2355
- if (workspaceBootstrapPending)
2356
- return;
2357
- const loadKey = currentWorkspaceId;
2358
- if (lastWorkspaceSyncStateLoadKeyRef.current === loadKey)
2359
- return;
2360
- lastWorkspaceSyncStateLoadKeyRef.current = loadKey;
2361
- void loadWorkspaceSyncState();
2362
- }, [shouldDeferProtectedApiCalls, shouldBlockProtectedApiCalls, currentWorkspaceId, loadWorkspaceSyncState, workspaceBootstrapPending]);
2363
- useEffect(() => {
2364
- if (runtimeMode !== 'cloud' || !authSessionResolved || !isAuthenticated) {
2365
- if (runtimeMode !== 'cloud') {
2366
- setAvailableWorkspaces([]);
2367
- }
2368
- workspaceListHydratedRef.current = false;
2369
- return;
2370
- }
2371
- if (workspaceBootstrapPending)
2372
- return;
2373
- if (!hasBootstrappedDataRef.current || workspaceListHydratedRef.current)
2374
- return;
2375
- fetchWorkspaces();
2376
- }, [runtimeMode, authSessionResolved, isAuthenticated, fetchWorkspaces, workspaceBootstrapPending]);
2377
2705
  // Report task count to parent
2378
2706
  useEffect(() => {
2379
2707
  if (onTaskCountChange) {
@@ -2539,6 +2867,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2539
2867
  return;
2540
2868
  lastTaskSurfaceLoadKeyRef.current = loadKey;
2541
2869
  fetchTasks();
2870
+ fetchPlanningEntities();
2542
2871
  if (showArchive || taskScope === 'archived') {
2543
2872
  fetchArchive(true);
2544
2873
  }
@@ -2565,6 +2894,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2565
2894
  showArchive,
2566
2895
  taskScope,
2567
2896
  fetchTasks,
2897
+ fetchPlanningEntities,
2568
2898
  fetchArchive,
2569
2899
  fetchDeletedTasks,
2570
2900
  fetchConfig,
@@ -3023,6 +3353,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3023
3353
  workspaceCloudSyncEnabled,
3024
3354
  saveWorkspaceCloudSyncSettings,
3025
3355
  applyWorkspaceSyncStateSnapshot,
3356
+ bootstrapState,
3026
3357
  authSessionResolved,
3027
3358
  workspaceBootstrapPending,
3028
3359
  bootstrapPhase,
@@ -3039,6 +3370,12 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3039
3370
  deleteWorkspace,
3040
3371
  switchWorkspace,
3041
3372
  loginWithCredentials,
3373
+ beginOAuthLogin,
3374
+ availableAuthProviders,
3375
+ fetchLoginMethods,
3376
+ unlinkLoginMethod,
3377
+ addPasswordToAccount,
3378
+ beginOAuthLink,
3042
3379
  registerWithCredentials,
3043
3380
  updateCurrentUserProfile,
3044
3381
  requestEmailVerification,
@@ -3071,6 +3408,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3071
3408
  archivedTasks,
3072
3409
  initiatives,
3073
3410
  workstreams,
3411
+ planningBootstrapTaskSummaries,
3074
3412
  deletedTasks,
3075
3413
  activeCategories,
3076
3414
  activeTypes,
@@ -3093,6 +3431,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3093
3431
  setFilterAssignees,
3094
3432
  filterTaxonomies,
3095
3433
  setFilterTaxonomies,
3434
+ hasInitedFilters,
3096
3435
  sortBy,
3097
3436
  setSortBy,
3098
3437
  sortOrder,