@taskforcehq/taskforce 0.3.308 → 0.3.311

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 (54) hide show
  1. package/README.md +59 -135
  2. package/dist/components/context/ContextAttachmentManager.js +4 -3
  3. package/dist/components/ui/DisclosureSectionHeader.d.ts +12 -0
  4. package/dist/components/ui/DisclosureSectionHeader.js +8 -0
  5. package/dist/components/views/PlanComparisonPage.js +1 -1
  6. package/dist/components/views/PlanComparisonPage.test.js +1 -0
  7. package/dist/components/views/StandaloneLayout.js +21 -4
  8. package/dist/components/views/panels/PlanningDrawer.js +2 -1
  9. package/dist/components/views/panels/PlanningDrawer.test.js +2 -2
  10. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -1
  11. package/dist/components/views/standalone/modals/AccountHubModal.js +3 -2
  12. package/dist/components/views/standalone/modals/AccountHubModal.test.js +18 -12
  13. package/dist/components/views/standalone/modals/HelpModal.d.ts +3 -1
  14. package/dist/components/views/standalone/modals/HelpModal.js +4 -2
  15. package/dist/components/views/standalone/modals/HelpModal.test.d.ts +1 -0
  16. package/dist/components/views/standalone/modals/HelpModal.test.js +23 -0
  17. package/dist/core/PlanEntitlementService.d.ts +2 -0
  18. package/dist/core/PlanEntitlementService.js +57 -4
  19. package/dist/core/PlanVersionPolicy.test.js +32 -0
  20. package/dist/localization/locales/en-US.js +1 -1
  21. package/dist/localization/locales/es-419.js +1 -1
  22. package/dist/localization/locales/pt-BR.js +1 -1
  23. package/dist/mcp/runtime.js +49 -14
  24. package/dist/mcp/runtime.test.js +91 -0
  25. package/dist/server/routes/billing.js +0 -1
  26. package/dist/server/routes/billing.test.js +60 -0
  27. package/dist/ui/assets/AgentsModule-D8u9P5dZ.js +1 -0
  28. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CCckzWYZ.js → AnnotatedAttachmentWorkspace-CsUauFA2.js} +1 -1
  29. package/dist/ui/assets/ContextAttachmentManager-DbQvDHFz.js +3 -0
  30. package/dist/ui/assets/{DocumentWorkspace-BVaWPA25.js → DocumentWorkspace-BmTkN1A8.js} +1 -1
  31. package/dist/ui/assets/{EntityActivityTimeline-DSj0ThaK.js → EntityActivityTimeline-BXHTM-Nh.js} +1 -1
  32. package/dist/ui/assets/{InitiativesModule-AxMkWtxH.js → InitiativesModule-D33RyXx0.js} +1 -1
  33. package/dist/ui/assets/PlansPage-D0RXlT9_.js +1 -0
  34. package/dist/ui/assets/PlansPage-DeAzWYTb.css +1 -0
  35. package/dist/ui/assets/{TaskContextUpload-Dw4rTF4i.js → TaskContextUpload-Vsg67lpR.js} +1 -1
  36. package/dist/ui/assets/{TaskSettings-f2ga42_V.js → TaskSettings-DshLw7_d.js} +1 -1
  37. package/dist/ui/assets/{WorkflowsModule-E4UL3mov.js → WorkflowsModule-CLM-1BnF.js} +1 -1
  38. package/dist/ui/assets/documentReferences-BPWp6ce0.js +1 -0
  39. package/dist/ui/assets/{index-BvAbqx5Q.css → index-DOR9PRqt.css} +1 -1
  40. package/dist/ui/assets/index-zvbo2Jse.js +5 -0
  41. package/dist/ui/assets/{vendor-icons-B32hGuF2.js → vendor-icons-CLnehDTw.js} +1 -1
  42. package/dist/ui/index.html +3 -3
  43. package/dist/utils/supportContact.d.ts +9 -0
  44. package/dist/utils/supportContact.js +32 -0
  45. package/dist/utils/supportContact.test.d.ts +1 -0
  46. package/dist/utils/supportContact.test.js +19 -0
  47. package/package.json +1 -1
  48. package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +0 -1
  49. package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +0 -3
  50. package/dist/ui/assets/PlansPage-B_z-D6Nh.css +0 -1
  51. package/dist/ui/assets/PlansPage-Ndt7mYfo.js +0 -1
  52. package/dist/ui/assets/documentReferences-BOUcJm6-.js +0 -1
  53. package/dist/ui/assets/index-MAHKvFqp.js +0 -4
  54. /package/dist/ui/agent-logos/{ChatGPT.png → chatgpt.png} +0 -0
@@ -36,6 +36,7 @@ export declare class PlanEntitlementService {
36
36
  private readPlanVersionTrialDays;
37
37
  private applyPlanVersionTrialDaysMetadata;
38
38
  private sanitizePlanFeatureConfig;
39
+ private hydratePlanFeatureConfig;
39
40
  private isEntitlementStateAllowed;
40
41
  private isIsoTimestampExpired;
41
42
  private readAccountEntitlementRow;
@@ -50,6 +51,7 @@ export declare class PlanEntitlementService {
50
51
  normalizeLegacyPlanDataIfNeeded(): void;
51
52
  private renameLegacyWorkspacesFeatureKey;
52
53
  private removeLegacyPlanFeatureRows;
54
+ private hydrateMissingPlanFeatureTemplateConfigs;
53
55
  private migrateLegacySeatLimitFeatureConfigToVersionSeatLimit;
54
56
  private normalizeLegacyBillingMirrorRows;
55
57
  backfillAccountEntitlements(): void;
@@ -265,6 +265,19 @@ export class PlanEntitlementService {
265
265
  }
266
266
  return config && typeof config === 'object' ? { ...config } : {};
267
267
  }
268
+ hydratePlanFeatureConfig(featureKey, access, config) {
269
+ const normalizedFeatureKey = this.canonicalizePlanFeatureKey(featureKey);
270
+ const sanitizedConfig = this.sanitizePlanFeatureConfig(normalizedFeatureKey, access, config);
271
+ if (Object.keys(sanitizedConfig).length > 0)
272
+ return sanitizedConfig;
273
+ const catalogFeature = DEFAULT_PLAN_FEATURE_CATALOG.find((feature) => feature.featureKey === normalizedFeatureKey);
274
+ const template = catalogFeature?.configTemplate && typeof catalogFeature.configTemplate === 'object'
275
+ ? catalogFeature.configTemplate
276
+ : {};
277
+ if (Object.keys(template).length === 0)
278
+ return {};
279
+ return this.sanitizePlanFeatureConfig(normalizedFeatureKey, access, template);
280
+ }
268
281
  isEntitlementStateAllowed(state) {
269
282
  return state === 'active' || state === 'trialing' || state === 'grace';
270
283
  }
@@ -401,6 +414,7 @@ export class PlanEntitlementService {
401
414
  this.migrateLegacySeatLimitFeatureConfigToVersionSeatLimit();
402
415
  this.normalizeLegacyBillingMirrorRows();
403
416
  this.renameLegacyWorkspacesFeatureKey();
417
+ this.hydrateMissingPlanFeatureTemplateConfigs();
404
418
  this.removeLegacyPlanFeatureRows();
405
419
  }
406
420
  renameLegacyWorkspacesFeatureKey() {
@@ -453,6 +467,43 @@ export class PlanEntitlementService {
453
467
  AND feature_key IN (${placeholders})
454
468
  `).run(this.tenantId, ...featureKeys);
455
469
  }
470
+ hydrateMissingPlanFeatureTemplateConfigs() {
471
+ const rows = this.db.prepare(`
472
+ SELECT plan_version_id, feature_key, access, config_json
473
+ FROM plan_feature_matrix
474
+ WHERE tenant_id = ?
475
+ `).all(this.tenantId);
476
+ if (rows.length < 1)
477
+ return;
478
+ const now = new Date().toISOString();
479
+ const updates = [];
480
+ for (const row of rows) {
481
+ const planVersionId = String(row.plan_version_id || '').trim();
482
+ const featureKey = this.canonicalizePlanFeatureKey(row.feature_key);
483
+ if (!planVersionId || !featureKey)
484
+ continue;
485
+ const access = this.normalizeFeatureAccess(row.access);
486
+ const currentConfig = this.parseJsonObject(row.config_json || '{}');
487
+ const hydratedConfig = this.hydratePlanFeatureConfig(featureKey, access, currentConfig);
488
+ const currentConfigJson = JSON.stringify(currentConfig);
489
+ const hydratedConfigJson = JSON.stringify(hydratedConfig);
490
+ if (currentConfigJson === hydratedConfigJson)
491
+ continue;
492
+ updates.push({ planVersionId, featureKey, configJson: hydratedConfigJson });
493
+ }
494
+ if (updates.length < 1)
495
+ return;
496
+ const tx = this.db.transaction(() => {
497
+ for (const update of updates) {
498
+ this.db.prepare(`
499
+ UPDATE plan_feature_matrix
500
+ SET config_json = ?, updated_at = ?
501
+ WHERE tenant_id = ? AND plan_version_id = ? AND feature_key = ?
502
+ `).run(update.configJson, now, this.tenantId, update.planVersionId, update.featureKey);
503
+ }
504
+ });
505
+ tx();
506
+ }
456
507
  migrateLegacySeatLimitFeatureConfigToVersionSeatLimit() {
457
508
  const rows = this.db.prepare(`
458
509
  SELECT pfm.plan_version_id, pfm.config_json, pv.seat_limit
@@ -1281,10 +1332,11 @@ export class PlanEntitlementService {
1281
1332
  const featureKey = String(featureRow.feature_key || '').trim();
1282
1333
  if (!featureKey)
1283
1334
  continue;
1335
+ const access = this.normalizeFeatureAccess(featureRow.access);
1284
1336
  features[featureKey] = {
1285
1337
  featureKey,
1286
- access: this.normalizeFeatureAccess(featureRow.access),
1287
- config: this.parseJsonObject(featureRow.config_json || '{}')
1338
+ access,
1339
+ config: this.hydratePlanFeatureConfig(featureKey, access, this.parseJsonObject(featureRow.config_json || '{}'))
1288
1340
  };
1289
1341
  }
1290
1342
  const metadata = this.parseJsonObject(row.metadata_json || '{}');
@@ -1945,10 +1997,11 @@ export class PlanEntitlementService {
1945
1997
  if (key === WORKSPACES_FEATURE_KEY && originalKey === LEGACY_WORKSPACES_FEATURE_KEY && features[key]) {
1946
1998
  continue;
1947
1999
  }
2000
+ const access = this.normalizeFeatureAccess(featureRow.access);
1948
2001
  features[key] = {
1949
2002
  featureKey: key,
1950
- access: this.normalizeFeatureAccess(featureRow.access),
1951
- config: this.parseJsonObject(featureRow.config_json || '{}')
2003
+ access,
2004
+ config: this.hydratePlanFeatureConfig(key, access, this.parseJsonObject(featureRow.config_json || '{}'))
1952
2005
  };
1953
2006
  }
1954
2007
  const metadata = this.parseJsonObject(row.metadata_json || '{}');
@@ -153,6 +153,38 @@ describe('TaskforceCore plan version policy', () => {
153
153
  fs.rmSync(projectRoot, { recursive: true, force: true });
154
154
  }
155
155
  });
156
+ it('hydrates missing built-in feature config templates', () => {
157
+ const projectRoot = makeProjectRoot();
158
+ const core = openCore(projectRoot);
159
+ try {
160
+ core.updatePlanVersionFeatures({
161
+ planVersionId: 'free-v1',
162
+ features: [
163
+ { featureKey: 'context.uploads', access: 'enabled', config: { storageLimitMb: 64 } }
164
+ ]
165
+ });
166
+ const db = core.db;
167
+ db.prepare(`
168
+ UPDATE plan_feature_matrix
169
+ SET config_json = '{}'
170
+ WHERE tenant_id = ? AND plan_version_id = ? AND feature_key = ?
171
+ `).run('default', 'free-v1', 'context.uploads');
172
+ expect(core.getPlanVersionFeatures('free', 'free-v1').features['context.uploads']?.config).toEqual({
173
+ storageLimitMb: 1024
174
+ });
175
+ core.planEntitlementService.normalizeLegacyPlanDataIfNeeded();
176
+ const row = db.prepare(`
177
+ SELECT config_json
178
+ FROM plan_feature_matrix
179
+ WHERE tenant_id = ? AND plan_version_id = ? AND feature_key = ?
180
+ `).get('default', 'free-v1', 'context.uploads');
181
+ expect(JSON.parse(row?.config_json || '{}')).toEqual({ storageLimitMb: 1024 });
182
+ }
183
+ finally {
184
+ core.close();
185
+ fs.rmSync(projectRoot, { recursive: true, force: true });
186
+ }
187
+ });
156
188
  it('migrates legacy workspace feature keys to the canonical key', () => {
157
189
  const projectRoot = makeProjectRoot();
158
190
  const core = openCore(projectRoot);
@@ -92,7 +92,7 @@ export const enUS = {
92
92
  enterZenMode: 'Enter Zen Mode',
93
93
  toggleFilters: 'Toggle Filters',
94
94
  addTask: 'Add Task',
95
- helpTutorial: 'Help / Tutorial',
95
+ helpTutorial: 'Help & Support',
96
96
  settings: 'Settings',
97
97
  filtersCaps: 'FILTERS:',
98
98
  categoryLabel: 'Category',
@@ -92,7 +92,7 @@ export const es419 = {
92
92
  enterZenMode: 'Entrar en modo zen',
93
93
  toggleFilters: 'Alternar filtros',
94
94
  addTask: 'Agregar tarea',
95
- helpTutorial: 'Ayuda / Tutorial',
95
+ helpTutorial: 'Ayuda y soporte',
96
96
  settings: 'Configuracion',
97
97
  filtersCaps: 'FILTROS:',
98
98
  categoryLabel: 'Categoria',
@@ -92,7 +92,7 @@ export const ptBR = {
92
92
  enterZenMode: 'Entrar no modo zen',
93
93
  toggleFilters: 'Alternar filtros',
94
94
  addTask: 'Adicionar tarefa',
95
- helpTutorial: 'Ajuda / Tutorial',
95
+ helpTutorial: 'Ajuda e suporte',
96
96
  settings: 'Configuracoes',
97
97
  filtersCaps: 'FILTROS:',
98
98
  categoryLabel: 'Categoria',
@@ -69,25 +69,60 @@ function formatTaskIdentifier(task) {
69
69
  return id;
70
70
  return 'unknown task';
71
71
  }
72
+ const toolLogExcerptKeys = new Set(['description', 'text', 'content']);
73
+ const TOOL_LOG_REDACTED_VALUE = '[redacted]';
74
+ const TOOL_LOG_TRUNCATED_DEPTH_VALUE = '[truncated-depth]';
75
+ const TOOL_LOG_MAX_STRING_LENGTH = 160;
76
+ const TOOL_LOG_MAX_ARRAY_ITEMS = 8;
77
+ const toolLogSensitiveWordSet = new Set(['token', 'tokens', 'password', 'passwords', 'secret', 'secrets', 'authorization']);
78
+ function truncateToolLogString(value) {
79
+ return `${value.slice(0, TOOL_LOG_MAX_STRING_LENGTH)}${value.length > TOOL_LOG_MAX_STRING_LENGTH ? '…' : ''}`;
80
+ }
81
+ function shouldRedactToolLogKey(key) {
82
+ const words = String(key || '')
83
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
84
+ .split(/[^a-zA-Z0-9]+/)
85
+ .map((part) => part.trim().toLowerCase())
86
+ .filter(Boolean);
87
+ if (words.length === 0)
88
+ return false;
89
+ if (words.some((word) => toolLogSensitiveWordSet.has(word)))
90
+ return true;
91
+ const compact = words.join('');
92
+ return compact === 'apikey'
93
+ || compact === 'clientsecret'
94
+ || compact === 'bearertoken'
95
+ || compact === 'proxyauthorization';
96
+ }
72
97
  function sanitizeToolArgsForLog(args) {
73
- if (!args || typeof args !== 'object' || Array.isArray(args))
74
- return args;
75
- const input = args;
76
- const output = {};
77
- for (const [key, value] of Object.entries(input)) {
78
- if (key === 'description' || key === 'text' || key === 'content') {
79
- output[key] = typeof value === 'string'
80
- ? `${String(value).slice(0, 160)}${String(value).length > 160 ? '' : ''}`
98
+ const visit = (value, depth, key) => {
99
+ if (key && shouldRedactToolLogKey(key))
100
+ return TOOL_LOG_REDACTED_VALUE;
101
+ if (value === null || value === undefined)
102
+ return value;
103
+ if (depth > 6)
104
+ return TOOL_LOG_TRUNCATED_DEPTH_VALUE;
105
+ if (typeof value === 'string') {
106
+ return key && toolLogExcerptKeys.has(key)
107
+ ? truncateToolLogString(value)
81
108
  : value;
82
- continue;
83
109
  }
84
110
  if (Array.isArray(value)) {
85
- output[key] = value.length > 8 ? [...value.slice(0, 8), `…(${value.length - 8} more)`] : value;
86
- continue;
111
+ const items = value.slice(0, TOOL_LOG_MAX_ARRAY_ITEMS).map((item) => visit(item, depth + 1));
112
+ if (value.length > TOOL_LOG_MAX_ARRAY_ITEMS) {
113
+ items.push(`…(${value.length - TOOL_LOG_MAX_ARRAY_ITEMS} more)`);
114
+ }
115
+ return items;
87
116
  }
88
- output[key] = value;
89
- }
90
- return output;
117
+ if (typeof value !== 'object')
118
+ return value;
119
+ const output = {};
120
+ for (const [childKey, childValue] of Object.entries(value)) {
121
+ output[childKey] = visit(childValue, depth + 1, childKey);
122
+ }
123
+ return output;
124
+ };
125
+ return visit(args, 0);
91
126
  }
92
127
  function resolveRecoverableToolError(error) {
93
128
  const message = error instanceof Error ? String(error.message || '').trim() : '';
@@ -4,6 +4,7 @@ import os from 'os';
4
4
  import path from 'path';
5
5
  import { createHash, randomBytes } from 'crypto';
6
6
  import { createTaskforceMcpServer } from './runtime';
7
+ import * as canonicalAssetHelpersModule from './canonicalAssetHelpers';
7
8
  import { MCP_TOOL_REGISTRY } from './toolRegistry';
8
9
  import { TaskforceCore, TaskforceRuleError } from '../core/Taskforce';
9
10
  import { WorkspaceAssetStore } from '../storage/workspaceAssetStore';
@@ -1256,6 +1257,96 @@ describe('Taskforce MCP tool descriptions', () => {
1256
1257
  fs.rmSync(projectRoot, { recursive: true, force: true });
1257
1258
  }
1258
1259
  });
1260
+ it('redacts secret-bearing arguments from mutating MCP failure logs', async () => {
1261
+ const projectRoot = createProjectRoot();
1262
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
1263
+ try {
1264
+ const resolved = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
1265
+ name: 'Codex'
1266
+ }, { resolveProfile: false }));
1267
+ const result = await callTool(projectRoot, 'local', 'start_task', {
1268
+ id: 'T-999999',
1269
+ profileToken: resolved.profile.profileToken,
1270
+ headers: {
1271
+ authorization: 'Bearer mutating-secret'
1272
+ },
1273
+ nested: {
1274
+ apiKey: 'sk-live-mutating-secret'
1275
+ },
1276
+ text: 'x'.repeat(220),
1277
+ }, { resolveProfile: false });
1278
+ expect(result).toBeTruthy();
1279
+ const logEntry = warnSpy.mock.calls.find(([message]) => message === '[Taskforce MCP] start_task failed');
1280
+ expect(logEntry).toBeTruthy();
1281
+ const payload = logEntry?.[1];
1282
+ expect(payload.args).toMatchObject({
1283
+ profileToken: '[redacted]',
1284
+ headers: {
1285
+ authorization: '[redacted]'
1286
+ },
1287
+ nested: {
1288
+ apiKey: '[redacted]'
1289
+ },
1290
+ text: `${'x'.repeat(160)}…`,
1291
+ });
1292
+ expect(JSON.stringify(payload)).not.toContain(resolved.profile.profileToken);
1293
+ expect(JSON.stringify(payload)).not.toContain('mutating-secret');
1294
+ expect(JSON.stringify(payload)).not.toContain('sk-live-mutating-secret');
1295
+ }
1296
+ finally {
1297
+ warnSpy.mockRestore();
1298
+ fs.rmSync(projectRoot, { recursive: true, force: true });
1299
+ }
1300
+ });
1301
+ it('redacts secret-bearing arguments from MCP read retry logs', async () => {
1302
+ const projectRoot = createProjectRoot();
1303
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
1304
+ const originalCreateCanonicalAssetHelpers = canonicalAssetHelpersModule.createCanonicalAssetHelpers;
1305
+ let readAttempts = 0;
1306
+ const helperSpy = vi.spyOn(canonicalAssetHelpersModule, 'createCanonicalAssetHelpers').mockImplementation((...args) => {
1307
+ const helpers = originalCreateCanonicalAssetHelpers(...args);
1308
+ return {
1309
+ ...helpers,
1310
+ readCanonicalDocumentBuffer: async (...bufferArgs) => {
1311
+ readAttempts += 1;
1312
+ if (readAttempts === 1) {
1313
+ throw new Error('database timeout while reading canonical document');
1314
+ }
1315
+ return await helpers.readCanonicalDocumentBuffer(...bufferArgs);
1316
+ }
1317
+ };
1318
+ });
1319
+ try {
1320
+ seedDocumentReviewFixture(projectRoot);
1321
+ const result = parseToolJson(await callTool(projectRoot, 'local', 'read_document_content', {
1322
+ documentId: 'D-123',
1323
+ profileToken: 'tfp_retry_secret',
1324
+ requestHeaders: {
1325
+ authorization: 'Bearer retry-secret'
1326
+ },
1327
+ }, { workspaceId: 'workspace-1' }));
1328
+ expect(result.document.referenceLabel).toBe('D-123');
1329
+ const retryLog = warnSpy.mock.calls
1330
+ .map(([message]) => String(message))
1331
+ .find((message) => message.includes('"event":"tool_read_retry"'));
1332
+ expect(retryLog).toBeTruthy();
1333
+ const retryPayload = JSON.parse(String(retryLog).replace(/^\[Taskforce MCP\] /, ''));
1334
+ expect(retryPayload.args).toMatchObject({
1335
+ documentId: 'D-123',
1336
+ profileToken: '[redacted]',
1337
+ requestHeaders: {
1338
+ authorization: '[redacted]'
1339
+ }
1340
+ });
1341
+ expect(retryLog).not.toContain('tfp_retry_secret');
1342
+ expect(retryLog).not.toContain('retry-secret');
1343
+ }
1344
+ finally {
1345
+ helperSpy.mockRestore();
1346
+ warnSpy.mockRestore();
1347
+ fs.rmSync(projectRoot, { recursive: true, force: true });
1348
+ }
1349
+ });
1259
1350
  it('reports token_lookup binding when get_current_profile is inspected with an explicit profileToken', async () => {
1260
1351
  const projectRoot = createProjectRoot();
1261
1352
  try {
@@ -497,7 +497,6 @@ function resolvePlanVersionIdForPriceIdFallback(core, priceIdRaw) {
497
497
  FROM billing_price_mappings
498
498
  WHERE tenant_id = ?
499
499
  AND stripe_price_id = ?
500
- AND COALESCE(pricing_audience, 'public') = 'public'
501
500
  AND active = 1
502
501
  ORDER BY plan_version_id ASC
503
502
  `).all(tenantId, priceId);
@@ -3395,6 +3395,66 @@ describe('Billing Webhook Routes', () => {
3395
3395
  `).get();
3396
3396
  expect(entitlement.plan_version_id).toBe('team-v1');
3397
3397
  });
3398
+ it('resolves campaign price mappings during webhook fallback when Stripe metadata is absent', async () => {
3399
+ const now = new Date().toISOString();
3400
+ db.prepare(`
3401
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
3402
+ VALUES ('team-v1', 'default', 'team', 1, 1, '{}', ?, ?)
3403
+ `).run(now, now);
3404
+ db.prepare(`
3405
+ INSERT INTO billing_price_mappings (
3406
+ tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
3407
+ ) VALUES ('default', 'team-v1', 'month', 'campaign', 'price_team_campaign', 'stripe', 1, 900, 'usd', ?, ?)
3408
+ `).run(now, now);
3409
+ db.prepare(`
3410
+ INSERT INTO user_billing (user_id, tenant_id, stripe_customer_id, entitlement_state, created_at, updated_at)
3411
+ VALUES ('u1', 'default', 'cus_123', 'active', ?, ?)
3412
+ `).run(now, now);
3413
+ mockConstructEvent.mockReturnValue({
3414
+ id: 'evt_campaign_price_fallback',
3415
+ type: 'customer.subscription.updated',
3416
+ data: {
3417
+ object: {
3418
+ id: 'sub_123',
3419
+ customer: 'cus_123',
3420
+ status: 'active',
3421
+ trial_end: null,
3422
+ metadata: {},
3423
+ items: {
3424
+ data: [{
3425
+ current_period_end: Math.floor(Date.now() / 1000) + 86400,
3426
+ price: { id: 'price_team_campaign', recurring: { interval: 'month' } }
3427
+ }]
3428
+ }
3429
+ }
3430
+ }
3431
+ });
3432
+ const req = createMockReq('{"id":"evt_campaign_price_fallback"}', {
3433
+ 'x-taskforce-runtime-mode': 'cloud',
3434
+ 'stripe-signature': 'sig'
3435
+ });
3436
+ const res = createMockRes();
3437
+ await webhookHandler(req, res, {});
3438
+ expect(res.statusCode).toBe(200);
3439
+ const entitlement = db.prepare(`
3440
+ SELECT plan_version_id, entitlement_state
3441
+ FROM account_entitlements
3442
+ WHERE account_id = 'u1' AND tenant_id = 'default'
3443
+ LIMIT 1
3444
+ `).get();
3445
+ expect(entitlement).toEqual(expect.objectContaining({
3446
+ plan_version_id: 'team-v1',
3447
+ entitlement_state: 'active'
3448
+ }));
3449
+ const failure = db.prepare(`
3450
+ SELECT reason
3451
+ FROM stripe_webhook_failures
3452
+ WHERE event_id = 'evt_campaign_price_fallback'
3453
+ ORDER BY created_at DESC
3454
+ LIMIT 1
3455
+ `).get();
3456
+ expect(failure).toBeUndefined();
3457
+ });
3398
3458
  it('logs webhook failure when shared price fallback is ambiguous and metadata is missing', async () => {
3399
3459
  const now = new Date().toISOString();
3400
3460
  db.prepare(`
@@ -0,0 +1 @@
1
+ import{r as c,j as e}from"./vendor-react-CKJs5o3c.js";import{s as Ue,p as _,u as ie,v as De,w as se,x as le,y as we,t as r,E as Te,z as Me,B as ke,C as fe,F as b,G as oe}from"./index-zvbo2Jse.js";import{a0 as Re,x as $e,h as Ee,B as ne,m as C,b as Le,$ as Be,a5 as Ge}from"./vendor-icons-CLnehDTw.js";import"./vendor-markdown-BUxTU7dS.js";import"./vendor-dnd-DRzYolkg.js";import"./vendor-router-BbWMxlnO.js";function Fe(l){return typeof l.avatarUrl=="string"&&l.avatarUrl.trim().length>0}function K(l){return l?fe(l.avatarUrl,l.avatarRevision,l.avatarUpdatedAt):""}function ze(l){return l?fe(l.avatarSourceUrl,l.avatarRevision,l.avatarUpdatedAt):""}function He(l){return l.find(Fe)||l[0]}function ce(l){return`${l.seatScope||"unknown"}:${l.name.trim().toLowerCase()}`}function Oe(l){const m=Math.max(0,l-1);return`${m} duplicate${m===1?"":"s"}`}function de(l){return!l||typeof l!="object"?null:{used:Math.max(0,Number(l.used||0)),limit:l.limit===null||l.limit===void 0?null:Math.max(0,Number(l.limit||0)),remaining:l.remaining===null||l.remaining===void 0?null:Math.max(0,Number(l.remaining||0))}}function pe(l,m){const U=m?.variant==="inline"?`${r.aiProfileSeatScopeIcon} ${r.aiProfileSeatScopeIconInline}`:m?.variant==="detail"?`${r.aiProfileSeatScopeIcon} ${r.aiProfileSeatScopeIconDetail}`:r.aiProfileSeatScopeIcon;return l==="cloud_metered"?e.jsx("span",{className:`${U} ${r.aiProfileSeatScopeIconCloud}`,title:"Cloud MCP","aria-label":"Cloud MCP",children:e.jsx(Be,{size:20})}):l==="local_unmetered"?e.jsx("span",{className:`${U} ${r.aiProfileSeatScopeIconLocal}`,title:"Local MCP","aria-label":"Local MCP",children:e.jsx(Ge,{size:20})}):null}function qe({workspaceId:l,cloudAuthConfigured:m=!1,authSessionResolved:U=!1,isAuthenticated:ue=!1,cloudAiProfileSeatUsage:me=null,agentTrayOpen:E=!1,onCloseAgentTray:he,mcpSettingsNode:V=null}){const[S,L]=c.useState([]),[ve,ge]=c.useState(null),[B,J]=c.useState(!1),[h,D]=c.useState(null),[w,v]=c.useState(null),[T,x]=c.useState(null),[W,G]=c.useState(null),[ye,Y]=c.useState(!1),[P,j]=c.useState(null),[q,Q]=c.useState(null),[X,M]=c.useState(!1),[Pe,g]=c.useState(null),k=c.useCallback(async()=>{if(l){J(!0);try{const a=`/api/taskforce/workspace/assignee-options?workspaceId=${encodeURIComponent(l)}&kind=agent`,t=await fetch(a,{credentials:"include"}),i=t.ok?await t.json().catch(()=>({})):{},o=de(i?.aiProfileSeatUsage),d=Array.isArray(i?.assignees)?i.assignees.filter(s=>s.kind==="agent").map(s=>({id:String(s.value||""),name:String(s.label||s.value||"Unknown Agent"),username:String(s.username||s.value||""),icon:String(s.icon||"Bot"),color:String(s.color||"#6B7280"),avatarUrl:typeof s.avatarUrl=="string"?s.avatarUrl:null,avatarSourceUrl:typeof s.avatarSourceUrl=="string"?s.avatarSourceUrl:null,avatarRevision:Number.isFinite(Number(s.avatarRevision))?Math.max(0,Math.floor(Number(s.avatarRevision))):0,avatarUpdatedAt:typeof s.avatarUpdatedAt=="string"?s.avatarUpdatedAt:null,kind:String(s.kind||"agent"),description:typeof s.description=="string"?s.description:null,role:typeof s.role=="string"?s.role:null,provider:typeof s.provider=="string"?s.provider:null,model:typeof s.model=="string"?s.model:null,surfaceType:_(s.surfaceType),seatScope:Ue(s.seatScope),archivedAt:typeof s.archivedAt=="string"?s.archivedAt:null,createdAt:String(s.createdAt||""),updatedAt:String(s.updatedAt||s.createdAt||""),lastActiveAt:typeof s.lastActiveAt=="string"?s.lastActiveAt:null})):[];L(d),ge(o),x(s=>s&&!d.some(p=>p.id===s.profileId)?null:s),G(s=>s&&!d.some(p=>p.id===s.profileId)?null:s)}finally{J(!1)}}},[l]);c.useEffect(()=>{k()},[k]);const R=c.useMemo(()=>{const a=new Map;for(const t of S){const i=ie(t.surfaceType);a.has(i)||a.set(i,new Map);const o=a.get(i),d=ce(t);o.has(d)||o.set(d,[]),o.get(d).push(t)}return De.map(t=>({section:t,label:se(t),groups:Array.from(a.get(t)?.entries()||[]).map(([i,o])=>({groupId:`${t}:${i}`,section:t,sectionLabel:se(t),profiles:o,primaryProfile:He(o)}))})).filter(t=>t.groups.length>0)},[S]),y=c.useMemo(()=>R.flatMap(a=>a.groups),[R]),n=c.useMemo(()=>y.find(a=>a.groupId===P)||y[0]||null,[y,P]),u=c.useMemo(()=>S.find(a=>a.id===q)||null,[S,q]);c.useEffect(()=>{if(!y.length){P!==null&&j(null);return}(!P||!y.some(a=>a.groupId===P))&&j(y[0].groupId)},[y,P]);const Ae=async(a,t)=>{v(null),x(null);try{const i=await fetch("/api/taskforce/workspace/ai-profiles/merge",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({keepId:a,mergeId:t})}),o=await i.json().catch(()=>({}));if(!i.ok){v({type:"error",message:String(o?.error||"Failed to merge AI profiles.")});return}D(null),v({type:"success",message:"AI profiles merged."}),await k(),b({workspaceId:l,profileId:a,reason:"merge"})}catch{v({type:"error",message:"Failed to merge AI profiles."})}},Z=async a=>{if(window.confirm(`Remove ${a.name} from the active roster? This frees an AI profile seat and preserves task and comment history.`)){x(null),v(null);try{const i=await fetch("/api/taskforce/workspace/ai-profiles/archive",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({profileId:a.id,reason:"manual_archive"})}),o=await i.json().catch(()=>({}));if(!i.ok){x({profileId:a.id,message:String(o?.error||"Failed to remove AI profile from roster.")});return}(h?.keepId===a.id||h?.mergeId===a.id)&&D(null),v({type:"success",message:"AI profile removed from active roster."}),await k(),b({workspaceId:l,profileId:a.id,reason:"archive"})}catch{x({profileId:a.id,message:"Failed to remove AI profile from roster."})}}},Se=async(a,t)=>{const i=_(t),o=new Set(a.profiles.map(p=>p.surfaceType??"")),d=i??"";if(o.size===1&&o.has(d))return;Y(!0),G(null),v(null);const s=[];try{for(const f of a.profiles){const I=await fetch("/api/taskforce/workspace/ai-profiles/surface-type",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({profileId:f.id,surfaceType:i})}),H=await I.json().catch(()=>({}));if(!I.ok||!H?.profile)throw new Error(String(H?.error||"Failed to update AI profile category."));const O=H.profile;s.push({...f,surfaceType:_(O.surfaceType),updatedAt:typeof O.updatedAt=="string"?O.updatedAt:f.updatedAt})}const p=new Map(s.map(f=>[f.id,f]));L(f=>f.map(I=>p.get(I.id)||I));const A=p.get(a.primaryProfile.id)||{...a.primaryProfile,surfaceType:i},Ce=ie(A.surfaceType);j(`${Ce}:${ce(A)}`),v({type:"success",message:"AI profile category updated."});for(const f of s)b({workspaceId:l,profileId:f.id,reason:"update"})}catch(p){G({profileId:a.primaryProfile.id,message:String(p?.message||"Failed to update AI profile category.")})}finally{Y(!1)}},ee=a=>new Promise((t,i)=>{const o=new FileReader;o.onload=()=>t(String(o.result||"")),o.onerror=()=>i(new Error("Failed to read image file.")),o.readAsDataURL(a)}),ae=a=>{const t=String(a?.id||"").trim();t&&L(i=>i.map(o=>o.id===t?{...o,avatarUrl:typeof a.avatarUrl=="string"?a.avatarUrl:null,avatarSourceUrl:typeof a.avatarSourceUrl=="string"?a.avatarSourceUrl:null,avatarRevision:Number.isFinite(Number(a.avatarRevision))?Math.max(0,Math.floor(Number(a.avatarRevision))):o.avatarRevision,avatarUpdatedAt:typeof a.avatarUpdatedAt=="string"?a.avatarUpdatedAt:o.avatarUpdatedAt,updatedAt:typeof a.updatedAt=="string"?a.updatedAt:o.updatedAt}:o))},xe=async(a,t,i)=>{const o=await ee(t),d=i?await ee(i):null,s={profileId:a,displayImage:{dataUrl:o,mimeType:t.type||"application/octet-stream",originalName:t.name||"display-avatar"}};i&&d&&(s.sourceImage={dataUrl:d,mimeType:i.type||"application/octet-stream",originalName:i.name||"source-avatar"});const p=await fetch("/api/taskforce/workspace/ai-profiles/avatar/upload",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)}),A=await p.json().catch(()=>({}));if(!p.ok||!A?.profile)throw new Error(String(A?.error||"Failed to update AI profile avatar."));ae(A.profile),b({workspaceId:l,profileId:a,reason:"avatar"})},je=async a=>{const t=await fetch("/api/taskforce/workspace/ai-profiles/avatar",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({profileId:a,avatarUrl:null,avatarSourceUrl:null})}),i=await t.json().catch(()=>({}));if(!t.ok||!i?.profile)throw new Error(String(i?.error||"Failed to update AI profile avatar."));ae(i.profile),b({workspaceId:l,profileId:a,reason:"avatar"})},Ne=async(a,t)=>{if(!u)return!1;if(!a.type.startsWith("image/"))return g("AI profile photo must be an image file."),!1;M(!0),g(null);try{const i=await oe(a,{maxBytes:5242880});if(i.exceededLimit)throw new Error(a.type==="image/gif"?"Animated GIF AI profile photos must be 5 MB or smaller.":"AI profile photo must be 5 MB or smaller.");const o=i.file;let d=null;if(t){const s=await oe(t,{maxBytes:5242880});if(s.exceededLimit)throw new Error(t.type==="image/gif"?"Animated GIF AI profile photos must be 5 MB or smaller.":"AI profile source photo must be 5 MB or smaller.");d=s.file}return await xe(u.id,o,d),!0}catch(i){return g(String(i?.message||"Failed to update AI profile photo.")),!1}finally{M(!1)}},Ie=async()=>{if(u){M(!0),g(null);try{await je(u.id)}catch(a){g(String(a?.message||"Failed to remove AI profile photo."))}finally{M(!1)}}},N=a=>{const t=String(a||"").trim();if(!t)return"Unknown";const i=Date.parse(t);return Number.isFinite(i)?new Date(i).toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"}):t},re=c.useMemo(()=>{if(!n)return{attributes:[],dates:[]};const a=n.primaryProfile,t=i=>i||"Not set";return{attributes:[{label:"Category",value:a.surfaceType?le(a.surfaceType):""},{label:"Connection",value:a.seatScope?we(a.seatScope):""},{label:"Provider",value:a.provider||""},{label:"Model",value:a.model||""}].map(i=>({...i,value:t(i.value)})),dates:[{label:"Status",value:a.archivedAt?"Retired":"Active"},{label:"Recruited",value:N(a.createdAt)},{label:"Last updated",value:N(a.updatedAt)},{label:"Last active",value:N(a.lastActiveAt)}].map(i=>({...i,value:t(i.value)}))}},[n]),be=!!n&&n.profiles.length>1,te=m&&U&&!ue,$=m?de(me):ve,F=te?"Log into account for Cloud agents":$?`${$.used}/${$.limit===null?"Unlimited":$.limit}`:null,z=!!V;return e.jsxs("section",{className:`${r.agentsModuleRoot} ${z?r.agentsModuleWithTray:""} ${z&&E?r.agentsModuleTrayOpen:""}`.trim(),children:[z&&e.jsxs("aside",{className:`${r.agentTrayPanel} ${E?r.agentTrayPanelOpen:""}`.trim(),"aria-label":"Agent MCP settings tray","aria-hidden":!E,children:[e.jsxs("div",{className:r.agentTrayHeader,children:[e.jsxs("span",{className:r.agentTrayTitle,children:[e.jsx(Re,{size:14}),"MCP Settings"]}),e.jsx("button",{type:"button",className:"tf-control-icon",onClick:he,title:"Collapse agent tray","aria-label":"Collapse agent tray",children:e.jsx($e,{size:16})})]}),e.jsx("div",{className:`${r.agentTrayContent} tf-scrollbar tf-scrollbar--track-transparent`,children:e.jsx("div",{className:r.agentTrayContentInner,children:V})})]}),e.jsx("div",{className:r.agentsModuleContent,children:e.jsx("div",{className:`${r.settingGroup} ${r.agentsModuleGroup}`,children:e.jsxs("div",{children:[e.jsx("h4",{className:r.settingSubTitle,children:"Registered AI Profiles"}),e.jsx("p",{className:`${r.settingsHint} ${r.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."}),F&&e.jsx("div",{className:r.aiProfileSeatSummary,children:te?F:`Registered Cloud Agents: ${F}`}),B&&e.jsxs("div",{className:r.settingsHint,children:[e.jsx(Ee,{size:13,className:r.spinner})," Loading profiles…"]}),!B&&S.length===0&&e.jsx("div",{className:r.settingsHint,children:"No AI profiles registered yet."}),!B&&R.length>0&&e.jsxs("div",{className:r.aiProfilesExplorer,children:[e.jsx("div",{className:`${r.aiProfilesListPane} tf-scrollbar tf-scrollbar--track-transparent`,children:e.jsx("div",{className:r.aiProfilesList,children:R.map(a=>e.jsxs("div",{className:r.aiProfilesSection,children:[e.jsx("div",{className:r.aiProfilesSectionHeader,children:a.label}),a.groups.map(t=>{const i=n?.groupId===t.groupId,o=t.primaryProfile;return e.jsxs("div",{role:"button",tabIndex:0,className:`${r.aiProfileGroup} ${t.profiles.length>1?r.aiProfileGroupDuplicate:""} ${i?r.aiProfileGroupSelected:""}`,onClick:()=>j(t.groupId),onKeyDown:d=>{d.key!=="Enter"&&d.key!==" "||(d.preventDefault(),j(t.groupId))},"aria-pressed":i,children:[pe(o.seatScope),e.jsxs("div",{className:r.aiProfileGroupHeader,children:[e.jsx("span",{className:r.aiProfileGroupAvatar,style:{color:o.color},children:o.avatarUrl?e.jsx("img",{src:K(o),alt:""}):e.jsx(ne,{size:22})}),e.jsxs("span",{className:r.aiProfileGroupIdentity,children:[e.jsx("span",{className:r.aiProfileName,children:o.name}),e.jsxs("span",{className:r.aiProfileHandle,children:["@",o.username]}),e.jsx("span",{className:r.aiProfileRole,children:o.role||"Role not set"}),t.profiles.length>1&&e.jsxs("span",{className:r.aiProfileDuplicateBadge,children:[e.jsx(C,{size:11})," ",Oe(t.profiles.length)]})]})]})]},t.groupId)})]},a.section))})}),n&&e.jsx("div",{className:r.aiProfileDetailPane,children:e.jsxs("div",{className:r.aiProfileDetailCard,children:[pe(n.primaryProfile.seatScope,{variant:"detail"}),e.jsxs("div",{className:r.aiProfileDetailHero,children:[e.jsx(Te,{label:"Edit AI profile photo",imageUrl:K(n.primaryProfile),fallback:e.jsx(ne,{size:38}),accentColor:n.primaryProfile.color,size:176,width:153,height:207,radius:6,editBadgeSize:28,editIconSize:14,className:r.aiProfileDetailAvatar,onClick:()=>{g(null),Q(n.primaryProfile.id)}}),e.jsxs("div",{className:r.aiProfileDetailHeading,children:[e.jsx("div",{className:r.aiProfileDetailTitleRow,children:e.jsx("h5",{className:r.aiProfileDetailTitle,children:n.primaryProfile.name})}),e.jsxs("div",{className:r.aiProfileDetailMetaRow,children:[e.jsxs("span",{className:r.aiProfileDetailHandle,children:["@",n.primaryProfile.username]}),n.profiles.length>1&&e.jsxs("span",{className:r.aiProfileDetailLinkedCount,children:[n.profiles.length," linked"]})]}),e.jsxs("div",{className:r.aiProfileDetailRole,children:["Role: ",n.primaryProfile.role||"Not set"]}),n.primaryProfile.description&&e.jsx("div",{className:r.aiProfileDetailDescription,children:n.primaryProfile.description}),e.jsxs("div",{className:r.aiProfileSignatureColor,children:[e.jsx("span",{children:"Signature color"}),e.jsx("span",{className:r.aiProfileSignatureSwatch,style:{backgroundColor:n.primaryProfile.color},"aria-hidden":"true"}),e.jsx("span",{children:n.primaryProfile.color})]})]})]}),e.jsxs("div",{className:r.aiProfileDetailDataList,children:[e.jsx("div",{className:r.aiProfileDetailDataGroup,children:re.attributes.map(a=>e.jsxs("div",{className:r.aiProfileDetailDataRow,children:[e.jsx("span",{className:r.aiProfileDetailDataLabel,children:a.label}),a.label==="Category"?e.jsx("span",{className:r.aiProfileDetailDataValue,children:e.jsxs("select",{className:r.aiProfileStatusSelect,"aria-label":"Agent category",value:n.primaryProfile.surfaceType??"",disabled:ye,onChange:t=>{Se(n,t.target.value)},children:[e.jsx("option",{value:"",children:"Unclassified"}),Me.map(t=>e.jsx("option",{value:t,children:le(t)},t))]})}):e.jsx("span",{className:r.aiProfileDetailDataValue,children:a.value})]},a.label))}),e.jsx("div",{className:`${r.aiProfileDetailDataGroup} ${r.aiProfileDetailDateGroup}`,children:re.dates.map(a=>e.jsxs("div",{className:r.aiProfileDetailDataRow,children:[e.jsx("span",{className:r.aiProfileDetailDataLabel,children:a.label}),a.label==="Status"?e.jsx("span",{className:r.aiProfileDetailDataValue,children:e.jsxs("select",{className:r.aiProfileStatusSelect,"aria-label":"Agent roster status",value:n.primaryProfile.archivedAt?"retired":"active",onChange:t=>{t.target.value==="retire"&&Z(n.primaryProfile)},children:[e.jsx("option",{value:"active",children:"Active"}),n.primaryProfile.archivedAt?e.jsx("option",{value:"retired",children:"Retired"}):e.jsx("option",{value:"retire",children:"Retire from roster"})]})}):e.jsx("span",{className:r.aiProfileDetailDataValue,children:a.value})]},a.label))})]}),W?.profileId===n.primaryProfile.id&&e.jsxs("div",{className:r.aiProfileInlineError,role:"alert",children:[e.jsx(C,{size:12}),e.jsx("span",{children:W.message})]}),be?e.jsxs("div",{className:r.aiProfileInstanceSection,children:[e.jsxs("div",{className:r.aiProfileInstanceSectionHeader,children:[e.jsx("span",{children:"Profile instances"}),e.jsx("span",{className:r.settingsHint,children:"Choose a keeper here if duplicates need to be merged."})]}),e.jsx("div",{className:r.aiProfileInstanceList,children:n.profiles.map(a=>e.jsxs("div",{className:r.aiProfileInstanceCard,children:[e.jsxs("div",{className:r.aiProfileInstanceTopRow,children:[e.jsxs("div",{children:[e.jsxs("div",{className:r.aiProfileInstanceName,children:["@",a.username]}),e.jsx("div",{className:r.aiProfileIdChip,children:a.id})]}),h?.keepId===a.id&&e.jsx("span",{className:r.aiProfileKeepBadge,children:"Keeping"})]}),e.jsxs("div",{className:r.aiProfileInstanceMeta,children:[e.jsxs("span",{children:["Created ",N(a.createdAt)]}),e.jsxs("span",{children:["Updated ",N(a.updatedAt)]})]}),h?.keepId!==a.id&&e.jsx("button",{className:r.secondaryHeaderBtn,title:"Keep this profile, merge others into it",onClick:()=>{const t=n.profiles.find(i=>i.id!==a.id)?.id;t&&D({keepId:a.id,mergeId:t})},children:"Keep this"}),T?.profileId===a.id&&e.jsxs("div",{className:r.aiProfileInlineError,role:"alert",children:[e.jsx(C,{size:12}),e.jsx("span",{children:T.message})]}),e.jsx("button",{className:r.aiProfileDangerTextButton,title:"Remove this profile from the active roster",onClick:()=>{Z(a)},children:"Remove from roster"})]},a.id))}),h&&n.profiles.some(a=>a.id===h.keepId)&&e.jsxs("div",{className:r.aiProfileMergeActions,children:[e.jsx("button",{className:r.dangerBtn,onClick:()=>{Ae(h.keepId,h.mergeId)},children:"Merge duplicates"}),e.jsx("button",{className:r.secondaryHeaderBtn,onClick:()=>D(null),children:"Cancel"})]})]}):e.jsxs("div",{className:r.aiProfileIdFooter,children:[e.jsx("div",{className:r.aiProfileIdFooterValue,children:n.primaryProfile.id}),T?.profileId===n.primaryProfile.id&&e.jsxs("div",{className:r.aiProfileInlineError,role:"alert",children:[e.jsx(C,{size:12}),e.jsx("span",{children:T.message})]})]})]})})]}),w&&e.jsxs("div",{className:`${w.type==="success"?r.successMessage:r.errorMessage} ${r.marginTop12}`,children:[w.type==="success"?e.jsx(Le,{size:14}):e.jsx(C,{size:14}),w.message]})]})})}),e.jsx(ke,{isOpen:!!u,theme:"dark",title:"Edit AI Profile Photo",currentImageUrl:K(u),editorImageUrl:ze(u),fallbackInitial:(u?.name||"AI").charAt(0).toUpperCase(),accept:"image/png,image/jpeg,image/webp,image/gif",busy:X,hasPendingImage:!1,canRemove:!!u?.avatarUrl,error:Pe,notice:null,onClose:()=>{X||(Q(null),g(null))},onApplyImage:Ne,onRemoveImage:Ie})]})}export{qe as AgentsModule};