@taskforcehq/taskforce 0.3.303 → 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 (165) hide show
  1. package/dist/Taskforce.module.css +102 -5
  2. package/dist/TaskforceCore.js +514 -212
  3. package/dist/TaskforceCore.test.js +1172 -133
  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/TaskStatusActions.d.ts +6 -1
  21. package/dist/components/task/TaskStatusActions.js +8 -3
  22. package/dist/components/task/TaskStatusActions.test.js +9 -0
  23. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  24. package/dist/components/task/taskKanbanDropRules.js +187 -0
  25. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  26. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  27. package/dist/components/ui/Modal.d.ts +1 -1
  28. package/dist/components/ui/Modal.js +1 -0
  29. package/dist/components/views/AppShellHeader.js +5 -1
  30. package/dist/components/views/AppShellHeader.test.js +4 -0
  31. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  32. package/dist/components/views/PlanComparisonPage.js +4 -2
  33. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  34. package/dist/components/views/PlansPage.d.ts +5 -1
  35. package/dist/components/views/PlansPage.js +39 -12
  36. package/dist/components/views/PlansPage.test.js +59 -2
  37. package/dist/components/views/StandaloneLayout.js +113 -38
  38. package/dist/components/views/WidgetView.js +11 -2
  39. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  40. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  41. package/dist/core/PlanEntitlementService.d.ts +3 -1
  42. package/dist/core/PlanEntitlementService.js +61 -11
  43. package/dist/core/SignupMonetizationSettings.test.js +37 -2
  44. package/dist/core/TaskActivityService.d.ts +4 -1
  45. package/dist/core/TaskActivityService.js +45 -0
  46. package/dist/core/TaskAuditTimeline.test.js +52 -1
  47. package/dist/core/Taskforce.d.ts +15 -11
  48. package/dist/core/Taskforce.js +43 -2
  49. package/dist/core/WorkspacePlanMode.test.js +65 -0
  50. package/dist/documentReviews/service.d.ts +1 -1
  51. package/dist/documentReviews/types.d.ts +1 -1
  52. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  53. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  54. package/dist/hooks/sync/auth.d.ts +37 -0
  55. package/dist/hooks/sync/auth.js +61 -0
  56. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  57. package/dist/hooks/sync/bootstrap.js +52 -0
  58. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  59. package/dist/hooks/sync/orchestratorShared.js +209 -0
  60. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  61. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  62. package/dist/hooks/sync/realtime.d.ts +25 -0
  63. package/dist/hooks/sync/realtime.js +60 -0
  64. package/dist/hooks/sync/recovery.d.ts +69 -0
  65. package/dist/hooks/sync/recovery.js +118 -0
  66. package/dist/hooks/sync/transfers.d.ts +122 -0
  67. package/dist/hooks/sync/transfers.js +431 -0
  68. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  69. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  70. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  71. package/dist/hooks/ui/useResourceExport.js +5 -2
  72. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  73. package/dist/hooks/useRealtimeSync.js +2 -1
  74. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  75. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  76. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  77. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  78. package/dist/hooks/useSyncOrchestrator.js +314 -873
  79. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
  80. package/dist/hooks/useTaskMutations.js +45 -6
  81. package/dist/hooks/useTaskMutations.test.js +147 -0
  82. package/dist/hooks/useTaskforce.d.ts +8 -2
  83. package/dist/hooks/useTaskforce.js +285 -812
  84. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  85. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  86. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  87. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  88. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  89. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  90. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  91. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  92. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  93. package/dist/localization/locales/en-US.js +2 -2
  94. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  95. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  96. package/dist/mcp/clientIntegrations.d.ts +12 -1
  97. package/dist/mcp/clientIntegrations.js +161 -6
  98. package/dist/mcp/clientIntegrations.test.js +76 -4
  99. package/dist/mcp/documentHelpers.d.ts +100 -0
  100. package/dist/mcp/documentHelpers.js +161 -0
  101. package/dist/mcp/runtime.js +174 -476
  102. package/dist/mcp/runtime.test.js +81 -8
  103. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  104. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  105. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  106. package/dist/mcp/taskAttachmentTypes.js +1 -0
  107. package/dist/plugins/vite.js +1 -1
  108. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  109. package/dist/runtime/appGateDefinitions.js +1 -1
  110. package/dist/server/index.d.ts +1 -0
  111. package/dist/server/index.js +46 -3
  112. package/dist/server/index.test.js +12 -1
  113. package/dist/server/routes/admin.js +13 -19
  114. package/dist/server/routes/auth.js +37 -2
  115. package/dist/server/routes/billing.js +18 -67
  116. package/dist/server/routes/billing.test.js +43 -0
  117. package/dist/server/routes/shared.d.ts +30 -0
  118. package/dist/server/routes/shared.js +26 -0
  119. package/dist/server/routes/sync.integration.test.js +20 -0
  120. package/dist/server/routes/sync.js +110 -237
  121. package/dist/server/routes/tasks.js +49 -7
  122. package/dist/server/routes.js +8 -19
  123. package/dist/server/routes.test.js +243 -5
  124. package/dist/shared/taskActor.d.ts +10 -0
  125. package/dist/shared/taskActor.js +1 -0
  126. package/dist/sync/collaborationSyncPayload.js +4 -0
  127. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  128. package/dist/sync/collaborationSyncState.d.ts +3 -1
  129. package/dist/sync/collaborationSyncState.js +19 -0
  130. package/dist/sync/contentSyncState.js +12 -10
  131. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  132. package/dist/sync/syncApplyHandlers.js +370 -8
  133. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  134. package/dist/sync/syncApplyHandlers.test.js +377 -0
  135. package/dist/sync/syncService.d.ts +8 -1
  136. package/dist/sync/syncService.js +36 -2
  137. package/dist/sync/syncService.test.js +48 -0
  138. package/dist/sync/workspacePullFeed.d.ts +1 -0
  139. package/dist/sync/workspacePullFeed.js +66 -59
  140. package/dist/sync/workspacePullFeed.test.js +72 -4
  141. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  142. package/dist/sync/workspaceSyncModel.js +65 -0
  143. package/dist/sync/workspaceSyncModel.test.js +119 -0
  144. package/dist/types.d.ts +2 -10
  145. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  146. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  147. package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  148. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-BjR6iBf9.js} +1 -1
  149. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  150. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  151. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  152. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-DnFqdUME.js} +1 -1
  153. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  154. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  155. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
  156. package/dist/ui/index.html +4 -4
  157. package/dist/utils/taskActivity.js +4 -0
  158. package/dist/utils/taskActivity.test.js +25 -1
  159. package/package.json +3 -1
  160. package/scripts/playwright-auth.sh +4 -0
  161. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  162. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  163. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  164. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  165. 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);
@@ -345,6 +347,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
345
347
  const [bootstrapError, setBootstrapError] = useState(null);
346
348
  const [bootstrapStartedAt, setBootstrapStartedAt] = useState(null);
347
349
  const authSessionRequestRef = useRef(null);
350
+ const authSessionEpochRef = useRef(0);
348
351
  const authSessionLastCheckedAtRef = useRef(0);
349
352
  const authSessionLastResultRef = useRef(hasOptimisticLocalAuth);
350
353
  const currentWorkspaceIdRef = useRef(currentWorkspaceId);
@@ -375,25 +378,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
375
378
  useEffect(() => {
376
379
  currentWorkspaceIdRef.current = currentWorkspaceId;
377
380
  }, [currentWorkspaceId]);
378
- const applyResolvedWorkspaceId = useCallback((nextWorkspaceIdRaw, options) => {
381
+ const applyResolvedWorkspaceId = useCallback((nextWorkspaceIdRaw) => {
379
382
  const nextWorkspaceId = String(nextWorkspaceIdRaw || '').trim();
380
383
  if (!nextWorkspaceId)
381
384
  return;
382
- setCurrentWorkspaceId((prev) => {
385
+ setCurrentWorkspaceId(() => {
383
386
  const explicitWorkspaceId = String(explicitWorkspaceSelectionRef.current || '').trim();
384
387
  let resolvedWorkspaceId = nextWorkspaceId;
385
388
  if (explicitWorkspaceId && explicitWorkspaceId !== nextWorkspaceId) {
386
389
  resolvedWorkspaceId = explicitWorkspaceId;
387
390
  }
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
391
  currentWorkspaceIdRef.current = resolvedWorkspaceId;
398
392
  return resolvedWorkspaceId;
399
393
  });
@@ -477,7 +471,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
477
471
  shouldBlockProtectedApiCalls
478
472
  });
479
473
  const { zenMode, setZenModeState, toggleZenMode } = useZenMode();
480
- 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
+ });
481
479
  // Unsaved Changes Modal State
482
480
  const [unsavedModalOpen, setUnsavedModalOpen] = useState(false);
483
481
  const [pendingNavigation, setPendingNavigation] = useState(null);
@@ -813,158 +811,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
813
811
  await fetchDeletedTasks(true);
814
812
  await fetchPlanningEntities();
815
813
  }, [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
- ]);
814
+ const setUserGlobalSyncStatusRef = useRef(() => undefined);
815
+ const setUserGlobalSyncErrorRef = useRef(() => undefined);
816
+ const checkAuthSessionImplRef = useRef(async () => false);
817
+ const checkAuthSession = useCallback((options) => checkAuthSessionImplRef.current(options), []);
968
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({
969
819
  currentWorkspaceId,
970
820
  cloudAuthConfigured,
@@ -1046,6 +896,50 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1046
896
  assigneeOptionsLoaded,
1047
897
  assigneeOptions
1048
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;
1049
943
  const [activeWorkspaceModule, setActiveWorkspaceModule] = useState('tasks');
1050
944
  const [showComments, setShowComments] = useState(false);
1051
945
  const [descriptionFocused, setDescriptionFocused] = useState(false);
@@ -1069,6 +963,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1069
963
  const skipNextUiStatePersistRef = useRef(false);
1070
964
  const referenceDataRequestRef = useRef(null);
1071
965
  const workspaceListHydratedRef = useRef(false);
966
+ const hasBootstrappedDataRef = useRef(false);
1072
967
  const [uiStateReady, setUiStateReady] = useState(false);
1073
968
  const { handleSaveSettings, handleSaveTheme, handleSaveGlobalTheme, handleJsonBackupEnabledChange, handleSaveGlobalJsonBackupEnabled, handleSaveGlobalWeekStartsOn, handleSaveLocale, handleManualComplexityEnabledChange, handleChecklistDropdownEnabledChange, handleShowTaskCardStatusLabelChange, handleResetProjectToGlobal } = useSettingsPersistence({
1074
969
  keyShortcut,
@@ -1236,15 +1131,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1236
1131
  writePersistedAppUiState(uiStateSnapshot, workspaceUiStateScope);
1237
1132
  const timeoutId = window.setTimeout(async () => {
1238
1133
  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
- })
1134
+ await persistRemoteUiState({
1135
+ stateKey: 'app',
1136
+ workspaceId: currentWorkspaceIdRef.current,
1137
+ patch: uiStateSnapshot
1248
1138
  });
1249
1139
  }
1250
1140
  catch {
@@ -1281,13 +1171,15 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1281
1171
  ? data.userId.trim()
1282
1172
  : 'anonymous';
1283
1173
  const serverAuthenticated = nextRuntimeMode === 'cloud' && nextUserId !== 'anonymous';
1174
+ const knownAuthenticated = serverAuthenticated || isAuthenticatedRef.current;
1175
+ const authResolved = authSessionResolvedRef.current;
1284
1176
  setRuntimeMode(nextRuntimeMode);
1285
1177
  setAuthRequiredForApi(nextAuthRequiredForApi);
1286
1178
  if (serverAuthenticated) {
1287
1179
  setIsAuthenticated(true);
1288
1180
  setAuthUserId(nextUserId);
1289
1181
  setAuthBlocked(false);
1290
- if (!authSessionResolved) {
1182
+ if (!authResolved) {
1291
1183
  setAuthSessionResolved(true);
1292
1184
  }
1293
1185
  }
@@ -1297,11 +1189,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1297
1189
  commitCurrentWorkspaceId(nextWorkspaceId);
1298
1190
  }
1299
1191
  else {
1300
- applyResolvedWorkspaceId(nextWorkspaceId, { preserveNonDefaultDefault: true });
1192
+ applyResolvedWorkspaceId(nextWorkspaceId);
1301
1193
  }
1302
1194
  }
1303
1195
  if (nextRuntimeMode === 'cloud' && nextAuthRequiredForApi) {
1304
- setAuthBlocked(!(serverAuthenticated || isAuthenticated));
1196
+ setAuthBlocked(!knownAuthenticated);
1305
1197
  }
1306
1198
  else {
1307
1199
  setAuthBlocked(false);
@@ -1399,7 +1291,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1399
1291
  else {
1400
1292
  setShowTaskCardStatusLabel(true);
1401
1293
  }
1402
- }, [applyResolvedWorkspaceId, authSessionResolved, commitCurrentWorkspaceId, isAuthenticated]);
1294
+ }, [applyResolvedWorkspaceId, commitCurrentWorkspaceId]);
1403
1295
  const fetchBuildInfo = useCallback(async () => {
1404
1296
  const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
1405
1297
  try {
@@ -1425,417 +1317,83 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1425
1317
  }
1426
1318
  }
1427
1319
  }, [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,
1320
+ const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks } = useTaskforceWorkspaceBootstrap({
1520
1321
  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,
1322
+ shouldBlockProtectedApiCalls,
1603
1323
  getWorkspaceRequestState,
1604
- handleUnauthorized,
1324
+ isWorkspaceRequestStale,
1325
+ isWorkspaceEpochStale,
1605
1326
  isAbortError,
1606
1327
  isBootstrapTimeoutError,
1607
- isWorkspaceEpochStale,
1608
- isWorkspaceRequestStale,
1328
+ fetchWithTimeout,
1329
+ handleUnauthorized,
1330
+ applyBootstrapConfig,
1609
1331
  loadPersistedUiState,
1332
+ fetchBuildInfo,
1610
1333
  markBootstrapPhase,
1611
1334
  markBootstrapStalled,
1612
- shouldDeferProtectedApiCalls,
1613
- shouldBlockProtectedApiCalls
1614
- ]);
1615
- const fetchConfig = useCallback(async (options) => {
1616
- await fetchBootstrapConfig(options);
1617
- await fetchReferenceData(options);
1618
- }, [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
+ });
1619
1393
  const refreshSetupContext = useCallback(async () => {
1620
1394
  await checkAuthSession();
1621
1395
  await fetchConfig();
1622
1396
  }, [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
1397
  useEffect(() => {
1840
1398
  if (!configLoaded || workspaceBootstrapPending)
1841
1399
  return;
@@ -1857,162 +1415,44 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1857
1415
  setHydratedWorkspaceRole(null);
1858
1416
  }
1859
1417
  }, [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);
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' };
1918
1425
  }
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) {
1426
+ if (workspaceSwitchingEnabled) {
1427
+ const workspaceResolution = await resolveWorkspaceAfterAuth();
1428
+ if (!workspaceResolution.success) {
1429
+ await fetchConfig({ ignoreAuthGuard: true });
1939
1430
  return {
1940
1431
  success: false,
1941
- workspaceSetupRequired: false,
1942
- 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
1943
1435
  };
1944
1436
  }
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) {
1437
+ if (workspaceResolution.workspaceSetupRequired) {
2008
1438
  await fetchConfig({ ignoreAuthGuard: true });
2009
- return;
1439
+ return { success: true, destination: 'setup' };
2010
1440
  }
2011
1441
  }
2012
- 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' };
2013
1449
  }, [
2014
1450
  checkAuthSession,
1451
+ fetchBootstrapConfig,
2015
1452
  fetchConfig,
1453
+ fetchPlanningEntities,
1454
+ fetchReferenceData,
1455
+ fetchTasks,
2016
1456
  markBootstrapPhase,
2017
1457
  resolveWorkspaceAfterAuth,
2018
1458
  workspaceSwitchingEnabled
@@ -2106,6 +1546,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2106
1546
  }, [refreshSetupContext]);
2107
1547
  const saveWorkspaceProfile = useCallback(async (input) => {
2108
1548
  const name = String(input?.name || '').trim();
1549
+ const requestedWorkspaceId = typeof input?.workspaceId === 'string'
1550
+ ? input.workspaceId.trim()
1551
+ : '';
2109
1552
  if (!name)
2110
1553
  return { success: false, error: 'Workspace name is required.', code: 'WORKSPACE_NAME_REQUIRED' };
2111
1554
  try {
@@ -2117,7 +1560,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2117
1560
  },
2118
1561
  credentials: 'include',
2119
1562
  body: JSON.stringify({
2120
- workspaceId: input.workspaceId,
1563
+ workspaceId: requestedWorkspaceId && requestedWorkspaceId !== 'default'
1564
+ ? requestedWorkspaceId
1565
+ : undefined,
2121
1566
  name,
2122
1567
  description: typeof input.description === 'string' ? input.description : undefined,
2123
1568
  allowCreate: true
@@ -2180,6 +1625,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2180
1625
  };
2181
1626
  }
2182
1627
  if (workspaceResolution.workspaceSetupRequired) {
1628
+ await fetchConfig({ ignoreAuthGuard: true });
2183
1629
  return { success: true, workspaceSetupRequired: true };
2184
1630
  }
2185
1631
  await Promise.all([
@@ -2226,15 +1672,64 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2226
1672
  });
2227
1673
  const data = await res.json().catch(() => ({}));
2228
1674
  if (!res.ok || !data?.success) {
2229
- 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
+ };
2230
1725
  }
2231
1726
  return {
2232
1727
  success: true,
2233
1728
  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),
1729
+ planSelectionRequired,
1730
+ checkoutPending,
1731
+ commercialState,
1732
+ verificationRequired,
2238
1733
  verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
2239
1734
  emailSent: data?.emailSent !== false,
2240
1735
  emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
@@ -2243,7 +1738,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2243
1738
  catch {
2244
1739
  return { success: false, error: 'Create account failed.' };
2245
1740
  }
2246
- }, [resolveCloudAuthUrl, cloudAuthConfigured]);
1741
+ }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
2247
1742
  const updateCurrentUserProfile = useCallback(async (input) => {
2248
1743
  if (!cloudAuthConfigured) {
2249
1744
  return { success: false, error: 'Cloud auth endpoint is not configured.' };
@@ -2516,6 +2011,34 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2516
2011
  return { success: false, error: 'Invite join failed.' };
2517
2012
  }
2518
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]);
2519
2042
  const logout = useCallback(async () => {
2520
2043
  try {
2521
2044
  if (cloudAuthConfigured) {
@@ -2529,6 +2052,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2529
2052
  // no-op
2530
2053
  }
2531
2054
  finally {
2055
+ abortWorkspaceScopedRequests();
2056
+ authSessionEpochRef.current += 1;
2532
2057
  authSessionRequestRef.current = null;
2533
2058
  authSessionLastResultRef.current = false;
2534
2059
  authSessionLastCheckedAtRef.current = Date.now();
@@ -2543,19 +2068,18 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2543
2068
  setUserGlobalSyncStatus('disconnected');
2544
2069
  setUserGlobalSyncError(null);
2545
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
+ }
2546
2081
  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
- });
2082
+ commitCurrentWorkspaceId('default');
2559
2083
  setAvailableWorkspaces([]);
2560
2084
  workspaceListHydratedRef.current = false;
2561
2085
  setHydratedWorkspaceRole(null);
@@ -2568,12 +2092,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2568
2092
  ]);
2569
2093
  return;
2570
2094
  }
2571
- setTasks([]);
2572
- setInitiatives([]);
2573
- setWorkstreams([]);
2095
+ hasBootstrappedDataRef.current = false;
2096
+ resetCloudLogoutClientState();
2574
2097
  }
2575
2098
  }, [
2099
+ abortWorkspaceScopedRequests,
2576
2100
  authRequiredForApi,
2101
+ clearPersistedWorkspaceId,
2102
+ commitCurrentWorkspaceId,
2577
2103
  resolveCloudAuthUrl,
2578
2104
  cloudAuthConfigured,
2579
2105
  workspaceSelectionScope,
@@ -2581,7 +2107,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2581
2107
  fetchConfig,
2582
2108
  fetchPlanningEntities,
2583
2109
  fetchReferenceData,
2584
- fetchTasks
2110
+ fetchTasks,
2111
+ resetCloudLogoutClientState
2585
2112
  ]);
2586
2113
  const createInitiativeFromTemplate = useCallback(async (payload) => {
2587
2114
  try {
@@ -2699,7 +2226,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2699
2226
  }, [fetchPlanningEntities]);
2700
2227
  // Initialize auth once, then bootstrap protected data once auth gates are settled.
2701
2228
  const hasInitializedAuthRef = useRef(false);
2702
- const hasBootstrappedDataRef = useRef(false);
2703
2229
  useEffect(() => {
2704
2230
  setIsOpen(true); // Always open in standalone-first
2705
2231
  if (hasInitializedAuthRef.current)
@@ -2958,73 +2484,19 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2958
2484
  userId: normalizedRealtimeUserId || undefined
2959
2485
  });
2960
2486
  // 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
- }, [
2487
+ useDataVersionRefresh({
3018
2488
  isOpen,
3019
- fetchTasks,
3020
- fetchArchive,
3021
2489
  authBlocked,
3022
2490
  authRequiredForApi,
3023
2491
  isAuthenticated,
3024
- authGuardPolicy,
2492
+ canCallProtectedApi: authGuardPolicy.canCallProtectedApi,
3025
2493
  shouldGateProtectedApiCalls,
2494
+ authSessionResolvedRef,
2495
+ authRequiredForApiRef,
2496
+ isAuthenticatedRef,
2497
+ dataVersionRef,
3026
2498
  refreshTaskCollectionsForDataVersion
3027
- ]);
2499
+ });
3028
2500
  const relationshipTasks = useMemo(() => {
3029
2501
  const byId = new Map();
3030
2502
  for (const task of archivedTasks)
@@ -3548,6 +3020,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3548
3020
  runtimeCapabilities,
3549
3021
  refreshSetupContext,
3550
3022
  retryBootstrapChecks,
3023
+ continueAfterCommercialOnboarding,
3551
3024
  saveWorkspaceProfile,
3552
3025
  fetchWorkspaces,
3553
3026
  createWorkspace,