@taskforcehq/taskforce 0.3.301 → 0.3.303

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 (171) hide show
  1. package/dist/Taskforce.module.css +37 -2
  2. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  3. package/dist/TaskforceCore.js +72 -33
  4. package/dist/TaskforceCore.test.js +848 -130
  5. package/dist/components/features/TaskSettings.js +14 -82
  6. package/dist/components/features/TaskSettings.test.js +33 -70
  7. package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
  8. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
  9. package/dist/components/task/TaskActionHeader.js +6 -3
  10. package/dist/components/task/TaskCard.js +6 -3
  11. package/dist/components/task/TaskForm.js +2 -2
  12. package/dist/components/task/TaskForm.test.js +41 -2
  13. package/dist/components/task/TaskKanban.test.js +17 -0
  14. package/dist/components/ui/ReferenceBadgeButton.js +1 -1
  15. package/dist/components/views/PlansPage.d.ts +3 -1
  16. package/dist/components/views/PlansPage.js +109 -49
  17. package/dist/components/views/PlansPage.test.d.ts +1 -0
  18. package/dist/components/views/PlansPage.test.js +100 -0
  19. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  20. package/dist/components/views/StandaloneLayout.js +473 -256
  21. package/dist/components/views/panels/PlanningDrawer.js +18 -1
  22. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
  23. package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
  24. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  25. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  26. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  27. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  28. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  29. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  30. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  31. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  32. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  33. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  34. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  35. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  36. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  37. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  38. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  39. package/dist/components/views/standalone/modals/types.js +1 -0
  40. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  41. package/dist/components/views/teamManagementAccess.js +2 -2
  42. package/dist/components/views/teamManagementAccess.test.js +3 -3
  43. package/dist/config/deployment.js +2 -2
  44. package/dist/config/deployment.test.js +5 -3
  45. package/dist/core/AiProfileService.js +7 -2
  46. package/dist/core/AiProfiles.test.js +19 -0
  47. package/dist/core/EntitlementsPolicy.test.js +2 -2
  48. package/dist/core/GlobalSettingsService.js +78 -18
  49. package/dist/core/PlanEntitlementService.d.ts +29 -5
  50. package/dist/core/PlanEntitlementService.js +297 -219
  51. package/dist/core/SignupMonetizationSettings.test.js +173 -49
  52. package/dist/core/SyncReconciliationService.d.ts +2 -0
  53. package/dist/core/SyncReconciliationService.js +3 -0
  54. package/dist/core/SystemAdmin.test.js +186 -96
  55. package/dist/core/TaskAuditTimeline.test.js +40 -0
  56. package/dist/core/Taskforce.d.ts +100 -10
  57. package/dist/core/Taskforce.ids.test.js +133 -0
  58. package/dist/core/Taskforce.js +459 -127
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  61. package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
  62. package/dist/core/taskReferencePolicy.d.ts +28 -0
  63. package/dist/core/taskReferencePolicy.js +189 -0
  64. package/dist/core/types.d.ts +13 -12
  65. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  66. package/dist/hooks/useSyncOrchestrator.js +17 -3
  67. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
  68. package/dist/hooks/useTaskData.js +3 -2
  69. package/dist/hooks/useTaskData.test.js +66 -0
  70. package/dist/hooks/useTaskforce.d.ts +8 -0
  71. package/dist/hooks/useTaskforce.js +72 -19
  72. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  73. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  74. package/dist/mcp/aiProfileBootstrap.js +80 -0
  75. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  76. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  77. package/dist/mcp/clientIntegrations.d.ts +2 -8
  78. package/dist/mcp/clientIntegrations.js +21 -40
  79. package/dist/mcp/clientIntegrations.test.js +28 -2
  80. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  81. package/dist/mcp/clientProfileDefaults.js +37 -0
  82. package/dist/mcp/runtime.d.ts +10 -7
  83. package/dist/mcp/runtime.js +131 -48
  84. package/dist/mcp/runtime.test.js +321 -83
  85. package/dist/migrations/billingSchemaParity.test.js +3 -0
  86. package/dist/migrations/taskSchemaMigrations.js +68 -1
  87. package/dist/qaOpen.test.d.ts +1 -0
  88. package/dist/qaOpen.test.js +56 -0
  89. package/dist/server/index.cookieProxy.test.js +2 -0
  90. package/dist/server/index.d.ts +1 -0
  91. package/dist/server/index.js +17 -1
  92. package/dist/server/index.test.js +15 -1
  93. package/dist/server/mockStripe.d.ts +1 -0
  94. package/dist/server/mockStripe.js +2 -0
  95. package/dist/server/routes/admin.js +105 -45
  96. package/dist/server/routes/auth.d.ts +6 -0
  97. package/dist/server/routes/auth.js +505 -30
  98. package/dist/server/routes/billing.js +517 -24
  99. package/dist/server/routes/billing.test.js +967 -45
  100. package/dist/server/routes/documents.js +6 -1
  101. package/dist/server/routes/shared.d.ts +1 -1
  102. package/dist/server/routes/shared.js +16 -6
  103. package/dist/server/routes/sync.integration.test.js +290 -0
  104. package/dist/server/routes/sync.js +234 -1922
  105. package/dist/server/routes/tasks.js +18 -4
  106. package/dist/server/routes.js +29 -22
  107. package/dist/server/routes.test.js +639 -82
  108. package/dist/shared/runtimeContract.d.ts +1 -1
  109. package/dist/sync/cloudSyncApi.d.ts +1 -0
  110. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  111. package/dist/sync/collaborationSyncPayload.js +131 -0
  112. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  113. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  114. package/dist/sync/collaborationSyncState.d.ts +15 -0
  115. package/dist/sync/collaborationSyncState.js +60 -0
  116. package/dist/sync/contentSyncPayload.d.ts +45 -0
  117. package/dist/sync/contentSyncPayload.js +104 -0
  118. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  119. package/dist/sync/contentSyncPayload.test.js +104 -0
  120. package/dist/sync/contentSyncState.d.ts +98 -0
  121. package/dist/sync/contentSyncState.js +1004 -0
  122. package/dist/sync/planningSyncPayload.d.ts +9 -0
  123. package/dist/sync/planningSyncPayload.js +82 -0
  124. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  125. package/dist/sync/planningSyncPayload.test.js +68 -0
  126. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  127. package/dist/sync/syncApplyHandlers.js +445 -0
  128. package/dist/sync/syncService.d.ts +1 -0
  129. package/dist/sync/syncService.js +21 -8
  130. package/dist/sync/taskSyncPayload.d.ts +33 -0
  131. package/dist/sync/taskSyncPayload.js +126 -0
  132. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  133. package/dist/sync/taskSyncPayload.test.js +54 -0
  134. package/dist/sync/workspacePullFeed.d.ts +38 -0
  135. package/dist/sync/workspacePullFeed.js +349 -0
  136. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  137. package/dist/sync/workspacePullFeed.test.js +278 -0
  138. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  139. package/dist/sync/workspaceSyncModel.js +9 -2
  140. package/dist/sync/workspaceSyncModel.test.js +29 -0
  141. package/dist/test/setup.js +1 -1
  142. package/dist/types.d.ts +1 -0
  143. package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
  144. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
  145. package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
  146. package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
  147. package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
  148. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
  149. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  150. package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
  151. package/dist/ui/assets/index-CLIMgR6g.js +6 -0
  152. package/dist/ui/assets/index-DneETxcu.css +1 -0
  153. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
  154. package/dist/ui/index.html +3 -3
  155. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  156. package/dist/utils/accountProfileSummaryCache.js +97 -0
  157. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  158. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  159. package/dist/utils/taskActivity.js +17 -4
  160. package/dist/utils/taskActivity.test.js +27 -0
  161. package/dist/utils/taskNormalization.js +10 -2
  162. package/dist/utils/taskReferences.d.ts +11 -1
  163. package/dist/utils/taskReferences.js +33 -1
  164. package/package.json +4 -3
  165. package/scripts/qa-open-lib.mjs +175 -0
  166. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  167. package/scripts/qa-open.mjs +352 -0
  168. package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
  169. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
  170. package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
  171. package/dist/ui/assets/index-xBWLPOEb.js +0 -6
@@ -44,6 +44,25 @@ function makeAbortableRequestRejectingWithReason(payload) {
44
44
  }
45
45
  };
46
46
  }
47
+ function makeControlledAbortableRequest(payload) {
48
+ let rejectRequest = null;
49
+ let resolveRequest = null;
50
+ const promise = new Promise((resolve, reject) => {
51
+ rejectRequest = reject;
52
+ resolveRequest = () => resolve(makeAbortableFetchResponse(payload));
53
+ });
54
+ return {
55
+ promise,
56
+ resolve() {
57
+ resolveRequest?.();
58
+ },
59
+ attach(signal) {
60
+ signal?.addEventListener('abort', () => {
61
+ rejectRequest?.(new DOMException('Aborted', 'AbortError'));
62
+ }, { once: true });
63
+ }
64
+ };
65
+ }
47
66
  describe('useTaskData', () => {
48
67
  beforeEach(() => {
49
68
  vi.stubGlobal('fetch', vi.fn());
@@ -177,4 +196,51 @@ describe('useTaskData', () => {
177
196
  expect(consoleErrorSpy).not.toHaveBeenCalledWith('[Taskforce] Failed to fetch archive:', 'superseded');
178
197
  expect(consoleErrorSpy).not.toHaveBeenCalled();
179
198
  });
199
+ it('keeps loading active until the replacement tasks request finishes', async () => {
200
+ const firstRequest = makeControlledAbortableRequest({ tasks: [] });
201
+ const secondRequest = makeControlledAbortableRequest({ tasks: [] });
202
+ const fetchMock = vi.mocked(fetch);
203
+ const setLoadingTasks = vi.fn();
204
+ let requestCount = 0;
205
+ fetchMock.mockImplementation((_input, init) => {
206
+ const signal = init?.signal;
207
+ requestCount += 1;
208
+ if (requestCount === 1) {
209
+ firstRequest.attach(signal);
210
+ return firstRequest.promise;
211
+ }
212
+ secondRequest.attach(signal);
213
+ return secondRequest.promise;
214
+ });
215
+ const { result } = renderHook(() => useTaskData({
216
+ tasks: [],
217
+ archivedTasks: [],
218
+ setTasks: vi.fn(),
219
+ setArchivedTasks: vi.fn(),
220
+ setLoadingTasks,
221
+ getCurrentWorkspaceId: () => 'workspace-1',
222
+ workspaceResetKey: 'workspace-1',
223
+ shouldDeferProtectedApiCalls: false,
224
+ shouldBlockProtectedApiCalls: false,
225
+ handleUnauthorized: vi.fn(),
226
+ authRequiredForApi: false
227
+ }));
228
+ let firstPromise;
229
+ let secondPromise;
230
+ await act(async () => {
231
+ firstPromise = result.current.fetchTasks(false);
232
+ secondPromise = result.current.fetchTasks(false);
233
+ await Promise.resolve();
234
+ });
235
+ expect(setLoadingTasks).toHaveBeenNthCalledWith(1, true);
236
+ expect(setLoadingTasks).toHaveBeenNthCalledWith(2, true);
237
+ expect(setLoadingTasks).toHaveBeenCalledTimes(2);
238
+ await act(async () => {
239
+ secondRequest.resolve();
240
+ await secondPromise;
241
+ await firstPromise;
242
+ });
243
+ expect(setLoadingTasks).toHaveBeenLastCalledWith(false);
244
+ expect(setLoadingTasks).toHaveBeenCalledTimes(3);
245
+ });
180
246
  });
@@ -117,8 +117,10 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
117
117
  authRequiredForApi: boolean;
118
118
  authBlocked: boolean;
119
119
  isAuthenticated: boolean;
120
+ authUserId: string;
120
121
  authUserEmail: string;
121
122
  authUserDisplayName: string;
123
+ authUserAvatarUrl: string;
122
124
  authWorkspaceId: string;
123
125
  userGlobalSyncStatus: "error" | "idle" | "syncing" | "disconnected";
124
126
  workspaceLastPullAt: string | null;
@@ -247,9 +249,14 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
247
249
  emailSent?: boolean;
248
250
  emailError?: string;
249
251
  workspaceSetupRequired?: boolean;
252
+ planSelectionRequired?: boolean;
253
+ checkoutPending?: boolean;
254
+ commercialState?: string | null;
250
255
  }>;
251
256
  updateCurrentUserProfile: (input: {
252
257
  displayName: string;
258
+ avatarDraftId?: string | null;
259
+ clearAvatar?: boolean;
253
260
  }) => Promise<{
254
261
  success: boolean;
255
262
  error?: string;
@@ -258,6 +265,7 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
258
265
  userId: string;
259
266
  email: string;
260
267
  displayName: string | null;
268
+ avatarUrl: string | null;
261
269
  };
262
270
  }>;
263
271
  requestEmailVerification: (email: string) => Promise<{
@@ -45,7 +45,7 @@ import { cloneDefaultWorkflowDefinitions } from '../shared/workflowDefaults.js';
45
45
  import { resolveClientDeploymentConfig } from '../config/clientDeployment';
46
46
  import { resolveTaskforceMcpBaseUrlFromBaseUrl } from '../config/cloudEnvironment';
47
47
  import { createDefaultAssigneeOptions, createWorkspaceAssigneeOptions, mergeAssigneeOptions } from '../utils/assignees';
48
- import { persistLocalMutationActorUserId } from '../utils/taskforceApiClient';
48
+ import { persistLocalMutationActorUserId, readLocalMutationActorUserId } from '../utils/taskforceApiClient';
49
49
  import { useSyncOrchestrator } from './useSyncOrchestrator';
50
50
  import { useRealtimeSync } from './useRealtimeSync';
51
51
  import { useUiNotice } from './ui/useUiNotice';
@@ -129,8 +129,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
129
129
  const workspaceMode = workspaceModeRaw === 'single-local' || workspaceModeRaw === 'multi-cloud'
130
130
  ? workspaceModeRaw
131
131
  : (runtimeModeHint === 'cloud' ? 'multi-cloud' : 'single-local');
132
- const authSourceRaw = String(cfg.authSource || '').trim().toLowerCase();
133
- const authSource = authSourceRaw === 'local' ? 'local' : 'cloud';
134
132
  setRuntimeConfigOverride({
135
133
  cloudEnvironment: typeof cfg.cloudEnvironment === 'string'
136
134
  ? String(cfg.cloudEnvironment).trim().toLowerCase()
@@ -142,7 +140,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
142
140
  cloudAuthBaseUrl: normalizeBaseUrl(cfg.cloudAuthBaseUrl),
143
141
  wsBaseUrl: normalizeBaseUrl(cfg.wsBaseUrl),
144
142
  cloudAuthViaLocalProxy: Boolean(cfg.cloudAuthViaLocalProxy),
145
- authSource,
143
+ authSource: 'cloud',
146
144
  workspaceMode,
147
145
  workspaceSwitchingEnabled: typeof cfg.workspaceSwitchingEnabled === 'boolean'
148
146
  ? Boolean(cfg.workspaceSwitchingEnabled)
@@ -182,8 +180,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
182
180
  || '';
183
181
  const explicitConfiguredCloudAuthBaseUrl = propCloudAuthBaseUrl || envCloudAuthBaseUrl || envBaseUrl;
184
182
  const runtimeResolvedCloudAuthBaseUrl = runtimeConfigOverride.cloudAuthBaseUrl || runtimeConfigOverride.baseUrl;
185
- const normalizedCloudAuthBaseUrl = (explicitConfiguredCloudAuthBaseUrl
186
- || runtimeResolvedCloudAuthBaseUrl
183
+ const preferredCloudAuthBaseUrl = isLoopbackHost
184
+ ? (runtimeResolvedCloudAuthBaseUrl || explicitConfiguredCloudAuthBaseUrl)
185
+ : (explicitConfiguredCloudAuthBaseUrl || runtimeResolvedCloudAuthBaseUrl);
186
+ const normalizedCloudAuthBaseUrl = (preferredCloudAuthBaseUrl
187
187
  || normalizedApiBaseUrl
188
188
  || defaultCloudAuthBaseUrl).trim().replace(/\/+$/, '');
189
189
  const inferredCloudMcpBaseUrl = normalizeBaseUrl(resolveTaskforceMcpBaseUrlFromBaseUrl(runtimeConfigOverride.cloudMcpBaseUrl
@@ -208,6 +208,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
208
208
  const shouldGateProtectedApiCalls = shouldProbeCloudAuth && !authOnlyCloudMode;
209
209
  const cloudAuthConfigured = Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl || isCloudHost);
210
210
  const initialRuntimeMode = isCloudHost ? 'cloud' : 'local';
211
+ const optimisticLocalAuthUserId = initialRuntimeMode === 'local' && isLoopbackHost && cloudAuthConfigured
212
+ ? readLocalMutationActorUserId()
213
+ : '';
214
+ const hasOptimisticLocalAuth = optimisticLocalAuthUserId.length > 0;
211
215
  const resolveApiUrl = useCallback((path) => {
212
216
  if (!path)
213
217
  return path;
@@ -225,6 +229,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
225
229
  if (!path.startsWith('/'))
226
230
  return path;
227
231
  const isCloudScopedPath = path.startsWith('/api/taskforce/auth/')
232
+ || path.startsWith('/api/taskforce/account/')
233
+ || path.startsWith('/api/taskforce/billing/')
228
234
  || path.startsWith('/api/taskforce/sync/')
229
235
  || path.startsWith('/api/taskforce/settings/mcp/');
230
236
  if (!isCloudHost && isCloudScopedPath && (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy)) {
@@ -310,11 +316,12 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
310
316
  : runtimeContractFallback.workspaceSwitchingEnabled;
311
317
  const [authRequiredForApi, setAuthRequiredForApi] = useState(false);
312
318
  const [authBlocked, setAuthBlocked] = useState(false);
313
- const [isAuthenticated, setIsAuthenticated] = useState(false);
314
- const [authUserId, setAuthUserId] = useState('anonymous');
319
+ const [isAuthenticated, setIsAuthenticated] = useState(hasOptimisticLocalAuth);
320
+ const [authUserId, setAuthUserId] = useState(hasOptimisticLocalAuth ? optimisticLocalAuthUserId : 'anonymous');
315
321
  const [authWorkspaceId, setAuthWorkspaceId] = useState('');
316
322
  const [authUserEmail, setAuthUserEmail] = useState('');
317
323
  const [authUserDisplayName, setAuthUserDisplayName] = useState('');
324
+ const [authUserAvatarUrl, setAuthUserAvatarUrl] = useState('');
318
325
  const [hasBetaAccess, setHasBetaAccess] = useState(true);
319
326
  const [hydratedWorkspaceRole, setHydratedWorkspaceRole] = useState(null);
320
327
  const [currentWorkspaceId, setCurrentWorkspaceId] = useState(() => {
@@ -333,13 +340,13 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
333
340
  const [availableWorkspaces, setAvailableWorkspaces] = useState([]);
334
341
  const [assigneeOptions, setAssigneeOptions] = useState(() => createDefaultAssigneeOptions());
335
342
  const [workspaceBootstrapPending, setWorkspaceBootstrapPending] = useState(false);
336
- const [authSessionResolved, setAuthSessionResolved] = useState(false);
343
+ const [authSessionResolved, setAuthSessionResolved] = useState(hasOptimisticLocalAuth);
337
344
  const [bootstrapPhase, setBootstrapPhase] = useState('idle');
338
345
  const [bootstrapError, setBootstrapError] = useState(null);
339
346
  const [bootstrapStartedAt, setBootstrapStartedAt] = useState(null);
340
347
  const authSessionRequestRef = useRef(null);
341
348
  const authSessionLastCheckedAtRef = useRef(0);
342
- const authSessionLastResultRef = useRef(false);
349
+ const authSessionLastResultRef = useRef(hasOptimisticLocalAuth);
343
350
  const currentWorkspaceIdRef = useRef(currentWorkspaceId);
344
351
  const explicitWorkspaceSelectionRef = useRef(null);
345
352
  const workspaceTransitionEpochRef = useRef(0);
@@ -622,6 +629,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
622
629
  setIsAuthenticated(false);
623
630
  setAuthUserEmail('');
624
631
  setAuthUserDisplayName('');
632
+ setAuthUserAvatarUrl('');
625
633
  setError(AUTH_REQUIRED_ERROR);
626
634
  }, [AUTH_REQUIRED_ERROR, runtimeMode]);
627
635
  // === SECTION: Task Data ===
@@ -822,6 +830,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
822
830
  persistLocalMutationActorUserId(null);
823
831
  setAuthUserEmail('');
824
832
  setAuthUserDisplayName('');
833
+ setAuthUserAvatarUrl('');
825
834
  setUserGlobalSyncStatus('disconnected');
826
835
  setUserGlobalSyncError(null);
827
836
  setHasBetaAccess(true);
@@ -881,6 +890,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
881
890
  const userDisplayName = typeof data.displayName === 'string'
882
891
  ? data.displayName.trim()
883
892
  : '';
893
+ const userAvatarUrl = typeof data.avatarUrl === 'string'
894
+ ? data.avatarUrl.trim()
895
+ : '';
884
896
  const isLocalRuntime = runtimeMode === 'local';
885
897
  const keepLocalWorkspaceContext = runtimeMode === 'local' || authOnlyCloudMode;
886
898
  setAuthRequiredForApi(authOnlyCloudMode ? false : (isLocalRuntime ? false : required));
@@ -892,6 +904,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
892
904
  }
893
905
  setAuthUserEmail(authenticated ? userEmail : '');
894
906
  setAuthUserDisplayName(authenticated ? userDisplayName : '');
907
+ setAuthUserAvatarUrl(authenticated ? userAvatarUrl : '');
895
908
  if (authenticated) {
896
909
  setError((prev) => prev === AUTH_REQUIRED_ERROR ? '' : prev);
897
910
  }
@@ -1769,7 +1782,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1769
1782
  if (currentMemberRole === 'owner' || currentMemberRole === 'admin' || currentMemberRole === 'member' || currentMemberRole === 'read-only') {
1770
1783
  setHydratedWorkspaceRole(currentMemberRole);
1771
1784
  }
1772
- else if (!workspaceSwitchingEnabled) {
1785
+ else if (!workspaceSwitchingEnabled && users.length === 0) {
1773
1786
  setHydratedWorkspaceRole(null);
1774
1787
  }
1775
1788
  const workspaceMemberCandidates = users
@@ -1793,7 +1806,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1793
1806
  candidate
1794
1807
  ])).values()).filter((candidate) => String(candidate.userId || '').trim().length > 0);
1795
1808
  }
1796
- else if (!workspaceSwitchingEnabled) {
1809
+ else if (!workspaceSwitchingEnabled && !fallbackCandidates.length) {
1797
1810
  setHydratedWorkspaceRole(null);
1798
1811
  }
1799
1812
  const mergedOptions = mergeAssigneeOptions([
@@ -1988,7 +2001,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1988
2001
  const retryBootstrapChecks = useCallback(async () => {
1989
2002
  setBootstrapError(null);
1990
2003
  markBootstrapPhase('auth');
1991
- const authenticated = await checkAuthSession();
2004
+ const authenticated = await checkAuthSession({ force: true });
1992
2005
  if (workspaceSwitchingEnabled && authenticated) {
1993
2006
  const workspaceResolution = await resolveWorkspaceAfterAuth();
1994
2007
  if (!workspaceResolution.success || workspaceResolution.workspaceSetupRequired) {
@@ -2055,7 +2068,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2055
2068
  if (!res.ok || data?.success === false) {
2056
2069
  return { success: false, error: data?.error || `Failed to delete workspace (${res.status})`, code: data?.code };
2057
2070
  }
2058
- await checkAuthSession();
2071
+ await checkAuthSession({ force: true });
2059
2072
  await Promise.all([fetchConfig(), fetchWorkspaces()]);
2060
2073
  const nextWorkspaceId = typeof data?.nextWorkspaceId === 'string' ? data.nextWorkspaceId.trim() : '';
2061
2074
  if (nextWorkspaceId) {
@@ -2157,7 +2170,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2157
2170
  return { success: false, error: data?.error || 'Sign in failed.', code: data?.code };
2158
2171
  }
2159
2172
  setError('');
2160
- await checkAuthSession();
2173
+ await checkAuthSession({ force: true });
2161
2174
  const workspaceResolution = await resolveWorkspaceAfterAuth();
2162
2175
  if (!workspaceResolution.success) {
2163
2176
  return {
@@ -2218,6 +2231,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2218
2231
  return {
2219
2232
  success: true,
2220
2233
  workspaceSetupRequired: Boolean(data?.workspaceSetupRequired),
2234
+ planSelectionRequired: data?.planSelectionRequired === true,
2235
+ checkoutPending: data?.checkoutPending === true,
2236
+ commercialState: typeof data?.commercialState === 'string' ? data.commercialState : null,
2221
2237
  verificationRequired: Boolean(data?.verificationRequired),
2222
2238
  verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
2223
2239
  emailSent: data?.emailSent !== false,
@@ -2233,6 +2249,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2233
2249
  return { success: false, error: 'Cloud auth endpoint is not configured.' };
2234
2250
  }
2235
2251
  const trimmedDisplayName = String(input.displayName || '').trim();
2252
+ const trimmedAvatarDraftId = typeof input.avatarDraftId === 'string' ? input.avatarDraftId.trim() : '';
2253
+ const shouldClearAvatar = input.clearAvatar === true;
2236
2254
  if (!trimmedDisplayName) {
2237
2255
  return { success: false, error: 'Display name is required.' };
2238
2256
  }
@@ -2241,7 +2259,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2241
2259
  method: 'POST',
2242
2260
  headers: { 'Content-Type': 'application/json' },
2243
2261
  credentials: 'include',
2244
- body: JSON.stringify({ displayName: trimmedDisplayName })
2262
+ body: JSON.stringify({
2263
+ displayName: trimmedDisplayName,
2264
+ ...(trimmedAvatarDraftId ? { avatarDraftId: trimmedAvatarDraftId } : {}),
2265
+ ...(shouldClearAvatar ? { clearAvatar: true } : {})
2266
+ })
2245
2267
  });
2246
2268
  const data = await res.json().catch(() => ({}));
2247
2269
  if (!res.ok || !data?.success) {
@@ -2250,9 +2272,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2250
2272
  const profile = {
2251
2273
  userId: typeof data?.profile?.userId === 'string' ? data.profile.userId : authUserId,
2252
2274
  email: typeof data?.profile?.email === 'string' ? data.profile.email : authUserEmail,
2253
- displayName: typeof data?.profile?.displayName === 'string' ? data.profile.displayName : null
2275
+ displayName: typeof data?.profile?.displayName === 'string' ? data.profile.displayName : null,
2276
+ avatarUrl: typeof data?.profile?.avatarUrl === 'string' ? data.profile.avatarUrl : null
2254
2277
  };
2255
2278
  setAuthUserDisplayName(profile.displayName || '');
2279
+ setAuthUserAvatarUrl(profile.avatarUrl || '');
2256
2280
  return { success: true, profile };
2257
2281
  }
2258
2282
  catch {
@@ -2424,7 +2448,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2424
2448
  return { success: false, error: data?.error || 'Invite acceptance failed.', code: data?.code };
2425
2449
  }
2426
2450
  setError('');
2427
- await checkAuthSession();
2451
+ await checkAuthSession({ force: true });
2428
2452
  const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
2429
2453
  const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
2430
2454
  if (!workspaceResolution.success) {
@@ -2505,13 +2529,20 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2505
2529
  // no-op
2506
2530
  }
2507
2531
  finally {
2532
+ authSessionRequestRef.current = null;
2533
+ authSessionLastResultRef.current = false;
2534
+ authSessionLastCheckedAtRef.current = Date.now();
2508
2535
  setIsAuthenticated(false);
2536
+ setAuthSessionResolved(true);
2509
2537
  setAuthUserId('anonymous');
2538
+ setAuthWorkspaceId('');
2510
2539
  persistLocalMutationActorUserId(null);
2511
2540
  setAuthUserEmail('');
2512
2541
  setAuthUserDisplayName('');
2542
+ setAuthUserAvatarUrl('');
2513
2543
  setUserGlobalSyncStatus('disconnected');
2514
2544
  setUserGlobalSyncError(null);
2545
+ setHasBetaAccess(true);
2515
2546
  setAuthBlocked(runtimeMode === 'cloud' && authRequiredForApi);
2516
2547
  setCurrentWorkspaceId((prev) => {
2517
2548
  const existing = String(prev || '').trim();
@@ -2527,11 +2558,31 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2527
2558
  });
2528
2559
  setAvailableWorkspaces([]);
2529
2560
  workspaceListHydratedRef.current = false;
2561
+ setHydratedWorkspaceRole(null);
2562
+ if (runtimeMode === 'local') {
2563
+ await Promise.allSettled([
2564
+ fetchConfig({ ignoreAuthGuard: true }),
2565
+ fetchTasks(true, { ignoreAuthGuard: true }),
2566
+ fetchPlanningEntities({ ignoreAuthGuard: true }),
2567
+ fetchReferenceData({ ignoreAuthGuard: true, force: true })
2568
+ ]);
2569
+ return;
2570
+ }
2530
2571
  setTasks([]);
2531
2572
  setInitiatives([]);
2532
2573
  setWorkstreams([]);
2533
2574
  }
2534
- }, [authRequiredForApi, resolveCloudAuthUrl, cloudAuthConfigured, workspaceSelectionScope, runtimeMode]);
2575
+ }, [
2576
+ authRequiredForApi,
2577
+ resolveCloudAuthUrl,
2578
+ cloudAuthConfigured,
2579
+ workspaceSelectionScope,
2580
+ runtimeMode,
2581
+ fetchConfig,
2582
+ fetchPlanningEntities,
2583
+ fetchReferenceData,
2584
+ fetchTasks
2585
+ ]);
2535
2586
  const createInitiativeFromTemplate = useCallback(async (payload) => {
2536
2587
  try {
2537
2588
  const res = await fetch('/api/taskforce/initiative-templates/create', {
@@ -3456,8 +3507,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3456
3507
  authRequiredForApi,
3457
3508
  authBlocked,
3458
3509
  isAuthenticated,
3510
+ authUserId,
3459
3511
  authUserEmail,
3460
3512
  authUserDisplayName,
3513
+ authUserAvatarUrl,
3461
3514
  authWorkspaceId,
3462
3515
  userGlobalSyncStatus,
3463
3516
  workspaceLastPullAt,
@@ -87,6 +87,7 @@ const archivedTaskRecord = (id, title) => ({
87
87
  describe('useTaskforce sync behavior', () => {
88
88
  beforeEach(() => {
89
89
  localStorage.clear();
90
+ sessionStorage.clear();
90
91
  });
91
92
  afterEach(() => {
92
93
  vi.useRealTimers();
@@ -97,7 +98,7 @@ describe('useTaskforce sync behavior', () => {
97
98
  it('merges a server-returned task into the active collection immediately', async () => {
98
99
  vi.stubGlobal('fetch', vi.fn((url, init) => {
99
100
  if (url.includes('/api/taskforce/auth/runtime-config')) {
100
- return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } });
101
+ return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
101
102
  }
102
103
  if (url.includes('/api/taskforce/auth/session')) {
103
104
  return jsonResponse({
@@ -188,7 +189,7 @@ describe('useTaskforce sync behavior', () => {
188
189
  it('keeps the dawn theme when settings re-fetch configuration', async () => {
189
190
  const fetchMock = vi.fn((url) => {
190
191
  if (url.includes('/api/taskforce/auth/runtime-config')) {
191
- return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } });
192
+ return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
192
193
  }
193
194
  if (url.includes('/api/taskforce/auth/session')) {
194
195
  return jsonResponse({
@@ -261,7 +262,7 @@ describe('useTaskforce sync behavior', () => {
261
262
  it('merges a server-returned archived task immediately instead of dropping it between collections', async () => {
262
263
  vi.stubGlobal('fetch', vi.fn((url, init) => {
263
264
  if (url.includes('/api/taskforce/auth/runtime-config')) {
264
- return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } });
265
+ return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
265
266
  }
266
267
  if (url.includes('/api/taskforce/ui-state?key=app')) {
267
268
  return jsonResponse({ success: true, state: {} });
@@ -339,7 +340,7 @@ describe('useTaskforce sync behavior', () => {
339
340
  it('merges a server-returned completed task immediately when toggling done', async () => {
340
341
  vi.stubGlobal('fetch', vi.fn((url, init) => {
341
342
  if (url.includes('/api/taskforce/auth/runtime-config')) {
342
- return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } });
343
+ return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
343
344
  }
344
345
  if (url.includes('/api/taskforce/ui-state?key=app')) {
345
346
  return jsonResponse({ success: true, state: {} });
@@ -416,7 +417,7 @@ describe('useTaskforce sync behavior', () => {
416
417
  vi.stubGlobal('confirm', vi.fn(() => true));
417
418
  vi.stubGlobal('fetch', vi.fn((url, init) => {
418
419
  if (url.includes('/api/taskforce/auth/runtime-config')) {
419
- return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } });
420
+ return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
420
421
  }
421
422
  if (url.includes('/api/taskforce/ui-state?key=app')) {
422
423
  return jsonResponse({ success: true, state: {} });
@@ -503,7 +504,7 @@ describe('useTaskforce sync behavior', () => {
503
504
  config: {
504
505
  runtimeMode: 'local',
505
506
  workspaceMode: 'single-local',
506
- authSource: 'local',
507
+ authSource: 'cloud',
507
508
  baseUrl: 'http://localhost:5174',
508
509
  apiBaseUrl: 'http://localhost:5174',
509
510
  cloudAuthBaseUrl: 'http://localhost:5174'
@@ -918,6 +919,184 @@ describe('useTaskforce sync behavior', () => {
918
919
  expect(result.current.assigneeOptions.some((option) => option.value === 'ai-profile-codex' && option.label === 'Codex')).toBe(true);
919
920
  });
920
921
  });
922
+ it('keeps the local hydrated workspace role when workspace-members temporarily fails', async () => {
923
+ let workspaceMembersCalls = 0;
924
+ vi.stubGlobal('fetch', vi.fn((url) => {
925
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
926
+ return jsonResponse({
927
+ config: {
928
+ runtimeMode: 'local',
929
+ workspaceMode: 'single-local',
930
+ authSource: 'cloud',
931
+ workspaceSwitchingEnabled: false
932
+ }
933
+ });
934
+ }
935
+ if (url.includes('/api/taskforce/auth/session')) {
936
+ return jsonResponse({
937
+ authenticated: true,
938
+ authRequiredForApi: true,
939
+ betaAccess: true,
940
+ userId: 'user-1',
941
+ email: 'user@example.com',
942
+ workspaceId: 'workspace-cloud-1'
943
+ });
944
+ }
945
+ if (url.includes('/api/taskforce/ui-state?key=app'))
946
+ return jsonResponse({ success: true, state: {} });
947
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
948
+ return jsonResponse({
949
+ success: true,
950
+ state: { version: 2, enabled: false, phase: 'idle', pullCursor: null }
951
+ });
952
+ }
953
+ if (url.includes('/api/taskforce/config')) {
954
+ return jsonResponse({
955
+ projectName: 'Local Project',
956
+ workspaceId: 'workspace-cloud-1',
957
+ runtimeMode: 'local',
958
+ authRequiredForApi: true,
959
+ userId: 'user-1',
960
+ setupState: {
961
+ globalSetupState: 'complete',
962
+ workspaceSetupState: 'complete',
963
+ workspaceId: 'workspace-cloud-1'
964
+ }
965
+ });
966
+ }
967
+ if (url.includes('/api/taskforce/global-settings'))
968
+ return jsonResponse({});
969
+ if (url.includes('/api/taskforce/categories'))
970
+ return jsonResponse({ categories: [{ value: 'general', label: 'General' }] });
971
+ if (url.includes('/api/taskforce/types'))
972
+ return jsonResponse({ types: [] });
973
+ if (url.includes('/api/taskforce/priorities'))
974
+ return jsonResponse({ priorities: [] });
975
+ if (url.includes('/api/taskforce/approaches'))
976
+ return jsonResponse({ approaches: [] });
977
+ if (url.includes('/api/taskforce/specialists'))
978
+ return jsonResponse({ specialists: [] });
979
+ if (url.includes('/api/taskforce/tasks'))
980
+ return jsonResponse({ tasks: [activeTask] });
981
+ if (url.includes('/api/taskforce/archive'))
982
+ return jsonResponse({ archived: [] });
983
+ if (url.includes('/api/taskforce/workspace/assignee-options')) {
984
+ return jsonResponse({
985
+ assignees: [
986
+ { value: 'member-1', label: 'Member One', icon: 'User', color: '#22c55e', kind: 'member', userId: 'member-1', email: 'member1@example.com' }
987
+ ]
988
+ });
989
+ }
990
+ if (url.includes('/api/taskforce/auth/workspace-members')) {
991
+ workspaceMembersCalls += 1;
992
+ if (workspaceMembersCalls === 1) {
993
+ return jsonResponse({
994
+ users: [
995
+ {
996
+ userId: 'user-1',
997
+ email: 'user@example.com',
998
+ displayName: 'Signed In Owner',
999
+ role: 'owner',
1000
+ status: 'active',
1001
+ disabled: false
1002
+ }
1003
+ ]
1004
+ });
1005
+ }
1006
+ return Promise.reject(new Error('temporary workspace-members failure'));
1007
+ }
1008
+ if (url.includes('/api/taskforce/data-version'))
1009
+ return jsonResponse({ dataVersion: 1 });
1010
+ return jsonResponse({});
1011
+ }));
1012
+ const { result } = renderHook(() => useTaskforce({ config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }), { wrapper });
1013
+ await waitFor(() => {
1014
+ expect(result.current.currentWorkspaceRole).toBe('owner');
1015
+ });
1016
+ await act(async () => {
1017
+ await result.current.fetchAssigneeOptions();
1018
+ });
1019
+ expect(result.current.currentWorkspaceRole).toBe('owner');
1020
+ });
1021
+ it('seeds local auth state from the persisted loopback actor while localhost session bootstrap is in flight', async () => {
1022
+ sessionStorage.setItem('taskforce.localMutationActorUserId.v1', 'user-1');
1023
+ const fetchMock = vi.fn((url) => {
1024
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
1025
+ return jsonResponse({
1026
+ config: {
1027
+ runtimeMode: 'local',
1028
+ workspaceMode: 'single-local',
1029
+ authSource: 'cloud',
1030
+ workspaceSwitchingEnabled: false,
1031
+ cloudAuthBaseUrl: 'https://app.taskforcehq.ai',
1032
+ cloudAuthViaLocalProxy: true
1033
+ }
1034
+ });
1035
+ }
1036
+ if (url.includes('/api/taskforce/auth/session')) {
1037
+ return jsonResponse({
1038
+ authenticated: true,
1039
+ authRequiredForApi: true,
1040
+ betaAccess: true,
1041
+ userId: 'user-1',
1042
+ email: 'user@example.com',
1043
+ workspaceId: 'workspace-cloud-1'
1044
+ });
1045
+ }
1046
+ if (url.includes('/api/taskforce/ui-state?key=app'))
1047
+ return jsonResponse({ success: true, state: {} });
1048
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1049
+ return jsonResponse({
1050
+ success: true,
1051
+ state: { version: 2, enabled: false, phase: 'idle', pullCursor: null }
1052
+ });
1053
+ }
1054
+ if (url.includes('/api/taskforce/config')) {
1055
+ return jsonResponse({
1056
+ projectName: 'Local Project',
1057
+ workspaceId: 'workspace-cloud-1',
1058
+ runtimeMode: 'local',
1059
+ setupState: {
1060
+ globalSetupState: 'complete',
1061
+ workspaceSetupState: 'complete',
1062
+ workspaceId: 'workspace-cloud-1'
1063
+ }
1064
+ });
1065
+ }
1066
+ if (url.includes('/api/taskforce/global-settings'))
1067
+ return jsonResponse({});
1068
+ if (url.includes('/api/taskforce/categories'))
1069
+ return jsonResponse({ categories: [{ value: 'general', label: 'General' }] });
1070
+ if (url.includes('/api/taskforce/types'))
1071
+ return jsonResponse({ types: [] });
1072
+ if (url.includes('/api/taskforce/priorities'))
1073
+ return jsonResponse({ priorities: [] });
1074
+ if (url.includes('/api/taskforce/approaches'))
1075
+ return jsonResponse({ approaches: [] });
1076
+ if (url.includes('/api/taskforce/specialists'))
1077
+ return jsonResponse({ specialists: [] });
1078
+ if (url.includes('/api/taskforce/tasks'))
1079
+ return jsonResponse({ tasks: [activeTask] });
1080
+ if (url.includes('/api/taskforce/archive'))
1081
+ return jsonResponse({ archived: [] });
1082
+ if (url.includes('/api/taskforce/workspace/assignee-options'))
1083
+ return jsonResponse({ assignees: [] });
1084
+ if (url.includes('/api/taskforce/data-version'))
1085
+ return jsonResponse({ dataVersion: 1 });
1086
+ return jsonResponse({});
1087
+ });
1088
+ vi.stubGlobal('fetch', fetchMock);
1089
+ const { result } = renderHook(() => useTaskforce({ config: { cloudAuthBaseUrl: 'https://app.taskforcehq.ai' } }), { wrapper });
1090
+ expect(result.current.isAuthenticated).toBe(true);
1091
+ expect(result.current.authSessionResolved).toBe(true);
1092
+ await waitFor(() => {
1093
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/auth/session'))).toBe(true);
1094
+ });
1095
+ await waitFor(() => {
1096
+ expect(result.current.isAuthenticated).toBe(true);
1097
+ expect(result.current.authSessionResolved).toBe(true);
1098
+ });
1099
+ });
921
1100
  it('enables cloud realtime refresh for authenticated sessions and refreshes open task comments from signals', async () => {
922
1101
  vi.stubGlobal('WebSocket', MockWebSocket);
923
1102
  let invalidationTaskFetchCount = 0;