@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
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
@@ -44,6 +44,9 @@ function createEmptyReviewSessionsResponse() {
44
44
  json: async () => ({ sessions: [] }),
45
45
  };
46
46
  }
47
+ function renderDocumentWorkspace(props) {
48
+ return render(_jsx(DocumentWorkspace, { tasks: props.tasks ?? [], documents: props.documents, loading: false, error: null, selectedDoc: props.selectedDoc ?? null, searchQuery: props.searchQuery ?? '', hasActiveFilters: props.hasActiveFilters ?? false, onSearchChange: props.onSearchChange ?? (() => { }), onSelectDoc: props.onSelectDoc ?? (() => { }), onRefresh: props.onRefresh ?? (() => { }), onDeleteSelected: props.onDeleteSelected ?? (() => { }) }));
49
+ }
47
50
  describe('Document workspace management', () => {
48
51
  beforeEach(() => {
49
52
  fetchTaskforceApi.mockReset();
@@ -87,7 +90,7 @@ describe('Document workspace management', () => {
87
90
  ];
88
91
  function Harness() {
89
92
  const [selectedDoc, setSelectedDoc] = useState(null);
90
- return (_jsx(DocumentWorkspace, { tasks: [{ id: 'task-1', title: 'Parent task' }], documents: docs, loading: false, error: null, selectedDoc: selectedDoc, searchQuery: "", typeFilters: [], attachmentFilters: ['unattached'], onSearchChange: () => { }, onSelectDoc: setSelectedDoc, onRefresh: () => { } }));
93
+ return (_jsx(DocumentWorkspace, { tasks: [{ id: 'task-1', title: 'Parent task' }], documents: docs.filter((doc) => doc.attachmentState === 'unattached'), loading: false, error: null, selectedDoc: selectedDoc, searchQuery: "", hasActiveFilters: true, onSearchChange: () => { }, onSelectDoc: setSelectedDoc, onRefresh: () => { }, onDeleteSelected: () => { } }));
91
94
  }
92
95
  render(_jsx(Harness, {}));
93
96
  expect(screen.getByRole('button', { name: /Orphan notes/i })).toBeInTheDocument();
@@ -95,13 +98,13 @@ describe('Document workspace management', () => {
95
98
  expect(screen.getByText('Unattached')).toBeInTheDocument();
96
99
  });
97
100
  it('keeps document search in the index without duplicate filter controls', () => {
98
- render(_jsx(DocumentWorkspace, { tasks: [], documents: [createDoc()], loading: false, error: null, selectedDoc: null, searchQuery: "", typeFilters: [], attachmentFilters: [], onSearchChange: () => { }, onSelectDoc: () => { }, onRefresh: () => { } }));
101
+ renderDocumentWorkspace({ documents: [createDoc()] });
99
102
  expect(screen.getByPlaceholderText('Search documents…')).toBeInTheDocument();
100
103
  expect(screen.queryByRole('button', { name: 'Attached' })).not.toBeInTheDocument();
101
104
  expect(screen.queryByRole('button', { name: 'Plans' })).not.toBeInTheDocument();
102
105
  });
103
106
  it('shows the document reference badge in the bottom metadata row of index cards', () => {
104
- render(_jsx(DocumentWorkspace, { tasks: [], documents: [createDoc({ referenceLabel: 'D-42' })], loading: false, error: null, selectedDoc: null, searchQuery: "", typeFilters: [], attachmentFilters: [], onSearchChange: () => { }, onSelectDoc: () => { }, onRefresh: () => { } }));
107
+ renderDocumentWorkspace({ documents: [createDoc({ referenceLabel: 'D-42' })] });
105
108
  expect(screen.getByText('D-42')).toBeInTheDocument();
106
109
  expect(screen.getByText(/64B/)).toBeInTheDocument();
107
110
  });
@@ -142,6 +145,49 @@ describe('Document workspace management', () => {
142
145
  confirmSpy.mockRestore();
143
146
  alertSpy.mockRestore();
144
147
  });
148
+ it('shows delete and unlink for attached docs and sends forceDetach on delete', async () => {
149
+ const user = userEvent.setup();
150
+ fetchTaskforceApi
151
+ .mockResolvedValueOnce({
152
+ ok: true,
153
+ text: async () => '# Example\n',
154
+ })
155
+ .mockResolvedValueOnce(createEmptyReviewSessionsResponse())
156
+ .mockResolvedValueOnce({
157
+ ok: true,
158
+ json: async () => ({ success: true, softDeleted: true, detachedTaskCount: 2 }),
159
+ });
160
+ const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
161
+ const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => { });
162
+ const onDeleted = vi.fn();
163
+ render(_jsx(DocumentViewer, { doc: {
164
+ id: 'doc-attached',
165
+ assetId: 'asset-doc-attached',
166
+ fsPath: 'workspaces/default/assets/documents/doc-attached.md',
167
+ apiUrl: '/api/taskforce/documents/asset-doc-attached/content',
168
+ taskId: 'task-1',
169
+ displayName: 'Attached spec',
170
+ title: 'Attached spec',
171
+ docType: 'other',
172
+ sizeBytes: 64,
173
+ updatedAt: '2026-03-19T11:00:00.000Z',
174
+ editable: true,
175
+ attachmentCount: 2,
176
+ attachmentState: 'attached',
177
+ }, taskTitle: null, onDeleted: onDeleted }));
178
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenCalledWith('/api/taskforce/documents/asset-doc-attached/content', { credentials: 'same-origin' }, ''));
179
+ await user.click(screen.getByRole('button', { name: /Delete & Unlink/i }));
180
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenLastCalledWith('/api/taskforce/documents/asset-doc-attached', expect.objectContaining({
181
+ method: 'DELETE',
182
+ credentials: 'include',
183
+ headers: { 'Content-Type': 'application/json' },
184
+ body: JSON.stringify({ forceDetach: true })
185
+ }), ''));
186
+ expect(confirmSpy).toHaveBeenCalledWith(expect.stringContaining('remove it from 2 linked tasks'));
187
+ expect(onDeleted).toHaveBeenCalled();
188
+ confirmSpy.mockRestore();
189
+ alertSpy.mockRestore();
190
+ });
145
191
  it('uses same-origin document routes in local runtime even when an api base is configured', async () => {
146
192
  const user = userEvent.setup();
147
193
  fetchTaskforceApi
@@ -226,6 +272,46 @@ describe('Document workspace management', () => {
226
272
  expect(await screen.findByText('Please tighten the rollout section.')).toBeInTheDocument();
227
273
  expect(screen.getByText('Lines 18-24')).toBeInTheDocument();
228
274
  });
275
+ it('toggles the review sidebar from the document header', async () => {
276
+ const user = userEvent.setup();
277
+ fetchTaskforceApi
278
+ .mockResolvedValueOnce({
279
+ ok: true,
280
+ text: async () => '# Example\n',
281
+ })
282
+ .mockResolvedValueOnce({
283
+ ok: true,
284
+ json: async () => ({
285
+ sessions: [{
286
+ id: 'review-1',
287
+ tenantId: 'default',
288
+ workspaceId: 'default',
289
+ assetId: 'asset-doc-1',
290
+ documentId: 'doc-1',
291
+ documentVersion: 1,
292
+ title: 'Review 1',
293
+ status: 'open',
294
+ createdByActorId: 'user',
295
+ updatedByActorId: 'user',
296
+ createdByActor: null,
297
+ updatedByActor: null,
298
+ createdAt: '2026-03-19T12:00:00.000Z',
299
+ updatedAt: '2026-03-19T12:05:00.000Z',
300
+ deletedAt: null,
301
+ comments: [],
302
+ }],
303
+ }),
304
+ });
305
+ render(_jsx(DocumentViewer, { doc: createDoc(), taskTitle: null }));
306
+ expect(await screen.findByText('Review & Comments')).toBeInTheDocument();
307
+ expect(screen.getByPlaceholderText('Add a comment for this document review…')).toBeInTheDocument();
308
+ await user.click(screen.getByRole('button', { name: 'Hide Review' }));
309
+ expect(screen.queryByText('Review & Comments')).not.toBeInTheDocument();
310
+ expect(screen.queryByPlaceholderText('Add a comment for this document review…')).not.toBeInTheDocument();
311
+ await user.click(screen.getByRole('button', { name: 'Show Review' }));
312
+ expect(await screen.findByText('Review & Comments')).toBeInTheDocument();
313
+ expect(screen.getByPlaceholderText('Add a comment for this document review…')).toBeInTheDocument();
314
+ });
229
315
  it('creates a review session and posts comments from the document viewer', async () => {
230
316
  const user = userEvent.setup();
231
317
  fetchTaskforceApi
@@ -1087,13 +1173,114 @@ Body copy.
1087
1173
  attachmentCount: 0,
1088
1174
  },
1089
1175
  ];
1090
- const { rerender } = render(_jsx(DocumentWorkspace, { tasks: [{ id: 'task-1', title: 'Parent task' }], documents: docs, loading: false, error: null, selectedDoc: null, searchQuery: "", typeFilters: [], attachmentFilters: ['attached'], onSearchChange: () => { }, onSelectDoc: () => { }, onRefresh: () => { } }));
1176
+ const { rerender } = renderDocumentWorkspace({
1177
+ tasks: [{ id: 'task-1', title: 'Parent task' }],
1178
+ documents: docs.filter((doc) => doc.taskId),
1179
+ hasActiveFilters: true,
1180
+ });
1091
1181
  expect(screen.getByText('Attached fallback')).toBeInTheDocument();
1092
1182
  expect(screen.queryByText('Unattached fallback')).not.toBeInTheDocument();
1093
- rerender(_jsx(DocumentWorkspace, { tasks: [{ id: 'task-1', title: 'Parent task' }], documents: docs, loading: false, error: null, selectedDoc: null, searchQuery: "", typeFilters: [], attachmentFilters: ['unattached'], onSearchChange: () => { }, onSelectDoc: () => { }, onRefresh: () => { } }));
1183
+ rerender(_jsx(DocumentWorkspace, { tasks: [{ id: 'task-1', title: 'Parent task' }], documents: docs.filter((doc) => !doc.taskId), loading: false, error: null, selectedDoc: null, searchQuery: "", hasActiveFilters: true, onSearchChange: () => { }, onSelectDoc: () => { }, onRefresh: () => { }, onDeleteSelected: () => { } }));
1094
1184
  expect(screen.getByText('Unattached fallback')).toBeInTheDocument();
1095
1185
  expect(screen.queryByText('Attached fallback')).not.toBeInTheDocument();
1096
1186
  });
1187
+ it('switches the active document when a different index item is selected', async () => {
1188
+ const user = userEvent.setup();
1189
+ const docs = [
1190
+ createDoc({
1191
+ id: 'doc-first',
1192
+ assetId: 'asset-doc-first',
1193
+ apiUrl: '/api/taskforce/documents/asset-doc-first/content',
1194
+ displayName: 'First notes',
1195
+ title: 'First notes',
1196
+ }),
1197
+ createDoc({
1198
+ id: 'doc-second',
1199
+ assetId: 'asset-doc-second',
1200
+ apiUrl: '/api/taskforce/documents/asset-doc-second/content',
1201
+ displayName: 'Second notes',
1202
+ title: 'Second notes',
1203
+ fsPath: 'workspaces/default/assets/documents/second-notes.md',
1204
+ }),
1205
+ ];
1206
+ fetchTaskforceApi
1207
+ .mockResolvedValueOnce({
1208
+ ok: true,
1209
+ json: async () => ({ documents: docs }),
1210
+ })
1211
+ .mockResolvedValueOnce({
1212
+ ok: true,
1213
+ text: async () => '# First content\n',
1214
+ })
1215
+ .mockResolvedValueOnce(createEmptyReviewSessionsResponse())
1216
+ .mockResolvedValueOnce({
1217
+ ok: true,
1218
+ text: async () => '# Second content\n',
1219
+ })
1220
+ .mockResolvedValueOnce(createEmptyReviewSessionsResponse());
1221
+ render(_jsx(DocumentWorkspaceShell, { tasks: [], runtimeMode: "local" }));
1222
+ expect(await screen.findByText('First content')).toBeInTheDocument();
1223
+ await user.click(screen.getByRole('button', { name: /Second notes/i }));
1224
+ expect(await screen.findByText('Second content')).toBeInTheDocument();
1225
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(4, '/api/taskforce/documents/asset-doc-second/content', expect.objectContaining({ credentials: 'same-origin' }), ''));
1226
+ });
1227
+ it('returns to the remaining filtered document when the active selection is filtered out', async () => {
1228
+ const user = userEvent.setup();
1229
+ const docs = [
1230
+ createDoc({
1231
+ id: 'doc-attached-return',
1232
+ assetId: 'asset-doc-attached-return',
1233
+ apiUrl: '/api/taskforce/documents/asset-doc-attached-return/content',
1234
+ displayName: 'Attached return doc',
1235
+ title: 'Attached return doc',
1236
+ attachmentCount: 1,
1237
+ attachmentState: 'attached',
1238
+ taskId: 'task-1',
1239
+ }),
1240
+ createDoc({
1241
+ id: 'doc-unattached-return',
1242
+ assetId: 'asset-doc-unattached-return',
1243
+ apiUrl: '/api/taskforce/documents/asset-doc-unattached-return/content',
1244
+ displayName: 'Unattached return doc',
1245
+ title: 'Unattached return doc',
1246
+ fsPath: 'workspaces/default/assets/documents/unattached-return-doc.md',
1247
+ attachmentCount: 0,
1248
+ attachmentState: 'unattached',
1249
+ taskId: null,
1250
+ }),
1251
+ ];
1252
+ function Harness() {
1253
+ const [attachmentFilters, setAttachmentFilters] = useState([]);
1254
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: () => setAttachmentFilters(['attached']), children: "Show attached only" }), _jsx(DocumentWorkspaceShell, { tasks: [{ id: 'task-1', title: 'Parent task' }], runtimeMode: "local", attachmentFilters: attachmentFilters })] }));
1255
+ }
1256
+ fetchTaskforceApi
1257
+ .mockResolvedValueOnce({
1258
+ ok: true,
1259
+ json: async () => ({ documents: docs }),
1260
+ })
1261
+ .mockResolvedValueOnce({
1262
+ ok: true,
1263
+ text: async () => '# Attached return content\n',
1264
+ })
1265
+ .mockResolvedValueOnce(createEmptyReviewSessionsResponse())
1266
+ .mockResolvedValueOnce({
1267
+ ok: true,
1268
+ text: async () => '# Unattached return content\n',
1269
+ })
1270
+ .mockResolvedValueOnce(createEmptyReviewSessionsResponse())
1271
+ .mockResolvedValueOnce({
1272
+ ok: true,
1273
+ text: async () => '# Attached return content\n',
1274
+ })
1275
+ .mockResolvedValueOnce(createEmptyReviewSessionsResponse());
1276
+ render(_jsx(Harness, {}));
1277
+ expect(await screen.findByText('Attached return content')).toBeInTheDocument();
1278
+ await user.click(screen.getByRole('button', { name: /Unattached return doc/i }));
1279
+ expect(await screen.findByText('Unattached return content')).toBeInTheDocument();
1280
+ await user.click(screen.getByRole('button', { name: /Show attached only/i }));
1281
+ expect(await screen.findByText('Attached return content')).toBeInTheDocument();
1282
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(6, '/api/taskforce/documents/asset-doc-attached-return/content', expect.objectContaining({ credentials: 'same-origin' }), ''));
1283
+ });
1097
1284
  it('selects a requested document by asset id when normalized paths differ', async () => {
1098
1285
  const docs = [
1099
1286
  {
@@ -3,7 +3,7 @@ import React, { useEffect, useMemo, useState } from 'react';
3
3
  import { Sun, Moon, Sparkles, Keyboard, FileText, Check, AlertTriangle, Loader2, Settings, Layers, Box, Copy, Trash2, Terminal, Bot, Codepen, Command, Wind, Github, Zap, ChevronDown, ChevronRight, Plug } from 'lucide-react';
4
4
  import styles from '../../Taskforce.module.css';
5
5
  import { TASKFORCE_THEME_OPTIONS } from '../../utils/theme';
6
- import { MCP_CLIENT_OPTIONS, buildCloudMcpClientIntegration, buildCloudMcpServerId, sanitizeMcpId } from '../../mcp/clientIntegrations';
6
+ import { MCP_CLIENT_OPTIONS, buildCloudMcpClientIntegration, buildLocalMcpClientIntegration, buildCloudMcpServerId, sanitizeMcpId } from '../../mcp/clientIntegrations';
7
7
  import { resolveTaskforceMcpBaseUrlFromBaseUrl } from '../../config/cloudEnvironment';
8
8
  import { TaxonomyManager } from './settings/TaxonomyManager';
9
9
  const THEME_ICON_MAP = {
@@ -12,6 +12,7 @@ const THEME_ICON_MAP = {
12
12
  moon: Moon,
13
13
  sparkles: Sparkles,
14
14
  };
15
+ const DEFAULT_MCP_TOKEN_SCOPES = ['tasks:read', 'tasks:write', 'workspace:read'];
15
16
  export function TaskSettings(props) {
16
17
  const { settingsModel, onSectionChange } = props;
17
18
  const { currentTheme, globalTheme, themeUseGlobalDefault, keyShortcut, jsonBackupEnabled, globalJsonBackupEnabled, globalWeekStartsOn, locale = 'en-US', supportedLocales = ['en-US', 'es-419', 'pt-BR'], jsonBackupUseGlobalDefault, manualComplexityEnabled, checklistDropdownEnabled = true, showTaskCardStatusLabel = true, exportWorkflowsPath, exportingResource, exportResult, initialSection, onThemeChange, onSaveTheme, onSaveGlobalTheme, onResetProjectToGlobal, onKeyShortcutChange, onJsonBackupEnabledChange, onSaveGlobalJsonBackupEnabled, onSaveGlobalWeekStartsOn, onSaveLocale, onManualComplexityEnabledChange, onChecklistDropdownEnabledChange, onShowTaskCardStatusLabelChange, onSaveSettings, onExportWorkflowsPathChange, onShowFolderBrowserChange, onBrowserTargetChange, onFetchFolders, categories, pathValidation, taxonomyDisplayLabels, onUpdateCategory, onRemoveCategory, onSaveCategory, onAddPath, onRemovePath, onUpdateCategoryIcon, onUpdateCategoryColor, types, onSaveType, onRemoveType, onUpdateType, taxonomies, onUpdateTaxonomies, priorities, onUpdatePriorities, onAnalyzeSystemTaxonomyPack, onApplySystemTaxonomyPack, onUpdateTaxonomyDisplayLabels, projectRoot, projectName, mcpHostRoot, serverHostRoot, mcpScriptPath, tenantId, workspaceId, runtimeMode = 'local', cloudAuthBaseUrl = '', cloudMcpBaseUrl = '', fetchCloudAuthApi, workspaceSwitchingEnabled = false, currentWorkspaceRole = 'member', currentWorkspaceName = '', onDeleteWorkspace, onMcpHostRootChange, isAuthenticated = false, exportEnvironment, setupState = null, buildInfo = null, onExportEnvironmentChange, onSaveSetupMode, onSaveWorkspaceProfile, availableWorkflows = [], onExportWorkflows, initiativeTemplates = [], onRefreshInitiativeTemplates, onCreateInitiativeFromTemplate, onRefreshWorkflows } = settingsModel;
@@ -147,7 +148,7 @@ export function TaskSettings(props) {
147
148
  const cloudMcpAvailable = runtimeMode === 'cloud' || isAuthenticated;
148
149
  const defaultMcpMode = runtimeMode === 'cloud'
149
150
  ? 'cloud'
150
- : (isAuthenticated ? 'cloud' : 'local');
151
+ : 'local';
151
152
  const [selectedMcpMode, setSelectedMcpMode] = useState(defaultMcpMode);
152
153
  const [copiedConfig, setCopiedConfig] = useState(false);
153
154
  const [useGlobalMcp, setUseGlobalMcp] = useState(true);
@@ -167,11 +168,10 @@ export function TaskSettings(props) {
167
168
  return `${label} MCP`;
168
169
  }, [currentWorkspaceName, setupState?.workspace?.name, projectName, workspaceId]);
169
170
  const [newTokenName, setNewTokenName] = useState(defaultTokenName);
170
- const [newTokenScopes, setNewTokenScopes] = useState(['tasks:read', 'tasks:write', 'workspace:read']);
171
171
  const [newTokenExpiresAt, setNewTokenExpiresAt] = useState('');
172
172
  const [mcpTokenNotice, setMcpTokenNotice] = useState(null);
173
173
  const [generatedTokenValue, setGeneratedTokenValue] = useState(null);
174
- const [selectedMcpClient, setSelectedMcpClient] = useState('cursor');
174
+ const [selectedMcpClient, setSelectedMcpClient] = useState('general');
175
175
  const [mcpConnectionToken, setMcpConnectionToken] = useState('');
176
176
  const [mcpConnectionBusy, setMcpConnectionBusy] = useState(false);
177
177
  const [mcpConnectionResult, setMcpConnectionResult] = useState(null);
@@ -282,16 +282,6 @@ export function TaskSettings(props) {
282
282
  const segments = String(text || '').split(tokenText);
283
283
  return segments.map((segment, index) => (_jsxs(React.Fragment, { children: [_jsx("span", { style: normalText, children: segment }), index < segments.length - 1 && _jsx("span", { style: highlighted, children: tokenText })] }, `text-config-${index}`)));
284
284
  };
285
- const toggleTokenScope = (scope) => {
286
- setNewTokenScopes((current) => {
287
- if (current.includes(scope)) {
288
- if (current.length === 1)
289
- return current;
290
- return current.filter((item) => item !== scope);
291
- }
292
- return [...current, scope];
293
- });
294
- };
295
285
  const copyToClipboard = (value, mode) => {
296
286
  navigator.clipboard.writeText(value);
297
287
  setCopiedConfig(mode);
@@ -351,13 +341,45 @@ export function TaskSettings(props) {
351
341
  }
352
342
  return { serverId, serverConfig };
353
343
  };
344
+ const localMcpServer = useMemo(() => buildMcpServerConfig(), [
345
+ mcpScriptPath,
346
+ projectRoot,
347
+ mcpHostRoot,
348
+ serverHostRoot,
349
+ projectName,
350
+ tenantId,
351
+ workspaceId,
352
+ setupState?.workspaceId,
353
+ useGlobalMcp
354
+ ]);
355
+ const localClientIntegration = useMemo(() => buildLocalMcpClientIntegration({
356
+ clientId: selectedMcpClient,
357
+ serverId: localMcpServer.serverId,
358
+ serverConfig: localMcpServer.serverConfig
359
+ }), [localMcpServer, selectedMcpClient]);
360
+ const localMcpServerConfig = localClientIntegration.kind === 'json'
361
+ ? localClientIntegration.serverConfig ?? null
362
+ : null;
363
+ const localMcpFullConfig = useMemo(() => {
364
+ if (localClientIntegration.kind === 'text') {
365
+ return localClientIntegration.renderedText || '';
366
+ }
367
+ let payload = {
368
+ [localClientIntegration.rootKey || 'mcpServers']: {
369
+ [localMcpServer.serverId]: localClientIntegration.serverConfig
370
+ }
371
+ };
372
+ for (const key of [...(localClientIntegration.wrapperKeys || [])].reverse()) {
373
+ payload = { [key]: payload };
374
+ }
375
+ return JSON.stringify(payload, null, 2);
376
+ }, [localClientIntegration, localMcpServer.serverId]);
354
377
  const handleCopyConfig = (mode = 'full') => {
355
378
  if (!mcpScriptPath && !useGlobalMcp)
356
379
  return;
357
- const { serverId, serverConfig } = buildMcpServerConfig();
358
380
  const toCopy = mode === 'full'
359
- ? JSON.stringify({ mcpServers: { [serverId]: serverConfig } }, null, 2)
360
- : `"${serverId}": ${JSON.stringify(serverConfig, null, 2)}`;
381
+ ? localMcpFullConfig
382
+ : `"${localMcpServer.serverId}": ${JSON.stringify(localMcpServer.serverConfig, null, 2)}`;
361
383
  copyToClipboard(toCopy, mode);
362
384
  };
363
385
  const loadMcpTokens = async () => {
@@ -423,7 +445,7 @@ export function TaskSettings(props) {
423
445
  headers: { 'Content-Type': 'application/json' },
424
446
  body: JSON.stringify({
425
447
  name: trimmedName,
426
- scopes: newTokenScopes,
448
+ scopes: DEFAULT_MCP_TOKEN_SCOPES,
427
449
  expiresAt: newTokenExpiresAt ? new Date(`${newTokenExpiresAt}T00:00:00`).toISOString() : null
428
450
  })
429
451
  });
@@ -500,7 +522,7 @@ export function TaskSettings(props) {
500
522
  headers: { 'Content-Type': 'application/json' },
501
523
  body: JSON.stringify({
502
524
  token: trimmedToken,
503
- requiredScopes: newTokenScopes
525
+ requiredScopes: DEFAULT_MCP_TOKEN_SCOPES
504
526
  })
505
527
  });
506
528
  const payload = await res.json().catch(() => ({}));
@@ -512,13 +534,36 @@ export function TaskSettings(props) {
512
534
  const workspaceName = typeof details?.tokenWorkspaceId === 'string'
513
535
  ? String(details.tokenWorkspaceId)
514
536
  : undefined;
515
- const message = payload?.code === 'MCP_TOKEN_MISSING_SCOPES' && missingScopes.length > 0
537
+ const endpoint = typeof details?.endpoint === 'string'
538
+ ? String(details.endpoint)
539
+ : cloudMcpEndpoint;
540
+ let message = payload?.code === 'MCP_TOKEN_MISSING_SCOPES' && missingScopes.length > 0
516
541
  ? `Token is missing required scopes: ${missingScopes.join(', ')}.`
517
542
  : String(payload?.error || 'Failed to test MCP connection.');
543
+ let followup;
544
+ if (payload?.code === 'MCP_ENDPOINT_GATEWAY_ERROR') {
545
+ message = 'The dedicated MCP host is responding with a gateway error.';
546
+ followup = 'This usually means the cloud MCP service is degraded. Retry shortly, and if it keeps failing treat it as an environment incident rather than a bad token.';
547
+ }
548
+ else if (payload?.code === 'MCP_ENDPOINT_TIMEOUT') {
549
+ message = 'The dedicated MCP host timed out during MCP initialization.';
550
+ followup = 'This usually points to a degraded or overloaded cloud MCP service.';
551
+ }
552
+ else if (payload?.code === 'MCP_ENDPOINT_UNREACHABLE') {
553
+ message = 'The dedicated MCP host could not be reached.';
554
+ followup = 'Check the cloud MCP hostname and environment health before rotating credentials.';
555
+ }
556
+ else if (payload?.code === 'MCP_ENDPOINT_REJECTED_VALIDATED_TOKEN') {
557
+ message = 'The dedicated MCP host rejected a token that validated locally.';
558
+ followup = 'This suggests the MCP host is stale, routed incorrectly, or out of sync with auth data.';
559
+ }
518
560
  setMcpConnectionResult({
519
561
  type: 'error',
520
562
  message,
521
563
  workspaceName,
564
+ endpoint,
565
+ errorCode: typeof payload?.code === 'string' ? String(payload.code) : undefined,
566
+ followup,
522
567
  });
523
568
  return;
524
569
  }
@@ -529,12 +574,15 @@ export function TaskSettings(props) {
529
574
  scopes: Array.isArray(result?.scopes) ? result.scopes.map((scope) => String(scope)) : [],
530
575
  workspaceName: typeof result?.workspaceName === 'string' ? String(result.workspaceName) : '',
531
576
  workspaceId: typeof result?.workspaceId === 'string' ? String(result.workspaceId) : '',
577
+ endpoint: typeof result?.endpoint === 'string' ? String(result.endpoint) : cloudMcpEndpoint,
532
578
  });
533
579
  }
534
580
  catch {
535
581
  setMcpConnectionResult({
536
582
  type: 'error',
537
- message: 'Failed to test MCP connection.'
583
+ message: 'Failed to test MCP connection.',
584
+ endpoint: cloudMcpEndpoint,
585
+ followup: 'The settings page could not complete the connection probe. Check your network connection and the selected cloud environment.'
538
586
  });
539
587
  }
540
588
  finally {
@@ -834,13 +882,13 @@ export function TaskSettings(props) {
834
882
  });
835
883
  }, initialTab: (initialSection === 'categories' || initialSection === 'types' || initialSection === 'priorities')
836
884
  ? initialSection
837
- : undefined })] })), activeTab === 'mcp' && (_jsxs("div", { className: styles.settingsGroup, children: [showMcpModeToggle && (_jsxs("div", { className: styles.settingsTabs, children: [_jsx("button", { className: `${styles.settingsTabBtn} ${selectedMcpMode === 'cloud' ? styles.settingsTabBtnActive : ''}`, onClick: () => setSelectedMcpMode('cloud'), children: "Cloud MCP" }), _jsx("button", { className: `${styles.settingsTabBtn} ${selectedMcpMode === 'local' ? styles.settingsTabBtnActive : ''}`, onClick: () => setSelectedMcpMode('local'), children: "Local MCP" })] })), showCloudMcp && (_jsx(_Fragment, { children: selectedMcpClientUsesOAuth ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.settingLabelGroup, children: [_jsx("h3", { className: styles.settingTitle, children: "OAuth Connector Setup" }), _jsx("span", { className: styles.settingDescription, children: selectedMcpClient === 'chatgpt-desktop'
885
+ : undefined })] })), activeTab === 'mcp' && (_jsxs("div", { className: styles.settingsGroup, children: [showMcpModeToggle && (_jsxs("div", { className: styles.settingsTabs, children: [_jsx("button", { className: `${styles.settingsTabBtn} ${selectedMcpMode === 'local' ? styles.settingsTabBtnActive : ''}`, onClick: () => setSelectedMcpMode('local'), children: "Local MCP" }), _jsx("button", { className: `${styles.settingsTabBtn} ${selectedMcpMode === 'cloud' ? styles.settingsTabBtnActive : ''}`, onClick: () => setSelectedMcpMode('cloud'), children: "Cloud MCP" })] })), showCloudMcp && (_jsx(_Fragment, { children: selectedMcpClientUsesOAuth ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.settingGroup, children: [_jsxs("div", { className: styles.settingLabelGroup, children: [_jsx("h3", { className: styles.settingTitle, children: "OAuth Connector Setup" }), _jsx("span", { className: styles.settingDescription, children: selectedMcpClient === 'chatgpt-desktop'
838
886
  ? 'ChatGPT Desktop uses the Taskforce OAuth connector flow instead of a personal MCP token.'
839
887
  : 'Claude Desktop uses the Taskforce OAuth connector flow instead of a personal MCP token.' })] }), _jsx("div", { className: `${selectedConnectorGrant ? styles.successMessage : styles.settingHelper} ${styles.marginTop12}`, children: selectedConnectorGrant ? (_jsxs(_Fragment, { children: [_jsx(Check, { size: 14 }), _jsxs("div", { children: [_jsx("div", { children: "Connector already authorized for this workspace." }), _jsxs("div", { className: styles.settingHelper, children: ["Last used ", selectedConnectorGrant.lastUsedAt
840
888
  ? new Date(selectedConnectorGrant.lastUsedAt).toLocaleString()
841
- : 'not yet'] })] })] })) : ('No active connector authorization for this workspace yet.') }), _jsx("div", { className: styles.settingDescription, style: { marginTop: '12px' }, children: "Use the server URL above in your client, then finish the OAuth authorization flow from the Integrations tab." }), _jsx("div", { style: { display: 'flex', justifyContent: 'flex-end', width: '100%', marginTop: '12px' }, children: _jsx("button", { className: styles.copyBtn, onClick: () => setActiveTab('integrations'), children: "Open Integrations" }) })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom} ${styles.marginTop12}`, style: { minHeight: '260px' }, children: [_jsxs("div", { className: styles.codeHeader, children: [_jsx("span", { className: styles.codeLabel, children: "Cloud MCP Configuration" }), _jsxs("div", { className: styles.codeHeaderActions, children: [_jsx("select", { className: styles.settingInput, value: selectedMcpClient, onChange: (e) => setSelectedMcpClient(e.target.value), "aria-label": "MCP client", style: { width: 'auto', minWidth: '170px', padding: '6px 12px', fontSize: '12px', borderRadius: '8px' }, children: MCP_CLIENT_OPTIONS.map((option) => (_jsx("option", { value: option.id, disabled: option.disabled, children: option.label }, option.id))) }), _jsx("button", { className: `${styles.copyBtn} ${copiedConfig === 'full' ? styles.copyBtnActive : ''}`, onClick: handleCopyCloudMcpConfig, title: "Copy full configuration file", children: copiedConfig === 'full' ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) })] })] }), _jsx("pre", { className: styles.codeBlock, children: _jsx("code", { children: cloudMcpServerConfig
889
+ : 'not yet'] })] })] })) : ('No active connector authorization for this workspace yet.') }), _jsx("div", { className: styles.settingDescription, style: { marginTop: '12px' }, children: "Use the server URL above in your client, then finish the OAuth authorization flow from the Integrations tab." }), _jsx("div", { style: { display: 'flex', justifyContent: 'flex-end', width: '100%', marginTop: '12px' }, children: _jsx("button", { className: styles.copyBtn, onClick: () => setActiveTab('integrations'), children: "Open Integrations" }) })] }), _jsxs("div", { className: styles.settingGroup, children: [_jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom} ${styles.marginTop12}`, style: { minHeight: '260px' }, children: [_jsxs("div", { className: styles.codeHeader, children: [_jsx("span", { className: styles.codeLabel, children: "Cloud MCP Configuration" }), _jsxs("div", { className: styles.codeHeaderActions, children: [_jsx("select", { className: styles.settingInput, value: selectedMcpClient, onChange: (e) => setSelectedMcpClient(e.target.value), "aria-label": "MCP client", style: { width: 'auto', minWidth: '170px', padding: '6px 12px', fontSize: '12px', borderRadius: '8px' }, children: MCP_CLIENT_OPTIONS.map((option) => (_jsx("option", { value: option.id, disabled: option.disabled, children: option.label }, option.id))) }), _jsx("button", { className: `${styles.copyBtn} ${copiedConfig === 'full' ? styles.copyBtnActive : ''}`, onClick: handleCopyCloudMcpConfig, title: "Copy full configuration file", children: copiedConfig === 'full' ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) })] })] }), _jsx("pre", { className: styles.codeBlock, children: _jsx("code", { children: cloudMcpServerConfig
842
890
  ? renderMcpConfigBlock(cloudMcpServerId, cloudMcpServerConfig, cloudClientIntegration.rootKey || 'mcpServers', cloudClientIntegration.wrapperKeys || [])
843
- : renderTextConfigBlock(cloudClientIntegration.renderedText || '') }) })] }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: cloudClientIntegration.instructionLabel }) }), _jsx("div", { style: { padding: '16px 20px' }, children: _jsx("div", { className: styles.settingDescription, children: cloudClientIntegration.instructionText }) })] })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.settingLabelGroup, children: [_jsx("h3", { className: styles.settingTitle, children: "Generate Token" }), _jsx("span", { className: styles.settingDescription, children: "Tokens are shown once. Default scopes allow task read/write. Add workspace read if you want config and skills metadata in the client." })] }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 2fr) minmax(220px, 1fr)', gap: '12px', alignItems: 'end' }, children: [_jsxs("div", { className: styles.settingsItem, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Token Name" }) }), _jsx("input", { type: "text", className: styles.settingInput, placeholder: "Cursor on MacBook", value: newTokenName, onChange: (e) => setNewTokenName(e.target.value) })] }), _jsxs("div", { className: styles.settingsItem, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Expiry (Optional)" }) }), _jsx("input", { type: "date", className: `${styles.settingInput} ${styles.taskFormDateInput}`, value: newTokenExpiresAt, onChange: (e) => setNewTokenExpiresAt(e.target.value) })] })] }), _jsxs("div", { className: `${styles.settingsItem} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Scopes" }) }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '96px repeat(3, minmax(110px, 1fr))', gap: '10px 16px', alignItems: 'center' }, children: [_jsx("span", {}), _jsx("span", { className: styles.settingDescription, children: "Tasks" }), _jsx("span", { className: styles.settingDescription, children: "Workspace" }), _jsx("span", { className: styles.settingDescription, style: { opacity: 0.55 }, children: "Attachments" }), _jsx("span", { className: styles.settingDescription, children: "Read" }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('tasks:read'), onChange: () => toggleTokenScope('tasks:read') }) }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('workspace:read'), onChange: () => toggleTokenScope('workspace:read') }) }), _jsx("label", { className: styles.checkboxLabel, style: { opacity: 0.55 }, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) }), _jsx("span", { className: styles.settingDescription, children: "Write" }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('tasks:write'), onChange: () => toggleTokenScope('tasks:write') }) }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) }), _jsx("label", { className: styles.checkboxLabel, style: { opacity: 0.55 }, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) })] })] })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsx("div", { className: styles.settingHelper, children: "Generated tokens are only shown once. Copy the token before leaving this page." }), (() => {
891
+ : renderTextConfigBlock(cloudClientIntegration.renderedText || '') }) })] }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: cloudClientIntegration.instructionLabel }) }), _jsx("div", { style: { padding: '16px 20px' }, children: _jsx("div", { className: styles.settingDescription, children: cloudClientIntegration.instructionText }) })] })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom} ${styles.marginTop16}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("div", { className: styles.settingLabelGroup, style: { gap: '2px' }, children: _jsx("span", { className: styles.codeLabel, children: "Generate Token" }) }) }), _jsx("div", { className: styles.mcpTokenCardBody, children: _jsxs("div", { className: styles.mcpTokenFormGrid, children: [_jsxs("div", { className: styles.settingsItem, style: { gap: '6px' }, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Token Name" }) }), _jsx("input", { type: "text", className: styles.settingInput, placeholder: "Cursor on MacBook", value: newTokenName, onChange: (e) => setNewTokenName(e.target.value) })] }), _jsxs("div", { className: styles.settingsItem, style: { gap: '6px' }, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Expiry (Optional)" }) }), _jsx("input", { type: "date", className: `${styles.settingInput} ${styles.taskFormDateInput}`, value: newTokenExpiresAt, onChange: (e) => setNewTokenExpiresAt(e.target.value) })] })] }) }), (() => {
844
892
  const tokenBarError = mcpTokenNotice?.type === 'error'
845
893
  ? mcpTokenNotice.message
846
894
  : null;
@@ -848,20 +896,19 @@ export function TaskSettings(props) {
848
896
  || (copiedConfig === 'token'
849
897
  ? 'Copied to clipboard'
850
898
  : (generatedTokenValue || ''));
851
- return (_jsx("div", { style: { display: 'flex', gap: '8px', alignItems: 'stretch' }, children: _jsx("div", { className: styles.codeBlockWrapper, style: { flex: 1, cursor: generatedTokenValue ? 'pointer' : 'default' }, onClick: () => generatedTokenValue && copyToClipboard(generatedTokenValue, 'token'), role: generatedTokenValue ? 'button' : undefined, tabIndex: generatedTokenValue ? 0 : -1, onKeyDown: (e) => {
852
- if (!generatedTokenValue)
853
- return;
854
- if (e.key === 'Enter' || e.key === ' ') {
855
- e.preventDefault();
856
- copyToClipboard(generatedTokenValue, 'token');
857
- }
858
- }, title: generatedTokenValue ? (copiedConfig === 'token' ? 'Token copied' : 'Click to copy token') : undefined, children: _jsx("pre", { className: styles.codeBlock, style: {
859
- textAlign: 'center',
899
+ return (_jsxs("div", { className: styles.mcpTokenOutputWrap, style: { cursor: generatedTokenValue ? 'pointer' : 'default' }, onClick: () => generatedTokenValue && copyToClipboard(generatedTokenValue, 'token'), role: generatedTokenValue ? 'button' : undefined, tabIndex: generatedTokenValue ? 0 : -1, onKeyDown: (e) => {
900
+ if (!generatedTokenValue)
901
+ return;
902
+ if (e.key === 'Enter' || e.key === ' ') {
903
+ e.preventDefault();
904
+ copyToClipboard(generatedTokenValue, 'token');
905
+ }
906
+ }, title: generatedTokenValue ? (copiedConfig === 'token' ? 'Token copied' : 'Click to copy token') : undefined, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Token" }) }), _jsx("div", { className: styles.mcpTokenValueField, style: {
860
907
  color: tokenBarError ? 'var(--status-danger-strong, #f87171)' : undefined,
861
- }, children: _jsx("code", { children: displayedGeneratedToken }) }) }) }));
862
- })()] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom} ${styles.marginTop12}`, style: { minHeight: '260px' }, children: [_jsxs("div", { className: styles.codeHeader, children: [_jsx("span", { className: styles.codeLabel, children: "Cloud MCP Configuration" }), _jsxs("div", { className: styles.codeHeaderActions, children: [_jsx("select", { className: styles.settingInput, value: selectedMcpClient, onChange: (e) => setSelectedMcpClient(e.target.value), "aria-label": "MCP client", style: { width: 'auto', minWidth: '170px', padding: '6px 12px', fontSize: '12px', borderRadius: '8px' }, children: MCP_CLIENT_OPTIONS.map((option) => (_jsx("option", { value: option.id, disabled: option.disabled, children: option.label }, option.id))) }), _jsxs("button", { className: styles.copyBtn, onClick: () => void handleCreateMcpToken(), disabled: mcpActionBusyId === 'create', title: "Generate Token", "aria-label": "Generate Token", children: [mcpActionBusyId === 'create' ? _jsx(Loader2, { size: 14, className: styles.spinIcon }) : _jsx(Zap, { size: 14 }), _jsx("span", { children: "GENERATE TOKEN" })] }), _jsx("button", { className: `${styles.copyBtn} ${copiedConfig === 'full' ? styles.copyBtnActive : ''}`, onClick: handleCopyCloudMcpConfig, title: "Copy full configuration file", children: copiedConfig === 'full' ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) })] })] }), _jsx("pre", { className: styles.codeBlock, children: _jsx("code", { children: cloudMcpServerConfig
908
+ }, children: _jsx("span", { className: styles.mcpTokenValueText, children: _jsx("code", { children: displayedGeneratedToken }) }) })] }));
909
+ })(), _jsx("div", { className: styles.mcpTokenActionRow, children: _jsxs("button", { className: styles.copyBtn, onClick: () => void handleCreateMcpToken(), disabled: mcpActionBusyId === 'create', title: "Generate Token", "aria-label": "Generate Token", children: [mcpActionBusyId === 'create' ? _jsx(Loader2, { size: 14, className: styles.spinIcon }) : _jsx(Zap, { size: 14 }), _jsx("span", { children: "GENERATE TOKEN" })] }) })] }), _jsxs("div", { className: styles.settingGroup, children: [_jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom} ${styles.marginTop12}`, style: { minHeight: '260px' }, children: [_jsxs("div", { className: styles.codeHeader, children: [_jsx("span", { className: styles.codeLabel, children: "Cloud MCP Configuration" }), _jsxs("div", { className: styles.codeHeaderActions, children: [_jsx("select", { className: styles.settingInput, value: selectedMcpClient, onChange: (e) => setSelectedMcpClient(e.target.value), "aria-label": "MCP client", style: { width: 'auto', minWidth: '170px', padding: '6px 12px', fontSize: '12px', borderRadius: '8px' }, children: MCP_CLIENT_OPTIONS.map((option) => (_jsx("option", { value: option.id, disabled: option.disabled, children: option.label }, option.id))) }), _jsx("button", { className: `${styles.copyBtn} ${copiedConfig === 'full' ? styles.copyBtnActive : ''}`, onClick: handleCopyCloudMcpConfig, title: "Copy full configuration file", children: copiedConfig === 'full' ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) })] })] }), _jsx("pre", { className: styles.codeBlock, children: _jsx("code", { children: cloudMcpServerConfig
863
910
  ? renderMcpConfigBlock(cloudMcpServerId, cloudMcpServerConfig, cloudClientIntegration.rootKey || 'mcpServers', cloudClientIntegration.wrapperKeys || [])
864
- : renderTextConfigBlock(cloudClientIntegration.renderedText || '') }) })] }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: cloudClientIntegration.instructionLabel }) }), _jsx("div", { style: { padding: '16px 20px' }, children: _jsx("div", { className: styles.settingDescription, children: cloudClientIntegration.instructionText }) })] })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => setShowMcpConnectionTest((prev) => !prev), onKeyDown: (e) => {
911
+ : renderTextConfigBlock(cloudClientIntegration.renderedText || '') }) })] }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: cloudClientIntegration.instructionLabel }) }), _jsx("div", { style: { padding: '16px 20px' }, children: _jsx("div", { className: styles.settingDescription, children: cloudClientIntegration.instructionText }) })] })] }), _jsxs("div", { className: styles.settingGroup, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => setShowMcpConnectionTest((prev) => !prev), onKeyDown: (e) => {
865
912
  if (e.key === 'Enter' || e.key === ' ') {
866
913
  e.preventDefault();
867
914
  setShowMcpConnectionTest((prev) => !prev);
@@ -872,7 +919,7 @@ export function TaskSettings(props) {
872
919
  } })] }), _jsxs("div", { className: styles.marginTop12, style: { display: 'flex', gap: '8px', flexWrap: 'wrap', justifyContent: 'flex-end' }, children: [generatedTokenValue && (_jsx("button", { className: styles.copyBtn, onClick: () => {
873
920
  setMcpConnectionToken(generatedTokenValue);
874
921
  setMcpConnectionResult(null);
875
- }, children: "Use Generated Token" })), _jsxs("button", { className: styles.copyBtn, onClick: () => void handleTestMcpConnection(), disabled: mcpConnectionBusy, children: [mcpConnectionBusy ? _jsx(Loader2, { size: 14, className: styles.spinIcon }) : _jsx(Zap, { size: 14 }), "Test Connection"] })] }), mcpConnectionResult && (_jsxs("div", { className: `${mcpConnectionResult.type === 'success' ? styles.successMessage : styles.errorMessage} ${styles.marginTop12}`, children: [mcpConnectionResult.type === 'success' ? _jsx(Check, { size: 14 }) : _jsx(AlertTriangle, { size: 14 }), _jsxs("div", { children: [_jsx("div", { children: mcpConnectionResult.message }), mcpConnectionResult.type === 'success' && (mcpConnectionResult.workspaceName || mcpConnectionResult.workspaceId) && (_jsxs("div", { className: styles.settingHelper, children: ["Workspace: ", mcpConnectionResult.workspaceName || mcpConnectionResult.workspaceId, mcpConnectionResult.workspaceId ? ` (${mcpConnectionResult.workspaceId})` : ''] })), mcpConnectionResult.type === 'success' && Array.isArray(mcpConnectionResult.scopes) && mcpConnectionResult.scopes.length > 0 && (_jsxs("div", { className: styles.settingHelper, children: ["Granted scopes: ", mcpConnectionResult.scopes.join(', ')] }))] })] }))] }))] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => setShowMcpTokensList((prev) => !prev), onKeyDown: (e) => {
922
+ }, children: "Use Generated Token" })), _jsxs("button", { className: styles.copyBtn, onClick: () => void handleTestMcpConnection(), disabled: mcpConnectionBusy, children: [mcpConnectionBusy ? _jsx(Loader2, { size: 14, className: styles.spinIcon }) : _jsx(Zap, { size: 14 }), "Test Connection"] })] }), mcpConnectionResult && (_jsxs("div", { className: `${mcpConnectionResult.type === 'success' ? styles.successMessage : styles.errorMessage} ${styles.marginTop12}`, children: [mcpConnectionResult.type === 'success' ? _jsx(Check, { size: 14 }) : _jsx(AlertTriangle, { size: 14 }), _jsxs("div", { children: [_jsx("div", { children: mcpConnectionResult.message }), mcpConnectionResult.type === 'success' && (mcpConnectionResult.workspaceName || mcpConnectionResult.workspaceId) && (_jsxs("div", { className: styles.settingHelper, children: ["Workspace: ", mcpConnectionResult.workspaceName || mcpConnectionResult.workspaceId, mcpConnectionResult.workspaceId ? ` (${mcpConnectionResult.workspaceId})` : ''] })), mcpConnectionResult.type === 'success' && Array.isArray(mcpConnectionResult.scopes) && mcpConnectionResult.scopes.length > 0 && (_jsxs("div", { className: styles.settingHelper, children: ["Granted scopes: ", mcpConnectionResult.scopes.join(', ')] })), mcpConnectionResult.endpoint && (_jsxs("div", { className: styles.settingHelper, children: ["Endpoint: ", mcpConnectionResult.endpoint] })), mcpConnectionResult.type === 'error' && mcpConnectionResult.errorCode && (_jsxs("div", { className: styles.settingHelper, children: ["Error code: ", mcpConnectionResult.errorCode] })), mcpConnectionResult.type === 'error' && mcpConnectionResult.followup && (_jsx("div", { className: styles.settingHelper, children: mcpConnectionResult.followup }))] })] }))] }))] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => setShowMcpTokensList((prev) => !prev), onKeyDown: (e) => {
876
923
  if (e.key === 'Enter' || e.key === ' ') {
877
924
  e.preventDefault();
878
925
  setShowMcpTokensList((prev) => !prev);
@@ -882,10 +929,9 @@ export function TaskSettings(props) {
882
929
  borderRadius: '12px',
883
930
  border: '1px solid var(--border-primary, rgba(255, 255, 255, 0.1))',
884
931
  background: 'rgba(255, 255, 255, 0.03)'
885
- }, children: [_jsxs("div", { className: styles.settingLabelGroup, children: [_jsxs("div", { className: styles.labelGroupFlex, children: [_jsx("label", { className: styles.settingLabel, children: token.name }), _jsx("span", { className: `${styles.configBadge} ${token.status === 'revoked' ? styles.configBadgeRevoked : token.status === 'expired' ? styles.configBadgeExpired : styles.configBadgeActive}`, children: token.status })] }), _jsx("span", { className: styles.settingDescription, style: { fontFamily: '"JetBrains Mono", "Fira Code", monospace' }, children: token.tokenPrefix }), _jsxs("span", { className: styles.settingDescription, children: ["Scopes: ", token.scopes.join(', ')] }), _jsxs("span", { className: styles.settingDescription, children: ["Created ", new Date(token.createdAt).toLocaleDateString(), token.lastUsedAt ? ` · Last used ${new Date(token.lastUsedAt).toLocaleString()}` : ' · Never used'] })] }), _jsxs("div", { style: { display: 'flex', gap: '8px', flexWrap: 'wrap', justifyContent: 'flex-end' }, children: [token.status !== 'revoked' && (_jsx("button", { className: styles.copyBtn, onClick: () => void handleMcpTokenAction(token.id, 'regenerate'), disabled: mcpActionBusyId === `regenerate:${token.id}`, children: "Regenerate" })), token.status === 'active' && (_jsx("button", { className: styles.copyBtn, onClick: () => void handleMcpTokenAction(token.id, 'revoke'), disabled: mcpActionBusyId === `revoke:${token.id}`, children: "Revoke" }))] })] }, token.id))) }))] }))] })] })) })), showLocalMcp && (_jsxs(_Fragment, { children: [_jsx("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: _jsxs("div", { className: styles.settingLabelGroup, children: [_jsx("h3", { className: styles.settingTitle, children: "Local MCP" }), _jsx("span", { className: styles.settingDescription, children: "Local MCP runs over stdio and does not require a cloud token. Use this for Codex, Claude Code CLI, Gemini CLI, and similar local MCP clients." })] }) }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom}`, children: [_jsxs("div", { className: styles.codeHeader, children: [_jsx("span", { className: styles.codeLabel, children: "Local MCP Configuration" }), _jsx("div", { className: styles.codeHeaderActions, children: _jsx("button", { className: `${styles.copyBtn} ${copiedConfig === 'full' ? styles.copyBtnActive : ''}`, onClick: () => handleCopyConfig('full'), title: "Copy full configuration file", children: copiedConfig === 'full' ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) }) })] }), _jsx("pre", { className: styles.codeBlock, children: _jsx("code", { children: (() => {
886
- const { serverId, serverConfig } = buildMcpServerConfig();
887
- return renderMcpConfigBlock(serverId, serverConfig);
888
- })() }) })] }), _jsx("div", { className: `${styles.settingGroup} ${styles.marginTop12}`, children: _jsxs("div", { className: styles.toggleHeading, onClick: () => setShowAdvancedMcp((prev) => !prev), onKeyDown: (e) => {
932
+ }, children: [_jsxs("div", { className: styles.settingLabelGroup, children: [_jsxs("div", { className: styles.labelGroupFlex, children: [_jsx("label", { className: styles.settingLabel, children: token.name }), _jsx("span", { className: `${styles.configBadge} ${token.status === 'revoked' ? styles.configBadgeRevoked : token.status === 'expired' ? styles.configBadgeExpired : styles.configBadgeActive}`, children: token.status })] }), _jsx("span", { className: styles.settingDescription, style: { fontFamily: '"JetBrains Mono", "Fira Code", monospace' }, children: token.tokenPrefix }), _jsxs("span", { className: styles.settingDescription, children: ["Scopes: ", token.scopes.join(', ')] }), _jsxs("span", { className: styles.settingDescription, children: ["Created ", new Date(token.createdAt).toLocaleDateString(), token.lastUsedAt ? ` · Last used ${new Date(token.lastUsedAt).toLocaleString()}` : ' · Never used'] })] }), _jsxs("div", { style: { display: 'flex', gap: '8px', flexWrap: 'wrap', justifyContent: 'flex-end' }, children: [token.status !== 'revoked' && (_jsx("button", { className: styles.copyBtn, onClick: () => void handleMcpTokenAction(token.id, 'regenerate'), disabled: mcpActionBusyId === `regenerate:${token.id}`, children: "Regenerate" })), token.status === 'active' && (_jsx("button", { className: styles.copyBtn, onClick: () => void handleMcpTokenAction(token.id, 'revoke'), disabled: mcpActionBusyId === `revoke:${token.id}`, children: "Revoke" }))] })] }, token.id))) }))] }))] })] })) })), showLocalMcp && (_jsxs(_Fragment, { children: [_jsx("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: _jsxs("div", { className: styles.settingLabelGroup, children: [_jsx("h3", { className: styles.settingTitle, children: "Local MCP" }), _jsx("span", { className: styles.settingDescription, children: "Local MCP runs over stdio and does not require a cloud token. Use this for Codex, Claude Code CLI, Gemini CLI, and similar local MCP clients." })] }) }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom}`, children: [_jsxs("div", { className: styles.codeHeader, children: [_jsx("span", { className: styles.codeLabel, children: "Local MCP Configuration" }), _jsxs("div", { className: styles.codeHeaderActions, children: [_jsx("select", { className: styles.settingInput, value: selectedMcpClient, onChange: (e) => setSelectedMcpClient(e.target.value), "aria-label": "MCP client", style: { width: 'auto', minWidth: '170px', padding: '6px 12px', fontSize: '12px', borderRadius: '8px' }, children: MCP_CLIENT_OPTIONS.map((option) => (_jsx("option", { value: option.id, disabled: option.disabled, children: option.label }, option.id))) }), _jsx("button", { className: `${styles.copyBtn} ${copiedConfig === 'full' ? styles.copyBtnActive : ''}`, onClick: () => handleCopyConfig('full'), title: "Copy full configuration file", children: copiedConfig === 'full' ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) })] })] }), _jsx("pre", { className: styles.codeBlock, children: _jsx("code", { children: localMcpServerConfig
933
+ ? renderMcpConfigBlock(localMcpServer.serverId, localMcpServerConfig, localClientIntegration.rootKey || 'mcpServers', localClientIntegration.wrapperKeys || [])
934
+ : renderTextConfigBlock(localClientIntegration.renderedText || '') }) })] }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: localClientIntegration.instructionLabel }) }), _jsx("div", { style: { padding: '16px 20px' }, children: _jsx("div", { className: styles.settingDescription, children: localClientIntegration.instructionText }) })] }), _jsx("div", { className: `${styles.settingGroup} ${styles.marginTop12}`, children: _jsxs("div", { className: styles.toggleHeading, onClick: () => setShowAdvancedMcp((prev) => !prev), onKeyDown: (e) => {
889
935
  if (e.key === 'Enter' || e.key === ' ') {
890
936
  e.preventDefault();
891
937
  setShowAdvancedMcp((prev) => !prev);