@taskforcehq/taskforce 0.3.330 → 0.3.333

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 (107) hide show
  1. package/README.md +7 -2
  2. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +2 -2
  3. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.js +2 -2
  4. package/dist/components/features/planning/PlanningModule.js +15 -3
  5. package/dist/components/features/planning/planningMetadata.d.ts +10 -0
  6. package/dist/components/features/planning/planningMetadata.js +8 -0
  7. package/dist/components/views/StandaloneLayout.js +35 -1
  8. package/dist/components/views/panels/PlanningDrawer.d.ts +2 -1
  9. package/dist/components/views/panels/PlanningDrawer.js +12 -8
  10. package/dist/config/envSchema.js +7 -0
  11. package/dist/core/McpTokenService.d.ts +2 -0
  12. package/dist/core/McpTokenService.js +13 -5
  13. package/dist/core/Taskforce.d.ts +11 -3
  14. package/dist/core/Taskforce.js +33 -3
  15. package/dist/core/types.d.ts +1 -0
  16. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +2 -0
  17. package/dist/documentReviews/DocumentReviewCommentStore.js +28 -9
  18. package/dist/mcp/httpProtocolRouting.d.ts +1 -0
  19. package/dist/mcp/httpProtocolRouting.js +8 -5
  20. package/dist/mcp/runtime.js +24 -5
  21. package/dist/mcp/taskPlanningRegistrar.js +2 -2
  22. package/dist/mcp/toolProfiles.js +1 -0
  23. package/dist/mcp/toolRegistry.js +8 -8
  24. package/dist/server/buildInfo.d.ts +1 -0
  25. package/dist/server/buildInfo.js +5 -1
  26. package/dist/server/cloudMcpCapacityTelemetry.d.ts +68 -0
  27. package/dist/server/cloudMcpCapacityTelemetry.js +221 -0
  28. package/dist/server/localWorkspaceSyncServerRuntime.js +13 -5
  29. package/dist/server/routes/admin.js +14 -2
  30. package/dist/server/routes/localService.js +1 -0
  31. package/dist/server/routes/shared.d.ts +3 -0
  32. package/dist/server/routes/shared.js +280 -240
  33. package/dist/server/routes/syncPushRoutes.d.ts +3 -0
  34. package/dist/server/routes/syncPushRoutes.js +8 -2
  35. package/dist/service/localServiceCli.d.ts +2 -2
  36. package/dist/service/localServiceCli.js +4 -3
  37. package/dist/services/executorPhaseAIssuerCore.d.ts +13 -0
  38. package/dist/services/executorPhaseAIssuerCore.js +107 -0
  39. package/dist/services/taskforceAgentMcpBridge.d.ts +1 -1
  40. package/dist/services/taskforceAgentMcpBridge.js +35 -5
  41. package/dist/shared/executorPhaseASchemaValidation.js +1 -1
  42. package/dist/storage/postgresAdapter.js +3 -1
  43. package/dist/storage/postgresRequestDiagnostics.d.ts +9 -1
  44. package/dist/storage/postgresRequestDiagnostics.js +15 -2
  45. package/dist/storage/postgresWorker.js +13 -3
  46. package/dist/storage/postgresWorkerProtocol.d.ts +6 -0
  47. package/dist/storage/workflowStore.d.ts +2 -0
  48. package/dist/storage/workflowStore.js +39 -2
  49. package/dist/sync/cloudSyncApi.js +4 -2
  50. package/dist/sync/collaborationSyncState.js +5 -0
  51. package/dist/sync/coordinator/localWorkspaceSyncRunner.d.ts +3 -1
  52. package/dist/sync/coordinator/localWorkspaceSyncRunner.js +18 -0
  53. package/dist/sync/coordinator/localWorkspaceSyncRunnerState.d.ts +1 -0
  54. package/dist/sync/coordinator/localWorkspaceSyncRunnerState.js +7 -6
  55. package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +2 -2
  56. package/dist/sync/engine/workspaceSyncEngineLocalApply.d.ts +1 -0
  57. package/dist/sync/engine/workspaceSyncEnginePushCompletion.d.ts +6 -0
  58. package/dist/sync/engine/workspaceSyncEnginePushCompletion.js +34 -1
  59. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +2 -2
  60. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +4 -1
  61. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.d.ts +1 -0
  62. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +55 -10
  63. package/dist/sync/syncApplyHandlers.d.ts +1 -0
  64. package/dist/sync/syncApplyHandlers.js +72 -15
  65. package/dist/sync/syncService.d.ts +1 -0
  66. package/dist/sync/syncService.js +5 -2
  67. package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.d.ts +7 -7
  68. package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.js +50 -13
  69. package/dist/sync/v3/localOutboxRecoveryPreparationService.js +8 -2
  70. package/dist/sync/v3/localTaskCreateOutboxService.d.ts +3 -1
  71. package/dist/sync/v3/localTaskCreateOutboxService.js +6 -1
  72. package/dist/sync/v3/localTaskDeleteOutboxService.js +1 -0
  73. package/dist/sync/v3/localTaskLifecycleOutboxService.d.ts +2 -1
  74. package/dist/sync/v3/localTaskLifecycleOutboxService.js +5 -1
  75. package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -1
  76. package/dist/sync/v3/localTaskMetadataOutboxService.js +6 -2
  77. package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -1
  78. package/dist/sync/v3/localTaskStatusOutboxService.js +20 -5
  79. package/dist/sync/v3/syncDurabilityStore.d.ts +2 -1
  80. package/dist/sync/v3/syncDurabilityStore.js +34 -6
  81. package/dist/sync/v3/workflowRuntimeMutationService.d.ts +5 -0
  82. package/dist/sync/v3/workflowRuntimeMutationService.js +124 -71
  83. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +25 -3
  84. package/dist/sync/v3/workspaceSyncV3Feed.d.ts +1 -0
  85. package/dist/sync/v3/workspaceSyncV3Feed.js +6 -1
  86. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +2 -0
  87. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +49 -18
  88. package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.d.ts +1 -0
  89. package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.js +50 -5
  90. package/dist/ui/assets/{AiIdentityRosterCard-OcSz1PkK.js → AiIdentityRosterCard-Cfvb9Nuv.js} +1 -1
  91. package/dist/ui/assets/{AiProfilesModule-ByhYW5Xi.js → AiProfilesModule-0oR70eFG.js} +1 -1
  92. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BTM6nIg_.js → AnnotatedAttachmentWorkspace-C33iH_r_.js} +1 -1
  93. package/dist/ui/assets/{AssetTraySortControl-DabEG8L4.js → AssetTraySortControl-BAqxUtke.js} +1 -1
  94. package/dist/ui/assets/{ContextAttachmentManager-Coa8yVGM.js → ContextAttachmentManager-BudIbOR9.js} +2 -2
  95. package/dist/ui/assets/{DocumentWorkspace-BB0qhdix.js → DocumentWorkspace-A8dFQyId.js} +1 -1
  96. package/dist/ui/assets/{EntityActivityTimeline-CZ8x4UJE.js → EntityActivityTimeline-g7vtdsx1.js} +1 -1
  97. package/dist/ui/assets/PlanningModule-D-DeQs4F.js +1 -0
  98. package/dist/ui/assets/{PlansPage-D86pZSSl.js → PlansPage-BWmfGi0Z.js} +1 -1
  99. package/dist/ui/assets/{TaskContextUpload-BVIkU9yT.js → TaskContextUpload-BA_lQmS9.js} +1 -1
  100. package/dist/ui/assets/{TaskSettings-Cv1mNhv_.js → TaskSettings-DAlH4YIP.js} +1 -1
  101. package/dist/ui/assets/{TaskforceAgentsModule-gmH-PD2q.js → TaskforceAgentsModule-DmeCf6pJ.js} +2 -2
  102. package/dist/ui/assets/{WorkflowManagerModule-DbfnHJ-f.js → WorkflowManagerModule-p08eDOaa.js} +1 -1
  103. package/dist/ui/assets/index-GHJgcW3j.js +7 -0
  104. package/dist/ui/index.html +1 -1
  105. package/package.json +3 -1
  106. package/dist/ui/assets/PlanningModule-pRPjR7v5.js +0 -1
  107. package/dist/ui/assets/index-CTsDBaef.js +0 -7
package/README.md CHANGED
@@ -68,12 +68,17 @@ To confirm which workspace Taskforce is using:
68
68
  taskforce status
69
69
  ```
70
70
 
71
- To connect a local AI client through MCP, start the MCP server from the same workspace:
71
+ To connect a local AI client through MCP, run the MCP entrypoint from the same workspace:
72
72
 
73
73
  ```bash
74
74
  taskforce mcp
75
75
  ```
76
76
 
77
+ The command is a thin stdio adapter for the shared local Taskforce service. Multiple
78
+ local AI clients and `taskforce open` reuse one service process, which remains the sole
79
+ owner of the workspace SQLite database and local sync executor. Client configuration
80
+ does not require a streaming option or a cloud token.
81
+
77
82
  ## What Taskforce Includes
78
83
 
79
84
  ### Task Management
@@ -115,7 +120,7 @@ taskforce mcp
115
120
 
116
121
  Taskforce includes built-in MCP support so AI clients can work directly with your workspace.
117
122
 
118
- Local MCP is for AI clients running on the same machine as your workspace. It does not require a cloud token.
123
+ Local MCP is for AI clients running on the same machine as your workspace. It does not require a cloud token. Generated configurations use a shared-proxy stdio entrypoint; the Global CLI configuration launches `taskforce mcp`, while Local / npx configurations launch the corresponding packaged or source entrypoint. Each discovers or starts the same shared local service and forwards stdio MCP messages to it. Use `taskforce service status <project>` to inspect that service and `taskforce service stop <project> --drain` for a routine administrative stop after active clients disconnect.
119
124
 
120
125
  Cloud MCP is for hosted remote clients and uses token-based access.
121
126
 
@@ -10,8 +10,8 @@ export declare const TASKFORCE_TOOL_MODULES: readonly [{
10
10
  readonly icon: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
11
11
  }, {
12
12
  readonly key: "documents";
13
- readonly label: "Documents";
14
- readonly description: "Find and read Taskforce documents and their review context.";
13
+ readonly label: "Documents and reviews";
14
+ readonly description: "Find and read Taskforce documents, inspect review context, and post document review feedback.";
15
15
  readonly icon: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
16
16
  }, {
17
17
  readonly key: "images";
@@ -17,8 +17,8 @@ export const TASKFORCE_TOOL_MODULES = [
17
17
  },
18
18
  {
19
19
  key: 'documents',
20
- label: 'Documents',
21
- description: 'Find and read Taskforce documents and their review context.',
20
+ label: 'Documents and reviews',
21
+ description: 'Find and read Taskforce documents, inspect review context, and post document review feedback.',
22
22
  icon: FileText,
23
23
  },
24
24
  {
@@ -11,6 +11,7 @@ import { PlanningOwnerDropdown, normalizePlanningOwnerOptions, } from './Plannin
11
11
  import { buildPlanningNavigatorResults, PLANNING_NAVIGATOR_SORT_OPTIONS, } from './planningNavigator';
12
12
  import { PlanningEntitySummary, PlanningReferenceBadge, } from './PlanningEntitySummary';
13
13
  import { PlanningVisualIdentity } from './PlanningVisualIdentity';
14
+ import { hasPlanningMetadataChanges } from './planningMetadata';
14
15
  import { PlanningTaskCard } from './PlanningTaskCard';
15
16
  import { PlanningTaskArrangeControl } from './PlanningTaskArrangeControl';
16
17
  import planningDropdownStyles from './PlanningDropdownControl.module.css';
@@ -21,7 +22,7 @@ import { PlanningUnlinkConfirmation, } from './PlanningUnlinkConfirmation';
21
22
  import { PlanningLinkPicker } from './PlanningLinkPicker';
22
23
  import { PlanningEntityActivity, PlanningEntityContext, PlanningEntityDescription, } from './PlanningEntityDetails';
23
24
  import styles from './PlanningModule.module.css';
24
- function PlanningWorkspaceEditor({ entityType, mode, initialTitle = '', initialDescription = '', ownerOptions, fixedInitiativeId, formId, showActions = true, onSavingChange, onCancel, onSave, }) {
25
+ function PlanningWorkspaceEditor({ entityType, mode, initialTitle = '', initialDescription = '', ownerOptions, fixedInitiativeId, formId, showActions = true, onSavingChange, onDirtyChange, onCancel, onSave, }) {
25
26
  const label = entityType === 'initiative' ? 'Initiative' : 'Workstream';
26
27
  const titleInputId = React.useId();
27
28
  const titleErrorId = React.useId();
@@ -35,6 +36,10 @@ function PlanningWorkspaceEditor({ entityType, mode, initialTitle = '', initialD
35
36
  const [titleError, setTitleError] = React.useState(null);
36
37
  const [saveError, setSaveError] = React.useState(null);
37
38
  const [isSaving, setIsSaving] = React.useState(false);
39
+ const isDirty = mode === 'create' || hasPlanningMetadataChanges({ title: initialTitle, description: initialDescription }, { title, description });
40
+ React.useEffect(() => {
41
+ onDirtyChange?.(isDirty);
42
+ }, [isDirty, onDirtyChange]);
38
43
  React.useEffect(() => {
39
44
  titleInputRef.current?.focus();
40
45
  }, []);
@@ -48,6 +53,8 @@ function PlanningWorkspaceEditor({ entityType, mode, initialTitle = '', initialD
48
53
  titleInputRef.current?.focus();
49
54
  return;
50
55
  }
56
+ if (!isDirty)
57
+ return;
51
58
  setTitleError(null);
52
59
  setSaveError(null);
53
60
  setIsSaving(true);
@@ -87,7 +94,7 @@ function PlanningWorkspaceEditor({ entityType, mode, initialTitle = '', initialD
87
94
  setTitleError(null);
88
95
  }, placeholder: entityType === 'initiative' ? 'Enter initiative title' : 'Enter workstream title' }), titleError ? _jsx("p", { id: titleErrorId, className: "tf-text-error", children: titleError }) : null] }), _jsxs("div", { className: styles.workspaceEditorField, children: [_jsxs("label", { htmlFor: descriptionInputId, children: [label, " description"] }), _jsx("textarea", { id: descriptionInputId, value: description, disabled: isSaving, rows: 5, onChange: (event) => setDescription(event.target.value), placeholder: entityType === 'initiative'
89
96
  ? 'Describe the broader outcome this initiative is meant to achieve.'
90
- : 'Describe the lane of work this workstream will coordinate.' })] }), mode === 'create' && ownerOptions ? (_jsxs("div", { className: styles.workspaceEditorField, children: [_jsxs("span", { children: [label, " owner"] }), _jsx(PlanningOwnerDropdown, { value: ownerId, options: ownerOptions, onChange: setOwnerId, disabled: isSaving, ariaLabel: `${label} owner`, className: `${planningDropdownStyles.control} ${styles.selectedEntityOwnerDropdown}` })] })) : null, saveError ? _jsx("p", { className: "tf-text-error", role: "alert", children: saveError }) : null, showActions ? _jsxs("div", { className: styles.workspaceEditorActions, children: [_jsx("button", { type: "button", className: "tf-button-secondary tf-button-compact", onClick: onCancel, disabled: isSaving, children: "Cancel" }), _jsxs("button", { type: "submit", className: "tf-button-primary tf-button-compact", disabled: isSaving, children: [isSaving ? _jsx(Loader2, { size: 14, className: styles.spinner, "aria-hidden": "true" }) : null, mode === 'create' ? `Create ${label}` : 'Save'] })] }) : null] }));
97
+ : 'Describe the lane of work this workstream will coordinate.' })] }), mode === 'create' && ownerOptions ? (_jsxs("div", { className: styles.workspaceEditorField, children: [_jsxs("span", { children: [label, " owner"] }), _jsx(PlanningOwnerDropdown, { value: ownerId, options: ownerOptions, onChange: setOwnerId, disabled: isSaving, ariaLabel: `${label} owner`, className: `${planningDropdownStyles.control} ${styles.selectedEntityOwnerDropdown}` })] })) : null, saveError ? _jsx("p", { className: "tf-text-error", role: "alert", children: saveError }) : null, showActions ? _jsxs("div", { className: styles.workspaceEditorActions, children: [_jsx("button", { type: "button", className: "tf-button-secondary tf-button-compact", onClick: onCancel, disabled: isSaving, children: "Cancel" }), _jsxs("button", { type: "submit", className: "tf-button-primary tf-button-compact", disabled: isSaving || !isDirty, children: [isSaving ? _jsx(Loader2, { size: 14, className: styles.spinner, "aria-hidden": "true" }) : null, mode === 'create' ? `Create ${label}` : 'Save'] })] }) : null] }));
91
98
  }
92
99
  function InitiativeButton({ initiative, ownerOption, ownerOptions, workstreamCount, selected, onSelect, onChangeOwner, onChangeIdentity, }) {
93
100
  return (_jsxs("div", { className: `${styles.entityCard} ${initiative.isArchived ? styles.entityCardArchived : ''} ${selected ? styles.entityCardSelected : ''}`.trim(), children: [_jsx(PlanningReferenceBadge, { label: formatInitiativeReference(initiative), entityType: "initiative", entityName: initiative.title, interactive: false }), _jsx("div", { className: styles.entityCardBody, children: _jsx("div", { role: "button", tabIndex: 0, className: styles.entityCardContentButton, onClick: onSelect, onKeyDown: (event) => {
@@ -115,6 +122,7 @@ function SelectedEntityContext({ item, entityType, ownerOptions, onChangeOwner,
115
122
  const [isSavingOwner, setIsSavingOwner] = React.useState(false);
116
123
  const [isEditing, setIsEditing] = React.useState(false);
117
124
  const [isEditingSaving, setIsEditingSaving] = React.useState(false);
125
+ const [isEditingDirty, setIsEditingDirty] = React.useState(false);
118
126
  const [archiveConfirmationVisible, setArchiveConfirmationVisible] = React.useState(false);
119
127
  const [archiveBusy, setArchiveBusy] = React.useState(false);
120
128
  const editFormId = React.useId();
@@ -140,6 +148,7 @@ function SelectedEntityContext({ item, entityType, ownerOptions, onChangeOwner,
140
148
  React.useEffect(() => {
141
149
  setIsEditing(false);
142
150
  setIsEditingSaving(false);
151
+ setIsEditingDirty(false);
143
152
  setArchiveConfirmationVisible(false);
144
153
  setArchiveBusy(false);
145
154
  }, [item.id]);
@@ -199,7 +208,10 @@ function SelectedEntityContext({ item, entityType, ownerOptions, onChangeOwner,
199
208
  ? archiveConfirmationVisible
200
209
  : undefined, children: archiveState.mode === 'archived'
201
210
  ? _jsx(RotateCcw, { size: 14, "aria-hidden": "true" })
202
- : _jsx(Archive, { size: 14, "aria-hidden": "true" }) })) : null, !isEditing && !item.isArchived && onUpdate ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: () => setIsEditing(true), "aria-label": `Edit ${label.toLowerCase()}`, title: `Edit ${label.toLowerCase()}`, children: _jsx(Pencil, { size: 14, "aria-hidden": "true" }) })) : null, isEditing ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "tf-button-secondary tf-button-compact", onClick: () => setIsEditing(false), disabled: isEditingSaving, children: "Cancel" }), _jsx("button", { type: "submit", form: editFormId, className: "tf-button-primary tf-button-compact", disabled: isEditingSaving, children: "Save" })] })) : null] })] }), archiveConfirmationVisible && archiveState.mode === 'ready' ? (_jsx(PlanningArchiveConfirmation, { entityType: entityType, title: item.title, activeTaskCount: item.archiveActiveTaskCount, activeWorkstreamCount: item.archiveActiveWorkstreamCount, busy: archiveBusy, onCancel: () => setArchiveConfirmationVisible(false), onConfirm: () => void handleConfirmArchive() })) : null, relationshipContent, isEditing && onUpdate ? (_jsx(PlanningWorkspaceEditor, { entityType: entityType, mode: "edit", formId: editFormId, showActions: false, onSavingChange: setIsEditingSaving, initialTitle: item.title, initialDescription: item.description || '', onCancel: () => setIsEditing(false), onSave: async (input) => {
211
+ : _jsx(Archive, { size: 14, "aria-hidden": "true" }) })) : null, !isEditing && !item.isArchived && onUpdate ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: () => {
212
+ setIsEditingDirty(false);
213
+ setIsEditing(true);
214
+ }, "aria-label": `Edit ${label.toLowerCase()}`, title: `Edit ${label.toLowerCase()}`, children: _jsx(Pencil, { size: 14, "aria-hidden": "true" }) })) : null, isEditing ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "tf-button-secondary tf-button-compact", onClick: () => setIsEditing(false), disabled: isEditingSaving, children: "Cancel" }), _jsx("button", { type: "submit", form: editFormId, className: "tf-button-primary tf-button-compact", disabled: isEditingSaving || !isEditingDirty, children: "Save" })] })) : null] })] }), archiveConfirmationVisible && archiveState.mode === 'ready' ? (_jsx(PlanningArchiveConfirmation, { entityType: entityType, title: item.title, activeTaskCount: item.archiveActiveTaskCount, activeWorkstreamCount: item.archiveActiveWorkstreamCount, busy: archiveBusy, onCancel: () => setArchiveConfirmationVisible(false), onConfirm: () => void handleConfirmArchive() })) : null, relationshipContent, isEditing && onUpdate ? (_jsx(PlanningWorkspaceEditor, { entityType: entityType, mode: "edit", formId: editFormId, showActions: false, onSavingChange: setIsEditingSaving, onDirtyChange: setIsEditingDirty, initialTitle: item.title, initialDescription: item.description || '', onCancel: () => setIsEditing(false), onSave: async (input) => {
203
215
  await onUpdate(input);
204
216
  setIsEditing(false);
205
217
  } }, `${entityType}:${item.id}`)) : (_jsxs(_Fragment, { children: [_jsx("h3", { className: "tf-heading-card", children: item.title }), _jsx(PlanningEntityDescription, { description: item.description, label: label, taskReferences: taskReferences, className: `${styles.selectedEntityDescription} tf-scrollbar`.trim(), markdownClassName: styles.selectedEntityDescriptionMarkdown })] })), _jsxs("div", { className: styles.selectedEntityOwnerField, children: [_jsx("span", { children: "Owner" }), _jsx(PlanningOwnerDropdown, { value: ownerValue, options: ownerOptions, onChange: (nextOwnerId) => void handleOwnerChange(nextOwnerId), disabled: !onChangeOwner || Boolean(item.isArchived) || isSavingOwner, ariaLabel: `${label} owner`, className: `${planningDropdownStyles.control} ${styles.selectedEntityOwnerDropdown}` })] }), _jsx("div", { className: styles.selectedEntityDetailsSection, children: _jsx(PlanningEntityContext, { entityType: entityType, entityId: item.id, referenceLabel: entityType === 'initiative'
@@ -0,0 +1,10 @@
1
+ export type PlanningMetadataDraft = {
2
+ title: string;
3
+ description?: string | null;
4
+ };
5
+ export declare function hasPlanningMetadataChanges(current: PlanningMetadataDraft, draft: PlanningMetadataDraft): boolean;
6
+ export declare function hasWorkstreamMetadataChanges(current: PlanningMetadataDraft & {
7
+ initiativeId?: string | null;
8
+ }, draft: PlanningMetadataDraft & {
9
+ initiativeId?: string | null;
10
+ }): boolean;
@@ -0,0 +1,8 @@
1
+ export function hasPlanningMetadataChanges(current, draft) {
2
+ return draft.title.trim() !== current.title.trim()
3
+ || (draft.description?.trim() || null) !== (current.description?.trim() || null);
4
+ }
5
+ export function hasWorkstreamMetadataChanges(current, draft) {
6
+ return hasPlanningMetadataChanges(current, draft)
7
+ || (draft.initiativeId || null) !== (current.initiativeId || null);
8
+ }
@@ -64,6 +64,7 @@ import { FilterToolbar } from './panels/FilterToolbar';
64
64
  import { PlanningDrawer, getPlanningDrawerWidthPx, } from './panels/PlanningDrawer';
65
65
  import { normalizePlanningTaskArrangeMode, } from '../features/planning/planningTaskArrangement';
66
66
  import { resolveWorkstreamCreateInitiativeId } from '../features/planning/planningCreateIntent';
67
+ import { hasPlanningMetadataChanges, hasWorkstreamMetadataChanges, } from '../features/planning/planningMetadata';
67
68
  import { shouldUsePlanningBootstrapTaskSummaries } from '../features/planning/planningWorkspaceModel';
68
69
  import { getWorkspaceChromeDefinition, getWorkspaceModuleDefinitions, resolveWorkspaceModule, } from './chrome/workspaceChrome';
69
70
  import { WorkspaceHeaderActions, WorkspaceHeaderDivider, WorkspaceHeaderSearch, WorkspaceHeaderSelect, WorkspacePrimaryAction, } from './chrome/WorkspaceChromeSections';
@@ -3087,6 +3088,36 @@ export function StandaloneLayout(props) {
3087
3088
  setPlanningDraftColor(null);
3088
3089
  setPlanningDraftInitiativeId('');
3089
3090
  }, []);
3091
+ const planningEditorHasChanges = useMemo(() => {
3092
+ const activeEditor = planningSecondaryEditor || planningEditor;
3093
+ if (!activeEditor || activeEditor.mode === 'create')
3094
+ return true;
3095
+ const draft = {
3096
+ title: planningDraftTitle,
3097
+ description: planningDraftDescription,
3098
+ };
3099
+ if (activeEditor.entityType === 'initiative') {
3100
+ const current = planningStructure.allInitiativeById.get(activeEditor.targetId);
3101
+ return !current || hasPlanningMetadataChanges(current, draft);
3102
+ }
3103
+ const current = planningStructure.allWorkstreamById.get(activeEditor.targetId);
3104
+ if (!current)
3105
+ return true;
3106
+ const resolvedInitiative = resolveWorkstreamCreateInitiativeId(planningStructure.allInitiatives, null, planningDraftInitiativeId);
3107
+ return resolvedInitiative.invalid || hasWorkstreamMetadataChanges(current, {
3108
+ ...draft,
3109
+ initiativeId: resolvedInitiative.initiativeId,
3110
+ });
3111
+ }, [
3112
+ planningDraftDescription,
3113
+ planningDraftInitiativeId,
3114
+ planningDraftTitle,
3115
+ planningEditor,
3116
+ planningSecondaryEditor,
3117
+ planningStructure.allInitiativeById,
3118
+ planningStructure.allInitiatives,
3119
+ planningStructure.allWorkstreamById,
3120
+ ]);
3090
3121
  const submitPlanningEditor = useCallback(async () => {
3091
3122
  const activeEditor = planningSecondaryEditor || planningEditor;
3092
3123
  if (!activeEditor || planningEditorSubmitInFlightRef.current)
@@ -3094,6 +3125,8 @@ export function StandaloneLayout(props) {
3094
3125
  const title = planningDraftTitle.trim();
3095
3126
  if (!title)
3096
3127
  return;
3128
+ if (activeEditor.mode === 'edit' && !planningEditorHasChanges)
3129
+ return;
3097
3130
  planningEditorSubmitInFlightRef.current = true;
3098
3131
  setIsSubmittingPlanningEditor(true);
3099
3132
  const isSecondaryEditor = Boolean(planningSecondaryEditor);
@@ -3188,6 +3221,7 @@ export function StandaloneLayout(props) {
3188
3221
  planningDraftOwner,
3189
3222
  planningDraftTitle,
3190
3223
  planningEditor,
3224
+ planningEditorHasChanges,
3191
3225
  planningSecondaryEditor,
3192
3226
  planningStructure.allInitiatives,
3193
3227
  props,
@@ -4230,7 +4264,7 @@ export function StandaloneLayout(props) {
4230
4264
  setIsSubmittingPlanningEditor(false);
4231
4265
  setPlanningSecondaryEditor(null);
4232
4266
  setPlanningNestedWorkstreamDetailId(null);
4233
- }, treeCollapsed: planningTreeCollapsed, primaryCollapsed: planningPrimaryCollapsed, secondaryCollapsed: planningSecondaryCollapsed, onCancelEditor: closePlanningEditor, onCancelSecondaryEditor: closePlanningSecondaryEditor, onSubmitEditor: submitPlanningEditor, isSubmittingEditor: isSubmittingPlanningEditor, onCreateInitiative: () => openPlanningCreateEditor('initiative'), onCreateWorkstream: () => openPlanningCreateEditor('workstream'), onCreateTaskInWorkstream: openAddTaskModalForWorkstream, onCreateWorkstreamInInitiative: (initiativeId) => openPlanningCreateEditor('workstream', initiativeId), onAssignInitiativeToWorkstream: handleAssignInitiativeToWorkstream, onOpenTaskById: planningWorkspaceModel.openTask, onSetTaskStatus: handleSetPlanningTaskStatus, onChangeTaskAssignee: handleChangePlanningTaskAssignee, onArchivePlanningTask: handleArchivePlanningTask, onArchiveResolvedWorkstream: handleArchiveResolvedWorkstream, onArchiveResolvedInitiative: handleArchiveResolvedInitiative, taskReferences: taskReferences, onAssignTaskToWorkstream: handleAssignTaskToWorkstream, onAttachTaskToWorkstreamByReference: handleAttachTaskToWorkstreamByReference, onAttachWorkstreamToInitiativeByReference: handleAttachWorkstreamToInitiativeByReference, onAddPlanningContextFile: handleAddPlanningContextFile, onRemovePlanningContextFile: handleRemovePlanningContextFile, onUpdatePlanningContextCaption: handleUpdatePlanningContextCaption, onToggleInitiative: togglePlanningInitiative, onSelectInitiative: selectPlanningInitiative, onSelectWorkstream: selectPlanningWorkstream, onOpenInitiativeDetails: openInitiativeDetails, onOpenWorkstreamDetails: openWorkstreamDetails, onOpenNestedWorkstreamDetails: openNestedWorkstreamDetails, onEditInitiative: (initiativeId) => openPlanningEditDialog('initiative', initiativeId), onEditWorkstream: (workstreamId) => openPlanningEditDialog('workstream', workstreamId), onChangePlanningOwner: handleChangePlanningOwner, onChangePlanningIdentity: handleChangePlanningIdentity, onArchiveInitiative: (initiativeId) => {
4267
+ }, treeCollapsed: planningTreeCollapsed, primaryCollapsed: planningPrimaryCollapsed, secondaryCollapsed: planningSecondaryCollapsed, onCancelEditor: closePlanningEditor, onCancelSecondaryEditor: closePlanningSecondaryEditor, onSubmitEditor: submitPlanningEditor, isSubmittingEditor: isSubmittingPlanningEditor, isEditorSubmitDisabled: Boolean((planningSecondaryEditor || planningEditor)?.mode === 'edit') && !planningEditorHasChanges, onCreateInitiative: () => openPlanningCreateEditor('initiative'), onCreateWorkstream: () => openPlanningCreateEditor('workstream'), onCreateTaskInWorkstream: openAddTaskModalForWorkstream, onCreateWorkstreamInInitiative: (initiativeId) => openPlanningCreateEditor('workstream', initiativeId), onAssignInitiativeToWorkstream: handleAssignInitiativeToWorkstream, onOpenTaskById: planningWorkspaceModel.openTask, onSetTaskStatus: handleSetPlanningTaskStatus, onChangeTaskAssignee: handleChangePlanningTaskAssignee, onArchivePlanningTask: handleArchivePlanningTask, onArchiveResolvedWorkstream: handleArchiveResolvedWorkstream, onArchiveResolvedInitiative: handleArchiveResolvedInitiative, taskReferences: taskReferences, onAssignTaskToWorkstream: handleAssignTaskToWorkstream, onAttachTaskToWorkstreamByReference: handleAttachTaskToWorkstreamByReference, onAttachWorkstreamToInitiativeByReference: handleAttachWorkstreamToInitiativeByReference, onAddPlanningContextFile: handleAddPlanningContextFile, onRemovePlanningContextFile: handleRemovePlanningContextFile, onUpdatePlanningContextCaption: handleUpdatePlanningContextCaption, onToggleInitiative: togglePlanningInitiative, onSelectInitiative: selectPlanningInitiative, onSelectWorkstream: selectPlanningWorkstream, onOpenInitiativeDetails: openInitiativeDetails, onOpenWorkstreamDetails: openWorkstreamDetails, onOpenNestedWorkstreamDetails: openNestedWorkstreamDetails, onEditInitiative: (initiativeId) => openPlanningEditDialog('initiative', initiativeId), onEditWorkstream: (workstreamId) => openPlanningEditDialog('workstream', workstreamId), onChangePlanningOwner: handleChangePlanningOwner, onChangePlanningIdentity: handleChangePlanningIdentity, onArchiveInitiative: (initiativeId) => {
4234
4268
  void props.archiveInitiative(initiativeId).then(() => {
4235
4269
  pushNotice('Initiative archived', 'success');
4236
4270
  }).catch((error) => {
@@ -116,6 +116,7 @@ type PlanningDrawerProps = {
116
116
  onCancelSecondaryEditor?: () => void;
117
117
  onSubmitEditor: () => void;
118
118
  isSubmittingEditor: boolean;
119
+ isEditorSubmitDisabled?: boolean;
119
120
  onCreateInitiative: () => void;
120
121
  onCreateWorkstream: () => void;
121
122
  onToggleInitiative: (initiativeId: string) => void;
@@ -149,4 +150,4 @@ type PlanningDrawerProps = {
149
150
  onRemovePlanningContextFile?: (entityType: 'initiative' | 'workstream', entityId: string, index: number) => void;
150
151
  onUpdatePlanningContextCaption?: (entityType: 'initiative' | 'workstream', entityId: string, index: number, caption: string) => void;
151
152
  };
152
- export declare function PlanningDrawer({ open, leftOffset, returnFocusRef, onWidthChange, initiatives, standaloneWorkstreams, planningLoadState, onRetryPlanning, taskScope, activeInitiativeId, activeWorkstreamId, navigatorSort, navigatorSortReady, onNavigatorSortChange, taskArrangeMode, onTaskArrangeModeChange, onReorderWorkstreamTasks, planningFavorites, onTogglePlanningFavorite, favoritesSectionCollapsed, initiativesSectionCollapsed, workstreamsSectionCollapsed, onToggleFavoritesSection, onToggleInitiativesSection, onToggleWorkstreamsSection, expandedInitiativeIds, detail, editor, secondaryEditor, secondaryPane, primaryHydrationState, secondaryHydrationState, onRetryPrimaryHydration, onRetrySecondaryHydration, currentWorkspaceId, currentOwnerId, theme, assigneeOptions, linkOptions, showTaskCardStatusLabel, draftInitiativeSummary, draftTitle, draftDescription, draftOwner, draftInitiativeId, draftIcon, draftColor, onChangeDraftTitle, onChangeDraftDescription, onChangeDraftOwner, onChangeDraftInitiativeId, onChangeDraftIcon, onChangeDraftColor, onCollapseTreePane, onExpandTreePane, onCollapsePrimaryPane, onExpandPrimaryPane, onCollapseSecondaryPane, onExpandSecondaryPane, onBackFromSecondary, treeCollapsed, primaryCollapsed, secondaryCollapsed, onCancelEditor, onCancelSecondaryEditor, onSubmitEditor, isSubmittingEditor, onCreateInitiative, onCreateWorkstream, onToggleInitiative, onSelectInitiative, onSelectWorkstream, onOpenInitiativeDetails, onOpenWorkstreamDetails, onOpenNestedWorkstreamDetails, onEditInitiative, onEditWorkstream, onChangePlanningOwner, onChangePlanningIdentity, onChangeTaskAssignee, onArchiveInitiative, onUnarchiveInitiative, onArchiveWorkstream, onUnarchiveWorkstream, onArchivePlanningTask, onArchiveResolvedWorkstream, onArchiveResolvedInitiative, onCreateTaskInWorkstream, onCreateWorkstreamInInitiative, onAssignInitiativeToWorkstream, onOpenTaskById, onSetTaskStatus, taskReferences, onAssignTaskToWorkstream, onAttachTaskToWorkstreamByReference, onAttachWorkstreamToInitiativeByReference, onAddPlanningContextFile, onRemovePlanningContextFile, onUpdatePlanningContextCaption, }: PlanningDrawerProps): import("react/jsx-runtime").JSX.Element;
153
+ export declare function PlanningDrawer({ open, leftOffset, returnFocusRef, onWidthChange, initiatives, standaloneWorkstreams, planningLoadState, onRetryPlanning, taskScope, activeInitiativeId, activeWorkstreamId, navigatorSort, navigatorSortReady, onNavigatorSortChange, taskArrangeMode, onTaskArrangeModeChange, onReorderWorkstreamTasks, planningFavorites, onTogglePlanningFavorite, favoritesSectionCollapsed, initiativesSectionCollapsed, workstreamsSectionCollapsed, onToggleFavoritesSection, onToggleInitiativesSection, onToggleWorkstreamsSection, expandedInitiativeIds, detail, editor, secondaryEditor, secondaryPane, primaryHydrationState, secondaryHydrationState, onRetryPrimaryHydration, onRetrySecondaryHydration, currentWorkspaceId, currentOwnerId, theme, assigneeOptions, linkOptions, showTaskCardStatusLabel, draftInitiativeSummary, draftTitle, draftDescription, draftOwner, draftInitiativeId, draftIcon, draftColor, onChangeDraftTitle, onChangeDraftDescription, onChangeDraftOwner, onChangeDraftInitiativeId, onChangeDraftIcon, onChangeDraftColor, onCollapseTreePane, onExpandTreePane, onCollapsePrimaryPane, onExpandPrimaryPane, onCollapseSecondaryPane, onExpandSecondaryPane, onBackFromSecondary, treeCollapsed, primaryCollapsed, secondaryCollapsed, onCancelEditor, onCancelSecondaryEditor, onSubmitEditor, isSubmittingEditor, isEditorSubmitDisabled, onCreateInitiative, onCreateWorkstream, onToggleInitiative, onSelectInitiative, onSelectWorkstream, onOpenInitiativeDetails, onOpenWorkstreamDetails, onOpenNestedWorkstreamDetails, onEditInitiative, onEditWorkstream, onChangePlanningOwner, onChangePlanningIdentity, onChangeTaskAssignee, onArchiveInitiative, onUnarchiveInitiative, onArchiveWorkstream, onUnarchiveWorkstream, onArchivePlanningTask, onArchiveResolvedWorkstream, onArchiveResolvedInitiative, onCreateTaskInWorkstream, onCreateWorkstreamInInitiative, onAssignInitiativeToWorkstream, onOpenTaskById, onSetTaskStatus, taskReferences, onAssignTaskToWorkstream, onAttachTaskToWorkstreamByReference, onAttachWorkstreamToInitiativeByReference, onAddPlanningContextFile, onRemovePlanningContextFile, onUpdatePlanningContextCaption, }: PlanningDrawerProps): import("react/jsx-runtime").JSX.Element;
@@ -758,7 +758,7 @@ function PlanningDrawerDetailView({ detail, showArchivedChildren, inlineEditor,
758
758
  });
759
759
  }, title: "Unlink workstream from initiative", ariaLabel: "Unlink workstream from initiative", children: _jsx(Unlink, { size: 14, "aria-hidden": "true" }) })) : null] })) : null })) : null, detail.type === 'workstream'
760
760
  && !parentInitiativeReferenceLabel
761
- && !inlineEditor ? (_jsx(PlanningLinkPicker, { options: linkOptions.initiatives, entityLabel: "initiative", onSelect: handleLinkInitiative, disabled: !onAssignInitiativeToWorkstream || isChangingRelationship, iconTrigger: true, triggerTitle: "Link initiative" })) : null, _jsx(PlanningReferenceBadge, { label: detailReferenceLabel || 'Reference pending', entityType: detail.type, entityName: detail.item.title, pending: !detailReferenceLabel })] }), _jsx("div", { className: panelStyles.detailTopActions, children: inlineEditor ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "tf-button-ghost tf-button-compact", onClick: inlineEditor.onCancel, disabled: inlineEditor.isSubmitting, children: "Cancel" }), _jsx("button", { type: "submit", form: inlineEditorFormId, className: "tf-button-primary tf-button-compact", disabled: inlineEditor.isSubmitting, children: inlineEditor.isSubmitting ? 'Saving…' : 'Save' })] })) : (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "tf-control-icon", disabled: archiveDetailState.mode === 'blocked' || archiveDetailState.mode === 'checking', onClick: () => {
761
+ && !inlineEditor ? (_jsx(PlanningLinkPicker, { options: linkOptions.initiatives, entityLabel: "initiative", onSelect: handleLinkInitiative, disabled: !onAssignInitiativeToWorkstream || isChangingRelationship, iconTrigger: true, triggerTitle: "Link initiative" })) : null, _jsx(PlanningReferenceBadge, { label: detailReferenceLabel || 'Reference pending', entityType: detail.type, entityName: detail.item.title, pending: !detailReferenceLabel })] }), _jsx("div", { className: panelStyles.detailTopActions, children: inlineEditor ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "tf-button-ghost tf-button-compact", onClick: inlineEditor.onCancel, disabled: inlineEditor.isSubmitting, children: "Cancel" }), _jsx("button", { type: "submit", form: inlineEditorFormId, className: "tf-button-primary tf-button-compact", disabled: inlineEditor.isSubmitting || inlineEditor.isSubmitDisabled, children: inlineEditor.isSubmitting ? 'Saving…' : 'Save' })] })) : (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "tf-control-icon", disabled: archiveDetailState.mode === 'blocked' || archiveDetailState.mode === 'checking', onClick: () => {
762
762
  if (archiveDetailState.mode === 'archived') {
763
763
  onUnarchive?.();
764
764
  }
@@ -772,7 +772,7 @@ function PlanningDrawerDetailView({ detail, showArchivedChildren, inlineEditor,
772
772
  ? archiveEntityConfirmationVisible
773
773
  : undefined, children: archiveDetailState.mode === 'archived'
774
774
  ? _jsx(RotateCcw, { size: 14, "aria-hidden": "true" })
775
- : _jsx(Archive, { size: 14, "aria-hidden": "true" }) }), _jsx("button", { type: "button", className: "tf-control-icon", onClick: onEdit, title: `Edit ${detailLabel.toLowerCase()} details`, "aria-label": `Edit ${detailLabel.toLowerCase()} details`, children: _jsx(Pencil, { size: 14, "aria-hidden": "true" }) })] })) })] }), archiveEntityConfirmationVisible && archiveDetailState.mode === 'ready' ? (_jsx(PlanningArchiveConfirmation, { entityType: detail.type, title: detail.item.title, activeTaskCount: detail.item.archiveActiveTaskCount, activeWorkstreamCount: detail.item.archiveActiveWorkstreamCount, busy: archiveEntityBusy, onCancel: () => setArchiveEntityConfirmationVisible(false), onConfirm: () => void handleConfirmArchiveEntity() })) : null, detail.type === 'workstream' ? renderRelationshipFeedback('workstream', detail.item.id) : null, inlineEditor ? (_jsx(PlanningDrawerEditor, { editor: inlineEditor.editor, draftTitle: inlineEditor.draftTitle, draftDescription: inlineEditor.draftDescription, draftOwner: inlineEditor.draftOwner, draftInitiativeId: inlineEditor.draftInitiativeId, assigneeOptions: assigneeOptions, initiativeLinkOptions: linkOptions.initiatives, draftInitiativeSummary: inlineEditor.draftInitiativeSummary, onChangeDraftTitle: inlineEditor.onChangeDraftTitle, onChangeDraftDescription: inlineEditor.onChangeDraftDescription, onChangeDraftOwner: inlineEditor.onChangeDraftOwner, onChangeDraftInitiativeId: inlineEditor.onChangeDraftInitiativeId, onCancel: inlineEditor.onCancel, onSubmit: inlineEditor.onSubmit, isSubmitting: inlineEditor.isSubmitting, inline: true, formId: inlineEditorFormId })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: panelStyles.detailTitleRow, children: [_jsx(PlanningVisualIdentity, { entityType: detail.type, entityId: detail.item.id, icon: detail.item.icon, color: detail.item.color, size: "detail", disabled: Boolean(detail.item.isArchived), onChange: onChangePlanningIdentity
775
+ : _jsx(Archive, { size: 14, "aria-hidden": "true" }) }), _jsx("button", { type: "button", className: "tf-control-icon", onClick: onEdit, title: `Edit ${detailLabel.toLowerCase()} details`, "aria-label": `Edit ${detailLabel.toLowerCase()} details`, children: _jsx(Pencil, { size: 14, "aria-hidden": "true" }) })] })) })] }), archiveEntityConfirmationVisible && archiveDetailState.mode === 'ready' ? (_jsx(PlanningArchiveConfirmation, { entityType: detail.type, title: detail.item.title, activeTaskCount: detail.item.archiveActiveTaskCount, activeWorkstreamCount: detail.item.archiveActiveWorkstreamCount, busy: archiveEntityBusy, onCancel: () => setArchiveEntityConfirmationVisible(false), onConfirm: () => void handleConfirmArchiveEntity() })) : null, detail.type === 'workstream' ? renderRelationshipFeedback('workstream', detail.item.id) : null, inlineEditor ? (_jsx(PlanningDrawerEditor, { editor: inlineEditor.editor, draftTitle: inlineEditor.draftTitle, draftDescription: inlineEditor.draftDescription, draftOwner: inlineEditor.draftOwner, draftInitiativeId: inlineEditor.draftInitiativeId, assigneeOptions: assigneeOptions, initiativeLinkOptions: linkOptions.initiatives, draftInitiativeSummary: inlineEditor.draftInitiativeSummary, onChangeDraftTitle: inlineEditor.onChangeDraftTitle, onChangeDraftDescription: inlineEditor.onChangeDraftDescription, onChangeDraftOwner: inlineEditor.onChangeDraftOwner, onChangeDraftInitiativeId: inlineEditor.onChangeDraftInitiativeId, onCancel: inlineEditor.onCancel, onSubmit: inlineEditor.onSubmit, isSubmitting: inlineEditor.isSubmitting, isSubmitDisabled: inlineEditor.isSubmitDisabled, inline: true, formId: inlineEditorFormId })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: panelStyles.detailTitleRow, children: [_jsx(PlanningVisualIdentity, { entityType: detail.type, entityId: detail.item.id, icon: detail.item.icon, color: detail.item.color, size: "detail", disabled: Boolean(detail.item.isArchived), onChange: onChangePlanningIdentity
776
776
  ? (patch) => onChangePlanningIdentity(detail.type, detail.item.id, patch)
777
777
  : undefined }), _jsx("h3", { className: panelStyles.detailTitle, children: detail.item.title })] }), _jsx(PlanningEntityDescription, { description: detail.item.description, label: detailLabel, taskReferences: taskReferences, className: `${panelStyles.detailDescription} ${styles.appScrollbar} tf-scrollbar`.trim(), markdownClassName: panelStyles.detailDescriptionMarkdown })] })), _jsxs("div", { className: panelStyles.detailOwnerField, children: [_jsx("span", { className: panelStyles.detailOwnerLabel, children: "Owner" }), _jsx(PlanningOwnerDropdown, { value: ownerValue, disabled: !onChangeOwner || isSavingOwner, onChange: (nextOwnerId) => void handleOwnerChange(nextOwnerId), ariaLabel: `${detailLabel} owner`, options: assigneeOptions, className: `${planningDropdownStyles.control} ${panelStyles.ownerSelect}` })] }), detail.item.taskCount > 0 ? (_jsx(PlanningProgress, { value: detail.item.progressPercent, label: `${detail.item.progressPercent}% resolved`, statusCounts: detail.item.statusCounts })) : (_jsx(PlanningNoTasks, {})), _jsx(PlanningSummaryMeta, { item: detail.item, workstreamCount: detail.type === 'initiative'
778
778
  ? detail.item.workstreamCount ?? detail.item.workstreams.length
@@ -847,7 +847,7 @@ function PlanningDrawerDetailView({ detail, showArchivedChildren, inlineEditor,
847
847
  }, title: "Archive task", "aria-label": `Archive task ${referenceLabel || task.title}`, children: _jsx(Archive, { size: 14, "aria-hidden": "true" }) })) : undefined }) }, task.id));
848
848
  } })) : (_jsx("div", { className: panelStyles.passiveGuidance, children: "No tasks are linked to this workstream." }))), detachedRelationshipRecovery] }), _jsx("div", { className: panelStyles.listCard, children: _jsx(PlanningEntityContext, { entityType: detail.type, entityId: detail.item.id, referenceLabel: detailReferenceLabel, workspaceId: currentWorkspaceId, attachments: detail.item.attachments, readOnly: Boolean(detail.item.isArchived), onAddAttachment: (file) => onAddPlanningContextFile?.(detail.type, detail.item.id, file), onRemoveAttachment: (index) => onRemovePlanningContextFile?.(detail.type, detail.item.id, index), onUpdateAttachmentCaption: (index, caption) => (onUpdatePlanningContextCaption?.(detail.type, detail.item.id, index, caption)) }) }), _jsx("div", { className: panelStyles.listCard, children: _jsx(PlanningEntityActivity, { entityType: detail.type, item: detail.item, assigneeOptions: assigneeOptions.map(toPlanningAssigneeOption), currentActorId: currentActorId, taskReferences: taskReferences, parentInitiative: parentInitiative, listClassName: panelStyles.activityTimelineList, emptyClassName: panelStyles.activityTimelineEmpty, hydrationState: hydrationState, onRetryHydration: onRetryHydration }) }), _jsx(PlanningUnlinkConfirmation, { item: pendingUnlink, busy: isChangingRelationship, theme: theme, onCancel: () => setPendingUnlink(null), onConfirm: () => void handleConfirmUnlink() })] }));
849
849
  }
850
- function PlanningDrawerEditor({ editor, draftTitle, draftDescription, draftOwner, draftInitiativeId, draftIcon, draftColor, assigneeOptions, initiativeLinkOptions, draftInitiativeSummary, onChangeDraftTitle, onChangeDraftDescription, onChangeDraftOwner, onChangeDraftInitiativeId, onChangeDraftIcon, onChangeDraftColor, onCancel, onSubmit, isSubmitting, inline = false, formId, }) {
850
+ function PlanningDrawerEditor({ editor, draftTitle, draftDescription, draftOwner, draftInitiativeId, draftIcon, draftColor, assigneeOptions, initiativeLinkOptions, draftInitiativeSummary, onChangeDraftTitle, onChangeDraftDescription, onChangeDraftOwner, onChangeDraftInitiativeId, onChangeDraftIcon, onChangeDraftColor, onCancel, onSubmit, isSubmitting, isSubmitDisabled = false, inline = false, formId, }) {
851
851
  const entityLabel = editor.entityType === 'initiative' ? 'Initiative' : 'Workstream';
852
852
  const titleInputId = React.useId();
853
853
  const titleErrorId = React.useId();
@@ -864,9 +864,11 @@ function PlanningDrawerEditor({ editor, draftTitle, draftDescription, draftOwner
864
864
  titleInputRef.current?.focus();
865
865
  return;
866
866
  }
867
+ if (isSubmitDisabled)
868
+ return;
867
869
  setTitleError(null);
868
870
  onSubmit();
869
- }, [draftTitle, entityLabel, onSubmit]);
871
+ }, [draftTitle, entityLabel, isSubmitDisabled, onSubmit]);
870
872
  const handleKeyDown = React.useCallback((event) => {
871
873
  if (!inline || event.key !== 'Escape' || isSubmitting)
872
874
  return;
@@ -884,9 +886,9 @@ function PlanningDrawerEditor({ editor, draftTitle, draftDescription, draftOwner
884
886
  onChangeDraftTitle(event.target.value);
885
887
  }, placeholder: editor.entityType === 'initiative' ? 'Enter initiative title' : 'Enter workstream title' }), titleError ? _jsx("p", { id: titleErrorId, className: "tf-text-error", children: titleError }) : null] }), _jsxs("div", { className: panelStyles.editorField, children: [_jsxs("label", { className: panelStyles.editorLabel, htmlFor: descriptionInputId, children: [entityLabel, " description"] }), _jsx("textarea", { id: descriptionInputId, className: styles.textarea, disabled: isSubmitting, value: draftDescription, onChange: (event) => onChangeDraftDescription(event.target.value), placeholder: editor.entityType === 'initiative'
886
888
  ? 'Describe the broader outcome this initiative is meant to achieve.'
887
- : 'Describe the lane of work this workstream will coordinate.', rows: 5 })] }), editor.entityType === 'workstream' && (_jsxs("div", { className: panelStyles.editorField, children: [_jsx("span", { className: panelStyles.editorLabel, children: "Initiative" }), _jsxs("div", { className: panelStyles.editorRelationshipRow, children: [_jsx(PlanningLinkPicker, { options: initiativeLinkOptions, selectedId: draftInitiativeSummary?.id || '', entityLabel: "initiative", disabled: isSubmitting, onSelect: (option) => onChangeDraftInitiativeId(option.referenceLabel) }), draftInitiativeId.trim() ? (_jsx("button", { type: "button", className: "tf-button-ghost tf-button-compact", disabled: isSubmitting, onClick: () => onChangeDraftInitiativeId(''), children: "Remove" })) : null] })] })), !inline ? _jsxs("div", { className: panelStyles.editorField, children: [_jsxs("span", { className: panelStyles.editorLabel, children: [entityLabel, " owner"] }), _jsx(PlanningOwnerDropdown, { disabled: isSubmitting, value: draftOwner, onChange: onChangeDraftOwner, ariaLabel: `${entityLabel} owner`, options: assigneeOptions, className: panelStyles.editorSelect })] }) : null, !inline ? _jsxs("div", { className: panelStyles.editorActions, children: [_jsx("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: onCancel, disabled: isSubmitting, children: "Cancel" }), _jsx("button", { type: "submit", className: styles.primaryUpdateBtn, disabled: isSubmitting, children: submitLabel })] }) : null] }));
889
+ : 'Describe the lane of work this workstream will coordinate.', rows: 5 })] }), editor.entityType === 'workstream' && (_jsxs("div", { className: panelStyles.editorField, children: [_jsx("span", { className: panelStyles.editorLabel, children: "Initiative" }), _jsxs("div", { className: panelStyles.editorRelationshipRow, children: [_jsx(PlanningLinkPicker, { options: initiativeLinkOptions, selectedId: draftInitiativeSummary?.id || '', entityLabel: "initiative", disabled: isSubmitting, onSelect: (option) => onChangeDraftInitiativeId(option.referenceLabel) }), draftInitiativeId.trim() ? (_jsx("button", { type: "button", className: "tf-button-ghost tf-button-compact", disabled: isSubmitting, onClick: () => onChangeDraftInitiativeId(''), children: "Remove" })) : null] })] })), !inline ? _jsxs("div", { className: panelStyles.editorField, children: [_jsxs("span", { className: panelStyles.editorLabel, children: [entityLabel, " owner"] }), _jsx(PlanningOwnerDropdown, { disabled: isSubmitting, value: draftOwner, onChange: onChangeDraftOwner, ariaLabel: `${entityLabel} owner`, options: assigneeOptions, className: panelStyles.editorSelect })] }) : null, !inline ? _jsxs("div", { className: panelStyles.editorActions, children: [_jsx("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: onCancel, disabled: isSubmitting, children: "Cancel" }), _jsx("button", { type: "submit", className: styles.primaryUpdateBtn, disabled: isSubmitting || isSubmitDisabled, children: submitLabel })] }) : null] }));
888
890
  }
889
- export function PlanningDrawer({ open, leftOffset = 0, returnFocusRef, onWidthChange, initiatives, standaloneWorkstreams, planningLoadState, onRetryPlanning, taskScope, activeInitiativeId, activeWorkstreamId, navigatorSort, navigatorSortReady, onNavigatorSortChange, taskArrangeMode = 'execution', onTaskArrangeModeChange = () => { }, onReorderWorkstreamTasks, planningFavorites, onTogglePlanningFavorite, favoritesSectionCollapsed, initiativesSectionCollapsed, workstreamsSectionCollapsed, onToggleFavoritesSection, onToggleInitiativesSection, onToggleWorkstreamsSection, expandedInitiativeIds, detail, editor, secondaryEditor = null, secondaryPane, primaryHydrationState, secondaryHydrationState, onRetryPrimaryHydration, onRetrySecondaryHydration, currentWorkspaceId, currentOwnerId, theme, assigneeOptions, linkOptions, showTaskCardStatusLabel = false, draftInitiativeSummary, draftTitle, draftDescription, draftOwner, draftInitiativeId, draftIcon = null, draftColor = null, onChangeDraftTitle, onChangeDraftDescription, onChangeDraftOwner, onChangeDraftInitiativeId, onChangeDraftIcon = () => { }, onChangeDraftColor = () => { }, onCollapseTreePane, onExpandTreePane, onCollapsePrimaryPane, onExpandPrimaryPane, onCollapseSecondaryPane, onExpandSecondaryPane, onBackFromSecondary, treeCollapsed, primaryCollapsed, secondaryCollapsed, onCancelEditor, onCancelSecondaryEditor, onSubmitEditor, isSubmittingEditor, onCreateInitiative, onCreateWorkstream, onToggleInitiative, onSelectInitiative, onSelectWorkstream, onOpenInitiativeDetails, onOpenWorkstreamDetails, onOpenNestedWorkstreamDetails, onEditInitiative, onEditWorkstream, onChangePlanningOwner, onChangePlanningIdentity = () => { }, onChangeTaskAssignee, onArchiveInitiative, onUnarchiveInitiative, onArchiveWorkstream, onUnarchiveWorkstream, onArchivePlanningTask, onArchiveResolvedWorkstream, onArchiveResolvedInitiative, onCreateTaskInWorkstream, onCreateWorkstreamInInitiative, onAssignInitiativeToWorkstream, onOpenTaskById, onSetTaskStatus, taskReferences, onAssignTaskToWorkstream, onAttachTaskToWorkstreamByReference, onAttachWorkstreamToInitiativeByReference, onAddPlanningContextFile, onRemovePlanningContextFile, onUpdatePlanningContextCaption, }) {
891
+ export function PlanningDrawer({ open, leftOffset = 0, returnFocusRef, onWidthChange, initiatives, standaloneWorkstreams, planningLoadState, onRetryPlanning, taskScope, activeInitiativeId, activeWorkstreamId, navigatorSort, navigatorSortReady, onNavigatorSortChange, taskArrangeMode = 'execution', onTaskArrangeModeChange = () => { }, onReorderWorkstreamTasks, planningFavorites, onTogglePlanningFavorite, favoritesSectionCollapsed, initiativesSectionCollapsed, workstreamsSectionCollapsed, onToggleFavoritesSection, onToggleInitiativesSection, onToggleWorkstreamsSection, expandedInitiativeIds, detail, editor, secondaryEditor = null, secondaryPane, primaryHydrationState, secondaryHydrationState, onRetryPrimaryHydration, onRetrySecondaryHydration, currentWorkspaceId, currentOwnerId, theme, assigneeOptions, linkOptions, showTaskCardStatusLabel = false, draftInitiativeSummary, draftTitle, draftDescription, draftOwner, draftInitiativeId, draftIcon = null, draftColor = null, onChangeDraftTitle, onChangeDraftDescription, onChangeDraftOwner, onChangeDraftInitiativeId, onChangeDraftIcon = () => { }, onChangeDraftColor = () => { }, onCollapseTreePane, onExpandTreePane, onCollapsePrimaryPane, onExpandPrimaryPane, onCollapseSecondaryPane, onExpandSecondaryPane, onBackFromSecondary, treeCollapsed, primaryCollapsed, secondaryCollapsed, onCancelEditor, onCancelSecondaryEditor, onSubmitEditor, isSubmittingEditor, isEditorSubmitDisabled = false, onCreateInitiative, onCreateWorkstream, onToggleInitiative, onSelectInitiative, onSelectWorkstream, onOpenInitiativeDetails, onOpenWorkstreamDetails, onOpenNestedWorkstreamDetails, onEditInitiative, onEditWorkstream, onChangePlanningOwner, onChangePlanningIdentity = () => { }, onChangeTaskAssignee, onArchiveInitiative, onUnarchiveInitiative, onArchiveWorkstream, onUnarchiveWorkstream, onArchivePlanningTask, onArchiveResolvedWorkstream, onArchiveResolvedInitiative, onCreateTaskInWorkstream, onCreateWorkstreamInInitiative, onAssignInitiativeToWorkstream, onOpenTaskById, onSetTaskStatus, taskReferences, onAssignTaskToWorkstream, onAttachTaskToWorkstreamByReference, onAttachWorkstreamToInitiativeByReference, onAddPlanningContextFile, onRemovePlanningContextFile, onUpdatePlanningContextCaption, }) {
890
892
  const primaryInlineEditor = editor?.mode === 'edit'
891
893
  && detail
892
894
  && editor.entityType === detail.type
@@ -1085,7 +1087,7 @@ export function PlanningDrawer({ open, leftOffset = 0, returnFocusRef, onWidthCh
1085
1087
  pointerEvents: open ? 'auto' : 'none',
1086
1088
  transition: 'left 220ms cubic-bezier(0.4, 0, 0.2, 1)',
1087
1089
  willChange: 'left',
1088
- }, children: _jsxs("div", { className: panelStyles.drawerBody, children: [showTreePane && (effectiveTreeCollapsed ? (_jsx(CollapsedPane, { label: "Planning", onExpand: onExpandTreePane })) : (_jsxs("div", { ref: treePaneRef, "data-planning-pane": "tree", className: `${panelStyles.paneShell} ${styles.appScrollbar} tf-scrollbar ${panelStyles.treePane} ${hasPrimaryPane && !primaryCollapsed ? panelStyles.treePaneSplit : ''}`.trim(), style: paneWidthStyle(layoutMode === 'narrow' ? drawerWidth : PLANNING_DRAWER_TREE_WIDTH_PX), children: [_jsxs("div", { className: panelStyles.paneHeader, children: [_jsx("h2", { className: panelStyles.paneHeaderLabel, tabIndex: -1, "data-planning-pane-heading": true, children: "Planning" }), _jsxs("div", { className: panelStyles.headerActionGroup, children: [_jsx(ArchiveVisibilityToggle, { visible: showArchivedPlanning, disabled: taskScope === 'deleted' || archivedPlanningItemCount === 0, disabledLabel: taskScope === 'deleted' ? 'Archive visibility unavailable in Trash' : undefined, itemLabel: "planning items", onToggle: () => setShowArchivedPlanning((current) => !current) }), wideLayout ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: onCollapseTreePane, "aria-label": "Collapse Planning pane", title: "Collapse Planning pane", children: _jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }) })) : null] })] }), _jsx("div", { className: panelStyles.paneContent, "aria-busy": planningLoadState.status === 'loading' || planningLoadState.isRefreshing, children: _jsx(PlanningDrawerList, { initiatives: initiatives, standaloneWorkstreams: standaloneWorkstreams, planningLoadState: planningLoadState, taskScope: taskScope, showArchivedPlanning: showArchivedPlanning, currentOwnerId: currentOwnerId, assigneeOptions: assigneeOptions, showTaskCardStatusLabel: showTaskCardStatusLabel, taskArrangeMode: taskArrangeMode, activeInitiativeId: activeInitiativeId, activeWorkstreamId: activeWorkstreamId, navigatorSort: navigatorSort, navigatorSortReady: navigatorSortReady, onNavigatorSortChange: onNavigatorSortChange, planningFavorites: planningFavorites, onTogglePlanningFavorite: onTogglePlanningFavorite, favoritesSectionCollapsed: favoritesSectionCollapsed, initiativesSectionCollapsed: initiativesSectionCollapsed, workstreamsSectionCollapsed: workstreamsSectionCollapsed, onToggleFavoritesSection: onToggleFavoritesSection, onToggleInitiativesSection: onToggleInitiativesSection, onToggleWorkstreamsSection: onToggleWorkstreamsSection, expandedInitiativeIds: expandedInitiativeIds, detail: detail, secondaryPane: secondaryPane, onCreateInitiative: onCreateInitiative, onCreateWorkstream: onCreateWorkstream, onToggleInitiative: onToggleInitiative, onSelectInitiative: onSelectInitiative, onSelectWorkstream: onSelectWorkstream, onOpenInitiativeDetails: onOpenInitiativeDetails, onOpenWorkstreamDetails: onOpenWorkstreamDetails, onOpenNestedWorkstreamDetails: onOpenNestedWorkstreamDetails, onOpenTaskById: onOpenTaskById, onSetTaskStatus: onSetTaskStatus, onChangePlanningOwner: onChangePlanningOwner, onChangePlanningIdentity: onChangePlanningIdentity, onChangeTaskAssignee: onChangeTaskAssignee, onArchivePlanningTask: onArchivePlanningTask, onArchiveResolvedWorkstream: onArchiveResolvedWorkstream, onArchiveResolvedInitiative: onArchiveResolvedInitiative, linkOptions: linkOptions, onAssignInitiativeToWorkstream: onAssignInitiativeToWorkstream, onRetryPlanning: onRetryPlanning }) })] }))), wideLayout && !primaryIsInitiative && (initiativeRelationshipExpanded ? (_jsx(RelationshipPane, { label: "Initiative", title: initiativeRelationship.title, description: initiativeRelationship.description, action: initiativeRelationship.action, onCollapse: () => setInitiativeRelationshipExpanded(false), paneRef: initiativeRelationshipPaneRef })) : (_jsx(CollapsedPane, { label: "Initiative", muted: !hasInitiativeRelationship, onExpand: () => setInitiativeRelationshipExpanded(true) }))), showPrimaryPane && (effectivePrimaryCollapsed ? (_jsx(CollapsedPane, { label: primaryEntityType === 'initiative' ? 'Initiative' : 'Workstream', onExpand: onExpandPrimaryPane })) : (_jsxs("div", { ref: primaryPaneRef, "data-planning-pane": "primary", className: `${panelStyles.paneShell} ${styles.appScrollbar} tf-scrollbar ${panelStyles.detailPane}`.trim(), style: paneWidthStyle(wideLayout ? PLANNING_DRAWER_DETAIL_WIDTH_PX : adaptiveDetailWidth), children: [_jsxs("div", { className: panelStyles.paneHeader, children: [layoutMode === 'narrow' ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: handleBackFromPrimary, "aria-label": "Back to Planning", title: "Back to Planning", children: _jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }) })) : null, _jsx("h2", { className: panelStyles.paneHeaderLabel, tabIndex: -1, "data-planning-pane-heading": true, children: primaryPaneLabel }), _jsxs("div", { className: panelStyles.headerActionGroup, children: [primaryArchiveDetail ? (_jsx(ArchiveVisibilityToggle, { visible: archivedDetailVisibility[primaryArchiveDetail.type], disabled: countArchivedDetailChildren(primaryArchiveDetail) === 0, itemLabel: primaryArchiveDetail.type === 'initiative' ? 'workstreams' : 'tasks', onToggle: () => toggleArchivedDetailVisibility(primaryArchiveDetail.type) })) : null, wideLayout ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: onCollapsePrimaryPane, "aria-label": `Collapse ${primaryPaneLabel} pane`, title: `Collapse ${primaryPaneLabel} pane`, children: _jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }) })) : null] })] }), _jsx("div", { className: panelStyles.paneContent, "aria-busy": primaryHydrationState?.status === 'loading', children: dedicatedPrimaryEditor ? (_jsx(PlanningDrawerEditor, { editor: dedicatedPrimaryEditor, draftTitle: draftTitle, draftDescription: draftDescription, draftOwner: draftOwner, draftInitiativeId: draftInitiativeId, draftIcon: draftIcon, draftColor: draftColor, assigneeOptions: assigneeOptions, initiativeLinkOptions: linkOptions.initiatives, draftInitiativeSummary: draftInitiativeSummary, onChangeDraftTitle: onChangeDraftTitle, onChangeDraftDescription: onChangeDraftDescription, onChangeDraftOwner: onChangeDraftOwner, onChangeDraftInitiativeId: onChangeDraftInitiativeId, onChangeDraftIcon: onChangeDraftIcon, onChangeDraftColor: onChangeDraftColor, onCancel: onCancelEditor, onSubmit: onSubmitEditor, isSubmitting: isSubmittingEditor })) : detail ? (_jsx(PlanningDrawerDetailView, { detail: detail, showArchivedChildren: archivedDetailVisibility[detail.type], inlineEditor: primaryInlineEditor ? {
1090
+ }, children: _jsxs("div", { className: panelStyles.drawerBody, children: [showTreePane && (effectiveTreeCollapsed ? (_jsx(CollapsedPane, { label: "Planning", onExpand: onExpandTreePane })) : (_jsxs("div", { ref: treePaneRef, "data-planning-pane": "tree", className: `${panelStyles.paneShell} ${styles.appScrollbar} tf-scrollbar ${panelStyles.treePane} ${hasPrimaryPane && !primaryCollapsed ? panelStyles.treePaneSplit : ''}`.trim(), style: paneWidthStyle(layoutMode === 'narrow' ? drawerWidth : PLANNING_DRAWER_TREE_WIDTH_PX), children: [_jsxs("div", { className: panelStyles.paneHeader, children: [_jsx("h2", { className: panelStyles.paneHeaderLabel, tabIndex: -1, "data-planning-pane-heading": true, children: "Planning" }), _jsxs("div", { className: panelStyles.headerActionGroup, children: [_jsx(ArchiveVisibilityToggle, { visible: showArchivedPlanning, disabled: taskScope === 'deleted' || archivedPlanningItemCount === 0, disabledLabel: taskScope === 'deleted' ? 'Archive visibility unavailable in Trash' : undefined, itemLabel: "planning items", onToggle: () => setShowArchivedPlanning((current) => !current) }), wideLayout ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: onCollapseTreePane, "aria-label": "Collapse Planning pane", title: "Collapse Planning pane", children: _jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }) })) : null] })] }), _jsx("div", { className: panelStyles.paneContent, "aria-busy": planningLoadState.status === 'loading' || planningLoadState.isRefreshing, children: _jsx(PlanningDrawerList, { initiatives: initiatives, standaloneWorkstreams: standaloneWorkstreams, planningLoadState: planningLoadState, taskScope: taskScope, showArchivedPlanning: showArchivedPlanning, currentOwnerId: currentOwnerId, assigneeOptions: assigneeOptions, showTaskCardStatusLabel: showTaskCardStatusLabel, taskArrangeMode: taskArrangeMode, activeInitiativeId: activeInitiativeId, activeWorkstreamId: activeWorkstreamId, navigatorSort: navigatorSort, navigatorSortReady: navigatorSortReady, onNavigatorSortChange: onNavigatorSortChange, planningFavorites: planningFavorites, onTogglePlanningFavorite: onTogglePlanningFavorite, favoritesSectionCollapsed: favoritesSectionCollapsed, initiativesSectionCollapsed: initiativesSectionCollapsed, workstreamsSectionCollapsed: workstreamsSectionCollapsed, onToggleFavoritesSection: onToggleFavoritesSection, onToggleInitiativesSection: onToggleInitiativesSection, onToggleWorkstreamsSection: onToggleWorkstreamsSection, expandedInitiativeIds: expandedInitiativeIds, detail: detail, secondaryPane: secondaryPane, onCreateInitiative: onCreateInitiative, onCreateWorkstream: onCreateWorkstream, onToggleInitiative: onToggleInitiative, onSelectInitiative: onSelectInitiative, onSelectWorkstream: onSelectWorkstream, onOpenInitiativeDetails: onOpenInitiativeDetails, onOpenWorkstreamDetails: onOpenWorkstreamDetails, onOpenNestedWorkstreamDetails: onOpenNestedWorkstreamDetails, onOpenTaskById: onOpenTaskById, onSetTaskStatus: onSetTaskStatus, onChangePlanningOwner: onChangePlanningOwner, onChangePlanningIdentity: onChangePlanningIdentity, onChangeTaskAssignee: onChangeTaskAssignee, onArchivePlanningTask: onArchivePlanningTask, onArchiveResolvedWorkstream: onArchiveResolvedWorkstream, onArchiveResolvedInitiative: onArchiveResolvedInitiative, linkOptions: linkOptions, onAssignInitiativeToWorkstream: onAssignInitiativeToWorkstream, onRetryPlanning: onRetryPlanning }) })] }))), wideLayout && !primaryIsInitiative && (initiativeRelationshipExpanded ? (_jsx(RelationshipPane, { label: "Initiative", title: initiativeRelationship.title, description: initiativeRelationship.description, action: initiativeRelationship.action, onCollapse: () => setInitiativeRelationshipExpanded(false), paneRef: initiativeRelationshipPaneRef })) : (_jsx(CollapsedPane, { label: "Initiative", muted: !hasInitiativeRelationship, onExpand: () => setInitiativeRelationshipExpanded(true) }))), showPrimaryPane && (effectivePrimaryCollapsed ? (_jsx(CollapsedPane, { label: primaryEntityType === 'initiative' ? 'Initiative' : 'Workstream', onExpand: onExpandPrimaryPane })) : (_jsxs("div", { ref: primaryPaneRef, "data-planning-pane": "primary", className: `${panelStyles.paneShell} ${styles.appScrollbar} tf-scrollbar ${panelStyles.detailPane}`.trim(), style: paneWidthStyle(wideLayout ? PLANNING_DRAWER_DETAIL_WIDTH_PX : adaptiveDetailWidth), children: [_jsxs("div", { className: panelStyles.paneHeader, children: [layoutMode === 'narrow' ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: handleBackFromPrimary, "aria-label": "Back to Planning", title: "Back to Planning", children: _jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }) })) : null, _jsx("h2", { className: panelStyles.paneHeaderLabel, tabIndex: -1, "data-planning-pane-heading": true, children: primaryPaneLabel }), _jsxs("div", { className: panelStyles.headerActionGroup, children: [primaryArchiveDetail ? (_jsx(ArchiveVisibilityToggle, { visible: archivedDetailVisibility[primaryArchiveDetail.type], disabled: countArchivedDetailChildren(primaryArchiveDetail) === 0, itemLabel: primaryArchiveDetail.type === 'initiative' ? 'workstreams' : 'tasks', onToggle: () => toggleArchivedDetailVisibility(primaryArchiveDetail.type) })) : null, wideLayout ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: onCollapsePrimaryPane, "aria-label": `Collapse ${primaryPaneLabel} pane`, title: `Collapse ${primaryPaneLabel} pane`, children: _jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }) })) : null] })] }), _jsx("div", { className: panelStyles.paneContent, "aria-busy": primaryHydrationState?.status === 'loading', children: dedicatedPrimaryEditor ? (_jsx(PlanningDrawerEditor, { editor: dedicatedPrimaryEditor, draftTitle: draftTitle, draftDescription: draftDescription, draftOwner: draftOwner, draftInitiativeId: draftInitiativeId, draftIcon: draftIcon, draftColor: draftColor, assigneeOptions: assigneeOptions, initiativeLinkOptions: linkOptions.initiatives, draftInitiativeSummary: draftInitiativeSummary, onChangeDraftTitle: onChangeDraftTitle, onChangeDraftDescription: onChangeDraftDescription, onChangeDraftOwner: onChangeDraftOwner, onChangeDraftInitiativeId: onChangeDraftInitiativeId, onChangeDraftIcon: onChangeDraftIcon, onChangeDraftColor: onChangeDraftColor, onCancel: onCancelEditor, onSubmit: onSubmitEditor, isSubmitting: isSubmittingEditor, isSubmitDisabled: isEditorSubmitDisabled })) : detail ? (_jsx(PlanningDrawerDetailView, { detail: detail, showArchivedChildren: archivedDetailVisibility[detail.type], inlineEditor: primaryInlineEditor ? {
1089
1091
  editor: primaryInlineEditor,
1090
1092
  draftTitle,
1091
1093
  draftDescription,
@@ -1099,6 +1101,7 @@ export function PlanningDrawer({ open, leftOffset = 0, returnFocusRef, onWidthCh
1099
1101
  onCancel: onCancelEditor,
1100
1102
  onSubmit: onSubmitEditor,
1101
1103
  isSubmitting: isSubmittingEditor,
1104
+ isSubmitDisabled: isEditorSubmitDisabled,
1102
1105
  } : null, selectedChildWorkstreamId: secondaryPane?.kind === 'detail'
1103
1106
  ? secondaryPane.detail.item.id
1104
1107
  : null, currentWorkspaceId: currentWorkspaceId, currentActorId: currentOwnerId, theme: theme, assigneeOptions: assigneeOptions, linkOptions: linkOptions, showTaskCardStatusLabel: showTaskCardStatusLabel, taskArrangeMode: taskArrangeMode, onTaskArrangeModeChange: onTaskArrangeModeChange, onReorderWorkstreamTasks: onReorderWorkstreamTasks, hydrationState: primaryHydrationState, onRetryHydration: onRetryPrimaryHydration, onSetTaskStatus: detail.type === 'workstream' ? onSetTaskStatus : undefined, onArchivePlanningTask: detail.type === 'workstream' ? onArchivePlanningTask : undefined, onArchiveResolvedWorkstream: detail.type === 'initiative' ? onArchiveResolvedWorkstream : undefined, onArchiveResolvedEntity: detail.type === 'initiative'
@@ -1111,7 +1114,7 @@ export function PlanningDrawer({ open, leftOffset = 0, returnFocusRef, onWidthCh
1111
1114
  ? onUnarchiveInitiative(detail.item.id)
1112
1115
  : onUnarchiveWorkstream(detail.item.id), onAddPlanningContextFile: onAddPlanningContextFile, onRemovePlanningContextFile: onRemovePlanningContextFile, onUpdatePlanningContextCaption: onUpdatePlanningContextCaption })) : null })] }))), wideLayout && !primaryIsWorkstream && !hasSecondaryPane && (workstreamRelationshipExpanded ? (_jsx(RelationshipPane, { label: "Workstream", title: workstreamRelationship.title, description: workstreamRelationship.description, action: workstreamRelationship.action, onCollapse: () => setWorkstreamRelationshipExpanded(false), paneRef: workstreamRelationshipPaneRef })) : (_jsx(CollapsedPane, { label: "Workstream", muted: true, onExpand: () => setWorkstreamRelationshipExpanded(true) }))), showSecondaryPane && (effectiveSecondaryCollapsed ? (_jsx(CollapsedPane, { label: "Workstream", onExpand: onExpandSecondaryPane })) : (_jsxs("div", { ref: secondaryPaneRef, "data-planning-pane": "secondary", className: `${panelStyles.paneShell} ${styles.appScrollbar} tf-scrollbar ${panelStyles.detailPane}`.trim(), style: paneWidthStyle(wideLayout ? PLANNING_DRAWER_DETAIL_WIDTH_PX : adaptiveDetailWidth), children: [_jsxs("div", { className: panelStyles.paneHeader, children: [!wideLayout ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: secondaryInlineEditor
1113
1116
  ? (onCancelSecondaryEditor || onBackFromSecondary)
1114
- : onBackFromSecondary, "aria-label": secondaryInlineEditor ? 'Cancel workstream editing' : `Back to ${primaryPaneLabel}`, title: secondaryInlineEditor ? 'Cancel workstream editing' : `Back to ${primaryPaneLabel}`, children: _jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }) })) : null, _jsx("h2", { className: panelStyles.paneHeaderLabel, tabIndex: -1, "data-planning-pane-heading": true, children: secondaryPaneLabel }), _jsxs("div", { className: panelStyles.headerActionGroup, children: [secondaryArchiveDetail ? (_jsx(ArchiveVisibilityToggle, { visible: archivedDetailVisibility.workstream, disabled: countArchivedDetailChildren(secondaryArchiveDetail) === 0, itemLabel: "tasks", onToggle: () => toggleArchivedDetailVisibility('workstream') })) : null, wideLayout ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: onCollapseSecondaryPane, "aria-label": `Collapse ${secondaryPaneLabel} pane`, title: `Collapse ${secondaryPaneLabel} pane`, children: _jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }) })) : null] })] }), _jsx("div", { className: panelStyles.paneContent, "aria-busy": secondaryHydrationState?.status === 'loading', children: secondaryPane?.kind === 'editor' ? (_jsx(PlanningDrawerEditor, { editor: secondaryPane.editor, draftTitle: draftTitle, draftDescription: draftDescription, draftOwner: draftOwner, draftInitiativeId: draftInitiativeId, draftIcon: draftIcon, draftColor: draftColor, assigneeOptions: assigneeOptions, initiativeLinkOptions: linkOptions.initiatives, draftInitiativeSummary: draftInitiativeSummary, onChangeDraftTitle: onChangeDraftTitle, onChangeDraftDescription: onChangeDraftDescription, onChangeDraftOwner: onChangeDraftOwner, onChangeDraftInitiativeId: onChangeDraftInitiativeId, onChangeDraftIcon: onChangeDraftIcon, onChangeDraftColor: onChangeDraftColor, onCancel: onBackFromSecondary, onSubmit: onSubmitEditor, isSubmitting: isSubmittingEditor })) : secondaryPane ? (_jsx(PlanningDrawerDetailView, { detail: secondaryPane.detail, showArchivedChildren: archivedDetailVisibility.workstream, inlineEditor: secondaryInlineEditor ? {
1117
+ : onBackFromSecondary, "aria-label": secondaryInlineEditor ? 'Cancel workstream editing' : `Back to ${primaryPaneLabel}`, title: secondaryInlineEditor ? 'Cancel workstream editing' : `Back to ${primaryPaneLabel}`, children: _jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }) })) : null, _jsx("h2", { className: panelStyles.paneHeaderLabel, tabIndex: -1, "data-planning-pane-heading": true, children: secondaryPaneLabel }), _jsxs("div", { className: panelStyles.headerActionGroup, children: [secondaryArchiveDetail ? (_jsx(ArchiveVisibilityToggle, { visible: archivedDetailVisibility.workstream, disabled: countArchivedDetailChildren(secondaryArchiveDetail) === 0, itemLabel: "tasks", onToggle: () => toggleArchivedDetailVisibility('workstream') })) : null, wideLayout ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: onCollapseSecondaryPane, "aria-label": `Collapse ${secondaryPaneLabel} pane`, title: `Collapse ${secondaryPaneLabel} pane`, children: _jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }) })) : null] })] }), _jsx("div", { className: panelStyles.paneContent, "aria-busy": secondaryHydrationState?.status === 'loading', children: secondaryPane?.kind === 'editor' ? (_jsx(PlanningDrawerEditor, { editor: secondaryPane.editor, draftTitle: draftTitle, draftDescription: draftDescription, draftOwner: draftOwner, draftInitiativeId: draftInitiativeId, draftIcon: draftIcon, draftColor: draftColor, assigneeOptions: assigneeOptions, initiativeLinkOptions: linkOptions.initiatives, draftInitiativeSummary: draftInitiativeSummary, onChangeDraftTitle: onChangeDraftTitle, onChangeDraftDescription: onChangeDraftDescription, onChangeDraftOwner: onChangeDraftOwner, onChangeDraftInitiativeId: onChangeDraftInitiativeId, onChangeDraftIcon: onChangeDraftIcon, onChangeDraftColor: onChangeDraftColor, onCancel: onBackFromSecondary, onSubmit: onSubmitEditor, isSubmitting: isSubmittingEditor, isSubmitDisabled: isEditorSubmitDisabled })) : secondaryPane ? (_jsx(PlanningDrawerDetailView, { detail: secondaryPane.detail, showArchivedChildren: archivedDetailVisibility.workstream, inlineEditor: secondaryInlineEditor ? {
1115
1118
  editor: secondaryInlineEditor,
1116
1119
  draftTitle,
1117
1120
  draftDescription,
@@ -1125,5 +1128,6 @@ export function PlanningDrawer({ open, leftOffset = 0, returnFocusRef, onWidthCh
1125
1128
  onCancel: onCancelSecondaryEditor || onBackFromSecondary,
1126
1129
  onSubmit: onSubmitEditor,
1127
1130
  isSubmitting: isSubmittingEditor,
1131
+ isSubmitDisabled: isEditorSubmitDisabled,
1128
1132
  } : null, currentWorkspaceId: currentWorkspaceId, currentActorId: currentOwnerId, theme: theme, assigneeOptions: assigneeOptions, linkOptions: linkOptions, showTaskCardStatusLabel: showTaskCardStatusLabel, taskArrangeMode: taskArrangeMode, onTaskArrangeModeChange: onTaskArrangeModeChange, onReorderWorkstreamTasks: onReorderWorkstreamTasks, hydrationState: secondaryHydrationState, onRetryHydration: onRetrySecondaryHydration, onSetTaskStatus: onSetTaskStatus, onArchivePlanningTask: onArchivePlanningTask, onArchiveResolvedEntity: onArchiveResolvedWorkstream, onCreateTaskInWorkstream: onCreateTaskInWorkstream, onOpenTaskById: onOpenTaskById, onChangeTaskAssignee: onChangeTaskAssignee, taskReferences: taskReferences, onAssignTaskToWorkstream: onAssignTaskToWorkstream, onAssignInitiativeToWorkstream: onAssignInitiativeToWorkstream, onAttachTaskToWorkstreamByReference: onAttachTaskToWorkstreamByReference, parentInitiative: resolveParentInitiative(secondaryPane.detail.item), onEdit: () => onEditWorkstream(secondaryPane.detail.item.id), onChangeOwner: (ownerId) => onChangePlanningOwner('workstream', secondaryPane.detail.item.id, ownerId), onChangePlanningOwner: onChangePlanningOwner, onChangePlanningIdentity: onChangePlanningIdentity, onArchive: () => onArchiveWorkstream(secondaryPane.detail.item.id), onUnarchive: () => onUnarchiveWorkstream(secondaryPane.detail.item.id), onAddPlanningContextFile: onAddPlanningContextFile, onRemovePlanningContextFile: onRemovePlanningContextFile, onUpdatePlanningContextCaption: onUpdatePlanningContextCaption })) : null })] })))] }) }));
1129
1133
  }
@@ -172,6 +172,12 @@ const SPECS = [
172
172
  { key: 'TASKFORCE_ADMIN_AUDIT_SYNC_RETENTION_DAYS', description: 'Retention period for non-routine sync audit failures and exceptional events.', category: 'Security', defaultValue: '30', targets: ['server', 'all'] },
173
173
  { key: 'TASKFORCE_ADMIN_AUDIT_RETENTION_DAYS', description: 'Default retention period for administrative and security audit events.', category: 'Security', defaultValue: '365', targets: ['server', 'all'] },
174
174
  { key: 'TASKFORCE_SYNC_KEY', description: 'Optional sync encryption key.', category: 'Security', secret: true, targets: ['server', 'all'] },
175
+ { key: 'TASKFORCE_GITHUB_APP_ID', description: 'GitHub App identifier for the Taskforce GitHub integration.', category: 'Security', targets: ['server', 'all'] },
176
+ { key: 'TASKFORCE_GITHUB_APP_PRIVATE_KEY_BASE64', description: 'Base64-encoded private key for the Taskforce GitHub App.', category: 'Security', secret: true, targets: ['server', 'all'] },
177
+ { key: 'TASKFORCE_GITHUB_APP_SLUG', description: 'Public slug for the Taskforce GitHub App.', category: 'Security', targets: ['server', 'all'] },
178
+ { key: 'TASKFORCE_GITHUB_APP_CLIENT_ID', description: 'OAuth client identifier for the Taskforce GitHub App.', category: 'Security', targets: ['server', 'all'] },
179
+ { key: 'TASKFORCE_GITHUB_APP_CLIENT_SECRET', description: 'OAuth client secret for the Taskforce GitHub App.', category: 'Security', secret: true, targets: ['server', 'all'] },
180
+ { key: 'TASKFORCE_GITHUB_APP_WEBHOOK_SECRET', description: 'Webhook signature secret for the Taskforce GitHub App.', category: 'Security', secret: true, targets: ['server', 'all'] },
175
181
  { key: 'TASKFORCE_GITHUB_REVIEW_APP_ID', description: 'GitHub App identifier used for private-repository review evidence.', category: 'Security', targets: ['server', 'all'] },
176
182
  { key: 'TASKFORCE_GITHUB_REVIEW_APP_PRIVATE_KEY_BASE64', description: 'Base64-encoded GitHub App private key used only to mint short-lived installation tokens.', category: 'Security', secret: true, targets: ['server', 'all'] },
177
183
  { key: 'TASKFORCE_PHASE_A_APPROVAL_ISSUER_ENABLED', description: 'Enable performance-cloud Phase A approval receipt issuance.', category: 'Security', defaultValue: 'false', targets: ['server', 'all'] },
@@ -206,6 +212,7 @@ const SPECS = [
206
212
  { key: 'SYNC_SOAK_DURATION_MIN', description: 'Soak duration in minutes.', category: 'Soak', defaultValue: '60', targets: ['soak', 'all'] },
207
213
  { key: 'SYNC_SOAK_SEED', description: 'Deterministic soak seed.', category: 'Soak', example: '4242', targets: ['soak', 'all'] },
208
214
  { key: 'SYNC_SOAK_TASK_ID_NAMESPACE', description: 'Optional per-run namespace for deterministic generated soak task IDs.', category: 'Soak', targets: ['soak', 'all'] },
215
+ { key: 'SYNC_SOAK_RUNTIME_ROOT', description: 'Root directory for isolated local soak runtimes and cleanup verification.', category: 'Soak', targets: ['soak', 'all'] },
209
216
  { key: 'SYNC_SOAK_ARTIFACT_DIR', description: 'Soak artifacts output directory.', category: 'Soak', defaultValue: '.taskforce/soak-artifacts', targets: ['soak', 'all'] },
210
217
  { key: 'SYNC_SOAK_ENABLE_SYNC_DEBUG', description: 'Enable sync debug diagnostics during soak.', category: 'Soak', defaultValue: 'false', targets: ['soak', 'all'] },
211
218
  { key: 'SYNC_SOAK_STRESS_ACTIONS', description: 'Stress action count.', category: 'Soak', defaultValue: '150', targets: ['soak', 'all'] },
@@ -24,6 +24,7 @@ export declare class McpTokenService {
24
24
  private createMcpAccessTokenPrefix;
25
25
  private parseMcpAccessToken;
26
26
  private normalizeMcpOAuthProvider;
27
+ private normalizeMcpOAuthApplicationType;
27
28
  private defaultMcpOAuthClientName;
28
29
  private createMcpOAuthCodePrefix;
29
30
  private createMcpOAuthAccessTokenPrefix;
@@ -96,6 +97,7 @@ export declare class McpTokenService {
96
97
  provider: McpOAuthProvider | string;
97
98
  clientName: string;
98
99
  redirectUris: unknown;
100
+ applicationType?: unknown;
99
101
  productClientId?: string | null;
100
102
  }): McpOAuthClientRecord;
101
103
  getMcpOAuthClient(clientId: string, provider?: McpOAuthProvider | string | null): McpOAuthClientRecord | null;
@@ -98,6 +98,12 @@ export class McpTokenService {
98
98
  return 'perplexity';
99
99
  throw new TaskforceRuleError('Unsupported MCP OAuth provider', 400, 'MCP_OAUTH_PROVIDER_INVALID');
100
100
  }
101
+ normalizeMcpOAuthApplicationType(raw) {
102
+ const normalized = String(raw || 'web').trim().toLowerCase();
103
+ if (normalized === 'native' || normalized === 'web')
104
+ return normalized;
105
+ throw new TaskforceRuleError('application_type must be native or web', 400, 'MCP_OAUTH_APPLICATION_TYPE_INVALID');
106
+ }
101
107
  defaultMcpOAuthClientName(provider) {
102
108
  if (provider === 'claude')
103
109
  return 'Claude Chat Connector';
@@ -169,14 +175,14 @@ export class McpTokenService {
169
175
  return null;
170
176
  if (provider) {
171
177
  return this.db.prepare(`
172
- SELECT id, provider, client_name, redirect_uris_json, product_client_id, status, created_at, updated_at
178
+ SELECT id, provider, client_name, redirect_uris_json, application_type, product_client_id, status, created_at, updated_at
173
179
  FROM mcp_oauth_clients
174
180
  WHERE tenant_id = ? AND id = ? AND provider = ?
175
181
  LIMIT 1
176
182
  `).get(this.tenantId, normalizedClientId, provider) || null;
177
183
  }
178
184
  return this.db.prepare(`
179
- SELECT id, provider, client_name, redirect_uris_json, product_client_id, status, created_at, updated_at
185
+ SELECT id, provider, client_name, redirect_uris_json, application_type, product_client_id, status, created_at, updated_at
180
186
  FROM mcp_oauth_clients
181
187
  WHERE tenant_id = ? AND id = ?
182
188
  LIMIT 1
@@ -194,6 +200,7 @@ export class McpTokenService {
194
200
  id: String(row?.id || ''),
195
201
  provider: this.normalizeMcpOAuthProvider(row?.provider),
196
202
  clientName: String(row?.client_name || ''),
203
+ applicationType: this.normalizeMcpOAuthApplicationType(row?.application_type),
197
204
  productClientId: row?.product_client_id ? String(row.product_client_id) : null,
198
205
  redirectUris,
199
206
  status: String(row?.status || '').trim().toLowerCase() === 'revoked' ? 'revoked' : 'active',
@@ -716,15 +723,16 @@ export class McpTokenService {
716
723
  const provider = this.normalizeMcpOAuthProvider(input.provider);
717
724
  const clientName = String(input.clientName || '').trim() || this.defaultMcpOAuthClientName(provider);
718
725
  const redirectUris = this.normalizeRedirectUris(input.redirectUris);
726
+ const applicationType = this.normalizeMcpOAuthApplicationType(input.applicationType);
719
727
  const productClientId = String(input.productClientId || '').trim() || null;
720
728
  const now = new Date().toISOString();
721
729
  const clientId = this.generateEntityId(`mcp-oauth-client-${provider}`);
722
730
  this.db.prepare(`
723
731
  INSERT INTO mcp_oauth_clients (
724
- id, tenant_id, provider, client_name, redirect_uris_json, product_client_id, status, created_at, updated_at
732
+ id, tenant_id, provider, client_name, redirect_uris_json, application_type, product_client_id, status, created_at, updated_at
725
733
  )
726
- VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?)
727
- `).run(clientId, this.tenantId, provider, clientName, JSON.stringify(redirectUris), productClientId, now, now);
734
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)
735
+ `).run(clientId, this.tenantId, provider, clientName, JSON.stringify(redirectUris), applicationType, productClientId, now, now);
728
736
  const row = this.getMcpOAuthClientRow(clientId, provider);
729
737
  if (!row) {
730
738
  throw new TaskforceRuleError('Failed to register MCP OAuth client', 500, 'MCP_OAUTH_CLIENT_CREATE_FAILED');
@@ -821,6 +821,7 @@ export interface McpOAuthClientRecord {
821
821
  id: string;
822
822
  provider: McpOAuthProvider;
823
823
  clientName: string;
824
+ applicationType: 'native' | 'web';
824
825
  productClientId?: string | null;
825
826
  redirectUris: string[];
826
827
  status: McpOAuthClientStatus;
@@ -973,7 +974,9 @@ export interface TaskforceSyncCapable {
973
974
  upsertInitiativeForSync(initiative: InitiativeItem, workspaceId?: string | null): InitiativeItem;
974
975
  upsertWorkstreamForSync(workstream: WorkstreamItem, workspaceId?: string | null): WorkstreamItem;
975
976
  upsertDocumentReviewSessionForSync(session: DocumentReviewSessionRecord): DocumentReviewSessionRecord;
976
- upsertDocumentReviewCommentForSync(comment: DocumentReviewCommentSyncRecord): DocumentReviewCommentSyncRecord;
977
+ upsertDocumentReviewCommentForSync(comment: DocumentReviewCommentSyncRecord, options?: {
978
+ allowAuthoritativeThreadReconciliation?: boolean;
979
+ }): DocumentReviewCommentSyncRecord;
977
980
  upsertAnnotatedAttachmentSessionForSync(session: AnnotatedAttachmentSessionRecord): AnnotatedAttachmentSessionRecord;
978
981
  upsertTaskEventForSync(event: TaskEvent): TaskEvent;
979
982
  upsertEntityEventForSync(event: EntityEvent, options?: {
@@ -1786,7 +1789,9 @@ export declare class TaskforceCore implements TaskforceSyncCapable {
1786
1789
  includeDeleted?: boolean;
1787
1790
  }): AnnotatedAttachmentSessionRecord[];
1788
1791
  upsertDocumentReviewSessionForSync(session: DocumentReviewSessionRecord): DocumentReviewSessionRecord;
1789
- upsertDocumentReviewCommentForSync(comment: Omit<DocumentReviewCommentSyncRecord, 'parentCommentId' | 'threadStatus' | 'threadStatusUpdatedAt' | 'resolvedAt' | 'resolvedByActorId'> & Partial<Pick<DocumentReviewCommentSyncRecord, 'parentCommentId' | 'threadStatus' | 'threadStatusUpdatedAt' | 'resolvedAt' | 'resolvedByActorId'>>): DocumentReviewCommentSyncRecord;
1792
+ upsertDocumentReviewCommentForSync(comment: Omit<DocumentReviewCommentSyncRecord, 'parentCommentId' | 'threadStatus' | 'threadStatusUpdatedAt' | 'resolvedAt' | 'resolvedByActorId'> & Partial<Pick<DocumentReviewCommentSyncRecord, 'parentCommentId' | 'threadStatus' | 'threadStatusUpdatedAt' | 'resolvedAt' | 'resolvedByActorId'>>, options?: {
1793
+ allowAuthoritativeThreadReconciliation?: boolean;
1794
+ }): DocumentReviewCommentSyncRecord;
1790
1795
  upsertAnnotatedAttachmentSessionForSync(session: AnnotatedAttachmentSessionRecord): AnnotatedAttachmentSessionRecord;
1791
1796
  upsertTaskEventForSync(event: TaskEvent): TaskEvent;
1792
1797
  upsertEntityEventForSync(event: EntityEvent, options?: {
@@ -2241,6 +2246,7 @@ export declare class TaskforceCore implements TaskforceSyncCapable {
2241
2246
  provider: McpOAuthProvider | string;
2242
2247
  clientName: string;
2243
2248
  redirectUris: unknown;
2249
+ applicationType?: unknown;
2244
2250
  productClientId?: string | null;
2245
2251
  }): McpOAuthClientRecord;
2246
2252
  getMcpOAuthClient(clientId: string, provider?: McpOAuthProvider | string | null): McpOAuthClientRecord | null;
@@ -2456,7 +2462,9 @@ export declare class TaskforceCore implements TaskforceSyncCapable {
2456
2462
  notifyDurableWorkflowRuntimeCommitted(workspaceId: string, _executionId: string): void;
2457
2463
  installDurableInitiativeAuthoritativeForSync(initiative: InitiativeItem, workspaceId: string): InitiativeItem;
2458
2464
  installDurableWorkstreamAuthoritativeForSync(workstream: WorkstreamItem, workspaceId: string): WorkstreamItem;
2459
- installDurableTaskCreateAuthoritativeForSync(task: TaskItem, workspaceId: string): TaskItem;
2465
+ installDurableTaskCreateAuthoritativeForSync(task: TaskItem, workspaceId: string, options?: {
2466
+ preserveDeletedSuccessor?: boolean;
2467
+ }): TaskItem;
2460
2468
  installWorkspaceSyncV3TaskRootAuthoritativeForSync(task: TaskItem, workspaceId: string): TaskItem;
2461
2469
  bulkCreateTasks(tasks: Array<Partial<TaskItem>>, options?: {
2462
2470
  atomic?: boolean;