@taskforcehq/taskforce 0.3.303 → 0.3.305

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +102 -5
  3. package/dist/TaskforceCore.js +635 -238
  4. package/dist/TaskforceCore.test.js +2243 -495
  5. package/dist/annotatedAttachments/service.d.ts +1 -1
  6. package/dist/annotatedAttachments/types.d.ts +1 -1
  7. package/dist/compat/workspaceSyncCompat.js +6 -0
  8. package/dist/components/features/AgentsModule.js +1 -1
  9. package/dist/components/features/AgentsModule.test.js +1 -1
  10. package/dist/components/features/DocumentIndex.d.ts +1 -1
  11. package/dist/components/features/DocumentIndex.js +1 -1
  12. package/dist/components/features/DocumentViewer.d.ts +1 -1
  13. package/dist/components/features/DocumentViewer.js +32 -9
  14. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  15. package/dist/components/features/DocumentWorkspace.js +12 -155
  16. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  17. package/dist/components/features/TaskSettings.js +87 -41
  18. package/dist/components/features/TaskSettings.test.js +180 -40
  19. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  20. package/dist/components/features/documentWorkspaceModel.js +57 -0
  21. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  22. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  23. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  24. package/dist/components/task/TaskActionHeader.js +3 -3
  25. package/dist/components/task/TaskActionHeader.test.js +8 -0
  26. package/dist/components/task/TaskCard.js +16 -1
  27. package/dist/components/task/TaskCard.test.js +31 -0
  28. package/dist/components/task/TaskForm.d.ts +9 -8
  29. package/dist/components/task/TaskForm.js +374 -219
  30. package/dist/components/task/TaskForm.test.js +482 -361
  31. package/dist/components/task/TaskKanban.d.ts +1 -1
  32. package/dist/components/task/TaskKanban.js +33 -161
  33. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  34. package/dist/components/task/TaskStatusActions.js +8 -3
  35. package/dist/components/task/TaskStatusActions.test.js +9 -0
  36. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  37. package/dist/components/task/taskKanbanDropRules.js +187 -0
  38. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  39. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  40. package/dist/components/ui/Modal.d.ts +1 -1
  41. package/dist/components/ui/Modal.js +1 -0
  42. package/dist/components/views/AppShellHeader.js +5 -1
  43. package/dist/components/views/AppShellHeader.test.js +4 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  45. package/dist/components/views/PlanComparisonPage.js +9 -10
  46. package/dist/components/views/PlanComparisonPage.test.js +55 -41
  47. package/dist/components/views/PlansPage.d.ts +5 -1
  48. package/dist/components/views/PlansPage.js +150 -81
  49. package/dist/components/views/PlansPage.test.js +367 -41
  50. package/dist/components/views/StandaloneLayout.js +135 -54
  51. package/dist/components/views/WidgetView.js +11 -2
  52. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  53. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  54. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  55. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  56. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  57. package/dist/config/envSchema.js +1 -1
  58. package/dist/core/AiProfileService.d.ts +6 -1
  59. package/dist/core/AiProfileService.js +161 -16
  60. package/dist/core/AiProfileService.test.js +3 -1
  61. package/dist/core/AiProfiles.test.js +200 -0
  62. package/dist/core/AuthTokenService.test.d.ts +1 -0
  63. package/dist/core/AuthTokenService.test.js +78 -0
  64. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  65. package/dist/core/EntitlementsPolicy.test.js +75 -13
  66. package/dist/core/PlanEntitlementService.d.ts +9 -1
  67. package/dist/core/PlanEntitlementService.js +144 -19
  68. package/dist/core/PlanVersionPolicy.test.js +19 -26
  69. package/dist/core/SignupMonetizationSettings.test.js +75 -15
  70. package/dist/core/SystemAdmin.test.js +212 -56
  71. package/dist/core/TaskActivityService.d.ts +4 -1
  72. package/dist/core/TaskActivityService.js +45 -0
  73. package/dist/core/TaskAuditTimeline.test.js +52 -1
  74. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  75. package/dist/core/Taskforce.d.ts +18 -11
  76. package/dist/core/Taskforce.js +93 -12
  77. package/dist/core/WorkspacePlanMode.test.js +73 -8
  78. package/dist/core/shared.d.ts +2 -0
  79. package/dist/core/shared.js +11 -0
  80. package/dist/documentReviews/service.d.ts +1 -1
  81. package/dist/documentReviews/types.d.ts +1 -1
  82. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  83. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  84. package/dist/hooks/sync/auth.d.ts +37 -0
  85. package/dist/hooks/sync/auth.js +63 -0
  86. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  87. package/dist/hooks/sync/bootstrap.js +52 -0
  88. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  89. package/dist/hooks/sync/orchestratorShared.js +209 -0
  90. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  91. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  92. package/dist/hooks/sync/realtime.d.ts +25 -0
  93. package/dist/hooks/sync/realtime.js +60 -0
  94. package/dist/hooks/sync/recovery.d.ts +69 -0
  95. package/dist/hooks/sync/recovery.js +118 -0
  96. package/dist/hooks/sync/transfers.d.ts +123 -0
  97. package/dist/hooks/sync/transfers.js +435 -0
  98. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  99. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  100. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  101. package/dist/hooks/ui/useResourceExport.js +5 -2
  102. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  103. package/dist/hooks/useRealtimeSync.js +2 -1
  104. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  105. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  106. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  107. package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
  108. package/dist/hooks/useSyncOrchestrator.js +388 -879
  109. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
  110. package/dist/hooks/useTaskMutations.d.ts +2 -1
  111. package/dist/hooks/useTaskMutations.js +57 -7
  112. package/dist/hooks/useTaskMutations.test.js +176 -0
  113. package/dist/hooks/useTaskforce.d.ts +13 -4
  114. package/dist/hooks/useTaskforce.js +302 -817
  115. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  116. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  117. package/dist/hooks/useWorkspaceSyncController.js +33 -11
  118. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  119. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  120. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  121. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  122. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  123. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  124. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  125. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  126. package/dist/localization/locales/en-US.js +2 -2
  127. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  128. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  129. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  130. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  131. package/dist/mcp/clientIntegrations.d.ts +12 -1
  132. package/dist/mcp/clientIntegrations.js +161 -6
  133. package/dist/mcp/clientIntegrations.test.js +76 -4
  134. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  135. package/dist/mcp/collaborationRegistrar.js +71 -0
  136. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  137. package/dist/mcp/documentAssetRegistrar.js +346 -0
  138. package/dist/mcp/documentHelpers.d.ts +100 -0
  139. package/dist/mcp/documentHelpers.js +161 -0
  140. package/dist/mcp/runtime.js +2455 -3925
  141. package/dist/mcp/runtime.test.js +240 -8
  142. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  143. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  144. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  145. package/dist/mcp/taskAttachmentTypes.js +1 -0
  146. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  147. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  148. package/dist/mcp/toolCatalog.d.ts +35 -0
  149. package/dist/mcp/toolCatalog.js +3 -0
  150. package/dist/plugins/vite.js +1 -1
  151. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  152. package/dist/runtime/appGateDefinitions.js +1 -1
  153. package/dist/server/index.d.ts +1 -0
  154. package/dist/server/index.js +46 -3
  155. package/dist/server/index.test.js +12 -1
  156. package/dist/server/routes/admin.d.ts +1 -5
  157. package/dist/server/routes/admin.js +151 -80
  158. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  159. package/dist/server/routes/annotatedAttachments.js +1 -1
  160. package/dist/server/routes/auth.d.ts +1 -4
  161. package/dist/server/routes/auth.js +59 -64
  162. package/dist/server/routes/authSupport.d.ts +30 -0
  163. package/dist/server/routes/authSupport.js +81 -0
  164. package/dist/server/routes/billing.d.ts +1 -1
  165. package/dist/server/routes/billing.js +122 -255
  166. package/dist/server/routes/billing.test.js +257 -86
  167. package/dist/server/routes/documentReviews.d.ts +1 -1
  168. package/dist/server/routes/documentReviews.js +1 -1
  169. package/dist/server/routes/documents.d.ts +4 -2
  170. package/dist/server/routes/documents.js +73 -7
  171. package/dist/server/routes/primitives.d.ts +11 -0
  172. package/dist/server/routes/primitives.js +73 -0
  173. package/dist/server/routes/resources.d.ts +1 -1
  174. package/dist/server/routes/resources.js +1 -1
  175. package/dist/server/routes/shared.d.ts +31 -1
  176. package/dist/server/routes/shared.js +26 -0
  177. package/dist/server/routes/sync.d.ts +1 -1
  178. package/dist/server/routes/sync.integration.test.js +20 -0
  179. package/dist/server/routes/sync.js +111 -238
  180. package/dist/server/routes/tasks.d.ts +1 -1
  181. package/dist/server/routes/tasks.js +50 -8
  182. package/dist/server/routes/workspaces.d.ts +1 -1
  183. package/dist/server/routes/workspaces.js +1 -1
  184. package/dist/server/routes.d.ts +2 -9
  185. package/dist/server/routes.js +84 -181
  186. package/dist/server/routes.test.js +912 -32
  187. package/dist/shared/taskActor.d.ts +10 -0
  188. package/dist/shared/taskActor.js +1 -0
  189. package/dist/sync/collaborationSyncPayload.js +4 -0
  190. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  191. package/dist/sync/collaborationSyncState.d.ts +3 -1
  192. package/dist/sync/collaborationSyncState.js +19 -0
  193. package/dist/sync/contentSyncState.js +28 -17
  194. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  195. package/dist/sync/syncApplyHandlers.js +370 -8
  196. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  197. package/dist/sync/syncApplyHandlers.test.js +379 -0
  198. package/dist/sync/syncService.d.ts +9 -1
  199. package/dist/sync/syncService.js +44 -2
  200. package/dist/sync/syncService.test.js +62 -0
  201. package/dist/sync/workspacePullFeed.d.ts +1 -0
  202. package/dist/sync/workspacePullFeed.js +66 -59
  203. package/dist/sync/workspacePullFeed.test.js +72 -4
  204. package/dist/sync/workspaceSyncModel.d.ts +25 -1
  205. package/dist/sync/workspaceSyncModel.js +77 -3
  206. package/dist/sync/workspaceSyncModel.test.js +153 -0
  207. package/dist/sync/workspaceSyncState.d.ts +2 -0
  208. package/dist/sync/workspaceSyncState.js +1 -0
  209. package/dist/types.d.ts +2 -10
  210. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
  211. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  212. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  213. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  214. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  215. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  216. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  217. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  218. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
  219. package/dist/ui/assets/index-C2-OXZV7.css +1 -0
  220. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  221. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
  222. package/dist/ui/index.html +4 -4
  223. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  224. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  225. package/dist/utils/taskActivity.js +4 -0
  226. package/dist/utils/taskActivity.test.js +25 -1
  227. package/package.json +10 -1
  228. package/scripts/check-cloud-mcp.mjs +83 -0
  229. package/scripts/playwright-auth.sh +9 -1
  230. package/scripts/run-billing-performance-smoke.sh +24 -0
  231. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  232. package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
  233. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  234. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  235. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  236. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  237. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -1097,6 +1097,84 @@ describe('useTaskforce sync behavior', () => {
1097
1097
  expect(result.current.authSessionResolved).toBe(true);
1098
1098
  });
1099
1099
  });
1100
+ it('keeps loopback cloud runtime auth on same-origin after runtime config resolves', async () => {
1101
+ const fetchMock = vi.fn((url) => {
1102
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
1103
+ return jsonResponse({
1104
+ config: {
1105
+ runtimeMode: 'cloud',
1106
+ workspaceMode: 'multi-cloud',
1107
+ authSource: 'cloud',
1108
+ workspaceSwitchingEnabled: true
1109
+ }
1110
+ });
1111
+ }
1112
+ if (url.startsWith('https://app.taskforcehq.ai/api/taskforce/auth/session')) {
1113
+ return jsonResponse({ authenticated: false, authRequiredForApi: true }, 401);
1114
+ }
1115
+ if (url.includes('/api/taskforce/auth/session')) {
1116
+ return jsonResponse({
1117
+ authenticated: true,
1118
+ authRequiredForApi: true,
1119
+ betaAccess: true,
1120
+ userId: 'user-1',
1121
+ email: 'user@example.com',
1122
+ workspaceId: 'workspace-cloud-1'
1123
+ });
1124
+ }
1125
+ if (url.includes('/api/taskforce/ui-state?key=app'))
1126
+ return jsonResponse({ success: true, state: {} });
1127
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1128
+ return jsonResponse({
1129
+ success: true,
1130
+ state: { version: 2, enabled: false, phase: 'idle', pullCursor: null }
1131
+ });
1132
+ }
1133
+ if (url.includes('/api/taskforce/config')) {
1134
+ return jsonResponse({
1135
+ projectName: 'Loopback Cloud Project',
1136
+ workspaceId: 'workspace-cloud-1',
1137
+ runtimeMode: 'cloud',
1138
+ authRequiredForApi: true,
1139
+ userId: 'user-1',
1140
+ setupState: {
1141
+ globalSetupState: 'complete',
1142
+ workspaceSetupState: 'complete',
1143
+ workspaceId: 'workspace-cloud-1'
1144
+ }
1145
+ });
1146
+ }
1147
+ if (url.includes('/api/taskforce/global-settings'))
1148
+ return jsonResponse({});
1149
+ if (url.includes('/api/taskforce/categories'))
1150
+ return jsonResponse({ categories: [{ value: 'general', label: 'General' }] });
1151
+ if (url.includes('/api/taskforce/types'))
1152
+ return jsonResponse({ types: [] });
1153
+ if (url.includes('/api/taskforce/priorities'))
1154
+ return jsonResponse({ priorities: [] });
1155
+ if (url.includes('/api/taskforce/approaches'))
1156
+ return jsonResponse({ approaches: [] });
1157
+ if (url.includes('/api/taskforce/specialists'))
1158
+ return jsonResponse({ specialists: [] });
1159
+ if (url.includes('/api/taskforce/tasks'))
1160
+ return jsonResponse({ tasks: [activeTask] });
1161
+ if (url.includes('/api/taskforce/archive'))
1162
+ return jsonResponse({ archived: [] });
1163
+ if (url.includes('/api/taskforce/workspace/assignee-options'))
1164
+ return jsonResponse({ assignees: [] });
1165
+ if (url.includes('/api/taskforce/data-version'))
1166
+ return jsonResponse({ dataVersion: 1 });
1167
+ return jsonResponse({});
1168
+ });
1169
+ vi.stubGlobal('fetch', fetchMock);
1170
+ const { result } = renderHook(() => useTaskforce({ config: { cloudAuthBaseUrl: 'https://app.taskforcehq.ai' } }), { wrapper });
1171
+ await waitFor(() => {
1172
+ expect(result.current.authSessionResolved).toBe(true);
1173
+ expect(result.current.isAuthenticated).toBe(true);
1174
+ expect(result.current.currentWorkspaceId).toBe('workspace-cloud-1');
1175
+ });
1176
+ expect(fetchMock.mock.calls.some(([url]) => String(url).startsWith('https://app.taskforcehq.ai/api/taskforce/auth/session'))).toBe(false);
1177
+ });
1100
1178
  it('enables cloud realtime refresh for authenticated sessions and refreshes open task comments from signals', async () => {
1101
1179
  vi.stubGlobal('WebSocket', MockWebSocket);
1102
1180
  let invalidationTaskFetchCount = 0;
@@ -1,5 +1,5 @@
1
1
  import type { EnsureWorkspaceReadyResult } from '../sync/syncService';
2
- import { type WorkspaceSyncPhase, type WorkspaceSyncStateV2 } from '../sync/workspaceSyncState';
2
+ import { type WorkspaceSyncSetupIntent, type WorkspaceSyncPhase, type WorkspaceSyncStateV2 } from '../sync/workspaceSyncState';
3
3
  type WorkspaceSyncLeaseOperation = 'pull' | 'push' | 'repair';
4
4
  interface WorkspaceSyncLeaseRecord {
5
5
  ownerId: string;
@@ -12,6 +12,7 @@ interface UseWorkspaceSyncControllerInput {
12
12
  currentWorkspaceId: string;
13
13
  setWorkspaceCloudSyncEnabled: (value: boolean) => void;
14
14
  setWorkspaceSyncPhase: (value: WorkspaceSyncPhase) => void;
15
+ setWorkspaceSyncSetupIntent: (value: WorkspaceSyncSetupIntent) => void;
15
16
  setWorkspaceLastPullAt: (value: string | null) => void;
16
17
  setWorkspaceLastPushAt: (value: string | null) => void;
17
18
  setWorkspaceLastErrorMessage: (value: string | null) => void;
@@ -30,12 +31,14 @@ export declare function useWorkspaceSyncController(input: UseWorkspaceSyncContro
30
31
  workspacePendingSignatureRef: import("react").RefObject<string>;
31
32
  workspaceLastPushedTaskIdsRef: import("react").RefObject<Set<string>>;
32
33
  workspaceDeletedTaskIdsRef: import("react").RefObject<Set<string>>;
34
+ workspaceDeletedTaskWatermarksRef: import("react").RefObject<Map<string, string>>;
33
35
  workspacePullCursorRef: import("react").RefObject<string | null>;
34
36
  clearWorkspaceRetry: () => void;
35
37
  scheduleWorkspaceRetry: (invokeSync: () => Promise<boolean>, options?: {
36
38
  minDelayMs?: number;
37
39
  }) => void;
38
40
  persistWorkspaceSyncPatch: (patch: Partial<WorkspaceSyncStateV2>) => Promise<WorkspaceSyncStateV2>;
41
+ applyWorkspaceSyncPatchLocally: (patch: Partial<WorkspaceSyncStateV2>) => WorkspaceSyncStateV2;
39
42
  loadWorkspaceSyncState: () => Promise<void>;
40
43
  applyWorkspaceSyncStateSnapshot: (nextState: Partial<WorkspaceSyncStateV2> & {
41
44
  enabled: boolean;
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { calculateWorkspaceRetryDelayMs } from '../sync/syncService';
3
3
  import { normalizeWorkspaceSyncStateCompat } from '../compat/workspaceSyncCompat';
4
4
  import { createDefaultWorkspaceSyncState } from '../sync/workspaceSyncState';
5
+ import { persistRemoteUiState } from './workspace/workspaceLocalState';
5
6
  const WORKSPACE_SYNC_LEASE_KEY_PREFIX = 'taskforce.sync.lease.v1';
6
7
  const WORKSPACE_SYNC_LEASE_CHANNEL = 'taskforce.sync.lease.v1';
7
8
  const WORKSPACE_SYNC_LEASE_HEARTBEAT_MS = 2500;
@@ -58,6 +59,7 @@ function buildWorkspaceSyncStatePatch(current, patch) {
58
59
  ...patch,
59
60
  version: 2,
60
61
  phase: patch.phase || current.phase,
62
+ setupIntent: patch.setupIntent === undefined ? (current.setupIntent ?? null) : (patch.setupIntent ?? null),
61
63
  pullCursor: patch.pullCursor === undefined ? current.pullCursor : normalizeCursor(patch.pullCursor),
62
64
  lastPullAt: patch.lastPullAt === undefined ? current.lastPullAt : normalizeCursor(patch.lastPullAt),
63
65
  lastPushAt: patch.lastPushAt === undefined ? current.lastPushAt : normalizeCursor(patch.lastPushAt),
@@ -66,12 +68,13 @@ function buildWorkspaceSyncStatePatch(current, patch) {
66
68
  };
67
69
  if (!next.enabled) {
68
70
  next.phase = 'idle';
71
+ next.setupIntent = null;
69
72
  next.pullCursor = null;
70
73
  }
71
74
  return next;
72
75
  }
73
76
  export function useWorkspaceSyncController(input) {
74
- const { currentWorkspaceId, setWorkspaceCloudSyncEnabled, setWorkspaceSyncPhase, setWorkspaceLastPullAt, setWorkspaceLastPushAt, setWorkspaceLastErrorMessage } = input;
77
+ const { currentWorkspaceId, setWorkspaceCloudSyncEnabled, setWorkspaceSyncPhase, setWorkspaceSyncSetupIntent, setWorkspaceLastPullAt, setWorkspaceLastPushAt, setWorkspaceLastErrorMessage } = input;
75
78
  const [workspaceRetryAt, setWorkspaceRetryAt] = useState(null);
76
79
  const workspaceRetryTimerRef = useRef(null);
77
80
  const workspaceRetryAtMsRef = useRef(null);
@@ -85,6 +88,7 @@ export function useWorkspaceSyncController(input) {
85
88
  const workspacePendingSignatureRef = useRef('');
86
89
  const workspaceLastPushedTaskIdsRef = useRef(new Set());
87
90
  const workspaceDeletedTaskIdsRef = useRef(new Set());
91
+ const workspaceDeletedTaskWatermarksRef = useRef(new Map());
88
92
  const workspacePullCursorRef = useRef(null);
89
93
  const workspaceSyncStateRef = useRef(createDefaultWorkspaceSyncState());
90
94
  const workspaceSyncLeaseOwnerIdRef = useRef('');
@@ -254,6 +258,7 @@ export function useWorkspaceSyncController(input) {
254
258
  const next = buildWorkspaceSyncStatePatch(current, {
255
259
  enabled: Boolean(nextState.enabled),
256
260
  phase: nextState.phase || current.phase,
261
+ setupIntent: nextState.setupIntent,
257
262
  pullCursor: nextState.pullCursor,
258
263
  lastPullAt: nextState.lastPullAt,
259
264
  lastPushAt: nextState.lastPushAt,
@@ -264,31 +269,43 @@ export function useWorkspaceSyncController(input) {
264
269
  workspacePullCursorRef.current = next.pullCursor;
265
270
  setWorkspaceCloudSyncEnabled(next.enabled);
266
271
  setWorkspaceSyncPhase(next.phase);
272
+ setWorkspaceSyncSetupIntent(next.setupIntent ?? null);
267
273
  setWorkspaceLastPullAt(next.lastPullAt);
268
274
  setWorkspaceLastPushAt(next.lastPushAt);
269
275
  setWorkspaceLastErrorMessage(next.lastErrorMessage ?? null);
270
276
  }, [
271
277
  setWorkspaceCloudSyncEnabled,
272
278
  setWorkspaceSyncPhase,
279
+ setWorkspaceSyncSetupIntent,
273
280
  setWorkspaceLastPullAt,
274
281
  setWorkspaceLastPushAt,
275
282
  setWorkspaceLastErrorMessage
276
283
  ]);
284
+ const applyWorkspaceSyncPatchLocally = useCallback((patch) => {
285
+ const next = buildWorkspaceSyncStatePatch(workspaceSyncStateRef.current, patch);
286
+ applyWorkspaceSyncStateSnapshot(next);
287
+ return next;
288
+ }, [applyWorkspaceSyncStateSnapshot]);
277
289
  const persistWorkspaceSyncPatch = useCallback(async (patch) => {
278
290
  const next = buildWorkspaceSyncStatePatch(workspaceSyncStateRef.current, patch);
279
291
  workspaceSyncStateRef.current = next;
280
292
  workspacePullCursorRef.current = next.pullCursor;
281
- const res = await fetch('/api/taskforce/ui-state', {
282
- method: 'POST',
283
- headers: { 'Content-Type': 'application/json' },
284
- credentials: 'include',
285
- body: JSON.stringify({
286
- workspaceId: currentWorkspaceId,
287
- stateKey: 'workspace-sync',
288
- patch: next
289
- })
293
+ const result = await persistRemoteUiState({
294
+ workspaceId: currentWorkspaceId,
295
+ stateKey: 'workspace-sync',
296
+ patch: next,
297
+ throwOnError: true,
298
+ respectFailureCooldown: false
290
299
  });
291
- const payload = await res.json().catch(() => ({}));
300
+ const res = result.response;
301
+ const payload = result.payload;
302
+ if (result.skipped) {
303
+ applyWorkspaceSyncStateSnapshot(next);
304
+ return next;
305
+ }
306
+ if (!res) {
307
+ throw new Error('Failed to persist workspace sync state (missing response)');
308
+ }
292
309
  if (!res.ok || payload?.success === false) {
293
310
  throw new Error(payload?.error || `Failed to persist workspace sync state (${res.status})`);
294
311
  }
@@ -332,6 +349,7 @@ export function useWorkspaceSyncController(input) {
332
349
  await persistWorkspaceSyncPatch({
333
350
  enabled: false,
334
351
  phase: 'idle',
352
+ setupIntent: null,
335
353
  pullCursor: null
336
354
  });
337
355
  return { success: true };
@@ -349,6 +367,7 @@ export function useWorkspaceSyncController(input) {
349
367
  await persistWorkspaceSyncPatch({
350
368
  enabled: true,
351
369
  phase: ensured.provisioned ? 'provision-local' : 'attach-cloud',
370
+ setupIntent: null,
352
371
  pullCursor: null,
353
372
  lastPullAt: null,
354
373
  lastPushAt: null,
@@ -365,6 +384,7 @@ export function useWorkspaceSyncController(input) {
365
384
  workspacePendingSignatureRef.current = '';
366
385
  workspaceLastPushedTaskIdsRef.current = new Set();
367
386
  workspaceDeletedTaskIdsRef.current = new Set();
387
+ workspaceDeletedTaskWatermarksRef.current = new Map();
368
388
  workspacePullCursorRef.current = null;
369
389
  workspaceSyncStateRef.current = createDefaultWorkspaceSyncState();
370
390
  workspacePushInFlightRef.current = false;
@@ -402,10 +422,12 @@ export function useWorkspaceSyncController(input) {
402
422
  workspacePendingSignatureRef,
403
423
  workspaceLastPushedTaskIdsRef,
404
424
  workspaceDeletedTaskIdsRef,
425
+ workspaceDeletedTaskWatermarksRef,
405
426
  workspacePullCursorRef,
406
427
  clearWorkspaceRetry,
407
428
  scheduleWorkspaceRetry,
408
429
  persistWorkspaceSyncPatch,
430
+ applyWorkspaceSyncPatchLocally,
409
431
  loadWorkspaceSyncState,
410
432
  applyWorkspaceSyncStateSnapshot,
411
433
  saveWorkspaceCloudSyncSettings,
@@ -5,6 +5,7 @@ const makeInput = (overrides = {}) => ({
5
5
  currentWorkspaceId: 'ws-test',
6
6
  setWorkspaceCloudSyncEnabled: vi.fn(),
7
7
  setWorkspaceSyncPhase: vi.fn(),
8
+ setWorkspaceSyncSetupIntent: vi.fn(),
8
9
  setWorkspaceLastPullAt: vi.fn(),
9
10
  setWorkspaceLastPushAt: vi.fn(),
10
11
  setWorkspaceLastErrorMessage: vi.fn(),
@@ -57,4 +57,21 @@ describe('useTaskforceApiRouting', () => {
57
57
  expect(headers.get('x-taskforce-workspace-id')).toBe('workspace-2');
58
58
  expect(headers.get('x-taskforce-workspace-authoritative')).toBe('1');
59
59
  });
60
+ it('stops injecting the workspace header when the live ref resets to default', async () => {
61
+ const fetchMock = vi.fn(async (_input, init) => new Response(null, { status: 204 }));
62
+ vi.stubGlobal('fetch', fetchMock);
63
+ const currentWorkspaceIdRef = createRef();
64
+ currentWorkspaceIdRef.current = 'workspace-1';
65
+ renderHook(() => useTaskforceApiRouting({
66
+ currentWorkspaceIdRef,
67
+ runtimeMode: 'cloud',
68
+ resolveApiUrl: (path) => `https://cloud.example.com${path}`
69
+ }));
70
+ currentWorkspaceIdRef.current = 'default';
71
+ await window.fetch('/api/taskforce/tasks');
72
+ const [, init] = fetchMock.mock.calls.at(-1);
73
+ const headers = init.headers;
74
+ expect(headers.get('x-taskforce-workspace-id')).toBeNull();
75
+ expect(headers.get('x-taskforce-workspace-authoritative')).toBeNull();
76
+ });
60
77
  });
@@ -0,0 +1,166 @@
1
+ import { type MutableRefObject } from 'react';
2
+ import type { TaskAssigneeOption } from '../../utils/assignees';
3
+ export type BootstrapPhase = 'idle' | 'auth' | 'workspace' | 'config' | 'ready' | 'stalled';
4
+ export type WorkspaceRole = 'owner' | 'admin' | 'member' | 'read-only';
5
+ export interface WorkspaceSummary {
6
+ id: string;
7
+ name: string;
8
+ role: WorkspaceRole;
9
+ slug?: string | null;
10
+ description?: string | null;
11
+ status?: string;
12
+ betaAccess?: boolean;
13
+ }
14
+ interface UseTaskforceWorkspaceBootstrapArgs {
15
+ shouldDeferProtectedApiCalls: boolean;
16
+ shouldBlockProtectedApiCalls: boolean;
17
+ getWorkspaceRequestState: () => {
18
+ workspaceId: string;
19
+ epoch: number;
20
+ };
21
+ isWorkspaceRequestStale: (requestState: {
22
+ workspaceId: string;
23
+ epoch: number;
24
+ }) => boolean;
25
+ isWorkspaceEpochStale: (requestState: {
26
+ workspaceId: string;
27
+ epoch: number;
28
+ }) => boolean;
29
+ isAbortError: (error: unknown) => boolean;
30
+ isBootstrapTimeoutError: (error: unknown) => boolean;
31
+ fetchWithTimeout: (url: string, init?: RequestInit, timeoutMs?: number, options?: {
32
+ signal?: AbortSignal;
33
+ }) => Promise<Response>;
34
+ handleUnauthorized: () => void;
35
+ applyBootstrapConfig: (data: any) => void;
36
+ loadPersistedUiState: (workspaceIdOverride?: string) => Promise<void>;
37
+ fetchBuildInfo: () => Promise<void>;
38
+ markBootstrapPhase: (phase: Exclude<BootstrapPhase, 'stalled'>) => void;
39
+ markBootstrapStalled: (message: string) => void;
40
+ fetchWorkflows: () => Promise<void>;
41
+ fetchInitiativeTemplates: () => Promise<void>;
42
+ loadAvailableEnvironments: () => Promise<void>;
43
+ setReferenceDataLoaded: (value: boolean) => void;
44
+ setCustomCategories: (value: any[]) => void;
45
+ setCustomTypes: (value: any[]) => void;
46
+ setPriorities: (value: any[]) => void;
47
+ setTaxonomies: (value: any[]) => void;
48
+ setTaxonomyDisplayLabels: (value: any) => void;
49
+ setConfigLoaded: (value: boolean) => void;
50
+ hasLoadedUiStateRef: MutableRefObject<boolean>;
51
+ currentWorkspaceIdRef: MutableRefObject<string>;
52
+ referenceDataRequestRef: MutableRefObject<{
53
+ workspaceId: string;
54
+ promise: Promise<void>;
55
+ } | null>;
56
+ referenceDataAbortRef: MutableRefObject<AbortController | null>;
57
+ bootstrapConfigAbortRef: MutableRefObject<AbortController | null>;
58
+ workspaceListAbortRef: MutableRefObject<AbortController | null>;
59
+ assigneeOptionsAbortRef: MutableRefObject<AbortController | null>;
60
+ workspaceListHydratedRef: MutableRefObject<boolean>;
61
+ setAvailableWorkspaces: (value: any[]) => void;
62
+ runtimeMode: 'local' | 'cloud';
63
+ applyResolvedWorkspaceId: (workspaceId: string, options?: {
64
+ preserveNonDefaultDefault?: boolean;
65
+ }) => void;
66
+ workspaceSwitchingEnabled: boolean;
67
+ authSessionResolved: boolean;
68
+ isAuthenticated: boolean;
69
+ authUserId: string;
70
+ setHydratedWorkspaceRole: (value: WorkspaceRole | null) => void;
71
+ setAssigneeOptions: (value: TaskAssigneeOption[]) => void;
72
+ setAssigneeOptionsLoaded: (value: boolean) => void;
73
+ commitCurrentWorkspaceId: (workspaceId: string, options?: {
74
+ clearExplicitSelection?: boolean;
75
+ }) => void;
76
+ currentWorkspaceId: string;
77
+ workspaceSelectionScope: string;
78
+ abortWorkspaceScopedRequests: () => void;
79
+ setWorkspaceBootstrapPending: (value: boolean) => void;
80
+ checkAuthSession: (options?: {
81
+ force?: boolean;
82
+ }) => Promise<boolean>;
83
+ lastConfigRefreshKeyRef: MutableRefObject<string>;
84
+ lastAssigneeOptionsLoadKeyRef: MutableRefObject<string>;
85
+ lastWorkspaceSyncStateLoadKeyRef: MutableRefObject<string>;
86
+ lastTaskSurfaceLoadKeyRef: MutableRefObject<string>;
87
+ lastSettingsSurfaceLoadKeyRef: MutableRefObject<string>;
88
+ isOpen: boolean;
89
+ activeTab: string;
90
+ showArchive: boolean;
91
+ taskScope: string;
92
+ settingsSection: string | null;
93
+ fetchTasks: (isSilent?: boolean, options?: {
94
+ ignoreAuthGuard?: boolean;
95
+ }) => Promise<void>;
96
+ fetchArchive: (isSilent?: boolean, options?: {
97
+ ignoreAuthGuard?: boolean;
98
+ }) => Promise<void>;
99
+ fetchPlanningEntities: (options?: {
100
+ ignoreAuthGuard?: boolean;
101
+ }) => Promise<void>;
102
+ refreshTaskCollections: (options?: {
103
+ isSilent?: boolean;
104
+ ignoreAuthGuard?: boolean;
105
+ }) => Promise<void>;
106
+ loadWorkspaceSyncState: () => Promise<void>;
107
+ archiveBootstrapTimerRef: MutableRefObject<number | null>;
108
+ hasBootstrappedDataRef: MutableRefObject<boolean>;
109
+ }
110
+ export declare function useTaskforceWorkspaceBootstrap(args: UseTaskforceWorkspaceBootstrapArgs): {
111
+ fetchReferenceData: (options?: {
112
+ ignoreAuthGuard?: boolean;
113
+ force?: boolean;
114
+ }) => Promise<void>;
115
+ fetchBootstrapConfig: (options?: {
116
+ ignoreAuthGuard?: boolean;
117
+ }) => Promise<void>;
118
+ fetchConfig: (options?: {
119
+ ignoreAuthGuard?: boolean;
120
+ }) => Promise<void>;
121
+ fetchWorkspaces: () => Promise<{
122
+ success: boolean;
123
+ error?: string;
124
+ workspaces?: WorkspaceSummary[];
125
+ }>;
126
+ fetchAssigneeOptions: () => Promise<{
127
+ success: boolean;
128
+ error?: string;
129
+ }>;
130
+ switchWorkspace: (workspaceId: string, options?: {
131
+ hydrate?: boolean;
132
+ }) => Promise<{
133
+ success: boolean;
134
+ error?: string;
135
+ code?: string;
136
+ }>;
137
+ resolveWorkspaceAfterAuth: (options?: {
138
+ preferredWorkspaceId?: string | null;
139
+ }) => Promise<{
140
+ success: boolean;
141
+ workspaceSetupRequired: boolean;
142
+ workspaceId: string;
143
+ error?: undefined;
144
+ code?: undefined;
145
+ } | {
146
+ success: boolean;
147
+ workspaceSetupRequired: boolean;
148
+ error: string;
149
+ workspaceId?: undefined;
150
+ code?: undefined;
151
+ } | {
152
+ success: boolean;
153
+ workspaceSetupRequired: boolean;
154
+ workspaceId?: undefined;
155
+ error?: undefined;
156
+ code?: undefined;
157
+ } | {
158
+ success: boolean;
159
+ workspaceSetupRequired: boolean;
160
+ error: string;
161
+ code: string | undefined;
162
+ workspaceId?: undefined;
163
+ }>;
164
+ retryBootstrapChecks: () => Promise<void>;
165
+ };
166
+ export {};