@taskforcehq/taskforce 0.3.302 → 0.3.304

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 (192) hide show
  1. package/dist/Taskforce.module.css +113 -5
  2. package/dist/TaskforceCore.js +560 -208
  3. package/dist/TaskforceCore.test.js +1833 -91
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskKanban.test.js +17 -0
  21. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  22. package/dist/components/task/TaskStatusActions.js +8 -3
  23. package/dist/components/task/TaskStatusActions.test.js +9 -0
  24. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  25. package/dist/components/task/taskKanbanDropRules.js +187 -0
  26. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  27. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  28. package/dist/components/ui/Modal.d.ts +1 -1
  29. package/dist/components/ui/Modal.js +1 -0
  30. package/dist/components/views/AppShellHeader.js +5 -1
  31. package/dist/components/views/AppShellHeader.test.js +4 -0
  32. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  33. package/dist/components/views/PlanComparisonPage.js +4 -2
  34. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  35. package/dist/components/views/PlansPage.d.ts +7 -1
  36. package/dist/components/views/PlansPage.js +139 -53
  37. package/dist/components/views/PlansPage.test.d.ts +1 -0
  38. package/dist/components/views/PlansPage.test.js +157 -0
  39. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  40. package/dist/components/views/StandaloneLayout.js +216 -186
  41. package/dist/components/views/WidgetView.js +11 -2
  42. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  43. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  44. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  45. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  46. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  47. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  48. package/dist/core/EntitlementsPolicy.test.js +2 -2
  49. package/dist/core/GlobalSettingsService.js +78 -18
  50. package/dist/core/PlanEntitlementService.d.ts +31 -5
  51. package/dist/core/PlanEntitlementService.js +350 -222
  52. package/dist/core/SignupMonetizationSettings.test.js +208 -49
  53. package/dist/core/SystemAdmin.test.js +186 -96
  54. package/dist/core/TaskActivityService.d.ts +4 -1
  55. package/dist/core/TaskActivityService.js +45 -0
  56. package/dist/core/TaskAuditTimeline.test.js +52 -1
  57. package/dist/core/Taskforce.d.ts +65 -20
  58. package/dist/core/Taskforce.js +261 -51
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  61. package/dist/core/WorkspacePlanMode.test.js +65 -0
  62. package/dist/core/types.d.ts +11 -12
  63. package/dist/documentReviews/service.d.ts +1 -1
  64. package/dist/documentReviews/types.d.ts +1 -1
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  66. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  67. package/dist/hooks/sync/auth.d.ts +37 -0
  68. package/dist/hooks/sync/auth.js +61 -0
  69. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  70. package/dist/hooks/sync/bootstrap.js +52 -0
  71. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  72. package/dist/hooks/sync/orchestratorShared.js +209 -0
  73. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  74. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  75. package/dist/hooks/sync/realtime.d.ts +25 -0
  76. package/dist/hooks/sync/realtime.js +60 -0
  77. package/dist/hooks/sync/recovery.d.ts +69 -0
  78. package/dist/hooks/sync/recovery.js +118 -0
  79. package/dist/hooks/sync/transfers.d.ts +122 -0
  80. package/dist/hooks/sync/transfers.js +431 -0
  81. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  82. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  83. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  84. package/dist/hooks/ui/useResourceExport.js +5 -2
  85. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  86. package/dist/hooks/useRealtimeSync.js +2 -1
  87. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  88. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  89. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  90. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  91. package/dist/hooks/useSyncOrchestrator.js +315 -860
  92. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
  93. package/dist/hooks/useTaskMutations.js +45 -6
  94. package/dist/hooks/useTaskMutations.test.js +147 -0
  95. package/dist/hooks/useTaskforce.d.ts +11 -2
  96. package/dist/hooks/useTaskforce.js +290 -812
  97. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  98. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  99. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  100. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  101. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  102. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  103. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  104. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  105. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  106. package/dist/localization/locales/en-US.js +2 -2
  107. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  108. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  109. package/dist/mcp/clientIntegrations.d.ts +12 -1
  110. package/dist/mcp/clientIntegrations.js +161 -6
  111. package/dist/mcp/clientIntegrations.test.js +76 -4
  112. package/dist/mcp/documentHelpers.d.ts +100 -0
  113. package/dist/mcp/documentHelpers.js +161 -0
  114. package/dist/mcp/runtime.js +264 -505
  115. package/dist/mcp/runtime.test.js +353 -89
  116. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  117. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  118. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  119. package/dist/mcp/taskAttachmentTypes.js +1 -0
  120. package/dist/migrations/billingSchemaParity.test.js +3 -0
  121. package/dist/migrations/taskSchemaMigrations.js +15 -0
  122. package/dist/plugins/vite.js +1 -1
  123. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  124. package/dist/runtime/appGateDefinitions.js +1 -1
  125. package/dist/server/index.cookieProxy.test.js +1 -0
  126. package/dist/server/index.d.ts +1 -0
  127. package/dist/server/index.js +48 -3
  128. package/dist/server/index.test.js +14 -2
  129. package/dist/server/mockStripe.d.ts +1 -0
  130. package/dist/server/mockStripe.js +2 -0
  131. package/dist/server/routes/admin.js +91 -40
  132. package/dist/server/routes/auth.d.ts +1 -0
  133. package/dist/server/routes/auth.js +231 -55
  134. package/dist/server/routes/billing.js +473 -39
  135. package/dist/server/routes/billing.test.js +972 -37
  136. package/dist/server/routes/shared.d.ts +30 -0
  137. package/dist/server/routes/shared.js +31 -3
  138. package/dist/server/routes/sync.integration.test.js +180 -0
  139. package/dist/server/routes/sync.js +125 -242
  140. package/dist/server/routes/tasks.js +49 -7
  141. package/dist/server/routes.js +11 -22
  142. package/dist/server/routes.test.js +530 -81
  143. package/dist/shared/taskActor.d.ts +10 -0
  144. package/dist/shared/taskActor.js +1 -0
  145. package/dist/sync/cloudSyncApi.d.ts +1 -0
  146. package/dist/sync/collaborationSyncPayload.js +4 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -1
  149. package/dist/sync/collaborationSyncState.js +19 -0
  150. package/dist/sync/contentSyncState.d.ts +10 -0
  151. package/dist/sync/contentSyncState.js +277 -14
  152. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  153. package/dist/sync/syncApplyHandlers.js +370 -8
  154. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  155. package/dist/sync/syncApplyHandlers.test.js +377 -0
  156. package/dist/sync/syncService.d.ts +9 -1
  157. package/dist/sync/syncService.js +57 -10
  158. package/dist/sync/syncService.test.js +48 -0
  159. package/dist/sync/workspacePullFeed.d.ts +11 -0
  160. package/dist/sync/workspacePullFeed.js +123 -68
  161. package/dist/sync/workspacePullFeed.test.js +180 -0
  162. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  163. package/dist/sync/workspaceSyncModel.js +65 -0
  164. package/dist/sync/workspaceSyncModel.test.js +119 -0
  165. package/dist/types.d.ts +2 -10
  166. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  167. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  168. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  169. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
  170. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  171. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  172. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  173. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
  174. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  175. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  176. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
  177. package/dist/ui/index.html +4 -4
  178. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  179. package/dist/utils/accountProfileSummaryCache.js +97 -0
  180. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  181. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  182. package/dist/utils/taskActivity.js +4 -0
  183. package/dist/utils/taskActivity.test.js +25 -1
  184. package/package.json +5 -3
  185. package/scripts/playwright-auth.sh +4 -0
  186. package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
  187. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  188. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
  189. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  190. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  191. package/dist/utils/billingStatusCache.d.ts +0 -21
  192. package/dist/utils/billingStatusCache.js +0 -70
@@ -109,6 +109,17 @@ describe('TaskSettings Component', () => {
109
109
  };
110
110
  return render(_jsx(TaskSettings, { ...props }));
111
111
  };
112
+ const openMcpSettings = async (user) => {
113
+ await user.click(screen.getByRole('button', { name: /MCP/i }));
114
+ };
115
+ const openCloudMcpSettings = async (user) => {
116
+ await openMcpSettings(user);
117
+ const cloudToggle = screen.queryByRole('button', { name: 'Cloud MCP' });
118
+ if (cloudToggle) {
119
+ await user.click(cloudToggle);
120
+ }
121
+ await screen.findByText('Cloud MCP Configuration');
122
+ };
112
123
  const originalFetch = global.fetch;
113
124
  it('4.1 Open settings - Renders correctly', () => {
114
125
  renderComponent();
@@ -859,7 +870,7 @@ describe('TaskSettings Component', () => {
859
870
  credentials: 'include'
860
871
  }));
861
872
  }, 10000);
862
- it('4.21 Local authenticated runtime defaults MCP tab to cloud with toggle', async () => {
873
+ it('4.21 Local authenticated runtime defaults MCP tab to local with local-first toggle', async () => {
863
874
  const fetchCloudAuthApi = vi.fn(async () => ({
864
875
  ok: true,
865
876
  json: async () => ({ items: [] })
@@ -875,15 +886,17 @@ describe('TaskSettings Component', () => {
875
886
  }
876
887
  });
877
888
  await user.click(screen.getByRole('button', { name: /MCP/i }));
878
- expect(await screen.findByRole('button', { name: 'Cloud MCP' })).toBeInTheDocument();
879
- expect(screen.getByRole('button', { name: 'Local MCP' })).toBeInTheDocument();
880
- expect(screen.getByText('Cloud MCP Configuration')).toBeInTheDocument();
881
- expect(screen.getAllByText(/https:\/\/mcp\.taskforcehq\.ai\/mcp/).length).toBeGreaterThan(0);
882
- expect(screen.queryByText('MCP Path Mapping (Host Root)')).not.toBeInTheDocument();
883
- await user.click(screen.getByRole('button', { name: 'Local MCP' }));
889
+ const localButton = await screen.findByRole('button', { name: 'Local MCP' });
890
+ const cloudButton = screen.getByRole('button', { name: 'Cloud MCP' });
891
+ expect(localButton.compareDocumentPosition(cloudButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
892
+ expect(screen.getByText('Local MCP Configuration')).toBeInTheDocument();
893
+ expect(screen.queryByText('Cloud MCP Configuration')).not.toBeInTheDocument();
884
894
  expect(screen.queryByText('MCP Path Mapping (Host Root)')).not.toBeInTheDocument();
885
895
  await user.click(screen.getByRole('button', { name: 'Advanced' }));
886
896
  expect(screen.getByText('MCP Path Mapping (Host Root)')).toBeInTheDocument();
897
+ await user.click(cloudButton);
898
+ expect(screen.getByText('Cloud MCP Configuration')).toBeInTheDocument();
899
+ expect(screen.getAllByText(/https:\/\/mcp\.taskforcehq\.ai\/mcp/).length).toBeGreaterThan(0);
887
900
  expect(fetchCloudAuthApi).toHaveBeenCalledWith('/api/taskforce/settings/mcp/tokens', expect.objectContaining({
888
901
  method: 'GET',
889
902
  credentials: 'include'
@@ -900,7 +913,7 @@ describe('TaskSettings Component', () => {
900
913
  id: 'mcp-token-1',
901
914
  name: 'Workspace Alpha MCP',
902
915
  tokenPrefix: 'tfmcp_example',
903
- scopes: ['tasks:read', 'tasks:write'],
916
+ scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
904
917
  status: 'active',
905
918
  createdAt: new Date().toISOString()
906
919
  }
@@ -921,12 +934,19 @@ describe('TaskSettings Component', () => {
921
934
  fetchCloudAuthApi
922
935
  }
923
936
  });
924
- await user.click(screen.getByRole('button', { name: /MCP/i }));
925
- await user.click(await screen.findByRole('button', { name: 'Generate Token' }));
937
+ await openCloudMcpSettings(user);
938
+ await user.click(screen.getByRole('button', { name: 'Generate Token' }));
926
939
  expect(fetchCloudAuthApi).toHaveBeenCalledWith('/api/taskforce/settings/mcp/tokens', expect.objectContaining({
927
940
  method: 'POST',
928
941
  credentials: 'include'
929
942
  }));
943
+ expect(fetchCloudAuthApi).toHaveBeenCalledWith('/api/taskforce/settings/mcp/tokens', expect.objectContaining({
944
+ body: JSON.stringify({
945
+ name: 'Workspace Alpha MCP',
946
+ scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
947
+ expiresAt: null
948
+ })
949
+ }));
930
950
  expect((await screen.findAllByText('tfmcp_example_secret')).length).toBeGreaterThan(0);
931
951
  }, 10000);
932
952
  it('4.23c MCP token name defaults to the workspace name', async () => {
@@ -938,7 +958,7 @@ describe('TaskSettings Component', () => {
938
958
  currentWorkspaceName: 'Workspace Alpha'
939
959
  }
940
960
  });
941
- await user.click(screen.getByRole('button', { name: /MCP/i }));
961
+ await openCloudMcpSettings(user);
942
962
  expect(await screen.findByDisplayValue('Workspace Alpha MCP')).toBeInTheDocument();
943
963
  });
944
964
  it('4.23d MCP test connection uses the provided cloud auth helper', async () => {
@@ -950,7 +970,7 @@ describe('TaskSettings Component', () => {
950
970
  result: {
951
971
  workspaceName: 'Workspace Alpha',
952
972
  workspaceId: 'workspace-alpha',
953
- scopes: ['tasks:read', 'tasks:write']
973
+ scopes: ['tasks:read', 'tasks:write', 'workspace:read']
954
974
  }
955
975
  })
956
976
  };
@@ -969,7 +989,7 @@ describe('TaskSettings Component', () => {
969
989
  fetchCloudAuthApi
970
990
  }
971
991
  });
972
- await user.click(screen.getByRole('button', { name: /MCP/i }));
992
+ await openCloudMcpSettings(user);
973
993
  await user.click(screen.getByRole('button', { name: 'Test Connection' }));
974
994
  await user.type(await screen.findByPlaceholderText('tfmcp_xxxxx_replace-me'), 'tfmcp_example_secret');
975
995
  await user.click(screen.getAllByRole('button', { name: 'Test Connection' })[1]);
@@ -978,7 +998,7 @@ describe('TaskSettings Component', () => {
978
998
  credentials: 'include'
979
999
  }));
980
1000
  expect(await screen.findByText('Connection looks good for this workspace.')).toBeInTheDocument();
981
- expect(screen.getByText(/Granted scopes: tasks:read, tasks:write/i)).toBeInTheDocument();
1001
+ expect(screen.getByText(/Granted scopes: tasks:read, tasks:write, workspace:read/i)).toBeInTheDocument();
982
1002
  }, 10000);
983
1003
  it('4.23e Antigravity uses serverUrl instead of transport in the cloud snippet', async () => {
984
1004
  const fetchCloudAuthApi = vi.fn(async () => ({
@@ -995,7 +1015,7 @@ describe('TaskSettings Component', () => {
995
1015
  fetchCloudAuthApi
996
1016
  }
997
1017
  });
998
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1018
+ await openCloudMcpSettings(user);
999
1019
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'antigravity');
1000
1020
  const snippetMatches = await screen.findAllByText(/serverUrl/);
1001
1021
  expect(snippetMatches.length).toBeGreaterThan(0);
@@ -1018,7 +1038,7 @@ describe('TaskSettings Component', () => {
1018
1038
  fetchCloudAuthApi
1019
1039
  }
1020
1040
  });
1021
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1041
+ await openCloudMcpSettings(user);
1022
1042
  expect(await screen.findByText(/https:\/\/performance-mcp\.taskforcehq\.ai\/mcp/)).toBeInTheDocument();
1023
1043
  expect(screen.queryByText(/https:\/\/performance-app\.taskforcehq\.ai\/mcp/)).not.toBeInTheDocument();
1024
1044
  }, 10000);
@@ -1038,7 +1058,7 @@ describe('TaskSettings Component', () => {
1038
1058
  fetchCloudAuthApi
1039
1059
  }
1040
1060
  });
1041
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1061
+ await openCloudMcpSettings(user);
1042
1062
  expect(await screen.findByText(/https:\/\/performance-mcp\.taskforcehq\.ai\/mcp/)).toBeInTheDocument();
1043
1063
  expect(screen.queryByText(/https:\/\/performance-app\.taskforcehq\.ai\/mcp/)).not.toBeInTheDocument();
1044
1064
  }, 10000);
@@ -1057,7 +1077,7 @@ describe('TaskSettings Component', () => {
1057
1077
  fetchCloudAuthApi
1058
1078
  }
1059
1079
  });
1060
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1080
+ await openCloudMcpSettings(user);
1061
1081
  const urlMatches = await screen.findAllByText(/"url"/);
1062
1082
  expect(urlMatches.length).toBeGreaterThan(0);
1063
1083
  expect(screen.getAllByText(/https:\/\/mcp\.taskforcehq\.ai\/mcp/).length).toBeGreaterThan(0);
@@ -1079,7 +1099,7 @@ describe('TaskSettings Component', () => {
1079
1099
  fetchCloudAuthApi
1080
1100
  }
1081
1101
  });
1082
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1102
+ await openCloudMcpSettings(user);
1083
1103
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'cline');
1084
1104
  expect(await screen.findByText(/Cline Configuration Instructions/i)).toBeInTheDocument();
1085
1105
  expect(screen.getByText(/"type"/)).toBeInTheDocument();
@@ -1101,7 +1121,7 @@ describe('TaskSettings Component', () => {
1101
1121
  fetchCloudAuthApi
1102
1122
  }
1103
1123
  });
1104
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1124
+ await openCloudMcpSettings(user);
1105
1125
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'vscode');
1106
1126
  expect(await screen.findByText(/VS Code Configuration Instructions/i)).toBeInTheDocument();
1107
1127
  expect(screen.getByText(/"servers"/)).toBeInTheDocument();
@@ -1123,7 +1143,7 @@ describe('TaskSettings Component', () => {
1123
1143
  fetchCloudAuthApi
1124
1144
  }
1125
1145
  });
1126
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1146
+ await openCloudMcpSettings(user);
1127
1147
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
1128
1148
  expect((await screen.findAllByText(/\.codex\/config\.toml/i)).length).toBeGreaterThan(0);
1129
1149
  expect(screen.getByText(/\[mcp_servers\./)).toBeInTheDocument();
@@ -1145,7 +1165,7 @@ describe('TaskSettings Component', () => {
1145
1165
  fetchCloudAuthApi
1146
1166
  }
1147
1167
  });
1148
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1168
+ await openCloudMcpSettings(user);
1149
1169
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'claude-code');
1150
1170
  expect(await screen.findByText(/Claude Code Configuration Instructions/i)).toBeInTheDocument();
1151
1171
  expect(screen.getByText(/claude mcp add --transport http/i)).toBeInTheDocument();
@@ -1167,7 +1187,7 @@ describe('TaskSettings Component', () => {
1167
1187
  fetchCloudAuthApi
1168
1188
  }
1169
1189
  });
1170
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1190
+ await openCloudMcpSettings(user);
1171
1191
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'gemini-cli');
1172
1192
  expect(await screen.findByText(/Gemini CLI Configuration Instructions/i)).toBeInTheDocument();
1173
1193
  expect(screen.getByText(/gemini mcp add taskforce-/i)).toBeInTheDocument();
@@ -1190,7 +1210,7 @@ describe('TaskSettings Component', () => {
1190
1210
  fetchCloudAuthApi
1191
1211
  }
1192
1212
  });
1193
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1213
+ await openCloudMcpSettings(user);
1194
1214
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'openclaw');
1195
1215
  expect(await screen.findByText(/OpenClaw Configuration Instructions/i)).toBeInTheDocument();
1196
1216
  expect((await screen.findAllByText(/~\/\.openclaw\/openclaw\.json/i)).length).toBeGreaterThan(0);
@@ -1217,10 +1237,10 @@ describe('TaskSettings Component', () => {
1217
1237
  fetchCloudAuthApi
1218
1238
  }
1219
1239
  });
1220
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1240
+ await openCloudMcpSettings(user);
1221
1241
  expect(screen.queryByRole('option', { name: /Claude Desktop/i })).not.toBeInTheDocument();
1222
1242
  expect(screen.queryByRole('option', { name: /ChatGPT Desktop/i })).not.toBeInTheDocument();
1223
- expect(await screen.findByText('Cursor Configuration Instructions')).toBeInTheDocument();
1243
+ expect(await screen.findByText('General MCP Configuration Instructions')).toBeInTheDocument();
1224
1244
  }, 10000);
1225
1245
  it('4.23e4b Integrations tab shows ChatGPT and Claude connector management separately from PAT tokens', async () => {
1226
1246
  const fetchCloudAuthApi = vi.fn(async (path) => {
@@ -1294,8 +1314,7 @@ describe('TaskSettings Component', () => {
1294
1314
  fetchCloudAuthApi
1295
1315
  }
1296
1316
  });
1297
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1298
- expect(await screen.findByText('Cloud MCP Configuration')).toBeInTheDocument();
1317
+ await openCloudMcpSettings(user);
1299
1318
  expect(screen.queryByText('ChatGPT Connector')).not.toBeInTheDocument();
1300
1319
  expect(screen.queryByText(/^Connectors$/)).not.toBeInTheDocument();
1301
1320
  }, 10000);
@@ -1309,8 +1328,7 @@ describe('TaskSettings Component', () => {
1309
1328
  cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
1310
1329
  }
1311
1330
  });
1312
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1313
- expect(await screen.findByText('Cloud MCP Configuration')).toBeInTheDocument();
1331
+ await openCloudMcpSettings(user);
1314
1332
  expect(screen.getAllByRole('button', { name: /copy/i }).length).toBeGreaterThan(0);
1315
1333
  }, 10000);
1316
1334
  it('4.23g1 Cloud MCP copy warns before a token is generated', async () => {
@@ -1323,8 +1341,7 @@ describe('TaskSettings Component', () => {
1323
1341
  cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
1324
1342
  }
1325
1343
  });
1326
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1327
- await screen.findByText('Cloud MCP Configuration');
1344
+ await openCloudMcpSettings(user);
1328
1345
  await user.click(screen.getByTitle('Copy full configuration file'));
1329
1346
  expect(screen.getByText('Generate a token before copying the Cloud MCP configuration.')).toBeInTheDocument();
1330
1347
  }, 10000);
@@ -1338,8 +1355,7 @@ describe('TaskSettings Component', () => {
1338
1355
  cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
1339
1356
  }
1340
1357
  });
1341
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1342
- await screen.findByText('Cloud MCP Configuration');
1358
+ await openCloudMcpSettings(user);
1343
1359
  expect(screen.queryByText('Automation Identity (Optional)')).not.toBeInTheDocument();
1344
1360
  expect(screen.queryByRole('combobox', { name: 'Use existing AI profile' })).not.toBeInTheDocument();
1345
1361
  }, 10000);
@@ -1353,8 +1369,7 @@ describe('TaskSettings Component', () => {
1353
1369
  cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
1354
1370
  }
1355
1371
  });
1356
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1357
- await screen.findByText('Cloud MCP Configuration');
1372
+ await openCloudMcpSettings(user);
1358
1373
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
1359
1374
  expect(screen.queryByText(/X-Taskforce-AI-Name/i)).not.toBeInTheDocument();
1360
1375
  expect(screen.queryByText(/X-Taskforce-AI-Profile-Token/i)).not.toBeInTheDocument();
@@ -1394,7 +1409,7 @@ describe('TaskSettings Component', () => {
1394
1409
  fetchCloudAuthApi
1395
1410
  }
1396
1411
  });
1397
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1412
+ await openCloudMcpSettings(user);
1398
1413
  expect(await screen.findByText('Current Token')).toBeInTheDocument();
1399
1414
  expect(screen.getByText('Old Token')).toBeInTheDocument();
1400
1415
  const tokenLabels = screen.getAllByText(/^(Current Token|Old Token)$/);
@@ -1414,8 +1429,7 @@ describe('TaskSettings Component', () => {
1414
1429
  cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
1415
1430
  }
1416
1431
  });
1417
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1418
- await screen.findByText('Cloud MCP Configuration');
1432
+ await openCloudMcpSettings(user);
1419
1433
  const codeNodes = Array.from(document.querySelectorAll('code'));
1420
1434
  const snippetNode = codeNodes.find((node) => node.textContent?.includes('"mcpServers"'));
1421
1435
  expect(snippetNode).toBeTruthy();
@@ -1435,11 +1449,96 @@ describe('TaskSettings Component', () => {
1435
1449
  });
1436
1450
  await user.click(screen.getByRole('button', { name: /MCP/i }));
1437
1451
  expect(screen.getByText('Local MCP')).toBeInTheDocument();
1438
- expect(screen.getByText(/Claude Code CLI, Gemini CLI/i)).toBeInTheDocument();
1452
+ expect(screen.getByRole('combobox', { name: 'MCP client' })).toHaveValue('general');
1453
+ expect(screen.getByText('General MCP Configuration Instructions')).toBeInTheDocument();
1439
1454
  expect(screen.queryByText('MCP Path Mapping (Host Root)')).not.toBeInTheDocument();
1440
1455
  await user.click(screen.getByRole('button', { name: 'Advanced' }));
1441
1456
  expect(screen.getByText('MCP Path Mapping (Host Root)')).toBeInTheDocument();
1442
1457
  expect(screen.queryByText('Cloud MCP Configuration')).not.toBeInTheDocument();
1443
1458
  expect(screen.queryByRole('button', { name: 'Cloud MCP' })).not.toBeInTheDocument();
1444
1459
  });
1460
+ it('4.24a Local MCP selector offers General first and keeps the rest alphabetical', async () => {
1461
+ const user = userEvent.setup();
1462
+ renderComponent({
1463
+ settingsModel: {
1464
+ runtimeMode: 'local',
1465
+ isAuthenticated: false
1466
+ }
1467
+ });
1468
+ await user.click(screen.getByRole('button', { name: /MCP/i }));
1469
+ const options = screen.getAllByRole('option').map((option) => option.textContent);
1470
+ expect(options).toEqual([
1471
+ 'General',
1472
+ 'Antigravity',
1473
+ 'Claude Code CLI',
1474
+ 'Cline',
1475
+ 'Codex',
1476
+ 'Cursor',
1477
+ 'Gemini CLI',
1478
+ 'OpenClaw',
1479
+ 'VS Code / Copilot',
1480
+ 'Windsurf'
1481
+ ]);
1482
+ });
1483
+ it('4.24b Local Codex shows config.toml MCP output without token headers', async () => {
1484
+ const user = userEvent.setup();
1485
+ renderComponent({
1486
+ settingsModel: {
1487
+ runtimeMode: 'local',
1488
+ isAuthenticated: false,
1489
+ projectRoot: '/Users/rossburke/projects/taskforce',
1490
+ projectName: 'taskforce',
1491
+ tenantId: 'tenant-alpha',
1492
+ workspaceId: 'workspace-alpha',
1493
+ mcpScriptPath: '/usr/local/bin/taskforce'
1494
+ }
1495
+ });
1496
+ await user.click(screen.getByRole('button', { name: /MCP/i }));
1497
+ await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
1498
+ expect(await screen.findByText(/Codex Configuration Instructions/i)).toBeInTheDocument();
1499
+ expect((await screen.findAllByText(/\.codex\/config\.toml/i)).length).toBeGreaterThan(0);
1500
+ expect(screen.getByText(/\[mcp_servers\./)).toBeInTheDocument();
1501
+ expect(screen.getByText(/command = "taskforce"/i)).toBeInTheDocument();
1502
+ expect(screen.queryByText(/Authorization/i)).not.toBeInTheDocument();
1503
+ });
1504
+ it('4.24c Local VS Code uses stdio config under servers', async () => {
1505
+ const user = userEvent.setup();
1506
+ renderComponent({
1507
+ settingsModel: {
1508
+ runtimeMode: 'local',
1509
+ isAuthenticated: false,
1510
+ projectRoot: '/Users/rossburke/projects/taskforce',
1511
+ projectName: 'taskforce',
1512
+ tenantId: 'tenant-alpha',
1513
+ workspaceId: 'workspace-alpha',
1514
+ mcpScriptPath: '/usr/local/bin/taskforce'
1515
+ }
1516
+ });
1517
+ await user.click(screen.getByRole('button', { name: /MCP/i }));
1518
+ await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'vscode');
1519
+ expect(await screen.findByText(/VS Code Configuration Instructions/i)).toBeInTheDocument();
1520
+ expect(screen.getByText(/"servers"/)).toBeInTheDocument();
1521
+ expect(screen.getByText(/"type"/)).toBeInTheDocument();
1522
+ expect(screen.getByText(/"stdio"/)).toBeInTheDocument();
1523
+ });
1524
+ it('4.24d Local Claude Code shows a stdio add command', async () => {
1525
+ const user = userEvent.setup();
1526
+ renderComponent({
1527
+ settingsModel: {
1528
+ runtimeMode: 'local',
1529
+ isAuthenticated: false,
1530
+ projectRoot: '/Users/rossburke/projects/taskforce',
1531
+ projectName: 'taskforce',
1532
+ tenantId: 'tenant-alpha',
1533
+ workspaceId: 'workspace-alpha',
1534
+ mcpScriptPath: '/usr/local/bin/taskforce'
1535
+ }
1536
+ });
1537
+ await user.click(screen.getByRole('button', { name: /MCP/i }));
1538
+ await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'claude-code');
1539
+ expect(await screen.findByText(/Claude Code Configuration Instructions/i)).toBeInTheDocument();
1540
+ expect(screen.getByText(/claude mcp add taskforce-/i)).toBeInTheDocument();
1541
+ expect(screen.getByText(/--env TASKFORCE_TENANT_ID=tenant-alpha/i)).toBeInTheDocument();
1542
+ expect(screen.getByText(/-- taskforce mcp/i)).toBeInTheDocument();
1543
+ });
1445
1544
  });
@@ -23,8 +23,9 @@ interface TaskActionHeaderProps {
23
23
  handleToggleCancel: (task: TaskItem) => void;
24
24
  handleSetStatus?: (task: TaskItem, status: TaskStatus) => void;
25
25
  handleArchiveTask: (task: TaskItem) => void;
26
+ handleUnarchiveTask?: (taskId: string) => void;
26
27
  handleRestoreDeletedTask?: (deletedRecordId: string) => void;
27
28
  handlePermanentlyDeleteDeletedTask?: (deletedRecordId: string) => void;
28
29
  }
29
- export declare function TaskActionHeader({ editingTaskId, loading, autoSaveState, title, handleSubmit, resetForm, handleCopyId, copiedId, tasks, currentTask: currentTaskProp, currentTaskWorkstream, currentTaskInitiative, workstreamInput, onWorkstreamInputChange, onSetWorkstreamForCurrentTask, handleToggleInProgress, handleToggleReview, handleToggleComplete, handleToggleCancel, handleSetStatus, handleArchiveTask, handleRestoreDeletedTask, handlePermanentlyDeleteDeletedTask }: TaskActionHeaderProps): import("react/jsx-runtime").JSX.Element;
30
+ export declare function TaskActionHeader({ editingTaskId, loading, autoSaveState, title, handleSubmit, resetForm, handleCopyId, copiedId, tasks, currentTask: currentTaskProp, currentTaskWorkstream, currentTaskInitiative, workstreamInput, onWorkstreamInputChange, onSetWorkstreamForCurrentTask, handleToggleInProgress, handleToggleReview, handleToggleComplete, handleToggleCancel, handleSetStatus, handleArchiveTask, handleUnarchiveTask, handleRestoreDeletedTask, handlePermanentlyDeleteDeletedTask }: TaskActionHeaderProps): import("react/jsx-runtime").JSX.Element;
30
31
  export {};
@@ -7,7 +7,7 @@ import { TaskStatusActions } from './TaskStatusActions';
7
7
  import { getTaskReferenceDisplayState } from '../../utils/taskReferences';
8
8
  import { TaskReferenceBadge } from './TaskReferenceBadge';
9
9
  import { formatInitiativeReference, getWorkstreamReferenceLabel } from '../../utils/planningReferences';
10
- export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle', title, handleSubmit, resetForm, handleCopyId, copiedId, tasks, currentTask: currentTaskProp, currentTaskWorkstream, currentTaskInitiative, workstreamInput = '', onWorkstreamInputChange, onSetWorkstreamForCurrentTask, handleToggleInProgress, handleToggleReview, handleToggleComplete, handleToggleCancel, handleSetStatus, handleArchiveTask, handleRestoreDeletedTask, handlePermanentlyDeleteDeletedTask }) {
10
+ export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle', title, handleSubmit, resetForm, handleCopyId, copiedId, tasks, currentTask: currentTaskProp, currentTaskWorkstream, currentTaskInitiative, workstreamInput = '', onWorkstreamInputChange, onSetWorkstreamForCurrentTask, handleToggleInProgress, handleToggleReview, handleToggleComplete, handleToggleCancel, handleSetStatus, handleArchiveTask, handleUnarchiveTask, handleRestoreDeletedTask, handlePermanentlyDeleteDeletedTask }) {
11
11
  const [showWorkstreamEditor, setShowWorkstreamEditor] = React.useState(false);
12
12
  const currentTask = currentTaskProp ?? (editingTaskId ? tasks.find(t => t.id === editingTaskId) || null : null);
13
13
  const isArchivedReadOnly = Boolean(currentTask?.isArchived);
@@ -48,7 +48,7 @@ export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle
48
48
  } }), _jsx("button", { type: "button", className: "tf-control-icon tf-control-icon-compact", onClick: handleWorkstreamConfirm, disabled: !workstreamInput.trim() || loading, title: "Attach workstream", "aria-label": "Attach workstream", children: _jsx(Check, { size: 14 }) }), _jsx("button", { type: "button", className: "tf-control-icon tf-control-icon-compact", onClick: () => {
49
49
  onWorkstreamInputChange?.('');
50
50
  setShowWorkstreamEditor(false);
51
- }, disabled: loading, title: "Cancel workstream attach", "aria-label": "Cancel workstream attach", children: _jsx(X, { size: 14 }) })] })) : null] }), _jsx("div", { className: styles.taskSaveStatusCenter, children: _jsx("div", { className: `${styles.taskSaveStatus} ${autoSaveState === 'error' ? styles.taskSaveStatusError : ''}`, "aria-live": "polite", children: autoSaveState === 'saving' ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { size: 14, className: styles.spinner }), _jsx("span", { children: "Saving\u2026" })] })) : autoSaveState === 'saved' ? (_jsx("span", { children: "Saved" })) : autoSaveState === 'error' ? (_jsx("span", { children: "Save failed" })) : null }) }), _jsx("div", { className: styles.editActionsGroup, children: isDeletedReadOnly && currentTask?.deletedRecordId ? (_jsxs(_Fragment, { children: [handleRestoreDeletedTask && (_jsx("button", { type: "button", className: styles.actionBtn, onClick: () => handleRestoreDeletedTask(currentTask.deletedRecordId || ''), disabled: loading, title: "Restore deleted task", children: _jsx(RotateCcw, { size: 16 }) })), handlePermanentlyDeleteDeletedTask && (_jsx("button", { type: "button", className: `${styles.actionBtn} ${styles.deleteBtn}`, onClick: () => handlePermanentlyDeleteDeletedTask(currentTask.deletedRecordId || ''), disabled: loading, title: "Permanently delete deleted task", children: _jsx(Trash2, { size: 16 }) }))] })) : (_jsx(TaskStatusActions, { task: currentTask, disabled: loading || isArchivedReadOnly, onSetStatus: (task, status) => {
51
+ }, disabled: loading, title: "Cancel workstream attach", "aria-label": "Cancel workstream attach", children: _jsx(X, { size: 14 }) })] })) : null] }), _jsx("div", { className: styles.taskSaveStatusCenter, children: _jsx("div", { className: `${styles.taskSaveStatus} ${autoSaveState === 'error' ? styles.taskSaveStatusError : ''}`, "aria-live": "polite", children: autoSaveState === 'saving' ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { size: 14, className: styles.spinner }), _jsx("span", { children: "Saving\u2026" })] })) : autoSaveState === 'saved' ? (_jsx("span", { children: "Saved" })) : autoSaveState === 'error' ? (_jsx("span", { children: "Save failed" })) : null }) }), _jsx("div", { className: styles.editActionsGroup, children: isDeletedReadOnly && currentTask?.deletedRecordId ? (_jsxs(_Fragment, { children: [handleRestoreDeletedTask && (_jsx("button", { type: "button", className: styles.actionBtn, onClick: () => handleRestoreDeletedTask(currentTask.deletedRecordId || ''), disabled: loading, title: "Restore deleted task", children: _jsx(RotateCcw, { size: 16 }) })), handlePermanentlyDeleteDeletedTask && (_jsx("button", { type: "button", className: `${styles.actionBtn} ${styles.deleteBtn}`, onClick: () => handlePermanentlyDeleteDeletedTask(currentTask.deletedRecordId || ''), disabled: loading, title: "Permanently delete deleted task", children: _jsx(Trash2, { size: 16 }) }))] })) : (_jsx(TaskStatusActions, { task: currentTask, disabled: loading, statusDisabled: loading || isArchivedReadOnly, archiveActionMode: isArchivedReadOnly ? 'unarchive' : 'auto', onSetStatus: (task, status) => {
52
52
  if (handleSetStatus) {
53
53
  handleSetStatus(task, status);
54
54
  return;
@@ -68,5 +68,5 @@ export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle
68
68
  if (status === 'cancelled') {
69
69
  handleToggleCancel(task);
70
70
  }
71
- }, onArchiveTask: handleArchiveTask })) })] })) : (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: resetForm, disabled: loading, className: styles.secondaryHeaderBtn, title: t('actionHeader.clearFormTitle'), children: [_jsx(RotateCcw, { size: 16 }), t('actionHeader.clear')] }), _jsxs("button", { type: "button", onClick: (e) => handleSubmit(e), disabled: loading || !title.trim(), className: styles.primaryUpdateBtn, title: t('actionHeader.addTaskTitle'), children: [loading ? _jsx(Loader2, { size: 16, className: styles.spinner }) : _jsx(Plus, { size: 16 }), t('actionHeader.addTask')] })] })) }));
71
+ }, onArchiveTask: handleArchiveTask, onUnarchiveTask: (task) => handleUnarchiveTask?.(task.id) })) })] })) : (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: resetForm, disabled: loading, className: styles.secondaryHeaderBtn, title: t('actionHeader.clearFormTitle'), children: [_jsx(RotateCcw, { size: 16 }), t('actionHeader.clear')] }), _jsxs("button", { type: "button", onClick: (e) => handleSubmit(e), disabled: loading || !title.trim(), className: styles.primaryUpdateBtn, title: t('actionHeader.addTaskTitle'), children: [loading ? _jsx(Loader2, { size: 16, className: styles.spinner }) : _jsx(Plus, { size: 16 }), t('actionHeader.addTask')] })] })) }));
72
72
  }
@@ -70,4 +70,12 @@ describe('TaskActionHeader', () => {
70
70
  await user.click(screen.getByRole('button', { name: /cancel workstream attach/i }));
71
71
  expect(onWorkstreamInputChange).toHaveBeenCalledWith('');
72
72
  });
73
+ it('shows an unarchive action for archived tasks and wires it to the task id', async () => {
74
+ const user = userEvent.setup();
75
+ const handleUnarchiveTask = vi.fn();
76
+ render(_jsx(TaskActionHeader, { ...baseProps, editingTaskId: "task-1", title: "Polish task modal", currentTask: { ...baseTask, status: 'done', isArchived: true }, handleUnarchiveTask: handleUnarchiveTask }));
77
+ expect(screen.queryByRole('button', { name: /^Archive task$/i })).not.toBeInTheDocument();
78
+ await user.click(screen.getByRole('button', { name: /unarchive task/i }));
79
+ expect(handleUnarchiveTask).toHaveBeenCalledWith('task-1');
80
+ });
73
81
  });
@@ -54,7 +54,22 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
54
54
  const latestActivity = React.useMemo(() => getLatestTaskActivity(task), [task]);
55
55
  const resolvedReadOnlyMode = readOnlyMode || (task.isDeleted ? 'deleted' : (isArchived ? 'archived' : null));
56
56
  const isReadOnly = resolvedReadOnlyMode !== null;
57
- const latestActivityText = React.useMemo(() => summarizeTaskActivity(latestActivity), [latestActivity]);
57
+ const formatActivityFieldValue = React.useCallback((field, value) => {
58
+ const normalizedValue = String(value || '').trim();
59
+ if (!normalizedValue)
60
+ return undefined;
61
+ if (field === 'assignee') {
62
+ return getAssigneeLabel(normalizedValue, assigneeOptions);
63
+ }
64
+ if (field === 'workstreamId' && normalizedValue === taskWorkstream?.id) {
65
+ return workstreamReferenceLabel || taskWorkstream.id;
66
+ }
67
+ if (field === 'initiativeId' && normalizedValue === taskInitiative?.id) {
68
+ return initiativeReferenceLabel || taskInitiative.id;
69
+ }
70
+ return undefined;
71
+ }, [assigneeOptions, initiativeReferenceLabel, taskInitiative, taskWorkstream, workstreamReferenceLabel]);
72
+ const latestActivityText = React.useMemo(() => summarizeTaskActivity(latestActivity, formatActivityFieldValue), [formatActivityFieldValue, latestActivity]);
58
73
  const activityCount = Array.isArray(task.activity) && task.activity.length > 0
59
74
  ? task.activity.length
60
75
  : (task.comments?.length || 0);
@@ -108,6 +108,37 @@ describe('TaskCard parent/child progress', () => {
108
108
  expect(screen.getByText(/Status: task -> review/)).toBeInTheDocument();
109
109
  expect(screen.getByTitle('2 activity items')).toBeInTheDocument();
110
110
  });
111
+ it('uses the current workstream reference in latest activity summaries', () => {
112
+ const task = baseTask({
113
+ id: 'task-relationship-summary',
114
+ title: 'Relationship summary task',
115
+ workstreamId: 'workstream-1',
116
+ activity: [
117
+ {
118
+ id: 'event:event-relationship-1',
119
+ type: 'event',
120
+ timestamp: '2026-03-22T10:00:00.000Z',
121
+ event: {
122
+ id: 'event-relationship-1',
123
+ taskId: 'task-relationship-summary',
124
+ workspaceId: 'default',
125
+ action: 'task-relationship-changed',
126
+ actor: 'user',
127
+ actorType: 'human',
128
+ createdAt: '2026-03-22T10:00:00.000Z',
129
+ details: {
130
+ changes: {
131
+ workstreamId: { from: null, to: 'workstream-1' }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ ]
137
+ });
138
+ render(_jsx(TaskCard, { task: task, taskWorkstream: baseWorkstream({ id: 'workstream-1', referenceNumber: 12 }), onClick: () => { }, onCopyId: () => { } }));
139
+ expect(screen.getByText(/Workstream: none -> WS-12/)).toBeInTheDocument();
140
+ expect(screen.queryByText(/workstream-1/i)).not.toBeInTheDocument();
141
+ });
111
142
  it('renders named AI assignee labels from assignee options', () => {
112
143
  const task = baseTask({
113
144
  id: 'task-ai-assignee',
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import type { TaskItem, CategoryConfig, Comment, AttachmentItem, TaskAssignee, ChecklistItem } from '../../types';
2
+ import type { TaskItem, CategoryConfig, Comment, AttachmentItem, TaskAssignee, ChecklistItem, InitiativeItem } from '../../types';
3
3
  import type { TaxonomyDefinition } from '../../types';
4
4
  import { type TaskAssigneeOption } from '../../utils/assignees';
5
5
  import type { WorkstreamItem } from '../../types';
@@ -58,6 +58,7 @@ interface TaskFormProps {
58
58
  assigneeOptions: TaskAssigneeOption[];
59
59
  taxonomies: TaxonomyDefinition[];
60
60
  workstreams?: WorkstreamItem[];
61
+ initiatives?: InitiativeItem[];
61
62
  copiedId: string | null;
62
63
  onTitleChange: (value: string) => void;
63
64
  onDescriptionChange: (value: string) => void;
@@ -19,7 +19,7 @@ import { summarizeTaskChange, summarizeTaskEvent } from '../../utils/taskActivit
19
19
  import { ensureAssigneeOptionPresent, getAssigneeKind, getAssigneeLabel, } from '../../utils/assignees';
20
20
  import { getTaskReferenceLabel } from '../../utils/taskReferences';
21
21
  import { getVisibleTaskFormTaxonomies } from '../../utils/taxonomyFormDefaults.js';
22
- import { getWorkstreamReferenceLabel } from '../../utils/planningReferences';
22
+ import { getInitiativeReferenceLabel, getWorkstreamReferenceLabel } from '../../utils/planningReferences';
23
23
  function SortableChecklistRow({ id, item, onToggle, onRemove }) {
24
24
  const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id });
25
25
  const style = {
@@ -36,7 +36,7 @@ function hexToRgba(hex, opacity) {
36
36
  return `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}, ${opacity})`;
37
37
  }
38
38
  export function TaskForm(props) {
39
- const { editingTaskId, error, title, description, category, type, priority, complexity, manualComplexityEnabled = false, assignee, scheduledDate, dueDate, workstreamInput = '', checklistItems, comments, newCommentText, contextFiles, currentWorkspaceId, apiBaseUrl, descriptionFocused, showMarkdownHelp, showChecklist = false, checklistEnabled = true, showComments, formTaxonomies, onTaxonomyChange, onOpenSettings, categories, types, priorities, taxonomyDisplayLabels, assigneeOptions, taxonomies, workstreams = [], copiedId, onTitleChange, onDescriptionChange, onCategoryChange, onTypeChange, onPriorityChange, onComplexityChange, onAssigneeChange, onScheduledDateChange, onDueDateChange, onWorkstreamInputChange = () => { }, onChecklistItemsChange, onNewCommentTextChange, onDescriptionFocusedChange, onShowMarkdownHelpChange, onShowChecklistChange = () => { }, onShowCommentsChange, onSubmit, onAddComment, onAddContextFile, onRemoveContextFile, onUpdateContextCaption, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onOpenTaskById, currentTask, commentsEndRef, } = props;
39
+ const { editingTaskId, error, title, description, category, type, priority, complexity, manualComplexityEnabled = false, assignee, scheduledDate, dueDate, workstreamInput = '', checklistItems, comments, newCommentText, contextFiles, currentWorkspaceId, apiBaseUrl, descriptionFocused, showMarkdownHelp, showChecklist = false, checklistEnabled = true, showComments, formTaxonomies, onTaxonomyChange, onOpenSettings, categories, types, priorities, taxonomyDisplayLabels, assigneeOptions, taxonomies, workstreams = [], initiatives = [], copiedId, onTitleChange, onDescriptionChange, onCategoryChange, onTypeChange, onPriorityChange, onComplexityChange, onAssigneeChange, onScheduledDateChange, onDueDateChange, onWorkstreamInputChange = () => { }, onChecklistItemsChange, onNewCommentTextChange, onDescriptionFocusedChange, onShowMarkdownHelpChange, onShowChecklistChange = () => { }, onShowCommentsChange, onSubmit, onAddComment, onAddContextFile, onRemoveContextFile, onUpdateContextCaption, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onOpenTaskById, currentTask, commentsEndRef, } = props;
40
40
  const currentTaskReferenceLabel = getTaskReferenceLabel(currentTask);
41
41
  const visibleTaxonomies = React.useMemo(() => getVisibleTaskFormTaxonomies(taxonomies, formTaxonomies), [taxonomies, formTaxonomies]);
42
42
  const categoryOptions = React.useMemo(() => {
@@ -215,6 +215,14 @@ export function TaskForm(props) {
215
215
  ].filter(Boolean);
216
216
  return new Map(entries.map((actor) => [String(actor.id), actor]));
217
217
  }, [currentTask?.assigneeActor, currentTask?.createdByActor]);
218
+ const workstreamLabelById = React.useMemo(() => new Map(workstreams.map((workstream) => [
219
+ workstream.id,
220
+ getWorkstreamReferenceLabel(workstream) || workstream.id,
221
+ ])), [workstreams]);
222
+ const initiativeLabelById = React.useMemo(() => new Map(initiatives.map((initiative) => [
223
+ initiative.id,
224
+ getInitiativeReferenceLabel(initiative) || initiative.id,
225
+ ])), [initiatives]);
218
226
  const taskActivityItems = React.useMemo(() => {
219
227
  if (Array.isArray(currentTask?.activity) && currentTask.activity.length > 0) {
220
228
  return currentTask.activity;
@@ -227,9 +235,19 @@ export function TaskForm(props) {
227
235
  }));
228
236
  }, [comments, currentTask?.activity]);
229
237
  const formatActivityFieldValue = (field, value) => {
230
- if (field !== 'assignee')
238
+ const normalizedValue = String(value || '').trim();
239
+ if (!normalizedValue)
231
240
  return undefined;
232
- return getAssigneeLabel(String(value || '').trim(), resolvedAssigneeOptions);
241
+ if (field === 'assignee') {
242
+ return getAssigneeLabel(normalizedValue, resolvedAssigneeOptions);
243
+ }
244
+ if (field === 'workstreamId') {
245
+ return workstreamLabelById.get(normalizedValue);
246
+ }
247
+ if (field === 'initiativeId') {
248
+ return initiativeLabelById.get(normalizedValue);
249
+ }
250
+ return undefined;
233
251
  };
234
252
  const resolveActorLabel = React.useCallback((actorId, actorType, actorLabel) => {
235
253
  if (actorLabel && actorLabel.trim())
@@ -59,6 +59,16 @@ describe('TaskForm Component', () => {
59
59
  createdAt: '2026-04-03T00:00:00.000Z',
60
60
  },
61
61
  ],
62
+ initiatives: [
63
+ {
64
+ id: 'initiative-1',
65
+ referenceNumber: 3,
66
+ referenceLabel: 'IN-3',
67
+ title: 'Execution model',
68
+ description: null,
69
+ createdAt: '2026-04-03T00:00:00.000Z',
70
+ },
71
+ ],
62
72
  copiedId: null,
63
73
  // Callbacks
64
74
  onTitleChange: vi.fn(),
@@ -400,6 +410,42 @@ describe('TaskForm Component', () => {
400
410
  expect(screen.getByText('ASSIGNED TO: Unassigned -> Planner Bot')).toBeInTheDocument();
401
411
  expect(screen.queryByText(/ai-profile-1/i)).not.toBeInTheDocument();
402
412
  });
413
+ it('renders planning relationship audit activity with reference labels instead of raw ids', () => {
414
+ renderForm({
415
+ editingTaskId: '123',
416
+ showComments: true,
417
+ currentTask: {
418
+ id: '123',
419
+ title: 'Existing Task',
420
+ activity: [
421
+ {
422
+ id: 'event:task-event-relationship',
423
+ type: 'event',
424
+ timestamp: '2026-03-23T12:00:00.000Z',
425
+ event: {
426
+ id: 'task-event-relationship',
427
+ taskId: '123',
428
+ workspaceId: 'workspace-1',
429
+ action: 'task-relationship-changed',
430
+ actor: 'member-1',
431
+ actorType: 'human',
432
+ createdAt: '2026-03-23T12:00:00.000Z',
433
+ details: {
434
+ changes: {
435
+ workstreamId: {
436
+ from: null,
437
+ to: 'workstream-1',
438
+ },
439
+ },
440
+ },
441
+ },
442
+ },
443
+ ],
444
+ },
445
+ });
446
+ expect(screen.getByText('Workstream: none -> WS-12')).toBeInTheDocument();
447
+ expect(screen.queryByText(/workstream-1/i)).not.toBeInTheDocument();
448
+ });
403
449
  it('3.13 Update title', async () => {
404
450
  const user = userEvent.setup();
405
451
  renderForm({ editingTaskId: '123', title: 'Old Title' });