@taskforcehq/taskforce 0.3.303 → 0.3.304

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/Taskforce.module.css +102 -5
  2. package/dist/TaskforceCore.js +514 -212
  3. package/dist/TaskforceCore.test.js +1172 -133
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  21. package/dist/components/task/TaskStatusActions.js +8 -3
  22. package/dist/components/task/TaskStatusActions.test.js +9 -0
  23. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  24. package/dist/components/task/taskKanbanDropRules.js +187 -0
  25. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  26. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  27. package/dist/components/ui/Modal.d.ts +1 -1
  28. package/dist/components/ui/Modal.js +1 -0
  29. package/dist/components/views/AppShellHeader.js +5 -1
  30. package/dist/components/views/AppShellHeader.test.js +4 -0
  31. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  32. package/dist/components/views/PlanComparisonPage.js +4 -2
  33. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  34. package/dist/components/views/PlansPage.d.ts +5 -1
  35. package/dist/components/views/PlansPage.js +39 -12
  36. package/dist/components/views/PlansPage.test.js +59 -2
  37. package/dist/components/views/StandaloneLayout.js +113 -38
  38. package/dist/components/views/WidgetView.js +11 -2
  39. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  40. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  41. package/dist/core/PlanEntitlementService.d.ts +3 -1
  42. package/dist/core/PlanEntitlementService.js +61 -11
  43. package/dist/core/SignupMonetizationSettings.test.js +37 -2
  44. package/dist/core/TaskActivityService.d.ts +4 -1
  45. package/dist/core/TaskActivityService.js +45 -0
  46. package/dist/core/TaskAuditTimeline.test.js +52 -1
  47. package/dist/core/Taskforce.d.ts +15 -11
  48. package/dist/core/Taskforce.js +43 -2
  49. package/dist/core/WorkspacePlanMode.test.js +65 -0
  50. package/dist/documentReviews/service.d.ts +1 -1
  51. package/dist/documentReviews/types.d.ts +1 -1
  52. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  53. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  54. package/dist/hooks/sync/auth.d.ts +37 -0
  55. package/dist/hooks/sync/auth.js +61 -0
  56. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  57. package/dist/hooks/sync/bootstrap.js +52 -0
  58. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  59. package/dist/hooks/sync/orchestratorShared.js +209 -0
  60. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  61. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  62. package/dist/hooks/sync/realtime.d.ts +25 -0
  63. package/dist/hooks/sync/realtime.js +60 -0
  64. package/dist/hooks/sync/recovery.d.ts +69 -0
  65. package/dist/hooks/sync/recovery.js +118 -0
  66. package/dist/hooks/sync/transfers.d.ts +122 -0
  67. package/dist/hooks/sync/transfers.js +431 -0
  68. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  69. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  70. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  71. package/dist/hooks/ui/useResourceExport.js +5 -2
  72. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  73. package/dist/hooks/useRealtimeSync.js +2 -1
  74. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  75. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  76. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  77. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  78. package/dist/hooks/useSyncOrchestrator.js +314 -873
  79. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
  80. package/dist/hooks/useTaskMutations.js +45 -6
  81. package/dist/hooks/useTaskMutations.test.js +147 -0
  82. package/dist/hooks/useTaskforce.d.ts +8 -2
  83. package/dist/hooks/useTaskforce.js +285 -812
  84. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  85. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  86. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  87. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  88. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  89. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  90. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  91. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  92. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  93. package/dist/localization/locales/en-US.js +2 -2
  94. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  95. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  96. package/dist/mcp/clientIntegrations.d.ts +12 -1
  97. package/dist/mcp/clientIntegrations.js +161 -6
  98. package/dist/mcp/clientIntegrations.test.js +76 -4
  99. package/dist/mcp/documentHelpers.d.ts +100 -0
  100. package/dist/mcp/documentHelpers.js +161 -0
  101. package/dist/mcp/runtime.js +174 -476
  102. package/dist/mcp/runtime.test.js +81 -8
  103. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  104. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  105. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  106. package/dist/mcp/taskAttachmentTypes.js +1 -0
  107. package/dist/plugins/vite.js +1 -1
  108. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  109. package/dist/runtime/appGateDefinitions.js +1 -1
  110. package/dist/server/index.d.ts +1 -0
  111. package/dist/server/index.js +46 -3
  112. package/dist/server/index.test.js +12 -1
  113. package/dist/server/routes/admin.js +13 -19
  114. package/dist/server/routes/auth.js +37 -2
  115. package/dist/server/routes/billing.js +18 -67
  116. package/dist/server/routes/billing.test.js +43 -0
  117. package/dist/server/routes/shared.d.ts +30 -0
  118. package/dist/server/routes/shared.js +26 -0
  119. package/dist/server/routes/sync.integration.test.js +20 -0
  120. package/dist/server/routes/sync.js +110 -237
  121. package/dist/server/routes/tasks.js +49 -7
  122. package/dist/server/routes.js +8 -19
  123. package/dist/server/routes.test.js +243 -5
  124. package/dist/shared/taskActor.d.ts +10 -0
  125. package/dist/shared/taskActor.js +1 -0
  126. package/dist/sync/collaborationSyncPayload.js +4 -0
  127. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  128. package/dist/sync/collaborationSyncState.d.ts +3 -1
  129. package/dist/sync/collaborationSyncState.js +19 -0
  130. package/dist/sync/contentSyncState.js +12 -10
  131. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  132. package/dist/sync/syncApplyHandlers.js +370 -8
  133. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  134. package/dist/sync/syncApplyHandlers.test.js +377 -0
  135. package/dist/sync/syncService.d.ts +8 -1
  136. package/dist/sync/syncService.js +36 -2
  137. package/dist/sync/syncService.test.js +48 -0
  138. package/dist/sync/workspacePullFeed.d.ts +1 -0
  139. package/dist/sync/workspacePullFeed.js +66 -59
  140. package/dist/sync/workspacePullFeed.test.js +72 -4
  141. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  142. package/dist/sync/workspaceSyncModel.js +65 -0
  143. package/dist/sync/workspaceSyncModel.test.js +119 -0
  144. package/dist/types.d.ts +2 -10
  145. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  146. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  147. package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  148. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-BjR6iBf9.js} +1 -1
  149. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  150. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  151. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  152. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-DnFqdUME.js} +1 -1
  153. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  154. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  155. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
  156. package/dist/ui/index.html +4 -4
  157. package/dist/utils/taskActivity.js +4 -0
  158. package/dist/utils/taskActivity.test.js +25 -1
  159. package/package.json +3 -1
  160. package/scripts/playwright-auth.sh +4 -0
  161. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  162. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  163. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  164. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  165. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -148,6 +148,14 @@ export async function buildWorkspacePullFeed(input) {
148
148
  sessionId: String(session.id || '').trim()
149
149
  }))
150
150
  .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sessionId.length > 0);
151
+ const taskEventEntries = collaborationSyncState.listWorkspaceSyncTaskEventsSnapshot(workspaceId)
152
+ .map((event) => ({
153
+ kind: 'task-event',
154
+ watermark: String(event.createdAt || '').trim(),
155
+ sortId: `task-event:${String(event.id || '').trim()}`,
156
+ event
157
+ }))
158
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.event.id.length > 0 && entry.event.taskId.length > 0);
151
159
  const assetIndex = await contentSyncState.listWorkspaceSyncBinaryAssetIndex(workspaceId);
152
160
  const assetEntries = assetIndex
153
161
  .map((asset) => ({
@@ -190,6 +198,7 @@ export async function buildWorkspacePullFeed(input) {
190
198
  ...documentReviewCommentDeleteEntries,
191
199
  ...annotatedAttachmentSessionEntries,
192
200
  ...annotatedAttachmentSessionDeleteEntries,
201
+ ...taskEventEntries,
193
202
  ...assetEntries,
194
203
  ...assetDeleteEntries
195
204
  ];
@@ -238,16 +247,11 @@ export async function buildWorkspacePullFeed(input) {
238
247
  }
239
248
  }));
240
249
  const hydrationMs = Date.now() - hydrationStartedAtMs;
241
- const effectivePage = [];
242
- for (const entry of page) {
243
- if (entry.kind === 'document' && !hydratedDocuments.get(entry.path))
244
- break;
245
- if (entry.kind === 'asset' && !hydratedAssets.get(entry.path))
246
- break;
247
- effectivePage.push(entry);
248
- }
249
- const hasMore = afterCursor.length > effectivePage.length;
250
- const last = effectivePage.length > 0 ? effectivePage[effectivePage.length - 1] : null;
250
+ const skippedHydrationEntries = page.filter((entry) => ((entry.kind === 'document' && !hydratedDocuments.get(entry.path))
251
+ || (entry.kind === 'asset' && !hydratedAssets.get(entry.path)))).length;
252
+ const effectivePage = page;
253
+ const hasMore = afterCursor.length > page.length;
254
+ const last = page.length > 0 ? page[page.length - 1] : null;
251
255
  const nextCursor = encodeWorkspaceSyncCursor(last
252
256
  ? { u: last.watermark, id: String(last.sortId || '') }
253
257
  : cursor);
@@ -280,55 +284,57 @@ export async function buildWorkspacePullFeed(input) {
280
284
  ? [{ op: 'annotated-attachment-session-upsert', session: entry.session, watermark: entry.watermark }]
281
285
  : entry.kind === 'annotated-attachment-session-delete'
282
286
  ? [{ op: 'annotated-attachment-session-delete', sessionId: entry.sessionId, deletedAt: entry.watermark, watermark: entry.watermark }]
283
- : entry.kind === 'document'
284
- ? (() => {
285
- const payload = hydratedDocuments.get(entry.path);
286
- if (!payload)
287
- return [];
288
- return [{
289
- op: 'document-upsert',
290
- path: entry.path,
291
- updatedAt: entry.watermark,
292
- content: payload.content,
293
- assetId: payload.assetId,
294
- documentId: payload.documentId,
295
- referenceNumber: payload.referenceNumber,
296
- version: payload.version,
297
- taskId: payload.taskId,
298
- logicalName: payload.logicalName,
299
- caption: payload.caption,
300
- originalFilename: payload.originalFilename,
301
- linkRole: payload.linkRole === 'reference' ? 'reference' : 'attachment',
302
- watermark: entry.watermark
303
- }];
304
- })()
305
- : entry.kind === 'document-delete'
306
- ? [{ op: 'document-delete', path: entry.path, assetId: entry.assetId, deletedAt: entry.watermark, watermark: entry.watermark }]
307
- : entry.kind === 'asset'
308
- ? (() => {
309
- const payload = hydratedAssets.get(entry.path);
310
- if (!payload)
311
- return [];
312
- return [{
313
- op: 'asset-upsert',
314
- path: entry.path,
315
- updatedAt: entry.watermark,
316
- contentBase64: payload.contentBase64,
317
- assetId: payload.assetId,
318
- kind: payload.kind,
319
- mimeType: payload.mimeType,
320
- referenceNumber: payload.referenceNumber,
321
- taskId: payload.taskId,
322
- logicalName: payload.logicalName,
323
- caption: payload.caption,
324
- originalFilename: payload.originalFilename,
325
- linkRole: payload.linkRole,
326
- watermark: entry.watermark
327
- }];
328
- })()
329
- : entry.kind === 'asset-delete'
330
- ? [{ op: 'asset-delete', path: entry.path, assetId: entry.assetId, deletedAt: entry.watermark, watermark: entry.watermark }]
331
- : [{ op: 'upsert', task: entry.task, archived: Boolean(entry.archived), watermark: entry.watermark }]));
287
+ : entry.kind === 'task-event'
288
+ ? [{ op: 'task-event-upsert', event: entry.event, watermark: entry.watermark }]
289
+ : entry.kind === 'document'
290
+ ? (() => {
291
+ const payload = hydratedDocuments.get(entry.path);
292
+ if (!payload)
293
+ return [];
294
+ return [{
295
+ op: 'document-upsert',
296
+ path: entry.path,
297
+ updatedAt: entry.watermark,
298
+ content: payload.content,
299
+ assetId: payload.assetId,
300
+ documentId: payload.documentId,
301
+ referenceNumber: payload.referenceNumber,
302
+ version: payload.version,
303
+ taskId: payload.taskId,
304
+ logicalName: payload.logicalName,
305
+ caption: payload.caption,
306
+ originalFilename: payload.originalFilename,
307
+ linkRole: payload.linkRole === 'reference' ? 'reference' : 'attachment',
308
+ watermark: entry.watermark
309
+ }];
310
+ })()
311
+ : entry.kind === 'document-delete'
312
+ ? [{ op: 'document-delete', path: entry.path, assetId: entry.assetId, deletedAt: entry.watermark, watermark: entry.watermark }]
313
+ : entry.kind === 'asset'
314
+ ? (() => {
315
+ const payload = hydratedAssets.get(entry.path);
316
+ if (!payload)
317
+ return [];
318
+ return [{
319
+ op: 'asset-upsert',
320
+ path: entry.path,
321
+ updatedAt: entry.watermark,
322
+ contentBase64: payload.contentBase64,
323
+ assetId: payload.assetId,
324
+ kind: payload.kind,
325
+ mimeType: payload.mimeType,
326
+ referenceNumber: payload.referenceNumber,
327
+ taskId: payload.taskId,
328
+ logicalName: payload.logicalName,
329
+ caption: payload.caption,
330
+ originalFilename: payload.originalFilename,
331
+ linkRole: payload.linkRole,
332
+ watermark: entry.watermark
333
+ }];
334
+ })()
335
+ : entry.kind === 'asset-delete'
336
+ ? [{ op: 'asset-delete', path: entry.path, assetId: entry.assetId, deletedAt: entry.watermark, watermark: entry.watermark }]
337
+ : [{ op: 'upsert', task: entry.task, archived: Boolean(entry.archived), watermark: entry.watermark }]));
332
338
  const encodeMs = Date.now() - encodeStartedAtMs;
333
339
  return {
334
340
  workspaceMembers,
@@ -338,6 +344,7 @@ export async function buildWorkspacePullFeed(input) {
338
344
  diagnostics: {
339
345
  totalEntries: all.length,
340
346
  pageEntries: page.length,
347
+ skippedHydrationEntries,
341
348
  documentIndexCount: documentIndex.length,
342
349
  assetIndexCount: assetIndex.length,
343
350
  indexMs,
@@ -29,6 +29,7 @@ function createCollaborationSyncStateMock() {
29
29
  listWorkspaceSyncDocumentReviewSessionsSnapshot: () => [],
30
30
  listWorkspaceSyncDocumentReviewCommentsSnapshot: () => [],
31
31
  listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot: () => [],
32
+ listWorkspaceSyncTaskEventsSnapshot: () => [],
32
33
  };
33
34
  }
34
35
  function encodeCursor(cursor) {
@@ -234,9 +235,22 @@ describe('workspacePullFeed', () => {
234
235
  expect(loadedDocs).toEqual(['docs/a.md']);
235
236
  expect(loadedAssets).toEqual(['workspaces/ws/assets/images/a.png']);
236
237
  });
237
- it('does not advance the cursor past a hydration miss', async () => {
238
+ it('skips hydration misses without blocking later feed progress', async () => {
238
239
  const core = createCoreMock();
239
240
  const content = createContentSyncStateMock();
241
+ core.listTasksForSync = () => [
242
+ {
243
+ archived: false,
244
+ task: {
245
+ id: 'task-after-miss',
246
+ title: 'Task after miss',
247
+ status: 'task',
248
+ category: 'general',
249
+ type: 'task',
250
+ createdAt: '2026-04-12T10:00:00.000Z',
251
+ }
252
+ }
253
+ ];
240
254
  content.listWorkspaceSyncDocumentIndex = async () => [
241
255
  {
242
256
  path: 'docs/a.md',
@@ -260,7 +274,7 @@ describe('workspacePullFeed', () => {
260
274
  core,
261
275
  workspaceId: 'workspace-4',
262
276
  cursor: null,
263
- pageSize: 2,
277
+ pageSize: 3,
264
278
  encodeWorkspaceSyncCursor: encodeCursor,
265
279
  contentSyncState: content,
266
280
  collaborationSyncState: createCollaborationSyncStateMock(),
@@ -270,9 +284,63 @@ describe('workspacePullFeed', () => {
270
284
  op: 'document-upsert',
271
285
  path: 'docs/a.md',
272
286
  content: 'content:docs/a.md'
287
+ }),
288
+ expect.objectContaining({
289
+ op: 'upsert',
290
+ task: expect.objectContaining({
291
+ id: 'task-after-miss'
292
+ })
293
+ })
294
+ ]);
295
+ expect(result.nextCursor).toBe('2026-04-12T10:00:00.000Z|task-after-miss');
296
+ expect(result.hasMore).toBe(false);
297
+ expect(result.diagnostics.skippedHydrationEntries).toBe(1);
298
+ });
299
+ it('includes task-event upserts in the pull feed ordered by event watermark', async () => {
300
+ const core = createCoreMock();
301
+ const collaboration = createCollaborationSyncStateMock();
302
+ collaboration.listWorkspaceSyncTaskEventsSnapshot = () => [
303
+ {
304
+ id: 'task-event-2',
305
+ taskId: 'task-2',
306
+ workspaceId: 'workspace-5',
307
+ action: 'task-archived',
308
+ actor: 'ai-profile-2',
309
+ actorType: 'ai',
310
+ createdAt: '2026-04-12T10:06:00.000Z',
311
+ details: {}
312
+ },
313
+ {
314
+ id: 'task-event-1',
315
+ taskId: 'task-1',
316
+ workspaceId: 'workspace-5',
317
+ action: 'task-status-changed',
318
+ actor: 'ai-profile-1',
319
+ actorType: 'ai',
320
+ createdAt: '2026-04-12T10:05:00.000Z',
321
+ details: {}
322
+ }
323
+ ];
324
+ const result = await buildWorkspacePullFeed({
325
+ core,
326
+ workspaceId: 'workspace-5',
327
+ cursor: null,
328
+ pageSize: 5,
329
+ encodeWorkspaceSyncCursor: encodeCursor,
330
+ contentSyncState: createContentSyncStateMock(),
331
+ collaborationSyncState: collaboration,
332
+ });
333
+ expect(result.changes).toEqual([
334
+ expect.objectContaining({
335
+ op: 'task-event-upsert',
336
+ watermark: '2026-04-12T10:05:00.000Z',
337
+ event: expect.objectContaining({ id: 'task-event-1' })
338
+ }),
339
+ expect.objectContaining({
340
+ op: 'task-event-upsert',
341
+ watermark: '2026-04-12T10:06:00.000Z',
342
+ event: expect.objectContaining({ id: 'task-event-2' })
273
343
  })
274
344
  ]);
275
- expect(result.nextCursor).toBe('2026-04-10T10:00:00.000Z|doc:docs/a.md');
276
- expect(result.hasMore).toBe(true);
277
345
  });
278
346
  });
@@ -1,10 +1,20 @@
1
- import type { InitiativeItem, TaskItem, WorkstreamItem } from '../types';
1
+ import type { InitiativeItem, TaskEvent, TaskItem, WorkstreamItem } from '../types';
2
2
  import type { DocumentReviewCommentRecord, DocumentReviewSessionStatus } from '../documentReviews/types.js';
3
3
  import type { AnnotatedAttachmentAnnotation } from '../annotatedAttachments/types.js';
4
4
  import type { WorkspaceTaxonomyState } from '../shared/taxonomyState.js';
5
5
  import { type AiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
6
6
  import { type TaskSyncPayload } from './taskSyncPayload.js';
7
7
  export type WorkspaceSyncTaskSnapshot = TaskSyncPayload;
8
+ export interface WorkspaceSyncTaskEventSnapshot {
9
+ id: string;
10
+ taskId: string;
11
+ workspaceId: string;
12
+ action: string;
13
+ actor: string;
14
+ actorType: 'system' | 'human' | 'ai';
15
+ details?: TaskEvent['details'];
16
+ createdAt: string;
17
+ }
8
18
  export interface WorkspaceSyncAiProfileSnapshot {
9
19
  id: string;
10
20
  workspaceId: string;
@@ -13,6 +23,10 @@ export interface WorkspaceSyncAiProfileSnapshot {
13
23
  username: string;
14
24
  icon: string;
15
25
  color: string;
26
+ description?: string | null;
27
+ role?: string | null;
28
+ provider?: string | null;
29
+ model?: string | null;
16
30
  surfaceType?: AiProfileSurfaceType | null;
17
31
  providerMetadata?: Record<string, unknown> | null;
18
32
  createdAt: string;
@@ -177,6 +191,9 @@ export type WorkspaceSyncChange = {
177
191
  op: 'annotated-attachment-session-delete';
178
192
  sessionId: string;
179
193
  deletedAt?: string;
194
+ } | {
195
+ op: 'task-event-upsert';
196
+ event: WorkspaceSyncTaskEventSnapshot;
180
197
  };
181
198
  export interface WorkspaceSyncPayload {
182
199
  changes: WorkspaceSyncChange[];
@@ -203,6 +220,7 @@ export interface WorkspaceSyncPayload {
203
220
  currentAnnotatedAttachmentSessionIds: Set<string>;
204
221
  deleteAnnotatedAttachmentSessionIds: Set<string>;
205
222
  currentAnnotatedAttachmentSessionWatermarks: Map<string, string>;
223
+ currentTaskEventWatermarks: Map<string, string>;
206
224
  /** Watermarks for every task upsert included in this payload (id → updatedAt|createdAt). */
207
225
  pushedWatermarks: Map<string, string>;
208
226
  }
@@ -250,6 +268,7 @@ export declare function buildWorkspaceSyncPayloadModel(input: {
250
268
  documentReviewSessions?: WorkspaceSyncDocumentReviewSessionSnapshot[];
251
269
  documentReviewComments?: WorkspaceSyncDocumentReviewCommentSnapshot[];
252
270
  annotatedAttachmentSessions?: WorkspaceSyncAnnotatedAttachmentSessionSnapshot[];
271
+ taskEvents?: WorkspaceSyncTaskEventSnapshot[];
253
272
  lastPushedInitiativeIds?: Set<string>;
254
273
  lastPushedInitiativeWatermarks?: Map<string, string>;
255
274
  lastPushedWorkstreamIds?: Set<string>;
@@ -266,6 +285,7 @@ export declare function buildWorkspaceSyncPayloadModel(input: {
266
285
  lastPushedDocumentReviewCommentWatermarks?: Map<string, string>;
267
286
  lastPushedAnnotatedAttachmentSessionIds?: Set<string>;
268
287
  lastPushedAnnotatedAttachmentSessionWatermarks?: Map<string, string>;
288
+ lastPushedTaskEventWatermarks?: Map<string, string>;
269
289
  /**
270
290
  * Watermark (updatedAt|createdAt) of each task as of its last successful push.
271
291
  * When provided, only tasks whose watermark differs from this map are included
@@ -289,4 +309,5 @@ export declare function buildWorkspaceSyncSignatureModel(input: {
289
309
  documentReviewSessions?: WorkspaceSyncDocumentReviewSessionSnapshot[];
290
310
  documentReviewComments?: WorkspaceSyncDocumentReviewCommentSnapshot[];
291
311
  annotatedAttachmentSessions?: WorkspaceSyncAnnotatedAttachmentSessionSnapshot[];
312
+ taskEvents?: WorkspaceSyncTaskEventSnapshot[];
292
313
  }): string;
@@ -3,6 +3,16 @@ import { serializeTaskForSync } from './taskSyncPayload.js';
3
3
  export function toWorkspaceSyncTaskSnapshot(task) {
4
4
  return serializeTaskForSync(task);
5
5
  }
6
+ function sortTaskEventsForSync(taskEvents) {
7
+ return [...taskEvents].sort((left, right) => {
8
+ const leftCreatedAt = String(left?.createdAt || '').trim();
9
+ const rightCreatedAt = String(right?.createdAt || '').trim();
10
+ const watermarkDelta = leftCreatedAt.localeCompare(rightCreatedAt);
11
+ if (watermarkDelta !== 0)
12
+ return watermarkDelta;
13
+ return String(left?.id || '').trim().localeCompare(String(right?.id || '').trim());
14
+ });
15
+ }
6
16
  function sortTasksForSync(tasks) {
7
17
  return [...tasks].sort((left, right) => {
8
18
  const leftId = String(left?.id || '').trim();
@@ -175,6 +185,10 @@ export function buildWorkspaceSyncPayloadModel(input) {
175
185
  username: String(profile.username || '').trim(),
176
186
  icon: String(profile.icon || 'Bot').trim() || 'Bot',
177
187
  color: String(profile.color || '#8b5cf6').trim() || '#8b5cf6',
188
+ description: typeof profile.description === 'string' ? profile.description.trim() || null : null,
189
+ role: typeof profile.role === 'string' ? profile.role.trim() || null : null,
190
+ provider: typeof profile.provider === 'string' ? profile.provider.trim() || null : null,
191
+ model: typeof profile.model === 'string' ? profile.model.trim() || null : null,
178
192
  surfaceType: parseAiProfileSurfaceType(profile.surfaceType),
179
193
  providerMetadata: profile.providerMetadata && typeof profile.providerMetadata === 'object' && !Array.isArray(profile.providerMetadata)
180
194
  ? profile.providerMetadata
@@ -391,6 +405,40 @@ export function buildWorkspaceSyncPayloadModel(input) {
391
405
  return true;
392
406
  return previousAnnotatedAttachmentSessionWatermarks.get(change.session.id) !== change.session.updatedAt;
393
407
  });
408
+ const taskEvents = sortTaskEventsForSync(Array.isArray(input.taskEvents) ? input.taskEvents : []);
409
+ const currentTaskEventWatermarks = new Map(taskEvents
410
+ .map((event) => [String(event?.id || '').trim(), String(event?.createdAt || '').trim()])
411
+ .filter(([id]) => id.length > 0));
412
+ const previousTaskEventWatermarks = input.lastPushedTaskEventWatermarks;
413
+ const shouldSendAllTaskEvents = !previousTaskEventWatermarks || previousTaskEventWatermarks.size === 0;
414
+ const taskEventUpsertChanges = taskEvents
415
+ .map((event) => ({
416
+ op: 'task-event-upsert',
417
+ event: {
418
+ id: String(event.id || '').trim(),
419
+ taskId: String(event.taskId || '').trim(),
420
+ workspaceId: String(event.workspaceId || '').trim(),
421
+ action: String(event.action || '').trim(),
422
+ actor: String(event.actor || 'system').trim() || 'system',
423
+ actorType: event.actorType === 'ai' || event.actorType === 'human' || event.actorType === 'system'
424
+ ? event.actorType
425
+ : 'system',
426
+ details: event.details && typeof event.details === 'object' && !Array.isArray(event.details)
427
+ ? event.details
428
+ : {},
429
+ createdAt: String(event.createdAt || '').trim(),
430
+ }
431
+ }))
432
+ .filter((change) => (change.event.id.length > 0
433
+ && change.event.taskId.length > 0
434
+ && change.event.workspaceId.length > 0
435
+ && change.event.action.length > 0
436
+ && Number.isFinite(Date.parse(change.event.createdAt))))
437
+ .filter((change) => {
438
+ if (shouldSendAllTaskEvents)
439
+ return true;
440
+ return previousTaskEventWatermarks.get(change.event.id) !== change.event.createdAt;
441
+ });
394
442
  const changes = [
395
443
  ...initiativeUpsertChanges,
396
444
  ...workstreamUpsertChanges,
@@ -438,6 +486,7 @@ export function buildWorkspaceSyncPayloadModel(input) {
438
486
  sessionId,
439
487
  deletedAt: payloadBuiltAt
440
488
  })),
489
+ ...taskEventUpsertChanges,
441
490
  ...activeChanges,
442
491
  ...archivedChanges,
443
492
  ...Array.from(deleteTaskIds).map((taskId) => ({ op: 'delete', taskId }))
@@ -467,6 +516,7 @@ export function buildWorkspaceSyncPayloadModel(input) {
467
516
  currentAnnotatedAttachmentSessionIds,
468
517
  deleteAnnotatedAttachmentSessionIds,
469
518
  currentAnnotatedAttachmentSessionWatermarks,
519
+ currentTaskEventWatermarks,
470
520
  pushedWatermarks
471
521
  };
472
522
  }
@@ -516,6 +566,21 @@ export function buildWorkspaceSyncSignatureModel(input) {
516
566
  .filter((profile) => profile.id.length > 0)
517
567
  .sort((a, b) => a.id.localeCompare(b.id))
518
568
  : [],
569
+ taskEvents: Array.isArray(input.taskEvents)
570
+ ? input.taskEvents
571
+ .map((event) => ({
572
+ id: String(event.id || '').trim(),
573
+ taskId: String(event.taskId || '').trim(),
574
+ action: String(event.action || '').trim(),
575
+ actor: String(event.actor || '').trim(),
576
+ actorType: event.actorType === 'ai' || event.actorType === 'human' || event.actorType === 'system'
577
+ ? event.actorType
578
+ : 'system',
579
+ createdAt: String(event.createdAt || '').trim()
580
+ }))
581
+ .filter((event) => event.id.length > 0 && event.taskId.length > 0)
582
+ .sort((a, b) => a.id.localeCompare(b.id))
583
+ : [],
519
584
  documents: Array.isArray(input.documents)
520
585
  ? input.documents
521
586
  .map((doc) => ({
@@ -20,6 +20,82 @@ describe('workspaceSyncModel', () => {
20
20
  expect.objectContaining({ op: 'document-delete', path: 'docs/remove.md' })
21
21
  ]));
22
22
  });
23
+ it('emits task-event-upsert changes for new activity events and tracks event watermarks', () => {
24
+ const payload = buildWorkspaceSyncPayloadModel({
25
+ tasks: [],
26
+ archivedTasks: [],
27
+ taskEvents: [
28
+ {
29
+ id: 'task-event-1',
30
+ taskId: 'task-1',
31
+ workspaceId: 'workspace-a',
32
+ action: 'task-status-changed',
33
+ actor: 'ai-profile-1',
34
+ actorType: 'ai',
35
+ createdAt: '2026-04-13T10:05:00.000Z',
36
+ details: {
37
+ changes: {
38
+ status: {
39
+ from: 'in-progress',
40
+ to: 'done'
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ],
46
+ lastPushedTaskIds: new Set(),
47
+ pendingDeletedTaskIds: new Set(),
48
+ lastPushedTaskEventWatermarks: new Map()
49
+ });
50
+ expect(payload.changes).toEqual(expect.arrayContaining([
51
+ expect.objectContaining({
52
+ op: 'task-event-upsert',
53
+ event: expect.objectContaining({
54
+ id: 'task-event-1',
55
+ taskId: 'task-1',
56
+ workspaceId: 'workspace-a',
57
+ action: 'task-status-changed',
58
+ createdAt: '2026-04-13T10:05:00.000Z'
59
+ })
60
+ })
61
+ ]));
62
+ expect(payload.currentTaskEventWatermarks.get('task-event-1')).toBe('2026-04-13T10:05:00.000Z');
63
+ });
64
+ it('does not resend unchanged task events when prior task-event watermarks exist', () => {
65
+ const payload = buildWorkspaceSyncPayloadModel({
66
+ tasks: [],
67
+ archivedTasks: [],
68
+ taskEvents: [
69
+ {
70
+ id: 'task-event-keep',
71
+ taskId: 'task-1',
72
+ workspaceId: 'workspace-a',
73
+ action: 'task-status-changed',
74
+ actor: 'ai-profile-1',
75
+ actorType: 'ai',
76
+ createdAt: '2026-04-13T10:05:00.000Z'
77
+ },
78
+ {
79
+ id: 'task-event-new',
80
+ taskId: 'task-2',
81
+ workspaceId: 'workspace-a',
82
+ action: 'task-archived',
83
+ actor: 'ai-profile-1',
84
+ actorType: 'ai',
85
+ createdAt: '2026-04-13T10:06:00.000Z'
86
+ }
87
+ ],
88
+ lastPushedTaskIds: new Set(),
89
+ pendingDeletedTaskIds: new Set(),
90
+ lastPushedTaskEventWatermarks: new Map([
91
+ ['task-event-keep', '2026-04-13T10:05:00.000Z']
92
+ ])
93
+ });
94
+ const taskEventIds = payload.changes
95
+ .filter((change) => change.op === 'task-event-upsert')
96
+ .map((change) => change.event.id);
97
+ expect(taskEventIds).toEqual(['task-event-new']);
98
+ });
23
99
  it('sends only changed/new document upserts when prior document watermarks exist', () => {
24
100
  const payload = buildWorkspaceSyncPayloadModel({
25
101
  tasks: [],
@@ -162,6 +238,10 @@ describe('workspaceSyncModel', () => {
162
238
  username: 'codex',
163
239
  icon: 'Bot',
164
240
  color: '#0ea5e9',
241
+ description: 'Terminal-first coding agent',
242
+ role: 'Implementer',
243
+ provider: 'OpenAI',
244
+ model: 'gpt-5.4',
165
245
  surfaceType: 'coding_tool',
166
246
  createdAt: '2026-03-22T10:00:00.000Z',
167
247
  updatedAt: '2026-03-22T10:05:00.000Z',
@@ -175,6 +255,10 @@ describe('workspaceSyncModel', () => {
175
255
  profile: expect.objectContaining({
176
256
  id: 'ai-profile-1',
177
257
  name: 'Codex',
258
+ description: 'Terminal-first coding agent',
259
+ role: 'Implementer',
260
+ provider: 'OpenAI',
261
+ model: 'gpt-5.4',
178
262
  surfaceType: 'coding_tool'
179
263
  })
180
264
  })
@@ -197,6 +281,10 @@ describe('workspaceSyncModel', () => {
197
281
  username: 'codex',
198
282
  icon: 'Bot',
199
283
  color: '#0ea5e9',
284
+ description: 'Terminal-first coding agent',
285
+ role: 'Implementer',
286
+ provider: 'OpenAI',
287
+ model: 'gpt-5.4',
200
288
  surfaceType: 'coding_tool',
201
289
  createdAt: '2026-03-22T10:00:00.000Z',
202
290
  updatedAt: '2026-03-22T10:05:00.000Z',
@@ -681,6 +769,37 @@ describe('workspaceSyncModel', () => {
681
769
  });
682
770
  expect(changedSignature).not.toEqual(baseSignature);
683
771
  });
772
+ it('changes when task activity changes', () => {
773
+ const baseSignature = buildWorkspaceSyncSignatureModel({
774
+ workspaceId: 'ws-1',
775
+ tasks: [],
776
+ archivedTasks: [],
777
+ taskEvents: [{
778
+ id: 'task-event-1',
779
+ taskId: 'task-1',
780
+ workspaceId: 'ws-1',
781
+ action: 'task-status-changed',
782
+ actor: 'ai-profile-1',
783
+ actorType: 'ai',
784
+ createdAt: '2026-04-13T10:05:00.000Z'
785
+ }]
786
+ });
787
+ const changedSignature = buildWorkspaceSyncSignatureModel({
788
+ workspaceId: 'ws-1',
789
+ tasks: [],
790
+ archivedTasks: [],
791
+ taskEvents: [{
792
+ id: 'task-event-1',
793
+ taskId: 'task-1',
794
+ workspaceId: 'ws-1',
795
+ action: 'task-status-changed',
796
+ actor: 'ai-profile-1',
797
+ actorType: 'ai',
798
+ createdAt: '2026-04-13T10:06:00.000Z'
799
+ }]
800
+ });
801
+ expect(changedSignature).not.toEqual(baseSignature);
802
+ });
684
803
  });
685
804
  describe('dirty task watermarks', () => {
686
805
  it('emits an upsert when a status-only server-confirmed update changes updatedAt', () => {
package/dist/types.d.ts CHANGED
@@ -4,23 +4,15 @@
4
4
  * A decoupled, reusable collaborative task manager.
5
5
  */
6
6
  export type { TaskforceTheme } from './utils/theme';
7
+ export type { TaskActor } from './shared/taskActor.js';
7
8
  import type { TaskforceTheme } from './utils/theme';
9
+ import type { TaskActor } from './shared/taskActor.js';
8
10
  export interface Environment {
9
11
  id: string;
10
12
  name: string;
11
13
  icon?: string;
12
14
  directory: string;
13
15
  }
14
- export interface TaskActor {
15
- id: string;
16
- kind: 'human' | 'ai';
17
- label: string;
18
- username: string;
19
- icon: string;
20
- color: string;
21
- isNamed: boolean;
22
- meta?: Record<string, unknown>;
23
- }
24
16
  export interface Comment {
25
17
  id: string;
26
18
  author: 'user' | 'ai' | string;
@@ -0,0 +1 @@
1
+ import{r as n,j as e}from"./vendor-react-CKJs5o3c.js";import{p as T,u as C,v as w,w as S,t as s,x as M}from"./index-DXUdtH1B.js";import{h as L,B as A,k as I,aa as B,aI as H,aJ as D,b as R}from"./vendor-icons-I7ZwG1z_.js";import"./vendor-markdown-BUxTU7dS.js";import"./vendor-dnd-DRzYolkg.js";import"./vendor-router-BbWMxlnO.js";function F({workspaceId:h}){const[x,k]=n.useState([]),[j,y]=n.useState(!1),[c,P]=n.useState(null),[p,m]=n.useState(null),[d,g]=n.useState(null),N=n.useCallback(async()=>{if(h){y(!0);try{const a=await(await fetch(`/api/taskforce/workspace/assignee-options?workspaceId=${encodeURIComponent(h)}`,{credentials:"include"})).json().catch(()=>({})),t=Array.isArray(a?.assignees)?a.assignees.filter(i=>i.kind==="agent").map(i=>({id:String(i.value||""),name:String(i.label||i.value||"Unknown Agent"),username:String(i.username||i.value||""),icon:String(i.icon||"Bot"),color:String(i.color||"#6B7280"),kind:String(i.kind||"agent"),description:typeof i.description=="string"?i.description:null,role:typeof i.role=="string"?i.role:null,provider:typeof i.provider=="string"?i.provider:null,model:typeof i.model=="string"?i.model:null,surfaceType:T(i.surfaceType),createdAt:String(i.createdAt||""),updatedAt:String(i.updatedAt||i.createdAt||""),lastActiveAt:typeof i.lastActiveAt=="string"?i.lastActiveAt:null})):[];k(t)}finally{y(!1)}}},[h]);n.useEffect(()=>{N()},[N]);const f=n.useMemo(()=>{const r=new Map;for(const a of x){const t=C(a.surfaceType);r.has(t)||r.set(t,new Map);const i=r.get(t),v=a.name.toLowerCase();i.has(v)||i.set(v,[]),i.get(v).push(a)}return w.map(a=>({section:a,label:S(a),groups:Array.from(r.get(a)?.entries()||[]).map(([t,i])=>({groupId:`${a}:${t}`,section:a,sectionLabel:S(a),profiles:i,primaryProfile:i[0]}))})).filter(a=>a.groups.length>0)},[x]),o=n.useMemo(()=>f.flatMap(r=>r.groups),[f]),l=n.useMemo(()=>o.find(r=>r.groupId===d)||o[0]||null,[o,d]);n.useEffect(()=>{if(!o.length){d!==null&&g(null);return}(!d||!o.some(r=>r.groupId===d))&&g(o[0].groupId)},[o,d]);const b=async(r,a)=>{m(null);try{const t=await fetch("/api/taskforce/workspace/ai-profiles/merge",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({keepId:r,mergeId:a})}),i=await t.json().catch(()=>({}));if(!t.ok){m({type:"error",message:String(i?.error||"Failed to merge AI profiles.")});return}P(null),m({type:"success",message:"AI profiles merged."}),await N()}catch{m({type:"error",message:"Failed to merge AI profiles."})}},u=r=>{const a=String(r||"").trim();if(!a)return"Unknown";const t=Date.parse(a);return Number.isFinite(t)?new Date(t).toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"}):a};return e.jsx("div",{className:`${s.standalonePage} ${s.standaloneContent}`,style:{overflow:"auto"},children:e.jsx("div",{className:s.settingsContent,style:{maxWidth:1080,margin:"0 auto",width:"100%"},children:e.jsx("div",{className:s.settingGroup,children:e.jsxs("div",{children:[e.jsx("h4",{className:s.settingSubTitle,children:"Registered AI Profiles"}),e.jsx("p",{className:`${s.settingsHint} ${s.marginBottom12}`,children:"AI agents register profiles when connecting via MCP. Merge duplicates created when a token was lost."}),j&&e.jsxs("div",{className:s.settingsHint,children:[e.jsx(L,{size:13,className:s.spinner})," Loading profiles…"]}),!j&&x.length===0&&e.jsx("div",{className:s.settingsHint,children:"No AI profiles registered yet."}),!j&&f.length>0&&e.jsxs("div",{className:s.aiProfilesExplorer,children:[e.jsx("div",{className:s.aiProfilesListPane,children:e.jsx("div",{className:s.aiProfilesList,children:f.map(r=>e.jsxs("div",{className:s.aiProfilesSection,children:[e.jsx("div",{className:s.aiProfilesSectionHeader,children:r.label}),r.groups.map(a=>{const t=l?.groupId===a.groupId,i=a.primaryProfile;return e.jsxs("button",{type:"button",className:`${s.aiProfileGroup} ${a.profiles.length>1?s.aiProfileGroupDuplicate:""} ${t?s.aiProfileGroupSelected:""}`,onClick:()=>g(a.groupId),"aria-pressed":t,children:[e.jsxs("div",{className:s.aiProfileGroupHeader,children:[e.jsx(A,{size:13,style:{color:i.color}}),e.jsx("span",{className:s.aiProfileName,children:i.name}),a.profiles.length>1&&e.jsxs("span",{className:s.aiProfileDuplicateBadge,children:[e.jsx(I,{size:11})," ",a.profiles.length," duplicates"]})]}),e.jsxs("div",{className:s.settingsHint,style:{marginTop:6},children:["@",i.username]}),(i.surfaceType||i.role||i.provider||i.model)&&e.jsxs("div",{className:s.aiProfileMetaRow,children:[i.surfaceType&&e.jsx("span",{className:s.aiProfileSurfaceBadge,children:M(i.surfaceType)}),i.role&&e.jsx("span",{children:i.role}),i.provider&&e.jsx("span",{children:i.provider}),i.model&&e.jsx("span",{children:i.model})]}),i.description&&e.jsx("div",{className:s.settingsHint,style:{marginTop:4},children:i.description})]},a.groupId)})]},r.section))})}),l&&e.jsx("div",{className:s.aiProfileDetailPane,children:e.jsxs("div",{className:s.aiProfileDetailCard,children:[e.jsxs("div",{className:s.aiProfileDetailHero,children:[e.jsx("div",{className:s.aiProfileDetailIcon,style:{color:l.primaryProfile.color},children:e.jsx(A,{size:18})}),e.jsxs("div",{className:s.aiProfileDetailHeading,children:[e.jsx("div",{className:s.aiProfileDetailTitleRow,children:e.jsx("h5",{className:s.aiProfileDetailTitle,children:l.primaryProfile.name})}),e.jsxs("div",{className:s.aiProfileDetailMetaRow,children:[e.jsxs("span",{className:s.aiProfileDetailHandle,children:["@",l.primaryProfile.username]}),e.jsx("span",{className:s.aiProfileDetailSectionBadge,children:l.sectionLabel}),l.profiles.length>1&&e.jsxs("span",{className:s.aiProfileDetailLinkedCount,children:[l.profiles.length," linked"]})]})]})]}),l.primaryProfile.description&&e.jsx("div",{className:s.aiProfileDetailDescription,children:l.primaryProfile.description}),e.jsxs("div",{className:s.aiProfileDetailGrid,children:[e.jsxs("div",{className:s.aiProfileDetailStat,children:[e.jsxs("span",{className:s.aiProfileDetailStatLabel,children:[e.jsx(B,{size:12})," Role"]}),e.jsx("span",{className:s.aiProfileDetailStatValue,children:l.primaryProfile.role||"Not set"})]}),e.jsxs("div",{className:s.aiProfileDetailStat,children:[e.jsxs("span",{className:s.aiProfileDetailStatLabel,children:[e.jsx(H,{size:12})," Provider"]}),e.jsx("span",{className:s.aiProfileDetailStatValue,children:[l.primaryProfile.provider,l.primaryProfile.model].filter(Boolean).join(" · ")||"Not set"})]}),e.jsxs("div",{className:s.aiProfileDetailStat,children:[e.jsxs("span",{className:s.aiProfileDetailStatLabel,children:[e.jsx(D,{size:12})," Last active"]}),e.jsx("span",{className:s.aiProfileDetailStatValue,children:u(l.primaryProfile.lastActiveAt)})]}),e.jsxs("div",{className:s.aiProfileDetailStat,children:[e.jsxs("span",{className:s.aiProfileDetailStatLabel,children:[e.jsx(D,{size:12})," Updated"]}),e.jsx("span",{className:s.aiProfileDetailStatValue,children:u(l.primaryProfile.updatedAt)})]})]}),e.jsxs("div",{className:s.aiProfileInstanceSection,children:[e.jsxs("div",{className:s.aiProfileInstanceSectionHeader,children:[e.jsx("span",{children:"Profile instances"}),e.jsx("span",{className:s.settingsHint,children:"Choose a keeper here if duplicates need to be merged."})]}),e.jsx("div",{className:s.aiProfileInstanceList,children:l.profiles.map(r=>e.jsxs("div",{className:s.aiProfileInstanceCard,children:[e.jsxs("div",{className:s.aiProfileInstanceTopRow,children:[e.jsxs("div",{children:[e.jsxs("div",{className:s.aiProfileInstanceName,children:["@",r.username]}),e.jsx("div",{className:s.aiProfileIdChip,children:r.id})]}),l.profiles.length>1&&c?.keepId===r.id&&e.jsx("span",{className:s.aiProfileKeepBadge,children:"Keeping"})]}),e.jsxs("div",{className:s.aiProfileInstanceMeta,children:[e.jsxs("span",{children:["Created ",u(r.createdAt)]}),e.jsxs("span",{children:["Updated ",u(r.updatedAt)]})]}),l.profiles.length>1&&c?.keepId!==r.id&&e.jsx("button",{className:s.secondaryHeaderBtn,title:"Keep this profile, merge others into it",onClick:()=>{const a=l.profiles.find(t=>t.id!==r.id)?.id;a&&P({keepId:r.id,mergeId:a})},children:"Keep this"})]},r.id))}),c&&l.profiles.some(r=>r.id===c.keepId)&&e.jsxs("div",{className:s.aiProfileMergeActions,children:[e.jsx("button",{className:s.dangerBtn,onClick:()=>{b(c.keepId,c.mergeId)},children:"Merge duplicates"}),e.jsx("button",{className:s.secondaryHeaderBtn,onClick:()=>P(null),children:"Cancel"})]})]})]})})]}),p&&e.jsxs("div",{className:`${p.type==="success"?s.successMessage:s.errorMessage} ${s.marginTop12}`,children:[p.type==="success"?e.jsx(R,{size:14}):e.jsx(I,{size:14}),p.message]})]})})})})}export{F as AgentsModule};