@taskforcehq/taskforce 0.3.304 → 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.
- package/README.md +140 -247
- package/dist/TaskforceCore.js +165 -70
- package/dist/TaskforceCore.test.js +799 -90
- package/dist/compat/workspaceSyncCompat.js +6 -0
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +1 -1
- package/dist/components/features/DocumentViewer.js +32 -9
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +193 -6
- package/dist/components/features/TaskSettings.js +29 -3
- package/dist/components/features/TaskSettings.test.js +42 -1
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- package/dist/components/task/TaskForm.d.ts +8 -8
- package/dist/components/task/TaskForm.js +358 -221
- package/dist/components/task/TaskForm.test.js +456 -381
- package/dist/components/task/TaskKanban.js +2 -1
- package/dist/components/views/PlanComparisonPage.js +5 -8
- package/dist/components/views/PlanComparisonPage.test.js +51 -41
- package/dist/components/views/PlansPage.js +126 -84
- package/dist/components/views/PlansPage.test.js +308 -39
- package/dist/components/views/StandaloneLayout.js +28 -22
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +6 -1
- package/dist/core/AiProfileService.js +161 -16
- package/dist/core/AiProfileService.test.js +3 -1
- package/dist/core/AiProfiles.test.js +200 -0
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/EntitlementsPolicy.test.js +75 -13
- package/dist/core/PlanEntitlementService.d.ts +7 -1
- package/dist/core/PlanEntitlementService.js +117 -42
- package/dist/core/PlanVersionPolicy.test.js +19 -26
- package/dist/core/SignupMonetizationSettings.test.js +46 -21
- package/dist/core/SystemAdmin.test.js +212 -56
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +3 -0
- package/dist/core/Taskforce.js +50 -10
- package/dist/core/WorkspacePlanMode.test.js +12 -12
- package/dist/core/shared.d.ts +2 -0
- package/dist/core/shared.js +11 -0
- package/dist/hooks/sync/auth.js +3 -1
- package/dist/hooks/sync/transfers.d.ts +2 -1
- package/dist/hooks/sync/transfers.js +8 -4
- package/dist/hooks/useSyncOrchestrator.d.ts +7 -4
- package/dist/hooks/useSyncOrchestrator.js +75 -7
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +80 -10
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +12 -1
- package/dist/hooks/useTaskMutations.test.js +30 -1
- package/dist/hooks/useTaskforce.d.ts +5 -2
- package/dist/hooks/useTaskforce.js +17 -5
- package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +3 -1
- package/dist/hooks/useWorkspaceSyncController.js +11 -1
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/runtime.js +2362 -3530
- package/dist/mcp/runtime.test.js +159 -0
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- package/dist/server/routes/admin.d.ts +1 -5
- package/dist/server/routes/admin.js +138 -61
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +1 -4
- package/dist/server/routes/auth.js +23 -63
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +104 -188
- package/dist/server/routes/billing.test.js +214 -86
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +4 -2
- package/dist/server/routes/documents.js +73 -7
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.js +1 -1
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +1 -1
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +1 -1
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +76 -162
- package/dist/server/routes.test.js +672 -30
- package/dist/sync/contentSyncState.js +16 -7
- package/dist/sync/syncApplyHandlers.test.js +2 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +8 -0
- package/dist/sync/syncService.test.js +14 -0
- package/dist/sync/workspaceSyncModel.d.ts +3 -0
- package/dist/sync/workspaceSyncModel.js +12 -3
- package/dist/sync/workspaceSyncModel.test.js +34 -0
- package/dist/sync/workspaceSyncState.d.ts +2 -0
- package/dist/sync/workspaceSyncState.js +1 -0
- package/dist/ui/assets/{AgentsModule-BLWVbuKP.js → AgentsModule-N2MnQBZk.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
- package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-Dhm7M8e7.js} +1 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BcS4afRn.js} +1 -1
- package/dist/ui/assets/{index-Ii0B0rTO.css → index-C2-OXZV7.css} +1 -1
- package/dist/ui/assets/index-DcSI5F86.js +6 -0
- package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-BSUaRwF8.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +196 -0
- package/package.json +8 -1
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/playwright-auth.sh +5 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
- package/dist/ui/assets/index-DXUdtH1B.js +0 -6
|
@@ -614,19 +614,20 @@ describe('useSyncOrchestrator', () => {
|
|
|
614
614
|
});
|
|
615
615
|
expect(result.current.workspaceCloudSyncEnabled).toBe(true);
|
|
616
616
|
expect(result.current.workspaceSyncPhase).toBe('provision-local');
|
|
617
|
+
runWorkspacePushSyncServiceMock.mockClear();
|
|
617
618
|
await act(async () => {
|
|
618
619
|
await result.current.pushWorkspaceChangesToCloud('sig-push-retry');
|
|
619
620
|
});
|
|
620
|
-
expect(runWorkspacePushSyncServiceMock).
|
|
621
|
+
expect(runWorkspacePushSyncServiceMock.mock.calls.length).toBeGreaterThanOrEqual(1);
|
|
621
622
|
expect(result.current.isWorkspaceRetryPending()).toBe(true);
|
|
622
623
|
await act(async () => {
|
|
623
624
|
await result.current.pushWorkspaceChangesToCloud('sig-push-retry');
|
|
624
625
|
});
|
|
625
626
|
await waitFor(() => {
|
|
626
|
-
expect(runWorkspacePushSyncServiceMock).
|
|
627
|
+
expect(runWorkspacePushSyncServiceMock.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
627
628
|
});
|
|
628
629
|
expect(result.current.workspaceSyncPhase).toBe('active');
|
|
629
|
-
expect(result.current.workspaceLastPushAt).
|
|
630
|
+
expect(result.current.workspaceLastPushAt).toBeTruthy();
|
|
630
631
|
await act(async () => {
|
|
631
632
|
await Promise.resolve();
|
|
632
633
|
await Promise.resolve();
|
|
@@ -786,14 +787,19 @@ describe('useSyncOrchestrator', () => {
|
|
|
786
787
|
await Promise.resolve();
|
|
787
788
|
});
|
|
788
789
|
expect(runWorkspacePushSyncServiceMock.mock.calls.length).toBeGreaterThanOrEqual(1);
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
790
|
+
const pushedChanges = runWorkspacePushSyncServiceMock.mock.calls
|
|
791
|
+
.map((call) => call?.[0]?.payload?.changes)
|
|
792
|
+
.filter(Array.isArray);
|
|
793
|
+
expect(pushedChanges).toEqual(expect.arrayContaining([
|
|
794
|
+
expect.arrayContaining([
|
|
795
|
+
expect.objectContaining({
|
|
796
|
+
op: 'upsert',
|
|
797
|
+
archived: false,
|
|
798
|
+
task: expect.objectContaining({
|
|
799
|
+
id: 'task-local-1'
|
|
800
|
+
})
|
|
795
801
|
})
|
|
796
|
-
|
|
802
|
+
])
|
|
797
803
|
]));
|
|
798
804
|
unmount();
|
|
799
805
|
});
|
|
@@ -1418,6 +1424,70 @@ describe('useSyncOrchestrator', () => {
|
|
|
1418
1424
|
expect(result.current.workspaceLastPullAt).toBe('2026-03-15T21:05:00.000Z');
|
|
1419
1425
|
unmount();
|
|
1420
1426
|
});
|
|
1427
|
+
it('periodically retries a stale local push signature even without a new state change', async () => {
|
|
1428
|
+
vi.useFakeTimers();
|
|
1429
|
+
const localTask = {
|
|
1430
|
+
id: 'task-local-watchdog',
|
|
1431
|
+
title: 'Watchdog push recovery',
|
|
1432
|
+
status: 'task',
|
|
1433
|
+
priority: 2,
|
|
1434
|
+
complexity: 3,
|
|
1435
|
+
type: 'feature',
|
|
1436
|
+
category: 'general',
|
|
1437
|
+
createdAt: '2026-03-15T18:00:00.000Z',
|
|
1438
|
+
updatedAt: '2026-03-15T18:00:00.000Z',
|
|
1439
|
+
comments: [],
|
|
1440
|
+
attachments: [],
|
|
1441
|
+
checklistItems: [],
|
|
1442
|
+
taxonomies: {},
|
|
1443
|
+
assignee: 'unassigned',
|
|
1444
|
+
referenceNumber: null,
|
|
1445
|
+
localReferenceNumber: 18
|
|
1446
|
+
};
|
|
1447
|
+
const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
|
|
1448
|
+
initialProps: buildInput({ tasks: [localTask] })
|
|
1449
|
+
});
|
|
1450
|
+
act(() => {
|
|
1451
|
+
result.current.applyWorkspaceSyncStateSnapshot({
|
|
1452
|
+
enabled: true,
|
|
1453
|
+
phase: 'active',
|
|
1454
|
+
pullCursor: null
|
|
1455
|
+
});
|
|
1456
|
+
});
|
|
1457
|
+
await act(async () => {
|
|
1458
|
+
await result.current.pushWorkspaceChangesToCloud('sig-initial-watchdog');
|
|
1459
|
+
});
|
|
1460
|
+
expect(runWorkspacePushSyncServiceMock).toHaveBeenCalledTimes(1);
|
|
1461
|
+
runWorkspacePushSyncServiceMock.mockClear();
|
|
1462
|
+
act(() => {
|
|
1463
|
+
result.current.workspaceLastPushedSignatureRef.current = 'stale-signature';
|
|
1464
|
+
result.current.workspacePullInFlightRef.current = true;
|
|
1465
|
+
});
|
|
1466
|
+
await act(async () => {
|
|
1467
|
+
await vi.advanceTimersByTimeAsync(20_000);
|
|
1468
|
+
});
|
|
1469
|
+
expect(runWorkspacePushSyncServiceMock).not.toHaveBeenCalled();
|
|
1470
|
+
act(() => {
|
|
1471
|
+
result.current.workspacePullInFlightRef.current = false;
|
|
1472
|
+
});
|
|
1473
|
+
await act(async () => {
|
|
1474
|
+
await vi.advanceTimersByTimeAsync(12_000);
|
|
1475
|
+
});
|
|
1476
|
+
expect(runWorkspacePushSyncServiceMock).toHaveBeenCalledTimes(1);
|
|
1477
|
+
expect(runWorkspacePushSyncServiceMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
1478
|
+
payload: expect.objectContaining({
|
|
1479
|
+
changes: expect.arrayContaining([
|
|
1480
|
+
expect.objectContaining({
|
|
1481
|
+
op: 'upsert',
|
|
1482
|
+
task: expect.objectContaining({
|
|
1483
|
+
id: 'task-local-watchdog'
|
|
1484
|
+
})
|
|
1485
|
+
})
|
|
1486
|
+
])
|
|
1487
|
+
})
|
|
1488
|
+
}));
|
|
1489
|
+
unmount();
|
|
1490
|
+
});
|
|
1421
1491
|
it('forces a full ai-profile push during manual retry even when the workspace signature is unchanged', async () => {
|
|
1422
1492
|
vi.stubGlobal('fetch', vi.fn((url, init) => {
|
|
1423
1493
|
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
@@ -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,6 +1,6 @@
|
|
|
1
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);
|
|
@@ -90,9 +90,14 @@ export function useTaskMutations({ tasks, archivedTasks, editingTaskId, setTasks
|
|
|
90
90
|
}
|
|
91
91
|
if (deletedRecord) {
|
|
92
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
|
+
}
|
|
93
97
|
}
|
|
94
98
|
else {
|
|
95
99
|
await fetchDeletedTasks(true);
|
|
100
|
+
workspaceDeletedTaskWatermarksRef.current.set(id, new Date().toISOString());
|
|
96
101
|
}
|
|
97
102
|
workspaceDeletedTaskIdsRef.current.add(id);
|
|
98
103
|
if (cloudAuthConfigured && runtimeMode === 'local' && isAuthenticated && workspaceCloudSyncEnabled) {
|
|
@@ -133,7 +138,13 @@ export function useTaskMutations({ tasks, archivedTasks, editingTaskId, setTasks
|
|
|
133
138
|
await fetchTasks(true);
|
|
134
139
|
}
|
|
135
140
|
setDeletedTasks((prev) => prev.filter((entry) => entry.id !== id && entry.taskId !== id));
|
|
141
|
+
const restoredTaskId = String(restoredTask?.id || '').trim();
|
|
136
142
|
workspaceDeletedTaskIdsRef.current.delete(id);
|
|
143
|
+
workspaceDeletedTaskWatermarksRef.current.delete(id);
|
|
144
|
+
if (restoredTaskId) {
|
|
145
|
+
workspaceDeletedTaskIdsRef.current.delete(restoredTaskId);
|
|
146
|
+
workspaceDeletedTaskWatermarksRef.current.delete(restoredTaskId);
|
|
147
|
+
}
|
|
137
148
|
queueWorkspaceSyncFromAuthoritativeTaskState();
|
|
138
149
|
setError('');
|
|
139
150
|
pushNotice('Task restored from Trash.', 'info');
|
|
@@ -38,6 +38,7 @@ describe('useTaskMutations', () => {
|
|
|
38
38
|
const pushWorkspaceChangesToCloud = vi.fn();
|
|
39
39
|
const workspacePendingSignatureRef = { current: null };
|
|
40
40
|
const workspaceDeletedTaskIdsRef = { current: new Set() };
|
|
41
|
+
const workspaceDeletedTaskWatermarksRef = { current: new Map() };
|
|
41
42
|
const hook = renderHook(() => useTaskMutations({
|
|
42
43
|
tasks: [baseTask],
|
|
43
44
|
archivedTasks: [],
|
|
@@ -61,6 +62,7 @@ describe('useTaskMutations', () => {
|
|
|
61
62
|
pushWorkspaceChangesToCloud,
|
|
62
63
|
workspacePendingSignatureRef,
|
|
63
64
|
workspaceDeletedTaskIdsRef,
|
|
65
|
+
workspaceDeletedTaskWatermarksRef,
|
|
64
66
|
...overrides
|
|
65
67
|
}));
|
|
66
68
|
return {
|
|
@@ -80,7 +82,8 @@ describe('useTaskMutations', () => {
|
|
|
80
82
|
buildWorkspaceSyncSignature,
|
|
81
83
|
pushWorkspaceChangesToCloud,
|
|
82
84
|
workspacePendingSignatureRef,
|
|
83
|
-
workspaceDeletedTaskIdsRef
|
|
85
|
+
workspaceDeletedTaskIdsRef,
|
|
86
|
+
workspaceDeletedTaskWatermarksRef
|
|
84
87
|
}
|
|
85
88
|
};
|
|
86
89
|
};
|
|
@@ -117,6 +120,7 @@ describe('useTaskMutations', () => {
|
|
|
117
120
|
const pushWorkspaceChangesToCloud = vi.fn();
|
|
118
121
|
const workspacePendingSignatureRef = { current: null };
|
|
119
122
|
const workspaceDeletedTaskIdsRef = { current: new Set(['task-1']) };
|
|
123
|
+
const workspaceDeletedTaskWatermarksRef = { current: new Map([['task-1', '2026-04-03T10:10:00.000Z']]) };
|
|
120
124
|
const { result } = renderHook(() => useTaskMutations({
|
|
121
125
|
tasks: [],
|
|
122
126
|
archivedTasks: [],
|
|
@@ -140,6 +144,7 @@ describe('useTaskMutations', () => {
|
|
|
140
144
|
pushWorkspaceChangesToCloud,
|
|
141
145
|
workspacePendingSignatureRef,
|
|
142
146
|
workspaceDeletedTaskIdsRef,
|
|
147
|
+
workspaceDeletedTaskWatermarksRef,
|
|
143
148
|
}));
|
|
144
149
|
await act(async () => {
|
|
145
150
|
await result.current.handleRestoreDeletedTask('task-1');
|
|
@@ -147,11 +152,35 @@ describe('useTaskMutations', () => {
|
|
|
147
152
|
await waitFor(() => {
|
|
148
153
|
expect(workspaceDeletedTaskIdsRef.current.has('task-1')).toBe(false);
|
|
149
154
|
});
|
|
155
|
+
expect(workspaceDeletedTaskWatermarksRef.current.has('task-1')).toBe(false);
|
|
150
156
|
expect(mergeTaskFromServer).toHaveBeenCalledWith(expect.objectContaining({ id: 'task-1' }));
|
|
151
157
|
expect(buildWorkspaceSyncSignature).toHaveBeenCalled();
|
|
152
158
|
expect(pushWorkspaceChangesToCloud).toHaveBeenCalledWith('sig-1');
|
|
153
159
|
expect(setDeletedTasks).toHaveBeenCalled();
|
|
154
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
|
+
});
|
|
155
184
|
it('optimistically marks a task done before the server responds', async () => {
|
|
156
185
|
let resolveResponse;
|
|
157
186
|
const fetchMock = vi.mocked(fetch);
|
|
@@ -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;
|
|
@@ -280,6 +281,7 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
|
|
|
280
281
|
code?: string;
|
|
281
282
|
verificationToken?: string | null;
|
|
282
283
|
emailSent?: boolean;
|
|
284
|
+
deliveryAttempted?: boolean;
|
|
283
285
|
emailError?: string;
|
|
284
286
|
}>;
|
|
285
287
|
confirmEmailVerification: (token: string) => Promise<{
|
|
@@ -293,6 +295,7 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
|
|
|
293
295
|
code?: string;
|
|
294
296
|
resetToken?: string | null;
|
|
295
297
|
emailSent?: boolean;
|
|
298
|
+
deliveryAttempted?: boolean;
|
|
296
299
|
emailError?: string;
|
|
297
300
|
}>;
|
|
298
301
|
confirmPasswordReset: (token: string, password: string) => Promise<{
|
|
@@ -235,8 +235,13 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
235
235
|
|| path.startsWith('/api/taskforce/billing/')
|
|
236
236
|
|| path.startsWith('/api/taskforce/sync/')
|
|
237
237
|
|| path.startsWith('/api/taskforce/settings/mcp/');
|
|
238
|
-
if (!isCloudHost && isCloudScopedPath
|
|
239
|
-
|
|
238
|
+
if (!isCloudHost && isCloudScopedPath) {
|
|
239
|
+
if (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy) {
|
|
240
|
+
return path;
|
|
241
|
+
}
|
|
242
|
+
if (runtimeConfigOverride.workspaceMode === 'multi-cloud') {
|
|
243
|
+
return path;
|
|
244
|
+
}
|
|
240
245
|
}
|
|
241
246
|
const base = normalizedCloudAuthBaseUrl || normalizedApiBaseUrl;
|
|
242
247
|
if (base) {
|
|
@@ -258,7 +263,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
258
263
|
if (!isCloudHost && isCloudScopedPath)
|
|
259
264
|
return path;
|
|
260
265
|
return path;
|
|
261
|
-
}, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost]);
|
|
266
|
+
}, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, runtimeConfigOverride.workspaceMode, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost]);
|
|
262
267
|
const resolveWebSocketUrl = useCallback((path) => {
|
|
263
268
|
const wsPath = String(path || '').trim() || '/taskforce-ws';
|
|
264
269
|
const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`;
|
|
@@ -815,7 +820,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
815
820
|
const setUserGlobalSyncErrorRef = useRef(() => undefined);
|
|
816
821
|
const checkAuthSessionImplRef = useRef(async () => false);
|
|
817
822
|
const checkAuthSession = useCallback((options) => checkAuthSessionImplRef.current(options), []);
|
|
818
|
-
const { userGlobalSyncStatus, setUserGlobalSyncStatus, userGlobalSyncError, setUserGlobalSyncError, workspaceLastPullAt, workspaceLastPushAt, workspaceLastErrorAt, workspaceLastErrorMessage, workspaceLastSuccessfulSyncAt, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceSyncStatus, workspaceSyncSummary, workspaceSyncRecommendedAction, workspaceSyncBusy, workspaceSyncPendingChanges, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspacePullCursorRef, clearWorkspaceRetry, persistWorkspaceSyncPatch, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, syncUserGlobalSettings, buildWorkspaceSyncSignature, pushWorkspaceChangesToCloud, saveWorkspaceCloudSyncSettings, retryUserGlobalSettingsSync, retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull, getWorkspaceSyncDiagnostics } = useSyncOrchestrator({
|
|
823
|
+
const { userGlobalSyncStatus, setUserGlobalSyncStatus, userGlobalSyncError, setUserGlobalSyncError, workspaceLastPullAt, workspaceLastPushAt, workspaceLastErrorAt, workspaceLastErrorMessage, workspaceLastSuccessfulSyncAt, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceSyncSetupIntent, workspaceSyncStatus, workspaceSyncSummary, workspaceSyncRecommendedAction, workspaceSyncBusy, workspaceSyncPendingChanges, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, workspacePullCursorRef, clearWorkspaceRetry, persistWorkspaceSyncPatch, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, syncUserGlobalSettings, buildWorkspaceSyncSignature, pushWorkspaceChangesToCloud, saveWorkspaceCloudSyncSettings, retryUserGlobalSettingsSync, retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull, getWorkspaceSyncDiagnostics } = useSyncOrchestrator({
|
|
819
824
|
currentWorkspaceId,
|
|
820
825
|
cloudAuthConfigured,
|
|
821
826
|
runtimeMode,
|
|
@@ -828,6 +833,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
828
833
|
realtimeSyncEnabled,
|
|
829
834
|
tasks,
|
|
830
835
|
archivedTasks,
|
|
836
|
+
deletedTasks,
|
|
831
837
|
initiatives,
|
|
832
838
|
workstreams,
|
|
833
839
|
taxonomies,
|
|
@@ -1800,6 +1806,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1800
1806
|
success: true,
|
|
1801
1807
|
verificationToken: data?.verificationToken ?? null,
|
|
1802
1808
|
emailSent: data?.emailSent !== false,
|
|
1809
|
+
deliveryAttempted: data?.deliveryAttempted === true
|
|
1810
|
+
|| (typeof data?.verificationToken === 'string' && data.verificationToken.trim().length > 0),
|
|
1803
1811
|
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
1804
1812
|
};
|
|
1805
1813
|
}
|
|
@@ -1853,6 +1861,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1853
1861
|
success: true,
|
|
1854
1862
|
resetToken: data?.resetToken ?? null,
|
|
1855
1863
|
emailSent: data?.emailSent !== false,
|
|
1864
|
+
deliveryAttempted: data?.deliveryAttempted === true
|
|
1865
|
+
|| (typeof data?.resetToken === 'string' && data.resetToken.trim().length > 0),
|
|
1856
1866
|
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
1857
1867
|
};
|
|
1858
1868
|
}
|
|
@@ -1987,7 +1997,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1987
1997
|
return { success: false, error: data?.error || 'Invite join failed.', code: data?.code };
|
|
1988
1998
|
}
|
|
1989
1999
|
setError('');
|
|
1990
|
-
await checkAuthSession();
|
|
2000
|
+
await checkAuthSession({ force: true });
|
|
1991
2001
|
const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
|
|
1992
2002
|
const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
|
|
1993
2003
|
if (!workspaceResolution.success) {
|
|
@@ -2723,6 +2733,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2723
2733
|
pushWorkspaceChangesToCloud,
|
|
2724
2734
|
workspacePendingSignatureRef,
|
|
2725
2735
|
workspaceDeletedTaskIdsRef,
|
|
2736
|
+
workspaceDeletedTaskWatermarksRef,
|
|
2726
2737
|
});
|
|
2727
2738
|
const { autoSaveState, flushAutoSave, scheduleWarningPrompt, handleSubmit, confirmScheduleWarning, cancelScheduleWarning, handleAddComment } = useTaskFormActions({
|
|
2728
2739
|
activeCategories,
|
|
@@ -2992,6 +3003,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2992
3003
|
workspaceLastErrorMessage,
|
|
2993
3004
|
workspaceLastSuccessfulSyncAt,
|
|
2994
3005
|
workspaceSyncPhase,
|
|
3006
|
+
workspaceSyncSetupIntent,
|
|
2995
3007
|
workspaceSyncStatus,
|
|
2996
3008
|
workspaceSyncSummary,
|
|
2997
3009
|
workspaceSyncRecommendedAction,
|
|
@@ -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,6 +31,7 @@ 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?: {
|
|
@@ -59,6 +59,7 @@ function buildWorkspaceSyncStatePatch(current, patch) {
|
|
|
59
59
|
...patch,
|
|
60
60
|
version: 2,
|
|
61
61
|
phase: patch.phase || current.phase,
|
|
62
|
+
setupIntent: patch.setupIntent === undefined ? (current.setupIntent ?? null) : (patch.setupIntent ?? null),
|
|
62
63
|
pullCursor: patch.pullCursor === undefined ? current.pullCursor : normalizeCursor(patch.pullCursor),
|
|
63
64
|
lastPullAt: patch.lastPullAt === undefined ? current.lastPullAt : normalizeCursor(patch.lastPullAt),
|
|
64
65
|
lastPushAt: patch.lastPushAt === undefined ? current.lastPushAt : normalizeCursor(patch.lastPushAt),
|
|
@@ -67,12 +68,13 @@ function buildWorkspaceSyncStatePatch(current, patch) {
|
|
|
67
68
|
};
|
|
68
69
|
if (!next.enabled) {
|
|
69
70
|
next.phase = 'idle';
|
|
71
|
+
next.setupIntent = null;
|
|
70
72
|
next.pullCursor = null;
|
|
71
73
|
}
|
|
72
74
|
return next;
|
|
73
75
|
}
|
|
74
76
|
export function useWorkspaceSyncController(input) {
|
|
75
|
-
const { currentWorkspaceId, setWorkspaceCloudSyncEnabled, setWorkspaceSyncPhase, setWorkspaceLastPullAt, setWorkspaceLastPushAt, setWorkspaceLastErrorMessage } = input;
|
|
77
|
+
const { currentWorkspaceId, setWorkspaceCloudSyncEnabled, setWorkspaceSyncPhase, setWorkspaceSyncSetupIntent, setWorkspaceLastPullAt, setWorkspaceLastPushAt, setWorkspaceLastErrorMessage } = input;
|
|
76
78
|
const [workspaceRetryAt, setWorkspaceRetryAt] = useState(null);
|
|
77
79
|
const workspaceRetryTimerRef = useRef(null);
|
|
78
80
|
const workspaceRetryAtMsRef = useRef(null);
|
|
@@ -86,6 +88,7 @@ export function useWorkspaceSyncController(input) {
|
|
|
86
88
|
const workspacePendingSignatureRef = useRef('');
|
|
87
89
|
const workspaceLastPushedTaskIdsRef = useRef(new Set());
|
|
88
90
|
const workspaceDeletedTaskIdsRef = useRef(new Set());
|
|
91
|
+
const workspaceDeletedTaskWatermarksRef = useRef(new Map());
|
|
89
92
|
const workspacePullCursorRef = useRef(null);
|
|
90
93
|
const workspaceSyncStateRef = useRef(createDefaultWorkspaceSyncState());
|
|
91
94
|
const workspaceSyncLeaseOwnerIdRef = useRef('');
|
|
@@ -255,6 +258,7 @@ export function useWorkspaceSyncController(input) {
|
|
|
255
258
|
const next = buildWorkspaceSyncStatePatch(current, {
|
|
256
259
|
enabled: Boolean(nextState.enabled),
|
|
257
260
|
phase: nextState.phase || current.phase,
|
|
261
|
+
setupIntent: nextState.setupIntent,
|
|
258
262
|
pullCursor: nextState.pullCursor,
|
|
259
263
|
lastPullAt: nextState.lastPullAt,
|
|
260
264
|
lastPushAt: nextState.lastPushAt,
|
|
@@ -265,12 +269,14 @@ export function useWorkspaceSyncController(input) {
|
|
|
265
269
|
workspacePullCursorRef.current = next.pullCursor;
|
|
266
270
|
setWorkspaceCloudSyncEnabled(next.enabled);
|
|
267
271
|
setWorkspaceSyncPhase(next.phase);
|
|
272
|
+
setWorkspaceSyncSetupIntent(next.setupIntent ?? null);
|
|
268
273
|
setWorkspaceLastPullAt(next.lastPullAt);
|
|
269
274
|
setWorkspaceLastPushAt(next.lastPushAt);
|
|
270
275
|
setWorkspaceLastErrorMessage(next.lastErrorMessage ?? null);
|
|
271
276
|
}, [
|
|
272
277
|
setWorkspaceCloudSyncEnabled,
|
|
273
278
|
setWorkspaceSyncPhase,
|
|
279
|
+
setWorkspaceSyncSetupIntent,
|
|
274
280
|
setWorkspaceLastPullAt,
|
|
275
281
|
setWorkspaceLastPushAt,
|
|
276
282
|
setWorkspaceLastErrorMessage
|
|
@@ -343,6 +349,7 @@ export function useWorkspaceSyncController(input) {
|
|
|
343
349
|
await persistWorkspaceSyncPatch({
|
|
344
350
|
enabled: false,
|
|
345
351
|
phase: 'idle',
|
|
352
|
+
setupIntent: null,
|
|
346
353
|
pullCursor: null
|
|
347
354
|
});
|
|
348
355
|
return { success: true };
|
|
@@ -360,6 +367,7 @@ export function useWorkspaceSyncController(input) {
|
|
|
360
367
|
await persistWorkspaceSyncPatch({
|
|
361
368
|
enabled: true,
|
|
362
369
|
phase: ensured.provisioned ? 'provision-local' : 'attach-cloud',
|
|
370
|
+
setupIntent: null,
|
|
363
371
|
pullCursor: null,
|
|
364
372
|
lastPullAt: null,
|
|
365
373
|
lastPushAt: null,
|
|
@@ -376,6 +384,7 @@ export function useWorkspaceSyncController(input) {
|
|
|
376
384
|
workspacePendingSignatureRef.current = '';
|
|
377
385
|
workspaceLastPushedTaskIdsRef.current = new Set();
|
|
378
386
|
workspaceDeletedTaskIdsRef.current = new Set();
|
|
387
|
+
workspaceDeletedTaskWatermarksRef.current = new Map();
|
|
379
388
|
workspacePullCursorRef.current = null;
|
|
380
389
|
workspaceSyncStateRef.current = createDefaultWorkspaceSyncState();
|
|
381
390
|
workspacePushInFlightRef.current = false;
|
|
@@ -413,6 +422,7 @@ export function useWorkspaceSyncController(input) {
|
|
|
413
422
|
workspacePendingSignatureRef,
|
|
414
423
|
workspaceLastPushedTaskIdsRef,
|
|
415
424
|
workspaceDeletedTaskIdsRef,
|
|
425
|
+
workspaceDeletedTaskWatermarksRef,
|
|
416
426
|
workspacePullCursorRef,
|
|
417
427
|
clearWorkspaceRetry,
|
|
418
428
|
scheduleWorkspaceRetry,
|
|
@@ -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(),
|