@taskforcehq/taskforce 0.3.302 → 0.3.304

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 (192) hide show
  1. package/dist/Taskforce.module.css +113 -5
  2. package/dist/TaskforceCore.js +560 -208
  3. package/dist/TaskforceCore.test.js +1833 -91
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskKanban.test.js +17 -0
  21. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  22. package/dist/components/task/TaskStatusActions.js +8 -3
  23. package/dist/components/task/TaskStatusActions.test.js +9 -0
  24. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  25. package/dist/components/task/taskKanbanDropRules.js +187 -0
  26. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  27. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  28. package/dist/components/ui/Modal.d.ts +1 -1
  29. package/dist/components/ui/Modal.js +1 -0
  30. package/dist/components/views/AppShellHeader.js +5 -1
  31. package/dist/components/views/AppShellHeader.test.js +4 -0
  32. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  33. package/dist/components/views/PlanComparisonPage.js +4 -2
  34. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  35. package/dist/components/views/PlansPage.d.ts +7 -1
  36. package/dist/components/views/PlansPage.js +139 -53
  37. package/dist/components/views/PlansPage.test.d.ts +1 -0
  38. package/dist/components/views/PlansPage.test.js +157 -0
  39. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  40. package/dist/components/views/StandaloneLayout.js +216 -186
  41. package/dist/components/views/WidgetView.js +11 -2
  42. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  43. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  44. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  45. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  46. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  47. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  48. package/dist/core/EntitlementsPolicy.test.js +2 -2
  49. package/dist/core/GlobalSettingsService.js +78 -18
  50. package/dist/core/PlanEntitlementService.d.ts +31 -5
  51. package/dist/core/PlanEntitlementService.js +350 -222
  52. package/dist/core/SignupMonetizationSettings.test.js +208 -49
  53. package/dist/core/SystemAdmin.test.js +186 -96
  54. package/dist/core/TaskActivityService.d.ts +4 -1
  55. package/dist/core/TaskActivityService.js +45 -0
  56. package/dist/core/TaskAuditTimeline.test.js +52 -1
  57. package/dist/core/Taskforce.d.ts +65 -20
  58. package/dist/core/Taskforce.js +261 -51
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  61. package/dist/core/WorkspacePlanMode.test.js +65 -0
  62. package/dist/core/types.d.ts +11 -12
  63. package/dist/documentReviews/service.d.ts +1 -1
  64. package/dist/documentReviews/types.d.ts +1 -1
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  66. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  67. package/dist/hooks/sync/auth.d.ts +37 -0
  68. package/dist/hooks/sync/auth.js +61 -0
  69. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  70. package/dist/hooks/sync/bootstrap.js +52 -0
  71. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  72. package/dist/hooks/sync/orchestratorShared.js +209 -0
  73. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  74. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  75. package/dist/hooks/sync/realtime.d.ts +25 -0
  76. package/dist/hooks/sync/realtime.js +60 -0
  77. package/dist/hooks/sync/recovery.d.ts +69 -0
  78. package/dist/hooks/sync/recovery.js +118 -0
  79. package/dist/hooks/sync/transfers.d.ts +122 -0
  80. package/dist/hooks/sync/transfers.js +431 -0
  81. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  82. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  83. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  84. package/dist/hooks/ui/useResourceExport.js +5 -2
  85. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  86. package/dist/hooks/useRealtimeSync.js +2 -1
  87. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  88. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  89. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  90. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  91. package/dist/hooks/useSyncOrchestrator.js +315 -860
  92. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
  93. package/dist/hooks/useTaskMutations.js +45 -6
  94. package/dist/hooks/useTaskMutations.test.js +147 -0
  95. package/dist/hooks/useTaskforce.d.ts +11 -2
  96. package/dist/hooks/useTaskforce.js +290 -812
  97. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  98. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  99. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  100. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  101. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  102. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  103. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  104. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  105. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  106. package/dist/localization/locales/en-US.js +2 -2
  107. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  108. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  109. package/dist/mcp/clientIntegrations.d.ts +12 -1
  110. package/dist/mcp/clientIntegrations.js +161 -6
  111. package/dist/mcp/clientIntegrations.test.js +76 -4
  112. package/dist/mcp/documentHelpers.d.ts +100 -0
  113. package/dist/mcp/documentHelpers.js +161 -0
  114. package/dist/mcp/runtime.js +264 -505
  115. package/dist/mcp/runtime.test.js +353 -89
  116. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  117. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  118. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  119. package/dist/mcp/taskAttachmentTypes.js +1 -0
  120. package/dist/migrations/billingSchemaParity.test.js +3 -0
  121. package/dist/migrations/taskSchemaMigrations.js +15 -0
  122. package/dist/plugins/vite.js +1 -1
  123. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  124. package/dist/runtime/appGateDefinitions.js +1 -1
  125. package/dist/server/index.cookieProxy.test.js +1 -0
  126. package/dist/server/index.d.ts +1 -0
  127. package/dist/server/index.js +48 -3
  128. package/dist/server/index.test.js +14 -2
  129. package/dist/server/mockStripe.d.ts +1 -0
  130. package/dist/server/mockStripe.js +2 -0
  131. package/dist/server/routes/admin.js +91 -40
  132. package/dist/server/routes/auth.d.ts +1 -0
  133. package/dist/server/routes/auth.js +231 -55
  134. package/dist/server/routes/billing.js +473 -39
  135. package/dist/server/routes/billing.test.js +972 -37
  136. package/dist/server/routes/shared.d.ts +30 -0
  137. package/dist/server/routes/shared.js +31 -3
  138. package/dist/server/routes/sync.integration.test.js +180 -0
  139. package/dist/server/routes/sync.js +125 -242
  140. package/dist/server/routes/tasks.js +49 -7
  141. package/dist/server/routes.js +11 -22
  142. package/dist/server/routes.test.js +530 -81
  143. package/dist/shared/taskActor.d.ts +10 -0
  144. package/dist/shared/taskActor.js +1 -0
  145. package/dist/sync/cloudSyncApi.d.ts +1 -0
  146. package/dist/sync/collaborationSyncPayload.js +4 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -1
  149. package/dist/sync/collaborationSyncState.js +19 -0
  150. package/dist/sync/contentSyncState.d.ts +10 -0
  151. package/dist/sync/contentSyncState.js +277 -14
  152. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  153. package/dist/sync/syncApplyHandlers.js +370 -8
  154. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  155. package/dist/sync/syncApplyHandlers.test.js +377 -0
  156. package/dist/sync/syncService.d.ts +9 -1
  157. package/dist/sync/syncService.js +57 -10
  158. package/dist/sync/syncService.test.js +48 -0
  159. package/dist/sync/workspacePullFeed.d.ts +11 -0
  160. package/dist/sync/workspacePullFeed.js +123 -68
  161. package/dist/sync/workspacePullFeed.test.js +180 -0
  162. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  163. package/dist/sync/workspaceSyncModel.js +65 -0
  164. package/dist/sync/workspaceSyncModel.test.js +119 -0
  165. package/dist/types.d.ts +2 -10
  166. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  167. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  168. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  169. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
  170. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  171. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  172. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  173. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
  174. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  175. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  176. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
  177. package/dist/ui/index.html +4 -4
  178. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  179. package/dist/utils/accountProfileSummaryCache.js +97 -0
  180. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  181. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  182. package/dist/utils/taskActivity.js +4 -0
  183. package/dist/utils/taskActivity.test.js +25 -1
  184. package/package.json +5 -3
  185. package/scripts/playwright-auth.sh +4 -0
  186. package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
  187. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  188. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
  189. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  190. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  191. package/dist/utils/billingStatusCache.d.ts +0 -21
  192. package/dist/utils/billingStatusCache.js +0 -70
@@ -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);
@@ -229,6 +231,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
229
231
  if (!path.startsWith('/'))
230
232
  return path;
231
233
  const isCloudScopedPath = path.startsWith('/api/taskforce/auth/')
234
+ || path.startsWith('/api/taskforce/account/')
235
+ || path.startsWith('/api/taskforce/billing/')
232
236
  || path.startsWith('/api/taskforce/sync/')
233
237
  || path.startsWith('/api/taskforce/settings/mcp/');
234
238
  if (!isCloudHost && isCloudScopedPath && (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy)) {
@@ -343,6 +347,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
343
347
  const [bootstrapError, setBootstrapError] = useState(null);
344
348
  const [bootstrapStartedAt, setBootstrapStartedAt] = useState(null);
345
349
  const authSessionRequestRef = useRef(null);
350
+ const authSessionEpochRef = useRef(0);
346
351
  const authSessionLastCheckedAtRef = useRef(0);
347
352
  const authSessionLastResultRef = useRef(hasOptimisticLocalAuth);
348
353
  const currentWorkspaceIdRef = useRef(currentWorkspaceId);
@@ -373,25 +378,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
373
378
  useEffect(() => {
374
379
  currentWorkspaceIdRef.current = currentWorkspaceId;
375
380
  }, [currentWorkspaceId]);
376
- const applyResolvedWorkspaceId = useCallback((nextWorkspaceIdRaw, options) => {
381
+ const applyResolvedWorkspaceId = useCallback((nextWorkspaceIdRaw) => {
377
382
  const nextWorkspaceId = String(nextWorkspaceIdRaw || '').trim();
378
383
  if (!nextWorkspaceId)
379
384
  return;
380
- setCurrentWorkspaceId((prev) => {
385
+ setCurrentWorkspaceId(() => {
381
386
  const explicitWorkspaceId = String(explicitWorkspaceSelectionRef.current || '').trim();
382
387
  let resolvedWorkspaceId = nextWorkspaceId;
383
388
  if (explicitWorkspaceId && explicitWorkspaceId !== nextWorkspaceId) {
384
389
  resolvedWorkspaceId = explicitWorkspaceId;
385
390
  }
386
- else {
387
- const prevValue = String(prev || '').trim();
388
- if (options?.preserveNonDefaultDefault
389
- && nextWorkspaceId === 'default'
390
- && prevValue
391
- && prevValue.toLowerCase() !== 'default') {
392
- resolvedWorkspaceId = prevValue;
393
- }
394
- }
395
391
  currentWorkspaceIdRef.current = resolvedWorkspaceId;
396
392
  return resolvedWorkspaceId;
397
393
  });
@@ -475,7 +471,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
475
471
  shouldBlockProtectedApiCalls
476
472
  });
477
473
  const { zenMode, setZenModeState, toggleZenMode } = useZenMode();
478
- const { exportEnvironment, setExportEnvironment, availableEnvironments, loadAvailableEnvironments, exportWorkflowsPath, setExportWorkflowsPath, exportingResource, exportResult, handleExportWorkflows } = useResourceExport({ storagePath });
474
+ const { exportEnvironment, setExportEnvironment, availableEnvironments, loadAvailableEnvironments, exportWorkflowsPath, setExportWorkflowsPath, exportingResource, exportResult, handleExportWorkflows } = useResourceExport({
475
+ storagePath,
476
+ shouldDeferProtectedApiCalls,
477
+ shouldBlockProtectedApiCalls
478
+ });
479
479
  // Unsaved Changes Modal State
480
480
  const [unsavedModalOpen, setUnsavedModalOpen] = useState(false);
481
481
  const [pendingNavigation, setPendingNavigation] = useState(null);
@@ -811,158 +811,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
811
811
  await fetchDeletedTasks(true);
812
812
  await fetchPlanningEntities();
813
813
  }, [fetchDeletedTasks, fetchPlanningEntities, refreshTaskCollectionsFromInvalidationBase]);
814
- const checkAuthSession = useCallback(async (options) => {
815
- const force = options?.force === true;
816
- markBootstrapPhase('auth');
817
- if (!runtimeConfigReady) {
818
- // Keep prior auth identity during bootstrap until runtime auth config is resolved.
819
- // This avoids transient "signed out" UI flicker on refresh.
820
- return authSessionLastResultRef.current;
821
- }
822
- if (!shouldProbeCloudAuth) {
823
- setRuntimeMode('local');
824
- setAuthRequiredForApi(false);
825
- setIsAuthenticated(false);
826
- setAuthUserId('anonymous');
827
- setAuthWorkspaceId('');
828
- persistLocalMutationActorUserId(null);
829
- setAuthUserEmail('');
830
- setAuthUserDisplayName('');
831
- setAuthUserAvatarUrl('');
832
- setUserGlobalSyncStatus('disconnected');
833
- setUserGlobalSyncError(null);
834
- setHasBetaAccess(true);
835
- setAvailableWorkspaces([]);
836
- setHydratedWorkspaceRole(null);
837
- setAssigneeOptions(createDefaultAssigneeOptions());
838
- setAuthBlocked(false);
839
- setAuthSessionResolved(true);
840
- authSessionLastResultRef.current = false;
841
- authSessionLastCheckedAtRef.current = Date.now();
842
- return false;
843
- }
844
- const sessionUrl = resolveCloudAuthUrl('/api/taskforce/auth/session');
845
- const now = Date.now();
846
- if (!force && authSessionRequestRef.current)
847
- return authSessionRequestRef.current;
848
- if (!force && authSessionResolved && (now - authSessionLastCheckedAtRef.current) < 1500) {
849
- return authSessionLastResultRef.current;
850
- }
851
- const request = (async () => {
852
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
853
- const abortController = new AbortController();
854
- const timeoutId = typeof window !== 'undefined'
855
- ? window.setTimeout(() => abortController.abort(), 8000)
856
- : null;
857
- try {
858
- const res = await fetch(sessionUrl, {
859
- method: 'GET',
860
- credentials: 'include',
861
- mode: 'cors',
862
- signal: abortController.signal
863
- });
864
- if (res.status === 429) {
865
- setAuthSessionResolved(true);
866
- return authSessionLastResultRef.current;
867
- }
868
- if (!res.ok) {
869
- if (isLoopbackHost)
870
- persistLocalMutationActorUserId(null);
871
- setAuthSessionResolved(true);
872
- authSessionLastResultRef.current = false;
873
- return false;
874
- }
875
- const data = await res.json();
876
- const required = Boolean(data.authRequiredForApi);
877
- const authenticated = Boolean(data.authenticated);
878
- const betaAccess = authenticated ? (data.betaAccess !== false) : true;
879
- const workspaceId = typeof data.workspaceId === 'string' && data.workspaceId.trim().length > 0
880
- ? data.workspaceId.trim()
881
- : '';
882
- const userId = typeof data.userId === 'string' && data.userId.trim().length > 0
883
- ? data.userId.trim()
884
- : 'anonymous';
885
- const userEmail = typeof data.email === 'string' && data.email.trim().length > 0
886
- ? data.email.trim().toLowerCase()
887
- : '';
888
- const userDisplayName = typeof data.displayName === 'string'
889
- ? data.displayName.trim()
890
- : '';
891
- const userAvatarUrl = typeof data.avatarUrl === 'string'
892
- ? data.avatarUrl.trim()
893
- : '';
894
- const isLocalRuntime = runtimeMode === 'local';
895
- const keepLocalWorkspaceContext = runtimeMode === 'local' || authOnlyCloudMode;
896
- setAuthRequiredForApi(authOnlyCloudMode ? false : (isLocalRuntime ? false : required));
897
- setIsAuthenticated(authenticated);
898
- setAuthUserId(authenticated ? userId : 'anonymous');
899
- setAuthWorkspaceId(authenticated ? workspaceId : '');
900
- if (isLoopbackHost) {
901
- persistLocalMutationActorUserId(authenticated ? userId : null);
902
- }
903
- setAuthUserEmail(authenticated ? userEmail : '');
904
- setAuthUserDisplayName(authenticated ? userDisplayName : '');
905
- setAuthUserAvatarUrl(authenticated ? userAvatarUrl : '');
906
- if (authenticated) {
907
- setError((prev) => prev === AUTH_REQUIRED_ERROR ? '' : prev);
908
- }
909
- setUserGlobalSyncStatus(authenticated ? 'idle' : 'disconnected');
910
- setUserGlobalSyncError(null);
911
- setHasBetaAccess(betaAccess);
912
- if (keepLocalWorkspaceContext) {
913
- // Local runtime resolves workspace identity from local DB via /api/taskforce/config.
914
- // Do not override it from cloud auth session payloads.
915
- }
916
- else {
917
- applyResolvedWorkspaceId(workspaceId || readPersistedWorkspaceId(workspaceSelectionScope) || 'default');
918
- }
919
- setAuthBlocked(authOnlyCloudMode ? false : (isLocalRuntime ? false : (required && !authenticated)));
920
- // Runtime mode should be sourced from /api/taskforce/config, not
921
- // from auth-session responses which may come from a separate cloud-auth origin.
922
- setAuthSessionResolved(true);
923
- authSessionLastResultRef.current = authenticated;
924
- logBootstrapDebug('auth_session_resolved', {
925
- authenticated,
926
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
927
- });
928
- if (authenticated)
929
- markBootstrapPhase('ready');
930
- return authenticated;
931
- }
932
- catch {
933
- if (isLoopbackHost)
934
- persistLocalMutationActorUserId(null);
935
- setAuthSessionResolved(true);
936
- authSessionLastResultRef.current = false;
937
- logBootstrapDebug('auth_session_failed', {
938
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
939
- });
940
- markBootstrapStalled('Unable to verify your session.');
941
- return authSessionLastResultRef.current;
942
- }
943
- finally {
944
- if (timeoutId !== null && typeof window !== 'undefined') {
945
- window.clearTimeout(timeoutId);
946
- }
947
- authSessionLastCheckedAtRef.current = Date.now();
948
- authSessionRequestRef.current = null;
949
- }
950
- })();
951
- authSessionRequestRef.current = request;
952
- return request;
953
- }, [
954
- runtimeConfigReady,
955
- shouldProbeCloudAuth,
956
- authSessionResolved,
957
- resolveCloudAuthUrl,
958
- authOnlyCloudMode,
959
- AUTH_REQUIRED_ERROR,
960
- runtimeMode,
961
- applyResolvedWorkspaceId,
962
- workspaceSelectionScope,
963
- markBootstrapPhase,
964
- markBootstrapStalled
965
- ]);
814
+ const setUserGlobalSyncStatusRef = useRef(() => undefined);
815
+ const setUserGlobalSyncErrorRef = useRef(() => undefined);
816
+ const checkAuthSessionImplRef = useRef(async () => false);
817
+ const checkAuthSession = useCallback((options) => checkAuthSessionImplRef.current(options), []);
966
818
  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({
967
819
  currentWorkspaceId,
968
820
  cloudAuthConfigured,
@@ -1044,6 +896,50 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1044
896
  assigneeOptionsLoaded,
1045
897
  assigneeOptions
1046
898
  });
899
+ setUserGlobalSyncStatusRef.current = setUserGlobalSyncStatus;
900
+ setUserGlobalSyncErrorRef.current = setUserGlobalSyncError;
901
+ const { checkAuthSession: checkAuthSessionImpl } = useTaskforceAuthBootstrap({
902
+ runtimeConfigReady,
903
+ shouldProbeCloudAuth,
904
+ authSessionResolved,
905
+ resolveCloudAuthUrl,
906
+ authOnlyCloudMode,
907
+ authRequiredError: AUTH_REQUIRED_ERROR,
908
+ runtimeMode,
909
+ workspaceSelectionScope,
910
+ markBootstrapPhase,
911
+ markBootstrapStalled,
912
+ setRuntimeMode,
913
+ setAuthRequiredForApi,
914
+ setIsAuthenticated,
915
+ setAuthUserId,
916
+ setAuthWorkspaceId,
917
+ setAuthUserEmail,
918
+ setAuthUserDisplayName,
919
+ setAuthUserAvatarUrl,
920
+ setUserGlobalSyncStatus: (value) => setUserGlobalSyncStatusRef.current(value),
921
+ setUserGlobalSyncError: (value) => setUserGlobalSyncErrorRef.current(value),
922
+ setHasBetaAccess,
923
+ setAvailableWorkspaces,
924
+ setHydratedWorkspaceRole,
925
+ setAssigneeOptions,
926
+ setAuthBlocked,
927
+ setAuthSessionResolved,
928
+ setError,
929
+ applyResolvedWorkspaceId,
930
+ readPersistedWorkspaceId,
931
+ authSessionRequestRef,
932
+ authSessionEpochRef,
933
+ authSessionLastCheckedAtRef,
934
+ authSessionLastResultRef,
935
+ isLoopbackHost,
936
+ setAuthStateReadyRefs: (authenticated, resolved, required) => {
937
+ authSessionResolvedRef.current = resolved;
938
+ authRequiredForApiRef.current = required;
939
+ isAuthenticatedRef.current = authenticated;
940
+ }
941
+ });
942
+ checkAuthSessionImplRef.current = checkAuthSessionImpl;
1047
943
  const [activeWorkspaceModule, setActiveWorkspaceModule] = useState('tasks');
1048
944
  const [showComments, setShowComments] = useState(false);
1049
945
  const [descriptionFocused, setDescriptionFocused] = useState(false);
@@ -1067,6 +963,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1067
963
  const skipNextUiStatePersistRef = useRef(false);
1068
964
  const referenceDataRequestRef = useRef(null);
1069
965
  const workspaceListHydratedRef = useRef(false);
966
+ const hasBootstrappedDataRef = useRef(false);
1070
967
  const [uiStateReady, setUiStateReady] = useState(false);
1071
968
  const { handleSaveSettings, handleSaveTheme, handleSaveGlobalTheme, handleJsonBackupEnabledChange, handleSaveGlobalJsonBackupEnabled, handleSaveGlobalWeekStartsOn, handleSaveLocale, handleManualComplexityEnabledChange, handleChecklistDropdownEnabledChange, handleShowTaskCardStatusLabelChange, handleResetProjectToGlobal } = useSettingsPersistence({
1072
969
  keyShortcut,
@@ -1234,15 +1131,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1234
1131
  writePersistedAppUiState(uiStateSnapshot, workspaceUiStateScope);
1235
1132
  const timeoutId = window.setTimeout(async () => {
1236
1133
  try {
1237
- await fetch('/api/taskforce/ui-state', {
1238
- method: 'POST',
1239
- headers: { 'Content-Type': 'application/json' },
1240
- credentials: 'include',
1241
- body: JSON.stringify({
1242
- stateKey: 'app',
1243
- workspaceId: currentWorkspaceIdRef.current,
1244
- patch: uiStateSnapshot
1245
- })
1134
+ await persistRemoteUiState({
1135
+ stateKey: 'app',
1136
+ workspaceId: currentWorkspaceIdRef.current,
1137
+ patch: uiStateSnapshot
1246
1138
  });
1247
1139
  }
1248
1140
  catch {
@@ -1279,13 +1171,15 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1279
1171
  ? data.userId.trim()
1280
1172
  : 'anonymous';
1281
1173
  const serverAuthenticated = nextRuntimeMode === 'cloud' && nextUserId !== 'anonymous';
1174
+ const knownAuthenticated = serverAuthenticated || isAuthenticatedRef.current;
1175
+ const authResolved = authSessionResolvedRef.current;
1282
1176
  setRuntimeMode(nextRuntimeMode);
1283
1177
  setAuthRequiredForApi(nextAuthRequiredForApi);
1284
1178
  if (serverAuthenticated) {
1285
1179
  setIsAuthenticated(true);
1286
1180
  setAuthUserId(nextUserId);
1287
1181
  setAuthBlocked(false);
1288
- if (!authSessionResolved) {
1182
+ if (!authResolved) {
1289
1183
  setAuthSessionResolved(true);
1290
1184
  }
1291
1185
  }
@@ -1295,11 +1189,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1295
1189
  commitCurrentWorkspaceId(nextWorkspaceId);
1296
1190
  }
1297
1191
  else {
1298
- applyResolvedWorkspaceId(nextWorkspaceId, { preserveNonDefaultDefault: true });
1192
+ applyResolvedWorkspaceId(nextWorkspaceId);
1299
1193
  }
1300
1194
  }
1301
1195
  if (nextRuntimeMode === 'cloud' && nextAuthRequiredForApi) {
1302
- setAuthBlocked(!(serverAuthenticated || isAuthenticated));
1196
+ setAuthBlocked(!knownAuthenticated);
1303
1197
  }
1304
1198
  else {
1305
1199
  setAuthBlocked(false);
@@ -1397,7 +1291,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1397
1291
  else {
1398
1292
  setShowTaskCardStatusLabel(true);
1399
1293
  }
1400
- }, [applyResolvedWorkspaceId, authSessionResolved, commitCurrentWorkspaceId, isAuthenticated]);
1294
+ }, [applyResolvedWorkspaceId, commitCurrentWorkspaceId]);
1401
1295
  const fetchBuildInfo = useCallback(async () => {
1402
1296
  const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
1403
1297
  try {
@@ -1423,417 +1317,83 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1423
1317
  }
1424
1318
  }
1425
1319
  }, [fetchWithTimeout, isBootstrapTimeoutError, markBootstrapStalled]);
1426
- const fetchReferenceData = useCallback(async (options) => {
1427
- const ignoreAuthGuard = options?.ignoreAuthGuard === true;
1428
- const force = options?.force === true;
1429
- if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
1430
- return;
1431
- const requestState = getWorkspaceRequestState();
1432
- if (!force && referenceDataRequestRef.current?.workspaceId === requestState.workspaceId) {
1433
- await referenceDataRequestRef.current.promise;
1434
- return;
1435
- }
1436
- setReferenceDataLoaded(false);
1437
- const abortController = new AbortController();
1438
- referenceDataAbortRef.current = abortController;
1439
- const request = (async () => {
1440
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
1441
- await Promise.allSettled([
1442
- (async () => {
1443
- const taxonomyRes = await fetch('/api/taskforce/taxonomy-state', { signal: abortController.signal });
1444
- if (!taxonomyRes.ok)
1445
- return;
1446
- const taxonomyData = await taxonomyRes.json().catch(() => ({}));
1447
- const hasUnifiedPayload = hasWorkspaceTaxonomyStatePayload(taxonomyData);
1448
- if (!hasUnifiedPayload) {
1449
- const [catRes, typeRes, priorityRes, taxRes] = await Promise.all([
1450
- fetch('/api/taskforce/categories', { signal: abortController.signal }),
1451
- fetch('/api/taskforce/types', { signal: abortController.signal }),
1452
- fetch('/api/taskforce/priorities', { signal: abortController.signal }),
1453
- fetch('/api/taskforce/taxonomies', { signal: abortController.signal })
1454
- ]);
1455
- const [catData, typeData, priorityData, taxData] = await Promise.all([
1456
- catRes.ok ? catRes.json().catch(() => ({})) : Promise.resolve({}),
1457
- typeRes.ok ? typeRes.json().catch(() => ({})) : Promise.resolve({}),
1458
- priorityRes.ok ? priorityRes.json().catch(() => ({})) : Promise.resolve({}),
1459
- taxRes.ok ? taxRes.json().catch(() => ({})) : Promise.resolve({})
1460
- ]);
1461
- taxonomyData.categories = catData.categories;
1462
- taxonomyData.types = typeData.types;
1463
- taxonomyData.priorities = priorityData.priorities;
1464
- taxonomyData.taxonomies = taxData.taxonomies;
1465
- }
1466
- const normalizedTaxonomyState = normalizeWorkspaceTaxonomyStatePayload(taxonomyData);
1467
- if (isWorkspaceRequestStale(requestState))
1468
- return;
1469
- if (normalizedTaxonomyState.categories.length > 0)
1470
- setCustomCategories(normalizedTaxonomyState.categories);
1471
- if (normalizedTaxonomyState.types.length > 0)
1472
- setCustomTypes(normalizedTaxonomyState.types);
1473
- if (normalizedTaxonomyState.priorities.length > 0)
1474
- setPriorities(normalizedTaxonomyState.priorities);
1475
- setTaxonomies(normalizedTaxonomyState.taxonomies);
1476
- setTaxonomyDisplayLabels(normalizedTaxonomyState.displayLabels || {});
1477
- })(),
1478
- (async () => {
1479
- await fetchWorkflows();
1480
- })(),
1481
- (async () => {
1482
- await fetchInitiativeTemplates();
1483
- })(),
1484
- (async () => {
1485
- await loadAvailableEnvironments();
1486
- })(),
1487
- ]);
1488
- if (isWorkspaceRequestStale(requestState))
1489
- return;
1490
- setReferenceDataLoaded(true);
1491
- logBootstrapDebug('reference_data_loaded', {
1492
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
1493
- });
1494
- })();
1495
- referenceDataRequestRef.current = { workspaceId: requestState.workspaceId, promise: request };
1496
- try {
1497
- await request;
1498
- }
1499
- catch (error) {
1500
- if (!isAbortError(error))
1501
- throw error;
1502
- }
1503
- finally {
1504
- if (referenceDataRequestRef.current?.promise === request) {
1505
- referenceDataRequestRef.current = null;
1506
- }
1507
- if (referenceDataAbortRef.current === abortController) {
1508
- referenceDataAbortRef.current = null;
1509
- }
1510
- }
1511
- }, [
1512
- fetchWorkflows,
1513
- fetchInitiativeTemplates,
1514
- getWorkspaceRequestState,
1515
- isAbortError,
1516
- isWorkspaceRequestStale,
1517
- loadAvailableEnvironments,
1320
+ const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks } = useTaskforceWorkspaceBootstrap({
1518
1321
  shouldDeferProtectedApiCalls,
1519
- shouldBlockProtectedApiCalls
1520
- ]);
1521
- const fetchBootstrapConfig = useCallback(async (options) => {
1522
- const ignoreAuthGuard = options?.ignoreAuthGuard === true;
1523
- if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
1524
- return;
1525
- markBootstrapPhase('config');
1526
- const requestState = getWorkspaceRequestState();
1527
- const abortController = new AbortController();
1528
- bootstrapConfigAbortRef.current = abortController;
1529
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
1530
- try {
1531
- const res = await fetchWithTimeout('/api/taskforce/config', undefined, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
1532
- signal: abortController.signal
1533
- });
1534
- if (isWorkspaceEpochStale(requestState))
1535
- return;
1536
- if (res.status === 401) {
1537
- handleUnauthorized();
1538
- setConfigLoaded(true);
1539
- return;
1540
- }
1541
- let configResolvedWorkspaceId = '';
1542
- if (res.ok) {
1543
- const data = await res.json();
1544
- if (isWorkspaceEpochStale(requestState))
1545
- return;
1546
- configResolvedWorkspaceId = typeof data?.workspaceId === 'string' && data.workspaceId.trim().length > 0
1547
- ? data.workspaceId.trim()
1548
- : '';
1549
- applyBootstrapConfig(data);
1550
- }
1551
- if (isWorkspaceEpochStale(requestState)) {
1552
- const liveWorkspaceId = String(currentWorkspaceIdRef.current || 'default').trim() || 'default';
1553
- if (!configResolvedWorkspaceId || liveWorkspaceId !== configResolvedWorkspaceId) {
1554
- return;
1555
- }
1556
- }
1557
- if (!hasLoadedUiStateRef.current) {
1558
- const uiStateWorkspaceId = configResolvedWorkspaceId || requestState.workspaceId;
1559
- hasLoadedUiStateRef.current = true;
1560
- await loadPersistedUiState(uiStateWorkspaceId);
1561
- }
1562
- if (isWorkspaceEpochStale(requestState)) {
1563
- const liveWorkspaceId = String(currentWorkspaceIdRef.current || 'default').trim() || 'default';
1564
- if (!configResolvedWorkspaceId || liveWorkspaceId !== configResolvedWorkspaceId) {
1565
- return;
1566
- }
1567
- }
1568
- setConfigLoaded(true);
1569
- markBootstrapPhase('ready');
1570
- logBootstrapDebug('bootstrap_config_loaded', {
1571
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
1572
- });
1573
- void fetchBuildInfo();
1574
- }
1575
- catch (error) {
1576
- if (isAbortError(error))
1577
- return;
1578
- if (isWorkspaceEpochStale(requestState))
1579
- return;
1580
- if (!hasLoadedUiStateRef.current) {
1581
- hasLoadedUiStateRef.current = true;
1582
- await loadPersistedUiState(requestState.workspaceId);
1583
- }
1584
- if (isBootstrapTimeoutError(error)) {
1585
- markBootstrapStalled('Startup checks timed out.');
1586
- }
1587
- else {
1588
- markBootstrapStalled('Unable to finish startup checks.');
1589
- }
1590
- setConfigLoaded(true);
1591
- }
1592
- finally {
1593
- if (bootstrapConfigAbortRef.current === abortController) {
1594
- bootstrapConfigAbortRef.current = null;
1595
- }
1596
- }
1597
- }, [
1598
- applyBootstrapConfig,
1599
- fetchBuildInfo,
1600
- fetchWithTimeout,
1322
+ shouldBlockProtectedApiCalls,
1601
1323
  getWorkspaceRequestState,
1602
- handleUnauthorized,
1324
+ isWorkspaceRequestStale,
1325
+ isWorkspaceEpochStale,
1603
1326
  isAbortError,
1604
1327
  isBootstrapTimeoutError,
1605
- isWorkspaceEpochStale,
1606
- isWorkspaceRequestStale,
1328
+ fetchWithTimeout,
1329
+ handleUnauthorized,
1330
+ applyBootstrapConfig,
1607
1331
  loadPersistedUiState,
1332
+ fetchBuildInfo,
1608
1333
  markBootstrapPhase,
1609
1334
  markBootstrapStalled,
1610
- shouldDeferProtectedApiCalls,
1611
- shouldBlockProtectedApiCalls
1612
- ]);
1613
- const fetchConfig = useCallback(async (options) => {
1614
- await fetchBootstrapConfig(options);
1615
- await fetchReferenceData(options);
1616
- }, [fetchBootstrapConfig, fetchReferenceData]);
1335
+ fetchWorkflows,
1336
+ fetchInitiativeTemplates,
1337
+ loadAvailableEnvironments,
1338
+ setReferenceDataLoaded,
1339
+ setCustomCategories,
1340
+ setCustomTypes,
1341
+ setPriorities,
1342
+ setTaxonomies,
1343
+ setTaxonomyDisplayLabels,
1344
+ setConfigLoaded,
1345
+ hasLoadedUiStateRef,
1346
+ currentWorkspaceIdRef,
1347
+ referenceDataRequestRef,
1348
+ referenceDataAbortRef,
1349
+ bootstrapConfigAbortRef,
1350
+ workspaceListAbortRef,
1351
+ assigneeOptionsAbortRef,
1352
+ workspaceListHydratedRef,
1353
+ setAvailableWorkspaces,
1354
+ runtimeMode,
1355
+ applyResolvedWorkspaceId,
1356
+ workspaceSwitchingEnabled,
1357
+ authSessionResolved,
1358
+ isAuthenticated,
1359
+ authUserId,
1360
+ setHydratedWorkspaceRole,
1361
+ setAssigneeOptions,
1362
+ setAssigneeOptionsLoaded,
1363
+ commitCurrentWorkspaceId: (workspaceId, options) => {
1364
+ explicitWorkspaceSelectionRef.current = workspaceId;
1365
+ commitCurrentWorkspaceId(workspaceId, options);
1366
+ if (options?.clearExplicitSelection !== false) {
1367
+ explicitWorkspaceSelectionRef.current = null;
1368
+ }
1369
+ },
1370
+ currentWorkspaceId,
1371
+ workspaceSelectionScope,
1372
+ abortWorkspaceScopedRequests,
1373
+ setWorkspaceBootstrapPending,
1374
+ checkAuthSession,
1375
+ lastConfigRefreshKeyRef,
1376
+ lastAssigneeOptionsLoadKeyRef,
1377
+ lastWorkspaceSyncStateLoadKeyRef,
1378
+ lastTaskSurfaceLoadKeyRef,
1379
+ lastSettingsSurfaceLoadKeyRef,
1380
+ isOpen,
1381
+ activeTab,
1382
+ showArchive,
1383
+ taskScope,
1384
+ settingsSection,
1385
+ fetchTasks,
1386
+ fetchArchive,
1387
+ fetchPlanningEntities,
1388
+ refreshTaskCollections,
1389
+ loadWorkspaceSyncState,
1390
+ archiveBootstrapTimerRef,
1391
+ hasBootstrappedDataRef
1392
+ });
1617
1393
  const refreshSetupContext = useCallback(async () => {
1618
1394
  await checkAuthSession();
1619
1395
  await fetchConfig();
1620
1396
  }, [checkAuthSession, fetchConfig]);
1621
- const fetchWorkspaces = useCallback(async () => {
1622
- if (!workspaceSwitchingEnabled) {
1623
- setAvailableWorkspaces([]);
1624
- workspaceListHydratedRef.current = false;
1625
- return { success: true, workspaces: [] };
1626
- }
1627
- const requestState = getWorkspaceRequestState();
1628
- const abortController = new AbortController();
1629
- workspaceListAbortRef.current = abortController;
1630
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
1631
- try {
1632
- const res = await fetchWithTimeout('/api/taskforce/workspaces', {
1633
- method: 'GET',
1634
- credentials: 'include'
1635
- }, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
1636
- signal: abortController.signal
1637
- });
1638
- if (isWorkspaceEpochStale(requestState)) {
1639
- return { success: false, error: 'Workspace changed while loading workspaces.' };
1640
- }
1641
- if (res.status === 401) {
1642
- setAvailableWorkspaces([]);
1643
- workspaceListHydratedRef.current = false;
1644
- return { success: false, error: 'Authentication required.' };
1645
- }
1646
- const data = await res.json().catch(() => ({}));
1647
- if (isWorkspaceEpochStale(requestState)) {
1648
- return { success: false, error: 'Workspace changed while loading workspaces.' };
1649
- }
1650
- if (!res.ok || data?.success === false) {
1651
- setAvailableWorkspaces([]);
1652
- workspaceListHydratedRef.current = false;
1653
- return { success: false, error: data?.error || `Failed to load workspaces (${res.status})` };
1654
- }
1655
- const workspaces = Array.isArray(data?.workspaces) ? data.workspaces : [];
1656
- setAvailableWorkspaces(workspaces);
1657
- workspaceListHydratedRef.current = true;
1658
- if (runtimeMode !== 'local' && typeof data?.currentWorkspaceId === 'string' && data.currentWorkspaceId.trim().length > 0) {
1659
- applyResolvedWorkspaceId(data.currentWorkspaceId.trim(), { preserveNonDefaultDefault: true });
1660
- }
1661
- logBootstrapDebug('workspace_list_loaded', {
1662
- workspaceCount: workspaces.length,
1663
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
1664
- });
1665
- return { success: true, workspaces };
1666
- }
1667
- catch (error) {
1668
- if (isAbortError(error)) {
1669
- return { success: false, error: 'Workspace list request aborted.' };
1670
- }
1671
- if (isBootstrapTimeoutError(error)) {
1672
- markBootstrapStalled('Startup checks timed out.');
1673
- }
1674
- setAvailableWorkspaces([]);
1675
- workspaceListHydratedRef.current = false;
1676
- return { success: false, error: 'Failed to load workspaces.' };
1677
- }
1678
- finally {
1679
- if (workspaceListAbortRef.current === abortController) {
1680
- workspaceListAbortRef.current = null;
1681
- }
1682
- }
1683
- }, [
1684
- applyResolvedWorkspaceId,
1685
- fetchWithTimeout,
1686
- getWorkspaceRequestState,
1687
- isAbortError,
1688
- isWorkspaceEpochStale,
1689
- isBootstrapTimeoutError,
1690
- isWorkspaceRequestStale,
1691
- markBootstrapStalled,
1692
- runtimeMode,
1693
- workspaceSwitchingEnabled
1694
- ]);
1695
- const fetchAssigneeOptions = useCallback(async () => {
1696
- const requestState = getWorkspaceRequestState();
1697
- const abortController = new AbortController();
1698
- assigneeOptionsAbortRef.current = abortController;
1699
- setAssigneeOptionsLoaded(false);
1700
- try {
1701
- const [assigneeResult, workspaceMembersResult] = await Promise.all([
1702
- fetchWithTimeout('/api/taskforce/workspace/assignee-options', {
1703
- method: 'GET',
1704
- credentials: 'include'
1705
- }, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
1706
- signal: abortController.signal
1707
- }),
1708
- (authSessionResolved && isAuthenticated)
1709
- ? fetchWithTimeout('/api/taskforce/auth/workspace-members', {
1710
- method: 'GET',
1711
- credentials: 'include'
1712
- }, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
1713
- signal: abortController.signal
1714
- }).catch(() => null)
1715
- : Promise.resolve(null)
1716
- ]);
1717
- if (isWorkspaceRequestStale(requestState)) {
1718
- return { success: false, error: 'Workspace changed while loading assignee options.' };
1719
- }
1720
- if (assigneeResult.status === 401) {
1721
- setAssigneeOptions(createDefaultAssigneeOptions());
1722
- setAssigneeOptionsLoaded(true);
1723
- return { success: false, error: 'Authentication required.' };
1724
- }
1725
- const data = await assigneeResult.json().catch(() => ({}));
1726
- if (isWorkspaceRequestStale(requestState)) {
1727
- return { success: false, error: 'Workspace changed while loading assignee options.' };
1728
- }
1729
- if (!assigneeResult.ok) {
1730
- setAssigneeOptions(createDefaultAssigneeOptions());
1731
- setAssigneeOptionsLoaded(true);
1732
- return { success: false, error: data?.error || `Failed to load assignee options (${assigneeResult.status})` };
1733
- }
1734
- const assignees = Array.isArray(data?.assignees) ? data.assignees : [];
1735
- const directOptions = assignees
1736
- .map((entry) => {
1737
- const value = String(entry?.value || '').trim();
1738
- const kindRaw = String(entry?.kind || '').trim().toLowerCase();
1739
- if (!value)
1740
- return null;
1741
- if (kindRaw === 'agent' || kindRaw === 'ai') {
1742
- return {
1743
- value,
1744
- label: String(entry?.label || value).trim() || value,
1745
- icon: String(entry?.icon || 'Bot'),
1746
- color: String(entry?.color || '#8b5cf6'),
1747
- kind: 'agent'
1748
- };
1749
- }
1750
- if (kindRaw === 'member' || kindRaw === 'human') {
1751
- return {
1752
- value,
1753
- label: String(entry?.label || value).trim() || value,
1754
- icon: String(entry?.icon || 'User'),
1755
- color: String(entry?.color || '#22c55e'),
1756
- kind: 'member'
1757
- };
1758
- }
1759
- return null;
1760
- })
1761
- .filter((candidate) => Boolean(candidate));
1762
- const fallbackCandidates = assignees
1763
- .map((candidate) => ({
1764
- userId: String(candidate?.userId || '').trim(),
1765
- email: String(candidate?.email || '').trim().toLowerCase(),
1766
- displayName: typeof candidate?.displayName === 'string' ? candidate.displayName : null,
1767
- status: typeof candidate?.status === 'string' ? candidate.status : null,
1768
- disabled: candidate?.disabled === true,
1769
- }))
1770
- .filter((candidate) => candidate.userId
1771
- && String(candidate.status || 'active').trim().toLowerCase() === 'active'
1772
- && candidate.disabled !== true);
1773
- let memberCandidates = fallbackCandidates;
1774
- if (workspaceMembersResult?.ok) {
1775
- const workspaceMembersData = await workspaceMembersResult.json().catch(() => ({}));
1776
- const users = Array.isArray(workspaceMembersData?.users) ? workspaceMembersData.users : [];
1777
- const normalizedAuthUserId = String(authUserId || '').trim();
1778
- const currentMember = users.find((candidate) => String(candidate?.userId || '').trim() === normalizedAuthUserId);
1779
- const currentMemberRole = String(currentMember?.role || '').trim().toLowerCase();
1780
- if (currentMemberRole === 'owner' || currentMemberRole === 'admin' || currentMemberRole === 'member' || currentMemberRole === 'read-only') {
1781
- setHydratedWorkspaceRole(currentMemberRole);
1782
- }
1783
- else if (!workspaceSwitchingEnabled && users.length === 0) {
1784
- setHydratedWorkspaceRole(null);
1785
- }
1786
- const workspaceMemberCandidates = users
1787
- .map((candidate) => ({
1788
- userId: String(candidate?.userId || '').trim(),
1789
- email: String(candidate?.email || '').trim().toLowerCase(),
1790
- displayName: typeof candidate?.displayName === 'string' ? candidate.displayName : null,
1791
- status: typeof candidate?.status === 'string' ? candidate.status : null,
1792
- disabled: candidate?.disabled === true,
1793
- }))
1794
- .filter((candidate) => candidate.userId
1795
- && String(candidate.status || 'active').trim().toLowerCase() === 'active'
1796
- && candidate.disabled !== true)
1797
- .map((candidate) => ({
1798
- userId: candidate.userId,
1799
- email: candidate.email,
1800
- displayName: candidate.displayName ?? null
1801
- }));
1802
- memberCandidates = Array.from(new Map([...fallbackCandidates, ...workspaceMemberCandidates].map((candidate) => [
1803
- String(candidate.userId || '').trim(),
1804
- candidate
1805
- ])).values()).filter((candidate) => String(candidate.userId || '').trim().length > 0);
1806
- }
1807
- else if (!workspaceSwitchingEnabled && !fallbackCandidates.length) {
1808
- setHydratedWorkspaceRole(null);
1809
- }
1810
- const mergedOptions = mergeAssigneeOptions([
1811
- ...directOptions,
1812
- ...createWorkspaceAssigneeOptions(memberCandidates).filter((option) => option.kind === 'member')
1813
- ]);
1814
- if (isWorkspaceRequestStale(requestState)) {
1815
- return { success: false, error: 'Workspace changed while loading assignee options.' };
1816
- }
1817
- setAssigneeOptions(mergedOptions);
1818
- setAssigneeOptionsLoaded(true);
1819
- return { success: true };
1820
- }
1821
- catch (error) {
1822
- if (isAbortError(error)) {
1823
- return { success: false, error: 'Assignee options request aborted.' };
1824
- }
1825
- if (!isWorkspaceRequestStale(requestState)) {
1826
- setAssigneeOptions(createDefaultAssigneeOptions());
1827
- setAssigneeOptionsLoaded(true);
1828
- }
1829
- return { success: false, error: 'Failed to load assignee options.' };
1830
- }
1831
- finally {
1832
- if (assigneeOptionsAbortRef.current === abortController) {
1833
- assigneeOptionsAbortRef.current = null;
1834
- }
1835
- }
1836
- }, [authSessionResolved, authUserId, fetchWithTimeout, getWorkspaceRequestState, isAbortError, isAuthenticated, isWorkspaceRequestStale, workspaceSwitchingEnabled]);
1837
1397
  useEffect(() => {
1838
1398
  if (!configLoaded || workspaceBootstrapPending)
1839
1399
  return;
@@ -1855,162 +1415,44 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1855
1415
  setHydratedWorkspaceRole(null);
1856
1416
  }
1857
1417
  }, [isAuthenticated]);
1858
- const switchWorkspace = useCallback(async (workspaceId, options) => {
1859
- if (!workspaceSwitchingEnabled) {
1860
- return { success: false, error: 'Workspace switching is unavailable in local mode.', code: 'WORKSPACE_SWITCHING_DISABLED' };
1861
- }
1862
- const normalizedWorkspaceId = String(workspaceId || '').trim();
1863
- if (!normalizedWorkspaceId)
1864
- return { success: false, error: 'workspaceId is required.' };
1865
- try {
1866
- const res = await fetch('/api/taskforce/session/workspace', {
1867
- method: 'POST',
1868
- headers: { 'Content-Type': 'application/json' },
1869
- credentials: 'include',
1870
- body: JSON.stringify({ workspaceId: normalizedWorkspaceId })
1871
- });
1872
- const data = await res.json().catch(() => ({}));
1873
- if (!res.ok || data?.success === false) {
1874
- return { success: false, error: data?.error || `Failed to switch workspace (${res.status})`, code: data?.code };
1875
- }
1876
- explicitWorkspaceSelectionRef.current = normalizedWorkspaceId;
1877
- commitCurrentWorkspaceId(normalizedWorkspaceId, { clearExplicitSelection: false });
1878
- abortWorkspaceScopedRequests();
1879
- setWorkspaceBootstrapPending(true);
1880
- markBootstrapPhase('workspace');
1881
- const authenticated = await checkAuthSession({ force: true });
1882
- if (options?.hydrate === false) {
1883
- await fetchWorkspaces();
1884
- }
1885
- else {
1886
- lastConfigRefreshKeyRef.current = `${runtimeMode}:${normalizedWorkspaceId}:${workspaceSelectionScope}`;
1887
- lastAssigneeOptionsLoadKeyRef.current = `${runtimeMode}:${authenticated ? 'auth' : 'guest'}:${normalizedWorkspaceId}`;
1888
- lastWorkspaceSyncStateLoadKeyRef.current = normalizedWorkspaceId;
1889
- if (isOpen && activeTab === 'tasks') {
1890
- lastTaskSurfaceLoadKeyRef.current = `${normalizedWorkspaceId}:${activeTab}:${showArchive ? 'archive' : 'active'}:${taskScope}`;
1891
- }
1892
- if (isOpen && activeTab === 'settings') {
1893
- lastSettingsSurfaceLoadKeyRef.current = `${normalizedWorkspaceId}:${activeTab}:${settingsSection}`;
1894
- }
1895
- await Promise.all([
1896
- fetchConfig(),
1897
- fetchWorkspaces(),
1898
- refreshTaskCollections({ isSilent: false }),
1899
- fetchAssigneeOptions(),
1900
- loadWorkspaceSyncState()
1901
- ]);
1902
- }
1903
- if (explicitWorkspaceSelectionRef.current === normalizedWorkspaceId) {
1904
- explicitWorkspaceSelectionRef.current = null;
1905
- }
1906
- return { success: true };
1907
- }
1908
- catch {
1909
- if (explicitWorkspaceSelectionRef.current === normalizedWorkspaceId) {
1910
- explicitWorkspaceSelectionRef.current = null;
1911
- }
1912
- return { success: false, error: 'Failed to switch workspace.' };
1913
- }
1914
- finally {
1915
- setWorkspaceBootstrapPending(false);
1418
+ const continueAfterCommercialOnboarding = useCallback(async () => {
1419
+ setBootstrapError(null);
1420
+ markBootstrapPhase('auth');
1421
+ const authenticated = await checkAuthSession({ force: true });
1422
+ if (!authenticated) {
1423
+ await fetchConfig({ ignoreAuthGuard: true });
1424
+ return { success: false, destination: 'login', error: 'Sign in required.', code: 'AUTH_REQUIRED' };
1916
1425
  }
1917
- }, [abortWorkspaceScopedRequests, activeTab, checkAuthSession, commitCurrentWorkspaceId, fetchAssigneeOptions, fetchConfig, fetchWorkspaces, isOpen, loadWorkspaceSyncState, markBootstrapPhase, refreshTaskCollections, runtimeMode, settingsSection, showArchive, taskScope, workspaceSelectionScope, workspaceSwitchingEnabled]);
1918
- const resolveWorkspaceAfterAuth = useCallback(async (options) => {
1919
- markBootstrapPhase('workspace');
1920
- setWorkspaceBootstrapPending(true);
1921
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
1922
- try {
1923
- if (!workspaceSwitchingEnabled) {
1924
- const localWorkspaceId = String(currentWorkspaceId || '').trim() || 'default';
1925
- commitCurrentWorkspaceId(localWorkspaceId);
1926
- logBootstrapDebug('workspace_resolved', {
1927
- workspaceId: localWorkspaceId,
1928
- workspaceSetupRequired: false,
1929
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
1930
- });
1931
- return { success: true, workspaceSetupRequired: false, workspaceId: localWorkspaceId };
1932
- }
1933
- const preferredWorkspaceId = String(options?.preferredWorkspaceId || '').trim();
1934
- const persistedWorkspaceId = readPersistedWorkspaceId(workspaceSelectionScope);
1935
- const workspacesResult = await fetchWorkspaces();
1936
- if (!workspacesResult.success) {
1426
+ if (workspaceSwitchingEnabled) {
1427
+ const workspaceResolution = await resolveWorkspaceAfterAuth();
1428
+ if (!workspaceResolution.success) {
1429
+ await fetchConfig({ ignoreAuthGuard: true });
1937
1430
  return {
1938
1431
  success: false,
1939
- workspaceSetupRequired: false,
1940
- error: workspacesResult.error || 'Failed to load workspaces after authentication.'
1432
+ destination: 'app',
1433
+ error: workspaceResolution.error || 'Unable to resolve workspace after plan selection.',
1434
+ code: workspaceResolution.code
1941
1435
  };
1942
1436
  }
1943
- const workspaces = Array.isArray(workspacesResult.workspaces) ? workspacesResult.workspaces : [];
1944
- if (workspaces.length === 0) {
1945
- commitCurrentWorkspaceId('default');
1946
- logBootstrapDebug('workspace_resolved', {
1947
- workspaceId: 'default',
1948
- workspaceSetupRequired: true,
1949
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
1950
- });
1951
- return { success: true, workspaceSetupRequired: true };
1952
- }
1953
- const ids = new Set(workspaces.map((workspace) => String(workspace.id || '').trim()).filter(Boolean));
1954
- const selectedWorkspaceId = ((preferredWorkspaceId && ids.has(preferredWorkspaceId) ? preferredWorkspaceId : '')
1955
- || (persistedWorkspaceId && ids.has(persistedWorkspaceId) ? persistedWorkspaceId : '')
1956
- || String(workspaces[0]?.id || '').trim());
1957
- if (!selectedWorkspaceId) {
1958
- commitCurrentWorkspaceId('default');
1959
- return { success: true, workspaceSetupRequired: true };
1960
- }
1961
- const current = String(currentWorkspaceIdRef.current || '').trim();
1962
- if (current !== selectedWorkspaceId) {
1963
- const switched = await switchWorkspace(selectedWorkspaceId, { hydrate: false });
1964
- if (!switched.success) {
1965
- return {
1966
- success: false,
1967
- workspaceSetupRequired: false,
1968
- error: switched.error || 'Failed to set workspace after authentication.',
1969
- code: switched.code
1970
- };
1971
- }
1972
- }
1973
- else {
1974
- commitCurrentWorkspaceId(selectedWorkspaceId);
1975
- }
1976
- logBootstrapDebug('workspace_resolved', {
1977
- workspaceId: selectedWorkspaceId,
1978
- workspaceSetupRequired: false,
1979
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
1980
- });
1981
- return { success: true, workspaceSetupRequired: false, workspaceId: selectedWorkspaceId };
1982
- }
1983
- catch {
1984
- markBootstrapStalled('Unable to resolve workspace access.');
1985
- return { success: false, workspaceSetupRequired: false, error: 'Failed to resolve workspace access.' };
1986
- }
1987
- finally {
1988
- setWorkspaceBootstrapPending(false);
1989
- }
1990
- }, [
1991
- commitCurrentWorkspaceId,
1992
- fetchWorkspaces,
1993
- markBootstrapPhase,
1994
- markBootstrapStalled,
1995
- workspaceSelectionScope,
1996
- switchWorkspace,
1997
- workspaceSwitchingEnabled
1998
- ]);
1999
- const retryBootstrapChecks = useCallback(async () => {
2000
- setBootstrapError(null);
2001
- markBootstrapPhase('auth');
2002
- const authenticated = await checkAuthSession();
2003
- if (workspaceSwitchingEnabled && authenticated) {
2004
- const workspaceResolution = await resolveWorkspaceAfterAuth();
2005
- if (!workspaceResolution.success || workspaceResolution.workspaceSetupRequired) {
1437
+ if (workspaceResolution.workspaceSetupRequired) {
2006
1438
  await fetchConfig({ ignoreAuthGuard: true });
2007
- return;
1439
+ return { success: true, destination: 'setup' };
2008
1440
  }
2009
1441
  }
2010
- await fetchConfig({ ignoreAuthGuard: true });
1442
+ await fetchBootstrapConfig();
1443
+ await Promise.all([
1444
+ fetchTasks(true, { ignoreAuthGuard: true }),
1445
+ fetchReferenceData(),
1446
+ fetchPlanningEntities()
1447
+ ]);
1448
+ return { success: true, destination: 'app' };
2011
1449
  }, [
2012
1450
  checkAuthSession,
1451
+ fetchBootstrapConfig,
2013
1452
  fetchConfig,
1453
+ fetchPlanningEntities,
1454
+ fetchReferenceData,
1455
+ fetchTasks,
2014
1456
  markBootstrapPhase,
2015
1457
  resolveWorkspaceAfterAuth,
2016
1458
  workspaceSwitchingEnabled
@@ -2066,7 +1508,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2066
1508
  if (!res.ok || data?.success === false) {
2067
1509
  return { success: false, error: data?.error || `Failed to delete workspace (${res.status})`, code: data?.code };
2068
1510
  }
2069
- await checkAuthSession();
1511
+ await checkAuthSession({ force: true });
2070
1512
  await Promise.all([fetchConfig(), fetchWorkspaces()]);
2071
1513
  const nextWorkspaceId = typeof data?.nextWorkspaceId === 'string' ? data.nextWorkspaceId.trim() : '';
2072
1514
  if (nextWorkspaceId) {
@@ -2104,6 +1546,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2104
1546
  }, [refreshSetupContext]);
2105
1547
  const saveWorkspaceProfile = useCallback(async (input) => {
2106
1548
  const name = String(input?.name || '').trim();
1549
+ const requestedWorkspaceId = typeof input?.workspaceId === 'string'
1550
+ ? input.workspaceId.trim()
1551
+ : '';
2107
1552
  if (!name)
2108
1553
  return { success: false, error: 'Workspace name is required.', code: 'WORKSPACE_NAME_REQUIRED' };
2109
1554
  try {
@@ -2115,7 +1560,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2115
1560
  },
2116
1561
  credentials: 'include',
2117
1562
  body: JSON.stringify({
2118
- workspaceId: input.workspaceId,
1563
+ workspaceId: requestedWorkspaceId && requestedWorkspaceId !== 'default'
1564
+ ? requestedWorkspaceId
1565
+ : undefined,
2119
1566
  name,
2120
1567
  description: typeof input.description === 'string' ? input.description : undefined,
2121
1568
  allowCreate: true
@@ -2168,7 +1615,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2168
1615
  return { success: false, error: data?.error || 'Sign in failed.', code: data?.code };
2169
1616
  }
2170
1617
  setError('');
2171
- await checkAuthSession();
1618
+ await checkAuthSession({ force: true });
2172
1619
  const workspaceResolution = await resolveWorkspaceAfterAuth();
2173
1620
  if (!workspaceResolution.success) {
2174
1621
  return {
@@ -2178,6 +1625,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2178
1625
  };
2179
1626
  }
2180
1627
  if (workspaceResolution.workspaceSetupRequired) {
1628
+ await fetchConfig({ ignoreAuthGuard: true });
2181
1629
  return { success: true, workspaceSetupRequired: true };
2182
1630
  }
2183
1631
  await Promise.all([
@@ -2224,12 +1672,64 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2224
1672
  });
2225
1673
  const data = await res.json().catch(() => ({}));
2226
1674
  if (!res.ok || !data?.success) {
2227
- return { success: false, error: data?.error || 'Create account failed.', code: data?.code };
1675
+ return {
1676
+ success: false,
1677
+ error: data?.error || 'Create account failed.',
1678
+ code: data?.code,
1679
+ verificationRequired: Boolean(data?.verificationRequired),
1680
+ verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
1681
+ emailSent: data?.emailSent !== false,
1682
+ emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
1683
+ };
1684
+ }
1685
+ const planSelectionRequired = data?.planSelectionRequired === true;
1686
+ const checkoutPending = data?.checkoutPending === true;
1687
+ const commercialState = typeof data?.commercialState === 'string' ? data.commercialState : null;
1688
+ const verificationRequired = Boolean(data?.verificationRequired);
1689
+ if (!verificationRequired) {
1690
+ setError('');
1691
+ await checkAuthSession({ force: true });
1692
+ const workspaceResolution = await resolveWorkspaceAfterAuth();
1693
+ if (!workspaceResolution.success) {
1694
+ return {
1695
+ success: false,
1696
+ error: workspaceResolution.error || 'Unable to resolve workspace after registration.',
1697
+ code: workspaceResolution.code
1698
+ };
1699
+ }
1700
+ const hasCommercialOnboardingGate = Boolean(planSelectionRequired
1701
+ || checkoutPending
1702
+ || commercialState === 'pending_plan_selection'
1703
+ || commercialState === 'checkout_pending');
1704
+ if (workspaceResolution.workspaceSetupRequired || hasCommercialOnboardingGate) {
1705
+ await fetchConfig({ ignoreAuthGuard: true });
1706
+ }
1707
+ else {
1708
+ await Promise.all([
1709
+ fetchConfig({ ignoreAuthGuard: true }),
1710
+ fetchTasks(true, { ignoreAuthGuard: true })
1711
+ ]);
1712
+ void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
1713
+ }
1714
+ return {
1715
+ success: true,
1716
+ workspaceSetupRequired: workspaceResolution.workspaceSetupRequired,
1717
+ planSelectionRequired,
1718
+ checkoutPending,
1719
+ commercialState,
1720
+ verificationRequired,
1721
+ verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
1722
+ emailSent: data?.emailSent !== false,
1723
+ emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
1724
+ };
2228
1725
  }
2229
1726
  return {
2230
1727
  success: true,
2231
1728
  workspaceSetupRequired: Boolean(data?.workspaceSetupRequired),
2232
- verificationRequired: Boolean(data?.verificationRequired),
1729
+ planSelectionRequired,
1730
+ checkoutPending,
1731
+ commercialState,
1732
+ verificationRequired,
2233
1733
  verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
2234
1734
  emailSent: data?.emailSent !== false,
2235
1735
  emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
@@ -2238,7 +1738,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2238
1738
  catch {
2239
1739
  return { success: false, error: 'Create account failed.' };
2240
1740
  }
2241
- }, [resolveCloudAuthUrl, cloudAuthConfigured]);
1741
+ }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
2242
1742
  const updateCurrentUserProfile = useCallback(async (input) => {
2243
1743
  if (!cloudAuthConfigured) {
2244
1744
  return { success: false, error: 'Cloud auth endpoint is not configured.' };
@@ -2443,7 +1943,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2443
1943
  return { success: false, error: data?.error || 'Invite acceptance failed.', code: data?.code };
2444
1944
  }
2445
1945
  setError('');
2446
- await checkAuthSession();
1946
+ await checkAuthSession({ force: true });
2447
1947
  const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
2448
1948
  const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
2449
1949
  if (!workspaceResolution.success) {
@@ -2511,6 +2011,34 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2511
2011
  return { success: false, error: 'Invite join failed.' };
2512
2012
  }
2513
2013
  }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
2014
+ const resetCloudLogoutClientState = useCallback(() => {
2015
+ abortWorkspaceScopedRequests();
2016
+ referenceDataRequestRef.current = null;
2017
+ lastLoadedUiStateKeyRef.current = '';
2018
+ lastAssigneeOptionsLoadKeyRef.current = '';
2019
+ lastConfigRefreshKeyRef.current = '';
2020
+ lastWorkspaceSyncStateLoadKeyRef.current = '';
2021
+ lastTaskSurfaceLoadKeyRef.current = '';
2022
+ lastSettingsSurfaceLoadKeyRef.current = '';
2023
+ setUiStateReady(false);
2024
+ setReferenceDataLoaded(false);
2025
+ setAssigneeOptions(createDefaultAssigneeOptions());
2026
+ setAssigneeOptionsLoaded(false);
2027
+ setDeletedTasks([]);
2028
+ setTasks([]);
2029
+ setArchivedTasks([]);
2030
+ setInitiatives([]);
2031
+ setWorkstreams([]);
2032
+ setLoadingTasks(false);
2033
+ if (cloudRealtimeFollowUpRefreshTimerRef.current !== null && typeof window !== 'undefined') {
2034
+ window.clearTimeout(cloudRealtimeFollowUpRefreshTimerRef.current);
2035
+ cloudRealtimeFollowUpRefreshTimerRef.current = null;
2036
+ }
2037
+ if (archiveBootstrapTimerRef.current !== null && typeof window !== 'undefined') {
2038
+ window.clearTimeout(archiveBootstrapTimerRef.current);
2039
+ archiveBootstrapTimerRef.current = null;
2040
+ }
2041
+ }, [abortWorkspaceScopedRequests, setArchivedTasks, setTasks]);
2514
2042
  const logout = useCallback(async () => {
2515
2043
  try {
2516
2044
  if (cloudAuthConfigured) {
@@ -2524,6 +2052,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2524
2052
  // no-op
2525
2053
  }
2526
2054
  finally {
2055
+ abortWorkspaceScopedRequests();
2056
+ authSessionEpochRef.current += 1;
2527
2057
  authSessionRequestRef.current = null;
2528
2058
  authSessionLastResultRef.current = false;
2529
2059
  authSessionLastCheckedAtRef.current = Date.now();
@@ -2538,19 +2068,18 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2538
2068
  setUserGlobalSyncStatus('disconnected');
2539
2069
  setUserGlobalSyncError(null);
2540
2070
  setHasBetaAccess(true);
2071
+ setProjectName('');
2072
+ setConfigLoaded(false);
2073
+ setReferenceDataLoaded(false);
2074
+ setSetupState(null);
2075
+ setBootstrapPhase('idle');
2076
+ setBootstrapError(null);
2077
+ hasLoadedUiStateRef.current = false;
2078
+ if (runtimeMode !== 'local') {
2079
+ clearPersistedWorkspaceId(workspaceSelectionScope);
2080
+ }
2541
2081
  setAuthBlocked(runtimeMode === 'cloud' && authRequiredForApi);
2542
- setCurrentWorkspaceId((prev) => {
2543
- const existing = String(prev || '').trim();
2544
- let resolvedWorkspaceId = 'default';
2545
- if (existing && existing.toLowerCase() !== 'default') {
2546
- resolvedWorkspaceId = existing;
2547
- }
2548
- else if (runtimeMode !== 'local') {
2549
- resolvedWorkspaceId = readPersistedWorkspaceId(workspaceSelectionScope) || 'default';
2550
- }
2551
- currentWorkspaceIdRef.current = resolvedWorkspaceId;
2552
- return resolvedWorkspaceId;
2553
- });
2082
+ commitCurrentWorkspaceId('default');
2554
2083
  setAvailableWorkspaces([]);
2555
2084
  workspaceListHydratedRef.current = false;
2556
2085
  setHydratedWorkspaceRole(null);
@@ -2563,12 +2092,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2563
2092
  ]);
2564
2093
  return;
2565
2094
  }
2566
- setTasks([]);
2567
- setInitiatives([]);
2568
- setWorkstreams([]);
2095
+ hasBootstrappedDataRef.current = false;
2096
+ resetCloudLogoutClientState();
2569
2097
  }
2570
2098
  }, [
2099
+ abortWorkspaceScopedRequests,
2571
2100
  authRequiredForApi,
2101
+ clearPersistedWorkspaceId,
2102
+ commitCurrentWorkspaceId,
2572
2103
  resolveCloudAuthUrl,
2573
2104
  cloudAuthConfigured,
2574
2105
  workspaceSelectionScope,
@@ -2576,7 +2107,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2576
2107
  fetchConfig,
2577
2108
  fetchPlanningEntities,
2578
2109
  fetchReferenceData,
2579
- fetchTasks
2110
+ fetchTasks,
2111
+ resetCloudLogoutClientState
2580
2112
  ]);
2581
2113
  const createInitiativeFromTemplate = useCallback(async (payload) => {
2582
2114
  try {
@@ -2694,7 +2226,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2694
2226
  }, [fetchPlanningEntities]);
2695
2227
  // Initialize auth once, then bootstrap protected data once auth gates are settled.
2696
2228
  const hasInitializedAuthRef = useRef(false);
2697
- const hasBootstrappedDataRef = useRef(false);
2698
2229
  useEffect(() => {
2699
2230
  setIsOpen(true); // Always open in standalone-first
2700
2231
  if (hasInitializedAuthRef.current)
@@ -2953,73 +2484,19 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2953
2484
  userId: normalizedRealtimeUserId || undefined
2954
2485
  });
2955
2486
  // Fallback sync for cross-process writes (e.g., MCP tools) that may miss watcher/websocket events.
2956
- useEffect(() => {
2957
- if (typeof window === 'undefined' || typeof document === 'undefined')
2958
- return;
2959
- if (shouldSkipDataVersionPolling({
2960
- isOpen,
2961
- authBlocked,
2962
- authRequiredForApi,
2963
- isAuthenticated,
2964
- canCallProtectedApi: authGuardPolicy.canCallProtectedApi
2965
- }))
2966
- return;
2967
- let cancelled = false;
2968
- const checkDataVersion = async () => {
2969
- if (cancelled || document.visibilityState !== 'visible')
2970
- return;
2971
- if (shouldSkipDataVersionTick({
2972
- shouldGateProtectedApiCalls,
2973
- authSessionResolved: authSessionResolvedRef.current,
2974
- authRequiredForApi: authRequiredForApiRef.current,
2975
- isAuthenticated: isAuthenticatedRef.current
2976
- }))
2977
- return;
2978
- try {
2979
- const res = await fetch('/api/taskforce/data-version');
2980
- if (!res.ok)
2981
- return;
2982
- const data = await res.json();
2983
- const version = Number(data?.dataVersion);
2984
- if (!Number.isFinite(version))
2985
- return;
2986
- if (dataVersionRef.current === null) {
2987
- dataVersionRef.current = version;
2988
- return;
2989
- }
2990
- if (version !== dataVersionRef.current) {
2991
- dataVersionRef.current = version;
2992
- await refreshTaskCollectionsForDataVersion();
2993
- }
2994
- }
2995
- catch {
2996
- // Keep polling best-effort and silent.
2997
- }
2998
- };
2999
- const onVisibilityChange = () => {
3000
- if (document.visibilityState === 'visible') {
3001
- checkDataVersion();
3002
- }
3003
- };
3004
- const intervalId = window.setInterval(checkDataVersion, 3000);
3005
- document.addEventListener('visibilitychange', onVisibilityChange);
3006
- checkDataVersion();
3007
- return () => {
3008
- cancelled = true;
3009
- window.clearInterval(intervalId);
3010
- document.removeEventListener('visibilitychange', onVisibilityChange);
3011
- };
3012
- }, [
2487
+ useDataVersionRefresh({
3013
2488
  isOpen,
3014
- fetchTasks,
3015
- fetchArchive,
3016
2489
  authBlocked,
3017
2490
  authRequiredForApi,
3018
2491
  isAuthenticated,
3019
- authGuardPolicy,
2492
+ canCallProtectedApi: authGuardPolicy.canCallProtectedApi,
3020
2493
  shouldGateProtectedApiCalls,
2494
+ authSessionResolvedRef,
2495
+ authRequiredForApiRef,
2496
+ isAuthenticatedRef,
2497
+ dataVersionRef,
3021
2498
  refreshTaskCollectionsForDataVersion
3022
- ]);
2499
+ });
3023
2500
  const relationshipTasks = useMemo(() => {
3024
2501
  const byId = new Map();
3025
2502
  for (const task of archivedTasks)
@@ -3543,6 +3020,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3543
3020
  runtimeCapabilities,
3544
3021
  refreshSetupContext,
3545
3022
  retryBootstrapChecks,
3023
+ continueAfterCommercialOnboarding,
3546
3024
  saveWorkspaceProfile,
3547
3025
  fetchWorkspaces,
3548
3026
  createWorkspace,