@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
@@ -24,8 +24,9 @@ export interface UseTaskMutationsProps {
24
24
  pushWorkspaceChangesToCloud: (sig: string) => void;
25
25
  workspacePendingSignatureRef: MutableRefObject<string | null>;
26
26
  workspaceDeletedTaskIdsRef: MutableRefObject<Set<string>>;
27
+ workspaceDeletedTaskWatermarksRef: MutableRefObject<Map<string, string>>;
27
28
  }
28
- export declare function useTaskMutations({ tasks, archivedTasks, editingTaskId, setTasks, setArchivedTasks, setDeletedTasks, setError, resetForm, setActiveTab, fetchTasks, fetchArchive, fetchDeletedTasks, mergeTaskFromServer, pushNotice, cloudAuthConfigured, runtimeMode, isAuthenticated, workspaceCloudSyncEnabled, buildWorkspaceSyncSignature, pushWorkspaceChangesToCloud, workspacePendingSignatureRef, workspaceDeletedTaskIdsRef, }: UseTaskMutationsProps): {
29
+ export declare function useTaskMutations({ tasks, archivedTasks, editingTaskId, setTasks, setArchivedTasks, setDeletedTasks, setError, resetForm, setActiveTab, fetchTasks, fetchArchive, fetchDeletedTasks, mergeTaskFromServer, pushNotice, cloudAuthConfigured, runtimeMode, isAuthenticated, workspaceCloudSyncEnabled, buildWorkspaceSyncSignature, pushWorkspaceChangesToCloud, workspacePendingSignatureRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, }: UseTaskMutationsProps): {
29
30
  copiedId: string | null;
30
31
  handleDelete: (id: string, archive?: boolean, options?: {
31
32
  skipConfirm?: boolean;
@@ -1,21 +1,46 @@
1
- import { useState, useCallback, useRef } from 'react';
1
+ import { useState, useCallback, useEffect, useRef } from 'react';
2
2
  import { normalizeTaskFromApi } from '../utils/taskNormalization.js';
3
- export function useTaskMutations({ tasks, archivedTasks, editingTaskId, setTasks, setArchivedTasks, setDeletedTasks, setError, resetForm, setActiveTab, fetchTasks, fetchArchive, fetchDeletedTasks, mergeTaskFromServer, pushNotice, cloudAuthConfigured, runtimeMode, isAuthenticated, workspaceCloudSyncEnabled, buildWorkspaceSyncSignature, pushWorkspaceChangesToCloud, workspacePendingSignatureRef, workspaceDeletedTaskIdsRef, }) {
3
+ export function useTaskMutations({ tasks, archivedTasks, editingTaskId, setTasks, setArchivedTasks, setDeletedTasks, setError, resetForm, setActiveTab, fetchTasks, fetchArchive, fetchDeletedTasks, mergeTaskFromServer, pushNotice, cloudAuthConfigured, runtimeMode, isAuthenticated, workspaceCloudSyncEnabled, buildWorkspaceSyncSignature, pushWorkspaceChangesToCloud, workspacePendingSignatureRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, }) {
4
4
  // Copy ID state + timer ref
5
5
  const [copiedId, setCopiedId] = useState(null);
6
6
  const copiedIdTimerRef = useRef(null);
7
+ const workspaceSyncTimerRef = useRef(null);
8
+ const workspaceSyncTargetAtRef = useRef(null);
9
+ useEffect(() => () => {
10
+ if (typeof window === 'undefined')
11
+ return;
12
+ if (workspaceSyncTimerRef.current !== null) {
13
+ window.clearTimeout(workspaceSyncTimerRef.current);
14
+ workspaceSyncTimerRef.current = null;
15
+ }
16
+ workspaceSyncTargetAtRef.current = null;
17
+ }, []);
7
18
  const queueWorkspaceSyncFromAuthoritativeTaskState = useCallback((delayMs = 0) => {
8
19
  if (!(cloudAuthConfigured && runtimeMode === 'local' && isAuthenticated && workspaceCloudSyncEnabled))
9
20
  return;
10
21
  if (typeof window === 'undefined')
11
22
  return;
12
- window.setTimeout(() => {
23
+ const normalizedDelayMs = Math.max(0, delayMs);
24
+ const requestedTargetAt = Date.now() + normalizedDelayMs;
25
+ const existingTargetAt = workspaceSyncTargetAtRef.current;
26
+ if (workspaceSyncTimerRef.current !== null) {
27
+ if (normalizedDelayMs <= 0)
28
+ return;
29
+ if (existingTargetAt !== null && existingTargetAt >= requestedTargetAt)
30
+ return;
31
+ window.clearTimeout(workspaceSyncTimerRef.current);
32
+ workspaceSyncTimerRef.current = null;
33
+ }
34
+ workspaceSyncTargetAtRef.current = requestedTargetAt;
35
+ workspaceSyncTimerRef.current = window.setTimeout(() => {
36
+ workspaceSyncTimerRef.current = null;
37
+ workspaceSyncTargetAtRef.current = null;
13
38
  const signature = buildWorkspaceSyncSignature();
14
39
  if (!signature)
15
40
  return;
16
41
  workspacePendingSignatureRef.current = signature;
17
42
  void pushWorkspaceChangesToCloud(signature);
18
- }, Math.max(0, delayMs));
43
+ }, normalizedDelayMs);
19
44
  }, [
20
45
  cloudAuthConfigured,
21
46
  runtimeMode,
@@ -65,9 +90,14 @@ export function useTaskMutations({ tasks, archivedTasks, editingTaskId, setTasks
65
90
  }
66
91
  if (deletedRecord) {
67
92
  setDeletedTasks(prev => [deletedRecord, ...prev.filter((entry) => entry.taskId !== id)]);
93
+ const deletedAt = String(deletedRecord.deletedAt || '').trim();
94
+ if (Number.isFinite(Date.parse(deletedAt))) {
95
+ workspaceDeletedTaskWatermarksRef.current.set(id, deletedAt);
96
+ }
68
97
  }
69
98
  else {
70
99
  await fetchDeletedTasks(true);
100
+ workspaceDeletedTaskWatermarksRef.current.set(id, new Date().toISOString());
71
101
  }
72
102
  workspaceDeletedTaskIdsRef.current.add(id);
73
103
  if (cloudAuthConfigured && runtimeMode === 'local' && isAuthenticated && workspaceCloudSyncEnabled) {
@@ -108,7 +138,13 @@ export function useTaskMutations({ tasks, archivedTasks, editingTaskId, setTasks
108
138
  await fetchTasks(true);
109
139
  }
110
140
  setDeletedTasks((prev) => prev.filter((entry) => entry.id !== id && entry.taskId !== id));
141
+ const restoredTaskId = String(restoredTask?.id || '').trim();
111
142
  workspaceDeletedTaskIdsRef.current.delete(id);
143
+ workspaceDeletedTaskWatermarksRef.current.delete(id);
144
+ if (restoredTaskId) {
145
+ workspaceDeletedTaskIdsRef.current.delete(restoredTaskId);
146
+ workspaceDeletedTaskWatermarksRef.current.delete(restoredTaskId);
147
+ }
112
148
  queueWorkspaceSyncFromAuthoritativeTaskState();
113
149
  setError('');
114
150
  pushNotice('Task restored from Trash.', 'info');
@@ -227,8 +263,20 @@ export function useTaskMutations({ tasks, archivedTasks, editingTaskId, setTasks
227
263
  }, [tasks, archivedTasks, pushNotice, mergeTaskFromServer, queueWorkspaceSyncFromAuthoritativeTaskState]);
228
264
  const handleToggleComplete = async (task) => {
229
265
  const newStatus = task.status === 'done' ? 'task' : 'done';
266
+ const completedAt = newStatus === 'done' ? new Date().toISOString() : undefined;
267
+ const optimisticUpdates = {
268
+ status: newStatus,
269
+ completedAt,
270
+ completed: newStatus === 'done',
271
+ inProgress: false,
272
+ readyForReview: false,
273
+ cancelled: false
274
+ };
275
+ const previousTasks = tasks;
276
+ const previousArchivedTasks = archivedTasks;
277
+ setTasks(prev => prev.map(t => t.id === task.id ? { ...t, ...optimisticUpdates } : t));
278
+ setArchivedTasks(prev => prev.map(t => t.id === task.id ? { ...t, ...optimisticUpdates } : t));
230
279
  try {
231
- const completedAt = newStatus === 'done' ? new Date().toISOString() : undefined;
232
280
  const res = await fetch(`/api/taskforce/task/${task.id}`, {
233
281
  method: 'PATCH',
234
282
  headers: { 'Content-Type': 'application/json' },
@@ -239,7 +287,8 @@ export function useTaskMutations({ tasks, archivedTasks, editingTaskId, setTasks
239
287
  const message = data.error || 'Failed to update task status.';
240
288
  setError(message);
241
289
  pushNotice(message, 'error');
242
- await fetchTasks(true);
290
+ setTasks(previousTasks);
291
+ setArchivedTasks(previousArchivedTasks);
243
292
  return;
244
293
  }
245
294
  const updatedTask = await res.json().catch(() => null);
@@ -257,7 +306,8 @@ export function useTaskMutations({ tasks, archivedTasks, editingTaskId, setTasks
257
306
  console.error("Failed to toggle complete", err);
258
307
  setError('Failed to update task status.');
259
308
  pushNotice('Failed to update task status.', 'error');
260
- fetchTasks();
309
+ setTasks(previousTasks);
310
+ setArchivedTasks(previousArchivedTasks);
261
311
  }
262
312
  };
263
313
  const handleToggleCancel = async (task) => {
@@ -6,9 +6,87 @@ describe('useTaskMutations', () => {
6
6
  vi.stubGlobal('fetch', vi.fn());
7
7
  });
8
8
  afterEach(() => {
9
+ vi.useRealTimers();
9
10
  vi.unstubAllGlobals();
10
11
  vi.clearAllMocks();
11
12
  });
13
+ const baseTask = {
14
+ id: 'task-1',
15
+ title: 'Test task',
16
+ description: '',
17
+ category: 'general',
18
+ type: 'task',
19
+ priority: 2,
20
+ complexity: 3,
21
+ status: 'task',
22
+ createdAt: '2026-04-03T10:00:00.000Z',
23
+ updatedAt: '2026-04-03T10:05:00.000Z'
24
+ };
25
+ const buildHook = (overrides = {}) => {
26
+ const setTasks = vi.fn();
27
+ const setArchivedTasks = vi.fn();
28
+ const setDeletedTasks = vi.fn();
29
+ const setError = vi.fn();
30
+ const resetForm = vi.fn();
31
+ const setActiveTab = vi.fn();
32
+ const fetchTasks = vi.fn();
33
+ const fetchArchive = vi.fn();
34
+ const fetchDeletedTasks = vi.fn();
35
+ const mergeTaskFromServer = vi.fn();
36
+ const pushNotice = vi.fn();
37
+ const buildWorkspaceSyncSignature = vi.fn(() => 'sig-1');
38
+ const pushWorkspaceChangesToCloud = vi.fn();
39
+ const workspacePendingSignatureRef = { current: null };
40
+ const workspaceDeletedTaskIdsRef = { current: new Set() };
41
+ const workspaceDeletedTaskWatermarksRef = { current: new Map() };
42
+ const hook = renderHook(() => useTaskMutations({
43
+ tasks: [baseTask],
44
+ archivedTasks: [],
45
+ editingTaskId: null,
46
+ setTasks,
47
+ setArchivedTasks,
48
+ setDeletedTasks,
49
+ setError,
50
+ resetForm,
51
+ setActiveTab,
52
+ fetchTasks,
53
+ fetchArchive,
54
+ fetchDeletedTasks,
55
+ mergeTaskFromServer,
56
+ pushNotice,
57
+ cloudAuthConfigured: true,
58
+ runtimeMode: 'local',
59
+ isAuthenticated: true,
60
+ workspaceCloudSyncEnabled: true,
61
+ buildWorkspaceSyncSignature,
62
+ pushWorkspaceChangesToCloud,
63
+ workspacePendingSignatureRef,
64
+ workspaceDeletedTaskIdsRef,
65
+ workspaceDeletedTaskWatermarksRef,
66
+ ...overrides
67
+ }));
68
+ return {
69
+ ...hook,
70
+ deps: {
71
+ setTasks,
72
+ setArchivedTasks,
73
+ setDeletedTasks,
74
+ setError,
75
+ resetForm,
76
+ setActiveTab,
77
+ fetchTasks,
78
+ fetchArchive,
79
+ fetchDeletedTasks,
80
+ mergeTaskFromServer,
81
+ pushNotice,
82
+ buildWorkspaceSyncSignature,
83
+ pushWorkspaceChangesToCloud,
84
+ workspacePendingSignatureRef,
85
+ workspaceDeletedTaskIdsRef,
86
+ workspaceDeletedTaskWatermarksRef
87
+ }
88
+ };
89
+ };
12
90
  it('clears pending delete ids when a deleted task is restored', async () => {
13
91
  const fetchMock = vi.mocked(fetch);
14
92
  fetchMock.mockResolvedValue({
@@ -42,6 +120,7 @@ describe('useTaskMutations', () => {
42
120
  const pushWorkspaceChangesToCloud = vi.fn();
43
121
  const workspacePendingSignatureRef = { current: null };
44
122
  const workspaceDeletedTaskIdsRef = { current: new Set(['task-1']) };
123
+ const workspaceDeletedTaskWatermarksRef = { current: new Map([['task-1', '2026-04-03T10:10:00.000Z']]) };
45
124
  const { result } = renderHook(() => useTaskMutations({
46
125
  tasks: [],
47
126
  archivedTasks: [],
@@ -65,6 +144,7 @@ describe('useTaskMutations', () => {
65
144
  pushWorkspaceChangesToCloud,
66
145
  workspacePendingSignatureRef,
67
146
  workspaceDeletedTaskIdsRef,
147
+ workspaceDeletedTaskWatermarksRef,
68
148
  }));
69
149
  await act(async () => {
70
150
  await result.current.handleRestoreDeletedTask('task-1');
@@ -72,9 +152,105 @@ describe('useTaskMutations', () => {
72
152
  await waitFor(() => {
73
153
  expect(workspaceDeletedTaskIdsRef.current.has('task-1')).toBe(false);
74
154
  });
155
+ expect(workspaceDeletedTaskWatermarksRef.current.has('task-1')).toBe(false);
75
156
  expect(mergeTaskFromServer).toHaveBeenCalledWith(expect.objectContaining({ id: 'task-1' }));
76
157
  expect(buildWorkspaceSyncSignature).toHaveBeenCalled();
77
158
  expect(pushWorkspaceChangesToCloud).toHaveBeenCalledWith('sig-1');
78
159
  expect(setDeletedTasks).toHaveBeenCalled();
79
160
  });
161
+ it('records the delete watermark immediately when a task delete succeeds', async () => {
162
+ const fetchMock = vi.mocked(fetch);
163
+ fetchMock.mockResolvedValue({
164
+ ok: true,
165
+ status: 200,
166
+ json: async () => ({
167
+ deleted: {
168
+ id: 'deleted-task-1',
169
+ taskId: 'task-1',
170
+ deletedAt: '2026-04-03T10:10:00.000Z',
171
+ taskSnapshot: baseTask
172
+ }
173
+ })
174
+ });
175
+ const { result, deps } = buildHook();
176
+ await act(async () => {
177
+ await result.current.handleDelete('task-1', false, { skipConfirm: true });
178
+ });
179
+ expect(deps.workspaceDeletedTaskIdsRef.current.has('task-1')).toBe(true);
180
+ expect(deps.workspaceDeletedTaskWatermarksRef.current.get('task-1')).toBe('2026-04-03T10:10:00.000Z');
181
+ expect(deps.buildWorkspaceSyncSignature).toHaveBeenCalled();
182
+ expect(deps.pushWorkspaceChangesToCloud).toHaveBeenCalledWith('sig-1');
183
+ });
184
+ it('optimistically marks a task done before the server responds', async () => {
185
+ let resolveResponse;
186
+ const fetchMock = vi.mocked(fetch);
187
+ fetchMock.mockImplementation(() => new Promise((resolve) => {
188
+ resolveResponse = resolve;
189
+ }));
190
+ const { result, deps } = buildHook();
191
+ let togglePromise = null;
192
+ await act(async () => {
193
+ togglePromise = result.current.handleToggleComplete(baseTask);
194
+ });
195
+ expect(deps.setTasks).toHaveBeenCalledTimes(1);
196
+ const optimisticUpdater = deps.setTasks.mock.calls[0]?.[0];
197
+ const optimisticTasks = optimisticUpdater([baseTask]);
198
+ expect(optimisticTasks[0]?.status).toBe('done');
199
+ expect(optimisticTasks[0]?.completed).toBe(true);
200
+ resolveResponse({
201
+ ok: true,
202
+ status: 200,
203
+ json: async () => ({ ...baseTask, status: 'done', completedAt: '2026-04-14T12:00:00.000Z' })
204
+ });
205
+ await act(async () => {
206
+ await togglePromise;
207
+ });
208
+ expect(deps.mergeTaskFromServer).toHaveBeenCalledWith(expect.objectContaining({ id: 'task-1', status: 'done' }));
209
+ });
210
+ it('rolls back the optimistic done state when the server update fails', async () => {
211
+ const fetchMock = vi.mocked(fetch);
212
+ fetchMock.mockResolvedValue({
213
+ ok: false,
214
+ status: 500,
215
+ json: async () => ({ error: 'Server exploded' })
216
+ });
217
+ const { result, deps } = buildHook();
218
+ await act(async () => {
219
+ await result.current.handleToggleComplete(baseTask);
220
+ });
221
+ expect(deps.setTasks).toHaveBeenCalledTimes(2);
222
+ expect(deps.setTasks.mock.calls[1]?.[0]).toEqual([baseTask]);
223
+ expect(deps.setArchivedTasks.mock.calls[1]?.[0]).toEqual([]);
224
+ expect(deps.pushNotice).toHaveBeenCalledWith('Server exploded', 'error');
225
+ expect(deps.mergeTaskFromServer).not.toHaveBeenCalled();
226
+ });
227
+ it('coalesces pending workspace sync pushes and preserves attachment delay', async () => {
228
+ vi.useFakeTimers();
229
+ const fetchMock = vi.mocked(fetch);
230
+ fetchMock.mockResolvedValue({
231
+ ok: true,
232
+ status: 200,
233
+ json: async () => ({ ...baseTask, updatedAt: '2026-04-14T12:00:00.000Z' })
234
+ });
235
+ const { result, deps } = buildHook();
236
+ await act(async () => {
237
+ await result.current.handleUpdateTask(baseTask.id, { description: 'First update' });
238
+ });
239
+ await act(async () => {
240
+ await result.current.handleUpdateTask(baseTask.id, {
241
+ attachments: [{ id: 'asset-1', path: 'workspaces/workspace-local-active/assets/file.txt' }]
242
+ });
243
+ });
244
+ await act(async () => {
245
+ await vi.advanceTimersByTimeAsync(149);
246
+ });
247
+ expect(deps.pushWorkspaceChangesToCloud).not.toHaveBeenCalled();
248
+ await act(async () => {
249
+ await vi.advanceTimersByTimeAsync(1);
250
+ });
251
+ expect(deps.buildWorkspaceSyncSignature).toHaveBeenCalledTimes(1);
252
+ expect(deps.workspacePendingSignatureRef.current).toBe('sig-1');
253
+ expect(deps.pushWorkspaceChangesToCloud).toHaveBeenCalledTimes(1);
254
+ expect(deps.pushWorkspaceChangesToCloud).toHaveBeenCalledWith('sig-1');
255
+ });
80
256
  });
@@ -122,7 +122,7 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
122
122
  authUserDisplayName: string;
123
123
  authUserAvatarUrl: string;
124
124
  authWorkspaceId: string;
125
- userGlobalSyncStatus: "error" | "idle" | "syncing" | "disconnected";
125
+ userGlobalSyncStatus: "error" | "idle" | "disconnected" | "syncing";
126
126
  workspaceLastPullAt: string | null;
127
127
  workspaceLastPushAt: string | null;
128
128
  workspaceLastErrorAt: string | null;
@@ -130,6 +130,7 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
130
130
  workspaceLastErrorMessage: string | null;
131
131
  workspaceLastSuccessfulSyncAt: string | null;
132
132
  workspaceSyncPhase: import("../sync/workspaceSyncState").WorkspaceSyncPhase;
133
+ workspaceSyncSetupIntent: import("../sync/workspaceSyncState").WorkspaceSyncSetupIntent;
133
134
  workspaceSyncStatus: import("./useSyncOrchestrator").WorkspaceSyncStatus;
134
135
  workspaceSyncSummary: string;
135
136
  workspaceSyncRecommendedAction: string;
@@ -180,10 +181,10 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
180
181
  success: boolean;
181
182
  error?: string;
182
183
  }>;
183
- applyWorkspaceSyncStateSnapshot: (nextState: Partial<import("../sync/workspaceSyncState").WorkspaceSyncStateV2> & {
184
+ applyWorkspaceSyncStateSnapshot: (nextState: Parameters<(nextState: Partial<import("../sync/workspaceSyncState").WorkspaceSyncStateV2> & {
184
185
  enabled: boolean;
185
186
  phase?: import("../sync/workspaceSyncState").WorkspaceSyncPhase | null;
186
- }) => void;
187
+ }) => void>[0]) => void;
187
188
  authSessionResolved: boolean;
188
189
  workspaceBootstrapPending: boolean;
189
190
  bootstrapPhase: BootstrapPhase;
@@ -193,6 +194,12 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
193
194
  runtimeCapabilities: RuntimeCapabilities | null;
194
195
  refreshSetupContext: () => Promise<void>;
195
196
  retryBootstrapChecks: () => Promise<void>;
197
+ continueAfterCommercialOnboarding: () => Promise<{
198
+ success: boolean;
199
+ destination: "app" | "setup" | "login";
200
+ error?: string;
201
+ code?: string;
202
+ }>;
196
203
  saveWorkspaceProfile: (input: {
197
204
  name: string;
198
205
  description?: string;
@@ -206,7 +213,7 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
206
213
  fetchWorkspaces: () => Promise<{
207
214
  success: boolean;
208
215
  error?: string;
209
- workspaces?: WorkspaceSummary[];
216
+ workspaces?: import("./workspace/useTaskforceWorkspaceBootstrap").WorkspaceSummary[];
210
217
  }>;
211
218
  createWorkspace: (name: string, description?: string) => Promise<{
212
219
  success: boolean;
@@ -274,6 +281,7 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
274
281
  code?: string;
275
282
  verificationToken?: string | null;
276
283
  emailSent?: boolean;
284
+ deliveryAttempted?: boolean;
277
285
  emailError?: string;
278
286
  }>;
279
287
  confirmEmailVerification: (token: string) => Promise<{
@@ -287,6 +295,7 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
287
295
  code?: string;
288
296
  resetToken?: string | null;
289
297
  emailSent?: boolean;
298
+ deliveryAttempted?: boolean;
290
299
  emailError?: string;
291
300
  }>;
292
301
  confirmPasswordReset: (token: string, password: string) => Promise<{