@taskforcehq/taskforce 0.3.307 → 0.3.309

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 (50) hide show
  1. package/dist/components/views/PlanComparisonPage.d.ts +20 -15
  2. package/dist/components/views/PlanComparisonPage.js +214 -83
  3. package/dist/components/views/PlanComparisonPage.test.js +249 -14
  4. package/dist/components/views/PlansPage.js +15 -9
  5. package/dist/components/views/PlansPage.test.js +4 -2
  6. package/dist/core/GlobalSettingsService.js +18 -0
  7. package/dist/core/GlobalSettingsService.test.d.ts +1 -0
  8. package/dist/core/GlobalSettingsService.test.js +45 -0
  9. package/dist/core/PlanEntitlementService.d.ts +20 -0
  10. package/dist/core/PlanEntitlementService.js +215 -19
  11. package/dist/core/PlanFeatureCatalog.test.js +55 -0
  12. package/dist/core/Taskforce.d.ts +14 -0
  13. package/dist/core/Taskforce.js +6 -0
  14. package/dist/core/types.d.ts +3 -0
  15. package/dist/migrations/taskSchemaMigrations.js +37 -15
  16. package/dist/server/routes/admin.js +146 -5
  17. package/dist/server/routes/authSupport.d.ts +1 -0
  18. package/dist/server/routes/authSupport.js +2 -1
  19. package/dist/server/routes/billing.d.ts +1 -0
  20. package/dist/server/routes/billing.js +105 -63
  21. package/dist/server/routes/billing.test.js +159 -11
  22. package/dist/server/routes.test.js +91 -10
  23. package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
  24. package/dist/ui/agent-logos/antigravity.jpeg +0 -0
  25. package/dist/ui/agent-logos/chatgpt.png +0 -0
  26. package/dist/ui/agent-logos/claude-code.jpeg +0 -0
  27. package/dist/ui/agent-logos/codex.jpeg +0 -0
  28. package/dist/ui/agent-logos/cursor.png +0 -0
  29. package/dist/ui/agent-logos/openclaw.jpeg +0 -0
  30. package/dist/ui/agent-logos/windsurf.png +0 -0
  31. package/dist/ui/assets/{AgentsModule-2y82_3DO.js → AgentsModule-BS4Pi_nC.js} +1 -1
  32. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BTLZZCiQ.js → AnnotatedAttachmentWorkspace-CCckzWYZ.js} +1 -1
  33. package/dist/ui/assets/{ContextAttachmentManager-D2epuNnG.js → ContextAttachmentManager-CfG_ER7C.js} +1 -1
  34. package/dist/ui/assets/{DocumentWorkspace-sVa-3Do6.js → DocumentWorkspace-BVaWPA25.js} +1 -1
  35. package/dist/ui/assets/{EntityActivityTimeline-Zods27y_.js → EntityActivityTimeline-DSj0ThaK.js} +1 -1
  36. package/dist/ui/assets/{InitiativesModule-D4XseTwW.js → InitiativesModule-AxMkWtxH.js} +1 -1
  37. package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
  38. package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
  39. package/dist/ui/assets/{TaskContextUpload-uC5qx4Bv.js → TaskContextUpload-Dw4rTF4i.js} +1 -1
  40. package/dist/ui/assets/{TaskSettings-CqvPAsTv.js → TaskSettings-f2ga42_V.js} +1 -1
  41. package/dist/ui/assets/{WorkflowsModule-oOA9bcdK.js → WorkflowsModule-E4UL3mov.js} +1 -1
  42. package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
  43. package/dist/ui/assets/{index-CYyodXsg.css → index-BvAbqx5Q.css} +1 -1
  44. package/dist/ui/assets/{index-607WPo_X.js → index-MAHKvFqp.js} +4 -4
  45. package/dist/ui/index.html +2 -2
  46. package/dist/ui/og-image.png +0 -0
  47. package/package.json +1 -1
  48. package/dist/ui/assets/PlansPage-D5AcbO0L.js +0 -1
  49. package/dist/ui/assets/PlansPage-Dvqsz5zc.css +0 -1
  50. package/dist/ui/assets/documentReferences-DPZuTgHh.js +0 -1
@@ -157,6 +157,7 @@ function setupBillingSchema(db) {
157
157
  tenant_id TEXT NOT NULL,
158
158
  plan_version_id TEXT NOT NULL,
159
159
  billing_interval TEXT NOT NULL,
160
+ pricing_audience TEXT NOT NULL DEFAULT 'public',
160
161
  stripe_price_id TEXT NOT NULL,
161
162
  pricing_type TEXT NOT NULL DEFAULT 'stripe',
162
163
  active INTEGER NOT NULL DEFAULT 1,
@@ -164,10 +165,10 @@ function setupBillingSchema(db) {
164
165
  currency TEXT,
165
166
  created_at TEXT NOT NULL,
166
167
  updated_at TEXT NOT NULL,
167
- PRIMARY KEY (tenant_id, plan_version_id, billing_interval, stripe_price_id)
168
+ PRIMARY KEY (tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id)
168
169
  );
169
170
  CREATE UNIQUE INDEX idx_billing_price_mappings_active_interval
170
- ON billing_price_mappings (tenant_id, plan_version_id, billing_interval)
171
+ ON billing_price_mappings (tenant_id, plan_version_id, billing_interval, pricing_audience)
171
172
  WHERE active = 1;
172
173
  CREATE TABLE plan_versions (
173
174
  plan_version_id TEXT PRIMARY KEY,
@@ -257,6 +258,7 @@ function readMockPlanVersionFeatures(db, tenantId, planVersionId) {
257
258
  function buildBillingRouteHandlers(db, opts) {
258
259
  const routes = new Map();
259
260
  const tenantId = 'default';
261
+ const siteSettings = opts?.siteSettings || {};
260
262
  const resolvePlanIdForVersion = (planVersionIdRaw) => {
261
263
  const planVersionId = String(planVersionIdRaw || '').trim();
262
264
  if (!planVersionId)
@@ -431,6 +433,9 @@ function buildBillingRouteHandlers(db, opts) {
431
433
  planId: planVersionId.startsWith('pro') ? 'pro' : 'starter',
432
434
  entitlementState: 'active'
433
435
  }),
436
+ getGlobalSettings: () => ({
437
+ site: siteSettings
438
+ }),
434
439
  resolveUserIdentity: () => opts?.userIdentity === undefined
435
440
  ? {
436
441
  userId: 'u1',
@@ -1001,6 +1006,39 @@ describe('Billing Webhook Routes', () => {
1001
1006
  expect(args.customer).toBeUndefined();
1002
1007
  expect(args.customer_email).toBe('owner@example.com');
1003
1008
  });
1009
+ it('creates checkout with the campaign price mapping when requested', async () => {
1010
+ const now = new Date().toISOString();
1011
+ db.prepare(`
1012
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
1013
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
1014
+ `).run(now, now);
1015
+ db.prepare(`
1016
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id, active, created_at, updated_at)
1017
+ VALUES ('default', 'collab-v1', 'month', 'public', 'price_collab_month', 1, ?, ?)
1018
+ `).run(now, now);
1019
+ db.prepare(`
1020
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id, active, created_at, updated_at)
1021
+ VALUES ('default', 'collab-v1', 'month', 'campaign', 'price_collab_campaign_month', 1, ?, ?)
1022
+ `).run(now, now);
1023
+ const checkoutHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session');
1024
+ const req = createMockReq(JSON.stringify({
1025
+ planVersionId: 'collab-v1',
1026
+ interval: 'month',
1027
+ pricingAudience: 'campaign'
1028
+ }), {
1029
+ 'x-taskforce-runtime-mode': 'cloud',
1030
+ host: 'app.taskforcehq.ai'
1031
+ });
1032
+ const res = createMockRes();
1033
+ await checkoutHandler?.(req, res, {});
1034
+ expect(res.statusCode).toBe(200);
1035
+ const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
1036
+ expect(args.line_items?.[0]?.price).toBe('price_collab_campaign_month');
1037
+ expect(String(args.success_url || '')).toContain('pricingAudience=campaign');
1038
+ expect(String(args.cancel_url || '')).toContain('pricingAudience=campaign');
1039
+ expect(args.metadata?.pricingAudience).toBe('campaign');
1040
+ expect(args.subscription_data?.metadata?.pricingAudience).toBe('campaign');
1041
+ });
1004
1042
  it('uses per-version trial days when creating checkout', async () => {
1005
1043
  const now = new Date().toISOString();
1006
1044
  db.prepare(`
@@ -2546,6 +2584,55 @@ describe('Billing Webhook Routes', () => {
2546
2584
  { stripe_price_id: 'price_old', active: 0, unit_amount: 1500 }
2547
2585
  ]);
2548
2586
  });
2587
+ it('keeps campaign price mappings separate from public mappings in admin routes', async () => {
2588
+ const now = new Date().toISOString();
2589
+ db.prepare(`
2590
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
2591
+ VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
2592
+ `).run(now, now);
2593
+ db.prepare(`
2594
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at)
2595
+ VALUES ('default', 'collab-v1', 'month', 'public', 'price_public', 'stripe', 1, 1500, 'usd', ?, ?)
2596
+ `).run(now, now);
2597
+ mockPriceRetrieve.mockResolvedValueOnce({
2598
+ id: 'price_campaign',
2599
+ active: true,
2600
+ currency: 'usd',
2601
+ unit_amount: 900,
2602
+ recurring: { interval: 'month' }
2603
+ });
2604
+ const upsertRoute = routeHandlers.get('PUT /api/taskforce/admin/billing/price-mappings');
2605
+ const upsertReq = createMockReq(JSON.stringify({
2606
+ planVersionId: 'collab-v1',
2607
+ interval: 'month',
2608
+ pricingAudience: 'campaign',
2609
+ stripePriceId: 'price_campaign',
2610
+ active: true
2611
+ }), { 'x-taskforce-runtime-mode': 'cloud' });
2612
+ const upsertRes = createMockRes();
2613
+ await upsertRoute(upsertReq, upsertRes, {});
2614
+ expect(upsertRes.statusCode).toBe(200);
2615
+ expect(JSON.parse(upsertRes.body)).toEqual(expect.objectContaining({
2616
+ mapping: expect.objectContaining({
2617
+ plan_version_id: 'collab-v1',
2618
+ billing_interval: 'month',
2619
+ pricing_audience: 'campaign',
2620
+ stripe_price_id: 'price_campaign',
2621
+ active: 1,
2622
+ unit_amount: 900
2623
+ })
2624
+ }));
2625
+ const rows = db.prepare(`
2626
+ SELECT pricing_audience, stripe_price_id, active
2627
+ FROM billing_price_mappings
2628
+ WHERE tenant_id = 'default' AND plan_version_id = 'collab-v1' AND billing_interval = 'month'
2629
+ ORDER BY pricing_audience ASC
2630
+ `).all();
2631
+ expect(rows).toEqual([
2632
+ { pricing_audience: 'campaign', stripe_price_id: 'price_campaign', active: 1 },
2633
+ { pricing_audience: 'public', stripe_price_id: 'price_public', active: 1 }
2634
+ ]);
2635
+ });
2549
2636
  it('rejects manual reuse of a Stripe price id across multiple plan versions', async () => {
2550
2637
  const now = new Date().toISOString();
2551
2638
  db.prepare(`
@@ -2723,6 +2810,10 @@ describe('Billing Webhook Routes', () => {
2723
2810
  INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at)
2724
2811
  VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 'stripe', 1, 1500, 'usd', ?, ?)
2725
2812
  `).run(now, now);
2813
+ db.prepare(`
2814
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at)
2815
+ VALUES ('default', 'pro-v1', 'month', 'campaign', 'price_pro_campaign_month', 'stripe', 1, 900, 'usd', ?, ?)
2816
+ `).run(now, now);
2726
2817
  db.prepare(`
2727
2818
  INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at)
2728
2819
  VALUES ('default', 'starter-v1', 'month', 'free:starter-v1:month', 'free', 1, 0, NULL, ?, ?)
@@ -2753,6 +2844,14 @@ describe('Billing Webhook Routes', () => {
2753
2844
  expect(monthPricing.currency).toBe('usd');
2754
2845
  expect(monthPricing.stripePriceId).toBe('price_pro_month');
2755
2846
  expect(proPlan.pricing.year).toBeNull();
2847
+ expect(proPlan.campaignPricing.month).toEqual(expect.objectContaining({
2848
+ pricingType: 'stripe',
2849
+ unitAmount: 900,
2850
+ currency: 'usd',
2851
+ stripePriceId: 'price_pro_campaign_month',
2852
+ interval: 'month'
2853
+ }));
2854
+ expect(proPlan.campaignPricing.year).toBeNull();
2756
2855
  // Free plan is explicitly free, not "missing"
2757
2856
  expect(freePlan.pricing.month.pricingType).toBe('free');
2758
2857
  expect(freePlan.pricing.month.unitAmount).toBe(0);
@@ -2793,6 +2892,40 @@ describe('Billing Webhook Routes', () => {
2793
2892
  const secondBody = JSON.parse(secondRes.body);
2794
2893
  expect(secondBody.plans.find((p) => p.planId === 'pro')?.pricing?.month?.unitAmount).toBe(2400);
2795
2894
  });
2895
+ it('invalidates cached public pricing when pricing page header copy changes', async () => {
2896
+ const now = new Date().toISOString();
2897
+ const siteSettings = {};
2898
+ routeHandlers = buildBillingRouteHandlers(db, { siteSettings });
2899
+ const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
2900
+ expect(pricingRoute).toBeTypeOf('function');
2901
+ db.prepare(`
2902
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
2903
+ VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
2904
+ `).run(now, now);
2905
+ db.prepare(`
2906
+ INSERT INTO billing_price_mappings (
2907
+ tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
2908
+ ) VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 'stripe', 1, 1200, 'usd', ?, ?)
2909
+ `).run(now, now);
2910
+ const firstReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
2911
+ const firstRes = createMockRes();
2912
+ await pricingRoute(firstReq, firstRes, {});
2913
+ expect(firstRes.statusCode).toBe(200);
2914
+ expect(JSON.parse(firstRes.body)).toMatchObject({
2915
+ pageTitle: 'Subscription Plans',
2916
+ pageDescription: 'Manage your subscription and explore available features. Your current plan is highlighted below.'
2917
+ });
2918
+ siteSettings.pricingPageTitle = 'Choose Your Plan';
2919
+ siteSettings.pricingPageDescription = 'Compare plans and pick the right fit for your team.';
2920
+ const secondReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
2921
+ const secondRes = createMockRes();
2922
+ await pricingRoute(secondReq, secondRes, {});
2923
+ expect(secondRes.statusCode).toBe(200);
2924
+ expect(JSON.parse(secondRes.body)).toMatchObject({
2925
+ pageTitle: 'Choose Your Plan',
2926
+ pageDescription: 'Compare plans and pick the right fit for your team.'
2927
+ });
2928
+ });
2796
2929
  it('invalidates cached public pricing when plan features change', async () => {
2797
2930
  const now = new Date().toISOString();
2798
2931
  routeHandlers = buildBillingRouteHandlers(db);
@@ -2846,7 +2979,8 @@ describe('Billing Webhook Routes', () => {
2846
2979
  });
2847
2980
  it('includes public feature copy and invalidates cached public pricing when catalog overrides change', async () => {
2848
2981
  const now = new Date().toISOString();
2849
- routeHandlers = buildBillingRouteHandlers(db);
2982
+ const siteSettings = {};
2983
+ routeHandlers = buildBillingRouteHandlers(db, { siteSettings });
2850
2984
  const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
2851
2985
  expect(pricingRoute).toBeTypeOf('function');
2852
2986
  db.prepare(`
@@ -2876,15 +3010,11 @@ describe('Billing Webhook Routes', () => {
2876
3010
  featureKey: 'collaboration.ai_profiles',
2877
3011
  label: 'AI Profiles',
2878
3012
  publicLabel: 'AI teammates',
2879
- publicDescription: 'Add reusable AI teammates to your workspace.'
3013
+ publicDescription: 'Add reusable AI teammates to your workspace.',
3014
+ publicDescriptionVisible: false
2880
3015
  })
2881
3016
  ]);
2882
- const updatedAt = new Date(Date.now() + 1000).toISOString();
2883
- db.prepare(`
2884
- UPDATE plan_feature_catalog_overrides
2885
- SET public_label = 'AI copilots', updated_at = ?
2886
- WHERE tenant_id = 'default' AND feature_key = 'collaboration.ai_profiles'
2887
- `).run(updatedAt);
3017
+ siteSettings.pricingPageShowPublicDescriptions = true;
2888
3018
  const secondReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
2889
3019
  const secondRes = createMockRes();
2890
3020
  await pricingRoute(secondReq, secondRes, {});
@@ -2893,7 +3023,25 @@ describe('Billing Webhook Routes', () => {
2893
3023
  expect(secondBody.plans.find((p) => p.planId === 'pro')?.features).toEqual([
2894
3024
  expect.objectContaining({
2895
3025
  featureKey: 'collaboration.ai_profiles',
2896
- publicLabel: 'AI copilots'
3026
+ publicDescriptionVisible: true
3027
+ })
3028
+ ]);
3029
+ const updatedAt = new Date(Date.now() + 1000).toISOString();
3030
+ db.prepare(`
3031
+ UPDATE plan_feature_catalog_overrides
3032
+ SET public_label = 'AI copilots', updated_at = ?
3033
+ WHERE tenant_id = 'default' AND feature_key = 'collaboration.ai_profiles'
3034
+ `).run(updatedAt);
3035
+ const thirdReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
3036
+ const thirdRes = createMockRes();
3037
+ await pricingRoute(thirdReq, thirdRes, {});
3038
+ expect(thirdRes.statusCode).toBe(200);
3039
+ const thirdBody = JSON.parse(thirdRes.body);
3040
+ expect(thirdBody.plans.find((p) => p.planId === 'pro')?.features).toEqual([
3041
+ expect.objectContaining({
3042
+ featureKey: 'collaboration.ai_profiles',
3043
+ publicLabel: 'AI copilots',
3044
+ publicDescriptionVisible: true
2897
3045
  })
2898
3046
  ]);
2899
3047
  });
@@ -11806,7 +11806,8 @@ describe('Server Routes', () => {
11806
11806
  label: null,
11807
11807
  description: null,
11808
11808
  publicLabel: null,
11809
- publicDescription: null
11809
+ publicDescription: null,
11810
+ publicDescriptionVisible: false
11810
11811
  }
11811
11812
  ]);
11812
11813
  }
@@ -11960,6 +11961,7 @@ describe('Server Routes', () => {
11960
11961
  description: 'Allow registered AI collaborator profiles with an optional per-workspace limit.',
11961
11962
  publicLabel: 'AI teammates',
11962
11963
  publicDescription: 'Add reusable AI teammates to your workspace.',
11964
+ publicDescriptionVisible: false,
11963
11965
  publicDisplayOrder: 20,
11964
11966
  allowedAccessModes: ['enabled', 'limited', 'disabled'],
11965
11967
  configTemplate: { maxAiProfiles: 1 }
@@ -11968,6 +11970,7 @@ describe('Server Routes', () => {
11968
11970
  const req = createMockReq('PATCH', '/api/platform/features/collaboration.ai_profiles', {
11969
11971
  publicLabel: 'AI teammates',
11970
11972
  publicDescription: 'Add reusable AI teammates to your workspace.',
11973
+ publicDescriptionVisible: false,
11971
11974
  publicDisplayOrder: 20
11972
11975
  }, {
11973
11976
  'x-taskforce-runtime-mode': 'cloud',
@@ -11977,13 +11980,65 @@ describe('Server Routes', () => {
11977
11980
  await match?.handler(req, res, { featureKey: 'collaboration.ai_profiles' });
11978
11981
  expect(core.updatePlanFeatureCatalogMarketingCopy).toHaveBeenCalledWith({
11979
11982
  featureKey: 'collaboration.ai_profiles',
11983
+ label: undefined,
11984
+ description: undefined,
11980
11985
  publicLabel: 'AI teammates',
11981
11986
  publicDescription: 'Add reusable AI teammates to your workspace.',
11987
+ publicDescriptionVisible: false,
11982
11988
  publicDisplayOrder: 20
11983
11989
  });
11984
11990
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
11985
11991
  expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"publicLabel":"AI teammates"'));
11986
11992
  });
11993
+ it('POST /api/platform/features should create a custom feature catalog entry', async () => {
11994
+ core.createCustomPlanFeatureCatalogEntry = vi.fn().mockReturnValue({
11995
+ featureKey: 'marketing.unlimited_local_workspaces',
11996
+ label: 'Unlimited Local Workspaces',
11997
+ description: 'Run unlimited local workspaces on your device.',
11998
+ isCustom: true,
11999
+ publicLabel: null,
12000
+ publicDescription: null,
12001
+ publicDescriptionVisible: true,
12002
+ publicDisplayOrder: 12,
12003
+ allowedAccessModes: ['enabled', 'disabled'],
12004
+ configTemplate: {}
12005
+ });
12006
+ const match = matchRoute('POST', '/api/platform/features', routes);
12007
+ const req = createMockReq('POST', '/api/platform/features', {
12008
+ label: 'Unlimited Local Workspaces',
12009
+ description: 'Run unlimited local workspaces on your device.',
12010
+ publicDescriptionVisible: true,
12011
+ publicDisplayOrder: 12
12012
+ }, {
12013
+ 'x-taskforce-runtime-mode': 'cloud',
12014
+ 'x-taskforce-system-role': 'system_admin'
12015
+ });
12016
+ const res = createMockRes();
12017
+ await match?.handler(req, res, {});
12018
+ expect(core.createCustomPlanFeatureCatalogEntry).toHaveBeenCalledWith({
12019
+ label: 'Unlimited Local Workspaces',
12020
+ description: 'Run unlimited local workspaces on your device.',
12021
+ publicLabel: undefined,
12022
+ publicDescription: undefined,
12023
+ publicDescriptionVisible: true,
12024
+ publicDisplayOrder: 12
12025
+ });
12026
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
12027
+ expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"featureKey":"marketing.unlimited_local_workspaces"'));
12028
+ });
12029
+ it('DELETE /api/platform/features/:featureKey should remove a custom feature catalog entry', async () => {
12030
+ core.deleteCustomPlanFeatureCatalogEntry = vi.fn();
12031
+ const match = matchRoute('DELETE', '/api/platform/features/marketing.unlimited_local_workspaces', routes);
12032
+ const req = createMockReq('DELETE', '/api/platform/features/marketing.unlimited_local_workspaces', undefined, {
12033
+ 'x-taskforce-runtime-mode': 'cloud',
12034
+ 'x-taskforce-system-role': 'system_admin'
12035
+ });
12036
+ const res = createMockRes();
12037
+ await match?.handler(req, res, { featureKey: 'marketing.unlimited_local_workspaces' });
12038
+ expect(core.deleteCustomPlanFeatureCatalogEntry).toHaveBeenCalledWith('marketing.unlimited_local_workspaces');
12039
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
12040
+ expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"success":true'));
12041
+ });
11987
12042
  it('PATCH /api/platform/plans/:planId/versions/:versionId/features should return normalized dependency state', async () => {
11988
12043
  core.updatePlanVersionFeatures.mockReturnValueOnce({
11989
12044
  planVersionId: 'collab-v1',
@@ -13566,7 +13621,8 @@ describe('Server Routes', () => {
13566
13621
  animatedBackgroundEnabled: true,
13567
13622
  siteTheme: 'light',
13568
13623
  pricingPageTitle: 'Choose Your Plan',
13569
- pricingPageDescription: 'Compare plans and pick the right one.'
13624
+ pricingPageDescription: 'Compare plans and pick the right one.',
13625
+ pricingPageShowPublicDescriptions: false
13570
13626
  }
13571
13627
  });
13572
13628
  const match = matchRoute('GET', '/api/taskforce/admin/site-settings', routes);
@@ -13580,12 +13636,14 @@ describe('Server Routes', () => {
13580
13636
  expect(payload.status.siteTheme).toBe('light');
13581
13637
  expect(payload.status.pricingPageTitle).toBe('Choose Your Plan');
13582
13638
  expect(payload.status.pricingPageDescription).toBe('Compare plans and pick the right one.');
13639
+ expect(payload.status.pricingPageShowPublicDescriptions).toBe(false);
13583
13640
  expect(payload.settings.showRunTaskforceLocally).toBe(false);
13584
13641
  expect(payload.settings.pricingPageEnabled).toBe(false);
13585
13642
  expect(payload.settings.animatedBackgroundEnabled).toBe(true);
13586
13643
  expect(payload.settings.siteTheme).toBe('light');
13587
13644
  expect(payload.settings.pricingPageTitle).toBe('Choose Your Plan');
13588
13645
  expect(payload.settings.pricingPageDescription).toBe('Compare plans and pick the right one.');
13646
+ expect(payload.settings.pricingPageShowPublicDescriptions).toBe(false);
13589
13647
  });
13590
13648
  it('GET /api/taskforce/admin/site_settings should expose effective website settings for legacy admin bundles', async () => {
13591
13649
  core.getGlobalSettings.mockReturnValue({
@@ -13595,7 +13653,8 @@ describe('Server Routes', () => {
13595
13653
  animatedBackgroundEnabled: true,
13596
13654
  siteTheme: 'light',
13597
13655
  pricingPageTitle: 'Choose Your Plan',
13598
- pricingPageDescription: 'Compare plans and pick the right one.'
13656
+ pricingPageDescription: 'Compare plans and pick the right one.',
13657
+ pricingPageShowPublicDescriptions: false
13599
13658
  }
13600
13659
  });
13601
13660
  const match = matchRoute('GET', '/api/taskforce/admin/site_settings', routes);
@@ -13609,22 +13668,26 @@ describe('Server Routes', () => {
13609
13668
  expect(payload.status.siteTheme).toBe('light');
13610
13669
  expect(payload.status.pricingPageTitle).toBe('Choose Your Plan');
13611
13670
  expect(payload.status.pricingPageDescription).toBe('Compare plans and pick the right one.');
13671
+ expect(payload.status.pricingPageShowPublicDescriptions).toBe(false);
13612
13672
  expect(payload.settings.showRunTaskforceLocally).toBe(false);
13613
13673
  expect(payload.settings.pricingPageEnabled).toBe(false);
13614
13674
  expect(payload.settings.animatedBackgroundEnabled).toBe(true);
13615
13675
  expect(payload.settings.siteTheme).toBe('light');
13616
13676
  expect(payload.settings.pricingPageTitle).toBe('Choose Your Plan');
13617
13677
  expect(payload.settings.pricingPageDescription).toBe('Compare plans and pick the right one.');
13678
+ expect(payload.settings.pricingPageShowPublicDescriptions).toBe(false);
13618
13679
  });
13619
13680
  it('POST /api/taskforce/admin/site-settings should persist website settings', async () => {
13620
13681
  const match = matchRoute('POST', '/api/taskforce/admin/site-settings', routes);
13682
+ const pricingPageDescription = `<p>${'Compare plans and pick the right one. '.repeat(12)}</p>`;
13621
13683
  const req = createMockReq('POST', '/api/taskforce/admin/site-settings', {
13622
13684
  showRunTaskforceLocally: false,
13623
13685
  pricingPageEnabled: false,
13624
13686
  animatedBackgroundEnabled: true,
13625
13687
  siteTheme: 'light',
13626
13688
  pricingPageTitle: 'Choose Your Plan',
13627
- pricingPageDescription: 'Compare plans and pick the right one.'
13689
+ pricingPageDescription,
13690
+ pricingPageShowPublicDescriptions: false
13628
13691
  }, { 'x-taskforce-role': 'admin' });
13629
13692
  const res = createMockRes();
13630
13693
  await match?.handler(req, res, {});
@@ -13635,10 +13698,22 @@ describe('Server Routes', () => {
13635
13698
  animatedBackgroundEnabled: true,
13636
13699
  siteTheme: 'light',
13637
13700
  pricingPageTitle: 'Choose Your Plan',
13638
- pricingPageDescription: 'Compare plans and pick the right one.'
13701
+ pricingPageDescription,
13702
+ pricingPageShowPublicDescriptions: false
13639
13703
  }
13640
13704
  });
13641
13705
  });
13706
+ it('POST /api/taskforce/admin/site-settings should reject pricing page descriptions over 2000 characters', async () => {
13707
+ const match = matchRoute('POST', '/api/taskforce/admin/site-settings', routes);
13708
+ const req = createMockReq('POST', '/api/taskforce/admin/site-settings', {
13709
+ pricingPageDescription: 'x'.repeat(2001)
13710
+ }, { 'x-taskforce-role': 'admin' });
13711
+ const res = createMockRes();
13712
+ await match?.handler(req, res, {});
13713
+ expect(res.writeHead).toHaveBeenCalledWith(400, { 'Content-Type': 'application/json' });
13714
+ expect(res.end.mock.calls[0][0]).toContain('PRICING_PAGE_DESCRIPTION_TOO_LONG');
13715
+ expect(core.updateGlobalSettings).not.toHaveBeenCalled();
13716
+ });
13642
13717
  it('POST /api/taskforce/admin/site_settings should persist website settings for legacy admin bundles', async () => {
13643
13718
  const match = matchRoute('POST', '/api/taskforce/admin/site_settings', routes);
13644
13719
  const req = createMockReq('POST', '/api/taskforce/admin/site_settings', {
@@ -13647,7 +13722,8 @@ describe('Server Routes', () => {
13647
13722
  animatedBackgroundEnabled: true,
13648
13723
  siteTheme: 'light',
13649
13724
  pricingPageTitle: 'Choose Your Plan',
13650
- pricingPageDescription: 'Compare plans and pick the right one.'
13725
+ pricingPageDescription: 'Compare plans and pick the right one.',
13726
+ pricingPageShowPublicDescriptions: false
13651
13727
  }, { 'x-taskforce-role': 'admin' });
13652
13728
  const res = createMockRes();
13653
13729
  await match?.handler(req, res, {});
@@ -13658,7 +13734,8 @@ describe('Server Routes', () => {
13658
13734
  animatedBackgroundEnabled: true,
13659
13735
  siteTheme: 'light',
13660
13736
  pricingPageTitle: 'Choose Your Plan',
13661
- pricingPageDescription: 'Compare plans and pick the right one.'
13737
+ pricingPageDescription: 'Compare plans and pick the right one.',
13738
+ pricingPageShowPublicDescriptions: false
13662
13739
  }
13663
13740
  });
13664
13741
  });
@@ -13670,7 +13747,8 @@ describe('Server Routes', () => {
13670
13747
  animatedBackgroundEnabled: true,
13671
13748
  siteTheme: 'light',
13672
13749
  pricingPageTitle: 'Choose Your Plan',
13673
- pricingPageDescription: 'Compare plans and pick the right one.'
13750
+ pricingPageDescription: 'Compare plans and pick the right one.',
13751
+ pricingPageShowPublicDescriptions: false
13674
13752
  }
13675
13753
  });
13676
13754
  const match = matchRoute('GET', '/api/taskforce/public/site-config', routes);
@@ -13689,7 +13767,8 @@ describe('Server Routes', () => {
13689
13767
  animatedBackgroundEnabled: true,
13690
13768
  siteTheme: 'light',
13691
13769
  pricingPageTitle: 'Choose Your Plan',
13692
- pricingPageDescription: 'Compare plans and pick the right one.'
13770
+ pricingPageDescription: 'Compare plans and pick the right one.',
13771
+ pricingPageShowPublicDescriptions: false
13693
13772
  }
13694
13773
  });
13695
13774
  });
@@ -13701,7 +13780,8 @@ describe('Server Routes', () => {
13701
13780
  animatedBackgroundEnabled: true,
13702
13781
  siteTheme: 'light',
13703
13782
  pricingPageTitle: 'Choose Your Plan',
13704
- pricingPageDescription: 'Compare plans and pick the right one.'
13783
+ pricingPageDescription: 'Compare plans and pick the right one.',
13784
+ pricingPageShowPublicDescriptions: false
13705
13785
  }
13706
13786
  });
13707
13787
  const match = matchRoute('GET', '/api/taskforce/public/site-config-script', routes);
@@ -13717,6 +13797,7 @@ describe('Server Routes', () => {
13717
13797
  expect(res.end.mock.calls[0][0]).toContain('"animatedBackgroundEnabled":true');
13718
13798
  expect(res.end.mock.calls[0][0]).toContain('"siteTheme":"light"');
13719
13799
  expect(res.end.mock.calls[0][0]).toContain('"pricingPageTitle":"Choose Your Plan"');
13800
+ expect(res.end.mock.calls[0][0]).toContain('"pricingPageShowPublicDescriptions":false');
13720
13801
  });
13721
13802
  it('POST /api/taskforce/admin/system-settings should reject invalid onboarding settings', async () => {
13722
13803
  core.validateOnboardingPolicy.mockReturnValueOnce({
Binary file
Binary file
Binary file
Binary file
@@ -1 +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-607WPo_X.js";import{Y as Re,w as $e,h as Ee,B as ne,m as C,b as Le,W as Be,a2 as Ge}from"./vendor-icons-B32hGuF2.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};
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-MAHKvFqp.js";import{Y as Re,w as $e,h as Ee,B as ne,m as C,b as Le,W as Be,a2 as Ge}from"./vendor-icons-B32hGuF2.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};