@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.
- package/dist/components/views/PlanComparisonPage.d.ts +20 -15
- package/dist/components/views/PlanComparisonPage.js +214 -83
- package/dist/components/views/PlanComparisonPage.test.js +249 -14
- package/dist/components/views/PlansPage.js +15 -9
- package/dist/components/views/PlansPage.test.js +4 -2
- package/dist/core/GlobalSettingsService.js +18 -0
- package/dist/core/GlobalSettingsService.test.d.ts +1 -0
- package/dist/core/GlobalSettingsService.test.js +45 -0
- package/dist/core/PlanEntitlementService.d.ts +20 -0
- package/dist/core/PlanEntitlementService.js +215 -19
- package/dist/core/PlanFeatureCatalog.test.js +55 -0
- package/dist/core/Taskforce.d.ts +14 -0
- package/dist/core/Taskforce.js +6 -0
- package/dist/core/types.d.ts +3 -0
- package/dist/migrations/taskSchemaMigrations.js +37 -15
- package/dist/server/routes/admin.js +146 -5
- package/dist/server/routes/authSupport.d.ts +1 -0
- package/dist/server/routes/authSupport.js +2 -1
- package/dist/server/routes/billing.d.ts +1 -0
- package/dist/server/routes/billing.js +105 -63
- package/dist/server/routes/billing.test.js +159 -11
- package/dist/server/routes.test.js +91 -10
- package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
- package/dist/ui/agent-logos/antigravity.jpeg +0 -0
- package/dist/ui/agent-logos/chatgpt.png +0 -0
- package/dist/ui/agent-logos/claude-code.jpeg +0 -0
- package/dist/ui/agent-logos/codex.jpeg +0 -0
- package/dist/ui/agent-logos/cursor.png +0 -0
- package/dist/ui/agent-logos/openclaw.jpeg +0 -0
- package/dist/ui/agent-logos/windsurf.png +0 -0
- package/dist/ui/assets/{AgentsModule-2y82_3DO.js → AgentsModule-BS4Pi_nC.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BTLZZCiQ.js → AnnotatedAttachmentWorkspace-CCckzWYZ.js} +1 -1
- package/dist/ui/assets/{ContextAttachmentManager-D2epuNnG.js → ContextAttachmentManager-CfG_ER7C.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-sVa-3Do6.js → DocumentWorkspace-BVaWPA25.js} +1 -1
- package/dist/ui/assets/{EntityActivityTimeline-Zods27y_.js → EntityActivityTimeline-DSj0ThaK.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-D4XseTwW.js → InitiativesModule-AxMkWtxH.js} +1 -1
- package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
- package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
- package/dist/ui/assets/{TaskContextUpload-uC5qx4Bv.js → TaskContextUpload-Dw4rTF4i.js} +1 -1
- package/dist/ui/assets/{TaskSettings-CqvPAsTv.js → TaskSettings-f2ga42_V.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-oOA9bcdK.js → WorkflowsModule-E4UL3mov.js} +1 -1
- package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
- package/dist/ui/assets/{index-CYyodXsg.css → index-BvAbqx5Q.css} +1 -1
- package/dist/ui/assets/{index-607WPo_X.js → index-MAHKvFqp.js} +4 -4
- package/dist/ui/index.html +2 -2
- package/dist/ui/og-image.png +0 -0
- package/package.json +1 -1
- package/dist/ui/assets/PlansPage-D5AcbO0L.js +0 -1
- package/dist/ui/assets/PlansPage-Dvqsz5zc.css +0 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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};
|