@taskforcehq/taskforce 0.3.306 → 0.3.308

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 (232) hide show
  1. package/README.md +0 -2
  2. package/dist/Taskforce.module.css +317 -27
  3. package/dist/TaskforceCore.js +1 -0
  4. package/dist/TaskforceCore.test.js +178 -0
  5. package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
  6. package/dist/components/activity/EntityActivityTimeline.js +81 -0
  7. package/dist/components/context/ContextAttachmentManager.js +15 -3
  8. package/dist/components/features/AgentsModule.d.ts +5 -1
  9. package/dist/components/features/AgentsModule.js +142 -39
  10. package/dist/components/features/AgentsModule.test.js +260 -11
  11. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
  12. package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
  13. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
  14. package/dist/components/features/DocumentIndex.d.ts +2 -2
  15. package/dist/components/features/DocumentIndex.js +3 -4
  16. package/dist/components/features/DocumentViewer.d.ts +3 -1
  17. package/dist/components/features/DocumentViewer.js +6 -5
  18. package/dist/components/features/DocumentWorkspace.d.ts +10 -2
  19. package/dist/components/features/DocumentWorkspace.js +15 -4
  20. package/dist/components/features/DocumentWorkspace.test.js +20 -3
  21. package/dist/components/features/TaskSettings.d.ts +2 -0
  22. package/dist/components/features/TaskSettings.js +83 -78
  23. package/dist/components/features/TaskSettings.test.js +171 -82
  24. package/dist/components/task/TaskActionHeader.js +11 -5
  25. package/dist/components/task/TaskActionHeader.test.js +10 -0
  26. package/dist/components/task/TaskCard.js +6 -2
  27. package/dist/components/task/TaskContextUpload.test.js +3 -3
  28. package/dist/components/task/TaskForm.js +14 -64
  29. package/dist/components/task/TaskKanban.js +0 -8
  30. package/dist/components/task/TaskList.js +5 -1
  31. package/dist/components/task/TaskList.test.js +4 -0
  32. package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
  33. package/dist/components/ui/HierarchyActionBadge.js +5 -0
  34. package/dist/components/views/AppShellHeader.d.ts +3 -1
  35. package/dist/components/views/AppShellHeader.js +27 -11
  36. package/dist/components/views/AppShellHeader.test.js +18 -1
  37. package/dist/components/views/PlanComparisonPage.d.ts +20 -15
  38. package/dist/components/views/PlanComparisonPage.js +221 -82
  39. package/dist/components/views/PlanComparisonPage.test.js +268 -14
  40. package/dist/components/views/PlansPage.js +16 -10
  41. package/dist/components/views/PlansPage.test.js +4 -2
  42. package/dist/components/views/StandaloneLayout.js +256 -69
  43. package/dist/components/views/WidgetView.js +10 -8
  44. package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
  45. package/dist/components/views/panels/PlanningDrawer.js +259 -51
  46. package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
  47. package/dist/config/envSchema.js +1 -0
  48. package/dist/core/AiProfileService.d.ts +7 -0
  49. package/dist/core/AiProfileService.js +460 -35
  50. package/dist/core/AiProfiles.test.js +648 -2
  51. package/dist/core/AttachmentLinkService.d.ts +3 -0
  52. package/dist/core/AttachmentLinkService.js +58 -13
  53. package/dist/core/GlobalSettingsService.js +30 -0
  54. package/dist/core/GlobalSettingsService.test.d.ts +1 -0
  55. package/dist/core/GlobalSettingsService.test.js +45 -0
  56. package/dist/core/McpTokenService.d.ts +12 -0
  57. package/dist/core/McpTokenService.js +44 -1
  58. package/dist/core/PlanEntitlementService.d.ts +20 -0
  59. package/dist/core/PlanEntitlementService.js +215 -19
  60. package/dist/core/PlanFeatureCatalog.test.js +55 -0
  61. package/dist/core/PlanningEntities.test.js +366 -4
  62. package/dist/core/TaskActivityService.d.ts +28 -1
  63. package/dist/core/TaskActivityService.js +227 -46
  64. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  65. package/dist/core/TaskActivitySync.test.js +90 -0
  66. package/dist/core/TaskAuditTimeline.test.js +96 -0
  67. package/dist/core/Taskforce.d.ts +204 -26
  68. package/dist/core/Taskforce.ids.test.js +58 -0
  69. package/dist/core/Taskforce.js +781 -71
  70. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  71. package/dist/core/types.d.ts +10 -0
  72. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
  73. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  74. package/dist/hooks/bootstrapTimeouts.js +8 -0
  75. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  76. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  77. package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
  78. package/dist/hooks/sync/orchestratorShared.js +61 -1
  79. package/dist/hooks/sync/recovery.js +8 -0
  80. package/dist/hooks/sync/recovery.test.js +5 -5
  81. package/dist/hooks/sync/transfers.d.ts +2 -1
  82. package/dist/hooks/sync/transfers.js +36 -9
  83. package/dist/hooks/sync/transfers.test.js +534 -2
  84. package/dist/hooks/useFilterSort.js +13 -13
  85. package/dist/hooks/useFilterSort.test.js +87 -1
  86. package/dist/hooks/useSyncOrchestrator.js +50 -5
  87. package/dist/hooks/useTaskforce.d.ts +85 -50
  88. package/dist/hooks/useTaskforce.js +44 -14
  89. package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
  90. package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
  91. package/dist/hooks/useWorkspaceSyncController.js +7 -1
  92. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  93. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
  94. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  95. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  96. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  97. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  98. package/dist/mcp/clientIntegrations.d.ts +1 -0
  99. package/dist/mcp/clientIntegrations.js +25 -0
  100. package/dist/mcp/clientIntegrations.test.js +14 -3
  101. package/dist/mcp/clientProfileDefaults.js +3 -3
  102. package/dist/mcp/collaborationRegistrar.js +7 -7
  103. package/dist/mcp/documentAssetRegistrar.js +113 -7
  104. package/dist/mcp/runtime.d.ts +1 -0
  105. package/dist/mcp/runtime.js +969 -122
  106. package/dist/mcp/runtime.test.js +1083 -11
  107. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  108. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  109. package/dist/mcp/toolRegistry.d.ts +56 -0
  110. package/dist/mcp/toolRegistry.js +8 -0
  111. package/dist/mcp/toolRegistry.test.js +4 -0
  112. package/dist/migrations/taskSchemaMigrations.js +58 -15
  113. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  114. package/dist/server/aiProfileAvatarAssets.js +319 -0
  115. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  116. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  117. package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
  118. package/dist/server/index.js +4 -0
  119. package/dist/server/index.test.js +6 -1
  120. package/dist/server/routes/admin.js +328 -9
  121. package/dist/server/routes/annotatedAttachments.js +51 -0
  122. package/dist/server/routes/auth.d.ts +11 -0
  123. package/dist/server/routes/auth.js +189 -27
  124. package/dist/server/routes/authSupport.d.ts +5 -0
  125. package/dist/server/routes/authSupport.js +13 -1
  126. package/dist/server/routes/billing.d.ts +1 -0
  127. package/dist/server/routes/billing.js +116 -63
  128. package/dist/server/routes/billing.test.js +162 -11
  129. package/dist/server/routes/documents.js +10 -0
  130. package/dist/server/routes/shared.js +0 -10
  131. package/dist/server/routes/sync.integration.test.js +221 -0
  132. package/dist/server/routes/sync.js +13 -3
  133. package/dist/server/routes/syncSnapshotRoutes.js +2 -0
  134. package/dist/server/routes/tasks.js +12 -9
  135. package/dist/server/routes.js +87 -8
  136. package/dist/server/routes.test.js +972 -23
  137. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  138. package/dist/shared/aiProfileSurfaceType.js +10 -8
  139. package/dist/storage/objectStorage.d.ts +1 -0
  140. package/dist/storage/objectStorage.js +2 -1
  141. package/dist/storage/objectStorageClient.d.ts +1 -0
  142. package/dist/storage/objectStorageClient.js +41 -8
  143. package/dist/storage/objectStorageClient.test.js +26 -1
  144. package/dist/storage/objectStorageConfig.js +3 -1
  145. package/dist/storage/objectStorageConfig.test.js +13 -1
  146. package/dist/sync/collaborationSyncPayload.js +2 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +1 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -3
  149. package/dist/sync/collaborationSyncState.js +19 -12
  150. package/dist/sync/contentSyncPayload.d.ts +5 -1
  151. package/dist/sync/contentSyncPayload.js +9 -0
  152. package/dist/sync/contentSyncPayload.test.js +39 -0
  153. package/dist/sync/contentSyncState.d.ts +5 -1
  154. package/dist/sync/contentSyncState.js +231 -37
  155. package/dist/sync/planningSyncPayload.js +5 -0
  156. package/dist/sync/planningSyncPayload.test.js +24 -0
  157. package/dist/sync/syncApplyHandlers.d.ts +4 -4
  158. package/dist/sync/syncApplyHandlers.js +89 -31
  159. package/dist/sync/syncApplyHandlers.test.js +184 -4
  160. package/dist/sync/syncResourceAdapters.d.ts +2 -0
  161. package/dist/sync/syncResourceAdapters.js +2 -0
  162. package/dist/sync/syncService.d.ts +1 -0
  163. package/dist/sync/syncService.js +13 -1
  164. package/dist/sync/syncService.test.js +39 -0
  165. package/dist/sync/workspacePullFeed.d.ts +4 -0
  166. package/dist/sync/workspacePullFeed.js +129 -70
  167. package/dist/sync/workspacePullFeed.test.js +49 -0
  168. package/dist/sync/workspaceSyncModel.d.ts +28 -6
  169. package/dist/sync/workspaceSyncModel.js +159 -45
  170. package/dist/sync/workspaceSyncModel.test.js +269 -3
  171. package/dist/types.d.ts +33 -2
  172. package/dist/ui/agent-logos/ChatGPT.png +0 -0
  173. package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
  174. package/dist/ui/agent-logos/antigravity.jpeg +0 -0
  175. package/dist/ui/agent-logos/claude-code.jpeg +0 -0
  176. package/dist/ui/agent-logos/codex.jpeg +0 -0
  177. package/dist/ui/agent-logos/cursor.png +0 -0
  178. package/dist/ui/agent-logos/openclaw.jpeg +0 -0
  179. package/dist/ui/agent-logos/windsurf.png +0 -0
  180. package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +1 -0
  181. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  182. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CCckzWYZ.js +3 -0
  183. package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +3 -0
  184. package/dist/ui/assets/DocumentWorkspace-BVaWPA25.js +252 -0
  185. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  186. package/dist/ui/assets/EntityActivityTimeline-DSj0ThaK.js +1 -0
  187. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  188. package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-AxMkWtxH.js} +1 -1
  189. package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
  190. package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
  191. package/dist/ui/assets/TaskContextUpload-Dw4rTF4i.js +1 -0
  192. package/dist/ui/assets/TaskSettings-f2ga42_V.js +9 -0
  193. package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-E4UL3mov.js} +4 -4
  194. package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
  195. package/dist/ui/assets/index-BvAbqx5Q.css +1 -0
  196. package/dist/ui/assets/index-MAHKvFqp.js +4 -0
  197. package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
  198. package/dist/ui/index.html +3 -3
  199. package/dist/ui/og-image.png +0 -0
  200. package/dist/ui/product/agent manager.png +0 -0
  201. package/dist/ui/product/agent_manager_02.png +0 -0
  202. package/dist/utils/aiProfileEvents.d.ts +7 -0
  203. package/dist/utils/aiProfileEvents.js +6 -0
  204. package/dist/utils/assignees.d.ts +6 -0
  205. package/dist/utils/assignees.js +8 -1
  206. package/dist/utils/assignees.test.d.ts +1 -0
  207. package/dist/utils/assignees.test.js +25 -0
  208. package/dist/utils/avatarDisplay.d.ts +1 -0
  209. package/dist/utils/avatarDisplay.js +15 -0
  210. package/dist/utils/contextFiles.d.ts +3 -1
  211. package/dist/utils/contextFiles.js +2 -1
  212. package/dist/utils/taskActivity.d.ts +7 -1
  213. package/dist/utils/taskActivity.js +63 -1
  214. package/dist/utils/taskActivity.test.js +23 -1
  215. package/dist/utils/taskSearch.d.ts +22 -0
  216. package/dist/utils/taskSearch.js +87 -0
  217. package/dist/utils/taskSearch.test.d.ts +1 -0
  218. package/dist/utils/taskSearch.test.js +69 -0
  219. package/dist/utils/taskUtils.js +10 -3
  220. package/dist/utils/taskUtils.test.d.ts +1 -0
  221. package/dist/utils/taskUtils.test.js +19 -0
  222. package/package.json +1 -1
  223. package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
  224. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  225. package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
  226. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
  227. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
  228. package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
  229. package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
  230. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
  231. package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
  232. package/dist/ui/assets/index-C6k75jr8.js +0 -6
@@ -1,12 +1,37 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useState } from 'react';
3
- import { AlertTriangle, Bot, Check, Cloud, LaptopMinimal, Loader2 } from 'lucide-react';
3
+ import { AlertTriangle, Bot, Check, ChevronLeft, Cloud, LaptopMinimal, Loader2, Plug } from 'lucide-react';
4
4
  import styles from '../../Taskforce.module.css';
5
5
  import { EditableAvatarButton } from '../ui/EditableAvatarButton';
6
6
  import { AvatarImageManagerModal } from '../views/standalone/modals/AvatarImageManagerModal';
7
7
  import { prepareAvatarUploadFile } from '../../utils/avatarUpload';
8
- import { AI_PROFILE_SURFACE_TYPE_SECTION_ORDER, formatAiProfileSurfaceTypeLabel, formatAiProfileSurfaceTypeSectionLabel, getAiProfileSurfaceTypeSection, parseAiProfileSurfaceType } from '../../shared/aiProfileSurfaceType.js';
8
+ import { buildVersionedAvatarUrl } from '../../utils/avatarDisplay';
9
+ import { dispatchTaskforceAiProfilesMutated } from '../../utils/aiProfileEvents';
10
+ import { AI_PROFILE_SURFACE_TYPE_SECTION_ORDER, AI_PROFILE_SURFACE_TYPES, formatAiProfileSurfaceTypeLabel, formatAiProfileSurfaceTypeSectionLabel, getAiProfileSurfaceTypeSection, parseAiProfileSurfaceType } from '../../shared/aiProfileSurfaceType.js';
9
11
  import { formatAiProfileSeatScopeLabel, parseAiProfileSeatScope } from '../../shared/aiProfileSeatScope.js';
12
+ function hasAiProfileAvatar(profile) {
13
+ return typeof profile.avatarUrl === 'string' && profile.avatarUrl.trim().length > 0;
14
+ }
15
+ function getAiProfileAvatarDisplayUrl(profile) {
16
+ if (!profile)
17
+ return '';
18
+ return buildVersionedAvatarUrl(profile.avatarUrl, profile.avatarRevision, profile.avatarUpdatedAt);
19
+ }
20
+ function getAiProfileAvatarSourceUrl(profile) {
21
+ if (!profile)
22
+ return '';
23
+ return buildVersionedAvatarUrl(profile.avatarSourceUrl, profile.avatarRevision, profile.avatarUpdatedAt);
24
+ }
25
+ function selectAiProfileGroupPrimary(profiles) {
26
+ return profiles.find(hasAiProfileAvatar) || profiles[0];
27
+ }
28
+ function buildAiProfileGroupKey(profile) {
29
+ return `${profile.seatScope || 'unknown'}:${profile.name.trim().toLowerCase()}`;
30
+ }
31
+ function formatDuplicateCount(count) {
32
+ const duplicates = Math.max(0, count - 1);
33
+ return `${duplicates} duplicate${duplicates === 1 ? '' : 's'}`;
34
+ }
10
35
  function parseAiProfileSeatUsage(value) {
11
36
  if (!value || typeof value !== 'object')
12
37
  return null;
@@ -34,13 +59,15 @@ function renderAiProfileSeatScopeIcon(seatScope, options) {
34
59
  }
35
60
  return null;
36
61
  }
37
- export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSessionResolved = false, isAuthenticated = false, cloudAiProfileSeatUsage = null, }) {
62
+ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSessionResolved = false, isAuthenticated = false, cloudAiProfileSeatUsage = null, agentTrayOpen = false, onCloseAgentTray, mcpSettingsNode = null, }) {
38
63
  const [aiProfiles, setAiProfiles] = useState([]);
39
64
  const [localAiProfileSeatUsage, setLocalAiProfileSeatUsage] = useState(null);
40
65
  const [aiProfilesLoading, setAiProfilesLoading] = useState(false);
41
66
  const [aiProfileMergeState, setAiProfileMergeState] = useState(null);
42
67
  const [aiProfileMergeStatus, setAiProfileMergeStatus] = useState(null);
43
68
  const [aiProfileArchiveError, setAiProfileArchiveError] = useState(null);
69
+ const [aiProfileCategoryError, setAiProfileCategoryError] = useState(null);
70
+ const [aiProfileCategoryBusy, setAiProfileCategoryBusy] = useState(false);
44
71
  const [selectedGroupId, setSelectedGroupId] = useState(null);
45
72
  const [avatarEditorProfileId, setAvatarEditorProfileId] = useState(null);
46
73
  const [aiAvatarBusy, setAiAvatarBusy] = useState(false);
@@ -50,7 +77,7 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
50
77
  return;
51
78
  setAiProfilesLoading(true);
52
79
  try {
53
- const rosterUrl = `/api/taskforce/workspace/assignee-options?workspaceId=${encodeURIComponent(workspaceId)}`;
80
+ const rosterUrl = `/api/taskforce/workspace/assignee-options?workspaceId=${encodeURIComponent(workspaceId)}&kind=agent`;
54
81
  const rosterResponse = await fetch(rosterUrl, {
55
82
  credentials: 'include',
56
83
  });
@@ -69,6 +96,8 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
69
96
  color: String(assignee.color || '#6B7280'),
70
97
  avatarUrl: typeof assignee.avatarUrl === 'string' ? assignee.avatarUrl : null,
71
98
  avatarSourceUrl: typeof assignee.avatarSourceUrl === 'string' ? assignee.avatarSourceUrl : null,
99
+ avatarRevision: Number.isFinite(Number(assignee.avatarRevision)) ? Math.max(0, Math.floor(Number(assignee.avatarRevision))) : 0,
100
+ avatarUpdatedAt: typeof assignee.avatarUpdatedAt === 'string' ? assignee.avatarUpdatedAt : null,
72
101
  kind: String(assignee.kind || 'agent'),
73
102
  description: typeof assignee.description === 'string' ? assignee.description : null,
74
103
  role: typeof assignee.role === 'string' ? assignee.role : null,
@@ -87,6 +116,9 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
87
116
  setAiProfileArchiveError((current) => (current && !agents.some((agent) => agent.id === current.profileId)
88
117
  ? null
89
118
  : current));
119
+ setAiProfileCategoryError((current) => (current && !agents.some((agent) => agent.id === current.profileId)
120
+ ? null
121
+ : current));
90
122
  }
91
123
  finally {
92
124
  setAiProfilesLoading(false);
@@ -102,7 +134,7 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
102
134
  if (!bySection.has(section))
103
135
  bySection.set(section, new Map());
104
136
  const sectionEntries = bySection.get(section);
105
- const key = profile.name.toLowerCase();
137
+ const key = buildAiProfileGroupKey(profile);
106
138
  if (!sectionEntries.has(key))
107
139
  sectionEntries.set(key, []);
108
140
  sectionEntries.get(key).push(profile);
@@ -116,7 +148,7 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
116
148
  section,
117
149
  sectionLabel: formatAiProfileSurfaceTypeSectionLabel(section),
118
150
  profiles,
119
- primaryProfile: profiles[0]
151
+ primaryProfile: selectAiProfileGroupPrimary(profiles)
120
152
  }))
121
153
  }))
122
154
  .filter((entry) => entry.groups.length > 0);
@@ -152,6 +184,7 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
152
184
  setAiProfileMergeState(null);
153
185
  setAiProfileMergeStatus({ type: 'success', message: 'AI profiles merged.' });
154
186
  await loadAiProfiles();
187
+ dispatchTaskforceAiProfilesMutated({ workspaceId, profileId: keepId, reason: 'merge' });
155
188
  }
156
189
  catch {
157
190
  setAiProfileMergeStatus({ type: 'error', message: 'Failed to merge AI profiles.' });
@@ -183,6 +216,7 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
183
216
  }
184
217
  setAiProfileMergeStatus({ type: 'success', message: 'AI profile removed from active roster.' });
185
218
  await loadAiProfiles();
219
+ dispatchTaskforceAiProfilesMutated({ workspaceId, profileId: profile.id, reason: 'archive' });
186
220
  }
187
221
  catch {
188
222
  setAiProfileArchiveError({
@@ -191,6 +225,61 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
191
225
  });
192
226
  }
193
227
  };
228
+ const handleCategoryChange = async (group, value) => {
229
+ const nextSurfaceType = parseAiProfileSurfaceType(value);
230
+ const currentSurfaceTypes = new Set(group.profiles.map((profile) => profile.surfaceType ?? ''));
231
+ const nextSurfaceTypeKey = nextSurfaceType ?? '';
232
+ if (currentSurfaceTypes.size === 1 && currentSurfaceTypes.has(nextSurfaceTypeKey))
233
+ return;
234
+ setAiProfileCategoryBusy(true);
235
+ setAiProfileCategoryError(null);
236
+ setAiProfileMergeStatus(null);
237
+ const updatedProfiles = [];
238
+ try {
239
+ for (const profile of group.profiles) {
240
+ const res = await fetch('/api/taskforce/workspace/ai-profiles/surface-type', {
241
+ method: 'POST',
242
+ credentials: 'include',
243
+ headers: { 'Content-Type': 'application/json' },
244
+ body: JSON.stringify({
245
+ profileId: profile.id,
246
+ surfaceType: nextSurfaceType
247
+ }),
248
+ });
249
+ const data = await res.json().catch(() => ({}));
250
+ if (!res.ok || !data?.profile) {
251
+ throw new Error(String(data?.error || 'Failed to update AI profile category.'));
252
+ }
253
+ const updated = data.profile;
254
+ updatedProfiles.push({
255
+ ...profile,
256
+ surfaceType: parseAiProfileSurfaceType(updated.surfaceType),
257
+ updatedAt: typeof updated.updatedAt === 'string' ? updated.updatedAt : profile.updatedAt
258
+ });
259
+ }
260
+ const updatedById = new Map(updatedProfiles.map((profile) => [profile.id, profile]));
261
+ setAiProfiles((current) => current.map((profile) => updatedById.get(profile.id) || profile));
262
+ const nextPrimary = updatedById.get(group.primaryProfile.id) || {
263
+ ...group.primaryProfile,
264
+ surfaceType: nextSurfaceType
265
+ };
266
+ const nextSection = getAiProfileSurfaceTypeSection(nextPrimary.surfaceType);
267
+ setSelectedGroupId(`${nextSection}:${buildAiProfileGroupKey(nextPrimary)}`);
268
+ setAiProfileMergeStatus({ type: 'success', message: 'AI profile category updated.' });
269
+ for (const profile of updatedProfiles) {
270
+ dispatchTaskforceAiProfilesMutated({ workspaceId, profileId: profile.id, reason: 'update' });
271
+ }
272
+ }
273
+ catch (error) {
274
+ setAiProfileCategoryError({
275
+ profileId: group.primaryProfile.id,
276
+ message: String(error?.message || 'Failed to update AI profile category.')
277
+ });
278
+ }
279
+ finally {
280
+ setAiProfileCategoryBusy(false);
281
+ }
282
+ };
194
283
  const fileToDataUrl = (file) => new Promise((resolve, reject) => {
195
284
  const reader = new FileReader();
196
285
  reader.onload = () => resolve(String(reader.result || ''));
@@ -206,46 +295,60 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
206
295
  ...profile,
207
296
  avatarUrl: typeof updatedProfile.avatarUrl === 'string' ? updatedProfile.avatarUrl : null,
208
297
  avatarSourceUrl: typeof updatedProfile.avatarSourceUrl === 'string' ? updatedProfile.avatarSourceUrl : null,
298
+ avatarRevision: Number.isFinite(Number(updatedProfile.avatarRevision)) ? Math.max(0, Math.floor(Number(updatedProfile.avatarRevision))) : profile.avatarRevision,
299
+ avatarUpdatedAt: typeof updatedProfile.avatarUpdatedAt === 'string' ? updatedProfile.avatarUpdatedAt : profile.avatarUpdatedAt,
209
300
  updatedAt: typeof updatedProfile.updatedAt === 'string' ? updatedProfile.updatedAt : profile.updatedAt
210
301
  }
211
302
  : profile)));
212
303
  };
213
- const updateAiProfileAvatar = async (profileId, avatarUrl, avatarSourceUrl) => {
214
- const res = await fetch('/api/taskforce/workspace/ai-profiles/avatar', {
304
+ const uploadAiProfileAvatar = async (profileId, displayFile, sourceFile) => {
305
+ const displayDataUrl = await fileToDataUrl(displayFile);
306
+ const sourceDataUrl = sourceFile ? await fileToDataUrl(sourceFile) : null;
307
+ const payload = {
308
+ profileId,
309
+ displayImage: {
310
+ dataUrl: displayDataUrl,
311
+ mimeType: displayFile.type || 'application/octet-stream',
312
+ originalName: displayFile.name || 'display-avatar'
313
+ }
314
+ };
315
+ if (sourceFile && sourceDataUrl) {
316
+ payload.sourceImage = {
317
+ dataUrl: sourceDataUrl,
318
+ mimeType: sourceFile.type || 'application/octet-stream',
319
+ originalName: sourceFile.name || 'source-avatar'
320
+ };
321
+ }
322
+ const res = await fetch('/api/taskforce/workspace/ai-profiles/avatar/upload', {
215
323
  method: 'POST',
216
324
  credentials: 'include',
217
325
  headers: { 'Content-Type': 'application/json' },
218
- body: JSON.stringify({
219
- profileId,
220
- avatarUrl,
221
- ...(avatarSourceUrl !== undefined ? { avatarSourceUrl } : {})
222
- }),
326
+ body: JSON.stringify(payload),
223
327
  });
224
328
  const data = await res.json().catch(() => ({}));
225
329
  if (!res.ok || !data?.profile) {
226
330
  throw new Error(String(data?.error || 'Failed to update AI profile avatar.'));
227
331
  }
228
332
  applyAiProfileAvatarUpdate(data.profile);
333
+ dispatchTaskforceAiProfilesMutated({ workspaceId, profileId, reason: 'avatar' });
229
334
  };
230
- const uploadAiProfileAvatarFile = async (file, profile, kind) => {
231
- const dataUrl = await fileToDataUrl(file);
232
- const uploadRes = await fetch('/api/taskforce/context-upload', {
335
+ const clearAiProfileAvatar = async (profileId) => {
336
+ const res = await fetch('/api/taskforce/workspace/ai-profiles/avatar', {
233
337
  method: 'POST',
234
338
  credentials: 'include',
235
339
  headers: { 'Content-Type': 'application/json' },
236
340
  body: JSON.stringify({
237
- workspaceId,
238
- file: dataUrl,
239
- originalName: `${kind}-${file.name}`,
240
- folderPath: `ai-profiles/${profile.id}/avatar`
341
+ profileId,
342
+ avatarUrl: null,
343
+ avatarSourceUrl: null
241
344
  })
242
345
  });
243
- const uploadData = await uploadRes.json().catch(() => ({}));
244
- const avatarUrl = typeof uploadData?.path === 'string' ? uploadData.path : '';
245
- if (!uploadRes.ok || !avatarUrl) {
246
- throw new Error(String(uploadData?.error || 'Failed to upload AI profile photo.'));
346
+ const data = await res.json().catch(() => ({}));
347
+ if (!res.ok || !data?.profile) {
348
+ throw new Error(String(data?.error || 'Failed to update AI profile avatar.'));
247
349
  }
248
- return avatarUrl;
350
+ applyAiProfileAvatarUpdate(data.profile);
351
+ dispatchTaskforceAiProfilesMutated({ workspaceId, profileId, reason: 'avatar' });
249
352
  };
250
353
  const handleAiProfileAvatarSelected = async (file, sourceFile) => {
251
354
  if (!avatarEditorProfile)
@@ -264,8 +367,7 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
264
367
  : 'AI profile photo must be 5 MB or smaller.');
265
368
  }
266
369
  const uploadFile = preparedAvatar.file;
267
- const avatarUrl = await uploadAiProfileAvatarFile(uploadFile, avatarEditorProfile, 'cropped');
268
- let avatarSourceUrl;
370
+ let preparedSourceFile = null;
269
371
  if (sourceFile) {
270
372
  const preparedSource = await prepareAvatarUploadFile(sourceFile, { maxBytes: 5 * 1024 * 1024 });
271
373
  if (preparedSource.exceededLimit) {
@@ -273,11 +375,9 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
273
375
  ? 'Animated GIF AI profile photos must be 5 MB or smaller.'
274
376
  : 'AI profile source photo must be 5 MB or smaller.');
275
377
  }
276
- avatarSourceUrl = sourceFile.type === 'image/gif'
277
- ? avatarUrl
278
- : await uploadAiProfileAvatarFile(preparedSource.file, avatarEditorProfile, 'source');
378
+ preparedSourceFile = preparedSource.file;
279
379
  }
280
- await updateAiProfileAvatar(avatarEditorProfile.id, avatarUrl, avatarSourceUrl);
380
+ await uploadAiProfileAvatar(avatarEditorProfile.id, uploadFile, preparedSourceFile);
281
381
  return true;
282
382
  }
283
383
  catch (error) {
@@ -294,7 +394,7 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
294
394
  setAiAvatarBusy(true);
295
395
  setAiAvatarError(null);
296
396
  try {
297
- await updateAiProfileAvatar(avatarEditorProfile.id, null, null);
397
+ await clearAiProfileAvatar(avatarEditorProfile.id);
298
398
  }
299
399
  catch (error) {
300
400
  setAiAvatarError(String(error?.message || 'Failed to remove AI profile photo.'));
@@ -328,10 +428,10 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
328
428
  { label: 'Model', value: profile.model || '' },
329
429
  ].map((row) => ({ ...row, value: formatRowValue(row.value) })),
330
430
  dates: [
431
+ { label: 'Status', value: profile.archivedAt ? 'Retired' : 'Active' },
331
432
  { label: 'Recruited', value: formatTimestamp(profile.createdAt) },
332
433
  { label: 'Last updated', value: formatTimestamp(profile.updatedAt) },
333
434
  { label: 'Last active', value: formatTimestamp(profile.lastActiveAt) },
334
- { label: 'Status', value: profile.archivedAt ? 'Retired' : 'Active' },
335
435
  ].map((row) => ({ ...row, value: formatRowValue(row.value) })),
336
436
  };
337
437
  }, [selectedGroup]);
@@ -345,7 +445,8 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
345
445
  : displayedSeatUsage
346
446
  ? `${displayedSeatUsage.used}/${displayedSeatUsage.limit === null ? 'Unlimited' : displayedSeatUsage.limit}`
347
447
  : null;
348
- return (_jsxs("section", { className: styles.agentsModuleRoot, children: [_jsx("div", { className: styles.agentsModuleContent, children: _jsx("div", { className: `${styles.settingGroup} ${styles.agentsModuleGroup}`, children: _jsxs("div", { children: [_jsx("h4", { className: styles.settingSubTitle, children: "Registered AI Profiles" }), _jsx("p", { className: `${styles.settingsHint} ${styles.marginBottom12}`, children: "AI agents register profiles when connecting via MCP. Merge duplicates created when a token was lost, or remove inactive profiles from the active roster to free seats while preserving history." }), cloudAgentSeatLabel && (_jsx("div", { className: styles.aiProfileSeatSummary, children: shouldPromptForCloudLogin
448
+ const showAgentTray = !!mcpSettingsNode;
449
+ return (_jsxs("section", { className: `${styles.agentsModuleRoot} ${showAgentTray ? styles.agentsModuleWithTray : ''} ${showAgentTray && agentTrayOpen ? styles.agentsModuleTrayOpen : ''}`.trim(), children: [showAgentTray && (_jsxs("aside", { className: `${styles.agentTrayPanel} ${agentTrayOpen ? styles.agentTrayPanelOpen : ''}`.trim(), "aria-label": "Agent MCP settings tray", "aria-hidden": !agentTrayOpen, children: [_jsxs("div", { className: styles.agentTrayHeader, children: [_jsxs("span", { className: styles.agentTrayTitle, children: [_jsx(Plug, { size: 14 }), "MCP Settings"] }), _jsx("button", { type: "button", className: "tf-control-icon", onClick: onCloseAgentTray, title: "Collapse agent tray", "aria-label": "Collapse agent tray", children: _jsx(ChevronLeft, { size: 16 }) })] }), _jsx("div", { className: `${styles.agentTrayContent} tf-scrollbar tf-scrollbar--track-transparent`, children: _jsx("div", { className: styles.agentTrayContentInner, children: mcpSettingsNode }) })] })), _jsx("div", { className: styles.agentsModuleContent, children: _jsx("div", { className: `${styles.settingGroup} ${styles.agentsModuleGroup}`, children: _jsxs("div", { children: [_jsx("h4", { className: styles.settingSubTitle, children: "Registered AI Profiles" }), _jsx("p", { className: `${styles.settingsHint} ${styles.marginBottom12}`, children: "AI agents register profiles when connecting via MCP. Merge duplicates created when a token was lost, or remove inactive profiles from the active roster to free seats while preserving history." }), cloudAgentSeatLabel && (_jsx("div", { className: styles.aiProfileSeatSummary, children: shouldPromptForCloudLogin
349
450
  ? cloudAgentSeatLabel
350
451
  : `Registered Cloud Agents: ${cloudAgentSeatLabel}` })), aiProfilesLoading && (_jsxs("div", { className: styles.settingsHint, children: [_jsx(Loader2, { size: 13, className: styles.spinner }), " Loading profiles\u2026"] })), !aiProfilesLoading && aiProfiles.length === 0 && (_jsx("div", { className: styles.settingsHint, children: "No AI profiles registered yet." })), !aiProfilesLoading && groupedProfilesBySurfaceType.length > 0 && (_jsxs("div", { className: styles.aiProfilesExplorer, children: [_jsx("div", { className: `${styles.aiProfilesListPane} tf-scrollbar tf-scrollbar--track-transparent`, children: _jsx("div", { className: styles.aiProfilesList, children: groupedProfilesBySurfaceType.map((section) => (_jsxs("div", { className: styles.aiProfilesSection, children: [_jsx("div", { className: styles.aiProfilesSectionHeader, children: section.label }), section.groups.map((group) => {
351
452
  const isSelected = selectedGroup?.groupId === group.groupId;
@@ -355,19 +456,21 @@ export function AgentsModule({ workspaceId, cloudAuthConfigured = false, authSes
355
456
  return;
356
457
  event.preventDefault();
357
458
  setSelectedGroupId(group.groupId);
358
- }, "aria-pressed": isSelected, children: [renderAiProfileSeatScopeIcon(primary.seatScope), _jsxs("div", { className: styles.aiProfileGroupHeader, children: [_jsx("span", { className: styles.aiProfileGroupAvatar, style: { color: primary.color }, children: primary.avatarUrl ? (_jsx("img", { src: primary.avatarUrl, alt: "" })) : (_jsx(Bot, { size: 22 })) }), _jsxs("span", { className: styles.aiProfileGroupIdentity, children: [_jsx("span", { className: styles.aiProfileName, children: primary.name }), _jsxs("span", { className: styles.aiProfileHandle, children: ["@", primary.username] }), _jsx("span", { className: styles.aiProfileRole, children: primary.role || 'Role not set' }), group.profiles.length > 1 && (_jsxs("span", { className: styles.aiProfileDuplicateBadge, children: [_jsx(AlertTriangle, { size: 11 }), " ", group.profiles.length, " duplicates"] }))] })] })] }, group.groupId));
359
- })] }, section.section))) }) }), selectedGroup && (_jsx("div", { className: styles.aiProfileDetailPane, children: _jsxs("div", { className: styles.aiProfileDetailCard, children: [renderAiProfileSeatScopeIcon(selectedGroup.primaryProfile.seatScope, { variant: 'detail' }), _jsxs("div", { className: styles.aiProfileDetailHero, children: [_jsx(EditableAvatarButton, { label: "Edit AI profile photo", imageUrl: selectedGroup.primaryProfile.avatarUrl, fallback: _jsx(Bot, { size: 38 }), accentColor: selectedGroup.primaryProfile.color, size: 176, width: 153, height: 207, radius: 6, editBadgeSize: 28, editIconSize: 14, className: styles.aiProfileDetailAvatar, onClick: () => {
459
+ }, "aria-pressed": isSelected, children: [renderAiProfileSeatScopeIcon(primary.seatScope), _jsxs("div", { className: styles.aiProfileGroupHeader, children: [_jsx("span", { className: styles.aiProfileGroupAvatar, style: { color: primary.color }, children: primary.avatarUrl ? (_jsx("img", { src: getAiProfileAvatarDisplayUrl(primary), alt: "" })) : (_jsx(Bot, { size: 22 })) }), _jsxs("span", { className: styles.aiProfileGroupIdentity, children: [_jsx("span", { className: styles.aiProfileName, children: primary.name }), _jsxs("span", { className: styles.aiProfileHandle, children: ["@", primary.username] }), _jsx("span", { className: styles.aiProfileRole, children: primary.role || 'Role not set' }), group.profiles.length > 1 && (_jsxs("span", { className: styles.aiProfileDuplicateBadge, children: [_jsx(AlertTriangle, { size: 11 }), " ", formatDuplicateCount(group.profiles.length)] }))] })] })] }, group.groupId));
460
+ })] }, section.section))) }) }), selectedGroup && (_jsx("div", { className: styles.aiProfileDetailPane, children: _jsxs("div", { className: styles.aiProfileDetailCard, children: [renderAiProfileSeatScopeIcon(selectedGroup.primaryProfile.seatScope, { variant: 'detail' }), _jsxs("div", { className: styles.aiProfileDetailHero, children: [_jsx(EditableAvatarButton, { label: "Edit AI profile photo", imageUrl: getAiProfileAvatarDisplayUrl(selectedGroup.primaryProfile), fallback: _jsx(Bot, { size: 38 }), accentColor: selectedGroup.primaryProfile.color, size: 176, width: 153, height: 207, radius: 6, editBadgeSize: 28, editIconSize: 14, className: styles.aiProfileDetailAvatar, onClick: () => {
360
461
  setAiAvatarError(null);
361
462
  setAvatarEditorProfileId(selectedGroup.primaryProfile.id);
362
- } }), _jsxs("div", { className: styles.aiProfileDetailHeading, children: [_jsx("div", { className: styles.aiProfileDetailTitleRow, children: _jsx("h5", { className: styles.aiProfileDetailTitle, children: selectedGroup.primaryProfile.name }) }), _jsxs("div", { className: styles.aiProfileDetailMetaRow, children: [_jsxs("span", { className: styles.aiProfileDetailHandle, children: ["@", selectedGroup.primaryProfile.username] }), selectedGroup.profiles.length > 1 && (_jsxs("span", { className: styles.aiProfileDetailLinkedCount, children: [selectedGroup.profiles.length, " linked"] }))] }), _jsxs("div", { className: styles.aiProfileDetailRole, children: ["Role: ", selectedGroup.primaryProfile.role || 'Not set'] }), selectedGroup.primaryProfile.description && (_jsx("div", { className: styles.aiProfileDetailDescription, children: selectedGroup.primaryProfile.description }))] })] }), _jsxs("div", { className: styles.aiProfileDetailDataList, children: [_jsx("div", { className: styles.aiProfileDetailDataGroup, children: selectedProfileDataGroups.attributes.map((row) => (_jsxs("div", { className: styles.aiProfileDetailDataRow, children: [_jsx("span", { className: styles.aiProfileDetailDataLabel, children: row.label }), _jsx("span", { className: styles.aiProfileDetailDataValue, children: row.value })] }, row.label))) }), _jsx("div", { className: `${styles.aiProfileDetailDataGroup} ${styles.aiProfileDetailDateGroup}`, children: selectedProfileDataGroups.dates.map((row) => (_jsxs("div", { className: styles.aiProfileDetailDataRow, children: [_jsx("span", { className: styles.aiProfileDetailDataLabel, children: row.label }), row.label === 'Status' ? (_jsx("span", { className: styles.aiProfileDetailDataValue, children: _jsxs("select", { className: styles.aiProfileStatusSelect, "aria-label": "Agent roster status", value: selectedGroup.primaryProfile.archivedAt ? 'retired' : 'active', onChange: (event) => {
463
+ } }), _jsxs("div", { className: styles.aiProfileDetailHeading, children: [_jsx("div", { className: styles.aiProfileDetailTitleRow, children: _jsx("h5", { className: styles.aiProfileDetailTitle, children: selectedGroup.primaryProfile.name }) }), _jsxs("div", { className: styles.aiProfileDetailMetaRow, children: [_jsxs("span", { className: styles.aiProfileDetailHandle, children: ["@", selectedGroup.primaryProfile.username] }), selectedGroup.profiles.length > 1 && (_jsxs("span", { className: styles.aiProfileDetailLinkedCount, children: [selectedGroup.profiles.length, " linked"] }))] }), _jsxs("div", { className: styles.aiProfileDetailRole, children: ["Role: ", selectedGroup.primaryProfile.role || 'Not set'] }), selectedGroup.primaryProfile.description && (_jsx("div", { className: styles.aiProfileDetailDescription, children: selectedGroup.primaryProfile.description })), _jsxs("div", { className: styles.aiProfileSignatureColor, children: [_jsx("span", { children: "Signature color" }), _jsx("span", { className: styles.aiProfileSignatureSwatch, style: { backgroundColor: selectedGroup.primaryProfile.color }, "aria-hidden": "true" }), _jsx("span", { children: selectedGroup.primaryProfile.color })] })] })] }), _jsxs("div", { className: styles.aiProfileDetailDataList, children: [_jsx("div", { className: styles.aiProfileDetailDataGroup, children: selectedProfileDataGroups.attributes.map((row) => (_jsxs("div", { className: styles.aiProfileDetailDataRow, children: [_jsx("span", { className: styles.aiProfileDetailDataLabel, children: row.label }), row.label === 'Category' ? (_jsx("span", { className: styles.aiProfileDetailDataValue, children: _jsxs("select", { className: styles.aiProfileStatusSelect, "aria-label": "Agent category", value: selectedGroup.primaryProfile.surfaceType ?? '', disabled: aiProfileCategoryBusy, onChange: (event) => {
464
+ void handleCategoryChange(selectedGroup, event.target.value);
465
+ }, children: [_jsx("option", { value: "", children: "Unclassified" }), AI_PROFILE_SURFACE_TYPES.map((surfaceType) => (_jsx("option", { value: surfaceType, children: formatAiProfileSurfaceTypeLabel(surfaceType) }, surfaceType)))] }) })) : (_jsx("span", { className: styles.aiProfileDetailDataValue, children: row.value }))] }, row.label))) }), _jsx("div", { className: `${styles.aiProfileDetailDataGroup} ${styles.aiProfileDetailDateGroup}`, children: selectedProfileDataGroups.dates.map((row) => (_jsxs("div", { className: styles.aiProfileDetailDataRow, children: [_jsx("span", { className: styles.aiProfileDetailDataLabel, children: row.label }), row.label === 'Status' ? (_jsx("span", { className: styles.aiProfileDetailDataValue, children: _jsxs("select", { className: styles.aiProfileStatusSelect, "aria-label": "Agent roster status", value: selectedGroup.primaryProfile.archivedAt ? 'retired' : 'active', onChange: (event) => {
363
466
  if (event.target.value === 'retire') {
364
467
  void handleArchive(selectedGroup.primaryProfile);
365
468
  }
366
- }, children: [_jsx("option", { value: "active", children: "Active" }), selectedGroup.primaryProfile.archivedAt ? (_jsx("option", { value: "retired", children: "Retired" })) : (_jsx("option", { value: "retire", children: "Retire from roster" }))] }) })) : (_jsx("span", { className: styles.aiProfileDetailDataValue, children: row.value }))] }, row.label))) })] }), selectedGroupHasDuplicates ? (_jsxs("div", { className: styles.aiProfileInstanceSection, children: [_jsxs("div", { className: styles.aiProfileInstanceSectionHeader, children: [_jsx("span", { children: "Profile instances" }), _jsx("span", { className: styles.settingsHint, children: "Choose a keeper here if duplicates need to be merged." })] }), _jsx("div", { className: styles.aiProfileInstanceList, children: selectedGroup.profiles.map((profile) => (_jsxs("div", { className: styles.aiProfileInstanceCard, children: [_jsxs("div", { className: styles.aiProfileInstanceTopRow, children: [_jsxs("div", { children: [_jsxs("div", { className: styles.aiProfileInstanceName, children: ["@", profile.username] }), _jsx("div", { className: styles.aiProfileIdChip, children: profile.id })] }), aiProfileMergeState?.keepId === profile.id && (_jsx("span", { className: styles.aiProfileKeepBadge, children: "Keeping" }))] }), _jsxs("div", { className: styles.aiProfileInstanceMeta, children: [_jsxs("span", { children: ["Created ", formatTimestamp(profile.createdAt)] }), _jsxs("span", { children: ["Updated ", formatTimestamp(profile.updatedAt)] })] }), aiProfileMergeState?.keepId !== profile.id && (_jsx("button", { className: styles.secondaryHeaderBtn, title: "Keep this profile, merge others into it", onClick: () => {
469
+ }, children: [_jsx("option", { value: "active", children: "Active" }), selectedGroup.primaryProfile.archivedAt ? (_jsx("option", { value: "retired", children: "Retired" })) : (_jsx("option", { value: "retire", children: "Retire from roster" }))] }) })) : (_jsx("span", { className: styles.aiProfileDetailDataValue, children: row.value }))] }, row.label))) })] }), aiProfileCategoryError?.profileId === selectedGroup.primaryProfile.id && (_jsxs("div", { className: styles.aiProfileInlineError, role: "alert", children: [_jsx(AlertTriangle, { size: 12 }), _jsx("span", { children: aiProfileCategoryError.message })] })), selectedGroupHasDuplicates ? (_jsxs("div", { className: styles.aiProfileInstanceSection, children: [_jsxs("div", { className: styles.aiProfileInstanceSectionHeader, children: [_jsx("span", { children: "Profile instances" }), _jsx("span", { className: styles.settingsHint, children: "Choose a keeper here if duplicates need to be merged." })] }), _jsx("div", { className: styles.aiProfileInstanceList, children: selectedGroup.profiles.map((profile) => (_jsxs("div", { className: styles.aiProfileInstanceCard, children: [_jsxs("div", { className: styles.aiProfileInstanceTopRow, children: [_jsxs("div", { children: [_jsxs("div", { className: styles.aiProfileInstanceName, children: ["@", profile.username] }), _jsx("div", { className: styles.aiProfileIdChip, children: profile.id })] }), aiProfileMergeState?.keepId === profile.id && (_jsx("span", { className: styles.aiProfileKeepBadge, children: "Keeping" }))] }), _jsxs("div", { className: styles.aiProfileInstanceMeta, children: [_jsxs("span", { children: ["Created ", formatTimestamp(profile.createdAt)] }), _jsxs("span", { children: ["Updated ", formatTimestamp(profile.updatedAt)] })] }), aiProfileMergeState?.keepId !== profile.id && (_jsx("button", { className: styles.secondaryHeaderBtn, title: "Keep this profile, merge others into it", onClick: () => {
367
470
  const otherId = selectedGroup.profiles.find((candidate) => candidate.id !== profile.id)?.id;
368
471
  if (otherId)
369
472
  setAiProfileMergeState({ keepId: profile.id, mergeId: otherId });
370
- }, children: "Keep this" })), aiProfileArchiveError?.profileId === profile.id && (_jsxs("div", { className: styles.aiProfileInlineError, role: "alert", children: [_jsx(AlertTriangle, { size: 12 }), _jsx("span", { children: aiProfileArchiveError.message })] })), _jsx("button", { className: styles.aiProfileDangerTextButton, title: "Remove this profile from the active roster", onClick: () => { void handleArchive(profile); }, children: "Remove from roster" })] }, profile.id))) }), aiProfileMergeState && selectedGroup.profiles.some((profile) => profile.id === aiProfileMergeState.keepId) && (_jsxs("div", { className: styles.aiProfileMergeActions, children: [_jsx("button", { className: styles.dangerBtn, onClick: () => { void handleMerge(aiProfileMergeState.keepId, aiProfileMergeState.mergeId); }, children: "Merge duplicates" }), _jsx("button", { className: styles.secondaryHeaderBtn, onClick: () => setAiProfileMergeState(null), children: "Cancel" })] }))] })) : (_jsxs("div", { className: styles.aiProfileIdFooter, children: [_jsx("div", { className: styles.aiProfileIdFooterValue, children: selectedGroup.primaryProfile.id }), aiProfileArchiveError?.profileId === selectedGroup.primaryProfile.id && (_jsxs("div", { className: styles.aiProfileInlineError, role: "alert", children: [_jsx(AlertTriangle, { size: 12 }), _jsx("span", { children: aiProfileArchiveError.message })] }))] }))] }) }))] })), aiProfileMergeStatus && (_jsxs("div", { className: `${aiProfileMergeStatus.type === 'success' ? styles.successMessage : styles.errorMessage} ${styles.marginTop12}`, children: [aiProfileMergeStatus.type === 'success' ? _jsx(Check, { size: 14 }) : _jsx(AlertTriangle, { size: 14 }), aiProfileMergeStatus.message] }))] }) }) }), _jsx(AvatarImageManagerModal, { isOpen: !!avatarEditorProfile, theme: "dark", title: "Edit AI Profile Photo", currentImageUrl: avatarEditorProfile?.avatarUrl || '', editorImageUrl: avatarEditorProfile?.avatarSourceUrl || '', fallbackInitial: (avatarEditorProfile?.name || 'AI').charAt(0).toUpperCase(), accept: "image/png,image/jpeg,image/webp,image/gif", busy: aiAvatarBusy, hasPendingImage: false, canRemove: !!avatarEditorProfile?.avatarUrl, error: aiAvatarError, notice: null, onClose: () => {
473
+ }, children: "Keep this" })), aiProfileArchiveError?.profileId === profile.id && (_jsxs("div", { className: styles.aiProfileInlineError, role: "alert", children: [_jsx(AlertTriangle, { size: 12 }), _jsx("span", { children: aiProfileArchiveError.message })] })), _jsx("button", { className: styles.aiProfileDangerTextButton, title: "Remove this profile from the active roster", onClick: () => { void handleArchive(profile); }, children: "Remove from roster" })] }, profile.id))) }), aiProfileMergeState && selectedGroup.profiles.some((profile) => profile.id === aiProfileMergeState.keepId) && (_jsxs("div", { className: styles.aiProfileMergeActions, children: [_jsx("button", { className: styles.dangerBtn, onClick: () => { void handleMerge(aiProfileMergeState.keepId, aiProfileMergeState.mergeId); }, children: "Merge duplicates" }), _jsx("button", { className: styles.secondaryHeaderBtn, onClick: () => setAiProfileMergeState(null), children: "Cancel" })] }))] })) : (_jsxs("div", { className: styles.aiProfileIdFooter, children: [_jsx("div", { className: styles.aiProfileIdFooterValue, children: selectedGroup.primaryProfile.id }), aiProfileArchiveError?.profileId === selectedGroup.primaryProfile.id && (_jsxs("div", { className: styles.aiProfileInlineError, role: "alert", children: [_jsx(AlertTriangle, { size: 12 }), _jsx("span", { children: aiProfileArchiveError.message })] }))] }))] }) }))] })), aiProfileMergeStatus && (_jsxs("div", { className: `${aiProfileMergeStatus.type === 'success' ? styles.successMessage : styles.errorMessage} ${styles.marginTop12}`, children: [aiProfileMergeStatus.type === 'success' ? _jsx(Check, { size: 14 }) : _jsx(AlertTriangle, { size: 14 }), aiProfileMergeStatus.message] }))] }) }) }), _jsx(AvatarImageManagerModal, { isOpen: !!avatarEditorProfile, theme: "dark", title: "Edit AI Profile Photo", currentImageUrl: getAiProfileAvatarDisplayUrl(avatarEditorProfile), editorImageUrl: getAiProfileAvatarSourceUrl(avatarEditorProfile), fallbackInitial: (avatarEditorProfile?.name || 'AI').charAt(0).toUpperCase(), accept: "image/png,image/jpeg,image/webp,image/gif", busy: aiAvatarBusy, hasPendingImage: false, canRemove: !!avatarEditorProfile?.avatarUrl, error: aiAvatarError, notice: null, onClose: () => {
371
474
  if (aiAvatarBusy)
372
475
  return;
373
476
  setAvatarEditorProfileId(null);