@taskforcehq/taskforce 0.3.303 → 0.3.305

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 (237) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +102 -5
  3. package/dist/TaskforceCore.js +635 -238
  4. package/dist/TaskforceCore.test.js +2243 -495
  5. package/dist/annotatedAttachments/service.d.ts +1 -1
  6. package/dist/annotatedAttachments/types.d.ts +1 -1
  7. package/dist/compat/workspaceSyncCompat.js +6 -0
  8. package/dist/components/features/AgentsModule.js +1 -1
  9. package/dist/components/features/AgentsModule.test.js +1 -1
  10. package/dist/components/features/DocumentIndex.d.ts +1 -1
  11. package/dist/components/features/DocumentIndex.js +1 -1
  12. package/dist/components/features/DocumentViewer.d.ts +1 -1
  13. package/dist/components/features/DocumentViewer.js +32 -9
  14. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  15. package/dist/components/features/DocumentWorkspace.js +12 -155
  16. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  17. package/dist/components/features/TaskSettings.js +87 -41
  18. package/dist/components/features/TaskSettings.test.js +180 -40
  19. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  20. package/dist/components/features/documentWorkspaceModel.js +57 -0
  21. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  22. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  23. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  24. package/dist/components/task/TaskActionHeader.js +3 -3
  25. package/dist/components/task/TaskActionHeader.test.js +8 -0
  26. package/dist/components/task/TaskCard.js +16 -1
  27. package/dist/components/task/TaskCard.test.js +31 -0
  28. package/dist/components/task/TaskForm.d.ts +9 -8
  29. package/dist/components/task/TaskForm.js +374 -219
  30. package/dist/components/task/TaskForm.test.js +482 -361
  31. package/dist/components/task/TaskKanban.d.ts +1 -1
  32. package/dist/components/task/TaskKanban.js +33 -161
  33. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  34. package/dist/components/task/TaskStatusActions.js +8 -3
  35. package/dist/components/task/TaskStatusActions.test.js +9 -0
  36. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  37. package/dist/components/task/taskKanbanDropRules.js +187 -0
  38. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  39. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  40. package/dist/components/ui/Modal.d.ts +1 -1
  41. package/dist/components/ui/Modal.js +1 -0
  42. package/dist/components/views/AppShellHeader.js +5 -1
  43. package/dist/components/views/AppShellHeader.test.js +4 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  45. package/dist/components/views/PlanComparisonPage.js +9 -10
  46. package/dist/components/views/PlanComparisonPage.test.js +55 -41
  47. package/dist/components/views/PlansPage.d.ts +5 -1
  48. package/dist/components/views/PlansPage.js +150 -81
  49. package/dist/components/views/PlansPage.test.js +367 -41
  50. package/dist/components/views/StandaloneLayout.js +135 -54
  51. package/dist/components/views/WidgetView.js +11 -2
  52. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  53. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  54. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  55. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  56. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  57. package/dist/config/envSchema.js +1 -1
  58. package/dist/core/AiProfileService.d.ts +6 -1
  59. package/dist/core/AiProfileService.js +161 -16
  60. package/dist/core/AiProfileService.test.js +3 -1
  61. package/dist/core/AiProfiles.test.js +200 -0
  62. package/dist/core/AuthTokenService.test.d.ts +1 -0
  63. package/dist/core/AuthTokenService.test.js +78 -0
  64. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  65. package/dist/core/EntitlementsPolicy.test.js +75 -13
  66. package/dist/core/PlanEntitlementService.d.ts +9 -1
  67. package/dist/core/PlanEntitlementService.js +144 -19
  68. package/dist/core/PlanVersionPolicy.test.js +19 -26
  69. package/dist/core/SignupMonetizationSettings.test.js +75 -15
  70. package/dist/core/SystemAdmin.test.js +212 -56
  71. package/dist/core/TaskActivityService.d.ts +4 -1
  72. package/dist/core/TaskActivityService.js +45 -0
  73. package/dist/core/TaskAuditTimeline.test.js +52 -1
  74. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  75. package/dist/core/Taskforce.d.ts +18 -11
  76. package/dist/core/Taskforce.js +93 -12
  77. package/dist/core/WorkspacePlanMode.test.js +73 -8
  78. package/dist/core/shared.d.ts +2 -0
  79. package/dist/core/shared.js +11 -0
  80. package/dist/documentReviews/service.d.ts +1 -1
  81. package/dist/documentReviews/types.d.ts +1 -1
  82. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  83. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  84. package/dist/hooks/sync/auth.d.ts +37 -0
  85. package/dist/hooks/sync/auth.js +63 -0
  86. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  87. package/dist/hooks/sync/bootstrap.js +52 -0
  88. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  89. package/dist/hooks/sync/orchestratorShared.js +209 -0
  90. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  91. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  92. package/dist/hooks/sync/realtime.d.ts +25 -0
  93. package/dist/hooks/sync/realtime.js +60 -0
  94. package/dist/hooks/sync/recovery.d.ts +69 -0
  95. package/dist/hooks/sync/recovery.js +118 -0
  96. package/dist/hooks/sync/transfers.d.ts +123 -0
  97. package/dist/hooks/sync/transfers.js +435 -0
  98. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  99. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  100. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  101. package/dist/hooks/ui/useResourceExport.js +5 -2
  102. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  103. package/dist/hooks/useRealtimeSync.js +2 -1
  104. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  105. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  106. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  107. package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
  108. package/dist/hooks/useSyncOrchestrator.js +388 -879
  109. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
  110. package/dist/hooks/useTaskMutations.d.ts +2 -1
  111. package/dist/hooks/useTaskMutations.js +57 -7
  112. package/dist/hooks/useTaskMutations.test.js +176 -0
  113. package/dist/hooks/useTaskforce.d.ts +13 -4
  114. package/dist/hooks/useTaskforce.js +302 -817
  115. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  116. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  117. package/dist/hooks/useWorkspaceSyncController.js +33 -11
  118. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  119. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  120. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  121. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  122. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  123. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  124. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  125. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  126. package/dist/localization/locales/en-US.js +2 -2
  127. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  128. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  129. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  130. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  131. package/dist/mcp/clientIntegrations.d.ts +12 -1
  132. package/dist/mcp/clientIntegrations.js +161 -6
  133. package/dist/mcp/clientIntegrations.test.js +76 -4
  134. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  135. package/dist/mcp/collaborationRegistrar.js +71 -0
  136. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  137. package/dist/mcp/documentAssetRegistrar.js +346 -0
  138. package/dist/mcp/documentHelpers.d.ts +100 -0
  139. package/dist/mcp/documentHelpers.js +161 -0
  140. package/dist/mcp/runtime.js +2455 -3925
  141. package/dist/mcp/runtime.test.js +240 -8
  142. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  143. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  144. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  145. package/dist/mcp/taskAttachmentTypes.js +1 -0
  146. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  147. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  148. package/dist/mcp/toolCatalog.d.ts +35 -0
  149. package/dist/mcp/toolCatalog.js +3 -0
  150. package/dist/plugins/vite.js +1 -1
  151. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  152. package/dist/runtime/appGateDefinitions.js +1 -1
  153. package/dist/server/index.d.ts +1 -0
  154. package/dist/server/index.js +46 -3
  155. package/dist/server/index.test.js +12 -1
  156. package/dist/server/routes/admin.d.ts +1 -5
  157. package/dist/server/routes/admin.js +151 -80
  158. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  159. package/dist/server/routes/annotatedAttachments.js +1 -1
  160. package/dist/server/routes/auth.d.ts +1 -4
  161. package/dist/server/routes/auth.js +59 -64
  162. package/dist/server/routes/authSupport.d.ts +30 -0
  163. package/dist/server/routes/authSupport.js +81 -0
  164. package/dist/server/routes/billing.d.ts +1 -1
  165. package/dist/server/routes/billing.js +122 -255
  166. package/dist/server/routes/billing.test.js +257 -86
  167. package/dist/server/routes/documentReviews.d.ts +1 -1
  168. package/dist/server/routes/documentReviews.js +1 -1
  169. package/dist/server/routes/documents.d.ts +4 -2
  170. package/dist/server/routes/documents.js +73 -7
  171. package/dist/server/routes/primitives.d.ts +11 -0
  172. package/dist/server/routes/primitives.js +73 -0
  173. package/dist/server/routes/resources.d.ts +1 -1
  174. package/dist/server/routes/resources.js +1 -1
  175. package/dist/server/routes/shared.d.ts +31 -1
  176. package/dist/server/routes/shared.js +26 -0
  177. package/dist/server/routes/sync.d.ts +1 -1
  178. package/dist/server/routes/sync.integration.test.js +20 -0
  179. package/dist/server/routes/sync.js +111 -238
  180. package/dist/server/routes/tasks.d.ts +1 -1
  181. package/dist/server/routes/tasks.js +50 -8
  182. package/dist/server/routes/workspaces.d.ts +1 -1
  183. package/dist/server/routes/workspaces.js +1 -1
  184. package/dist/server/routes.d.ts +2 -9
  185. package/dist/server/routes.js +84 -181
  186. package/dist/server/routes.test.js +912 -32
  187. package/dist/shared/taskActor.d.ts +10 -0
  188. package/dist/shared/taskActor.js +1 -0
  189. package/dist/sync/collaborationSyncPayload.js +4 -0
  190. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  191. package/dist/sync/collaborationSyncState.d.ts +3 -1
  192. package/dist/sync/collaborationSyncState.js +19 -0
  193. package/dist/sync/contentSyncState.js +28 -17
  194. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  195. package/dist/sync/syncApplyHandlers.js +370 -8
  196. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  197. package/dist/sync/syncApplyHandlers.test.js +379 -0
  198. package/dist/sync/syncService.d.ts +9 -1
  199. package/dist/sync/syncService.js +44 -2
  200. package/dist/sync/syncService.test.js +62 -0
  201. package/dist/sync/workspacePullFeed.d.ts +1 -0
  202. package/dist/sync/workspacePullFeed.js +66 -59
  203. package/dist/sync/workspacePullFeed.test.js +72 -4
  204. package/dist/sync/workspaceSyncModel.d.ts +25 -1
  205. package/dist/sync/workspaceSyncModel.js +77 -3
  206. package/dist/sync/workspaceSyncModel.test.js +153 -0
  207. package/dist/sync/workspaceSyncState.d.ts +2 -0
  208. package/dist/sync/workspaceSyncState.js +1 -0
  209. package/dist/types.d.ts +2 -10
  210. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
  211. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  212. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  213. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  214. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  215. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  216. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  217. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  218. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
  219. package/dist/ui/assets/index-C2-OXZV7.css +1 -0
  220. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  221. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
  222. package/dist/ui/index.html +4 -4
  223. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  224. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  225. package/dist/utils/taskActivity.js +4 -0
  226. package/dist/utils/taskActivity.test.js +25 -1
  227. package/package.json +10 -1
  228. package/scripts/check-cloud-mcp.mjs +83 -0
  229. package/scripts/playwright-auth.sh +9 -1
  230. package/scripts/run-billing-performance-smoke.sh +24 -0
  231. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  232. package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
  233. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  234. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  235. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  236. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  237. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -39,12 +39,11 @@ import { normalizeTaskFromApi } from '../utils/taskNormalization.js';
39
39
  import { getSupportedLocales, initializeLocale } from '../localization';
40
40
  import { WORKSPACE_BOOTSTRAP_SCOPE, isReservedWorkspaceId, resolveWorkspaceSelectionScopeForRuntime, resolveWorkspaceUiStateScopeForRuntime } from '../utils/workspaceIdentity';
41
41
  import { resolveRuntimeModeContract } from '../shared/runtimeContract';
42
- import { hasWorkspaceTaxonomyStatePayload, normalizeWorkspaceTaxonomyStatePayload } from '../shared/taxonomyState.js';
43
42
  import { DEFAULT_TYPE_VALUE } from '../shared/taxonomyDefaults.js';
44
43
  import { cloneDefaultWorkflowDefinitions } from '../shared/workflowDefaults.js';
45
44
  import { resolveClientDeploymentConfig } from '../config/clientDeployment';
46
45
  import { resolveTaskforceMcpBaseUrlFromBaseUrl } from '../config/cloudEnvironment';
47
- import { createDefaultAssigneeOptions, createWorkspaceAssigneeOptions, mergeAssigneeOptions } from '../utils/assignees';
46
+ import { createDefaultAssigneeOptions } from '../utils/assignees';
48
47
  import { persistLocalMutationActorUserId, readLocalMutationActorUserId } from '../utils/taskforceApiClient';
49
48
  import { useSyncOrchestrator } from './useSyncOrchestrator';
50
49
  import { useRealtimeSync } from './useRealtimeSync';
@@ -62,9 +61,12 @@ import { useTaskFilters } from './tasks/useTaskFilters';
62
61
  import { useTaskFormActions } from './tasks/useTaskFormActions';
63
62
  import { useTaskRelationships } from './tasks/useTaskRelationships';
64
63
  import { useTaskActions } from './tasks/useTaskActions';
65
- import { logBootstrapDebug, readPersistedAppUiState, readPersistedWorkspaceId, writePersistedAppUiState, writePersistedWorkspaceId } from './workspace/workspaceLocalState';
64
+ import { clearPersistedWorkspaceId, logBootstrapDebug, persistRemoteUiState, readPersistedAppUiState, readPersistedWorkspaceId, writePersistedAppUiState, writePersistedWorkspaceId } from './workspace/workspaceLocalState';
66
65
  import { useTaskforceApiRouting } from './workspace/useTaskforceApiRouting';
67
- import { deriveAuthGuardPolicy, shouldSkipDataVersionPolling, shouldSkipDataVersionTick } from './auth/useAuthGuardPolicy';
66
+ import { useTaskforceAuthBootstrap } from './auth/useTaskforceAuthBootstrap';
67
+ import { deriveAuthGuardPolicy } from './auth/useAuthGuardPolicy';
68
+ import { useTaskforceWorkspaceBootstrap } from './workspace/useTaskforceWorkspaceBootstrap';
69
+ import { useDataVersionRefresh } from './sync/useDataVersionRefresh';
68
70
  const ENV_DEPLOYMENT = resolveClientDeploymentConfig(import.meta.env);
69
71
  const DEFAULT_CLOUD_APP_ORIGIN = (() => {
70
72
  const configuredBase = ENV_DEPLOYMENT.baseUrl;
@@ -203,7 +205,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
203
205
  || envCloudBaseUrl
204
206
  || normalizedCloudAuthBaseUrl
205
207
  || defaultCloudAuthBaseUrl).trim().replace(/\/+$/, '');
206
- const authOnlyCloudMode = Boolean(normalizedCloudAuthBaseUrl) && !normalizedApiBaseUrl;
208
+ const authOnlyCloudMode = !isCloudHost && Boolean(normalizedCloudAuthBaseUrl) && !normalizedApiBaseUrl;
207
209
  const shouldProbeCloudAuth = runtimeConfigReady && (Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl) || isCloudHost);
208
210
  const shouldGateProtectedApiCalls = shouldProbeCloudAuth && !authOnlyCloudMode;
209
211
  const cloudAuthConfigured = Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl || isCloudHost);
@@ -233,8 +235,13 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
233
235
  || path.startsWith('/api/taskforce/billing/')
234
236
  || path.startsWith('/api/taskforce/sync/')
235
237
  || path.startsWith('/api/taskforce/settings/mcp/');
236
- if (!isCloudHost && isCloudScopedPath && (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy)) {
237
- return path;
238
+ if (!isCloudHost && isCloudScopedPath) {
239
+ if (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy) {
240
+ return path;
241
+ }
242
+ if (runtimeConfigOverride.workspaceMode === 'multi-cloud') {
243
+ return path;
244
+ }
238
245
  }
239
246
  const base = normalizedCloudAuthBaseUrl || normalizedApiBaseUrl;
240
247
  if (base) {
@@ -256,7 +263,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
256
263
  if (!isCloudHost && isCloudScopedPath)
257
264
  return path;
258
265
  return path;
259
- }, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost]);
266
+ }, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, runtimeConfigOverride.workspaceMode, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost]);
260
267
  const resolveWebSocketUrl = useCallback((path) => {
261
268
  const wsPath = String(path || '').trim() || '/taskforce-ws';
262
269
  const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`;
@@ -345,6 +352,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
345
352
  const [bootstrapError, setBootstrapError] = useState(null);
346
353
  const [bootstrapStartedAt, setBootstrapStartedAt] = useState(null);
347
354
  const authSessionRequestRef = useRef(null);
355
+ const authSessionEpochRef = useRef(0);
348
356
  const authSessionLastCheckedAtRef = useRef(0);
349
357
  const authSessionLastResultRef = useRef(hasOptimisticLocalAuth);
350
358
  const currentWorkspaceIdRef = useRef(currentWorkspaceId);
@@ -375,25 +383,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
375
383
  useEffect(() => {
376
384
  currentWorkspaceIdRef.current = currentWorkspaceId;
377
385
  }, [currentWorkspaceId]);
378
- const applyResolvedWorkspaceId = useCallback((nextWorkspaceIdRaw, options) => {
386
+ const applyResolvedWorkspaceId = useCallback((nextWorkspaceIdRaw) => {
379
387
  const nextWorkspaceId = String(nextWorkspaceIdRaw || '').trim();
380
388
  if (!nextWorkspaceId)
381
389
  return;
382
- setCurrentWorkspaceId((prev) => {
390
+ setCurrentWorkspaceId(() => {
383
391
  const explicitWorkspaceId = String(explicitWorkspaceSelectionRef.current || '').trim();
384
392
  let resolvedWorkspaceId = nextWorkspaceId;
385
393
  if (explicitWorkspaceId && explicitWorkspaceId !== nextWorkspaceId) {
386
394
  resolvedWorkspaceId = explicitWorkspaceId;
387
395
  }
388
- else {
389
- const prevValue = String(prev || '').trim();
390
- if (options?.preserveNonDefaultDefault
391
- && nextWorkspaceId === 'default'
392
- && prevValue
393
- && prevValue.toLowerCase() !== 'default') {
394
- resolvedWorkspaceId = prevValue;
395
- }
396
- }
397
396
  currentWorkspaceIdRef.current = resolvedWorkspaceId;
398
397
  return resolvedWorkspaceId;
399
398
  });
@@ -477,7 +476,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
477
476
  shouldBlockProtectedApiCalls
478
477
  });
479
478
  const { zenMode, setZenModeState, toggleZenMode } = useZenMode();
480
- const { exportEnvironment, setExportEnvironment, availableEnvironments, loadAvailableEnvironments, exportWorkflowsPath, setExportWorkflowsPath, exportingResource, exportResult, handleExportWorkflows } = useResourceExport({ storagePath });
479
+ const { exportEnvironment, setExportEnvironment, availableEnvironments, loadAvailableEnvironments, exportWorkflowsPath, setExportWorkflowsPath, exportingResource, exportResult, handleExportWorkflows } = useResourceExport({
480
+ storagePath,
481
+ shouldDeferProtectedApiCalls,
482
+ shouldBlockProtectedApiCalls
483
+ });
481
484
  // Unsaved Changes Modal State
482
485
  const [unsavedModalOpen, setUnsavedModalOpen] = useState(false);
483
486
  const [pendingNavigation, setPendingNavigation] = useState(null);
@@ -813,159 +816,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
813
816
  await fetchDeletedTasks(true);
814
817
  await fetchPlanningEntities();
815
818
  }, [fetchDeletedTasks, fetchPlanningEntities, refreshTaskCollectionsFromInvalidationBase]);
816
- const checkAuthSession = useCallback(async (options) => {
817
- const force = options?.force === true;
818
- markBootstrapPhase('auth');
819
- if (!runtimeConfigReady) {
820
- // Keep prior auth identity during bootstrap until runtime auth config is resolved.
821
- // This avoids transient "signed out" UI flicker on refresh.
822
- return authSessionLastResultRef.current;
823
- }
824
- if (!shouldProbeCloudAuth) {
825
- setRuntimeMode('local');
826
- setAuthRequiredForApi(false);
827
- setIsAuthenticated(false);
828
- setAuthUserId('anonymous');
829
- setAuthWorkspaceId('');
830
- persistLocalMutationActorUserId(null);
831
- setAuthUserEmail('');
832
- setAuthUserDisplayName('');
833
- setAuthUserAvatarUrl('');
834
- setUserGlobalSyncStatus('disconnected');
835
- setUserGlobalSyncError(null);
836
- setHasBetaAccess(true);
837
- setAvailableWorkspaces([]);
838
- setHydratedWorkspaceRole(null);
839
- setAssigneeOptions(createDefaultAssigneeOptions());
840
- setAuthBlocked(false);
841
- setAuthSessionResolved(true);
842
- authSessionLastResultRef.current = false;
843
- authSessionLastCheckedAtRef.current = Date.now();
844
- return false;
845
- }
846
- const sessionUrl = resolveCloudAuthUrl('/api/taskforce/auth/session');
847
- const now = Date.now();
848
- if (!force && authSessionRequestRef.current)
849
- return authSessionRequestRef.current;
850
- if (!force && authSessionResolved && (now - authSessionLastCheckedAtRef.current) < 1500) {
851
- return authSessionLastResultRef.current;
852
- }
853
- const request = (async () => {
854
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
855
- const abortController = new AbortController();
856
- const timeoutId = typeof window !== 'undefined'
857
- ? window.setTimeout(() => abortController.abort(), 8000)
858
- : null;
859
- try {
860
- const res = await fetch(sessionUrl, {
861
- method: 'GET',
862
- credentials: 'include',
863
- mode: 'cors',
864
- signal: abortController.signal
865
- });
866
- if (res.status === 429) {
867
- setAuthSessionResolved(true);
868
- return authSessionLastResultRef.current;
869
- }
870
- if (!res.ok) {
871
- if (isLoopbackHost)
872
- persistLocalMutationActorUserId(null);
873
- setAuthSessionResolved(true);
874
- authSessionLastResultRef.current = false;
875
- return false;
876
- }
877
- const data = await res.json();
878
- const required = Boolean(data.authRequiredForApi);
879
- const authenticated = Boolean(data.authenticated);
880
- const betaAccess = authenticated ? (data.betaAccess !== false) : true;
881
- const workspaceId = typeof data.workspaceId === 'string' && data.workspaceId.trim().length > 0
882
- ? data.workspaceId.trim()
883
- : '';
884
- const userId = typeof data.userId === 'string' && data.userId.trim().length > 0
885
- ? data.userId.trim()
886
- : 'anonymous';
887
- const userEmail = typeof data.email === 'string' && data.email.trim().length > 0
888
- ? data.email.trim().toLowerCase()
889
- : '';
890
- const userDisplayName = typeof data.displayName === 'string'
891
- ? data.displayName.trim()
892
- : '';
893
- const userAvatarUrl = typeof data.avatarUrl === 'string'
894
- ? data.avatarUrl.trim()
895
- : '';
896
- const isLocalRuntime = runtimeMode === 'local';
897
- const keepLocalWorkspaceContext = runtimeMode === 'local' || authOnlyCloudMode;
898
- setAuthRequiredForApi(authOnlyCloudMode ? false : (isLocalRuntime ? false : required));
899
- setIsAuthenticated(authenticated);
900
- setAuthUserId(authenticated ? userId : 'anonymous');
901
- setAuthWorkspaceId(authenticated ? workspaceId : '');
902
- if (isLoopbackHost) {
903
- persistLocalMutationActorUserId(authenticated ? userId : null);
904
- }
905
- setAuthUserEmail(authenticated ? userEmail : '');
906
- setAuthUserDisplayName(authenticated ? userDisplayName : '');
907
- setAuthUserAvatarUrl(authenticated ? userAvatarUrl : '');
908
- if (authenticated) {
909
- setError((prev) => prev === AUTH_REQUIRED_ERROR ? '' : prev);
910
- }
911
- setUserGlobalSyncStatus(authenticated ? 'idle' : 'disconnected');
912
- setUserGlobalSyncError(null);
913
- setHasBetaAccess(betaAccess);
914
- if (keepLocalWorkspaceContext) {
915
- // Local runtime resolves workspace identity from local DB via /api/taskforce/config.
916
- // Do not override it from cloud auth session payloads.
917
- }
918
- else {
919
- applyResolvedWorkspaceId(workspaceId || readPersistedWorkspaceId(workspaceSelectionScope) || 'default');
920
- }
921
- setAuthBlocked(authOnlyCloudMode ? false : (isLocalRuntime ? false : (required && !authenticated)));
922
- // Runtime mode should be sourced from /api/taskforce/config, not
923
- // from auth-session responses which may come from a separate cloud-auth origin.
924
- setAuthSessionResolved(true);
925
- authSessionLastResultRef.current = authenticated;
926
- logBootstrapDebug('auth_session_resolved', {
927
- authenticated,
928
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
929
- });
930
- if (authenticated)
931
- markBootstrapPhase('ready');
932
- return authenticated;
933
- }
934
- catch {
935
- if (isLoopbackHost)
936
- persistLocalMutationActorUserId(null);
937
- setAuthSessionResolved(true);
938
- authSessionLastResultRef.current = false;
939
- logBootstrapDebug('auth_session_failed', {
940
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
941
- });
942
- markBootstrapStalled('Unable to verify your session.');
943
- return authSessionLastResultRef.current;
944
- }
945
- finally {
946
- if (timeoutId !== null && typeof window !== 'undefined') {
947
- window.clearTimeout(timeoutId);
948
- }
949
- authSessionLastCheckedAtRef.current = Date.now();
950
- authSessionRequestRef.current = null;
951
- }
952
- })();
953
- authSessionRequestRef.current = request;
954
- return request;
955
- }, [
956
- runtimeConfigReady,
957
- shouldProbeCloudAuth,
958
- authSessionResolved,
959
- resolveCloudAuthUrl,
960
- authOnlyCloudMode,
961
- AUTH_REQUIRED_ERROR,
962
- runtimeMode,
963
- applyResolvedWorkspaceId,
964
- workspaceSelectionScope,
965
- markBootstrapPhase,
966
- markBootstrapStalled
967
- ]);
968
- const { userGlobalSyncStatus, setUserGlobalSyncStatus, userGlobalSyncError, setUserGlobalSyncError, workspaceLastPullAt, workspaceLastPushAt, workspaceLastErrorAt, workspaceLastErrorMessage, workspaceLastSuccessfulSyncAt, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceSyncStatus, workspaceSyncSummary, workspaceSyncRecommendedAction, workspaceSyncBusy, workspaceSyncPendingChanges, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspacePullCursorRef, clearWorkspaceRetry, persistWorkspaceSyncPatch, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, syncUserGlobalSettings, buildWorkspaceSyncSignature, pushWorkspaceChangesToCloud, saveWorkspaceCloudSyncSettings, retryUserGlobalSettingsSync, retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull, getWorkspaceSyncDiagnostics } = useSyncOrchestrator({
819
+ const setUserGlobalSyncStatusRef = useRef(() => undefined);
820
+ const setUserGlobalSyncErrorRef = useRef(() => undefined);
821
+ const checkAuthSessionImplRef = useRef(async () => false);
822
+ const checkAuthSession = useCallback((options) => checkAuthSessionImplRef.current(options), []);
823
+ const { userGlobalSyncStatus, setUserGlobalSyncStatus, userGlobalSyncError, setUserGlobalSyncError, workspaceLastPullAt, workspaceLastPushAt, workspaceLastErrorAt, workspaceLastErrorMessage, workspaceLastSuccessfulSyncAt, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceSyncSetupIntent, workspaceSyncStatus, workspaceSyncSummary, workspaceSyncRecommendedAction, workspaceSyncBusy, workspaceSyncPendingChanges, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, workspacePullCursorRef, clearWorkspaceRetry, persistWorkspaceSyncPatch, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, syncUserGlobalSettings, buildWorkspaceSyncSignature, pushWorkspaceChangesToCloud, saveWorkspaceCloudSyncSettings, retryUserGlobalSettingsSync, retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull, getWorkspaceSyncDiagnostics } = useSyncOrchestrator({
969
824
  currentWorkspaceId,
970
825
  cloudAuthConfigured,
971
826
  runtimeMode,
@@ -978,6 +833,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
978
833
  realtimeSyncEnabled,
979
834
  tasks,
980
835
  archivedTasks,
836
+ deletedTasks,
981
837
  initiatives,
982
838
  workstreams,
983
839
  taxonomies,
@@ -1046,6 +902,50 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1046
902
  assigneeOptionsLoaded,
1047
903
  assigneeOptions
1048
904
  });
905
+ setUserGlobalSyncStatusRef.current = setUserGlobalSyncStatus;
906
+ setUserGlobalSyncErrorRef.current = setUserGlobalSyncError;
907
+ const { checkAuthSession: checkAuthSessionImpl } = useTaskforceAuthBootstrap({
908
+ runtimeConfigReady,
909
+ shouldProbeCloudAuth,
910
+ authSessionResolved,
911
+ resolveCloudAuthUrl,
912
+ authOnlyCloudMode,
913
+ authRequiredError: AUTH_REQUIRED_ERROR,
914
+ runtimeMode,
915
+ workspaceSelectionScope,
916
+ markBootstrapPhase,
917
+ markBootstrapStalled,
918
+ setRuntimeMode,
919
+ setAuthRequiredForApi,
920
+ setIsAuthenticated,
921
+ setAuthUserId,
922
+ setAuthWorkspaceId,
923
+ setAuthUserEmail,
924
+ setAuthUserDisplayName,
925
+ setAuthUserAvatarUrl,
926
+ setUserGlobalSyncStatus: (value) => setUserGlobalSyncStatusRef.current(value),
927
+ setUserGlobalSyncError: (value) => setUserGlobalSyncErrorRef.current(value),
928
+ setHasBetaAccess,
929
+ setAvailableWorkspaces,
930
+ setHydratedWorkspaceRole,
931
+ setAssigneeOptions,
932
+ setAuthBlocked,
933
+ setAuthSessionResolved,
934
+ setError,
935
+ applyResolvedWorkspaceId,
936
+ readPersistedWorkspaceId,
937
+ authSessionRequestRef,
938
+ authSessionEpochRef,
939
+ authSessionLastCheckedAtRef,
940
+ authSessionLastResultRef,
941
+ isLoopbackHost,
942
+ setAuthStateReadyRefs: (authenticated, resolved, required) => {
943
+ authSessionResolvedRef.current = resolved;
944
+ authRequiredForApiRef.current = required;
945
+ isAuthenticatedRef.current = authenticated;
946
+ }
947
+ });
948
+ checkAuthSessionImplRef.current = checkAuthSessionImpl;
1049
949
  const [activeWorkspaceModule, setActiveWorkspaceModule] = useState('tasks');
1050
950
  const [showComments, setShowComments] = useState(false);
1051
951
  const [descriptionFocused, setDescriptionFocused] = useState(false);
@@ -1069,6 +969,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1069
969
  const skipNextUiStatePersistRef = useRef(false);
1070
970
  const referenceDataRequestRef = useRef(null);
1071
971
  const workspaceListHydratedRef = useRef(false);
972
+ const hasBootstrappedDataRef = useRef(false);
1072
973
  const [uiStateReady, setUiStateReady] = useState(false);
1073
974
  const { handleSaveSettings, handleSaveTheme, handleSaveGlobalTheme, handleJsonBackupEnabledChange, handleSaveGlobalJsonBackupEnabled, handleSaveGlobalWeekStartsOn, handleSaveLocale, handleManualComplexityEnabledChange, handleChecklistDropdownEnabledChange, handleShowTaskCardStatusLabelChange, handleResetProjectToGlobal } = useSettingsPersistence({
1074
975
  keyShortcut,
@@ -1236,15 +1137,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1236
1137
  writePersistedAppUiState(uiStateSnapshot, workspaceUiStateScope);
1237
1138
  const timeoutId = window.setTimeout(async () => {
1238
1139
  try {
1239
- await fetch('/api/taskforce/ui-state', {
1240
- method: 'POST',
1241
- headers: { 'Content-Type': 'application/json' },
1242
- credentials: 'include',
1243
- body: JSON.stringify({
1244
- stateKey: 'app',
1245
- workspaceId: currentWorkspaceIdRef.current,
1246
- patch: uiStateSnapshot
1247
- })
1140
+ await persistRemoteUiState({
1141
+ stateKey: 'app',
1142
+ workspaceId: currentWorkspaceIdRef.current,
1143
+ patch: uiStateSnapshot
1248
1144
  });
1249
1145
  }
1250
1146
  catch {
@@ -1281,13 +1177,15 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1281
1177
  ? data.userId.trim()
1282
1178
  : 'anonymous';
1283
1179
  const serverAuthenticated = nextRuntimeMode === 'cloud' && nextUserId !== 'anonymous';
1180
+ const knownAuthenticated = serverAuthenticated || isAuthenticatedRef.current;
1181
+ const authResolved = authSessionResolvedRef.current;
1284
1182
  setRuntimeMode(nextRuntimeMode);
1285
1183
  setAuthRequiredForApi(nextAuthRequiredForApi);
1286
1184
  if (serverAuthenticated) {
1287
1185
  setIsAuthenticated(true);
1288
1186
  setAuthUserId(nextUserId);
1289
1187
  setAuthBlocked(false);
1290
- if (!authSessionResolved) {
1188
+ if (!authResolved) {
1291
1189
  setAuthSessionResolved(true);
1292
1190
  }
1293
1191
  }
@@ -1297,11 +1195,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1297
1195
  commitCurrentWorkspaceId(nextWorkspaceId);
1298
1196
  }
1299
1197
  else {
1300
- applyResolvedWorkspaceId(nextWorkspaceId, { preserveNonDefaultDefault: true });
1198
+ applyResolvedWorkspaceId(nextWorkspaceId);
1301
1199
  }
1302
1200
  }
1303
1201
  if (nextRuntimeMode === 'cloud' && nextAuthRequiredForApi) {
1304
- setAuthBlocked(!(serverAuthenticated || isAuthenticated));
1202
+ setAuthBlocked(!knownAuthenticated);
1305
1203
  }
1306
1204
  else {
1307
1205
  setAuthBlocked(false);
@@ -1399,7 +1297,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1399
1297
  else {
1400
1298
  setShowTaskCardStatusLabel(true);
1401
1299
  }
1402
- }, [applyResolvedWorkspaceId, authSessionResolved, commitCurrentWorkspaceId, isAuthenticated]);
1300
+ }, [applyResolvedWorkspaceId, commitCurrentWorkspaceId]);
1403
1301
  const fetchBuildInfo = useCallback(async () => {
1404
1302
  const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
1405
1303
  try {
@@ -1425,417 +1323,83 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1425
1323
  }
1426
1324
  }
1427
1325
  }, [fetchWithTimeout, isBootstrapTimeoutError, markBootstrapStalled]);
1428
- const fetchReferenceData = useCallback(async (options) => {
1429
- const ignoreAuthGuard = options?.ignoreAuthGuard === true;
1430
- const force = options?.force === true;
1431
- if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
1432
- return;
1433
- const requestState = getWorkspaceRequestState();
1434
- if (!force && referenceDataRequestRef.current?.workspaceId === requestState.workspaceId) {
1435
- await referenceDataRequestRef.current.promise;
1436
- return;
1437
- }
1438
- setReferenceDataLoaded(false);
1439
- const abortController = new AbortController();
1440
- referenceDataAbortRef.current = abortController;
1441
- const request = (async () => {
1442
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
1443
- await Promise.allSettled([
1444
- (async () => {
1445
- const taxonomyRes = await fetch('/api/taskforce/taxonomy-state', { signal: abortController.signal });
1446
- if (!taxonomyRes.ok)
1447
- return;
1448
- const taxonomyData = await taxonomyRes.json().catch(() => ({}));
1449
- const hasUnifiedPayload = hasWorkspaceTaxonomyStatePayload(taxonomyData);
1450
- if (!hasUnifiedPayload) {
1451
- const [catRes, typeRes, priorityRes, taxRes] = await Promise.all([
1452
- fetch('/api/taskforce/categories', { signal: abortController.signal }),
1453
- fetch('/api/taskforce/types', { signal: abortController.signal }),
1454
- fetch('/api/taskforce/priorities', { signal: abortController.signal }),
1455
- fetch('/api/taskforce/taxonomies', { signal: abortController.signal })
1456
- ]);
1457
- const [catData, typeData, priorityData, taxData] = await Promise.all([
1458
- catRes.ok ? catRes.json().catch(() => ({})) : Promise.resolve({}),
1459
- typeRes.ok ? typeRes.json().catch(() => ({})) : Promise.resolve({}),
1460
- priorityRes.ok ? priorityRes.json().catch(() => ({})) : Promise.resolve({}),
1461
- taxRes.ok ? taxRes.json().catch(() => ({})) : Promise.resolve({})
1462
- ]);
1463
- taxonomyData.categories = catData.categories;
1464
- taxonomyData.types = typeData.types;
1465
- taxonomyData.priorities = priorityData.priorities;
1466
- taxonomyData.taxonomies = taxData.taxonomies;
1467
- }
1468
- const normalizedTaxonomyState = normalizeWorkspaceTaxonomyStatePayload(taxonomyData);
1469
- if (isWorkspaceRequestStale(requestState))
1470
- return;
1471
- if (normalizedTaxonomyState.categories.length > 0)
1472
- setCustomCategories(normalizedTaxonomyState.categories);
1473
- if (normalizedTaxonomyState.types.length > 0)
1474
- setCustomTypes(normalizedTaxonomyState.types);
1475
- if (normalizedTaxonomyState.priorities.length > 0)
1476
- setPriorities(normalizedTaxonomyState.priorities);
1477
- setTaxonomies(normalizedTaxonomyState.taxonomies);
1478
- setTaxonomyDisplayLabels(normalizedTaxonomyState.displayLabels || {});
1479
- })(),
1480
- (async () => {
1481
- await fetchWorkflows();
1482
- })(),
1483
- (async () => {
1484
- await fetchInitiativeTemplates();
1485
- })(),
1486
- (async () => {
1487
- await loadAvailableEnvironments();
1488
- })(),
1489
- ]);
1490
- if (isWorkspaceRequestStale(requestState))
1491
- return;
1492
- setReferenceDataLoaded(true);
1493
- logBootstrapDebug('reference_data_loaded', {
1494
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
1495
- });
1496
- })();
1497
- referenceDataRequestRef.current = { workspaceId: requestState.workspaceId, promise: request };
1498
- try {
1499
- await request;
1500
- }
1501
- catch (error) {
1502
- if (!isAbortError(error))
1503
- throw error;
1504
- }
1505
- finally {
1506
- if (referenceDataRequestRef.current?.promise === request) {
1507
- referenceDataRequestRef.current = null;
1508
- }
1509
- if (referenceDataAbortRef.current === abortController) {
1510
- referenceDataAbortRef.current = null;
1511
- }
1512
- }
1513
- }, [
1514
- fetchWorkflows,
1515
- fetchInitiativeTemplates,
1516
- getWorkspaceRequestState,
1517
- isAbortError,
1518
- isWorkspaceRequestStale,
1519
- loadAvailableEnvironments,
1326
+ const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks } = useTaskforceWorkspaceBootstrap({
1520
1327
  shouldDeferProtectedApiCalls,
1521
- shouldBlockProtectedApiCalls
1522
- ]);
1523
- const fetchBootstrapConfig = useCallback(async (options) => {
1524
- const ignoreAuthGuard = options?.ignoreAuthGuard === true;
1525
- if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
1526
- return;
1527
- markBootstrapPhase('config');
1528
- const requestState = getWorkspaceRequestState();
1529
- const abortController = new AbortController();
1530
- bootstrapConfigAbortRef.current = abortController;
1531
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
1532
- try {
1533
- const res = await fetchWithTimeout('/api/taskforce/config', undefined, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
1534
- signal: abortController.signal
1535
- });
1536
- if (isWorkspaceEpochStale(requestState))
1537
- return;
1538
- if (res.status === 401) {
1539
- handleUnauthorized();
1540
- setConfigLoaded(true);
1541
- return;
1542
- }
1543
- let configResolvedWorkspaceId = '';
1544
- if (res.ok) {
1545
- const data = await res.json();
1546
- if (isWorkspaceEpochStale(requestState))
1547
- return;
1548
- configResolvedWorkspaceId = typeof data?.workspaceId === 'string' && data.workspaceId.trim().length > 0
1549
- ? data.workspaceId.trim()
1550
- : '';
1551
- applyBootstrapConfig(data);
1552
- }
1553
- if (isWorkspaceEpochStale(requestState)) {
1554
- const liveWorkspaceId = String(currentWorkspaceIdRef.current || 'default').trim() || 'default';
1555
- if (!configResolvedWorkspaceId || liveWorkspaceId !== configResolvedWorkspaceId) {
1556
- return;
1557
- }
1558
- }
1559
- if (!hasLoadedUiStateRef.current) {
1560
- const uiStateWorkspaceId = configResolvedWorkspaceId || requestState.workspaceId;
1561
- hasLoadedUiStateRef.current = true;
1562
- await loadPersistedUiState(uiStateWorkspaceId);
1563
- }
1564
- if (isWorkspaceEpochStale(requestState)) {
1565
- const liveWorkspaceId = String(currentWorkspaceIdRef.current || 'default').trim() || 'default';
1566
- if (!configResolvedWorkspaceId || liveWorkspaceId !== configResolvedWorkspaceId) {
1567
- return;
1568
- }
1569
- }
1570
- setConfigLoaded(true);
1571
- markBootstrapPhase('ready');
1572
- logBootstrapDebug('bootstrap_config_loaded', {
1573
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
1574
- });
1575
- void fetchBuildInfo();
1576
- }
1577
- catch (error) {
1578
- if (isAbortError(error))
1579
- return;
1580
- if (isWorkspaceEpochStale(requestState))
1581
- return;
1582
- if (!hasLoadedUiStateRef.current) {
1583
- hasLoadedUiStateRef.current = true;
1584
- await loadPersistedUiState(requestState.workspaceId);
1585
- }
1586
- if (isBootstrapTimeoutError(error)) {
1587
- markBootstrapStalled('Startup checks timed out.');
1588
- }
1589
- else {
1590
- markBootstrapStalled('Unable to finish startup checks.');
1591
- }
1592
- setConfigLoaded(true);
1593
- }
1594
- finally {
1595
- if (bootstrapConfigAbortRef.current === abortController) {
1596
- bootstrapConfigAbortRef.current = null;
1597
- }
1598
- }
1599
- }, [
1600
- applyBootstrapConfig,
1601
- fetchBuildInfo,
1602
- fetchWithTimeout,
1328
+ shouldBlockProtectedApiCalls,
1603
1329
  getWorkspaceRequestState,
1604
- handleUnauthorized,
1330
+ isWorkspaceRequestStale,
1331
+ isWorkspaceEpochStale,
1605
1332
  isAbortError,
1606
1333
  isBootstrapTimeoutError,
1607
- isWorkspaceEpochStale,
1608
- isWorkspaceRequestStale,
1334
+ fetchWithTimeout,
1335
+ handleUnauthorized,
1336
+ applyBootstrapConfig,
1609
1337
  loadPersistedUiState,
1338
+ fetchBuildInfo,
1610
1339
  markBootstrapPhase,
1611
1340
  markBootstrapStalled,
1612
- shouldDeferProtectedApiCalls,
1613
- shouldBlockProtectedApiCalls
1614
- ]);
1615
- const fetchConfig = useCallback(async (options) => {
1616
- await fetchBootstrapConfig(options);
1617
- await fetchReferenceData(options);
1618
- }, [fetchBootstrapConfig, fetchReferenceData]);
1341
+ fetchWorkflows,
1342
+ fetchInitiativeTemplates,
1343
+ loadAvailableEnvironments,
1344
+ setReferenceDataLoaded,
1345
+ setCustomCategories,
1346
+ setCustomTypes,
1347
+ setPriorities,
1348
+ setTaxonomies,
1349
+ setTaxonomyDisplayLabels,
1350
+ setConfigLoaded,
1351
+ hasLoadedUiStateRef,
1352
+ currentWorkspaceIdRef,
1353
+ referenceDataRequestRef,
1354
+ referenceDataAbortRef,
1355
+ bootstrapConfigAbortRef,
1356
+ workspaceListAbortRef,
1357
+ assigneeOptionsAbortRef,
1358
+ workspaceListHydratedRef,
1359
+ setAvailableWorkspaces,
1360
+ runtimeMode,
1361
+ applyResolvedWorkspaceId,
1362
+ workspaceSwitchingEnabled,
1363
+ authSessionResolved,
1364
+ isAuthenticated,
1365
+ authUserId,
1366
+ setHydratedWorkspaceRole,
1367
+ setAssigneeOptions,
1368
+ setAssigneeOptionsLoaded,
1369
+ commitCurrentWorkspaceId: (workspaceId, options) => {
1370
+ explicitWorkspaceSelectionRef.current = workspaceId;
1371
+ commitCurrentWorkspaceId(workspaceId, options);
1372
+ if (options?.clearExplicitSelection !== false) {
1373
+ explicitWorkspaceSelectionRef.current = null;
1374
+ }
1375
+ },
1376
+ currentWorkspaceId,
1377
+ workspaceSelectionScope,
1378
+ abortWorkspaceScopedRequests,
1379
+ setWorkspaceBootstrapPending,
1380
+ checkAuthSession,
1381
+ lastConfigRefreshKeyRef,
1382
+ lastAssigneeOptionsLoadKeyRef,
1383
+ lastWorkspaceSyncStateLoadKeyRef,
1384
+ lastTaskSurfaceLoadKeyRef,
1385
+ lastSettingsSurfaceLoadKeyRef,
1386
+ isOpen,
1387
+ activeTab,
1388
+ showArchive,
1389
+ taskScope,
1390
+ settingsSection,
1391
+ fetchTasks,
1392
+ fetchArchive,
1393
+ fetchPlanningEntities,
1394
+ refreshTaskCollections,
1395
+ loadWorkspaceSyncState,
1396
+ archiveBootstrapTimerRef,
1397
+ hasBootstrappedDataRef
1398
+ });
1619
1399
  const refreshSetupContext = useCallback(async () => {
1620
1400
  await checkAuthSession();
1621
1401
  await fetchConfig();
1622
1402
  }, [checkAuthSession, fetchConfig]);
1623
- const fetchWorkspaces = useCallback(async () => {
1624
- if (!workspaceSwitchingEnabled) {
1625
- setAvailableWorkspaces([]);
1626
- workspaceListHydratedRef.current = false;
1627
- return { success: true, workspaces: [] };
1628
- }
1629
- const requestState = getWorkspaceRequestState();
1630
- const abortController = new AbortController();
1631
- workspaceListAbortRef.current = abortController;
1632
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
1633
- try {
1634
- const res = await fetchWithTimeout('/api/taskforce/workspaces', {
1635
- method: 'GET',
1636
- credentials: 'include'
1637
- }, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
1638
- signal: abortController.signal
1639
- });
1640
- if (isWorkspaceEpochStale(requestState)) {
1641
- return { success: false, error: 'Workspace changed while loading workspaces.' };
1642
- }
1643
- if (res.status === 401) {
1644
- setAvailableWorkspaces([]);
1645
- workspaceListHydratedRef.current = false;
1646
- return { success: false, error: 'Authentication required.' };
1647
- }
1648
- const data = await res.json().catch(() => ({}));
1649
- if (isWorkspaceEpochStale(requestState)) {
1650
- return { success: false, error: 'Workspace changed while loading workspaces.' };
1651
- }
1652
- if (!res.ok || data?.success === false) {
1653
- setAvailableWorkspaces([]);
1654
- workspaceListHydratedRef.current = false;
1655
- return { success: false, error: data?.error || `Failed to load workspaces (${res.status})` };
1656
- }
1657
- const workspaces = Array.isArray(data?.workspaces) ? data.workspaces : [];
1658
- setAvailableWorkspaces(workspaces);
1659
- workspaceListHydratedRef.current = true;
1660
- if (runtimeMode !== 'local' && typeof data?.currentWorkspaceId === 'string' && data.currentWorkspaceId.trim().length > 0) {
1661
- applyResolvedWorkspaceId(data.currentWorkspaceId.trim(), { preserveNonDefaultDefault: true });
1662
- }
1663
- logBootstrapDebug('workspace_list_loaded', {
1664
- workspaceCount: workspaces.length,
1665
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
1666
- });
1667
- return { success: true, workspaces };
1668
- }
1669
- catch (error) {
1670
- if (isAbortError(error)) {
1671
- return { success: false, error: 'Workspace list request aborted.' };
1672
- }
1673
- if (isBootstrapTimeoutError(error)) {
1674
- markBootstrapStalled('Startup checks timed out.');
1675
- }
1676
- setAvailableWorkspaces([]);
1677
- workspaceListHydratedRef.current = false;
1678
- return { success: false, error: 'Failed to load workspaces.' };
1679
- }
1680
- finally {
1681
- if (workspaceListAbortRef.current === abortController) {
1682
- workspaceListAbortRef.current = null;
1683
- }
1684
- }
1685
- }, [
1686
- applyResolvedWorkspaceId,
1687
- fetchWithTimeout,
1688
- getWorkspaceRequestState,
1689
- isAbortError,
1690
- isWorkspaceEpochStale,
1691
- isBootstrapTimeoutError,
1692
- isWorkspaceRequestStale,
1693
- markBootstrapStalled,
1694
- runtimeMode,
1695
- workspaceSwitchingEnabled
1696
- ]);
1697
- const fetchAssigneeOptions = useCallback(async () => {
1698
- const requestState = getWorkspaceRequestState();
1699
- const abortController = new AbortController();
1700
- assigneeOptionsAbortRef.current = abortController;
1701
- setAssigneeOptionsLoaded(false);
1702
- try {
1703
- const [assigneeResult, workspaceMembersResult] = await Promise.all([
1704
- fetchWithTimeout('/api/taskforce/workspace/assignee-options', {
1705
- method: 'GET',
1706
- credentials: 'include'
1707
- }, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
1708
- signal: abortController.signal
1709
- }),
1710
- (authSessionResolved && isAuthenticated)
1711
- ? fetchWithTimeout('/api/taskforce/auth/workspace-members', {
1712
- method: 'GET',
1713
- credentials: 'include'
1714
- }, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
1715
- signal: abortController.signal
1716
- }).catch(() => null)
1717
- : Promise.resolve(null)
1718
- ]);
1719
- if (isWorkspaceRequestStale(requestState)) {
1720
- return { success: false, error: 'Workspace changed while loading assignee options.' };
1721
- }
1722
- if (assigneeResult.status === 401) {
1723
- setAssigneeOptions(createDefaultAssigneeOptions());
1724
- setAssigneeOptionsLoaded(true);
1725
- return { success: false, error: 'Authentication required.' };
1726
- }
1727
- const data = await assigneeResult.json().catch(() => ({}));
1728
- if (isWorkspaceRequestStale(requestState)) {
1729
- return { success: false, error: 'Workspace changed while loading assignee options.' };
1730
- }
1731
- if (!assigneeResult.ok) {
1732
- setAssigneeOptions(createDefaultAssigneeOptions());
1733
- setAssigneeOptionsLoaded(true);
1734
- return { success: false, error: data?.error || `Failed to load assignee options (${assigneeResult.status})` };
1735
- }
1736
- const assignees = Array.isArray(data?.assignees) ? data.assignees : [];
1737
- const directOptions = assignees
1738
- .map((entry) => {
1739
- const value = String(entry?.value || '').trim();
1740
- const kindRaw = String(entry?.kind || '').trim().toLowerCase();
1741
- if (!value)
1742
- return null;
1743
- if (kindRaw === 'agent' || kindRaw === 'ai') {
1744
- return {
1745
- value,
1746
- label: String(entry?.label || value).trim() || value,
1747
- icon: String(entry?.icon || 'Bot'),
1748
- color: String(entry?.color || '#8b5cf6'),
1749
- kind: 'agent'
1750
- };
1751
- }
1752
- if (kindRaw === 'member' || kindRaw === 'human') {
1753
- return {
1754
- value,
1755
- label: String(entry?.label || value).trim() || value,
1756
- icon: String(entry?.icon || 'User'),
1757
- color: String(entry?.color || '#22c55e'),
1758
- kind: 'member'
1759
- };
1760
- }
1761
- return null;
1762
- })
1763
- .filter((candidate) => Boolean(candidate));
1764
- const fallbackCandidates = assignees
1765
- .map((candidate) => ({
1766
- userId: String(candidate?.userId || '').trim(),
1767
- email: String(candidate?.email || '').trim().toLowerCase(),
1768
- displayName: typeof candidate?.displayName === 'string' ? candidate.displayName : null,
1769
- status: typeof candidate?.status === 'string' ? candidate.status : null,
1770
- disabled: candidate?.disabled === true,
1771
- }))
1772
- .filter((candidate) => candidate.userId
1773
- && String(candidate.status || 'active').trim().toLowerCase() === 'active'
1774
- && candidate.disabled !== true);
1775
- let memberCandidates = fallbackCandidates;
1776
- if (workspaceMembersResult?.ok) {
1777
- const workspaceMembersData = await workspaceMembersResult.json().catch(() => ({}));
1778
- const users = Array.isArray(workspaceMembersData?.users) ? workspaceMembersData.users : [];
1779
- const normalizedAuthUserId = String(authUserId || '').trim();
1780
- const currentMember = users.find((candidate) => String(candidate?.userId || '').trim() === normalizedAuthUserId);
1781
- const currentMemberRole = String(currentMember?.role || '').trim().toLowerCase();
1782
- if (currentMemberRole === 'owner' || currentMemberRole === 'admin' || currentMemberRole === 'member' || currentMemberRole === 'read-only') {
1783
- setHydratedWorkspaceRole(currentMemberRole);
1784
- }
1785
- else if (!workspaceSwitchingEnabled && users.length === 0) {
1786
- setHydratedWorkspaceRole(null);
1787
- }
1788
- const workspaceMemberCandidates = users
1789
- .map((candidate) => ({
1790
- userId: String(candidate?.userId || '').trim(),
1791
- email: String(candidate?.email || '').trim().toLowerCase(),
1792
- displayName: typeof candidate?.displayName === 'string' ? candidate.displayName : null,
1793
- status: typeof candidate?.status === 'string' ? candidate.status : null,
1794
- disabled: candidate?.disabled === true,
1795
- }))
1796
- .filter((candidate) => candidate.userId
1797
- && String(candidate.status || 'active').trim().toLowerCase() === 'active'
1798
- && candidate.disabled !== true)
1799
- .map((candidate) => ({
1800
- userId: candidate.userId,
1801
- email: candidate.email,
1802
- displayName: candidate.displayName ?? null
1803
- }));
1804
- memberCandidates = Array.from(new Map([...fallbackCandidates, ...workspaceMemberCandidates].map((candidate) => [
1805
- String(candidate.userId || '').trim(),
1806
- candidate
1807
- ])).values()).filter((candidate) => String(candidate.userId || '').trim().length > 0);
1808
- }
1809
- else if (!workspaceSwitchingEnabled && !fallbackCandidates.length) {
1810
- setHydratedWorkspaceRole(null);
1811
- }
1812
- const mergedOptions = mergeAssigneeOptions([
1813
- ...directOptions,
1814
- ...createWorkspaceAssigneeOptions(memberCandidates).filter((option) => option.kind === 'member')
1815
- ]);
1816
- if (isWorkspaceRequestStale(requestState)) {
1817
- return { success: false, error: 'Workspace changed while loading assignee options.' };
1818
- }
1819
- setAssigneeOptions(mergedOptions);
1820
- setAssigneeOptionsLoaded(true);
1821
- return { success: true };
1822
- }
1823
- catch (error) {
1824
- if (isAbortError(error)) {
1825
- return { success: false, error: 'Assignee options request aborted.' };
1826
- }
1827
- if (!isWorkspaceRequestStale(requestState)) {
1828
- setAssigneeOptions(createDefaultAssigneeOptions());
1829
- setAssigneeOptionsLoaded(true);
1830
- }
1831
- return { success: false, error: 'Failed to load assignee options.' };
1832
- }
1833
- finally {
1834
- if (assigneeOptionsAbortRef.current === abortController) {
1835
- assigneeOptionsAbortRef.current = null;
1836
- }
1837
- }
1838
- }, [authSessionResolved, authUserId, fetchWithTimeout, getWorkspaceRequestState, isAbortError, isAuthenticated, isWorkspaceRequestStale, workspaceSwitchingEnabled]);
1839
1403
  useEffect(() => {
1840
1404
  if (!configLoaded || workspaceBootstrapPending)
1841
1405
  return;
@@ -1857,162 +1421,44 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1857
1421
  setHydratedWorkspaceRole(null);
1858
1422
  }
1859
1423
  }, [isAuthenticated]);
1860
- const switchWorkspace = useCallback(async (workspaceId, options) => {
1861
- if (!workspaceSwitchingEnabled) {
1862
- return { success: false, error: 'Workspace switching is unavailable in local mode.', code: 'WORKSPACE_SWITCHING_DISABLED' };
1863
- }
1864
- const normalizedWorkspaceId = String(workspaceId || '').trim();
1865
- if (!normalizedWorkspaceId)
1866
- return { success: false, error: 'workspaceId is required.' };
1867
- try {
1868
- const res = await fetch('/api/taskforce/session/workspace', {
1869
- method: 'POST',
1870
- headers: { 'Content-Type': 'application/json' },
1871
- credentials: 'include',
1872
- body: JSON.stringify({ workspaceId: normalizedWorkspaceId })
1873
- });
1874
- const data = await res.json().catch(() => ({}));
1875
- if (!res.ok || data?.success === false) {
1876
- return { success: false, error: data?.error || `Failed to switch workspace (${res.status})`, code: data?.code };
1877
- }
1878
- explicitWorkspaceSelectionRef.current = normalizedWorkspaceId;
1879
- commitCurrentWorkspaceId(normalizedWorkspaceId, { clearExplicitSelection: false });
1880
- abortWorkspaceScopedRequests();
1881
- setWorkspaceBootstrapPending(true);
1882
- markBootstrapPhase('workspace');
1883
- const authenticated = await checkAuthSession({ force: true });
1884
- if (options?.hydrate === false) {
1885
- await fetchWorkspaces();
1886
- }
1887
- else {
1888
- lastConfigRefreshKeyRef.current = `${runtimeMode}:${normalizedWorkspaceId}:${workspaceSelectionScope}`;
1889
- lastAssigneeOptionsLoadKeyRef.current = `${runtimeMode}:${authenticated ? 'auth' : 'guest'}:${normalizedWorkspaceId}`;
1890
- lastWorkspaceSyncStateLoadKeyRef.current = normalizedWorkspaceId;
1891
- if (isOpen && activeTab === 'tasks') {
1892
- lastTaskSurfaceLoadKeyRef.current = `${normalizedWorkspaceId}:${activeTab}:${showArchive ? 'archive' : 'active'}:${taskScope}`;
1893
- }
1894
- if (isOpen && activeTab === 'settings') {
1895
- lastSettingsSurfaceLoadKeyRef.current = `${normalizedWorkspaceId}:${activeTab}:${settingsSection}`;
1896
- }
1897
- await Promise.all([
1898
- fetchConfig(),
1899
- fetchWorkspaces(),
1900
- refreshTaskCollections({ isSilent: false }),
1901
- fetchAssigneeOptions(),
1902
- loadWorkspaceSyncState()
1903
- ]);
1904
- }
1905
- if (explicitWorkspaceSelectionRef.current === normalizedWorkspaceId) {
1906
- explicitWorkspaceSelectionRef.current = null;
1907
- }
1908
- return { success: true };
1909
- }
1910
- catch {
1911
- if (explicitWorkspaceSelectionRef.current === normalizedWorkspaceId) {
1912
- explicitWorkspaceSelectionRef.current = null;
1913
- }
1914
- return { success: false, error: 'Failed to switch workspace.' };
1915
- }
1916
- finally {
1917
- setWorkspaceBootstrapPending(false);
1424
+ const continueAfterCommercialOnboarding = useCallback(async () => {
1425
+ 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' };
1918
1431
  }
1919
- }, [abortWorkspaceScopedRequests, activeTab, checkAuthSession, commitCurrentWorkspaceId, fetchAssigneeOptions, fetchConfig, fetchWorkspaces, isOpen, loadWorkspaceSyncState, markBootstrapPhase, refreshTaskCollections, runtimeMode, settingsSection, showArchive, taskScope, workspaceSelectionScope, workspaceSwitchingEnabled]);
1920
- const resolveWorkspaceAfterAuth = useCallback(async (options) => {
1921
- markBootstrapPhase('workspace');
1922
- setWorkspaceBootstrapPending(true);
1923
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
1924
- try {
1925
- if (!workspaceSwitchingEnabled) {
1926
- const localWorkspaceId = String(currentWorkspaceId || '').trim() || 'default';
1927
- commitCurrentWorkspaceId(localWorkspaceId);
1928
- logBootstrapDebug('workspace_resolved', {
1929
- workspaceId: localWorkspaceId,
1930
- workspaceSetupRequired: false,
1931
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
1932
- });
1933
- return { success: true, workspaceSetupRequired: false, workspaceId: localWorkspaceId };
1934
- }
1935
- const preferredWorkspaceId = String(options?.preferredWorkspaceId || '').trim();
1936
- const persistedWorkspaceId = readPersistedWorkspaceId(workspaceSelectionScope);
1937
- const workspacesResult = await fetchWorkspaces();
1938
- if (!workspacesResult.success) {
1432
+ if (workspaceSwitchingEnabled) {
1433
+ const workspaceResolution = await resolveWorkspaceAfterAuth();
1434
+ if (!workspaceResolution.success) {
1435
+ await fetchConfig({ ignoreAuthGuard: true });
1939
1436
  return {
1940
1437
  success: false,
1941
- workspaceSetupRequired: false,
1942
- error: workspacesResult.error || 'Failed to load workspaces after authentication.'
1438
+ destination: 'app',
1439
+ error: workspaceResolution.error || 'Unable to resolve workspace after plan selection.',
1440
+ code: workspaceResolution.code
1943
1441
  };
1944
1442
  }
1945
- const workspaces = Array.isArray(workspacesResult.workspaces) ? workspacesResult.workspaces : [];
1946
- if (workspaces.length === 0) {
1947
- commitCurrentWorkspaceId('default');
1948
- logBootstrapDebug('workspace_resolved', {
1949
- workspaceId: 'default',
1950
- workspaceSetupRequired: true,
1951
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
1952
- });
1953
- return { success: true, workspaceSetupRequired: true };
1954
- }
1955
- const ids = new Set(workspaces.map((workspace) => String(workspace.id || '').trim()).filter(Boolean));
1956
- const selectedWorkspaceId = ((preferredWorkspaceId && ids.has(preferredWorkspaceId) ? preferredWorkspaceId : '')
1957
- || (persistedWorkspaceId && ids.has(persistedWorkspaceId) ? persistedWorkspaceId : '')
1958
- || String(workspaces[0]?.id || '').trim());
1959
- if (!selectedWorkspaceId) {
1960
- commitCurrentWorkspaceId('default');
1961
- return { success: true, workspaceSetupRequired: true };
1962
- }
1963
- const current = String(currentWorkspaceIdRef.current || '').trim();
1964
- if (current !== selectedWorkspaceId) {
1965
- const switched = await switchWorkspace(selectedWorkspaceId, { hydrate: false });
1966
- if (!switched.success) {
1967
- return {
1968
- success: false,
1969
- workspaceSetupRequired: false,
1970
- error: switched.error || 'Failed to set workspace after authentication.',
1971
- code: switched.code
1972
- };
1973
- }
1974
- }
1975
- else {
1976
- commitCurrentWorkspaceId(selectedWorkspaceId);
1977
- }
1978
- logBootstrapDebug('workspace_resolved', {
1979
- workspaceId: selectedWorkspaceId,
1980
- workspaceSetupRequired: false,
1981
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
1982
- });
1983
- return { success: true, workspaceSetupRequired: false, workspaceId: selectedWorkspaceId };
1984
- }
1985
- catch {
1986
- markBootstrapStalled('Unable to resolve workspace access.');
1987
- return { success: false, workspaceSetupRequired: false, error: 'Failed to resolve workspace access.' };
1988
- }
1989
- finally {
1990
- setWorkspaceBootstrapPending(false);
1991
- }
1992
- }, [
1993
- commitCurrentWorkspaceId,
1994
- fetchWorkspaces,
1995
- markBootstrapPhase,
1996
- markBootstrapStalled,
1997
- workspaceSelectionScope,
1998
- switchWorkspace,
1999
- workspaceSwitchingEnabled
2000
- ]);
2001
- const retryBootstrapChecks = useCallback(async () => {
2002
- setBootstrapError(null);
2003
- markBootstrapPhase('auth');
2004
- const authenticated = await checkAuthSession({ force: true });
2005
- if (workspaceSwitchingEnabled && authenticated) {
2006
- const workspaceResolution = await resolveWorkspaceAfterAuth();
2007
- if (!workspaceResolution.success || workspaceResolution.workspaceSetupRequired) {
1443
+ if (workspaceResolution.workspaceSetupRequired) {
2008
1444
  await fetchConfig({ ignoreAuthGuard: true });
2009
- return;
1445
+ return { success: true, destination: 'setup' };
2010
1446
  }
2011
1447
  }
2012
- await fetchConfig({ ignoreAuthGuard: true });
1448
+ await fetchBootstrapConfig();
1449
+ await Promise.all([
1450
+ fetchTasks(true, { ignoreAuthGuard: true }),
1451
+ fetchReferenceData(),
1452
+ fetchPlanningEntities()
1453
+ ]);
1454
+ return { success: true, destination: 'app' };
2013
1455
  }, [
2014
1456
  checkAuthSession,
1457
+ fetchBootstrapConfig,
2015
1458
  fetchConfig,
1459
+ fetchPlanningEntities,
1460
+ fetchReferenceData,
1461
+ fetchTasks,
2016
1462
  markBootstrapPhase,
2017
1463
  resolveWorkspaceAfterAuth,
2018
1464
  workspaceSwitchingEnabled
@@ -2106,6 +1552,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2106
1552
  }, [refreshSetupContext]);
2107
1553
  const saveWorkspaceProfile = useCallback(async (input) => {
2108
1554
  const name = String(input?.name || '').trim();
1555
+ const requestedWorkspaceId = typeof input?.workspaceId === 'string'
1556
+ ? input.workspaceId.trim()
1557
+ : '';
2109
1558
  if (!name)
2110
1559
  return { success: false, error: 'Workspace name is required.', code: 'WORKSPACE_NAME_REQUIRED' };
2111
1560
  try {
@@ -2117,7 +1566,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2117
1566
  },
2118
1567
  credentials: 'include',
2119
1568
  body: JSON.stringify({
2120
- workspaceId: input.workspaceId,
1569
+ workspaceId: requestedWorkspaceId && requestedWorkspaceId !== 'default'
1570
+ ? requestedWorkspaceId
1571
+ : undefined,
2121
1572
  name,
2122
1573
  description: typeof input.description === 'string' ? input.description : undefined,
2123
1574
  allowCreate: true
@@ -2180,6 +1631,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2180
1631
  };
2181
1632
  }
2182
1633
  if (workspaceResolution.workspaceSetupRequired) {
1634
+ await fetchConfig({ ignoreAuthGuard: true });
2183
1635
  return { success: true, workspaceSetupRequired: true };
2184
1636
  }
2185
1637
  await Promise.all([
@@ -2226,15 +1678,64 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2226
1678
  });
2227
1679
  const data = await res.json().catch(() => ({}));
2228
1680
  if (!res.ok || !data?.success) {
2229
- return { success: false, error: data?.error || 'Create account failed.', code: data?.code };
1681
+ return {
1682
+ success: false,
1683
+ error: data?.error || 'Create account failed.',
1684
+ code: data?.code,
1685
+ verificationRequired: Boolean(data?.verificationRequired),
1686
+ verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
1687
+ emailSent: data?.emailSent !== false,
1688
+ emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
1689
+ };
1690
+ }
1691
+ const planSelectionRequired = data?.planSelectionRequired === true;
1692
+ const checkoutPending = data?.checkoutPending === true;
1693
+ const commercialState = typeof data?.commercialState === 'string' ? data.commercialState : null;
1694
+ const verificationRequired = Boolean(data?.verificationRequired);
1695
+ if (!verificationRequired) {
1696
+ 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
+ const hasCommercialOnboardingGate = Boolean(planSelectionRequired
1707
+ || checkoutPending
1708
+ || commercialState === 'pending_plan_selection'
1709
+ || commercialState === 'checkout_pending');
1710
+ if (workspaceResolution.workspaceSetupRequired || hasCommercialOnboardingGate) {
1711
+ await fetchConfig({ ignoreAuthGuard: true });
1712
+ }
1713
+ else {
1714
+ await Promise.all([
1715
+ fetchConfig({ ignoreAuthGuard: true }),
1716
+ fetchTasks(true, { ignoreAuthGuard: true })
1717
+ ]);
1718
+ void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
1719
+ }
1720
+ return {
1721
+ success: true,
1722
+ workspaceSetupRequired: workspaceResolution.workspaceSetupRequired,
1723
+ planSelectionRequired,
1724
+ checkoutPending,
1725
+ commercialState,
1726
+ verificationRequired,
1727
+ verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
1728
+ emailSent: data?.emailSent !== false,
1729
+ emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
1730
+ };
2230
1731
  }
2231
1732
  return {
2232
1733
  success: true,
2233
1734
  workspaceSetupRequired: Boolean(data?.workspaceSetupRequired),
2234
- planSelectionRequired: data?.planSelectionRequired === true,
2235
- checkoutPending: data?.checkoutPending === true,
2236
- commercialState: typeof data?.commercialState === 'string' ? data.commercialState : null,
2237
- verificationRequired: Boolean(data?.verificationRequired),
1735
+ planSelectionRequired,
1736
+ checkoutPending,
1737
+ commercialState,
1738
+ verificationRequired,
2238
1739
  verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
2239
1740
  emailSent: data?.emailSent !== false,
2240
1741
  emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
@@ -2243,7 +1744,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2243
1744
  catch {
2244
1745
  return { success: false, error: 'Create account failed.' };
2245
1746
  }
2246
- }, [resolveCloudAuthUrl, cloudAuthConfigured]);
1747
+ }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
2247
1748
  const updateCurrentUserProfile = useCallback(async (input) => {
2248
1749
  if (!cloudAuthConfigured) {
2249
1750
  return { success: false, error: 'Cloud auth endpoint is not configured.' };
@@ -2305,6 +1806,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2305
1806
  success: true,
2306
1807
  verificationToken: data?.verificationToken ?? null,
2307
1808
  emailSent: data?.emailSent !== false,
1809
+ deliveryAttempted: data?.deliveryAttempted === true
1810
+ || (typeof data?.verificationToken === 'string' && data.verificationToken.trim().length > 0),
2308
1811
  emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
2309
1812
  };
2310
1813
  }
@@ -2358,6 +1861,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2358
1861
  success: true,
2359
1862
  resetToken: data?.resetToken ?? null,
2360
1863
  emailSent: data?.emailSent !== false,
1864
+ deliveryAttempted: data?.deliveryAttempted === true
1865
+ || (typeof data?.resetToken === 'string' && data.resetToken.trim().length > 0),
2361
1866
  emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
2362
1867
  };
2363
1868
  }
@@ -2492,7 +1997,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2492
1997
  return { success: false, error: data?.error || 'Invite join failed.', code: data?.code };
2493
1998
  }
2494
1999
  setError('');
2495
- await checkAuthSession();
2000
+ await checkAuthSession({ force: true });
2496
2001
  const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
2497
2002
  const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
2498
2003
  if (!workspaceResolution.success) {
@@ -2516,6 +2021,34 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2516
2021
  return { success: false, error: 'Invite join failed.' };
2517
2022
  }
2518
2023
  }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
2024
+ const resetCloudLogoutClientState = useCallback(() => {
2025
+ abortWorkspaceScopedRequests();
2026
+ referenceDataRequestRef.current = null;
2027
+ lastLoadedUiStateKeyRef.current = '';
2028
+ lastAssigneeOptionsLoadKeyRef.current = '';
2029
+ lastConfigRefreshKeyRef.current = '';
2030
+ lastWorkspaceSyncStateLoadKeyRef.current = '';
2031
+ lastTaskSurfaceLoadKeyRef.current = '';
2032
+ lastSettingsSurfaceLoadKeyRef.current = '';
2033
+ setUiStateReady(false);
2034
+ setReferenceDataLoaded(false);
2035
+ setAssigneeOptions(createDefaultAssigneeOptions());
2036
+ setAssigneeOptionsLoaded(false);
2037
+ setDeletedTasks([]);
2038
+ setTasks([]);
2039
+ setArchivedTasks([]);
2040
+ setInitiatives([]);
2041
+ setWorkstreams([]);
2042
+ setLoadingTasks(false);
2043
+ if (cloudRealtimeFollowUpRefreshTimerRef.current !== null && typeof window !== 'undefined') {
2044
+ window.clearTimeout(cloudRealtimeFollowUpRefreshTimerRef.current);
2045
+ cloudRealtimeFollowUpRefreshTimerRef.current = null;
2046
+ }
2047
+ if (archiveBootstrapTimerRef.current !== null && typeof window !== 'undefined') {
2048
+ window.clearTimeout(archiveBootstrapTimerRef.current);
2049
+ archiveBootstrapTimerRef.current = null;
2050
+ }
2051
+ }, [abortWorkspaceScopedRequests, setArchivedTasks, setTasks]);
2519
2052
  const logout = useCallback(async () => {
2520
2053
  try {
2521
2054
  if (cloudAuthConfigured) {
@@ -2529,6 +2062,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2529
2062
  // no-op
2530
2063
  }
2531
2064
  finally {
2065
+ abortWorkspaceScopedRequests();
2066
+ authSessionEpochRef.current += 1;
2532
2067
  authSessionRequestRef.current = null;
2533
2068
  authSessionLastResultRef.current = false;
2534
2069
  authSessionLastCheckedAtRef.current = Date.now();
@@ -2543,19 +2078,18 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2543
2078
  setUserGlobalSyncStatus('disconnected');
2544
2079
  setUserGlobalSyncError(null);
2545
2080
  setHasBetaAccess(true);
2081
+ setProjectName('');
2082
+ setConfigLoaded(false);
2083
+ setReferenceDataLoaded(false);
2084
+ setSetupState(null);
2085
+ setBootstrapPhase('idle');
2086
+ setBootstrapError(null);
2087
+ hasLoadedUiStateRef.current = false;
2088
+ if (runtimeMode !== 'local') {
2089
+ clearPersistedWorkspaceId(workspaceSelectionScope);
2090
+ }
2546
2091
  setAuthBlocked(runtimeMode === 'cloud' && authRequiredForApi);
2547
- setCurrentWorkspaceId((prev) => {
2548
- const existing = String(prev || '').trim();
2549
- let resolvedWorkspaceId = 'default';
2550
- if (existing && existing.toLowerCase() !== 'default') {
2551
- resolvedWorkspaceId = existing;
2552
- }
2553
- else if (runtimeMode !== 'local') {
2554
- resolvedWorkspaceId = readPersistedWorkspaceId(workspaceSelectionScope) || 'default';
2555
- }
2556
- currentWorkspaceIdRef.current = resolvedWorkspaceId;
2557
- return resolvedWorkspaceId;
2558
- });
2092
+ commitCurrentWorkspaceId('default');
2559
2093
  setAvailableWorkspaces([]);
2560
2094
  workspaceListHydratedRef.current = false;
2561
2095
  setHydratedWorkspaceRole(null);
@@ -2568,12 +2102,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2568
2102
  ]);
2569
2103
  return;
2570
2104
  }
2571
- setTasks([]);
2572
- setInitiatives([]);
2573
- setWorkstreams([]);
2105
+ hasBootstrappedDataRef.current = false;
2106
+ resetCloudLogoutClientState();
2574
2107
  }
2575
2108
  }, [
2109
+ abortWorkspaceScopedRequests,
2576
2110
  authRequiredForApi,
2111
+ clearPersistedWorkspaceId,
2112
+ commitCurrentWorkspaceId,
2577
2113
  resolveCloudAuthUrl,
2578
2114
  cloudAuthConfigured,
2579
2115
  workspaceSelectionScope,
@@ -2581,7 +2117,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2581
2117
  fetchConfig,
2582
2118
  fetchPlanningEntities,
2583
2119
  fetchReferenceData,
2584
- fetchTasks
2120
+ fetchTasks,
2121
+ resetCloudLogoutClientState
2585
2122
  ]);
2586
2123
  const createInitiativeFromTemplate = useCallback(async (payload) => {
2587
2124
  try {
@@ -2699,7 +2236,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2699
2236
  }, [fetchPlanningEntities]);
2700
2237
  // Initialize auth once, then bootstrap protected data once auth gates are settled.
2701
2238
  const hasInitializedAuthRef = useRef(false);
2702
- const hasBootstrappedDataRef = useRef(false);
2703
2239
  useEffect(() => {
2704
2240
  setIsOpen(true); // Always open in standalone-first
2705
2241
  if (hasInitializedAuthRef.current)
@@ -2958,73 +2494,19 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2958
2494
  userId: normalizedRealtimeUserId || undefined
2959
2495
  });
2960
2496
  // Fallback sync for cross-process writes (e.g., MCP tools) that may miss watcher/websocket events.
2961
- useEffect(() => {
2962
- if (typeof window === 'undefined' || typeof document === 'undefined')
2963
- return;
2964
- if (shouldSkipDataVersionPolling({
2965
- isOpen,
2966
- authBlocked,
2967
- authRequiredForApi,
2968
- isAuthenticated,
2969
- canCallProtectedApi: authGuardPolicy.canCallProtectedApi
2970
- }))
2971
- return;
2972
- let cancelled = false;
2973
- const checkDataVersion = async () => {
2974
- if (cancelled || document.visibilityState !== 'visible')
2975
- return;
2976
- if (shouldSkipDataVersionTick({
2977
- shouldGateProtectedApiCalls,
2978
- authSessionResolved: authSessionResolvedRef.current,
2979
- authRequiredForApi: authRequiredForApiRef.current,
2980
- isAuthenticated: isAuthenticatedRef.current
2981
- }))
2982
- return;
2983
- try {
2984
- const res = await fetch('/api/taskforce/data-version');
2985
- if (!res.ok)
2986
- return;
2987
- const data = await res.json();
2988
- const version = Number(data?.dataVersion);
2989
- if (!Number.isFinite(version))
2990
- return;
2991
- if (dataVersionRef.current === null) {
2992
- dataVersionRef.current = version;
2993
- return;
2994
- }
2995
- if (version !== dataVersionRef.current) {
2996
- dataVersionRef.current = version;
2997
- await refreshTaskCollectionsForDataVersion();
2998
- }
2999
- }
3000
- catch {
3001
- // Keep polling best-effort and silent.
3002
- }
3003
- };
3004
- const onVisibilityChange = () => {
3005
- if (document.visibilityState === 'visible') {
3006
- checkDataVersion();
3007
- }
3008
- };
3009
- const intervalId = window.setInterval(checkDataVersion, 3000);
3010
- document.addEventListener('visibilitychange', onVisibilityChange);
3011
- checkDataVersion();
3012
- return () => {
3013
- cancelled = true;
3014
- window.clearInterval(intervalId);
3015
- document.removeEventListener('visibilitychange', onVisibilityChange);
3016
- };
3017
- }, [
2497
+ useDataVersionRefresh({
3018
2498
  isOpen,
3019
- fetchTasks,
3020
- fetchArchive,
3021
2499
  authBlocked,
3022
2500
  authRequiredForApi,
3023
2501
  isAuthenticated,
3024
- authGuardPolicy,
2502
+ canCallProtectedApi: authGuardPolicy.canCallProtectedApi,
3025
2503
  shouldGateProtectedApiCalls,
2504
+ authSessionResolvedRef,
2505
+ authRequiredForApiRef,
2506
+ isAuthenticatedRef,
2507
+ dataVersionRef,
3026
2508
  refreshTaskCollectionsForDataVersion
3027
- ]);
2509
+ });
3028
2510
  const relationshipTasks = useMemo(() => {
3029
2511
  const byId = new Map();
3030
2512
  for (const task of archivedTasks)
@@ -3251,6 +2733,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3251
2733
  pushWorkspaceChangesToCloud,
3252
2734
  workspacePendingSignatureRef,
3253
2735
  workspaceDeletedTaskIdsRef,
2736
+ workspaceDeletedTaskWatermarksRef,
3254
2737
  });
3255
2738
  const { autoSaveState, flushAutoSave, scheduleWarningPrompt, handleSubmit, confirmScheduleWarning, cancelScheduleWarning, handleAddComment } = useTaskFormActions({
3256
2739
  activeCategories,
@@ -3520,6 +3003,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3520
3003
  workspaceLastErrorMessage,
3521
3004
  workspaceLastSuccessfulSyncAt,
3522
3005
  workspaceSyncPhase,
3006
+ workspaceSyncSetupIntent,
3523
3007
  workspaceSyncStatus,
3524
3008
  workspaceSyncSummary,
3525
3009
  workspaceSyncRecommendedAction,
@@ -3548,6 +3032,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3548
3032
  runtimeCapabilities,
3549
3033
  refreshSetupContext,
3550
3034
  retryBootstrapChecks,
3035
+ continueAfterCommercialOnboarding,
3551
3036
  saveWorkspaceProfile,
3552
3037
  fetchWorkspaces,
3553
3038
  createWorkspace,