@taskforcehq/taskforce 0.3.303 → 0.3.305

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +102 -5
  3. package/dist/TaskforceCore.js +635 -238
  4. package/dist/TaskforceCore.test.js +2243 -495
  5. package/dist/annotatedAttachments/service.d.ts +1 -1
  6. package/dist/annotatedAttachments/types.d.ts +1 -1
  7. package/dist/compat/workspaceSyncCompat.js +6 -0
  8. package/dist/components/features/AgentsModule.js +1 -1
  9. package/dist/components/features/AgentsModule.test.js +1 -1
  10. package/dist/components/features/DocumentIndex.d.ts +1 -1
  11. package/dist/components/features/DocumentIndex.js +1 -1
  12. package/dist/components/features/DocumentViewer.d.ts +1 -1
  13. package/dist/components/features/DocumentViewer.js +32 -9
  14. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  15. package/dist/components/features/DocumentWorkspace.js +12 -155
  16. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  17. package/dist/components/features/TaskSettings.js +87 -41
  18. package/dist/components/features/TaskSettings.test.js +180 -40
  19. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  20. package/dist/components/features/documentWorkspaceModel.js +57 -0
  21. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  22. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  23. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  24. package/dist/components/task/TaskActionHeader.js +3 -3
  25. package/dist/components/task/TaskActionHeader.test.js +8 -0
  26. package/dist/components/task/TaskCard.js +16 -1
  27. package/dist/components/task/TaskCard.test.js +31 -0
  28. package/dist/components/task/TaskForm.d.ts +9 -8
  29. package/dist/components/task/TaskForm.js +374 -219
  30. package/dist/components/task/TaskForm.test.js +482 -361
  31. package/dist/components/task/TaskKanban.d.ts +1 -1
  32. package/dist/components/task/TaskKanban.js +33 -161
  33. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  34. package/dist/components/task/TaskStatusActions.js +8 -3
  35. package/dist/components/task/TaskStatusActions.test.js +9 -0
  36. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  37. package/dist/components/task/taskKanbanDropRules.js +187 -0
  38. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  39. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  40. package/dist/components/ui/Modal.d.ts +1 -1
  41. package/dist/components/ui/Modal.js +1 -0
  42. package/dist/components/views/AppShellHeader.js +5 -1
  43. package/dist/components/views/AppShellHeader.test.js +4 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  45. package/dist/components/views/PlanComparisonPage.js +9 -10
  46. package/dist/components/views/PlanComparisonPage.test.js +55 -41
  47. package/dist/components/views/PlansPage.d.ts +5 -1
  48. package/dist/components/views/PlansPage.js +150 -81
  49. package/dist/components/views/PlansPage.test.js +367 -41
  50. package/dist/components/views/StandaloneLayout.js +135 -54
  51. package/dist/components/views/WidgetView.js +11 -2
  52. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  53. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  54. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  55. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  56. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  57. package/dist/config/envSchema.js +1 -1
  58. package/dist/core/AiProfileService.d.ts +6 -1
  59. package/dist/core/AiProfileService.js +161 -16
  60. package/dist/core/AiProfileService.test.js +3 -1
  61. package/dist/core/AiProfiles.test.js +200 -0
  62. package/dist/core/AuthTokenService.test.d.ts +1 -0
  63. package/dist/core/AuthTokenService.test.js +78 -0
  64. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  65. package/dist/core/EntitlementsPolicy.test.js +75 -13
  66. package/dist/core/PlanEntitlementService.d.ts +9 -1
  67. package/dist/core/PlanEntitlementService.js +144 -19
  68. package/dist/core/PlanVersionPolicy.test.js +19 -26
  69. package/dist/core/SignupMonetizationSettings.test.js +75 -15
  70. package/dist/core/SystemAdmin.test.js +212 -56
  71. package/dist/core/TaskActivityService.d.ts +4 -1
  72. package/dist/core/TaskActivityService.js +45 -0
  73. package/dist/core/TaskAuditTimeline.test.js +52 -1
  74. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  75. package/dist/core/Taskforce.d.ts +18 -11
  76. package/dist/core/Taskforce.js +93 -12
  77. package/dist/core/WorkspacePlanMode.test.js +73 -8
  78. package/dist/core/shared.d.ts +2 -0
  79. package/dist/core/shared.js +11 -0
  80. package/dist/documentReviews/service.d.ts +1 -1
  81. package/dist/documentReviews/types.d.ts +1 -1
  82. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  83. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  84. package/dist/hooks/sync/auth.d.ts +37 -0
  85. package/dist/hooks/sync/auth.js +63 -0
  86. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  87. package/dist/hooks/sync/bootstrap.js +52 -0
  88. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  89. package/dist/hooks/sync/orchestratorShared.js +209 -0
  90. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  91. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  92. package/dist/hooks/sync/realtime.d.ts +25 -0
  93. package/dist/hooks/sync/realtime.js +60 -0
  94. package/dist/hooks/sync/recovery.d.ts +69 -0
  95. package/dist/hooks/sync/recovery.js +118 -0
  96. package/dist/hooks/sync/transfers.d.ts +123 -0
  97. package/dist/hooks/sync/transfers.js +435 -0
  98. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  99. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  100. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  101. package/dist/hooks/ui/useResourceExport.js +5 -2
  102. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  103. package/dist/hooks/useRealtimeSync.js +2 -1
  104. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  105. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  106. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  107. package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
  108. package/dist/hooks/useSyncOrchestrator.js +388 -879
  109. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
  110. package/dist/hooks/useTaskMutations.d.ts +2 -1
  111. package/dist/hooks/useTaskMutations.js +57 -7
  112. package/dist/hooks/useTaskMutations.test.js +176 -0
  113. package/dist/hooks/useTaskforce.d.ts +13 -4
  114. package/dist/hooks/useTaskforce.js +302 -817
  115. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  116. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  117. package/dist/hooks/useWorkspaceSyncController.js +33 -11
  118. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  119. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  120. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  121. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  122. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  123. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  124. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  125. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  126. package/dist/localization/locales/en-US.js +2 -2
  127. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  128. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  129. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  130. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  131. package/dist/mcp/clientIntegrations.d.ts +12 -1
  132. package/dist/mcp/clientIntegrations.js +161 -6
  133. package/dist/mcp/clientIntegrations.test.js +76 -4
  134. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  135. package/dist/mcp/collaborationRegistrar.js +71 -0
  136. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  137. package/dist/mcp/documentAssetRegistrar.js +346 -0
  138. package/dist/mcp/documentHelpers.d.ts +100 -0
  139. package/dist/mcp/documentHelpers.js +161 -0
  140. package/dist/mcp/runtime.js +2455 -3925
  141. package/dist/mcp/runtime.test.js +240 -8
  142. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  143. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  144. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  145. package/dist/mcp/taskAttachmentTypes.js +1 -0
  146. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  147. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  148. package/dist/mcp/toolCatalog.d.ts +35 -0
  149. package/dist/mcp/toolCatalog.js +3 -0
  150. package/dist/plugins/vite.js +1 -1
  151. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  152. package/dist/runtime/appGateDefinitions.js +1 -1
  153. package/dist/server/index.d.ts +1 -0
  154. package/dist/server/index.js +46 -3
  155. package/dist/server/index.test.js +12 -1
  156. package/dist/server/routes/admin.d.ts +1 -5
  157. package/dist/server/routes/admin.js +151 -80
  158. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  159. package/dist/server/routes/annotatedAttachments.js +1 -1
  160. package/dist/server/routes/auth.d.ts +1 -4
  161. package/dist/server/routes/auth.js +59 -64
  162. package/dist/server/routes/authSupport.d.ts +30 -0
  163. package/dist/server/routes/authSupport.js +81 -0
  164. package/dist/server/routes/billing.d.ts +1 -1
  165. package/dist/server/routes/billing.js +122 -255
  166. package/dist/server/routes/billing.test.js +257 -86
  167. package/dist/server/routes/documentReviews.d.ts +1 -1
  168. package/dist/server/routes/documentReviews.js +1 -1
  169. package/dist/server/routes/documents.d.ts +4 -2
  170. package/dist/server/routes/documents.js +73 -7
  171. package/dist/server/routes/primitives.d.ts +11 -0
  172. package/dist/server/routes/primitives.js +73 -0
  173. package/dist/server/routes/resources.d.ts +1 -1
  174. package/dist/server/routes/resources.js +1 -1
  175. package/dist/server/routes/shared.d.ts +31 -1
  176. package/dist/server/routes/shared.js +26 -0
  177. package/dist/server/routes/sync.d.ts +1 -1
  178. package/dist/server/routes/sync.integration.test.js +20 -0
  179. package/dist/server/routes/sync.js +111 -238
  180. package/dist/server/routes/tasks.d.ts +1 -1
  181. package/dist/server/routes/tasks.js +50 -8
  182. package/dist/server/routes/workspaces.d.ts +1 -1
  183. package/dist/server/routes/workspaces.js +1 -1
  184. package/dist/server/routes.d.ts +2 -9
  185. package/dist/server/routes.js +84 -181
  186. package/dist/server/routes.test.js +912 -32
  187. package/dist/shared/taskActor.d.ts +10 -0
  188. package/dist/shared/taskActor.js +1 -0
  189. package/dist/sync/collaborationSyncPayload.js +4 -0
  190. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  191. package/dist/sync/collaborationSyncState.d.ts +3 -1
  192. package/dist/sync/collaborationSyncState.js +19 -0
  193. package/dist/sync/contentSyncState.js +28 -17
  194. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  195. package/dist/sync/syncApplyHandlers.js +370 -8
  196. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  197. package/dist/sync/syncApplyHandlers.test.js +379 -0
  198. package/dist/sync/syncService.d.ts +9 -1
  199. package/dist/sync/syncService.js +44 -2
  200. package/dist/sync/syncService.test.js +62 -0
  201. package/dist/sync/workspacePullFeed.d.ts +1 -0
  202. package/dist/sync/workspacePullFeed.js +66 -59
  203. package/dist/sync/workspacePullFeed.test.js +72 -4
  204. package/dist/sync/workspaceSyncModel.d.ts +25 -1
  205. package/dist/sync/workspaceSyncModel.js +77 -3
  206. package/dist/sync/workspaceSyncModel.test.js +153 -0
  207. package/dist/sync/workspaceSyncState.d.ts +2 -0
  208. package/dist/sync/workspaceSyncState.js +1 -0
  209. package/dist/types.d.ts +2 -10
  210. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
  211. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  212. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  213. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  214. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  215. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  216. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  217. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  218. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
  219. package/dist/ui/assets/index-C2-OXZV7.css +1 -0
  220. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  221. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
  222. package/dist/ui/index.html +4 -4
  223. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  224. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  225. package/dist/utils/taskActivity.js +4 -0
  226. package/dist/utils/taskActivity.test.js +25 -1
  227. package/package.json +10 -1
  228. package/scripts/check-cloud-mcp.mjs +83 -0
  229. package/scripts/playwright-auth.sh +9 -1
  230. package/scripts/run-billing-performance-smoke.sh +24 -0
  231. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  232. package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
  233. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  234. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  235. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  236. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  237. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -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,8 @@ 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'],
974
+ endpoint: 'https://performance-mcp.taskforcehq.ai/mcp'
954
975
  }
955
976
  })
956
977
  };
@@ -969,7 +990,7 @@ describe('TaskSettings Component', () => {
969
990
  fetchCloudAuthApi
970
991
  }
971
992
  });
972
- await user.click(screen.getByRole('button', { name: /MCP/i }));
993
+ await openCloudMcpSettings(user);
973
994
  await user.click(screen.getByRole('button', { name: 'Test Connection' }));
974
995
  await user.type(await screen.findByPlaceholderText('tfmcp_xxxxx_replace-me'), 'tfmcp_example_secret');
975
996
  await user.click(screen.getAllByRole('button', { name: 'Test Connection' })[1]);
@@ -978,7 +999,47 @@ describe('TaskSettings Component', () => {
978
999
  credentials: 'include'
979
1000
  }));
980
1001
  expect(await screen.findByText('Connection looks good for this workspace.')).toBeInTheDocument();
981
- expect(screen.getByText(/Granted scopes: tasks:read, tasks:write/i)).toBeInTheDocument();
1002
+ expect(screen.getByText(/Granted scopes: tasks:read, tasks:write, workspace:read/i)).toBeInTheDocument();
1003
+ expect(screen.getByText(/Endpoint: https:\/\/performance-mcp\.taskforcehq\.ai\/mcp/i)).toBeInTheDocument();
1004
+ }, 10000);
1005
+ it('4.23d2 MCP test connection shows a dedicated-host incident message when the endpoint returns a gateway failure', async () => {
1006
+ const fetchCloudAuthApi = vi.fn(async (path) => {
1007
+ if (path === '/api/taskforce/settings/mcp/test-connection') {
1008
+ return {
1009
+ ok: false,
1010
+ json: async () => ({
1011
+ code: 'MCP_ENDPOINT_GATEWAY_ERROR',
1012
+ error: 'Cloud MCP endpoint https://performance-mcp.taskforcehq.ai/mcp returned 502.',
1013
+ details: {
1014
+ endpoint: 'https://performance-mcp.taskforcehq.ai/mcp',
1015
+ upstreamStatus: 502
1016
+ }
1017
+ })
1018
+ };
1019
+ }
1020
+ return {
1021
+ ok: true,
1022
+ json: async () => ({ items: [] })
1023
+ };
1024
+ });
1025
+ const user = userEvent.setup();
1026
+ renderComponent({
1027
+ settingsModel: {
1028
+ runtimeMode: 'local',
1029
+ isAuthenticated: true,
1030
+ currentWorkspaceName: 'Workspace Alpha',
1031
+ cloudMcpBaseUrl: 'https://performance-mcp.taskforcehq.ai',
1032
+ fetchCloudAuthApi
1033
+ }
1034
+ });
1035
+ await openCloudMcpSettings(user);
1036
+ await user.click(screen.getByRole('button', { name: 'Test Connection' }));
1037
+ await user.type(await screen.findByPlaceholderText('tfmcp_xxxxx_replace-me'), 'tfmcp_example_secret');
1038
+ await user.click(screen.getAllByRole('button', { name: 'Test Connection' })[1]);
1039
+ expect(await screen.findByText('The dedicated MCP host is responding with a gateway error.')).toBeInTheDocument();
1040
+ expect(screen.getByText(/Endpoint: https:\/\/performance-mcp\.taskforcehq\.ai\/mcp/i)).toBeInTheDocument();
1041
+ expect(screen.getByText(/Error code: MCP_ENDPOINT_GATEWAY_ERROR/i)).toBeInTheDocument();
1042
+ expect(screen.getByText(/This usually means the cloud MCP service is degraded/i)).toBeInTheDocument();
982
1043
  }, 10000);
983
1044
  it('4.23e Antigravity uses serverUrl instead of transport in the cloud snippet', async () => {
984
1045
  const fetchCloudAuthApi = vi.fn(async () => ({
@@ -995,7 +1056,7 @@ describe('TaskSettings Component', () => {
995
1056
  fetchCloudAuthApi
996
1057
  }
997
1058
  });
998
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1059
+ await openCloudMcpSettings(user);
999
1060
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'antigravity');
1000
1061
  const snippetMatches = await screen.findAllByText(/serverUrl/);
1001
1062
  expect(snippetMatches.length).toBeGreaterThan(0);
@@ -1018,7 +1079,7 @@ describe('TaskSettings Component', () => {
1018
1079
  fetchCloudAuthApi
1019
1080
  }
1020
1081
  });
1021
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1082
+ await openCloudMcpSettings(user);
1022
1083
  expect(await screen.findByText(/https:\/\/performance-mcp\.taskforcehq\.ai\/mcp/)).toBeInTheDocument();
1023
1084
  expect(screen.queryByText(/https:\/\/performance-app\.taskforcehq\.ai\/mcp/)).not.toBeInTheDocument();
1024
1085
  }, 10000);
@@ -1038,7 +1099,7 @@ describe('TaskSettings Component', () => {
1038
1099
  fetchCloudAuthApi
1039
1100
  }
1040
1101
  });
1041
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1102
+ await openCloudMcpSettings(user);
1042
1103
  expect(await screen.findByText(/https:\/\/performance-mcp\.taskforcehq\.ai\/mcp/)).toBeInTheDocument();
1043
1104
  expect(screen.queryByText(/https:\/\/performance-app\.taskforcehq\.ai\/mcp/)).not.toBeInTheDocument();
1044
1105
  }, 10000);
@@ -1057,7 +1118,7 @@ describe('TaskSettings Component', () => {
1057
1118
  fetchCloudAuthApi
1058
1119
  }
1059
1120
  });
1060
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1121
+ await openCloudMcpSettings(user);
1061
1122
  const urlMatches = await screen.findAllByText(/"url"/);
1062
1123
  expect(urlMatches.length).toBeGreaterThan(0);
1063
1124
  expect(screen.getAllByText(/https:\/\/mcp\.taskforcehq\.ai\/mcp/).length).toBeGreaterThan(0);
@@ -1079,7 +1140,7 @@ describe('TaskSettings Component', () => {
1079
1140
  fetchCloudAuthApi
1080
1141
  }
1081
1142
  });
1082
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1143
+ await openCloudMcpSettings(user);
1083
1144
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'cline');
1084
1145
  expect(await screen.findByText(/Cline Configuration Instructions/i)).toBeInTheDocument();
1085
1146
  expect(screen.getByText(/"type"/)).toBeInTheDocument();
@@ -1101,7 +1162,7 @@ describe('TaskSettings Component', () => {
1101
1162
  fetchCloudAuthApi
1102
1163
  }
1103
1164
  });
1104
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1165
+ await openCloudMcpSettings(user);
1105
1166
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'vscode');
1106
1167
  expect(await screen.findByText(/VS Code Configuration Instructions/i)).toBeInTheDocument();
1107
1168
  expect(screen.getByText(/"servers"/)).toBeInTheDocument();
@@ -1123,7 +1184,7 @@ describe('TaskSettings Component', () => {
1123
1184
  fetchCloudAuthApi
1124
1185
  }
1125
1186
  });
1126
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1187
+ await openCloudMcpSettings(user);
1127
1188
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
1128
1189
  expect((await screen.findAllByText(/\.codex\/config\.toml/i)).length).toBeGreaterThan(0);
1129
1190
  expect(screen.getByText(/\[mcp_servers\./)).toBeInTheDocument();
@@ -1145,7 +1206,7 @@ describe('TaskSettings Component', () => {
1145
1206
  fetchCloudAuthApi
1146
1207
  }
1147
1208
  });
1148
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1209
+ await openCloudMcpSettings(user);
1149
1210
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'claude-code');
1150
1211
  expect(await screen.findByText(/Claude Code Configuration Instructions/i)).toBeInTheDocument();
1151
1212
  expect(screen.getByText(/claude mcp add --transport http/i)).toBeInTheDocument();
@@ -1167,7 +1228,7 @@ describe('TaskSettings Component', () => {
1167
1228
  fetchCloudAuthApi
1168
1229
  }
1169
1230
  });
1170
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1231
+ await openCloudMcpSettings(user);
1171
1232
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'gemini-cli');
1172
1233
  expect(await screen.findByText(/Gemini CLI Configuration Instructions/i)).toBeInTheDocument();
1173
1234
  expect(screen.getByText(/gemini mcp add taskforce-/i)).toBeInTheDocument();
@@ -1190,7 +1251,7 @@ describe('TaskSettings Component', () => {
1190
1251
  fetchCloudAuthApi
1191
1252
  }
1192
1253
  });
1193
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1254
+ await openCloudMcpSettings(user);
1194
1255
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'openclaw');
1195
1256
  expect(await screen.findByText(/OpenClaw Configuration Instructions/i)).toBeInTheDocument();
1196
1257
  expect((await screen.findAllByText(/~\/\.openclaw\/openclaw\.json/i)).length).toBeGreaterThan(0);
@@ -1217,10 +1278,10 @@ describe('TaskSettings Component', () => {
1217
1278
  fetchCloudAuthApi
1218
1279
  }
1219
1280
  });
1220
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1281
+ await openCloudMcpSettings(user);
1221
1282
  expect(screen.queryByRole('option', { name: /Claude Desktop/i })).not.toBeInTheDocument();
1222
1283
  expect(screen.queryByRole('option', { name: /ChatGPT Desktop/i })).not.toBeInTheDocument();
1223
- expect(await screen.findByText('Cursor Configuration Instructions')).toBeInTheDocument();
1284
+ expect(await screen.findByText('General MCP Configuration Instructions')).toBeInTheDocument();
1224
1285
  }, 10000);
1225
1286
  it('4.23e4b Integrations tab shows ChatGPT and Claude connector management separately from PAT tokens', async () => {
1226
1287
  const fetchCloudAuthApi = vi.fn(async (path) => {
@@ -1294,8 +1355,7 @@ describe('TaskSettings Component', () => {
1294
1355
  fetchCloudAuthApi
1295
1356
  }
1296
1357
  });
1297
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1298
- expect(await screen.findByText('Cloud MCP Configuration')).toBeInTheDocument();
1358
+ await openCloudMcpSettings(user);
1299
1359
  expect(screen.queryByText('ChatGPT Connector')).not.toBeInTheDocument();
1300
1360
  expect(screen.queryByText(/^Connectors$/)).not.toBeInTheDocument();
1301
1361
  }, 10000);
@@ -1309,8 +1369,7 @@ describe('TaskSettings Component', () => {
1309
1369
  cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
1310
1370
  }
1311
1371
  });
1312
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1313
- expect(await screen.findByText('Cloud MCP Configuration')).toBeInTheDocument();
1372
+ await openCloudMcpSettings(user);
1314
1373
  expect(screen.getAllByRole('button', { name: /copy/i }).length).toBeGreaterThan(0);
1315
1374
  }, 10000);
1316
1375
  it('4.23g1 Cloud MCP copy warns before a token is generated', async () => {
@@ -1323,8 +1382,7 @@ describe('TaskSettings Component', () => {
1323
1382
  cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
1324
1383
  }
1325
1384
  });
1326
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1327
- await screen.findByText('Cloud MCP Configuration');
1385
+ await openCloudMcpSettings(user);
1328
1386
  await user.click(screen.getByTitle('Copy full configuration file'));
1329
1387
  expect(screen.getByText('Generate a token before copying the Cloud MCP configuration.')).toBeInTheDocument();
1330
1388
  }, 10000);
@@ -1338,8 +1396,7 @@ describe('TaskSettings Component', () => {
1338
1396
  cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
1339
1397
  }
1340
1398
  });
1341
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1342
- await screen.findByText('Cloud MCP Configuration');
1399
+ await openCloudMcpSettings(user);
1343
1400
  expect(screen.queryByText('Automation Identity (Optional)')).not.toBeInTheDocument();
1344
1401
  expect(screen.queryByRole('combobox', { name: 'Use existing AI profile' })).not.toBeInTheDocument();
1345
1402
  }, 10000);
@@ -1353,8 +1410,7 @@ describe('TaskSettings Component', () => {
1353
1410
  cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
1354
1411
  }
1355
1412
  });
1356
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1357
- await screen.findByText('Cloud MCP Configuration');
1413
+ await openCloudMcpSettings(user);
1358
1414
  await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
1359
1415
  expect(screen.queryByText(/X-Taskforce-AI-Name/i)).not.toBeInTheDocument();
1360
1416
  expect(screen.queryByText(/X-Taskforce-AI-Profile-Token/i)).not.toBeInTheDocument();
@@ -1394,7 +1450,7 @@ describe('TaskSettings Component', () => {
1394
1450
  fetchCloudAuthApi
1395
1451
  }
1396
1452
  });
1397
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1453
+ await openCloudMcpSettings(user);
1398
1454
  expect(await screen.findByText('Current Token')).toBeInTheDocument();
1399
1455
  expect(screen.getByText('Old Token')).toBeInTheDocument();
1400
1456
  const tokenLabels = screen.getAllByText(/^(Current Token|Old Token)$/);
@@ -1414,8 +1470,7 @@ describe('TaskSettings Component', () => {
1414
1470
  cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
1415
1471
  }
1416
1472
  });
1417
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1418
- await screen.findByText('Cloud MCP Configuration');
1473
+ await openCloudMcpSettings(user);
1419
1474
  const codeNodes = Array.from(document.querySelectorAll('code'));
1420
1475
  const snippetNode = codeNodes.find((node) => node.textContent?.includes('"mcpServers"'));
1421
1476
  expect(snippetNode).toBeTruthy();
@@ -1435,11 +1490,96 @@ describe('TaskSettings Component', () => {
1435
1490
  });
1436
1491
  await user.click(screen.getByRole('button', { name: /MCP/i }));
1437
1492
  expect(screen.getByText('Local MCP')).toBeInTheDocument();
1438
- expect(screen.getByText(/Claude Code CLI, Gemini CLI/i)).toBeInTheDocument();
1493
+ expect(screen.getByRole('combobox', { name: 'MCP client' })).toHaveValue('general');
1494
+ expect(screen.getByText('General MCP Configuration Instructions')).toBeInTheDocument();
1439
1495
  expect(screen.queryByText('MCP Path Mapping (Host Root)')).not.toBeInTheDocument();
1440
1496
  await user.click(screen.getByRole('button', { name: 'Advanced' }));
1441
1497
  expect(screen.getByText('MCP Path Mapping (Host Root)')).toBeInTheDocument();
1442
1498
  expect(screen.queryByText('Cloud MCP Configuration')).not.toBeInTheDocument();
1443
1499
  expect(screen.queryByRole('button', { name: 'Cloud MCP' })).not.toBeInTheDocument();
1444
1500
  });
1501
+ it('4.24a Local MCP selector offers General first and keeps the rest alphabetical', async () => {
1502
+ const user = userEvent.setup();
1503
+ renderComponent({
1504
+ settingsModel: {
1505
+ runtimeMode: 'local',
1506
+ isAuthenticated: false
1507
+ }
1508
+ });
1509
+ await user.click(screen.getByRole('button', { name: /MCP/i }));
1510
+ const options = screen.getAllByRole('option').map((option) => option.textContent);
1511
+ expect(options).toEqual([
1512
+ 'General',
1513
+ 'Antigravity',
1514
+ 'Claude Code CLI',
1515
+ 'Cline',
1516
+ 'Codex',
1517
+ 'Cursor',
1518
+ 'Gemini CLI',
1519
+ 'OpenClaw',
1520
+ 'VS Code / Copilot',
1521
+ 'Windsurf'
1522
+ ]);
1523
+ });
1524
+ it('4.24b Local Codex shows config.toml MCP output without token headers', 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' }), 'codex');
1539
+ expect(await screen.findByText(/Codex Configuration Instructions/i)).toBeInTheDocument();
1540
+ expect((await screen.findAllByText(/\.codex\/config\.toml/i)).length).toBeGreaterThan(0);
1541
+ expect(screen.getByText(/\[mcp_servers\./)).toBeInTheDocument();
1542
+ expect(screen.getByText(/command = "taskforce"/i)).toBeInTheDocument();
1543
+ expect(screen.queryByText(/Authorization/i)).not.toBeInTheDocument();
1544
+ });
1545
+ it('4.24c Local VS Code uses stdio config under servers', async () => {
1546
+ const user = userEvent.setup();
1547
+ renderComponent({
1548
+ settingsModel: {
1549
+ runtimeMode: 'local',
1550
+ isAuthenticated: false,
1551
+ projectRoot: '/Users/rossburke/projects/taskforce',
1552
+ projectName: 'taskforce',
1553
+ tenantId: 'tenant-alpha',
1554
+ workspaceId: 'workspace-alpha',
1555
+ mcpScriptPath: '/usr/local/bin/taskforce'
1556
+ }
1557
+ });
1558
+ await user.click(screen.getByRole('button', { name: /MCP/i }));
1559
+ await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'vscode');
1560
+ expect(await screen.findByText(/VS Code Configuration Instructions/i)).toBeInTheDocument();
1561
+ expect(screen.getByText(/"servers"/)).toBeInTheDocument();
1562
+ expect(screen.getByText(/"type"/)).toBeInTheDocument();
1563
+ expect(screen.getByText(/"stdio"/)).toBeInTheDocument();
1564
+ });
1565
+ it('4.24d Local Claude Code shows a stdio add command', async () => {
1566
+ const user = userEvent.setup();
1567
+ renderComponent({
1568
+ settingsModel: {
1569
+ runtimeMode: 'local',
1570
+ isAuthenticated: false,
1571
+ projectRoot: '/Users/rossburke/projects/taskforce',
1572
+ projectName: 'taskforce',
1573
+ tenantId: 'tenant-alpha',
1574
+ workspaceId: 'workspace-alpha',
1575
+ mcpScriptPath: '/usr/local/bin/taskforce'
1576
+ }
1577
+ });
1578
+ await user.click(screen.getByRole('button', { name: /MCP/i }));
1579
+ await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'claude-code');
1580
+ expect(await screen.findByText(/Claude Code Configuration Instructions/i)).toBeInTheDocument();
1581
+ expect(screen.getByText(/claude mcp add taskforce-/i)).toBeInTheDocument();
1582
+ expect(screen.getByText(/--env TASKFORCE_TENANT_ID=tenant-alpha/i)).toBeInTheDocument();
1583
+ expect(screen.getByText(/-- taskforce mcp/i)).toBeInTheDocument();
1584
+ });
1445
1585
  });
@@ -0,0 +1,24 @@
1
+ import type { DocumentAttachmentFilter, DocumentTypeFilter } from './documentWorkspaceFilters';
2
+ export interface DocMeta {
3
+ id: string;
4
+ assetId?: string;
5
+ referenceNumber?: number | null;
6
+ referenceLabel?: string | null;
7
+ fsPath: string;
8
+ apiUrl: string;
9
+ taskId: string | null;
10
+ displayName: string;
11
+ originalFilename?: string | null;
12
+ title: string;
13
+ docType: 'implementation-plan' | 'review' | 'walkthrough' | 'planning' | 'other';
14
+ sizeBytes: number;
15
+ updatedAt: string | null;
16
+ editable?: boolean;
17
+ attachmentCount?: number;
18
+ attachmentState?: 'attached' | 'unattached' | 'standalone';
19
+ }
20
+ export declare function normalizeDocPath(value: string): string;
21
+ export declare function docPathMatches(candidate: string, requested: string): boolean;
22
+ export declare function extractAssetIdFromPath(value: string | null | undefined): string | null;
23
+ export declare function getEffectiveAttachmentState(doc: Pick<DocMeta, 'attachmentState' | 'attachmentCount' | 'taskId'>): 'attached' | 'unattached';
24
+ export declare function filterDocuments(documents: DocMeta[], searchQuery: string, typeFilters: DocumentTypeFilter[], attachmentFilters: DocumentAttachmentFilter[]): DocMeta[];
@@ -0,0 +1,57 @@
1
+ import { getReadableDocumentName } from '../../utils/documentNames';
2
+ export function normalizeDocPath(value) {
3
+ let decoded = value;
4
+ try {
5
+ decoded = decodeURIComponent(value);
6
+ }
7
+ catch {
8
+ decoded = value;
9
+ }
10
+ const normalized = decoded
11
+ .trim()
12
+ .replace(/\\/g, '/')
13
+ .replace(/^\/+/, '')
14
+ .replace(/^\.\//, '')
15
+ .replace(/^\.taskforce\//, '');
16
+ return normalized;
17
+ }
18
+ export function docPathMatches(candidate, requested) {
19
+ if (candidate === requested)
20
+ return true;
21
+ if (candidate.endsWith(`/${requested}`))
22
+ return true;
23
+ if (requested.endsWith(`/${candidate}`))
24
+ return true;
25
+ return false;
26
+ }
27
+ export function extractAssetIdFromPath(value) {
28
+ const normalized = normalizeDocPath(String(value || ''));
29
+ if (!normalized)
30
+ return null;
31
+ const match = normalized.match(/(^|\/)(asset-[a-f0-9]{32})(?=[^/]*$)/i);
32
+ return match?.[2] ? match[2].toLowerCase() : null;
33
+ }
34
+ export function getEffectiveAttachmentState(doc) {
35
+ if (doc.attachmentState === 'attached' || doc.attachmentState === 'unattached') {
36
+ return doc.attachmentState;
37
+ }
38
+ const attachmentCount = typeof doc.attachmentCount === 'number' && Number.isFinite(doc.attachmentCount)
39
+ ? doc.attachmentCount
40
+ : 0;
41
+ if (attachmentCount > 0 || !!doc.taskId) {
42
+ return 'attached';
43
+ }
44
+ return 'unattached';
45
+ }
46
+ export function filterDocuments(documents, searchQuery, typeFilters, attachmentFilters) {
47
+ const normalizedQuery = searchQuery.trim().toLowerCase();
48
+ return documents.filter((doc) => {
49
+ const readableName = getReadableDocumentName(doc);
50
+ const haystack = `${readableName} ${doc.originalFilename || ''}`.toLowerCase();
51
+ const matchesSearch = normalizedQuery.length === 0 || haystack.includes(normalizedQuery);
52
+ const matchesType = typeFilters.length === 0 || typeFilters.includes(doc.docType);
53
+ const attachmentState = getEffectiveAttachmentState(doc);
54
+ const matchesAttachment = attachmentFilters.length === 0 || attachmentFilters.includes(attachmentState);
55
+ return matchesSearch && matchesType && matchesAttachment;
56
+ });
57
+ }
@@ -0,0 +1,34 @@
1
+ import { type DocumentAttachmentFilter, type DocumentTypeFilter } from './documentWorkspaceFilters';
2
+ import { type DocMeta } from './documentWorkspaceModel';
3
+ interface Options {
4
+ runtimeMode?: 'local' | 'cloud';
5
+ apiBaseUrl?: string;
6
+ cloudAuthBaseUrl?: string;
7
+ typeFilters?: DocumentTypeFilter[];
8
+ attachmentFilters?: DocumentAttachmentFilter[];
9
+ onTypeFiltersChange?: (values: DocumentTypeFilter[]) => void;
10
+ onAttachmentFiltersChange?: (values: DocumentAttachmentFilter[]) => void;
11
+ requestedDocPath?: string | null;
12
+ requestedDocAssetId?: string | null;
13
+ onRequestedDocHandled?: () => void;
14
+ }
15
+ interface Result {
16
+ documentApiBaseUrl: string;
17
+ documents: DocMeta[];
18
+ filteredDocs: DocMeta[];
19
+ loading: boolean;
20
+ error: string | null;
21
+ selectedDoc: DocMeta | null;
22
+ searchQuery: string;
23
+ typeFilters: DocumentTypeFilter[];
24
+ attachmentFilters: DocumentAttachmentFilter[];
25
+ hasActiveFilters: boolean;
26
+ setSearchQuery: (value: string) => void;
27
+ handleTypeFiltersChange: (values: DocumentTypeFilter[]) => void;
28
+ handleAttachmentFiltersChange: (values: DocumentAttachmentFilter[]) => void;
29
+ fetchDocuments: () => Promise<void>;
30
+ selectDoc: (doc: DocMeta | null) => void;
31
+ handleDeleteSelected: () => Promise<void>;
32
+ }
33
+ export declare function useDocumentWorkspaceController({ runtimeMode, apiBaseUrl, cloudAuthBaseUrl, typeFilters: controlledTypeFilters, attachmentFilters: controlledAttachmentFilters, onTypeFiltersChange, onAttachmentFiltersChange, requestedDocPath, requestedDocAssetId, onRequestedDocHandled, }: Options): Result;
34
+ export {};
@@ -0,0 +1,113 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { fetchTaskforceApi } from '../../utils/taskforceApiClient';
3
+ import { logDocumentManagerDebug, resolveDocumentManagerApiBase } from '../../utils/documentManagerApi';
4
+ import { DOCUMENT_ATTACHMENT_FILTER_OPTIONS, DOCUMENT_TYPE_FILTER_OPTIONS, } from './documentWorkspaceFilters';
5
+ import { docPathMatches, extractAssetIdFromPath, filterDocuments, normalizeDocPath, } from './documentWorkspaceModel';
6
+ export function useDocumentWorkspaceController({ runtimeMode = 'local', apiBaseUrl = '', cloudAuthBaseUrl = '', typeFilters: controlledTypeFilters, attachmentFilters: controlledAttachmentFilters, onTypeFiltersChange, onAttachmentFiltersChange, requestedDocPath = null, requestedDocAssetId = null, onRequestedDocHandled, }) {
7
+ const [documents, setDocuments] = useState([]);
8
+ const [loading, setLoading] = useState(true);
9
+ const [error, setError] = useState(null);
10
+ const [selectedDocId, setSelectedDocId] = useState(null);
11
+ const [searchQuery, setSearchQuery] = useState('');
12
+ const [internalTypeFilters, setInternalTypeFilters] = useState(() => DOCUMENT_TYPE_FILTER_OPTIONS.map((option) => option.value));
13
+ const [internalAttachmentFilters, setInternalAttachmentFilters] = useState(() => DOCUMENT_ATTACHMENT_FILTER_OPTIONS.map((option) => option.value));
14
+ const documentApiBaseUrl = resolveDocumentManagerApiBase({
15
+ runtimeMode,
16
+ apiBaseUrl,
17
+ cloudAuthBaseUrl,
18
+ });
19
+ const typeFilters = controlledTypeFilters ?? internalTypeFilters;
20
+ const attachmentFilters = controlledAttachmentFilters ?? internalAttachmentFilters;
21
+ const handleTypeFiltersChange = onTypeFiltersChange ?? setInternalTypeFilters;
22
+ const handleAttachmentFiltersChange = onAttachmentFiltersChange ?? setInternalAttachmentFilters;
23
+ const fetchDocuments = useCallback(async () => {
24
+ setLoading(true);
25
+ setError(null);
26
+ try {
27
+ const res = await fetchTaskforceApi('/api/taskforce/documents', {
28
+ credentials: 'include',
29
+ }, documentApiBaseUrl);
30
+ if (!res.ok)
31
+ throw new Error(`Failed to load documents (${res.status})`);
32
+ const data = await res.json();
33
+ const nextDocuments = Array.isArray(data.documents) ? data.documents : [];
34
+ logDocumentManagerDebug('list.loaded', {
35
+ runtimeMode,
36
+ apiBaseUrl: documentApiBaseUrl || '(same-origin)',
37
+ count: nextDocuments.length,
38
+ });
39
+ setDocuments(nextDocuments);
40
+ }
41
+ catch (e) {
42
+ setError(e?.message || 'Failed to load documents');
43
+ logDocumentManagerDebug('list.error', {
44
+ runtimeMode,
45
+ apiBaseUrl: documentApiBaseUrl || '(same-origin)',
46
+ message: e?.message || 'Failed to load documents',
47
+ });
48
+ }
49
+ finally {
50
+ setLoading(false);
51
+ }
52
+ }, [documentApiBaseUrl, runtimeMode]);
53
+ useEffect(() => {
54
+ void fetchDocuments();
55
+ }, [fetchDocuments]);
56
+ const filteredDocs = useMemo(() => filterDocuments(documents, searchQuery, typeFilters, attachmentFilters), [attachmentFilters, documents, searchQuery, typeFilters]);
57
+ const selectedDoc = useMemo(() => (selectedDocId ? documents.find((doc) => doc.id === selectedDocId) ?? null : null), [documents, selectedDocId]);
58
+ useEffect(() => {
59
+ if ((!requestedDocPath && !requestedDocAssetId) || documents.length === 0)
60
+ return;
61
+ const requested = requestedDocPath ? normalizeDocPath(requestedDocPath) : '';
62
+ const normalizedRequestedAssetId = String(requestedDocAssetId || '').trim().toLowerCase()
63
+ || extractAssetIdFromPath(requestedDocPath);
64
+ const matched = documents.find((doc) => {
65
+ const candidateAssetId = extractAssetIdFromPath(doc.fsPath) || String(doc.assetId || '').trim().toLowerCase() || null;
66
+ if (normalizedRequestedAssetId && candidateAssetId && candidateAssetId === normalizedRequestedAssetId)
67
+ return true;
68
+ if (!requested)
69
+ return false;
70
+ return docPathMatches(normalizeDocPath(doc.fsPath), requested);
71
+ });
72
+ if (!matched)
73
+ return;
74
+ setSelectedDocId(matched.id);
75
+ onRequestedDocHandled?.();
76
+ }, [documents, onRequestedDocHandled, requestedDocAssetId, requestedDocPath]);
77
+ useEffect(() => {
78
+ if ((requestedDocPath || requestedDocAssetId) && !selectedDoc)
79
+ return;
80
+ if (filteredDocs.length === 0) {
81
+ if (selectedDocId !== null) {
82
+ setSelectedDocId(null);
83
+ }
84
+ return;
85
+ }
86
+ if (selectedDoc && filteredDocs.some((doc) => doc.id === selectedDoc.id))
87
+ return;
88
+ setSelectedDocId(filteredDocs[0].id);
89
+ }, [filteredDocs, requestedDocAssetId, requestedDocPath, selectedDoc, selectedDocId]);
90
+ const handleDeleteSelected = useCallback(async () => {
91
+ setSelectedDocId(null);
92
+ await fetchDocuments();
93
+ }, [fetchDocuments]);
94
+ const hasActiveFilters = searchQuery.trim().length > 0 || typeFilters.length > 0 || attachmentFilters.length > 0;
95
+ return {
96
+ documentApiBaseUrl,
97
+ documents,
98
+ filteredDocs,
99
+ loading,
100
+ error,
101
+ selectedDoc,
102
+ searchQuery,
103
+ typeFilters,
104
+ attachmentFilters,
105
+ hasActiveFilters,
106
+ setSearchQuery,
107
+ handleTypeFiltersChange,
108
+ handleAttachmentFiltersChange,
109
+ fetchDocuments,
110
+ selectDoc: (doc) => setSelectedDocId(doc?.id ?? null),
111
+ handleDeleteSelected,
112
+ };
113
+ }