@taskforcehq/taskforce 0.3.328 → 0.3.329

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 (264) hide show
  1. package/dist/Taskforce.module.css +233 -34
  2. package/dist/TaskforceCore.js +237 -230
  3. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +1 -1
  4. package/dist/cli.js +34 -0
  5. package/dist/cliCloudTargetGuard.d.ts +5 -0
  6. package/dist/cliCloudTargetGuard.js +11 -0
  7. package/dist/components/features/AnnotatedAttachmentWorkspace.js +24 -8
  8. package/dist/components/features/TaskSettings.d.ts +12 -1
  9. package/dist/components/features/TaskSettings.js +147 -40
  10. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +1 -1
  11. package/dist/components/features/planning/PlanningModule.js +2 -5
  12. package/dist/components/features/planning/PlanningTaskOrderList.d.ts +1 -1
  13. package/dist/components/features/planning/PlanningTaskOrderList.js +17 -6
  14. package/dist/components/features/planning/planningTaskArrangement.d.ts +1 -0
  15. package/dist/components/features/planning/planningTaskArrangement.js +5 -5
  16. package/dist/components/features/taskforceAgentEditorModel.js +3 -1
  17. package/dist/components/task/TaskCard.d.ts +5 -0
  18. package/dist/components/task/TaskCard.js +65 -10
  19. package/dist/components/task/TaskImageReviews.js +14 -5
  20. package/dist/components/task/TaskKanban.d.ts +6 -1
  21. package/dist/components/task/TaskKanban.js +152 -6
  22. package/dist/components/task/TaskList.d.ts +5 -0
  23. package/dist/components/task/TaskList.js +3 -3
  24. package/dist/components/ui/PasswordInput.d.ts +6 -0
  25. package/dist/components/ui/PasswordInput.js +11 -0
  26. package/dist/components/ui/TopNoticeLayer.js +1 -1
  27. package/dist/components/views/AppShellHeader.js +32 -11
  28. package/dist/components/views/PlanComparisonPage.js +12 -0
  29. package/dist/components/views/StandaloneLayout.js +7 -31
  30. package/dist/components/views/WidgetView.js +3 -3
  31. package/dist/components/views/panels/PlanningDrawer.js +5 -12
  32. package/dist/components/views/standalone/modals/AccountSettingsModal.js +26 -17
  33. package/dist/config/envSchema.js +12 -0
  34. package/dist/config/localServiceRuntimeContract.d.ts +13 -0
  35. package/dist/config/localServiceRuntimeContract.js +135 -0
  36. package/dist/core/AiProfileService.js +46 -8
  37. package/dist/core/DeletedTaskLifecycleService.d.ts +1 -0
  38. package/dist/core/DeletedTaskLifecycleService.js +37 -6
  39. package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +3 -2
  40. package/dist/core/TaskAttachmentLinksDurableMutationService.js +33 -9
  41. package/dist/core/Taskforce.d.ts +62 -2
  42. package/dist/core/Taskforce.js +635 -32
  43. package/dist/core/types.d.ts +6 -0
  44. package/dist/hooks/sync/useDurableLocalOutboxDrain.d.ts +1 -1
  45. package/dist/hooks/sync/useDurableTaskCreateMutation.js +1 -1
  46. package/dist/hooks/sync/useDurableTaskMetadataMutation.d.ts +1 -0
  47. package/dist/hooks/sync/useDurableTaskMetadataMutation.js +1 -0
  48. package/dist/hooks/sync/useDurableTaskStatusMutation.d.ts +1 -0
  49. package/dist/hooks/tasks/taskHttpMutation.js +1 -0
  50. package/dist/hooks/tasks/useCloudTaskSearch.d.ts +12 -0
  51. package/dist/hooks/tasks/useCloudTaskSearch.js +71 -0
  52. package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.d.ts +16 -0
  53. package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.js +197 -0
  54. package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +16 -3
  55. package/dist/hooks/tasks/useTaskEditorNavigation.js +72 -13
  56. package/dist/hooks/tasks/useTaskFormActions.d.ts +6 -0
  57. package/dist/hooks/tasks/useTaskFormActions.js +65 -12
  58. package/dist/hooks/ui/useSettingsModel.d.ts +24 -2
  59. package/dist/hooks/ui/useSettingsModel.js +13 -2
  60. package/dist/hooks/ui/useSettingsPersistence.d.ts +12 -1
  61. package/dist/hooks/ui/useSettingsPersistence.js +74 -47
  62. package/dist/hooks/useFilterSort.d.ts +7 -3
  63. package/dist/hooks/useFilterSort.js +15 -5
  64. package/dist/hooks/useTaskData.d.ts +10 -1
  65. package/dist/hooks/useTaskData.js +216 -7
  66. package/dist/hooks/useTaskMutations.d.ts +10 -2
  67. package/dist/hooks/useTaskMutations.js +223 -43
  68. package/dist/hooks/useTaskforce.d.ts +37 -9
  69. package/dist/hooks/useTaskforce.js +349 -39
  70. package/dist/hooks/useUiNotice.d.ts +4 -0
  71. package/dist/hooks/useUiNotice.js +4 -0
  72. package/dist/mcp/adminWorkspaceRegistrar.js +3 -3
  73. package/dist/mcp/documentAssetRegistrar.js +44 -35
  74. package/dist/mcp/documentHelpers.d.ts +1 -0
  75. package/dist/mcp/documentHelpers.js +7 -3
  76. package/dist/mcp/runtime.js +172 -42
  77. package/dist/mcp/taskPlanningRegistrar.js +41 -38
  78. package/dist/mcp/toolCallCompatibility.js +38 -1
  79. package/dist/mcp/toolCatalog.d.ts +1 -0
  80. package/dist/migrations/taskSchemaMigrations.js +12 -0
  81. package/dist/runtime/appGateDefinitions.d.ts +30 -6
  82. package/dist/runtime/appGateDefinitions.js +6 -6
  83. package/dist/runtime/appGates.d.ts +12 -13
  84. package/dist/runtime/appGates.js +29 -13
  85. package/dist/runtime/nodeAppGates.d.ts +8 -0
  86. package/dist/runtime/nodeAppGates.js +27 -0
  87. package/dist/server/cloudCollectionRead.d.ts +1 -1
  88. package/dist/server/cloudCollectionRead.js +2 -2
  89. package/dist/server/index.d.ts +3 -0
  90. package/dist/server/index.js +15 -3
  91. package/dist/server/localServiceLease.d.ts +3 -0
  92. package/dist/server/localServiceLease.js +9 -1
  93. package/dist/server/localWorkspaceSyncServerRuntime.js +10 -3
  94. package/dist/server/rateLimit.d.ts +1 -0
  95. package/dist/server/rateLimit.js +4 -0
  96. package/dist/server/realtimeSyncWs.js +36 -12
  97. package/dist/server/routes/admin.js +1 -0
  98. package/dist/server/routes/agentRoles.js +3 -2
  99. package/dist/server/routes/agentSkills.js +3 -2
  100. package/dist/server/routes/agents.js +118 -16
  101. package/dist/server/routes/annotatedAttachments.js +3 -2
  102. package/dist/server/routes/auth.js +3 -1
  103. package/dist/server/routes/durableTaskHttpRouters.d.ts +9 -2
  104. package/dist/server/routes/localService.js +2 -0
  105. package/dist/server/routes/primitives.d.ts +2 -0
  106. package/dist/server/routes/syncAuxRoutes.js +5 -2
  107. package/dist/server/routes/syncPullApplyRoutes.js +23 -4
  108. package/dist/server/routes/syncV3FeedRoutes.js +18 -16
  109. package/dist/server/routes/syncV3LocalCaptureRoutes.js +28 -2
  110. package/dist/server/routes/syncV3LocalFeedApplyRoutes.d.ts +1 -0
  111. package/dist/server/routes/syncV3LocalFeedApplyRoutes.js +26 -0
  112. package/dist/server/routes/syncV3OperationRoutes.js +10 -0
  113. package/dist/server/routes/tasks.js +103 -11
  114. package/dist/server/routes/workspaces.js +43 -1
  115. package/dist/server/workspaceDeletionAdmission.d.ts +23 -0
  116. package/dist/server/workspaceDeletionAdmission.js +50 -0
  117. package/dist/service/localServiceCli.js +16 -0
  118. package/dist/service/localServiceClientLifecycle.js +36 -10
  119. package/dist/services/codexAppServerModelGateway.d.ts +112 -0
  120. package/dist/services/codexAppServerModelGateway.js +438 -0
  121. package/dist/services/taskforceAgentMcpBridge.d.ts +7 -1
  122. package/dist/services/taskforceAgentMcpBridge.js +23 -3
  123. package/dist/shared/taskforceAgentDefinition.js +7 -1
  124. package/dist/shared/taskforceAgentModels.d.ts +1 -0
  125. package/dist/shared/taskforceAgentModels.js +30 -1
  126. package/dist/storage/postgresAdapter.d.ts +20 -3
  127. package/dist/storage/postgresAdapter.js +106 -14
  128. package/dist/storage/postgresWorker.js +13 -4
  129. package/dist/storage/postgresWorkerProtocol.d.ts +29 -0
  130. package/dist/storage/postgresWorkerProtocol.js +30 -0
  131. package/dist/sync/cloudSyncApi.d.ts +4 -1
  132. package/dist/sync/cloudSyncApi.js +7 -2
  133. package/dist/sync/contentSyncState.d.ts +1 -1
  134. package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +1 -0
  135. package/dist/sync/coordinator/localSyncExecutionPermitStore.js +7 -1
  136. package/dist/sync/coordinator/localWorkspaceSyncRunner.js +6 -2
  137. package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +20 -2
  138. package/dist/sync/engine/workspaceSyncEngineLocalServices.d.ts +4 -0
  139. package/dist/sync/engine/workspaceSyncEngineLocalServices.js +11 -0
  140. package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +11 -0
  141. package/dist/sync/engine/workspaceSyncEngineRemoteServices.js +2 -1
  142. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +31 -4
  143. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.d.ts +3 -1
  144. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +25 -8
  145. package/dist/sync/engine/workspaceSyncEngineTransfers.d.ts +1 -1
  146. package/dist/sync/engine/workspaceSyncEngineTransfers.js +8 -3
  147. package/dist/sync/syncService.d.ts +2 -0
  148. package/dist/sync/syncService.js +4 -1
  149. package/dist/sync/v3/combinedFeedCapability.js +4 -0
  150. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +2 -0
  151. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +11 -1
  152. package/dist/sync/v3/durableTaskChecklistMutationRouter.d.ts +2 -0
  153. package/dist/sync/v3/durableTaskChecklistMutationRouter.js +4 -0
  154. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.d.ts +1 -0
  155. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +30 -8
  156. package/dist/sync/v3/durableTaskHttpRootMutationRouter.d.ts +5 -3
  157. package/dist/sync/v3/durableTaskHttpRootMutationRouter.js +27 -2
  158. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +2 -0
  159. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +47 -7
  160. package/dist/sync/v3/localTaskCreateOutboxService.js +15 -6
  161. package/dist/sync/v3/localTaskMetadataClient.d.ts +1 -0
  162. package/dist/sync/v3/localTaskMetadataClient.js +1 -0
  163. package/dist/sync/v3/localTaskMetadataOutboxHandler.js +5 -1
  164. package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -0
  165. package/dist/sync/v3/localTaskMetadataOutboxService.js +7 -0
  166. package/dist/sync/v3/localTaskRestoreOutboxService.js +14 -6
  167. package/dist/sync/v3/localTaskStatusClient.d.ts +1 -0
  168. package/dist/sync/v3/localTaskStatusClient.js +6 -1
  169. package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -0
  170. package/dist/sync/v3/localTaskStatusOutboxService.js +54 -13
  171. package/dist/sync/v3/syncDurabilityStore.d.ts +7 -0
  172. package/dist/sync/v3/syncDurabilityStore.js +31 -11
  173. package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +6 -4
  174. package/dist/sync/v3/taskAttachmentLinksMutationService.js +19 -10
  175. package/dist/sync/v3/taskChecklistMutationService.d.ts +4 -2
  176. package/dist/sync/v3/taskChecklistMutationService.js +4 -0
  177. package/dist/sync/v3/taskCreateMutationService.d.ts +2 -2
  178. package/dist/sync/v3/taskCreateMutationService.js +7 -4
  179. package/dist/sync/v3/taskMetadataMutationService.d.ts +3 -0
  180. package/dist/sync/v3/taskMetadataMutationService.js +4 -0
  181. package/dist/sync/v3/taskRestoreMutationService.d.ts +2 -2
  182. package/dist/sync/v3/taskRestoreMutationService.js +7 -4
  183. package/dist/sync/v3/taskStatusMutationService.d.ts +2 -0
  184. package/dist/sync/v3/taskStatusMutationService.js +4 -0
  185. package/dist/sync/v3/workflowAssignmentSync.d.ts +1 -0
  186. package/dist/sync/v3/workflowAssignmentSync.js +3 -0
  187. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.d.ts +1 -1
  188. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +13 -3
  189. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.d.ts +11 -0
  190. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.js +27 -10
  191. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.d.ts +2 -1
  192. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.js +52 -10
  193. package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +9 -1
  194. package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +8 -3
  195. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +51 -10
  196. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +7 -0
  197. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +30 -18
  198. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +9 -4
  199. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +98 -47
  200. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.d.ts +10 -0
  201. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +58 -21
  202. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.d.ts +3 -1
  203. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.js +18 -0
  204. package/dist/sync/v3/workspaceSyncV3CoverageRegistry.js +6 -0
  205. package/dist/sync/v3/workspaceSyncV3FeedProfile.d.ts +1 -1
  206. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.d.ts +1 -0
  207. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.js +23 -10
  208. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.d.ts +3 -0
  209. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.js +64 -13
  210. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.d.ts +9 -0
  211. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.js +26 -0
  212. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +15 -5
  213. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +50 -14
  214. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.d.ts +2 -1
  215. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.js +11 -3
  216. package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +2 -0
  217. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.d.ts +2 -0
  218. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.js +19 -7
  219. package/dist/sync/v3/workspaceSyncV3TaskCommentAudit.js +9 -2
  220. package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.d.ts +7 -0
  221. package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.js +37 -0
  222. package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.d.ts +6 -0
  223. package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.js +9 -0
  224. package/dist/sync/workspacePullFeed.js +2 -1
  225. package/dist/sync/workspaceRepair.d.ts +3 -0
  226. package/dist/sync/workspaceRepair.js +67 -6
  227. package/dist/types.d.ts +47 -1
  228. package/dist/types.js +6 -0
  229. package/dist/ui/assets/{AiIdentityRosterCard-MZs7lVED.js → AiIdentityRosterCard-BZM4aQnM.js} +1 -1
  230. package/dist/ui/assets/AiProfilesModule-BbAEWtHH.js +1 -0
  231. package/dist/ui/assets/AnnotatedAttachmentWorkspace-B68Wg-RV.js +3 -0
  232. package/dist/ui/assets/AnnotatedAttachmentWorkspace-TzYu7pkG.css +1 -0
  233. package/dist/ui/assets/{AssetTraySortControl-Kf1pelSy.js → AssetTraySortControl-C8Rztyal.js} +1 -1
  234. package/dist/ui/assets/{ContextAttachmentManager-B8boOs6Q.js → ContextAttachmentManager-Cj3c7X9y.js} +2 -2
  235. package/dist/ui/assets/{DocumentWorkspace-DXH4XUsB.js → DocumentWorkspace-B5uvzems.js} +1 -1
  236. package/dist/ui/assets/{EntityActivityTimeline-BtSTq0ON.js → EntityActivityTimeline-BVFvCB5R.js} +1 -1
  237. package/dist/ui/assets/PlanningModule-B5DWQOGz.js +1 -0
  238. package/dist/ui/assets/PlansPage-AY4cGWco.js +1 -0
  239. package/dist/ui/assets/TaskContextUpload-DZxTVesW.js +1 -0
  240. package/dist/ui/assets/TaskSettings-BeWAuyZj.js +12 -0
  241. package/dist/ui/assets/{TaskforceAgentsModule-BazdUwxZ.js → TaskforceAgentsModule-BifCYtjR.js} +6 -6
  242. package/dist/ui/assets/{WorkflowManagerModule-DWFspC-o.js → WorkflowManagerModule-DEr5di_r.js} +1 -1
  243. package/dist/ui/assets/index-I5zhdtgt.js +7 -0
  244. package/dist/ui/assets/index-VRFsx3wz.css +1 -0
  245. package/dist/ui/assets/{vendor-icons-BkFLXavV.js → vendor-icons-D9Lpw-j4.js} +1 -1
  246. package/dist/ui/index.html +3 -3
  247. package/dist/utils/taskActivity.d.ts +3 -2
  248. package/dist/utils/taskActivity.js +19 -2
  249. package/dist/utils/taskEditorReconciliation.d.ts +40 -0
  250. package/dist/utils/taskEditorReconciliation.js +217 -0
  251. package/dist/utils/taskNormalization.d.ts +1 -0
  252. package/dist/utils/taskNormalization.js +39 -0
  253. package/dist/utils/taskSearch.js +2 -2
  254. package/dist/utils/uiNotice.d.ts +4 -0
  255. package/package.json +16 -4
  256. package/dist/ui/assets/AiProfilesModule-C3i0LOxe.js +0 -1
  257. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CBRZ8rca.css +0 -1
  258. package/dist/ui/assets/AnnotatedAttachmentWorkspace-DVZk5Tln.js +0 -3
  259. package/dist/ui/assets/PlanningModule-BPVyEa-f.js +0 -1
  260. package/dist/ui/assets/PlansPage-ChqgxALc.js +0 -1
  261. package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +0 -1
  262. package/dist/ui/assets/TaskSettings-g4ECKNOz.js +0 -12
  263. package/dist/ui/assets/index-BUplSsv_.js +0 -7
  264. package/dist/ui/assets/index-gz2EXuoK.css +0 -1
@@ -176,6 +176,12 @@ export interface TaskFile {
176
176
  jsonBackupEnabled?: boolean;
177
177
  manualComplexityEnabled?: boolean;
178
178
  checklistDropdownEnabled?: boolean;
179
+ showTaskCardStatusLabel?: boolean;
180
+ showTaskCardDescription?: boolean;
181
+ showTaskCardAttachments?: boolean;
182
+ showTaskCardChecklistProgress?: boolean;
183
+ showTaskCardLatestActivity?: boolean;
184
+ tintTaskCardActivityBackground?: boolean;
179
185
  taxonomies?: any[];
180
186
  };
181
187
  }
@@ -6,7 +6,7 @@ export declare function useDurableLocalOutboxDrain(input: {
6
6
  workspaceId: string;
7
7
  mutationFingerprint: string;
8
8
  authorityScope: object;
9
- remoteServices: Pick<WorkspaceSyncEngineRemoteServices, 'pullWorkspaceContent' | 'abandonTransferReceipts' | 'buildV2ApplyCompletionGroup'> | null;
9
+ remoteServices: Pick<WorkspaceSyncEngineRemoteServices, 'pullWorkspaceContent' | 'abandonTransferReceipts' | 'buildIndependentContentApplyCompletionGroup'> | null;
10
10
  localServices: Pick<WorkspaceSyncEngineLocalServices, 'applyV2Batch'>;
11
11
  isAuthorityCurrent?: (authorityScope: object) => boolean;
12
12
  onHydratedEventIds?: (eventIds: string[]) => void;
@@ -104,7 +104,7 @@ export function useDurableTaskCreateMutation(input) {
104
104
  clientRef.current = input.client || createLocalTaskCreateClient();
105
105
  const participant = await getParticipant(workspaceId);
106
106
  const selected = selectTaskCreateActivationInput(candidate);
107
- const participantId = selected
107
+ const participantId = input.enabled && selected
108
108
  ? participant.prepareForDraft(await fingerprintTaskDraft(selected.localTask))
109
109
  : participant.participantId;
110
110
  let result;
@@ -2,6 +2,7 @@ import type { TaskItem } from '../../types.js';
2
2
  import { createLocalTaskMetadataClient, type LocalTaskMetadataMutationResult } from '../../sync/v3/localTaskMetadataClient.js';
3
3
  export type DurableTaskMetadataMutationAdapter = (taskId: string, patch: Partial<TaskItem>, options?: {
4
4
  saveSource?: 'manual' | 'autosave';
5
+ expectedDetailRevision?: string;
5
6
  }) => Promise<LocalTaskMetadataMutationResult | null>;
6
7
  type LocalTaskMetadataClient = ReturnType<typeof createLocalTaskMetadataClient>;
7
8
  export declare function useDurableTaskMetadataMutation(input: {
@@ -17,6 +17,7 @@ export function useDurableTaskMetadataMutation(input) {
17
17
  entityId,
18
18
  patch: patch,
19
19
  ...(options?.saveSource ? { saveSource: options.saveSource } : {}),
20
+ ...(options?.expectedDetailRevision ? { expectedDetailRevision: options.expectedDetailRevision } : {}),
20
21
  isAuthenticated: input.isAuthenticated,
21
22
  dispatchEnabled: input.dispatchEnabled,
22
23
  });
@@ -6,6 +6,7 @@ export type DurableTaskStatusMutationAdapter = (taskId: string, payload: {
6
6
  assigneeIntent: DurableTaskStatusAssigneeIntent;
7
7
  canceledReason: string | null;
8
8
  metadataPatch?: Partial<TaskItem>;
9
+ expectedDetailRevision?: string;
9
10
  }) => Promise<LocalTaskStatusMutationResult | null>;
10
11
  type LocalTaskStatusClient = ReturnType<typeof createLocalTaskStatusClient>;
11
12
  export declare function useDurableTaskStatusMutation(input: {
@@ -44,6 +44,7 @@ export async function isConclusiveTaskHttpResponse(response) {
44
44
  'TASK_ATTACHMENT_LINKS_INVALID',
45
45
  'TASK_COMMENT_INVALID',
46
46
  'TASK_COMMENT_IDENTITY_CONFLICT',
47
+ 'TASK_DETAIL_REVISION_CONFLICT',
47
48
  ]).has(code);
48
49
  }
49
50
  function stableTaskCreateCandidate(candidate) {
@@ -0,0 +1,12 @@
1
+ interface UseCloudTaskSearchInput {
2
+ query: string;
3
+ runtimeMode: 'local' | 'cloud';
4
+ workspaceId: string;
5
+ enabled: boolean;
6
+ scope: 'active' | 'archive';
7
+ debounceMs?: number;
8
+ pageSize?: number;
9
+ maxMatches?: number;
10
+ }
11
+ export declare function useCloudTaskSearch({ query, runtimeMode, workspaceId, enabled, scope, debounceMs, pageSize, maxMatches, }: UseCloudTaskSearchInput): ReadonlySet<string>;
12
+ export {};
@@ -0,0 +1,71 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { normalizeTaskSearchQuery } from '../../utils/taskSearch';
3
+ export function useCloudTaskSearch({ query, runtimeMode, workspaceId, enabled, scope, debounceMs = 250, pageSize = 50, maxMatches = 500, }) {
4
+ const [matchIds, setMatchIds] = useState(() => new Set());
5
+ const requestSequenceRef = useRef(0);
6
+ const normalizedQuery = useMemo(() => normalizeTaskSearchQuery(query).raw, [query]);
7
+ useEffect(() => {
8
+ requestSequenceRef.current += 1;
9
+ const requestSequence = requestSequenceRef.current;
10
+ if (!enabled || runtimeMode !== 'cloud' || !normalizedQuery || !workspaceId.trim()) {
11
+ setMatchIds((current) => current.size === 0 ? current : new Set());
12
+ return;
13
+ }
14
+ setMatchIds((current) => current.size === 0 ? current : new Set());
15
+ const abortController = new AbortController();
16
+ const timer = window.setTimeout(() => {
17
+ void (async () => {
18
+ const collected = new Set();
19
+ let offset = 0;
20
+ let scanned = 0;
21
+ const boundedPageSize = Math.max(1, Math.min(50, Math.trunc(pageSize)));
22
+ const boundedMaxMatches = Math.max(1, Math.trunc(maxMatches));
23
+ try {
24
+ while (scanned < boundedMaxMatches) {
25
+ const limit = Math.min(boundedPageSize, boundedMaxMatches - scanned);
26
+ const params = new URLSearchParams({
27
+ query: query.trim(),
28
+ scope,
29
+ limit: String(limit),
30
+ offset: String(offset),
31
+ });
32
+ const response = await fetch(`/api/taskforce/tasks/search?${params.toString()}`, {
33
+ signal: abortController.signal,
34
+ });
35
+ if (!response.ok)
36
+ throw new Error(`Task search failed (${response.status}).`);
37
+ const body = await response.json();
38
+ const matches = body.matches || [];
39
+ scanned += matches.length;
40
+ for (const match of matches) {
41
+ const id = String(match?.id || '').trim();
42
+ if (id)
43
+ collected.add(id);
44
+ }
45
+ if (body.pagination?.hasMore !== true)
46
+ break;
47
+ const nextOffset = Number(body.pagination?.nextOffset);
48
+ if (!Number.isInteger(nextOffset) || nextOffset <= offset)
49
+ break;
50
+ offset = nextOffset;
51
+ }
52
+ if (!abortController.signal.aborted && requestSequenceRef.current === requestSequence) {
53
+ setMatchIds(collected);
54
+ }
55
+ }
56
+ catch (error) {
57
+ if (abortController.signal.aborted || requestSequenceRef.current !== requestSequence)
58
+ return;
59
+ // Server search augments the existing client matcher. On failure, retain
60
+ // the client-visible metadata results instead of blocking the task board.
61
+ setMatchIds(new Set());
62
+ }
63
+ })();
64
+ }, Math.max(0, debounceMs));
65
+ return () => {
66
+ window.clearTimeout(timer);
67
+ abortController.abort('superseded');
68
+ };
69
+ }, [debounceMs, enabled, maxMatches, normalizedQuery, pageSize, query, runtimeMode, scope, workspaceId]);
70
+ return matchIds;
71
+ }
@@ -0,0 +1,16 @@
1
+ export type TaskDescriptionImageDraftResolution = 'promote' | 'discard';
2
+ type DraftRecoveryFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
3
+ interface UseTaskDescriptionImageDraftRecoveryOptions {
4
+ fetchImpl?: DraftRecoveryFetch;
5
+ storage?: Storage | null;
6
+ now?: () => number;
7
+ }
8
+ export declare const TASK_DESCRIPTION_IMAGE_DRAFT_RECOVERY_STORAGE_KEY = "taskforce.task-description-image-draft-recovery.v1";
9
+ export declare function taskDescriptionImageDraftRetryDelay(attempts: number): number;
10
+ export declare function useTaskDescriptionImageDraftRecovery(workspaceId: string, createdByUserId: string, options?: UseTaskDescriptionImageDraftRecoveryOptions): {
11
+ resolveDraft: (input: {
12
+ draftUploadId: string;
13
+ resolution: TaskDescriptionImageDraftResolution;
14
+ }) => Promise<void>;
15
+ };
16
+ export {};
@@ -0,0 +1,197 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ const defaultFetch = (input, init) => globalThis.fetch(input, init);
3
+ export const TASK_DESCRIPTION_IMAGE_DRAFT_RECOVERY_STORAGE_KEY = 'taskforce.task-description-image-draft-recovery.v1';
4
+ const RETRY_DELAYS_MS = [1_000, 5_000, 15_000, 60_000];
5
+ const MAX_AUTOMATIC_RETRY_ATTEMPTS = 8;
6
+ const DORMANT_RETRY_AT = Number.MAX_SAFE_INTEGER;
7
+ export function taskDescriptionImageDraftRetryDelay(attempts) {
8
+ const index = Math.max(0, Math.min(RETRY_DELAYS_MS.length - 1, Math.floor(attempts) - 1));
9
+ return RETRY_DELAYS_MS[index];
10
+ }
11
+ function recordKey(record) {
12
+ return `${record.workspaceId}\u0000${record.createdByUserId}\u0000${record.draftUploadId}`;
13
+ }
14
+ function isRecoveryRecord(value) {
15
+ if (!value || typeof value !== 'object')
16
+ return false;
17
+ const record = value;
18
+ return typeof record.workspaceId === 'string'
19
+ && Boolean(record.workspaceId.trim())
20
+ && typeof record.createdByUserId === 'string'
21
+ && Boolean(record.createdByUserId.trim())
22
+ && typeof record.draftUploadId === 'string'
23
+ && Boolean(record.draftUploadId.trim())
24
+ && (record.resolution === 'promote' || record.resolution === 'discard')
25
+ && Number.isFinite(record.attempts)
26
+ && Number.isFinite(record.nextAttemptAt)
27
+ && Number.isFinite(record.createdAt);
28
+ }
29
+ function readRecoveryRecords(storage) {
30
+ if (!storage)
31
+ return [];
32
+ try {
33
+ const parsed = JSON.parse(storage.getItem(TASK_DESCRIPTION_IMAGE_DRAFT_RECOVERY_STORAGE_KEY) || '[]');
34
+ if (!Array.isArray(parsed))
35
+ return [];
36
+ const records = new Map();
37
+ parsed.filter(isRecoveryRecord).forEach((record) => {
38
+ records.set(recordKey(record), record);
39
+ });
40
+ return [...records.values()];
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ }
46
+ function writeRecoveryRecords(storage, records) {
47
+ if (!storage)
48
+ return;
49
+ try {
50
+ if (records.length === 0) {
51
+ storage.removeItem(TASK_DESCRIPTION_IMAGE_DRAFT_RECOVERY_STORAGE_KEY);
52
+ return;
53
+ }
54
+ storage.setItem(TASK_DESCRIPTION_IMAGE_DRAFT_RECOVERY_STORAGE_KEY, JSON.stringify(records));
55
+ }
56
+ catch {
57
+ // The request still gets an immediate attempt when browser storage is unavailable.
58
+ }
59
+ }
60
+ function updateRecoveryRecords(storage, update) {
61
+ const next = update(readRecoveryRecords(storage));
62
+ writeRecoveryRecords(storage, next);
63
+ return next;
64
+ }
65
+ function defaultStorage() {
66
+ try {
67
+ return globalThis.localStorage || null;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ export function useTaskDescriptionImageDraftRecovery(workspaceId, createdByUserId, options = {}) {
74
+ const fetchImpl = options.fetchImpl || defaultFetch;
75
+ const storage = options.storage === undefined ? defaultStorage() : options.storage;
76
+ const now = options.now || Date.now;
77
+ const timerRef = useRef(null);
78
+ const inFlightRef = useRef(new Map());
79
+ const drainRef = useRef(async () => { });
80
+ const clearRetryTimer = useCallback(() => {
81
+ if (timerRef.current === null)
82
+ return;
83
+ clearTimeout(timerRef.current);
84
+ timerRef.current = null;
85
+ }, []);
86
+ const scheduleRetry = useCallback(() => {
87
+ clearRetryTimer();
88
+ const nextRecord = readRecoveryRecords(storage)
89
+ .filter((record) => (record.workspaceId === workspaceId
90
+ && record.createdByUserId === createdByUserId
91
+ && record.nextAttemptAt < DORMANT_RETRY_AT))
92
+ .sort((left, right) => left.nextAttemptAt - right.nextAttemptAt)[0];
93
+ if (!nextRecord)
94
+ return;
95
+ const delay = Math.max(0, nextRecord.nextAttemptAt - now());
96
+ timerRef.current = setTimeout(() => {
97
+ timerRef.current = null;
98
+ void drainRef.current(false);
99
+ }, delay);
100
+ }, [clearRetryTimer, createdByUserId, now, storage, workspaceId]);
101
+ const attemptRecovery = useCallback((record) => {
102
+ const key = recordKey(record);
103
+ const existing = inFlightRef.current.get(key);
104
+ if (existing)
105
+ return existing;
106
+ const request = (async () => {
107
+ await Promise.resolve();
108
+ try {
109
+ const response = await fetchImpl(`/api/taskforce/context-upload/draft/${record.resolution}`, {
110
+ method: 'POST',
111
+ headers: { 'Content-Type': 'application/json' },
112
+ body: JSON.stringify({
113
+ workspaceId: record.workspaceId,
114
+ draftUploadId: record.draftUploadId,
115
+ uploadPurpose: 'task-description-draft',
116
+ }),
117
+ });
118
+ if (!response.ok) {
119
+ throw new Error(`Draft upload ${record.resolution} was rejected (${response.status}).`);
120
+ }
121
+ updateRecoveryRecords(storage, (records) => (records.filter((candidate) => recordKey(candidate) !== key)));
122
+ }
123
+ catch (error) {
124
+ updateRecoveryRecords(storage, (records) => records.map((candidate) => {
125
+ if (recordKey(candidate) !== key)
126
+ return candidate;
127
+ const attempts = Math.min(Number.MAX_SAFE_INTEGER, candidate.attempts + 1);
128
+ return {
129
+ ...candidate,
130
+ attempts,
131
+ nextAttemptAt: attempts >= MAX_AUTOMATIC_RETRY_ATTEMPTS
132
+ ? DORMANT_RETRY_AT
133
+ : now() + taskDescriptionImageDraftRetryDelay(attempts),
134
+ };
135
+ }));
136
+ throw error;
137
+ }
138
+ finally {
139
+ inFlightRef.current.delete(key);
140
+ scheduleRetry();
141
+ }
142
+ })();
143
+ inFlightRef.current.set(key, request);
144
+ return request;
145
+ }, [fetchImpl, now, scheduleRetry, storage]);
146
+ const drain = useCallback(async (force = false) => {
147
+ const currentTime = now();
148
+ const dueRecords = readRecoveryRecords(storage).filter((record) => (record.workspaceId === workspaceId
149
+ && record.createdByUserId === createdByUserId
150
+ && (force || record.nextAttemptAt <= currentTime)));
151
+ await Promise.allSettled(dueRecords.map((record) => attemptRecovery(record)));
152
+ scheduleRetry();
153
+ }, [attemptRecovery, createdByUserId, now, scheduleRetry, storage, workspaceId]);
154
+ drainRef.current = drain;
155
+ useEffect(() => {
156
+ void drain(true);
157
+ const retryWhenOnline = () => { void drain(true); };
158
+ const retryWhenVisible = () => {
159
+ if (document.visibilityState === 'visible')
160
+ void drain(true);
161
+ };
162
+ const retryAfterStorageChange = (event) => {
163
+ if (event.key === TASK_DESCRIPTION_IMAGE_DRAFT_RECOVERY_STORAGE_KEY)
164
+ void drain(false);
165
+ };
166
+ window.addEventListener('online', retryWhenOnline);
167
+ window.addEventListener('storage', retryAfterStorageChange);
168
+ document.addEventListener('visibilitychange', retryWhenVisible);
169
+ return () => {
170
+ clearRetryTimer();
171
+ window.removeEventListener('online', retryWhenOnline);
172
+ window.removeEventListener('storage', retryAfterStorageChange);
173
+ document.removeEventListener('visibilitychange', retryWhenVisible);
174
+ };
175
+ }, [clearRetryTimer, drain]);
176
+ const resolveDraft = useCallback(async (input) => {
177
+ const draftUploadId = input.draftUploadId.trim();
178
+ if (!workspaceId || !createdByUserId || !draftUploadId)
179
+ return;
180
+ const queuedAt = now();
181
+ const record = {
182
+ workspaceId,
183
+ createdByUserId,
184
+ draftUploadId,
185
+ resolution: input.resolution,
186
+ attempts: 0,
187
+ nextAttemptAt: queuedAt,
188
+ createdAt: queuedAt,
189
+ };
190
+ updateRecoveryRecords(storage, (records) => [
191
+ ...records.filter((candidate) => recordKey(candidate) !== recordKey(record)),
192
+ record,
193
+ ]);
194
+ await attemptRecovery(record);
195
+ }, [attemptRecovery, createdByUserId, now, storage, workspaceId]);
196
+ return { resolveDraft };
197
+ }
@@ -1,5 +1,12 @@
1
1
  import type { AttachmentItem, CategoryConfig, ChecklistItem, Comment, TaskAssignee, TaskItem, TaskStatus, TaxonomyDefinition, WorkstreamItem } from '../../types';
2
2
  type TaskforceTab = 'add' | 'tasks' | 'settings';
3
+ type ApplyTaskOptions = {
4
+ preserveReturnTrail?: boolean;
5
+ adoptAsBaseline?: boolean;
6
+ };
7
+ type LoadTaskOptions = ApplyTaskOptions & {
8
+ shouldApply?: () => boolean;
9
+ };
3
10
  interface UseTaskEditorNavigationInput {
4
11
  activeCategories: CategoryConfig[];
5
12
  activeTypes: Array<{
@@ -10,6 +17,7 @@ interface UseTaskEditorNavigationInput {
10
17
  attachments: Array<string | AttachmentItem>;
11
18
  checklistItems: ChecklistItem[];
12
19
  comments: Comment[];
20
+ cancelTaskDetailLoad?: () => void;
13
21
  description: string;
14
22
  editingTaskId: string | null;
15
23
  flushPendingAutoSave: () => Promise<boolean>;
@@ -17,7 +25,12 @@ interface UseTaskEditorNavigationInput {
17
25
  getPreferredCategoryValue: (categories: CategoryConfig[]) => string;
18
26
  getTitleDraft?: () => string;
19
27
  lastUsedCategory: string;
28
+ loadTaskDetail?: (task: TaskItem, options?: {
29
+ shouldCommit?: () => boolean;
30
+ force?: boolean;
31
+ }) => Promise<TaskItem | null>;
20
32
  newCommentText: string;
33
+ onTaskApplied?: (task: TaskItem) => void;
21
34
  relationshipTasks: TaskItem[];
22
35
  workstreams: WorkstreamItem[];
23
36
  showArchive: boolean;
@@ -56,9 +69,9 @@ export declare function useTaskEditorNavigation(input: UseTaskEditorNavigationIn
56
69
  resetForm: () => void;
57
70
  resolveTaskIdInput: (value: string) => TaskItem | null;
58
71
  resolveWorkstreamIdInput: (value: string) => WorkstreamItem | null;
59
- handleEdit: (task: TaskItem, options?: {
60
- preserveReturnTrail?: boolean;
61
- }) => void;
72
+ handleEdit: (task: TaskItem, options?: ApplyTaskOptions) => void;
73
+ refreshTaskDetail: (task: TaskItem, options?: LoadTaskOptions) => Promise<TaskItem | null>;
74
+ applyTaskToEditor: (task: TaskItem, options?: ApplyTaskOptions) => void;
62
75
  handleOpenTaskById: (taskId: string) => void;
63
76
  returnToPreviousTask: () => boolean;
64
77
  clearReturnToParentTask: () => void;
@@ -1,17 +1,26 @@
1
- import { useCallback } from 'react';
1
+ import { useCallback, useRef } from 'react';
2
2
  import { DEFAULT_TYPE_VALUE } from '../../shared/taxonomyDefaults.js';
3
3
  import { getTaskReferenceLabel, parseTaskReference } from '../../utils/taskReferences';
4
4
  import { formatWorkstreamReference, parseWorkstreamReference } from '../../utils/planningReferences';
5
5
  import { buildDefaultFormTaxonomies } from '../../utils/taxonomyFormDefaults.js';
6
6
  export function useTaskEditorNavigation(input) {
7
- const { activeCategories, activeTypes, activeTab, attachments, checklistItems, comments, description, editingTaskId, flushPendingAutoSave, getDescriptionDraft, getPreferredCategoryValue, getTitleDraft, lastUsedCategory, newCommentText, relationshipTasks, workstreams, showArchive, taxonomies, setActiveTab, setAssignee, setAttachments, setAttachmentsDirty, setCategory, setChecklistItems, setComments, setComplexity, setDescription, setDueDate, setEditingTaskId, setError, setFormTaxonomies, setIsOpen, setNewCommentText, setPendingNavigation, setWorkstreamInput, setPriority, setScheduledDate, setShowArchive, setStatus, setTaskReturnTrail, setTitle, setType, setUnsavedModalOpen, taskReturnTrail, title, } = input;
7
+ const { activeCategories, activeTypes, activeTab, attachments, checklistItems, comments, cancelTaskDetailLoad, description, editingTaskId, flushPendingAutoSave, getDescriptionDraft, getPreferredCategoryValue, getTitleDraft, lastUsedCategory, loadTaskDetail, newCommentText, onTaskApplied, relationshipTasks, workstreams, showArchive, taxonomies, setActiveTab, setAssignee, setAttachments, setAttachmentsDirty, setCategory, setChecklistItems, setComments, setComplexity, setDescription, setDueDate, setEditingTaskId, setError, setFormTaxonomies, setIsOpen, setNewCommentText, setPendingNavigation, setWorkstreamInput, setPriority, setScheduledDate, setShowArchive, setStatus, setTaskReturnTrail, setTitle, setType, setUnsavedModalOpen, taskReturnTrail, title, } = input;
8
+ const editRequestSequenceRef = useRef(0);
9
+ const invalidatePendingEdit = useCallback(() => {
10
+ editRequestSequenceRef.current += 1;
11
+ cancelTaskDetailLoad?.();
12
+ }, [cancelTaskDetailLoad]);
8
13
  const getPreferredTypeValue = useCallback(() => {
9
14
  return activeTypes.find((entry) => String(entry.value || '').trim().length > 0)?.value || DEFAULT_TYPE_VALUE;
10
15
  }, [activeTypes]);
11
16
  const handleNavigation = useCallback(async (action) => {
17
+ const navigate = () => {
18
+ invalidatePendingEdit();
19
+ action();
20
+ };
12
21
  if (editingTaskId) {
13
22
  if (await flushPendingAutoSave()) {
14
- action();
23
+ navigate();
15
24
  }
16
25
  return;
17
26
  }
@@ -22,10 +31,10 @@ export function useTaskEditorNavigation(input) {
22
31
  comments.length > 0 ||
23
32
  attachments.length > 0;
24
33
  if (!hasChanges) {
25
- action();
34
+ navigate();
26
35
  return;
27
36
  }
28
- setPendingNavigation(() => action);
37
+ setPendingNavigation(() => navigate);
29
38
  setUnsavedModalOpen(true);
30
39
  }, [
31
40
  attachments,
@@ -36,15 +45,18 @@ export function useTaskEditorNavigation(input) {
36
45
  getDescriptionDraft,
37
46
  getTitleDraft,
38
47
  checklistItems,
48
+ invalidatePendingEdit,
39
49
  newCommentText,
40
50
  setPendingNavigation,
41
51
  setUnsavedModalOpen,
42
52
  title,
43
53
  ]);
44
54
  const handleClose = useCallback(() => {
55
+ invalidatePendingEdit();
45
56
  setIsOpen(false);
46
- }, [setIsOpen]);
57
+ }, [invalidatePendingEdit, setIsOpen]);
47
58
  const resetForm = useCallback(() => {
59
+ invalidatePendingEdit();
48
60
  setEditingTaskId(null);
49
61
  setTitle('');
50
62
  setDescription('');
@@ -76,6 +88,7 @@ export function useTaskEditorNavigation(input) {
76
88
  activeCategories,
77
89
  getPreferredCategoryValue,
78
90
  getPreferredTypeValue,
91
+ invalidatePendingEdit,
79
92
  lastUsedCategory,
80
93
  setAssignee,
81
94
  setAttachments,
@@ -134,7 +147,7 @@ export function useTaskEditorNavigation(input) {
134
147
  return exactTitle;
135
148
  return null;
136
149
  }, [workstreams]);
137
- const handleEdit = useCallback((task, options) => {
150
+ const applyTaskToEditor = useCallback((task, options) => {
138
151
  if (!options?.preserveReturnTrail) {
139
152
  setTaskReturnTrail([]);
140
153
  }
@@ -157,6 +170,8 @@ export function useTaskEditorNavigation(input) {
157
170
  setAttachments(task.attachments || []);
158
171
  setAttachmentsDirty(false);
159
172
  setActiveTab('add');
173
+ if (options?.adoptAsBaseline !== false)
174
+ onTaskApplied?.(task);
160
175
  }, [
161
176
  activeCategories,
162
177
  getPreferredCategoryValue,
@@ -179,20 +194,62 @@ export function useTaskEditorNavigation(input) {
179
194
  setTaskReturnTrail,
180
195
  setTitle,
181
196
  setType,
197
+ onTaskApplied,
182
198
  workstreams
183
199
  ]);
200
+ const loadAndApplyTask = useCallback(async (task, options) => {
201
+ const requestSequence = editRequestSequenceRef.current + 1;
202
+ editRequestSequenceRef.current = requestSequence;
203
+ if (!loadTaskDetail) {
204
+ cancelTaskDetailLoad?.();
205
+ applyTaskToEditor(task, { preserveReturnTrail: options?.preserveReturnTrail });
206
+ return task;
207
+ }
208
+ if (!task.isSummary) {
209
+ cancelTaskDetailLoad?.();
210
+ const detail = options?.shouldApply
211
+ ? await loadTaskDetail(task, { shouldCommit: options.shouldApply })
212
+ : await loadTaskDetail(task);
213
+ if (editRequestSequenceRef.current === requestSequence) {
214
+ const taskToApply = detail || task;
215
+ applyTaskToEditor(taskToApply, { preserveReturnTrail: options?.preserveReturnTrail });
216
+ return taskToApply;
217
+ }
218
+ return null;
219
+ }
220
+ const detail = options?.shouldApply
221
+ ? await loadTaskDetail(task, { shouldCommit: options.shouldApply })
222
+ : await loadTaskDetail(task);
223
+ if (detail
224
+ && editRequestSequenceRef.current === requestSequence) {
225
+ applyTaskToEditor(detail, { preserveReturnTrail: options?.preserveReturnTrail });
226
+ return detail;
227
+ }
228
+ return null;
229
+ }, [applyTaskToEditor, cancelTaskDetailLoad, loadTaskDetail]);
230
+ const handleEdit = useCallback((task, options) => {
231
+ void loadAndApplyTask(task, options);
232
+ }, [loadAndApplyTask]);
233
+ const refreshTaskDetail = useCallback((task, options) => loadAndApplyTask(task, options), [loadAndApplyTask]);
184
234
  const handleOpenTaskById = useCallback((taskId) => {
185
235
  const target = relationshipTasks.find((task) => task.id === taskId);
186
236
  if (!target)
187
237
  return;
238
+ const openTarget = () => {
239
+ if (activeTab === 'add' && editingTaskId && editingTaskId !== target.id) {
240
+ setTaskReturnTrail((prev) => prev[prev.length - 1] === editingTaskId ? prev : [...prev, editingTaskId]);
241
+ }
242
+ if (target.isArchived && !showArchive) {
243
+ setShowArchive(true);
244
+ }
245
+ handleEdit(target, { preserveReturnTrail: true });
246
+ };
188
247
  if (activeTab === 'add' && editingTaskId && editingTaskId !== target.id) {
189
- setTaskReturnTrail((prev) => prev[prev.length - 1] === editingTaskId ? prev : [...prev, editingTaskId]);
190
- }
191
- if (target.isArchived && !showArchive) {
192
- setShowArchive(true);
248
+ void handleNavigation(openTarget);
249
+ return;
193
250
  }
194
- handleEdit(target, { preserveReturnTrail: true });
195
- }, [activeTab, editingTaskId, handleEdit, relationshipTasks, setShowArchive, setTaskReturnTrail, showArchive]);
251
+ openTarget();
252
+ }, [activeTab, editingTaskId, handleEdit, handleNavigation, relationshipTasks, setShowArchive, setTaskReturnTrail, showArchive]);
196
253
  const returnToPreviousTask = useCallback(() => {
197
254
  if (activeTab !== 'add' || !editingTaskId || taskReturnTrail.length === 0)
198
255
  return false;
@@ -224,6 +281,8 @@ export function useTaskEditorNavigation(input) {
224
281
  resolveTaskIdInput,
225
282
  resolveWorkstreamIdInput,
226
283
  handleEdit,
284
+ refreshTaskDetail,
285
+ applyTaskToEditor,
227
286
  handleOpenTaskById,
228
287
  returnToPreviousTask,
229
288
  clearReturnToParentTask
@@ -1,6 +1,7 @@
1
1
  import type { AttachmentItem, CategoryConfig, ChecklistItem, TaskAssignee, TaskItem, TaskStatus, TaxonomyDefinition, WorkstreamItem } from '../../types';
2
2
  import type { DurableTaskCreateMutationAdapter } from '../sync/useDurableTaskCreateMutation.js';
3
3
  import { createCloudTaskHttpMutationClient } from './taskHttpMutation.js';
4
+ import type { TaskEditorDraft } from '../../utils/taskEditorReconciliation.js';
4
5
  interface UseTaskFormActionsInput {
5
6
  activeCategories: CategoryConfig[];
6
7
  activeTab: 'add' | 'tasks' | 'settings';
@@ -20,6 +21,8 @@ interface UseTaskFormActionsInput {
20
21
  taskHttpMutationClient?: ReturnType<typeof createCloudTaskHttpMutationClient>;
21
22
  updateTask?: (taskId: string, updates: Partial<TaskItem>) => Promise<boolean | void>;
22
23
  editingTaskId: string | null;
24
+ editorBaselineDraft?: TaskEditorDraft | null;
25
+ revisionConflict?: boolean;
23
26
  fetchTasks: (isSilent?: boolean) => Promise<void>;
24
27
  formTaxonomies: Record<string, string | number | string[] | number[]>;
25
28
  getAttachmentsDraft?: () => Array<string | AttachmentItem>;
@@ -55,6 +58,9 @@ interface UseTaskFormActionsInput {
55
58
  export type TaskAutoSaveState = 'idle' | 'saving' | 'saved' | 'error';
56
59
  export declare function useTaskFormActions(input: UseTaskFormActionsInput): {
57
60
  autoSaveState: TaskAutoSaveState;
61
+ hasUnsavedChanges: boolean;
62
+ hasUnsavedChangesNow: () => boolean;
63
+ hasPendingAutoSave: boolean;
58
64
  flushAutoSave: () => Promise<boolean>;
59
65
  scheduleWarningPrompt: {
60
66
  dueDate: string;