@taskforcehq/taskforce 0.3.313 → 0.3.315

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 (51) hide show
  1. package/dist/compat/workspaceSyncCompat.js +2 -0
  2. package/dist/components/features/AnnotatedAttachmentWorkspace.js +51 -11
  3. package/dist/core/PlanEntitlementService.d.ts +2 -0
  4. package/dist/core/PlanEntitlementService.js +26 -9
  5. package/dist/core/Taskforce.d.ts +5 -0
  6. package/dist/core/Taskforce.js +16 -2
  7. package/dist/core/types.d.ts +1 -0
  8. package/dist/hooks/sync/recovery.d.ts +1 -0
  9. package/dist/hooks/sync/recovery.js +1 -0
  10. package/dist/hooks/sync/transfers.d.ts +2 -0
  11. package/dist/hooks/sync/transfers.js +3 -0
  12. package/dist/hooks/useSyncOrchestrator.js +9 -1
  13. package/dist/hooks/useTaskforce.js +13 -6
  14. package/dist/hooks/useWorkspaceSyncController.js +3 -0
  15. package/dist/mcp/canonicalAssetHelpers.js +33 -19
  16. package/dist/mcp/documentAssetRegistrar.js +6 -6
  17. package/dist/mcp/runtime.js +83 -11
  18. package/dist/mcp/taskAttachmentHelpers.js +29 -7
  19. package/dist/migrations/taskSchemaMigrations.js +4 -0
  20. package/dist/server/index.js +1 -1
  21. package/dist/server/routes/admin.js +10 -0
  22. package/dist/server/routes/billing.js +3 -0
  23. package/dist/server/routes/documents.js +2 -1
  24. package/dist/server/routes.js +10 -6
  25. package/dist/storage/documentIntegrity.js +1 -6
  26. package/dist/storage/documentPurge.js +11 -12
  27. package/dist/sync/workspaceRepair.js +11 -9
  28. package/dist/sync/workspaceSyncState.d.ts +1 -0
  29. package/dist/sync/workspaceSyncState.js +1 -0
  30. package/dist/ui/.well-known/mcp-registry-auth +1 -0
  31. package/dist/ui/assets/{AgentsModule-BULqIo8e.js → AgentsModule-CNBWCIXk.js} +1 -1
  32. package/dist/ui/assets/AnnotatedAttachmentWorkspace-9qxB0e6A.js +3 -0
  33. package/dist/ui/assets/{ContextAttachmentManager-DoMe2OpY.js → ContextAttachmentManager-CRyuFYlg.js} +1 -1
  34. package/dist/ui/assets/{DocumentWorkspace-DumBWhvb.js → DocumentWorkspace-Dx7wb9NF.js} +1 -1
  35. package/dist/ui/assets/{EntityActivityTimeline-sHk8Nzbq.js → EntityActivityTimeline-Cmal2OrJ.js} +1 -1
  36. package/dist/ui/assets/{InitiativesModule-3X9UNzHh.js → InitiativesModule-wzbYPQQL.js} +1 -1
  37. package/dist/ui/assets/{PlansPage-C3A3EYaV.js → PlansPage-BAnSfbTf.js} +1 -1
  38. package/dist/ui/assets/{TaskContextUpload-C8G7pWkX.js → TaskContextUpload-DENyMyUk.js} +1 -1
  39. package/dist/ui/assets/{TaskSettings-DM1o9Wla.js → TaskSettings-DASuVwpY.js} +1 -1
  40. package/dist/ui/assets/{WorkflowsModule-CW0crzvD.js → WorkflowsModule-DJMEA_yt.js} +1 -1
  41. package/dist/ui/assets/documentReferences-BhNx80zO.js +1 -0
  42. package/dist/ui/assets/index-CWg2olz9.js +5 -0
  43. package/dist/ui/index.html +1 -1
  44. package/dist/utils/pathContainment.d.ts +7 -0
  45. package/dist/utils/pathContainment.js +53 -0
  46. package/dist/utils/pathSafety.d.ts +6 -0
  47. package/dist/utils/pathSafety.js +73 -0
  48. package/package.json +3 -1
  49. package/dist/ui/assets/AnnotatedAttachmentWorkspace-DHr4rHK9.js +0 -3
  50. package/dist/ui/assets/documentReferences-BZB3nMRb.js +0 -1
  51. package/dist/ui/assets/index-BWFQ9mGn.js +0 -5
@@ -93,6 +93,7 @@ export function normalizeWorkspaceSyncStateCompat(rawState) {
93
93
  enabled: Boolean(state.enabled ?? state.cloudSyncEnabled),
94
94
  phase: 'idle',
95
95
  setupIntent: normalizeSetupIntent(state.setupIntent),
96
+ startupFullReconcileCompletedAt: normalizeTimestamp(state.startupFullReconcileCompletedAt),
96
97
  pullCursor: normalizeTimestamp(state.pullCursor),
97
98
  lastPullAt: normalizeTimestamp(state.lastPullAt),
98
99
  lastPushAt: normalizeTimestamp(state.lastPushAt),
@@ -116,6 +117,7 @@ export function normalizeWorkspaceSyncStateCompat(rawState) {
116
117
  || Boolean(state.enabled ?? state.cloudSyncEnabled) !== normalized.enabled
117
118
  || String(state.phase || '').trim() !== normalized.phase
118
119
  || normalizeSetupIntent(state.setupIntent) !== normalized.setupIntent
120
+ || normalizeTimestamp(state.startupFullReconcileCompletedAt) !== normalized.startupFullReconcileCompletedAt
119
121
  || normalizeTimestamp(state.pullCursor) !== normalized.pullCursor
120
122
  || normalizeTimestamp(state.lastPullAt) !== normalized.lastPullAt
121
123
  || normalizeTimestamp(state.lastPushAt) !== normalized.lastPushAt
@@ -118,6 +118,14 @@ function isEditableKeyboardTarget(target) {
118
118
  return true;
119
119
  return tagName === 'input' || tagName === 'textarea' || tagName === 'select';
120
120
  }
121
+ function isInteractiveKeyboardTarget(target) {
122
+ if (!(target instanceof HTMLElement))
123
+ return false;
124
+ if (isEditableKeyboardTarget(target))
125
+ return true;
126
+ const tagName = target.tagName.toLowerCase();
127
+ return tagName === 'button' || tagName === 'a' || target.getAttribute('role') === 'button';
128
+ }
121
129
  function normalizeOrder(annotations) {
122
130
  return annotations.map((annotation, index) => ({
123
131
  ...annotation,
@@ -438,6 +446,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
438
446
  const [imageLoadError, setImageLoadError] = useState(false);
439
447
  const [zoomLevel, setZoomLevel] = useState(1);
440
448
  const [panMode, setPanMode] = useState(false);
449
+ const [spacePanActive, setSpacePanActive] = useState(false);
441
450
  const [pastingImage, setPastingImage] = useState(false);
442
451
  const [naturalImageSize, setNaturalImageSize] = useState({ width: 0, height: 0 });
443
452
  const overlayRef = useRef(null);
@@ -542,6 +551,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
542
551
  || canvasMediaSize.height > (scroller.clientHeight + 1)));
543
552
  }, [canvasMediaSize.height, canvasMediaSize.width, zoomLevel]);
544
553
  const zoomLabel = `${Math.round(zoomLevel * 100)}%`;
554
+ const isPanActive = canPan && (panMode || spacePanActive);
545
555
  const annotationOrderNumbers = useMemo(() => {
546
556
  const orderMap = new Map();
547
557
  draftAnnotations.forEach((annotation, index) => {
@@ -1232,6 +1242,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1232
1242
  setNaturalImageSize({ width: 0, height: 0 });
1233
1243
  setZoomLevel(1);
1234
1244
  setPanMode(false);
1245
+ setSpacePanActive(false);
1235
1246
  }, [activeTarget?.path]);
1236
1247
  useEffect(() => {
1237
1248
  if (!imageLoading)
@@ -1250,6 +1261,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1250
1261
  if (canPan)
1251
1262
  return;
1252
1263
  setPanMode(false);
1264
+ setSpacePanActive(false);
1253
1265
  }, [canPan]);
1254
1266
  useEffect(() => {
1255
1267
  setPayloadPreview(null);
@@ -1657,6 +1669,34 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1657
1669
  window.addEventListener('keydown', handleKeyDown);
1658
1670
  return () => window.removeEventListener('keydown', handleKeyDown);
1659
1671
  }, [deleteSelectedAnnotation, selectedAnnotationId]);
1672
+ useEffect(() => {
1673
+ const handleKeyDown = (event) => {
1674
+ if (event.code !== 'Space')
1675
+ return;
1676
+ if (!canPan)
1677
+ return;
1678
+ if (isInteractiveKeyboardTarget(event.target))
1679
+ return;
1680
+ event.preventDefault();
1681
+ setSpacePanActive(true);
1682
+ };
1683
+ const handleKeyUp = (event) => {
1684
+ if (event.code !== 'Space')
1685
+ return;
1686
+ setSpacePanActive(false);
1687
+ };
1688
+ const handleWindowBlur = () => {
1689
+ setSpacePanActive(false);
1690
+ };
1691
+ window.addEventListener('keydown', handleKeyDown);
1692
+ window.addEventListener('keyup', handleKeyUp);
1693
+ window.addEventListener('blur', handleWindowBlur);
1694
+ return () => {
1695
+ window.removeEventListener('keydown', handleKeyDown);
1696
+ window.removeEventListener('keyup', handleKeyUp);
1697
+ window.removeEventListener('blur', handleWindowBlur);
1698
+ };
1699
+ }, [canPan]);
1660
1700
  const readPoint = useCallback((event) => {
1661
1701
  const rect = overlayRef.current?.getBoundingClientRect();
1662
1702
  if (!rect || rect.width <= 0 || rect.height <= 0)
@@ -1667,7 +1707,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1667
1707
  };
1668
1708
  }, []);
1669
1709
  const handleCanvasPointerDown = useCallback((event) => {
1670
- if (panMode && canPan) {
1710
+ if (isPanActive) {
1671
1711
  const scroller = canvasScrollerRef.current;
1672
1712
  if (!scroller)
1673
1713
  return;
@@ -1709,7 +1749,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1709
1749
  setDraftAnnotations((current) => normalizeOrder([...current, annotation]));
1710
1750
  setSelectedAnnotationId(annotation.id);
1711
1751
  markDraftChanged(300);
1712
- }, [canPan, defaultMarkerColor, markDraftChanged, panMode, readPoint, selectedSessionId, toolMode]);
1752
+ }, [defaultMarkerColor, isPanActive, markDraftChanged, readPoint, selectedSessionId, toolMode]);
1713
1753
  const handleCanvasPointerUp = useCallback((event) => {
1714
1754
  if (panRef.current?.pointerId === event.pointerId) {
1715
1755
  event.currentTarget.releasePointerCapture?.(event.pointerId);
@@ -1726,7 +1766,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1726
1766
  setToolMode('select');
1727
1767
  }, [readPoint, toolMode]);
1728
1768
  const startDragAnnotation = useCallback((event, annotation) => {
1729
- if (toolMode !== 'select' || panMode)
1769
+ if (toolMode !== 'select' || isPanActive)
1730
1770
  return;
1731
1771
  const point = readPoint(event);
1732
1772
  if (!point)
@@ -1738,7 +1778,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1738
1778
  };
1739
1779
  setSelectedAnnotationId(annotation.id);
1740
1780
  event.stopPropagation();
1741
- }, [panMode, readPoint, toolMode]);
1781
+ }, [isPanActive, readPoint, toolMode]);
1742
1782
  const handleCanvasPointerMove = useCallback((event) => {
1743
1783
  if (panRef.current?.pointerId === event.pointerId) {
1744
1784
  const scroller = canvasScrollerRef.current;
@@ -1872,7 +1912,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1872
1912
  panRef.current = null;
1873
1913
  }, []);
1874
1914
  const startResizeBox = useCallback((event, annotation) => {
1875
- if (toolMode !== 'select' || panMode)
1915
+ if (toolMode !== 'select' || isPanActive)
1876
1916
  return;
1877
1917
  const point = readPoint(event);
1878
1918
  if (!point)
@@ -1884,9 +1924,9 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1884
1924
  };
1885
1925
  setSelectedAnnotationId(annotation.id);
1886
1926
  event.stopPropagation();
1887
- }, [panMode, readPoint, toolMode]);
1927
+ }, [isPanActive, readPoint, toolMode]);
1888
1928
  const startArrowHandleDrag = useCallback((event, annotation, endpoint) => {
1889
- if (toolMode !== 'select' || panMode)
1929
+ if (toolMode !== 'select' || isPanActive)
1890
1930
  return;
1891
1931
  const point = readPoint(event);
1892
1932
  if (!point)
@@ -1899,7 +1939,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1899
1939
  };
1900
1940
  setSelectedAnnotationId(annotation.id);
1901
1941
  event.stopPropagation();
1902
- }, [panMode, readPoint, toolMode]);
1942
+ }, [isPanActive, readPoint, toolMode]);
1903
1943
  const activeSessionTimestamp = selectedSession?.updatedAt ? formatTimestamp(selectedSession.updatedAt) : 'Not saved yet';
1904
1944
  return (_jsxs("div", { className: `${styles.shell} ${showImageTray ? styles.shellWithImageTray : ''} ${showImageTray && imageTrayOpen ? styles.shellImageTrayOpen : ''}`.trim(), children: [showImageTray ? (_jsxs("aside", { className: `${styles.imageTrayPanel} ${imageTrayOpen ? styles.imageTrayPanelOpen : ''}`.trim(), "aria-label": "Image tray", "aria-hidden": !imageTrayOpen, children: [_jsxs("div", { className: styles.imageTrayHeader, children: [_jsxs("span", { className: styles.imageTrayTitle, children: [_jsx(FolderOpen, { size: 14 }), "Images"] }), _jsx("button", { type: "button", className: "tf-control-icon", onClick: onCloseImageTray, title: "Collapse image tray", "aria-label": "Collapse image tray", children: _jsx(ChevronLeft, { size: 16 }) })] }), _jsxs("div", { className: styles.imageTraySearch, children: [_jsx(Search, { size: 12, className: styles.imageTraySearchIcon }), _jsx("input", { type: "text", placeholder: "Search images...", value: imageTraySearchQuery, onChange: (event) => setImageTraySearchQuery(event.target.value), className: styles.imageTraySearchInput })] }), _jsx("div", { className: `tf-scrollbar ${styles.imageTrayList}`, children: imageTrayLoading ? (_jsx("div", { className: styles.imageTrayState, children: "Loading images..." })) : imageTrayError ? (_jsx("div", { className: styles.imageTrayStateError, children: imageTrayError })) : filteredImageTrayItems.length === 0 ? (_jsx("div", { className: styles.imageTrayState, children: imageTraySearchQuery.trim() ? 'No images match your search.' : 'No images found.' })) : filteredImageTrayItems.map((item) => {
1905
1945
  const isSelected = activeTarget?.assetId === item.assetId;
@@ -1957,7 +1997,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1957
1997
  if (!selectedSession)
1958
1998
  return;
1959
1999
  restoreLastSavedDraft();
1960
- }, disabled: !selectedSession || !hasUnsavedChanges, "aria-label": "Reset unsaved changes", title: "Reset unsaved changes", children: _jsx(RotateCcw, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomOut, disabled: !activeTarget || imageLoading || zoomLevel <= 0.25, "aria-label": "Zoom out", children: _jsx(ZoomOut, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: resetZoom, disabled: !activeTarget || imageLoading || zoomLevel === 1, "aria-label": `Reset zoom to 100 percent (currently ${zoomLabel})`, title: `Reset zoom to 100% (currently ${zoomLabel})`, children: _jsx("span", { children: zoomLabel }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomIn, disabled: !activeTarget || imageLoading || zoomLevel >= 4, "aria-label": "Zoom in", children: _jsx(ZoomIn, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.toolBtn} ${styles.toolbarButton} ${panMode ? styles.toolBtnActive : ''}`, onClick: () => setPanMode((current) => !current), disabled: !activeTarget || imageLoading || !canPan, "aria-label": "Pan canvas", title: "Pan canvas", children: _jsx(Hand, { size: 18 }) })] }), _jsx("span", { className: styles.toolbarSeparator, "aria-hidden": "true" }), _jsxs("div", { className: styles.toolbarActions, children: [_jsx("div", { className: styles.toolbarSelectionActions, children: selectedAnnotation ? (_jsxs(_Fragment, { children: [_jsxs("div", { ref: colorPickerRef, className: styles.toolbarColorPicker, children: [_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton} ${styles.colorPickerButton}`, onClick: () => setColorPickerOpen((current) => !current), "aria-label": "Marker color", "aria-expanded": colorPickerOpen, title: "Marker color", children: _jsx("span", { className: styles.colorPickerSwatch, style: { backgroundColor: selectedMarkerColor }, "aria-hidden": "true" }) }), colorPickerOpen ? (_jsx("div", { className: styles.colorPickerPopover, role: "menu", "aria-label": "Marker color options", children: MARKER_COLOR_SWATCHES.map((color) => (_jsx("button", { type: "button", className: `${styles.colorOption} ${selectedMarkerColor === color ? styles.colorOptionActive : ''}`, style: { backgroundColor: color }, onClick: () => applyMarkerColor(color), "aria-label": `Use marker color ${color}`, "aria-pressed": selectedMarkerColor === color }, color))) })) : null] }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: deleteSelectedAnnotation, "aria-label": "Delete marker", title: "Delete selected marker", children: _jsx(Trash2, { size: 18 }) }), _jsx("div", { className: styles.toolbarGeometryFields, "aria-label": "Marker geometry percent controls", children: getGeometryFields(selectedAnnotation).map((field) => (_jsxs("label", { className: styles.toolbarGeometryField, children: [_jsx("span", { className: `tf-text-meta ${styles.toolbarGeometryLabel}`, children: field.label }), _jsx("input", { className: `tf-field-shell ${styles.toolbarGeometryInput}`, type: "number", min: 0, max: 100, step: 0.1, value: field.value, onChange: (event) => {
2000
+ }, disabled: !selectedSession || !hasUnsavedChanges, "aria-label": "Reset unsaved changes", title: "Reset unsaved changes", children: _jsx(RotateCcw, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomOut, disabled: !activeTarget || imageLoading || zoomLevel <= 0.25, "aria-label": "Zoom out", children: _jsx(ZoomOut, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: resetZoom, disabled: !activeTarget || imageLoading || zoomLevel === 1, "aria-label": `Reset zoom to 100 percent (currently ${zoomLabel})`, title: `Reset zoom to 100% (currently ${zoomLabel})`, children: _jsx("span", { children: zoomLabel }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomIn, disabled: !activeTarget || imageLoading || zoomLevel >= 4, "aria-label": "Zoom in", children: _jsx(ZoomIn, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.toolBtn} ${styles.toolbarButton} ${isPanActive ? styles.toolBtnActive : ''}`, onClick: () => setPanMode((current) => !current), disabled: !activeTarget || imageLoading || !canPan, "aria-label": "Pan canvas", title: "Pan canvas", children: _jsx(Hand, { size: 18 }) })] }), _jsx("span", { className: styles.toolbarSeparator, "aria-hidden": "true" }), _jsxs("div", { className: styles.toolbarActions, children: [_jsx("div", { className: styles.toolbarSelectionActions, children: selectedAnnotation ? (_jsxs(_Fragment, { children: [_jsxs("div", { ref: colorPickerRef, className: styles.toolbarColorPicker, children: [_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton} ${styles.colorPickerButton}`, onClick: () => setColorPickerOpen((current) => !current), "aria-label": "Marker color", "aria-expanded": colorPickerOpen, title: "Marker color", children: _jsx("span", { className: styles.colorPickerSwatch, style: { backgroundColor: selectedMarkerColor }, "aria-hidden": "true" }) }), colorPickerOpen ? (_jsx("div", { className: styles.colorPickerPopover, role: "menu", "aria-label": "Marker color options", children: MARKER_COLOR_SWATCHES.map((color) => (_jsx("button", { type: "button", className: `${styles.colorOption} ${selectedMarkerColor === color ? styles.colorOptionActive : ''}`, style: { backgroundColor: color }, onClick: () => applyMarkerColor(color), "aria-label": `Use marker color ${color}`, "aria-pressed": selectedMarkerColor === color }, color))) })) : null] }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: deleteSelectedAnnotation, "aria-label": "Delete marker", title: "Delete selected marker", children: _jsx(Trash2, { size: 18 }) }), _jsx("div", { className: styles.toolbarGeometryFields, "aria-label": "Marker geometry percent controls", children: getGeometryFields(selectedAnnotation).map((field) => (_jsxs("label", { className: styles.toolbarGeometryField, children: [_jsx("span", { className: `tf-text-meta ${styles.toolbarGeometryLabel}`, children: field.label }), _jsx("input", { className: `tf-field-shell ${styles.toolbarGeometryInput}`, type: "number", min: 0, max: 100, step: 0.1, value: field.value, onChange: (event) => {
1961
2001
  const nextValue = parsePercentField(event.target.value);
1962
2002
  if (nextValue === null)
1963
2003
  return;
@@ -1980,7 +2020,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1980
2020
  setNaturalImageSize({ width: 0, height: 0 });
1981
2021
  setImageLoading(false);
1982
2022
  setImageLoadError(true);
1983
- } }), !imageLoading && !imageLoadError && selectedSession ? (_jsxs("div", { ref: overlayRef, className: `${styles.overlay} ${toolMode === 'select' ? styles.overlaySelect : ''} ${panMode && canPan ? styles.overlayPan : ''}`, onPointerDown: handleCanvasPointerDown, onPointerMove: handleCanvasPointerMove, onPointerUp: (event) => {
2023
+ } }), !imageLoading && !imageLoadError && selectedSession ? (_jsxs("div", { ref: overlayRef, className: `${styles.overlay} ${toolMode === 'select' ? styles.overlaySelect : ''} ${isPanActive ? styles.overlayPan : ''}`, onPointerDown: handleCanvasPointerDown, onPointerMove: handleCanvasPointerMove, onPointerUp: (event) => {
1984
2024
  handleCanvasPointerUp(event);
1985
2025
  handleCanvasPointerEnd();
1986
2026
  }, "data-testid": "annotated-attachment-overlay", onPointerLeave: handleCanvasPointerLeave, onPointerCancel: handleCanvasPointerCancel, children: [_jsx("svg", { className: styles.overlaySvg, viewBox: arrowOverlayViewBox, preserveAspectRatio: "none", "aria-hidden": "true", children: draftAnnotations.filter((annotation) => annotation.kind === 'arrow').map((annotation) => {
@@ -2022,7 +2062,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
2022
2062
  ? 'Save failed. Retry now.'
2023
2063
  : saveState === 'saving' || saveState === 'pending'
2024
2064
  ? 'Saving…'
2025
- : 'Saved' }), error ? _jsx("span", { children: error }) : _jsx("span", { children: !selectedSession ? 'Create a session to begin placing markers on this image.' : panMode && canPan ? 'Drag on the image to pan.' : toolMode === 'select' ? 'Select a marker to edit it.' : `Click on the image to place a ${toolMode}.` }), activeTarget && !selectedSession && showCreateSessionPrompt ? (_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => void createSession(), disabled: saving || loading, children: saving ? 'Creating…' : 'Create one now' })) : null] })] }), _jsx("section", { className: `tf-surface-panel ${styles.panel} ${styles.detailPanel}`, children: selectedSession ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.panelHeader, children: [_jsxs("div", { className: styles.panelHeaderText, children: [_jsx("div", { className: `tf-heading-card ${styles.panelTitle}`, children: "Markers" }), _jsxs("div", { className: "tf-text-secondary", children: [draftAnnotations.length, " in this session"] })] }), _jsxs("div", { className: styles.sessionActions, children: [_jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => moveSelectedAnnotation('up'), disabled: !selectedAnnotation || selectedAnnotationIndex <= 0, "aria-label": "Move marker up", children: _jsx(ArrowUp, { size: 16 }) }), _jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => moveSelectedAnnotation('down'), disabled: !selectedAnnotation || selectedAnnotationIndex === -1 || selectedAnnotationIndex >= draftAnnotations.length - 1, "aria-label": "Move marker down", children: _jsx(ArrowDown, { size: 16 }) })] })] }), _jsx("div", { className: `tf-scrollbar ${styles.annotationList}`, children: draftAnnotations.length === 0 ? (_jsx("div", { className: styles.detailEmpty, children: _jsx("p", { className: "tf-empty-copy", children: "Add a marker on the image to begin." }) })) : draftAnnotations.map((annotation, index) => {
2065
+ : 'Saved' }), error ? _jsx("span", { children: error }) : _jsx("span", { children: !selectedSession ? 'Create a session to begin placing markers on this image.' : isPanActive ? 'Drag on the image to pan.' : toolMode === 'select' ? 'Select a marker to edit it.' : `Click on the image to place a ${toolMode}.` }), activeTarget && !selectedSession && showCreateSessionPrompt ? (_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => void createSession(), disabled: saving || loading, children: saving ? 'Creating…' : 'Create one now' })) : null] })] }), _jsx("section", { className: `tf-surface-panel ${styles.panel} ${styles.detailPanel}`, children: selectedSession ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.panelHeader, children: [_jsxs("div", { className: styles.panelHeaderText, children: [_jsx("div", { className: `tf-heading-card ${styles.panelTitle}`, children: "Markers" }), _jsxs("div", { className: "tf-text-secondary", children: [draftAnnotations.length, " in this session"] })] }), _jsxs("div", { className: styles.sessionActions, children: [_jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => moveSelectedAnnotation('up'), disabled: !selectedAnnotation || selectedAnnotationIndex <= 0, "aria-label": "Move marker up", children: _jsx(ArrowUp, { size: 16 }) }), _jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => moveSelectedAnnotation('down'), disabled: !selectedAnnotation || selectedAnnotationIndex === -1 || selectedAnnotationIndex >= draftAnnotations.length - 1, "aria-label": "Move marker down", children: _jsx(ArrowDown, { size: 16 }) })] })] }), _jsx("div", { className: `tf-scrollbar ${styles.annotationList}`, children: draftAnnotations.length === 0 ? (_jsx("div", { className: styles.detailEmpty, children: _jsx("p", { className: "tf-empty-copy", children: "Add a marker on the image to begin." }) })) : draftAnnotations.map((annotation, index) => {
2026
2066
  const AnnotationKindIcon = ANNOTATION_KIND_ICON[annotation.kind];
2027
2067
  const annotationKindLabel = ANNOTATION_KIND_LABELS[annotation.kind];
2028
2068
  const annotationSequenceLabel = `${annotationKindLabel} ${index + 1}`;
@@ -184,6 +184,7 @@ export declare class PlanEntitlementService {
184
184
  createCustomPlanFeatureCatalogEntry(input: {
185
185
  label: string;
186
186
  description?: string | null;
187
+ publicVisible?: boolean | null;
187
188
  publicLabel?: string | null;
188
189
  publicDescription?: string | null;
189
190
  publicDescriptionVisible?: boolean | null;
@@ -194,6 +195,7 @@ export declare class PlanEntitlementService {
194
195
  featureKey: string;
195
196
  label?: string | null;
196
197
  description?: string | null;
198
+ publicVisible?: boolean | null;
197
199
  publicLabel?: string | null;
198
200
  publicDescription?: string | null;
199
201
  publicDescriptionVisible?: boolean | null;
@@ -92,7 +92,7 @@ export class PlanEntitlementService {
92
92
  }
93
93
  listCustomPlanFeatureCatalogEntries() {
94
94
  const rows = this.db.prepare(`
95
- SELECT feature_key, label, description, public_label, public_description, public_description_visible, public_display_order
95
+ SELECT feature_key, label, description, public_visible, public_label, public_description, public_description_visible, public_display_order
96
96
  FROM plan_feature_catalog_overrides
97
97
  WHERE tenant_id = ? AND is_custom = 1
98
98
  ORDER BY updated_at ASC, created_at ASC, feature_key ASC
@@ -108,6 +108,7 @@ export class PlanEntitlementService {
108
108
  label,
109
109
  description: this.normalizeRequiredCatalogLabel(row.description),
110
110
  isCustom: true,
111
+ publicVisible: this.normalizeOptionalCatalogDescriptionVisible(row.public_visible),
111
112
  publicLabel: this.normalizeOptionalCatalogMarketingCopy(row.public_label),
112
113
  publicDescription: this.normalizeOptionalCatalogMarketingCopy(row.public_description),
113
114
  publicDescriptionVisible: this.normalizeOptionalCatalogDescriptionVisible(row.public_description_visible),
@@ -123,7 +124,7 @@ export class PlanEntitlementService {
123
124
  if (!featureKey)
124
125
  return null;
125
126
  const row = this.db.prepare(`
126
- SELECT feature_key, label, description, public_label, public_description, public_description_visible, public_display_order
127
+ SELECT feature_key, label, description, public_visible, public_label, public_description, public_description_visible, public_display_order
127
128
  FROM plan_feature_catalog_overrides
128
129
  WHERE tenant_id = ? AND feature_key = ? AND is_custom = 1
129
130
  LIMIT 1
@@ -138,6 +139,7 @@ export class PlanEntitlementService {
138
139
  label,
139
140
  description: this.normalizeRequiredCatalogLabel(row.description),
140
141
  isCustom: true,
142
+ publicVisible: this.normalizeOptionalCatalogDescriptionVisible(row.public_visible),
141
143
  publicLabel: this.normalizeOptionalCatalogMarketingCopy(row.public_label),
142
144
  publicDescription: this.normalizeOptionalCatalogMarketingCopy(row.public_description),
143
145
  publicDescriptionVisible: this.normalizeOptionalCatalogDescriptionVisible(row.public_description_visible),
@@ -148,7 +150,7 @@ export class PlanEntitlementService {
148
150
  }
149
151
  listPlanFeatureCatalogOverrideMap() {
150
152
  const rows = this.db.prepare(`
151
- SELECT feature_key, public_label, public_description, public_description_visible, public_display_order, is_custom
153
+ SELECT feature_key, public_visible, public_label, public_description, public_description_visible, public_display_order, is_custom
152
154
  FROM plan_feature_catalog_overrides
153
155
  WHERE tenant_id = ?
154
156
  `).all(this.tenantId);
@@ -164,6 +166,7 @@ export class PlanEntitlementService {
164
166
  continue;
165
167
  }
166
168
  overrides.set(featureKey, {
169
+ publicVisible: this.normalizeOptionalCatalogDescriptionVisible(row.public_visible),
167
170
  publicLabel: this.normalizeOptionalCatalogMarketingCopy(row.public_label),
168
171
  publicDescription: this.normalizeOptionalCatalogMarketingCopy(row.public_description),
169
172
  publicDescriptionVisible: this.normalizeOptionalCatalogDescriptionVisible(row.public_description_visible),
@@ -255,12 +258,17 @@ export class PlanEntitlementService {
255
258
  };
256
259
  }
257
260
  if (normalizedFeatureKey === AI_PROFILES_FEATURE_KEY) {
261
+ const limit = this.readPositiveIntegerConfig(config[AI_PROFILES_LIMIT_CONFIG_KEY], 1);
258
262
  if (access === 'limited') {
259
- const limit = this.readPositiveIntegerConfig(config[AI_PROFILES_LIMIT_CONFIG_KEY], 1);
260
263
  return {
261
264
  [AI_PROFILES_LIMIT_CONFIG_KEY]: limit ?? 1
262
265
  };
263
266
  }
267
+ if (limit !== null) {
268
+ return {
269
+ [AI_PROFILES_LIMIT_CONFIG_KEY]: limit
270
+ };
271
+ }
264
272
  return {};
265
273
  }
266
274
  return config && typeof config === 'object' ? { ...config } : {};
@@ -1650,6 +1658,7 @@ export class PlanEntitlementService {
1650
1658
  label: feature.label,
1651
1659
  description: feature.description,
1652
1660
  isCustom: false,
1661
+ publicVisible: overrides.get(feature.featureKey)?.publicVisible ?? feature.publicVisible ?? null,
1653
1662
  publicLabel: overrides.get(feature.featureKey)?.publicLabel ?? feature.publicLabel ?? null,
1654
1663
  publicDescription: overrides.get(feature.featureKey)?.publicDescription ?? feature.publicDescription ?? null,
1655
1664
  publicDescriptionVisible: overrides.get(feature.featureKey)?.publicDescriptionVisible ?? feature.publicDescriptionVisible ?? null,
@@ -1669,6 +1678,7 @@ export class PlanEntitlementService {
1669
1678
  const featureKey = this.generateUniqueCustomPlanFeatureKey(label);
1670
1679
  const publicLabel = this.normalizeOptionalCatalogMarketingCopy(input.publicLabel);
1671
1680
  const publicDescription = this.normalizeOptionalCatalogMarketingCopy(input.publicDescription);
1681
+ const publicVisible = this.normalizeOptionalCatalogDescriptionVisible(input.publicVisible);
1672
1682
  const publicDescriptionVisible = this.normalizeOptionalCatalogDescriptionVisible(input.publicDescriptionVisible);
1673
1683
  const publicDisplayOrder = this.normalizeOptionalCatalogDisplayOrder(input.publicDisplayOrder);
1674
1684
  const now = new Date().toISOString();
@@ -1679,6 +1689,7 @@ export class PlanEntitlementService {
1679
1689
  label,
1680
1690
  description,
1681
1691
  is_custom,
1692
+ public_visible,
1682
1693
  public_label,
1683
1694
  public_description,
1684
1695
  public_description_visible,
@@ -1686,13 +1697,14 @@ export class PlanEntitlementService {
1686
1697
  created_at,
1687
1698
  updated_at
1688
1699
  )
1689
- VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?)
1690
- `).run(this.tenantId, featureKey, label, description, publicLabel, publicDescription, publicDescriptionVisible === null ? null : (publicDescriptionVisible ? 1 : 0), publicDisplayOrder, now, now);
1700
+ VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)
1701
+ `).run(this.tenantId, featureKey, label, description, publicVisible === null ? null : (publicVisible ? 1 : 0), publicLabel, publicDescription, publicDescriptionVisible === null ? null : (publicDescriptionVisible ? 1 : 0), publicDisplayOrder, now, now);
1691
1702
  return this.getCustomPlanFeatureCatalogEntry(featureKey) || {
1692
1703
  featureKey,
1693
1704
  label,
1694
1705
  description,
1695
1706
  isCustom: true,
1707
+ publicVisible,
1696
1708
  publicLabel,
1697
1709
  publicDescription,
1698
1710
  publicDescriptionVisible,
@@ -1737,11 +1749,12 @@ export class PlanEntitlementService {
1737
1749
  throw new TaskforceRuleError('Feature label is required', 400, 'PLAN_FEATURE_LABEL_REQUIRED');
1738
1750
  const publicLabel = this.normalizeOptionalCatalogMarketingCopy(input.publicLabel);
1739
1751
  const publicDescription = this.normalizeOptionalCatalogMarketingCopy(input.publicDescription);
1752
+ const publicVisible = this.normalizeOptionalCatalogDescriptionVisible(input.publicVisible);
1740
1753
  const publicDescriptionVisible = this.normalizeOptionalCatalogDescriptionVisible(input.publicDescriptionVisible);
1741
1754
  const publicDisplayOrder = this.normalizeOptionalCatalogDisplayOrder(input.publicDisplayOrder);
1742
1755
  const now = new Date().toISOString();
1743
1756
  const tx = this.db.transaction(() => {
1744
- if (!customFeature && !publicLabel && !publicDescription && publicDescriptionVisible === null && publicDisplayOrder === null) {
1757
+ if (!customFeature && publicVisible === null && !publicLabel && !publicDescription && publicDescriptionVisible === null && publicDisplayOrder === null) {
1745
1758
  this.db.prepare(`
1746
1759
  DELETE FROM plan_feature_catalog_overrides
1747
1760
  WHERE tenant_id = ? AND feature_key = ?
@@ -1755,6 +1768,7 @@ export class PlanEntitlementService {
1755
1768
  label,
1756
1769
  description,
1757
1770
  is_custom,
1771
+ public_visible,
1758
1772
  public_label,
1759
1773
  public_description,
1760
1774
  public_description_visible,
@@ -1762,17 +1776,18 @@ export class PlanEntitlementService {
1762
1776
  created_at,
1763
1777
  updated_at
1764
1778
  )
1765
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1779
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1766
1780
  ON CONFLICT (tenant_id, feature_key) DO UPDATE SET
1767
1781
  label = excluded.label,
1768
1782
  description = excluded.description,
1769
1783
  is_custom = excluded.is_custom,
1784
+ public_visible = excluded.public_visible,
1770
1785
  public_label = excluded.public_label,
1771
1786
  public_description = excluded.public_description,
1772
1787
  public_description_visible = excluded.public_description_visible,
1773
1788
  public_display_order = excluded.public_display_order,
1774
1789
  updated_at = excluded.updated_at
1775
- `).run(this.tenantId, featureKey, customFeature ? label : null, customFeature ? description : null, customFeature ? 1 : 0, publicLabel, publicDescription, publicDescriptionVisible === null ? null : (publicDescriptionVisible ? 1 : 0), publicDisplayOrder, now, now);
1790
+ `).run(this.tenantId, featureKey, customFeature ? label : null, customFeature ? description : null, customFeature ? 1 : 0, publicVisible === null ? null : (publicVisible ? 1 : 0), publicLabel, publicDescription, publicDescriptionVisible === null ? null : (publicDescriptionVisible ? 1 : 0), publicDisplayOrder, now, now);
1776
1791
  });
1777
1792
  tx();
1778
1793
  return this.listPlanFeatureCatalog().find((feature) => feature.featureKey === featureKey) || (customFeature ? {
@@ -1780,6 +1795,7 @@ export class PlanEntitlementService {
1780
1795
  label,
1781
1796
  description,
1782
1797
  isCustom: true,
1798
+ publicVisible,
1783
1799
  publicLabel,
1784
1800
  publicDescription,
1785
1801
  publicDescriptionVisible,
@@ -1791,6 +1807,7 @@ export class PlanEntitlementService {
1791
1807
  label: catalogFeature.label,
1792
1808
  description: catalogFeature.description,
1793
1809
  isCustom: false,
1810
+ publicVisible,
1794
1811
  publicLabel,
1795
1812
  publicDescription,
1796
1813
  publicDescriptionVisible,
@@ -618,6 +618,7 @@ export interface PlanFeatureCatalogItem {
618
618
  label: string;
619
619
  description: string;
620
620
  isCustom?: boolean;
621
+ publicVisible?: boolean | null;
621
622
  publicDescriptionVisible?: boolean | null;
622
623
  allowedAccessModes: PlanFeatureAccess[];
623
624
  configTemplate: Record<string, unknown>;
@@ -910,6 +911,8 @@ export declare class TaskforceCore implements TaskforceSyncCapable {
910
911
  private tenantId;
911
912
  private db;
912
913
  private globalSettingsDb;
914
+ private readonly globalSettingsSchemaEnsuredStores;
915
+ private readonly structuredSettingsSchemaEnsuredStores;
913
916
  private authTokenService;
914
917
  private identityService;
915
918
  private mcpTokenService;
@@ -1623,6 +1626,7 @@ export declare class TaskforceCore implements TaskforceSyncCapable {
1623
1626
  createCustomPlanFeatureCatalogEntry(input: {
1624
1627
  label: string;
1625
1628
  description?: string | null;
1629
+ publicVisible?: boolean | null;
1626
1630
  publicLabel?: string | null;
1627
1631
  publicDescription?: string | null;
1628
1632
  publicDescriptionVisible?: boolean | null;
@@ -1633,6 +1637,7 @@ export declare class TaskforceCore implements TaskforceSyncCapable {
1633
1637
  featureKey: string;
1634
1638
  label?: string | null;
1635
1639
  description?: string | null;
1640
+ publicVisible?: boolean | null;
1636
1641
  publicLabel?: string | null;
1637
1642
  publicDescription?: string | null;
1638
1643
  publicDescriptionVisible?: boolean | null;
@@ -40,6 +40,7 @@ import { formatInitiativeReference, formatWorkstreamReference, } from '../utils/
40
40
  import { getDocumentReferenceLabel } from '../utils/documentReferences.js';
41
41
  import { getImageReferenceLabel } from '../utils/imageReferences.js';
42
42
  import { getCanonicalDocumentName } from '../utils/documentNames.js';
43
+ import { isPathInsideRoot, resolveStorageKeyPathInsideRoot } from '../utils/pathSafety.js';
43
44
  import { DEFAULT_TASKFORCE_THEME } from '../utils/theme.js';
44
45
  import { ensureAssigneeTaskSchema, ensureAnnotatedAttachmentSessionsSchema, ensureAuthIdentitySchema, ensureBillingSchema, ensureDocumentReviewSessionsSchema, ensureDocumentReferenceSchema, ensureEntitlementsSchema, ensureScheduleTaskSchema, ensureTaskAssetsSchema, ensureTaskReferenceSchema, ensureTenantSchema, ensureWorkspaceAssetsSchema, ensureWorkspaceSchema } from '../migrations/taskSchemaMigrations.js';
45
46
  export class TaskforceRuleError extends Error {
@@ -142,6 +143,8 @@ export class TaskforceCore {
142
143
  tenantId;
143
144
  db;
144
145
  globalSettingsDb = null;
146
+ globalSettingsSchemaEnsuredStores = new WeakSet();
147
+ structuredSettingsSchemaEnsuredStores = new WeakSet();
145
148
  authTokenService;
146
149
  identityService;
147
150
  mcpTokenService;
@@ -2961,6 +2964,8 @@ export class TaskforceCore {
2961
2964
  return this.globalSettingsDb || this.db;
2962
2965
  }
2963
2966
  ensureGlobalSettingsTable(store) {
2967
+ if (this.globalSettingsSchemaEnsuredStores.has(store))
2968
+ return;
2964
2969
  store.exec(`
2965
2970
  CREATE TABLE IF NOT EXISTS global_settings (
2966
2971
  tenant_id TEXT PRIMARY KEY,
@@ -2968,8 +2973,11 @@ export class TaskforceCore {
2968
2973
  updated_at TEXT NOT NULL
2969
2974
  );
2970
2975
  `);
2976
+ this.globalSettingsSchemaEnsuredStores.add(store);
2971
2977
  }
2972
2978
  ensureStructuredSettingsTables(store) {
2979
+ if (this.structuredSettingsSchemaEnsuredStores.has(store))
2980
+ return;
2973
2981
  store.exec(`
2974
2982
  CREATE TABLE IF NOT EXISTS system_settings (
2975
2983
  tenant_id TEXT NOT NULL DEFAULT 'default',
@@ -3002,6 +3010,7 @@ export class TaskforceCore {
3002
3010
  CREATE INDEX IF NOT EXISTS idx_user_preferences_user_key
3003
3011
  ON user_preferences(user_id, prefs_key);
3004
3012
  `);
3013
+ this.structuredSettingsSchemaEnsuredStores.add(store);
3005
3014
  }
3006
3015
  readSettingsPayloadRow(store, tableName, whereValues) {
3007
3016
  try {
@@ -3260,8 +3269,13 @@ export class TaskforceCore {
3260
3269
  };
3261
3270
  }
3262
3271
  if (storageProvider !== 'r2') {
3263
- const candidatePath = path.join(this.basePath, storageKey);
3264
- if (!fs.existsSync(candidatePath) || !fs.statSync(candidatePath).isFile()) {
3272
+ const candidatePath = resolveStorageKeyPathInsideRoot(this.basePath, storageKey);
3273
+ if (!candidatePath || !fs.existsSync(candidatePath) || !fs.statSync(candidatePath).isFile()) {
3274
+ return null;
3275
+ }
3276
+ const storageRoot = fs.existsSync(this.basePath) ? fs.realpathSync(this.basePath) : path.resolve(this.basePath);
3277
+ const realCandidatePath = fs.realpathSync(candidatePath);
3278
+ if (!isPathInsideRoot(realCandidatePath, storageRoot)) {
3265
3279
  return null;
3266
3280
  }
3267
3281
  }
@@ -511,6 +511,7 @@ export interface PlanFeatureCatalogItem {
511
511
  label: string;
512
512
  description: string;
513
513
  isCustom?: boolean;
514
+ publicVisible?: boolean | null;
514
515
  publicLabel?: string | null;
515
516
  publicDescription?: string | null;
516
517
  publicDescriptionVisible?: boolean | null;
@@ -59,6 +59,7 @@ interface CreateSyncRecoveryCoordinatorArgs {
59
59
  persistWorkspaceSyncPatch: (patch: {
60
60
  phase: WorkspaceSyncPhase;
61
61
  pullCursor: null;
62
+ startupFullReconcileCompletedAt: null;
62
63
  lastErrorMessage: null;
63
64
  }) => Promise<unknown>;
64
65
  persistWorkspaceSyncWatermarksSnapshotBestEffort: () => boolean;
@@ -214,6 +214,7 @@ export function createSyncRecoveryCoordinator({ currentWorkspaceId, workspaceClo
214
214
  await persistWorkspaceSyncPatch({
215
215
  phase: repairPhase,
216
216
  pullCursor: null,
217
+ startupFullReconcileCompletedAt: null,
217
218
  lastErrorMessage: null
218
219
  });
219
220
  if (repairPhase === 'provision-local') {
@@ -32,6 +32,7 @@ interface CreateWorkspaceTransferCoordinatorArgs {
32
32
  lastPushAt?: string;
33
33
  lastPullAt?: string;
34
34
  lastSyncedAt: string;
35
+ startupFullReconcileCompletedAt?: string | null;
35
36
  pullCursor?: string | null;
36
37
  lastErrorMessage: null;
37
38
  } | {
@@ -42,6 +43,7 @@ interface CreateWorkspaceTransferCoordinatorArgs {
42
43
  pullCursor: string | null;
43
44
  lastPullAt: string;
44
45
  lastSyncedAt: string;
46
+ startupFullReconcileCompletedAt?: string | null;
45
47
  lastErrorMessage: null;
46
48
  }) => Promise<unknown>;
47
49
  persistWorkspaceSyncWatermarksSnapshotBestEffort: () => boolean;
@@ -486,6 +486,9 @@ export function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtim
486
486
  pullCursor: workspacePullCursorRef.current,
487
487
  lastPullAt: syncedAt,
488
488
  lastSyncedAt: syncedAt,
489
+ ...(options?.forceCursorNull === true
490
+ ? { startupFullReconcileCompletedAt: syncedAt }
491
+ : {}),
489
492
  lastErrorMessage: null
490
493
  });
491
494
  reportSyncEvent(currentWorkspaceId, {
@@ -527,7 +527,7 @@ export function useSyncOrchestrator(input) {
527
527
  workspaceLastPushedAnnotatedAttachmentSessionIdsRef.current = new Set();
528
528
  workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef.current = new Map();
529
529
  workspaceLastPushedTaskEventWatermarksRef.current = new Map();
530
- workspaceStartupFullReconcileRanRef.current = false;
530
+ workspaceStartupFullReconcileRanRef.current = Boolean(readWorkspaceSyncStateSnapshot().startupFullReconcileCompletedAt);
531
531
  workspaceFullReconcileInFlightRef.current = false;
532
532
  const persisted = readPersistedSyncWatermarks(currentWorkspaceId, taxonomyStateFingerprint);
533
533
  if (!hydrateWorkspacePushBaselines(persisted)) {
@@ -556,6 +556,14 @@ export function useSyncOrchestrator(input) {
556
556
  workspaceCloudSyncEnabled,
557
557
  workspaceSyncPhase
558
558
  ]);
559
+ useEffect(() => {
560
+ workspaceStartupFullReconcileRanRef.current = Boolean(readWorkspaceSyncStateSnapshot().startupFullReconcileCompletedAt);
561
+ }, [
562
+ currentWorkspaceId,
563
+ readWorkspaceSyncStateSnapshot,
564
+ workspaceLastPullAt,
565
+ workspaceSyncPhase
566
+ ]);
559
567
  const scheduleWorkspaceSyncRetry = useCallback((minDelayMs) => {
560
568
  scheduleWorkspaceRetry(() => retryWorkspaceSyncRef.current(), { minDelayMs });
561
569
  }, [scheduleWorkspaceRetry]);
@@ -2708,14 +2708,21 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2708
2708
  onTaskCountChange(tasks.length);
2709
2709
  }
2710
2710
  }, [tasks.length, onTaskCountChange]);
2711
+ const handledInitialTaskIdRef = useRef(null);
2711
2712
  // Deep linking handler
2712
2713
  useEffect(() => {
2713
- if (initialTaskId && tasks.length > 0) {
2714
- const task = tasks.find(t => t.id.endsWith(initialTaskId) || t.id === initialTaskId);
2715
- if (task) {
2716
- handleEdit(task);
2717
- setActiveTab('add');
2718
- }
2714
+ if (!initialTaskId) {
2715
+ handledInitialTaskIdRef.current = null;
2716
+ return;
2717
+ }
2718
+ if (handledInitialTaskIdRef.current === initialTaskId || tasks.length === 0) {
2719
+ return;
2720
+ }
2721
+ const task = tasks.find(t => t.id.endsWith(initialTaskId) || t.id === initialTaskId);
2722
+ if (task) {
2723
+ handledInitialTaskIdRef.current = initialTaskId;
2724
+ handleEdit(task);
2725
+ setActiveTab('add');
2719
2726
  }
2720
2727
  }, [initialTaskId, tasks]);
2721
2728
  // Update active category logic
@@ -60,6 +60,9 @@ function buildWorkspaceSyncStatePatch(current, patch) {
60
60
  version: 2,
61
61
  phase: patch.phase || current.phase,
62
62
  setupIntent: patch.setupIntent === undefined ? (current.setupIntent ?? null) : (patch.setupIntent ?? null),
63
+ startupFullReconcileCompletedAt: patch.startupFullReconcileCompletedAt === undefined
64
+ ? (current.startupFullReconcileCompletedAt ?? null)
65
+ : normalizeCursor(patch.startupFullReconcileCompletedAt),
63
66
  pullCursor: patch.pullCursor === undefined ? current.pullCursor : normalizeCursor(patch.pullCursor),
64
67
  lastPullAt: patch.lastPullAt === undefined ? current.lastPullAt : normalizeCursor(patch.lastPullAt),
65
68
  lastPushAt: patch.lastPushAt === undefined ? current.lastPushAt : normalizeCursor(patch.lastPushAt),