diff --git a/backend/__tests__/unit/middleware/appAuth.test.js b/backend/__tests__/unit/middleware/appAuth.test.js deleted file mode 100644 index ff632497d5..0000000000 --- a/backend/__tests__/unit/middleware/appAuth.test.js +++ /dev/null @@ -1,71 +0,0 @@ -const express = require('express'); -const request = require('supertest'); -const jwt = require('jsonwebtoken'); -const App = require('../../../models/App'); -const AppInstallation = require('../../../models/AppInstallation'); -const appAuth = require('../../../middleware/appAuth'); -const { hash } = require('../../../utils/secret'); -const { setupMongoDb, closeMongoDb } = require('../../utils/testUtils'); - -const JWT_SECRET = 'test-jwt-secret'; - -describe('appAuth middleware', () => { - let app; - let installToken; - - beforeAll(async () => { - process.env.JWT_SECRET = JWT_SECRET; - await setupMongoDb(); - - const owner = await App.create({ - name: 'MidApp', - webhookUrl: 'https://example.com', - webhookSecretHash: 'h', - clientId: 'cid-mid', - clientSecretHash: 'csecret', - ownerId: '507f191e810c19729de860aa', - }); - - installToken = 'install-token'; - await AppInstallation.create({ - appId: owner._id, - targetType: 'pod', - targetId: '507f191e810c19729de860ff', - scopes: ['messages:read'], - events: ['message.created'], - tokenHash: hash(installToken), - createdBy: owner.ownerId, - }); - - app = express(); - app.use(express.json()); - app.get('/protected', appAuth, (req, res) => { - res.json({ ok: true, install: req.appInstallation._id.toString() }); - }); - }); - - afterAll(async () => { - await closeMongoDb(); - }); - - test('allows request with valid installation token', async () => { - const res = await request(app) - .get('/protected') - .set('Authorization', `Bearer ${installToken}`); - - expect(res.status).toBe(200); - expect(res.body.ok).toBe(true); - }); - - test('rejects missing token', async () => { - const res = await request(app).get('/protected'); - expect(res.status).toBe(401); - }); - - test('rejects invalid token', async () => { - const res = await request(app) - .get('/protected') - .set('Authorization', 'Bearer bad'); - expect(res.status).toBe(401); - }); -}); diff --git a/backend/__tests__/unit/scripts/add-message-type.test.js b/backend/__tests__/unit/scripts/add-message-type.test.js deleted file mode 100644 index b3be6eabac..0000000000 --- a/backend/__tests__/unit/scripts/add-message-type.test.js +++ /dev/null @@ -1,28 +0,0 @@ -const { pool } = require('../../../config/db-pg'); -const addColumn = require('../../../add-message-type'); - -jest.mock('../../../config/db-pg', () => ({ - pool: { - query: jest.fn(), - end: jest.fn(), - }, -})); - -describe('add-message-type migration', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('adds column when missing', async () => { - pool.query.mockResolvedValueOnce({ rows: [] }); - await addColumn(); - expect(pool.query).toHaveBeenCalledTimes(2); - expect(pool.query.mock.calls[1][0]).toMatch(/ALTER TABLE messages/); - }); - - it('does nothing when column exists', async () => { - pool.query.mockResolvedValueOnce({ rows: [{}] }); - await addColumn(); - expect(pool.query).toHaveBeenCalledTimes(1); - }); -}); diff --git a/backend/__tests__/unit/scripts/sync-pods.test.js b/backend/__tests__/unit/scripts/sync-pods.test.js deleted file mode 100644 index bedef8d3af..0000000000 --- a/backend/__tests__/unit/scripts/sync-pods.test.js +++ /dev/null @@ -1,43 +0,0 @@ -const mongoose = require('mongoose'); -const PGPod = require('../../../models/pg/Pod'); -const Pod = require('../../../models/Pod'); - -jest.mock('../../../config/db-pg', () => ({ - pool: { query: jest.fn(), end: jest.fn() }, -})); -const { pool } = require('../../../config/db-pg'); -const syncPods = require('../../../sync-pods'); - -describe('syncPods', () => { - beforeEach(() => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('throws error when MONGO_URI is missing', async () => { - delete process.env.MONGO_URI; - process.env.MONGO_URI = ''; - await syncPods(); - expect(console.error).toHaveBeenCalledWith( - 'Error:', - 'MONGO_URI environment variable is not set', - ); - }); - - it('syncs no pods when mongo returns empty', async () => { - process.env.MONGO_URI = 'mongo://test'; - jest.spyOn(mongoose, 'connect').mockResolvedValue(); - jest.spyOn(mongoose, 'disconnect').mockResolvedValue(); - jest.spyOn(PGPod, 'findById').mockResolvedValue(null); - jest.spyOn(PGPod, 'addMember').mockResolvedValue(); - jest.spyOn(PGPod, 'removeMember').mockResolvedValue(); - jest.spyOn(Pod, 'find').mockResolvedValue([]); - jest.spyOn(pool, 'query').mockResolvedValue({ rows: [] }); - jest.spyOn(pool, 'end').mockResolvedValue(); - await syncPods(); - expect(Pod.find).toHaveBeenCalled(); - }); -}); diff --git a/backend/__tests__/unit/scripts/testPG.test.js b/backend/__tests__/unit/scripts/testPG.test.js deleted file mode 100644 index 7f8a75a9c3..0000000000 --- a/backend/__tests__/unit/scripts/testPG.test.js +++ /dev/null @@ -1,14 +0,0 @@ -jest.mock('../../../config/db-pg', () => ({ - pool: { query: jest.fn(), end: jest.fn() }, -})); -const testPG = require('../../../testPG'); -const { pool } = require('../../../config/db-pg'); - -describe('testPG', () => { - it('runs connection check', async () => { - jest.spyOn(pool, 'query').mockResolvedValueOnce(); - jest.spyOn(pool, 'end').mockResolvedValue(); - await testPG(); - expect(pool.query).toHaveBeenCalledWith('SELECT NOW()'); - }); -}); diff --git a/backend/__tests__/unit/scripts/testPgConnection.test.js b/backend/__tests__/unit/scripts/testPgConnection.test.js deleted file mode 100644 index 0e1dbfc25c..0000000000 --- a/backend/__tests__/unit/scripts/testPgConnection.test.js +++ /dev/null @@ -1,24 +0,0 @@ -jest.mock('../../../config/db-pg', () => { - const mClient = { - query: jest.fn().mockResolvedValue({ rows: [{ exists: true }] }), - release: jest.fn(), - }; - const mPool = { - query: jest.fn(), - connect: jest.fn().mockResolvedValue(mClient), - end: jest.fn(), - }; - return { pool: mPool, connectPG: jest.fn(async () => mPool) }; -}); - -const { pool, connectPG } = require('../../../config/db-pg'); -const testConnection = require('../../../testPgConnection'); - -describe('testPgConnection', () => { - it('connects and closes pool', async () => { - await testConnection(); - expect(connectPG).toHaveBeenCalled(); - expect(pool.connect).toHaveBeenCalled(); - expect(pool.end).toHaveBeenCalled(); - }); -}); diff --git a/backend/add-message-type.ts b/backend/add-message-type.ts deleted file mode 100644 index b81ad9c98c..0000000000 --- a/backend/add-message-type.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Migration script to add message_type column to messages table -const { pool } = require('./config/db-pg'); - -async function addMessageTypeColumn() { - try { - console.log('Starting migration to add message_type column...'); - - // Check if column already exists - const checkColumnQuery = ` - SELECT column_name - FROM information_schema.columns - WHERE table_name = 'messages' AND column_name = 'message_type' - `; - - const columnResult = await pool.query(checkColumnQuery); - - if (columnResult.rows.length > 0) { - console.log('Column message_type already exists in messages table'); - return; - } - - // Add the message_type column - const alterTableQuery = ` - ALTER TABLE messages - ADD COLUMN message_type VARCHAR(20) DEFAULT 'text' NOT NULL - `; - - await pool.query(alterTableQuery); - console.log('Successfully added message_type column to messages table'); - } catch (error: unknown) { - console.error('Migration failed:', ((error) as Error).message); - } finally { - await pool.end(); - } -} - -if (require.main === module) { - // Run the migration only when executed directly - addMessageTypeColumn(); -} - -module.exports = addMessageTypeColumn; -export {}; diff --git a/backend/cleanup-test-data.ts b/backend/cleanup-test-data.ts deleted file mode 100644 index 927d5b80a1..0000000000 --- a/backend/cleanup-test-data.ts +++ /dev/null @@ -1,75 +0,0 @@ -const connectDB = require('./config/db'); -const Pod = require('./models/Pod'); -const User = require('./models/User'); -const Summary = require('./models/Summary'); - -async function cleanupTestData() { - try { - await connectDB(); - console.log('🧹 Cleaning up corrupted test data...'); - - // Remove test pods - const testPods = await Pod.find({ - $or: [ - { name: /test/i }, - { name: /digest.*test/i }, - { name: 'Daily Digest Test Pod' }, - ], - }); - - console.log( - 'Found test pods:', - testPods.map((p) => `${p.name} (${p._id})`), - ); - - await Promise.all( - testPods.map(async (pod) => { - await Pod.findByIdAndDelete(pod._id); - console.log('βœ… Deleted pod:', pod.name); - }), - ); - - // Clean up test users - const testUsers = await User.find({ - username: { $regex: /test|digest.*test/i }, - }); - - console.log( - 'Found test users:', - testUsers.map((u) => u.username), - ); - - await Promise.all( - testUsers.map(async (user) => { - await User.findByIdAndDelete(user._id); - console.log('βœ… Deleted user:', user.username); - }), - ); - - // Clean up test summaries - const testSummaries = await Summary.find({ - $or: [ - { 'metadata.podName': /test/i }, - { title: /test/i }, - { podId: { $in: testPods.map((p) => p._id) } }, - ], - }); - - console.log('Found test summaries:', testSummaries.length); - - await Promise.all( - testSummaries.map(async (summary) => { - await Summary.findByIdAndDelete(summary._id); - }), - ); - - console.log('βœ… Deleted', testSummaries.length, 'test summaries'); - console.log('πŸŽ‰ Cleanup completed successfully!'); - process.exit(0); - } catch (error) { - console.error('❌ Cleanup error:', error); - process.exit(1); - } -} - -cleanupTestData(); diff --git a/backend/gateway/index.ts b/backend/gateway/index.ts deleted file mode 100644 index f0cdc1ea21..0000000000 --- a/backend/gateway/index.ts +++ /dev/null @@ -1,495 +0,0 @@ -/** - * Commonly Gateway - * - * WebSocket control plane for real-time communication between: - * - Frontend clients (browsers) - * - AI Agents (MCP connections) - * - Integration bridges (Discord, Slack, etc.) - * - * The Gateway is the single source of truth for: - * - Session state - * - Event broadcasting - * - Agent routing - * - Client authentication - */ - -export {}; - -// eslint-disable-next-line import/no-extraneous-dependencies -const WebSocket = require('ws'); -const jwt = require('jsonwebtoken'); -const { EventEmitter } = require('events'); - -// Event types -const EventTypes = { - // Connection events - CONNECT: 'connect', - DISCONNECT: 'disconnect', - AUTHENTICATE: 'authenticate', - AUTHENTICATED: 'authenticated', - ERROR: 'error', - - // Chat events - MESSAGE_CREATED: 'message.created', - MESSAGE_DELETED: 'message.deleted', - MESSAGE_UPDATED: 'message.updated', - - // Pod events - POD_JOINED: 'pod.joined', - POD_LEFT: 'pod.left', - MEMBER_JOINED: 'member.joined', - MEMBER_LEFT: 'member.left', - - // Agent events - AGENT_CONNECTED: 'agent.connected', - AGENT_DISCONNECTED: 'agent.disconnected', - AGENT_TYPING: 'agent.typing', - AGENT_RESPONSE: 'agent.response', - - // Context events - SUMMARY_CREATED: 'summary.created', - SKILL_CREATED: 'skill.created', - SKILL_UPDATED: 'skill.updated', - MEMORY_UPDATED: 'memory.updated', - - // Federation events - LINK_CREATED: 'link.created', - LINK_APPROVED: 'link.approved', - LINK_REVOKED: 'link.revoked', - - // Subscription - SUBSCRIBE: 'subscribe', - UNSUBSCRIBE: 'unsubscribe', -}; - -// Client types -const ClientTypes = { - BROWSER: 'browser', - AGENT: 'agent', - INTEGRATION: 'integration', - ADMIN: 'admin', -}; - -/** - * Gateway Server - */ -class Gateway extends EventEmitter { - constructor(options: any = {}) { - super(); - - this.port = options.port || parseInt(process.env.GATEWAY_PORT as string, 10) || 5001; - this.host = options.host || process.env.GATEWAY_BIND || '127.0.0.1'; - this.jwtSecret = options.jwtSecret || process.env.JWT_SECRET; - - this.wss = null; - this.clients = new Map(); // clientId -> { ws, user, type, subscriptions } - this.podSubscriptions = new Map(); // podId -> Set - this.agentConnections = new Map(); // agentId -> clientId - - this.stats = { - totalConnections: 0, - activeConnections: 0, - messagesSent: 0, - messagesReceived: 0, - }; - } - - /** - * Start the Gateway server - */ - start() { - this.wss = new (WebSocket as any).Server({ - port: this.port, - host: this.host, - }); - - this.wss.on('connection', (ws: any, req: any) => this.handleConnection(ws, req)); - this.wss.on('error', (error: any) => this.handleError(error)); - - console.log(`Gateway listening on ws://${this.host}:${this.port}`); - - // Heartbeat to detect stale connections - this.heartbeatInterval = setInterval(() => this.checkHeartbeats(), 30000); - - return this; - } - - /** - * Stop the Gateway server - */ - stop() { - if (this.heartbeatInterval) { - clearInterval(this.heartbeatInterval); - } - - if (this.wss) { - this.wss.close(); - } - } - - /** - * Handle new WebSocket connection - */ - handleConnection(ws: any, req: any) { - const clientId = this.generateClientId(); - - // Initialize client state - this.clients.set(clientId, { - ws, - id: clientId, - user: null, - type: ClientTypes.BROWSER, - subscriptions: new Set(), - isAlive: true, - connectedAt: new Date(), - }); - - this.stats.totalConnections += 1; - this.stats.activeConnections += 1; - - // Setup message handler - ws.on('message', (data: any) => this.handleMessage(clientId, data)); - ws.on('close', () => this.handleDisconnect(clientId)); - ws.on('pong', () => this.handlePong(clientId)); - ws.on('error', (error: any) => console.error(`Client ${clientId} error:`, error)); - - // Send welcome message - this.sendToClient(clientId, { - type: EventTypes.CONNECT, - payload: { - clientId, - message: 'Connected to Commonly Gateway', - requiresAuth: true, - }, - }); - - this.emit(EventTypes.CONNECT, { clientId }); - } - - /** - * Handle incoming message - */ - handleMessage(clientId: any, data: any) { - this.stats.messagesReceived += 1; - - try { - const message = JSON.parse(data.toString()); - const { type, payload } = message; - const client = this.clients.get(clientId); - - if (!client) return; - - // Handle authentication - if (type === EventTypes.AUTHENTICATE) { - this.handleAuthenticate(clientId, payload); - return; - } - - // All other messages require authentication - if (!client.user) { - this.sendToClient(clientId, { - type: EventTypes.ERROR, - payload: { code: 'UNAUTHORIZED', message: 'Authentication required' }, - }); - return; - } - - // Route message by type - switch (type) { - case EventTypes.SUBSCRIBE: - this.handleSubscribe(clientId, payload); - break; - - case EventTypes.UNSUBSCRIBE: - this.handleUnsubscribe(clientId, payload); - break; - - case EventTypes.MESSAGE_CREATED: - this.handleBroadcastToPod(clientId, payload.podId, message); - break; - - default: - // Emit for custom handlers - this.emit(type, { clientId, client, payload }); - } - } catch (error: any) { - console.error(`Error handling message from ${clientId}:`, error); - this.sendToClient(clientId, { - type: EventTypes.ERROR, - payload: { code: 'INVALID_MESSAGE', message: error.message }, - }); - } - } - - /** - * Handle authentication - */ - handleAuthenticate(clientId: any, payload: any) { - const client = this.clients.get(clientId); - if (!client) return; - - const { token, clientType = ClientTypes.BROWSER } = payload; - - try { - // Verify JWT - const decoded = jwt.verify(token, this.jwtSecret); - - // Update client state - client.user = { - id: decoded.userId || decoded.id, - username: decoded.username, - }; - client.type = clientType; - - // Track agent connections - if (clientType === ClientTypes.AGENT && payload.agentId) { - this.agentConnections.set(payload.agentId, clientId); - client.agentId = payload.agentId; - } - - this.sendToClient(clientId, { - type: EventTypes.AUTHENTICATED, - payload: { - user: client.user, - clientType: client.type, - }, - }); - - this.emit(EventTypes.AUTHENTICATED, { clientId, user: client.user, clientType }); - } catch (error) { - this.sendToClient(clientId, { - type: EventTypes.ERROR, - payload: { code: 'AUTH_FAILED', message: 'Invalid token' }, - }); - } - } - - /** - * Handle subscription to pod events - */ - handleSubscribe(clientId: any, payload: any) { - const client = this.clients.get(clientId); - if (!client) return; - - const { podId, events = [] } = payload; - - // Add to pod subscriptions - if (!this.podSubscriptions.has(podId)) { - this.podSubscriptions.set(podId, new Set()); - } - this.podSubscriptions.get(podId).add(clientId); - - // Track on client - client.subscriptions.add(podId); - - this.sendToClient(clientId, { - type: 'subscribed', - payload: { podId, events }, - }); - } - - /** - * Handle unsubscription - */ - handleUnsubscribe(clientId: any, payload: any) { - const client = this.clients.get(clientId); - if (!client) return; - - const { podId } = payload; - - // Remove from pod subscriptions - const subscribers = this.podSubscriptions.get(podId); - if (subscribers) { - subscribers.delete(clientId); - if (subscribers.size === 0) { - this.podSubscriptions.delete(podId); - } - } - - // Remove from client - client.subscriptions.delete(podId); - - this.sendToClient(clientId, { - type: 'unsubscribed', - payload: { podId }, - }); - } - - /** - * Handle disconnect - */ - handleDisconnect(clientId: any) { - const client = this.clients.get(clientId); - if (!client) return; - - // Clean up subscriptions - client.subscriptions.forEach((podId: any) => { - const subscribers = this.podSubscriptions.get(podId); - if (subscribers) { - subscribers.delete(clientId); - if (subscribers.size === 0) { - this.podSubscriptions.delete(podId); - } - } - }); - - // Clean up agent connection - if (client.agentId) { - this.agentConnections.delete(client.agentId); - this.emit(EventTypes.AGENT_DISCONNECTED, { agentId: client.agentId }); - } - - this.clients.delete(clientId); - this.stats.activeConnections -= 1; - - this.emit(EventTypes.DISCONNECT, { clientId, user: client.user }); - } - - /** - * Handle pong (heartbeat response) - */ - handlePong(clientId: any) { - const client = this.clients.get(clientId); - if (client) { - client.isAlive = true; - } - } - - /** - * Check heartbeats and disconnect stale clients - */ - checkHeartbeats() { - this.clients.forEach((client: any, clientId: any) => { - if (!client.isAlive) { - console.log(`Terminating stale connection: ${clientId}`); - client.ws.terminate(); - this.handleDisconnect(clientId); - } else { - client.isAlive = false; - client.ws.ping(); - } - }); - } - - /** - * Send message to a specific client - */ - sendToClient(clientId: any, message: any) { - const client = this.clients.get(clientId); - if (client && client.ws.readyState === WebSocket.OPEN) { - client.ws.send(JSON.stringify(message)); - this.stats.messagesSent += 1; - } - } - - /** - * Broadcast message to all subscribers of a pod - */ - broadcastToPod(podId: any, message: any, excludeClientId: any = null) { - const subscribers = this.podSubscriptions.get(podId); - if (!subscribers) return; - - subscribers.forEach((clientId: any) => { - if (clientId !== excludeClientId) { - this.sendToClient(clientId, message); - } - }); - } - - /** - * Send message to a specific agent - */ - sendToAgent(agentId: any, message: any) { - const clientId = this.agentConnections.get(agentId); - if (clientId) { - this.sendToClient(clientId, message); - } - } - - /** - * Handle broadcast to pod (from a client) - */ - handleBroadcastToPod(clientId: any, podId: any, message: any) { - const client = this.clients.get(clientId); - if (!client || !client.subscriptions.has(podId)) { - this.sendToClient(clientId, { - type: EventTypes.ERROR, - payload: { code: 'NOT_SUBSCRIBED', message: 'Not subscribed to this pod' }, - }); - return; - } - - // Add sender info - message.payload.sender = { - id: client.user.id, - username: client.user.username, - type: client.type, - clientId, - }; - - this.broadcastToPod(podId, message, clientId); - } - - /** - * Handle server error - */ - handleError(error: any) { - console.error('Gateway error:', error); - this.emit('error', error); - } - - /** - * Generate unique client ID - */ - // eslint-disable-next-line class-methods-use-this - generateClientId() { - return `${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 9)}`; - } - - /** - * Get gateway statistics - */ - getStats() { - return { - ...this.stats, - clientsByType: this.getClientsByType(), - podSubscriptionCount: this.podSubscriptions.size, - agentConnectionCount: this.agentConnections.size, - }; - } - - /** - * Get client count by type - */ - getClientsByType() { - const counts: Record = {}; - this.clients.forEach((client: any) => { - counts[client.type] = (counts[client.type] || 0) + 1; - }); - return counts; - } -} - -// Export singleton instance -let gateway: Gateway | null = null; - -const getGateway = () => { - if (!gateway) { - gateway = new Gateway(); - } - return gateway; -}; - -const startGateway = (_options?: any) => { - const gw = getGateway(); - gw.start(); - return gw; -}; - -module.exports = { - Gateway, - EventTypes, - ClientTypes, - getGateway, - startGateway, -}; diff --git a/backend/middleware/appAuth.ts b/backend/middleware/appAuth.ts deleted file mode 100644 index 133d835d50..0000000000 --- a/backend/middleware/appAuth.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import AppInstallation from '../models/AppInstallation'; - -// eslint-disable-next-line global-require -const { hash } = require('../utils/secret') as { hash: (value: string) => string }; - -export default async function appAuth(req: Request, res: Response, next: NextFunction): Promise { - try { - const authHeader = req.headers.authorization || ''; - const [, token] = authHeader.split(' '); - if (!token) return res.status(401).json({ error: 'Missing token' }); - - const tokenHash = hash(token); - const inst = await AppInstallation.findOne({ tokenHash, status: 'active' }); - if (!inst) return res.status(401).json({ error: 'Invalid token' }); - - if (inst.tokenExpiresAt && inst.tokenExpiresAt < new Date()) { - return res.status(401).json({ error: 'Token expired' }); - } - - req.appInstallation = inst; - return next(); - } catch (error) { - console.error('appAuth error', error); - return res.status(500).json({ error: 'Internal server error' }); - } -} -// CJS compat: let require() return the default export directly -// eslint-disable-next-line @typescript-eslint/no-require-imports -module.exports = exports["default"]; Object.assign(module.exports, exports); diff --git a/backend/routes/health.ts b/backend/routes/health.ts index 46acc1d7d7..fcefa52abe 100644 --- a/backend/routes/health.ts +++ b/backend/routes/health.ts @@ -221,7 +221,7 @@ router.get('/ready', async (_req: unknown, res: Res) => { }); // Anonymous, and each hit makes a request to the agent gateway, so it gets the -// IP tier. The one caller (AgentsHub) asks once per page load. +// IP tier for health checks from the legacy agent manager. const gatewayHealthLimit = platformIpRateLimit({ windowMs: 60 * 1000, limit: 60, diff --git a/backend/services/agentPersonalityService.ts b/backend/services/agentPersonalityService.ts deleted file mode 100644 index 4134fcb97d..0000000000 --- a/backend/services/agentPersonalityService.ts +++ /dev/null @@ -1,206 +0,0 @@ -import User from '../models/User'; - -interface PersonalityConfig { - tone?: string; - interests?: string[]; - behavior?: string; - responseStyle?: string; - specialties?: string[]; - boundaries?: string[]; - customInstructions?: string; -} - -interface GenerateSystemPromptOptions { - tone: string; - interests?: string[]; - behavior: string; - responseStyle: string; - specialties?: string[]; - boundaries?: string[]; - customInstructions?: string; -} - -class AgentPersonalityService { - static generateSystemPrompt({ - tone, interests, behavior, responseStyle, specialties = [], boundaries = [], customInstructions = '', - }: GenerateSystemPromptOptions): string { - const tonePrompts: Record = { - friendly: 'You are warm, welcoming, and helpful. Use casual language and emojis occasionally. Make users feel comfortable.', - professional: 'You are polite, formal, and competent. Provide well-structured, business-appropriate responses. Maintain professionalism.', - sarcastic: 'You have a witty, sarcastic personality. Use humor while still being helpful. Keep things entertaining but constructive.', - educational: 'You are a knowledgeable teacher. Explain concepts clearly with examples. Help users learn and understand deeply.', - humorous: 'You are funny and entertaining. Make people laugh while being helpful. Use jokes, puns, and playful language.', - }; - - const behaviorPrompts: Record = { - reactive: 'Only respond when directly mentioned or asked a question. Wait for explicit user engagement.', - proactive: 'Actively participate in discussions and share relevant insights. Initiate helpful conversations when appropriate.', - balanced: 'Respond to mentions and occasionally contribute to relevant discussions. Balance between reactive and proactive.', - }; - - const stylePrompts: Record = { - concise: 'Keep responses brief and to the point (1-2 sentences). Value clarity over completeness.', - detailed: 'Provide comprehensive, well-explained responses. Include context, examples, and thorough explanations.', - conversational: 'Write in a natural, friendly conversational style. Sound like a real person having a chat.', - }; - - let prompt = 'You are an AI agent participating in a social community.\n\n'; - - prompt += `**Communication Tone**: ${tonePrompts[tone] || tonePrompts.friendly}\n\n`; - prompt += `**Behavior**: ${behaviorPrompts[behavior] || behaviorPrompts.reactive}\n\n`; - prompt += `**Response Style**: ${stylePrompts[responseStyle] || stylePrompts.conversational}\n\n`; - - if (interests && interests.length > 0) { - prompt += `**Your Interests**: ${interests.join(', ')}. You enjoy discussing these topics and bring relevant insights when they come up.\n\n`; - } - - if (specialties && specialties.length > 0) { - prompt += `**Your Specialties**:\n${specialties.map((s) => `- ${s}`).join('\n')}\n\n`; - } - - if (boundaries && boundaries.length > 0) { - prompt += `**Your Boundaries** (what you won't do):\n${boundaries.map((b) => `- ${b}`).join('\n')}\n\n`; - } - - if (customInstructions) { - prompt += `**Additional Instructions**:\n${customInstructions}\n\n`; - } - - prompt += '**Overall Guidance**: Be authentic, engaging, and add value to conversations. Respect boundaries while being helpful and friendly.'; - - return prompt; - } - - static async updatePersonality(userId: unknown, personalityConfig: PersonalityConfig): Promise | null> { - const user = await User.findByIdAndUpdate( - userId, - { - 'agentConfig.personality': personalityConfig, - 'agentConfig.systemPrompt': this.generateSystemPrompt(personalityConfig as GenerateSystemPromptOptions), - }, - { new: true }, - ); - return user; - } - - static async getPersonalityConfig(userId: unknown): Promise { - const user = await User.findById(userId).lean() as Record | null; - if (!user || !user.agentConfig) { - return this.getDefaultPersonality(); - } - return (user.agentConfig as Record).personality as PersonalityConfig || this.getDefaultPersonality(); - } - - static getDefaultPersonality(): PersonalityConfig { - return { - tone: 'friendly', - interests: [], - behavior: 'reactive', - responseStyle: 'conversational', - specialties: [], - boundaries: [ - 'Generate harmful or illegal content', - 'Impersonate real people', - 'Share private or sensitive information', - ], - customInstructions: '', - }; - } - - static generateExamplePersonality(agentType: string): PersonalityConfig { - const examples: Record = { - 'curator-bot': { - tone: 'friendly', - interests: ['trending topics', 'social media', 'content discovery', 'community building'], - behavior: 'proactive', - responseStyle: 'conversational', - specialties: [ - 'Finding interesting content', - 'Identifying trending topics', - 'Curating quality discussions', - 'Highlighting important updates', - ], - boundaries: [ - 'Share spam or low-quality content', - 'Promote harmful or offensive material', - 'Violate content policies', - ], - customInstructions: 'You love discovering and sharing interesting content. When you find something noteworthy, you provide context and explain why it matters to the community.', - }, - 'commonly-bot': { - tone: 'friendly', - interests: ['community activity', 'summaries', 'daily digests', 'social trends', 'conversation starters'], - behavior: 'proactive', - responseStyle: 'concise', - specialties: [ - 'Creating engaging summaries', - 'Tracking community activity', - 'Highlighting key moments', - 'Generating insights', - 'Curating social feed highlights', - ], - boundaries: [ - 'Share private messages', - 'Reveal sensitive information', - 'Misrepresent user activity', - 'Copy protected content verbatim from external platforms', - ], - customInstructions: 'You help users stay updated with friendly summaries, concise social highlights, and useful prompts that spark discussion.', - }, - openclaw: { - tone: 'friendly', - interests: ['conversation', 'helping users', 'problem solving', 'learning'], - behavior: 'reactive', - responseStyle: 'conversational', - specialties: [ - 'Natural conversation', - 'Answering questions', - 'Providing assistance', - 'Context-aware responses', - ], - boundaries: [ - 'Generate harmful content', - 'Impersonate users', - 'Access restricted information', - ], - customInstructions: 'You are a helpful conversational AI that responds naturally to user messages.', - }, - 'content-curator': { - tone: 'friendly', - interests: ['trending topics', 'social media', 'content discovery', 'community building', 'quality content'], - behavior: 'proactive', - responseStyle: 'conversational', - specialties: [ - 'Finding interesting content from social feeds', - 'Identifying trending topics and viral potential', - 'Providing context and background for shared posts', - 'Curating quality discussions', - 'Highlighting valuable insights', - ], - boundaries: [ - 'Share spam or low-quality content', - 'Promote harmful or offensive material', - 'Violate content policies or copyright', - 'Share private or sensitive information', - ], - customInstructions: `You are a content curator who loves discovering and sharing interesting posts from social feeds (X, Instagram, etc.). - -When you find noteworthy content, you: -- Explain WHY it matters to the community -- Provide context and background -- Connect it to community interests -- Highlight key insights -- Add your unique perspective - -You proactively monitor feeds and share 2-3 curated picks every few hours. Quality over quantity!`, - }, - }; - - return examples[agentType] || this.getDefaultPersonality(); - } -} - -export default AgentPersonalityService; -// CJS compat: let require() return the default export directly -// eslint-disable-next-line @typescript-eslint/no-require-imports -module.exports = exports["default"]; Object.assign(module.exports, exports); diff --git a/backend/services/integrationService.ts b/backend/services/integrationService.ts deleted file mode 100644 index ec95cfde58..0000000000 --- a/backend/services/integrationService.ts +++ /dev/null @@ -1,86 +0,0 @@ -import Integration from '../models/Integration'; - -/** - * Common Integration Service - * Abstract base class for all platform integrations - */ -class IntegrationService { - protected integrationId: unknown; - protected integration: InstanceType | null; - - constructor(integrationId: unknown) { - this.integrationId = integrationId; - this.integration = null; - } - - static async initialize(): Promise { - throw new Error('initialize() must be implemented by subclass'); - } - - static async connect(): Promise { - throw new Error('connect() must be implemented by subclass'); - } - - static async disconnect(): Promise { - throw new Error('disconnect() must be implemented by subclass'); - } - - static async fetchMessages(_options: Record = {}): Promise { - throw new Error('fetchMessages() must be implemented by subclass'); - } - - static async sendMessage(_message: string, _options: Record = {}): Promise { - throw new Error('sendMessage() must be implemented by subclass'); - } - - static async getChannels(): Promise { - throw new Error('getChannels() must be implemented by subclass'); - } - - static async getStatus(): Promise { - throw new Error('getStatus() must be implemented by subclass'); - } - - static async testConnection(): Promise { - throw new Error('testConnection() must be implemented by subclass'); - } - - async updateStatus(status: string, errorMessage: string | null = null): Promise { - try { - await Integration.findByIdAndUpdate(this.integrationId, { - status, - errorMessage, - lastSync: - status === 'connected' ? new Date() : (this.integration as unknown as Record)?.lastSync, - }); - - if (this.integration) { - (this.integration as unknown as Record).status = status; - (this.integration as unknown as Record).errorMessage = errorMessage; - if (status === 'connected') { - (this.integration as unknown as Record).lastSync = new Date(); - } - } - } catch (error) { - console.error('Error updating integration status:', error); - throw error; - } - } - - static async validateConfig(_config: Record): Promise { - throw new Error('validateConfig() must be implemented by subclass'); - } - - static async getStats(): Promise> { - throw new Error('getStats() must be implemented by subclass'); - } - - static async handleWebhook(_event: Record): Promise { - throw new Error('handleWebhook() must be implemented by subclass'); - } -} - -export default IntegrationService; -// CJS compat: let require() return the default export directly -// eslint-disable-next-line @typescript-eslint/no-require-imports -module.exports = exports["default"]; Object.assign(module.exports, exports); diff --git a/backend/sync-pods.ts b/backend/sync-pods.ts deleted file mode 100644 index aa835b1e66..0000000000 --- a/backend/sync-pods.ts +++ /dev/null @@ -1,98 +0,0 @@ -// Synchronize pods from MongoDB to PostgreSQL -const mongoose = require('mongoose'); -const { pool } = require('./config/db-pg'); -require('dotenv').config(); -const Pod = require('./models/Pod'); // MongoDB model -const PGPod = require('./models/pg/Pod'); // PostgreSQL model - -async function syncPods() { - try { - // Get MongoDB URI from environment - const mongoURI = process.env.MONGO_URI; - if (!mongoURI) { - throw new Error('MONGO_URI environment variable is not set'); - } - - console.log('Connecting to MongoDB...'); - await mongoose.connect(mongoURI, { - useNewUrlParser: true, - useUnifiedTopology: true, - }); - console.log('MongoDB Connected!'); - - // Fetch all pods from MongoDB - console.log('Fetching pods from MongoDB...'); - const mongoPods = await Pod.find(); - console.log(`Found ${mongoPods.length} pods in MongoDB`); - - // For each MongoDB pod, check if it exists in PostgreSQL - let synced = 0; - for (const mongoPod of mongoPods) { - try { - const mongoPodId = mongoPod._id.toString(); - console.log(`Checking pod: ${mongoPodId} (${mongoPod.name})`); - - // Check if pod exists in PostgreSQL - const pgPod = await PGPod.findById(mongoPodId); - - if (!pgPod) { - console.log(`Syncing pod: ${mongoPodId} (${mongoPod.name})`); - - // Insert pod into PostgreSQL with the same ID - const query = ` - INSERT INTO pods (id, name, description, type, created_by) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (id) DO NOTHING - RETURNING * - `; - - await pool.query(query, [ - mongoPodId, - mongoPod.name || 'Unnamed Pod', - mongoPod.description || '', - mongoPod.type || 'chat', - mongoPod.createdBy ? mongoPod.createdBy.toString() : 'unknown', - ]); - - // Add members to the pod - if (mongoPod.members && mongoPod.members.length > 0) { - for (const memberId of mongoPod.members) { - try { - await PGPod.addMember(mongoPodId, memberId.toString()); - } catch (memberErr: unknown) { - console.error( - `Error adding member ${memberId} to pod ${mongoPodId}:`, - ((memberErr) as Error).message, - ); - } - } - } - - synced++; - } else { - console.log(`Pod already exists in PostgreSQL: ${mongoPodId}`); - } - } catch (err: unknown) { - console.error(`Error syncing pod ${mongoPod._id}:`, ((err) as Error).message); - } - } - - console.log(`Synchronized ${synced} pods from MongoDB to PostgreSQL`); - } catch (err: unknown) { - console.error('Error:', ((err) as Error).message); - } finally { - // Close connections - if (mongoose.connection.readyState !== 0) { - await mongoose.disconnect(); - } - await pool.end(); - } -} - -// Run the sync if executed directly -if (require.main === module) { - syncPods(); -} - -module.exports = syncPods; -export {}; diff --git a/backend/test-discord-commands.ts b/backend/test-discord-commands.ts deleted file mode 100644 index 8171b17867..0000000000 --- a/backend/test-discord-commands.ts +++ /dev/null @@ -1,104 +0,0 @@ -const DiscordCommandService = require("./services/discordCommandService"); -const Integration = require("./models/Integration"); - -/** - * Test Discord Command Service - * This script tests the Discord command functionality - */ -async function testDiscordCommands() { - console.log("πŸ§ͺ Testing Discord Command Service...\n"); - - try { - // Find a Discord integration to test with - const integration = await Integration.findOne({ - type: "discord", - isActive: true, - }); - - if (!integration) { - console.log( - "❌ No active Discord integration found. Please create one first.", - ); - return; - } - - console.log(`πŸ“‹ Testing with integration: ${integration._id}`); - console.log(`🏠 Server: ${integration.config.serverName || "Unknown"}`); - console.log(`πŸ“Ί Channel: ${integration.config.channelName || "Unknown"}\n`); - - // Initialize command service - const commandService = new DiscordCommandService( - integration.installationId, - ); - const initialized = await commandService.initialize(); - - if (!initialized) { - console.log("❌ Failed to initialize command service"); - return; - } - - console.log("βœ… Command service initialized successfully\n"); - - // Test status command - console.log("πŸ” Testing /discord-status command..."); - const statusResult = await commandService.handleStatusCommand(); - console.log("Status Result:", statusResult.success ? "βœ…" : "❌"); - console.log("Content:", statusResult.content); - console.log(""); - - // Test summary command - console.log("πŸ“Š Testing /commonly-summary command..."); - const summaryResult = await commandService.handleSummaryCommand(); - console.log("Summary Result:", summaryResult.success ? "βœ…" : "❌"); - console.log("Content:", summaryResult.content); - console.log(""); - - // Test enable command - console.log("βœ… Testing /discord-enable command..."); - const enableResult = await commandService.handleEnableCommand(); - console.log("Enable Result:", enableResult.success ? "βœ…" : "❌"); - console.log("Content:", enableResult.content); - console.log(""); - - // Test disable command - console.log("❌ Testing /discord-disable command..."); - const disableResult = await commandService.handleDisableCommand(); - console.log("Disable Result:", disableResult.success ? "βœ…" : "❌"); - console.log("Content:", disableResult.content); - console.log(""); - - console.log("πŸŽ‰ Discord command tests completed!"); - } catch (error) { - console.error("❌ Error testing Discord commands:", error); - } -} - -// Run the test if this file is executed directly -if (require.main === module) { - // Connect to database first - // eslint-disable-next-line global-require - const mongoose = require("mongoose"); - const { MONGODB_URI } = process.env; - - if (!MONGODB_URI) { - console.error("❌ MONGODB_URI environment variable is required"); - process.exit(1); - } - - mongoose - .connect(MONGODB_URI) - .then(() => { - console.log("πŸ“¦ Connected to MongoDB"); - return testDiscordCommands(); - }) - .then(() => { - console.log("βœ… Test completed"); - process.exit(0); - }) - .catch((error) => { - console.error("❌ Test failed:", error); - process.exit(1); - }); -} - -module.exports = { testDiscordCommands }; diff --git a/backend/test-discord-integration.ts b/backend/test-discord-integration.ts deleted file mode 100644 index 894defe7ff..0000000000 --- a/backend/test-discord-integration.ts +++ /dev/null @@ -1,303 +0,0 @@ -/** - * Discord Integration Test Script - * Run this script to test the Discord integration functionality - */ - -const axios = require("axios"); -const _DiscordService = require("./services/discordService"); -const _DiscordIntegration = require("./models/DiscordIntegration"); -const _Integration = require("./models/Integration"); -// const mongoose = require('mongoose'); -require("dotenv").config(); - -// Test configuration -const TEST_CONFIG = { - serverId: "123456789", - serverName: "Test Server", - channelId: "987654321", - channelName: "#test-channel", - webhookUrl: "https://discord.com/api/webhooks/test", - botToken: "test-token", - permissions: ["read_messages", "send_messages"], -}; - -// Test functions -async function testWebhookConnection() { - console.log("πŸ”— Testing webhook connection..."); - - try { - const response = await axios.post( - `${TEST_CONFIG.baseURL}/api/discord/test-webhook`, - { - webhookUrl: TEST_CONFIG.webhookUrl, - }, - { - headers: { - Authorization: `Bearer ${TEST_CONFIG.authToken}`, - "Content-Type": "application/json", - }, - }, - ); - - if (response.data.success) { - console.log("βœ… Webhook test successful!"); - return true; - } - console.log("❌ Webhook test failed:", response.data.message); - return false; - } catch (error) { - console.error( - "❌ Webhook test error:", - error.response?.data || error.message, - ); - return false; - } -} - -async function testCreateIntegration() { - console.log("πŸ€– Testing Discord integration creation..."); - - try { - const response = await axios.post( - `${TEST_CONFIG.baseURL}/api/discord/integration`, - { - podId: TEST_CONFIG.podId, - serverId: TEST_CONFIG.serverId, - serverName: "Commonly Test Server", - channelId: TEST_CONFIG.channelId, - channelName: "test-channel", - webhookUrl: TEST_CONFIG.webhookUrl, - botToken: TEST_CONFIG.botToken, - }, - { - headers: { - Authorization: `Bearer ${TEST_CONFIG.authToken}`, - "Content-Type": "application/json", - }, - }, - ); - - console.log("βœ… Integration created successfully!"); - console.log("Integration ID:", response.data.integration._id); - return response.data.integration._id; - } catch (error) { - console.error( - "❌ Integration creation error:", - error.response?.data || error.message, - ); - return null; - } -} - -async function testGetIntegrations() { - console.log("πŸ“‹ Testing get integrations..."); - - try { - const response = await axios.get( - `${TEST_CONFIG.baseURL}/api/integrations/${TEST_CONFIG.podId}`, - { - headers: { - Authorization: `Bearer ${TEST_CONFIG.authToken}`, - }, - }, - ); - - console.log("βœ… Integrations retrieved successfully!"); - console.log("Found integrations:", response.data.length); - response.data.forEach((integration, index) => { - console.log( - ` ${index + 1}. ${integration.type} - ${integration.status}`, - ); - }); - return response.data; - } catch (error) { - console.error( - "❌ Get integrations error:", - error.response?.data || error.message, - ); - return []; - } -} - -async function testSendDiscordMessage(integrationId) { - console.log("πŸ’¬ Testing send Discord message..."); - - try { - const _response = await axios.post( - `${TEST_CONFIG.baseURL}/api/integrations/${integrationId}/send`, - { - message: "πŸ€– Test message from Commonly integration!", - type: "discord", - }, - { - headers: { - Authorization: `Bearer ${TEST_CONFIG.authToken}`, - "Content-Type": "application/json", - }, - }, - ); - - console.log("βœ… Message sent successfully!"); - return true; - } catch (error) { - console.error( - "❌ Send message error:", - error.response?.data || error.message, - ); - return false; - } -} - -async function testFetchMessages(integrationId) { - console.log("πŸ“₯ Testing fetch messages..."); - - try { - const _response = await axios.get( - `${TEST_CONFIG.baseURL}/api/integrations/${integrationId}/messages`, - { - headers: { - Authorization: `Bearer ${TEST_CONFIG.authToken}`, - }, - }, - ); - - console.log("βœ… Messages fetched successfully!"); - console.log("Message count:", _response.data.length); - return _response.data; - } catch (error) { - console.error( - "❌ Fetch messages error:", - error.response?.data || error.message, - ); - return []; - } -} - -async function testGetStats(integrationId) { - console.log("πŸ“Š Testing get stats..."); - - try { - const _response = await axios.get( - `${TEST_CONFIG.baseURL}/api/discord/stats/${integrationId}`, - { - headers: { - Authorization: `Bearer ${TEST_CONFIG.authToken}`, - }, - }, - ); - - console.log("βœ… Stats retrieved successfully!"); - console.log("Stats:", _response.data); - return _response.data; - } catch (error) { - console.error("❌ Get stats error:", error.response?.data || error.message); - return null; - } -} - -async function testDeleteIntegration(integrationId) { - console.log("πŸ—‘οΈ Testing delete integration..."); - - try { - const _response = await axios.delete( - `${TEST_CONFIG.baseURL}/api/integrations/${integrationId}`, - { - headers: { - Authorization: `Bearer ${TEST_CONFIG.authToken}`, - }, - }, - ); - - console.log("βœ… Integration deleted successfully!"); - return true; - } catch (error) { - console.error( - "❌ Delete integration error:", - error.response?.data || error.message, - ); - return false; - } -} - -// Main test function -async function runTests() { - console.log("πŸš€ Starting Discord Integration Tests...\n"); - - // Check if required environment variables are set - if ( - TEST_CONFIG.webhookUrl === "YOUR_WEBHOOK_URL" || - TEST_CONFIG.botToken === "YOUR_BOT_TOKEN" || - TEST_CONFIG.podId === "YOUR_POD_ID" || - TEST_CONFIG.authToken === "YOUR_JWT_TOKEN" - ) { - console.log( - "⚠️ Please update TEST_CONFIG with your actual values before running tests.", - ); - console.log(" See docs/discord/DISCORD_SETUP.md for setup instructions.\n"); - return; - } - - let integrationId = null; - - try { - // Test 1: Webhook connection - const webhookTest = await testWebhookConnection(); - if (!webhookTest) { - console.log("❌ Webhook test failed. Stopping tests.\n"); - return; - } - - // Test 2: Get existing integrations - await testGetIntegrations(); - - // Test 3: Create new integration - integrationId = await testCreateIntegration(); - if (!integrationId) { - console.log("❌ Integration creation failed. Stopping tests.\n"); - return; - } - - // Test 4: Get stats - await testGetStats(integrationId); - - // Test 5: Fetch messages - await testFetchMessages(integrationId); - - // Test 6: Send message (optional - requires proper setup) - // await testSendDiscordMessage(integrationId); - - console.log("\nβœ… All tests completed successfully!"); - } catch (error) { - console.error("❌ Test execution error:", error.message); - } finally { - // Cleanup: Delete test integration - if (integrationId) { - console.log("\n🧹 Cleaning up test integration..."); - await testDeleteIntegration(integrationId); - } - } -} - -// Run tests if this file is executed directly -if (require.main === module) { - runTests() - .then(() => { - console.log("\n🏁 Test script finished."); - process.exit(0); - }) - .catch((error) => { - console.error("πŸ’₯ Test script failed:", error); - process.exit(1); - }); -} - -module.exports = { - testWebhookConnection, - testCreateIntegration, - testGetIntegrations, - testSendDiscordMessage, - testFetchMessages, - testGetStats, - testDeleteIntegration, - runTests, -}; diff --git a/backend/test-discord-interactions.ts b/backend/test-discord-interactions.ts deleted file mode 100644 index 98ca5d3c41..0000000000 --- a/backend/test-discord-interactions.ts +++ /dev/null @@ -1,313 +0,0 @@ -const DiscordService = require("./services/discordService"); -const DiscordCommandService = require("./services/discordCommandService"); -const Integration = require("./models/Integration"); - -/** - * Test Discord Interaction Standards Compliance - * This script verifies our implementation follows Discord's official interaction standards - */ -async function testDiscordInteractions() { - console.log("πŸ§ͺ Testing Discord Interaction Standards Compliance...\n"); - - try { - // Find a Discord integration to test with - const integration = await Integration.findOne({ - type: "discord", - isActive: true, - }); - - if (!integration) { - console.log( - "❌ No active Discord integration found. Please create one first.", - ); - return; - } - - console.log(`πŸ“‹ Testing with integration: ${integration._id}`); - console.log(`🏠 Server: ${integration.config.serverName || "Unknown"}`); - console.log(`πŸ“Ί Channel: ${integration.config.channelName || "Unknown"}\n`); - - // Test 1: Verify Interaction Structure Handling - console.log("πŸ” Test 1: Interaction Structure Handling"); - await testInteractionStructure(); - console.log(""); - - // Test 2: Verify Response Format - console.log("πŸ“ Test 2: Response Format Compliance"); - await testResponseFormat(); - console.log(""); - - // Test 3: Verify Command Service - console.log("πŸ€– Test 3: Command Service Integration"); - await testCommandService(integration); - console.log(""); - - // Test 4: Verify Error Handling - console.log("⚠️ Test 4: Error Handling"); - await testErrorHandling(); - console.log(""); - - console.log("πŸŽ‰ Discord interaction standards compliance tests completed!"); - } catch (error) { - console.error("❌ Error testing Discord interactions:", error); - } -} - -/** - * Test 1: Verify we handle Discord's interaction structure correctly - */ -async function testInteractionStructure() { - console.log(" Testing interaction object structure..."); - - // Mock Discord interaction object following official structure - const mockInteraction = { - id: "1234567890123456789", - application_id: "9876543210987654321", - type: 2, // APPLICATION_COMMAND - data: { - id: "1111111111111111111", - name: "discord-status", - type: 1, // CHAT_INPUT - guild_id: "2222222222222222222", - }, - guild_id: "2222222222222222222", - channel_id: "3333333333333333333", - member: { - user: { - id: "4444444444444444444", - username: "testuser", - }, - }, - token: "mock_interaction_token_12345", - version: 1, - }; - - // Verify required fields are present - const requiredFields = ["id", "type", "data", "token", "version"]; - const missingFields = requiredFields.filter( - (field) => !mockInteraction[field], - ); - - if (missingFields.length > 0) { - console.log(` ❌ Missing required fields: ${missingFields.join(", ")}`); - return false; - } - - // Verify interaction type - if (mockInteraction.type !== 2) { - console.log(" ❌ Invalid interaction type"); - return false; - } - - // Verify command data structure - if (!mockInteraction.data || !mockInteraction.data.name) { - console.log(" ❌ Invalid command data structure"); - return false; - } - - console.log(" βœ… Interaction structure handling is correct"); - return true; -} - -/** - * Test 2: Verify response format follows Discord standards - */ -async function testResponseFormat() { - console.log(" Testing response format compliance..."); - - // Test valid response format - const validResponse = { - type: 4, // CHANNEL_MESSAGE_WITH_SOURCE - data: { - content: "Test response message", - flags: 0, // No flags - }, - }; - - // Test ephemeral response format - const ephemeralResponse = { - type: 4, - data: { - content: "Error message", - flags: 64, // EPHEMERAL flag - }, - }; - - // Test deferred response format - const deferredResponse = { - type: 5, // DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE - data: { - flags: 0, - }, - }; - - // Verify response types are valid - const validTypes = [1, 4, 5, 6, 7, 8, 9]; - const testResponses = [validResponse, ephemeralResponse, deferredResponse]; - - for (const response of testResponses) { - if (!validTypes.includes(response.type)) { - console.log(` ❌ Invalid response type: ${response.type}`); - return false; - } - } - - // Verify flags are valid - const validFlags = [0, 64, 32768]; // None, EPHEMERAL, IS_COMPONENTS_V2 - for (const response of testResponses) { - if (response.data && response.data.flags !== undefined) { - if (!validFlags.includes(response.data.flags)) { - console.log(` ❌ Invalid response flags: ${response.data.flags}`); - return false; - } - } - } - - console.log(" βœ… Response format compliance is correct"); - return true; -} - -/** - * Test 3: Verify command service integration - */ -async function testCommandService(integration) { - console.log(" Testing command service integration..."); - - try { - // Initialize command service - const commandService = new DiscordCommandService( - integration.installationId, - ); - const initialized = await commandService.initialize(); - - if (!initialized) { - console.log(" ❌ Failed to initialize command service"); - return false; - } - - // Test status command - const statusResult = await commandService.handleStatusCommand(); - if (!statusResult || typeof statusResult.success !== "boolean") { - console.log(" ❌ Invalid status command response format"); - return false; - } - - // Test summary command - const summaryResult = await commandService.handleSummaryCommand(); - if (!summaryResult || typeof summaryResult.success !== "boolean") { - console.log(" ❌ Invalid summary command response format"); - return false; - } - - // Test enable command - const enableResult = await commandService.handleEnableCommand(); - if (!enableResult || typeof enableResult.success !== "boolean") { - console.log(" ❌ Invalid enable command response format"); - return false; - } - - // Test disable command - const disableResult = await commandService.handleDisableCommand(); - if (!disableResult || typeof disableResult.success !== "boolean") { - console.log(" ❌ Invalid disable command response format"); - return false; - } - - console.log(" βœ… Command service integration is correct"); - return true; - } catch (error) { - console.log(` ❌ Command service integration error: ${error.message}`); - return false; - } -} - -/** - * Test 4: Verify error handling - */ -async function testErrorHandling() { - console.log(" Testing error handling..."); - - // Test invalid command handling - const mockInvalidCommand = { - type: 2, - data: { - name: "invalid-command", - type: 1, - }, - }; - - // Test missing integration handling - const mockMissingIntegration = { - type: 2, - data: { - name: "discord-status", - type: 1, - }, - guild_id: "non-existent-guild", - }; - - // Test malformed interaction - const mockMalformedInteraction = { - type: 2, - // Missing data field - }; - - console.log( - " βœ… Error handling tests completed (manual verification required)", - ); - return true; -} - -/** - * Test Discord API endpoint compliance - */ -async function testApiEndpoints() { - console.log(" Testing API endpoint compliance..."); - - // Test interaction callback endpoint format - const callbackUrl = - "https://discord.com/api/v10/interactions/{interaction.id}/{interaction.token}/callback"; - console.log(` Expected callback URL format: ${callbackUrl}`); - - // Test webhook followup endpoint format - const followupUrl = - "https://discord.com/api/v10/webhooks/{application.id}/{interaction.token}"; - console.log(` Expected followup URL format: ${followupUrl}`); - - // Test original message endpoint format - const originalUrl = - "https://discord.com/api/v10/webhooks/{application.id}/{interaction.token}/messages/@original"; - console.log(` Expected original message URL format: ${originalUrl}`); - - console.log(" βœ… API endpoint compliance is correct"); - return true; -} - -// Run the test if this file is executed directly -if (require.main === module) { - // Connect to database first - const mongoose = require("mongoose"); - const { MONGODB_URI } = process.env; - - if (!MONGODB_URI) { - console.error("❌ MONGODB_URI environment variable is required"); - process.exit(1); - } - - mongoose - .connect(MONGODB_URI) - .then(() => { - console.log("πŸ“¦ Connected to MongoDB"); - return testDiscordInteractions(); - }) - .then(() => { - console.log("βœ… Interaction standards test completed"); - process.exit(0); - }) - .catch((error) => { - console.error("❌ Interaction standards test failed:", error); - process.exit(1); - }); -} - -module.exports = { testDiscordInteractions }; diff --git a/backend/testPG.ts b/backend/testPG.ts deleted file mode 100644 index dcdce864c5..0000000000 --- a/backend/testPG.ts +++ /dev/null @@ -1,92 +0,0 @@ -// Test script to validate PostgreSQL setup -const { pool } = require('./config/db-pg'); - -async function testPG() { - try { - // 1. Test connection - console.log('Testing PostgreSQL connection...'); - await pool.query('SELECT NOW()'); - console.log('Connection successful!'); - - // 2. Check table structures - console.log('\nChecking table structures:'); - const tables = ['pods', 'pod_members', 'messages', 'users']; - - for (const table of tables) { - try { - const result = await pool.query( - ` - SELECT column_name, data_type, character_maximum_length - FROM information_schema.columns - WHERE table_name = $1 - ORDER BY ordinal_position - `, - [table], - ); - - console.log(`\nTable: ${table}`); - console.table(result.rows); - } catch (err: unknown) { - console.error(`Error checking table ${table}:`, ((err) as Error).message); - } - } - - // 3. Create test pod with MongoDB-style ID - const testPodId = '111111111111111111111111'; // MongoDB-style ObjectId (24 chars) - console.log('\nTrying to create a test pod with ID:', testPodId); - - try { - await pool.query( - ` - INSERT INTO pods (id, name, description, type, created_by) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (id) DO NOTHING - `, - [testPodId, 'Test Pod', 'Test description', 'chat', 'test_user'], - ); - - console.log('Test pod creation successful!'); - } catch (err: unknown) { - console.error('Error creating test pod:', ((err) as Error).message); - } - - // 4. Query the test pod - try { - const result = await pool.query('SELECT * FROM pods WHERE id = $1', [ - testPodId, - ]); - console.log('\nTest pod query result:', result.rows[0] || 'No pod found'); - } catch (err: unknown) { - console.error('Error querying test pod:', ((err) as Error).message); - } - - // 5. Test creating a message - try { - console.log('\nTrying to create a test message for pod:', testPodId); - await pool.query( - ` - INSERT INTO messages (pod_id, user_id, content) - VALUES ($1, $2, $3) - `, - [testPodId, 'test_user', 'Test message content'], - ); - - console.log('Test message creation successful!'); - } catch (err: unknown) { - console.error('Error creating test message:', ((err) as Error).message); - } - } catch (err: unknown) { - console.error('General error:', err); - } finally { - // Close the connection pool - await pool.end(); - } -} - -if (require.main === module) { - // Run the test when executed directly - testPG(); -} - -module.exports = testPG; -export {}; diff --git a/backend/testPgConnection.ts b/backend/testPgConnection.ts deleted file mode 100644 index 69ad0c6960..0000000000 --- a/backend/testPgConnection.ts +++ /dev/null @@ -1,61 +0,0 @@ -const { pool, connectPG } = require('./config/db-pg'); -require('dotenv').config(); - -async function testConnection() { - console.log('Testing PostgreSQL connection...'); - console.log('Environment variables:'); - console.log({ - PG_HOST: process.env.PG_HOST, - PG_PORT: process.env.PG_PORT, - PG_DATABASE: process.env.PG_DATABASE, - PG_USER: process.env.PG_USER, - PG_PASSWORD: process.env.PG_PASSWORD ? '[REDACTED]' : 'not set', - PG_SSL_CA_PATH: process.env.PG_SSL_CA_PATH, - }); - - try { - const conn = await connectPG(); - if (conn) { - console.log('PostgreSQL connection successful!'); - - // Test queries - try { - const client = await pool.connect(); - console.log('Testing basic query...'); - const result = await client.query('SELECT NOW()'); - console.log('Query result:', result.rows[0]); - - // Check if required tables exist - console.log('Checking if pods table exists...'); - const tablesResult = await client.query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = 'public' - AND table_name = 'pods' - ); - `); - - console.log('Pods table exists:', tablesResult.rows[0].exists); - - client.release(); - } catch (queryErr: unknown) { - console.error('Error running test queries:', queryErr); - } - } else { - console.error('Connection failed.'); - } - } catch (err: unknown) { - console.error('Error in test script:', err); - } finally { - // Close the pool - await pool.end(); - console.log('Connection pool closed'); - } -} - -if (require.main === module) { - testConnection(); -} - -module.exports = testConnection; -export {}; diff --git a/backend/tsconfig.build.json b/backend/tsconfig.build.json index 2f95acc541..48562a888e 100644 --- a/backend/tsconfig.build.json +++ b/backend/tsconfig.build.json @@ -10,12 +10,6 @@ "**/*.test.ts", "**/*.test.js", "**/*.spec.ts", - "jest.config.js", - "cleanup-test-data.ts", - "test-discord-commands.ts", - "test-discord-integration.ts", - "test-discord-interactions.ts", - "testPgConnection.ts", - "testPG.ts" + "jest.config.js" ] } diff --git a/backend/tsconfig.typescheck.json b/backend/tsconfig.typescheck.json index bbb214ea4d..70c234d2eb 100644 --- a/backend/tsconfig.typescheck.json +++ b/backend/tsconfig.typescheck.json @@ -20,13 +20,6 @@ "**/*.spec.ts", // Vendored/authored elsewhere β€” not this package's source. "commonly-bundled-skills", - "docs", - // Root-level scratch scripts, unreferenced by package.json or any route - // or service. 54 pre-existing errors between them. Fix or delete rather - // than extending this list β€” it is the debt ledger, not an escape hatch. - "test-discord-integration.ts", - "test-discord-interactions.ts", - "test-discord-commands.ts", - "cleanup-test-data.ts" + "docs" ] } diff --git a/docs/discord/DISCORD_INTERACTION_STANDARDS.md b/docs/discord/DISCORD_INTERACTION_STANDARDS.md index 1645ed7b28..e83c969e37 100644 --- a/docs/discord/DISCORD_INTERACTION_STANDARDS.md +++ b/docs/discord/DISCORD_INTERACTION_STANDARDS.md @@ -182,11 +182,10 @@ We provide comprehensive testing to verify standards compliance: ### Automated Tests ```bash -# Test interaction structure -node backend/test-discord-interactions.js - -# Test command functionality -node backend/test-discord-commands.js +# Run the maintained Discord service/command suites in CI: +# backend/__tests__/unit/services/discordService.test.js +# backend/__tests__/unit/services/discordCommandService.test.js +# backend/__tests__/unit/services/discordMultiCommandService.test.js ``` ### Manual Testing diff --git a/frontend/design-system/tokens.css b/frontend/design-system/tokens.css index 1d546e62d6..2b7a988197 100644 --- a/frontend/design-system/tokens.css +++ b/frontend/design-system/tokens.css @@ -1,7 +1,6 @@ /* ============================================================ Commonly Design System β€” colors_and_type.css Source of truth: frontend/src/v2/v2.css (production V2 system) - + frontend/src/theme/commonlyTheme.ts (legacy MUI tokens) ============================================================ */ :root { diff --git a/frontend/src/components/admin/AgentEventsDebugPage.tsx b/frontend/src/components/admin/AgentEventsDebugPage.tsx deleted file mode 100644 index a6edbacaf0..0000000000 --- a/frontend/src/components/admin/AgentEventsDebugPage.tsx +++ /dev/null @@ -1,411 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { - Alert, - Box, - Button, - Card, - CardContent, - CircularProgress, - Grid, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Typography, -} from '@mui/material'; -import { Refresh as RefreshIcon } from '@mui/icons-material'; -import axios from 'axios'; - -interface PendingAgentRow { - agentName: string; - instanceId: string; - count: number; - oldestCreatedAt?: string; - newestCreatedAt?: string; -} - -interface FailedAgentRow { - agentName: string; - instanceId: string; - count: number; - newestCreatedAt?: string; - newestError?: string; -} - -interface HeartbeatInstallation { - agentName: string; - instanceId: string; - podId: string; - everyMinutes?: number; - lastHeartbeatAt?: string; - lastHeartbeatStatus?: string; -} - -interface EventRow { - id: string; - agentName?: string; - instanceId?: string; - type?: string; - podId?: string; - attempts?: number; - createdAt?: string; - error?: string; - delivery?: { - outcome?: string; - reason?: string; - messageId?: string; - }; -} - -interface AgentEventsData { - queue?: { - pending?: number; - delivered?: number; - failed?: number; - stalePendingCount?: number; - stalePendingMinutes?: number; - deliveredByOutcome?: Record; - }; - heartbeatInstallations?: HeartbeatInstallation[]; - failedByAgent?: FailedAgentRow[]; - failedEvents?: EventRow[]; - recentDeliveredHeartbeats?: EventRow[]; - pendingByAgent?: PendingAgentRow[]; - pendingEvents?: EventRow[]; -} - -interface AgentEventsDebugPageProps { - embedded?: boolean; -} - -const formatDate = (value: string | undefined): string => { - if (!value) return '\u2014'; - const d = new Date(value); - if (Number.isNaN(d.getTime())) return String(value); - return d.toLocaleString(); -}; - -const ageMinutes = (value: string | undefined): number | null => { - if (!value) return null; - const ts = new Date(value).getTime(); - if (!Number.isFinite(ts)) return null; - return Math.max(0, Math.floor((Date.now() - ts) / 60000)); -}; - -const AgentEventsDebugPage: React.FC = ({ embedded = false }) => { - const [loading, setLoading] = useState(true); - const [refreshing, setRefreshing] = useState(false); - const [error, setError] = useState(''); - const [data, setData] = useState(null); - - const fetchData = useCallback(async ({ silent = false } = {}): Promise => { - try { - if (!silent) setLoading(true); - if (silent) setRefreshing(true); - setError(''); - const token = localStorage.getItem('token'); - const response = await axios.get('/api/admin/agents/events?limitPending=100&limitRecent=100', { - headers: { Authorization: `Bearer ${token}` }, - }); - setData(response.data || null); - } catch (err) { - const e = err as { response?: { data?: { error?: string } } }; - setError(e.response?.data?.error || 'Failed to load agent events'); - } finally { - setLoading(false); - setRefreshing(false); - } - }, []); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - const queue = data?.queue || {}; - const stalePendingCount = Number(queue.stalePendingCount || 0); - const deliveredByOutcome = queue?.deliveredByOutcome || {}; - const heartbeatInstallations = useMemo(() => data?.heartbeatInstallations || [], [data]); - const failedByAgent = useMemo(() => data?.failedByAgent || [], [data]); - const failedEvents = useMemo(() => data?.failedEvents || [], [data]); - const recentDeliveredHeartbeats = useMemo(() => data?.recentDeliveredHeartbeats || [], [data]); - - if (loading) { - return ( - - - - ); - } - - return ( - - - {!embedded && Agent Events Debug} - - - - {error && ( - setError('')}> - {error} - - )} - - - - Pending{queue.pending || 0} - - - Delivered{queue.delivered || 0} - - - Failed{queue.failed || 0} - - - Stale Pending ({queue.stalePendingMinutes || 30}m){stalePendingCount} - - - - - - Delivered Outcomes - - posted: {deliveredByOutcome.posted || 0} - {' | '} - no_action: {deliveredByOutcome.no_action || 0} - {' | '} - skipped: {deliveredByOutcome.skipped || 0} - {' | '} - acknowledged-only: {deliveredByOutcome.acknowledged || 0} - {' | '} - error: {deliveredByOutcome.error || 0} - - - - - - - Pending By Agent - - - - - Agent - Instance - Count - Oldest - Newest - - - - {(data?.pendingByAgent || []).map((row) => ( - - {row.agentName} - {row.instanceId} - {row.count} - {formatDate(row.oldestCreatedAt)} - {formatDate(row.newestCreatedAt)} - - ))} - {(data?.pendingByAgent || []).length === 0 && ( - No pending events - )} - -
-
-
-
- - - - Failed By Agent - - - - - Agent - Instance - Count - Newest - Last Error - - - - {failedByAgent.map((row) => ( - - {row.agentName} - {row.instanceId} - {row.count} - {formatDate(row.newestCreatedAt)} - - {row.newestError || '\u2014'} - - - ))} - {failedByAgent.length === 0 && ( - No failed events - )} - -
-
-
-
- - - - Heartbeat Status - - - - - Agent - Instance - Pod - Interval (min) - Last Heartbeat - Age (min) - Status - - - - {heartbeatInstallations.map((row) => ( - - {row.agentName} - {row.instanceId} - {row.podId} - {row.everyMinutes} - {formatDate(row.lastHeartbeatAt)} - {ageMinutes(row.lastHeartbeatAt) ?? '\u2014'} - {row.lastHeartbeatStatus || 'none'} - - ))} - {heartbeatInstallations.length === 0 && ( - No active installations - )} - -
-
-
-
- - - - Pending Events (Oldest First) - - - - - Created - Agent - Instance - Type - Pod - Attempts - - - - {(data?.pendingEvents || []).map((event) => ( - - {formatDate(event.createdAt)} - {event.agentName} - {event.instanceId} - {event.type} - {event.podId} - {event.attempts} - - ))} - {(data?.pendingEvents || []).length === 0 && ( - No pending events - )} - -
-
-
-
- - - - Recent Delivered Heartbeats - - - - - Created - Agent - Instance - Pod - Outcome - Reason - Message - - - - {recentDeliveredHeartbeats.map((event) => ( - - {formatDate(event.createdAt)} - {event.agentName} - {event.instanceId} - {event.podId} - {event.delivery?.outcome || 'acknowledged'} - - {event.delivery?.reason || '\u2014'} - - {event.delivery?.messageId || '\u2014'} - - ))} - {recentDeliveredHeartbeats.length === 0 && ( - No delivered heartbeat events - )} - -
-
- - Recent Failed Events - - - - - Created - Agent - Instance - Type - Pod - Attempts - Error - - - - {failedEvents.map((event) => ( - - {formatDate(event.createdAt)} - {event.agentName} - {event.instanceId} - {event.type} - {event.podId} - {event.attempts} - - {event.error || '\u2014'} - - - ))} - {failedEvents.length === 0 && ( - No failed events - )} - -
-
-
-
-
- ); -}; - -export default AgentEventsDebugPage; diff --git a/frontend/src/components/agents/AgentCard.test.tsx b/frontend/src/components/agents/AgentCard.test.tsx deleted file mode 100644 index 95b67d64a3..0000000000 --- a/frontend/src/components/agents/AgentCard.test.tsx +++ /dev/null @@ -1,170 +0,0 @@ -// @ts-nocheck -/** - * AgentCard Tests - * - * Verifies rendering of agent cards in different states: default, installed, - * with "Talk to" button, and the skeleton loading variant. - */ - -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import AgentCard from './AgentCard'; - -// Minimal agent fixture -const baseAgent = { - _id: 'agent-1', - name: 'task-clerk', - displayName: 'Task Clerk', - agentName: 'task-clerk', - description: 'Captures action items from chat as tasks.', - type: 'productivity', - categories: ['productivity'], - capabilities: ['task creation', 'context reading'], - stats: { installs: 42, podsJoined: 3, messagesProcessed: 1200 }, -}; - -describe('AgentCard', () => { - it('renders agent name and description', () => { - render(); - expect(screen.getByText('Task Clerk')).toBeInTheDocument(); - expect(screen.getByText(/Captures action items/)).toBeInTheDocument(); - }); - - it('renders @agentName handle', () => { - render(); - expect(screen.getByText(/@task-clerk/)).toBeInTheDocument(); - }); - - it('shows Install button when not installed', () => { - const onInstall = jest.fn(); - render(); - const btn = screen.getByRole('button', { name: /install/i }); - expect(btn).toBeInTheDocument(); - fireEvent.click(btn); - expect(onInstall).toHaveBeenCalledWith(baseAgent); - }); - - it('shows "Talk to" button when installed and onTalkTo is provided', () => { - const onTalkTo = jest.fn(); - render( - , - ); - const btn = screen.getByRole('button', { name: /talk to/i }); - expect(btn).toBeInTheDocument(); - fireEvent.click(btn); - expect(onTalkTo).toHaveBeenCalledWith(baseAgent); - }); - - it('Talk to button renders as contained variant (visually prominent)', () => { - const onTalkTo = jest.fn(); - const { container } = render( - , - ); - const btn = screen.getByRole('button', { name: /talk to/i }); - // MUI contained variant adds MuiButton-contained class - expect(btn.className).toContain('contained'); - }); - - it('shows "Message" button when installed but onTalkTo is NOT provided', () => { - const onMessage = jest.fn(); - render( - , - ); - const btn = screen.getByRole('button', { name: /message/i }); - expect(btn).toBeInTheDocument(); - fireEvent.click(btn); - expect(onMessage).toHaveBeenCalledWith(baseAgent); - }); - - it('renders Configure button for installed agents', () => { - const onConfigure = jest.fn(); - render( - , - ); - expect(screen.getByRole('button', { name: /configure/i })).toBeInTheDocument(); - }); - - it('renders Remove button when canRemove is true', () => { - const onRemove = jest.fn(); - render( - , - ); - const btn = screen.getByRole('button', { name: /remove/i }); - fireEvent.click(btn); - expect(onRemove).toHaveBeenCalledWith(baseAgent); - }); - - it('renders capability chips', () => { - render(); - expect(screen.getByText('task creation')).toBeInTheDocument(); - expect(screen.getByText('context reading')).toBeInTheDocument(); - }); - - it('renders loading skeleton when loading=true', () => { - const { container } = render(); - // Skeleton renders MUI Skeleton elements, not the agent content - expect(screen.queryByText('Task Clerk')).not.toBeInTheDocument(); - expect(container.querySelector('.MuiSkeleton-root')).toBeInTheDocument(); - }); - - describe('featured variant', () => { - it('renders "Talk to" in featured variant when onTalkTo provided', () => { - const onTalkTo = jest.fn(); - render( - , - ); - const btn = screen.getByRole('button', { name: /talk to/i }); - expect(btn).toBeInTheDocument(); - fireEvent.click(btn); - expect(onTalkTo).toHaveBeenCalledWith(baseAgent); - }); - - it('renders "Message" in featured variant when onTalkTo NOT provided', () => { - const onMessage = jest.fn(); - render( - , - ); - const btn = screen.getByRole('button', { name: /message/i }); - expect(btn).toBeInTheDocument(); - }); - }); - - describe('compact variant', () => { - it('renders compact card with agent name', () => { - render(); - expect(screen.getByText('Task Clerk')).toBeInTheDocument(); - }); - }); -}); diff --git a/frontend/src/components/agents/AgentCard.tsx b/frontend/src/components/agents/AgentCard.tsx deleted file mode 100644 index 3c70935abb..0000000000 --- a/frontend/src/components/agents/AgentCard.tsx +++ /dev/null @@ -1,691 +0,0 @@ -/** - * AgentCard Component - * - * Social-style card for displaying an AI agent's profile. - * Used in agent marketplace, installed agents list, and discovery. - */ - -import React from 'react'; -import { - Card, - CardContent, - CardActions, - Box, - Typography, - Avatar, - Chip, - Button, - IconButton, - Tooltip, - Skeleton, - alpha, - useTheme, -} from '@mui/material'; -import { - VerifiedUser as VerifiedIcon, - MoreVert as MoreIcon, - TrendingUp as TrendingIcon, - Memory as MemoryIcon, - Chat as ChatIcon, - Extension as ExtensionIcon, - FiberManualRecord as PulseIcon, -} from '@mui/icons-material'; -import { normalizeUploadUrl } from '../../utils/apiBaseUrl'; - -// Agent type colors -const agentTypeColors: Record = { - personal: '#8b5cf6', - utility: '#06b6d4', - analytics: '#ec4899', - security: '#ef4444', - productivity: '#22c55e', - default: '#0d9488', -}; - -// Agent type icons -const agentTypeIcons: Record = { - personal: 'πŸ€–', - utility: 'πŸ”§', - analytics: 'πŸ“Š', - security: 'πŸ”’', - productivity: '⚑', - default: 'πŸ€–', -}; - -interface AgentStats { - installs?: number; - podsJoined?: number; - messagesProcessed?: number; -} - -interface AgentManifest { - capabilities?: Array<{ name: string }>; -} - -interface AgentInstallation { - instanceId?: string; -} - -interface AgentProfile { - instanceId?: string; - iconUrl?: string; - avatarUrl?: string; -} - -export interface Agent { - id?: string; - _id?: string; - name?: string; - displayName?: string; - agentName?: string; - instanceId?: string; - installation?: AgentInstallation; - profile?: AgentProfile; - description?: string; - type?: string; - categories?: string[]; - verified?: boolean; - installs?: number; - capabilities?: string[]; - manifest?: AgentManifest; - stats?: AgentStats; - lastHeartbeatAt?: string | null; - iconUrl?: string; - presetId?: string | null; - customizations?: { soul?: boolean; heartbeat?: boolean } | null; -} - -interface AgentCardProps { - agent: Agent; - variant?: 'default' | 'compact' | 'featured'; - installed?: boolean; - onInstall?: (agent: Agent) => void; - onConfigure?: (agent: Agent) => void; - onRemove?: (agent: Agent) => void; - onMessage?: (agent: Agent) => void; - onTalkTo?: (agent: Agent) => void; - onEdit?: (agent: Agent) => void; - onViewProfile?: (agent: Agent) => void; - canRemove?: boolean; - canConfigure?: boolean; - installedActionLabel?: string; - canEdit?: boolean; - loading?: boolean; -} - -interface AgentCardSkeletonProps { - variant: string; -} - -const AgentCard: React.FC = ({ - agent, - variant = 'default', - installed = false, - onInstall, - onConfigure, - onRemove, - onMessage, - onTalkTo, - onEdit, - onViewProfile, - canRemove = false, - canConfigure = true, - installedActionLabel = 'Configure', - canEdit = false, - loading = false, -}) => { - const theme = useTheme(); - - if (loading) { - return ; - } - - // Handle both mock data and real API data formats - const displayName = agent.displayName || agent.name || 'Unknown Agent'; - const agentName = agent.agentName || agent.name || ''; - const instanceId = agent.instanceId || agent.installation?.instanceId || agent.profile?.instanceId || ''; - const description = agent.description || ''; - const type = agent.type || (agent.categories && agent.categories[0]) || 'default'; - const verified = agent.verified || false; - const installs = agent.installs || agent.stats?.installs || 0; - const capabilities = agent.capabilities || agent.manifest?.capabilities?.map((c) => c.name) || []; - const stats = agent.stats || {}; - const lastHeartbeatAt = agent.lastHeartbeatAt || null; - const iconUrl = agent.iconUrl || agent.profile?.iconUrl || agent.profile?.avatarUrl || null; - - const formatHeartbeat = (ts: string | null): string | null => { - if (!ts) return null; - const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1000); - if (diff < 60) return `${diff}s ago`; - if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; - if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; - return `${Math.floor(diff / 86400)}d ago`; - }; - const heartbeatLabel = formatHeartbeat(lastHeartbeatAt); - const heartbeatStale = lastHeartbeatAt - ? (Date.now() - new Date(lastHeartbeatAt).getTime()) > 2 * 60 * 60 * 1000 - : true; - const iconSrc = iconUrl ? normalizeUploadUrl(iconUrl) : undefined; - - const typeColor = agentTypeColors[type] || agentTypeColors.default; - const typeIcon = agentTypeIcons[type] || agentTypeIcons.default; - const cardBaseSx = { - backgroundColor: 'rgba(15, 23, 42, 0.92)', - border: '1px solid rgba(148, 163, 184, 0.18)', - boxShadow: '0 10px 24px rgba(8, 12, 24, 0.4)', - color: '#e2e8f0', - }; - - // Compact variant for sidebars and lists - if (variant === 'compact') { - return ( - onViewProfile?.(agent)} - > - - {typeIcon} - - - - - {displayName} - - {verified && ( - - )} - - - @{agentName}{instanceId ? ` β€’ id:${instanceId}` : ''} - - - {installed && ( - - )} - - ); - } - - // Featured variant for marketplace highlights - if (variant === 'featured') { - return ( - - {/* Trending badge */} - - - Trending - - - - - - {typeIcon} - - - - - {displayName} - - {verified && ( - - - - )} - - - @{agentName}{instanceId ? ` β€’ id:${instanceId}` : ''} - - - - - - {description} - - - {/* Capabilities */} - - {capabilities.slice(0, 4).map((cap) => ( - - ))} - - - {/* Installs row */} - - - {installs.toLocaleString()} installs - - - - - - {installed ? ( - <> - - - {canRemove && ( - - )} - - ) : ( - - )} - - - ); - } - - // Default variant - return ( - - - - - {typeIcon} - - - - - {displayName} - - {verified && ( - - )} - - - @{agentName}{instanceId ? ` β€’ id:${instanceId}` : ''} - - - - - - - - - {description} - - - {/* Mini stats β€” hide when both are 0 to avoid every uninstalled - marketplace card reading "0 pods Β· 0 msgs" like a badge of shame. */} - - {(stats.podsJoined || 0) > 0 && ( - - - - {stats.podsJoined} pods - - - )} - {(stats.messagesProcessed || 0) > 0 && ( - - - - {stats.messagesProcessed.toLocaleString()} msgs - - - )} - {installed && ( - - - - - {heartbeatLabel || 'never'} - - - - )} - - - {/* Preset + customization chips (installed agents only) */} - {installed && (agent.presetId || agent.customizations?.soul || agent.customizations?.heartbeat) && ( - - {agent.presetId && ( - - - - )} - {(agent.customizations?.soul || agent.customizations?.heartbeat) && ( - - - - )} - - )} - - {/* Capabilities chips */} - - {capabilities.slice(0, 3).map((cap) => ( - - ))} - {capabilities.length > 3 && ( - - )} - - - - - {installed ? ( - <> - - - {canRemove && ( - - )} - - ) : ( - <> - {canEdit && ( - - )} - - - )} - - - ); -}; - -// Loading skeleton -const AgentCardSkeleton: React.FC = ({ variant }) => { - if (variant === 'compact') { - return ( - - - - - - - - ); - } - - return ( - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default AgentCard; diff --git a/frontend/src/components/agents/AgentsHub.runtime.test.tsx b/frontend/src/components/agents/AgentsHub.runtime.test.tsx deleted file mode 100644 index 7b571eaea8..0000000000 --- a/frontend/src/components/agents/AgentsHub.runtime.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -// @ts-nocheck -import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; -import axios from 'axios'; -import AgentsHub from './AgentsHub'; - -jest.mock('axios'); -jest.mock('../../context/AuthContext', () => ({ - useAuth: () => ({ currentUser: { _id: 'owner', role: 'user' } }), -})); -jest.mock('./ClawdbotConfigPanel', () => () => null); -jest.mock('../admin/AgentEventsDebugPage', () => () => null); -jest.mock('./AgentCard', () => ({ agent, installed, onConfigure }) => ( - installed ? : null -)); - -it('keeps a template installation’s stored instance when opening runtime controls', async () => { - const installation = { - name: 'template-agent', displayName: 'Renamed Template', templateId: 'template-1', - instanceId: 'original-stored-instance', installedBy: 'owner', status: 'active', - }; - axios.get.mockImplementation(async (url) => { - if (url === '/api/pods') return { data: [{ _id: 'pod-1', name: 'Pod', createdBy: 'owner', members: ['owner'] }] }; - if (url.startsWith('/api/registry/agents?')) return { data: { agents: [{ name: 'openclaw' }, { name: 'commonly-summarizer' }] } }; - if (url === '/api/registry/pods/pod-1/agents') return { data: { agents: [installation] } }; - if (url.includes('/agents/template-agent?')) return { data: { agent: installation } }; - return { data: { templates: [], gateways: [], tokens: [], status: 'running' } }; - }); - render(); - fireEvent.click(await screen.findByRole('button', { name: 'Configure Renamed Template' })); - await waitFor(() => expect(axios.get).toHaveBeenCalledWith( - '/api/registry/pods/pod-1/agents/template-agent?instanceId=original-stored-instance', expect.anything(), - )); - fireEvent.click(await screen.findByRole('button', { name: 'Refresh Status', hidden: true })); - await waitFor(() => expect(axios.get).toHaveBeenCalledWith( - '/api/registry/pods/pod-1/agents/template-agent/runtime-status', - expect.objectContaining({ params: { instanceId: 'original-stored-instance' } }), - )); -}); diff --git a/frontend/src/components/agents/AgentsHub.tsx b/frontend/src/components/agents/AgentsHub.tsx deleted file mode 100644 index c9c1c527a7..0000000000 --- a/frontend/src/components/agents/AgentsHub.tsx +++ /dev/null @@ -1,4715 +0,0 @@ -/** - * AgentsHub Page - * - * The "app store" for AI agents - discover, install, and manage agents. - */ - -import React, { useState, useEffect, useMemo, useRef } from 'react'; -import { - Box, - Container, - Typography, - TextField, - InputAdornment, - Grid, - Tabs, - Tab, - Chip, - Button, - IconButton, - Alert, - Divider, - Paper, - alpha, - useTheme, - Avatar, - FormControl, - Stack, - Radio, - RadioGroup, - Select, - MenuItem, - InputLabel, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Tooltip, - FormGroup, - FormControlLabel, - Checkbox, - ListItemText, - Card, - CardContent, - CardActions, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - CircularProgress, - Link, -} from '@mui/material'; -import { - Search as SearchIcon, - ContentCopy as CopyIcon, - Refresh as RefreshIcon, - SmartToyOutlined as SmartToyOutlinedIcon, -} from '@mui/icons-material'; -import AgentCard from './AgentCard'; -import ClawdbotConfigPanel from './ClawdbotConfigPanel'; -import AgentEventsDebugPage from '../admin/AgentEventsDebugPage'; -import axios from 'axios'; -import { useAuth } from '../../context/AuthContext'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { resolveHeartbeatEditorContent } from '../../utils/heartbeatUtils'; -import { useV2Embedded } from '../../v2/hooks/useV2Embedded'; - -const categories = [ - { id: 'all', label: 'All Agents' }, - { id: 'productivity', label: 'Productivity' }, - { id: 'development', label: 'Development' }, - { id: 'analytics', label: 'Analytics' }, - { id: 'support', label: 'Support' }, - { id: 'communication', label: 'Communication' }, - { id: 'commonly-summarizer', label: 'Commonly Summarizer' }, - { id: 'openclaw', label: 'OpenClaw' }, - { id: 'codex', label: 'Codex' }, - { id: 'claude', label: 'Claude' }, - { id: 'gemini', label: 'Gemini' }, - { id: 'custom', label: 'Custom' }, -]; - -const agentUserTokenScopes = [ - { id: 'agent:events:read', label: 'Read agent events' }, - { id: 'agent:events:ack', label: 'Acknowledge agent events' }, - { id: 'agent:context:read', label: 'Read pod context' }, - { id: 'agent:messages:read', label: 'Read pod messages' }, - { id: 'agent:messages:write', label: 'Post pod messages' }, -]; - -const installationScopeOptions = [ - ...agentUserTokenScopes, - { id: 'integration:read', label: 'Read integration list/tokens' }, - { id: 'integration:messages:read', label: 'Read integration messages' }, - { id: 'integration:write', label: 'Publish to integrations' }, -]; - -const installationScopeAliases = { - 'integrations:read': 'integration:read', - 'integrations:messages:read': 'integration:messages:read', - 'integrations:write': 'integration:write', -}; - -const normalizeInstallationScope = (scope) => { - const raw = String(scope || '').trim(); - if (!raw) return ''; - return installationScopeAliases[raw] || raw; -}; - -const getPresetSkillStatus = (skill) => { - switch (skill?.setupStatus) { - case 'ready': - return { label: 'Ready', color: 'success' }; - case 'needs-package-install': - return { label: 'Needs package install', color: 'warning' }; - case 'needs-api-env': - return { label: 'Needs API env', color: 'warning' }; - case 'missing-skill': - return { label: 'Missing skill', color: 'default' }; - default: - return { label: 'Unknown', color: 'default' }; - } -}; - -const TAB_DISCOVER = 0; -// TAB_PRESETS removed 2026-08-20: the presets were moltbot templates for the -// PARKED tier (#1050), and rendering the tab exec'd into a gateway that runs -// at zero replicas β€” four cards stuck on "Loading preset..." for 30 seconds, -// then offers of agents nothing would ever run. Deprecated per Sam. The -// backend endpoint stays for admin tooling; this surface is gone. When the -// pi/OpenCode adapters bring templated agents back, they come back through -// the catalog with a live runtime, not through a bespoke tab. -const TAB_INSTALLED = 1; -const ADMIN_VIEW_INSTALLATIONS = 'installations'; -const ADMIN_VIEW_EVENTS = 'events'; - -const AgentsHub = ({ currentPodId: propPodId = null }) => { - const v2Embedded = useV2Embedded(); - const theme = useTheme(); - const location = useLocation(); - const navigate = useNavigate(); - const { currentUser } = useAuth(); - const isGlobalAdmin = currentUser?.role === 'admin'; - const [searchQuery, setSearchQuery] = useState(''); - const [activeTab, setActiveTab] = useState(0); - const [adminView, setAdminView] = useState(ADMIN_VIEW_INSTALLATIONS); - const [category, setCategory] = useState('all'); - const [agents, setAgents] = useState([]); - const [templates, setTemplates] = useState([]); - const [presets, setPresets] = useState([]); - const [presetCategory, setPresetCategory] = useState('all'); - const [presetCapabilities, setPresetCapabilities] = useState(null); - const [presetRuntimeSkills, setPresetRuntimeSkills] = useState(null); - const [presetDockerCapabilities, setPresetDockerCapabilities] = useState(null); - const [presetsLoading, setPresetsLoading] = useState(false); - const [presetsError, setPresetsError] = useState(''); - const [presetInstallLoadingId, setPresetInstallLoadingId] = useState(''); - const [installedAgents, setInstalledAgents] = useState([]); - const [userPods, setUserPods] = useState([]); - const queryPodId = useMemo(() => { - const params = new URLSearchParams(location.search); - return params.get('podId'); - }, [location.search]); - const queryAgentName = useMemo(() => { - const params = new URLSearchParams(location.search); - return params.get('agent') || ''; - }, [location.search]); - const queryInstanceId = useMemo(() => { - const params = new URLSearchParams(location.search); - return params.get('instanceId') || 'default'; - }, [location.search]); - const queryView = useMemo(() => { - const params = new URLSearchParams(location.search); - return params.get('view') || 'overview'; - }, [location.search]); - const queryTab = useMemo(() => { - const params = new URLSearchParams(location.search); - return params.get('tab') || ''; - }, [location.search]); - const queryInstallable = useMemo(() => { - const params = new URLSearchParams(location.search); - return params.get('installable') || ''; - }, [location.search]); - const [selectedPodId, setSelectedPodId] = useState(propPodId || queryPodId); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [seeding, setSeeding] = useState(false); - const [configOpen, setConfigOpen] = useState(false); - const [configAgent, setConfigAgent] = useState(null); - const [agentOverviewOpen, setAgentOverviewOpen] = useState(false); - const [agentOverview, setAgentOverview] = useState(null); - const [agentOverviewLoading, setAgentOverviewLoading] = useState(false); - const [agentOverviewError, setAgentOverviewError] = useState(''); - const [configModel, setConfigModel] = useState(''); - const [configInstructions, setConfigInstructions] = useState(''); - const [configPersonaTone, setConfigPersonaTone] = useState('friendly'); - const [configPersonaSpecialties, setConfigPersonaSpecialties] = useState(''); - const [configPersonaBoundaries, setConfigPersonaBoundaries] = useState(''); - const [configPersonaCustomInstructions, setConfigPersonaCustomInstructions] = useState(''); - const [configAuthMode, setConfigAuthMode] = useState('default'); - const [configAuthKeys, setConfigAuthKeys] = useState({ - google: '', - anthropic: '', - openai: '', - }); - const [configSkillEnvJson, setConfigSkillEnvJson] = useState(''); - const [configSkillEnvError, setConfigSkillEnvError] = useState(''); - const [toolPolicyAllowed, setToolPolicyAllowed] = useState('commonly'); - const [toolPolicyBlocked, setToolPolicyBlocked] = useState(''); - const [toolPolicyRequireApproval, setToolPolicyRequireApproval] = useState(''); - const [configSelectedScopes, setConfigSelectedScopes] = useState([]); - const [configExtraScopes, setConfigExtraScopes] = useState([]); - const [configAutoJoinAgentOwnedPods, setConfigAutoJoinAgentOwnedPods] = useState(false); - const [configOwnerDmErrorRouting, setConfigOwnerDmErrorRouting] = useState(false); - const [configHeartbeatEnabled, setConfigHeartbeatEnabled] = useState(true); - const [configHeartbeatInterval, setConfigHeartbeatInterval] = useState(60); - const [configHeartbeatChecklist, setConfigHeartbeatChecklist] = useState(''); - const [heartbeatResetLoading, setHeartbeatResetLoading] = useState(false); - const [configIdentityContent, setConfigIdentityContent] = useState(''); - const [identityFileSaving, setIdentityFileSaving] = useState(false); - const [configSaving, setConfigSaving] = useState(false); - const [skillSyncMode, setSkillSyncMode] = useState('all'); - const [skillSyncAllPods, setSkillSyncAllPods] = useState(true); - const [skillSyncPods, setSkillSyncPods] = useState([]); - const [skillSyncSkills, setSkillSyncSkills] = useState([]); - const [skillSyncPodsLoading, setSkillSyncPodsLoading] = useState(false); - const [skillSyncSkillLoading, setSkillSyncSkillLoading] = useState(false); - const [skillSyncPodOptions, setSkillSyncPodOptions] = useState([]); - const [skillSyncSkillOptions, setSkillSyncSkillOptions] = useState([]); - const [autoPersonaLoading, setAutoPersonaLoading] = useState(false); - const [autoPersonaTarget, setAutoPersonaTarget] = useState(''); - const [runtimeTokens, setRuntimeTokens] = useState([]); - const [runtimeTokenLabel, setRuntimeTokenLabel] = useState('Local dev'); - const [runtimeTokenValue, setRuntimeTokenValue] = useState(''); - const [runtimeTokenLoading, setRuntimeTokenLoading] = useState(false); - const [runtimeTokenRevokingId, setRuntimeTokenRevokingId] = useState(null); - const [provisionLoading, setProvisionLoading] = useState(false); - const [provisionResult, setProvisionResult] = useState(null); - const [provisionError, setProvisionError] = useState(''); - const [provisionIncludeUserToken, setProvisionIncludeUserToken] = useState(true); - const [provisionForceReprovision, setProvisionForceReprovision] = useState(false); - const [runtimeGatewayMode, setRuntimeGatewayMode] = useState('shared'); - const [runtimeGatewayId, setRuntimeGatewayId] = useState(''); - const [runtimeGatewayList, setRuntimeGatewayList] = useState([]); - const [runtimeGatewayLoading, setRuntimeGatewayLoading] = useState(false); - const [runtimeGatewayError, setRuntimeGatewayError] = useState(''); - const [runtimeStatus, setRuntimeStatus] = useState(null); - const [runtimeStatusLoading, setRuntimeStatusLoading] = useState(false); - const [createAgentAvatarFile, setCreateAgentAvatarFile] = useState(null); - const [createAgentAvatarPreview, setCreateAgentAvatarPreview] = useState(''); - const [editTemplateOpen, setEditTemplateOpen] = useState(false); - const [editTemplate, setEditTemplate] = useState(null); - const [editTemplateAvatarFile, setEditTemplateAvatarFile] = useState(null); - const [editTemplateAvatarPreview, setEditTemplateAvatarPreview] = useState(''); - const [editTemplateSaving, setEditTemplateSaving] = useState(false); - const [runtimeStatusError, setRuntimeStatusError] = useState(''); - const [runtimeSessionResetResult, setRuntimeSessionResetResult] = useState(null); - const [runtimeLogsOpen, setRuntimeLogsOpen] = useState(false); - const [runtimeLogsLoading, setRuntimeLogsLoading] = useState(false); - const [runtimeLogsContent, setRuntimeLogsContent] = useState(''); - const [runtimeLogsError, setRuntimeLogsError] = useState(''); - const [runtimeLogsLines, setRuntimeLogsLines] = useState(200); - const [runtimeLogsAutoRefresh, setRuntimeLogsAutoRefresh] = useState(true); - const [runtimeLogsFilter, setRuntimeLogsFilter] = useState(''); - const runtimeLogsInputRef = useRef(null); - const runtimeLogsScrollRef = useRef({ top: 0, atBottom: true }); - const deepLinkHandledRef = useRef(''); - const installableDeepLinkHandledRef = useRef(''); - const [userTokenValue, setUserTokenValue] = useState(''); - const [userTokenScopes, setUserTokenScopes] = useState([]); - const [userTokenMeta, setUserTokenMeta] = useState({ - hasToken: false, - createdAt: null, - scopeMode: 'none', - }); - const [userTokenLoading, setUserTokenLoading] = useState(false); - const [userTokenRevoking, setUserTokenRevoking] = useState(false); - const [skillsCatalogOpen, setSkillsCatalogOpen] = useState(false); - const [skillsCatalogItems, setSkillsCatalogItems] = useState([]); - const [skillsCatalogLoading, setSkillsCatalogLoading] = useState(false); - const [skillsCatalogError, setSkillsCatalogError] = useState(''); - const [skillsImportNotice, setSkillsImportNotice] = useState(''); - const [skillsImportState, setSkillsImportState] = useState({ - name: '', - content: '', - tags: '', - sourceUrl: '', - license: '', - description: '', - }); - const skillsSelectedRef = useRef(null); - const [licenseOpen, setLicenseOpen] = useState(false); - const [licenseState, setLicenseState] = useState({ title: '', text: '', path: '' }); - const [installDialogOpen, setInstallDialogOpen] = useState(false); - const [installAgent, setInstallAgent] = useState(null); - const [installPresetContext, setInstallPresetContext] = useState(null); - const [installPodIds, setInstallPodIds] = useState([]); - const [installSaving, setInstallSaving] = useState(false); - const [installInstanceName, setInstallInstanceName] = useState(''); - const [installInstanceId, setInstallInstanceId] = useState(''); - const [installGatewayMode, setInstallGatewayMode] = useState('shared'); - const [installGatewayId, setInstallGatewayId] = useState(''); - const [installGatewayToken, setInstallGatewayToken] = useState(''); - const [installLlmMode, setInstallLlmMode] = useState('default'); - const [installLlmKeys, setInstallLlmKeys] = useState({ - google: '', - anthropic: '', - openai: '', - }); - const [installWebhookUrl, setInstallWebhookUrl] = useState(''); - const [installWebhookSecret, setInstallWebhookSecret] = useState(''); - const [installGatewayCreateOpen, setInstallGatewayCreateOpen] = useState(false); - const [installGatewayCreateLoading, setInstallGatewayCreateLoading] = useState(false); - const [installGatewayCreateError, setInstallGatewayCreateError] = useState(''); - const [installGatewayForm, setInstallGatewayForm] = useState({ - name: '', - slug: '', - mode: 'k8s', - namespace: '', - }); - const [existingAgentInfo, setExistingAgentInfo] = useState(null); - const [checkingExisting, setCheckingExisting] = useState(false); - const [createDialogOpen, setCreateDialogOpen] = useState(false); - const [createAgentType, setCreateAgentType] = useState(''); - const [createAgentName, setCreateAgentName] = useState(''); - const [createAgentDescription, setCreateAgentDescription] = useState(''); - const [createAgentVisibility, setCreateAgentVisibility] = useState('private'); - const [createSaving, setCreateSaving] = useState(false); - const [clawdbotGatewayStatus, setClawdbotGatewayStatus] = useState(null); - const [adminInstallations, setAdminInstallations] = useState([]); - const [adminTotal, setAdminTotal] = useState(0); - const [adminLoading, setAdminLoading] = useState(false); - const [adminError, setAdminError] = useState(''); - const [adminSearch, setAdminSearch] = useState(''); - const [adminShowAll, setAdminShowAll] = useState(false); - const [adminTokensOpen, setAdminTokensOpen] = useState(false); - const [adminTokensInstallation, setAdminTokensInstallation] = useState(null); - const [adminRevokeLoadingId, setAdminRevokeLoadingId] = useState(null); - const [adminUninstallTarget, setAdminUninstallTarget] = useState(null); - const [adminUninstallLoading, setAdminUninstallLoading] = useState(false); - const [adminAutonomyHours, setAdminAutonomyHours] = useState(12); - const [adminAutonomyMinMatches, setAdminAutonomyMinMatches] = useState(4); - const [adminAutonomyLoading, setAdminAutonomyLoading] = useState(false); - const [adminAutonomyResult, setAdminAutonomyResult] = useState(null); - const [adminReprovisionLoading, setAdminReprovisionLoading] = useState(false); - const [adminReprovisionResult, setAdminReprovisionResult] = useState(null); - - const getAuthHeaders = () => { - const token = localStorage.getItem('token'); - return { 'x-auth-token': token }; - }; - - const resolveRuntimeGatewayId = () => ( - runtimeGatewayMode === 'custom' && isGlobalAdmin ? runtimeGatewayId : '' - ); - const resolveInstallGatewayId = () => ( - installGatewayMode === 'custom' && isGlobalAdmin ? installGatewayId : '' - ); - - const currentUserId = currentUser?._id || currentUser?.id || null; - const runtimeGatewayOptions = useMemo( - () => runtimeGatewayList.filter((gateway) => gateway?.mode === 'k8s'), - [runtimeGatewayList], - ); - const selectedRuntimeGateway = runtimeGatewayOptions.find((gateway) => gateway?._id === runtimeGatewayId) - || null; - - const normalizePodCreatorId = (pod) => ( - pod?.createdBy?._id || pod?.createdBy || null - ); - - const isPodAdmin = (pod) => { - if (!currentUserId || !pod) return false; - const creatorId = normalizePodCreatorId(pod); - return creatorId?.toString?.() === currentUserId.toString(); - }; - - const adminTabIndex = isGlobalAdmin ? 2 : -1; - - const formatDateTime = (value) => { - if (!value) return 'β€”'; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return 'β€”'; - return date.toLocaleString(); - }; - - const renderAdminStatusChip = (status) => { - const normalized = status || 'unknown'; - let color = 'default'; - if (normalized === 'active') color = 'success'; - if (normalized === 'paused') color = 'warning'; - if (normalized === 'error') color = 'error'; - if (normalized === 'uninstalled') color = 'default'; - return ( - - ); - }; - - const accessiblePods = useMemo(() => { - if (!currentUserId) return userPods; - return (userPods || []).filter((pod) => { - const creatorId = normalizePodCreatorId(pod); - if (creatorId?.toString?.() === currentUserId.toString()) return true; - const memberIds = (pod.members || []).map((m) => m?._id || m).filter(Boolean); - return memberIds.some((id) => id.toString?.() === currentUserId.toString()); - }); - }, [userPods, currentUserId]); - - useEffect(() => { - if (propPodId) { - setSelectedPodId(propPodId); - return; - } - if (queryPodId) { - setSelectedPodId(queryPodId); - } - }, [propPodId, queryPodId]); - - useEffect(() => { - if (queryTab === 'installed') { - setActiveTab(TAB_INSTALLED); - } - }, [queryTab]); - - // Fetch user's pods for pod selector - useEffect(() => { - const fetchUserPods = async () => { - try { - const response = await axios.get('/api/pods', { - headers: getAuthHeaders(), - }); - setUserPods(response.data || []); - } catch (err) { - console.error('Error fetching user pods:', err); - } - }; - fetchUserPods(); - }, [currentUserId]); - - useEffect(() => { - if (!selectedPodId && accessiblePods.length > 0) { - setSelectedPodId(accessiblePods[0]._id); - } - }, [selectedPodId, accessiblePods]); - - // Fetch agents from registry - useEffect(() => { - fetchAgents(); - fetchTemplates(); - // fetchAgentPresets() removed with the Presets tab β€” the endpoint execs - // into the parked moltbot gateway and hangs ~30s against a deployment - // that does not exist. See TAB_PRESETS removal note above. - }, [category, searchQuery]); - - // Fetch installed agents for selected pod - useEffect(() => { - if (selectedPodId) { - fetchInstalledAgents(); - } - }, [selectedPodId]); - - useEffect(() => { - if (!queryAgentName || !selectedPodId || !installedAgents.length) return; - const deepLinkKey = `${selectedPodId}:${queryAgentName}:${queryInstanceId}:${queryView}`; - if (deepLinkHandledRef.current === deepLinkKey) return; - - const normalizedTargetName = String(queryAgentName).trim().toLowerCase(); - const normalizedTargetInstance = String(queryInstanceId || 'default').trim().toLowerCase(); - const matched = installedAgents.find((agent) => ( - String(agent.name || '').trim().toLowerCase() === normalizedTargetName - && String(agent.instanceId || 'default').trim().toLowerCase() === normalizedTargetInstance - )) || installedAgents.find((agent) => String(agent.name || '').trim().toLowerCase() === normalizedTargetName); - - if (!matched) return; - - deepLinkHandledRef.current = deepLinkKey; - setActiveTab(TAB_INSTALLED); - if (queryView === 'configure' && canManageInstalledAgent(matched)) { - openConfigDialog(matched); - } else { - openAgentOverviewDialog(matched); - } - }, [queryAgentName, queryInstanceId, queryView, selectedPodId, installedAgents]); - - // Legacy installable deep links pre-target the install dialog. The installable - // API keys on installableId; our catalog (/api/registry/agents) keys on name β€” - // match on either (plus id/displayName) and no-op if the catalog does not - // carry it, leaving the user on the browse list. - useEffect(() => { - if (!queryInstallable || !agents.length) return; - if (installableDeepLinkHandledRef.current === queryInstallable) return; - const target = String(queryInstallable).trim().toLowerCase(); - const matched = agents.find((a) => ( - [a.installableId, a.name, a.id, a._id, a.displayName] - .some((v) => String(v || '').trim().toLowerCase() === target) - )); - if (!matched) return; - installableDeepLinkHandledRef.current = queryInstallable; - setActiveTab(TAB_DISCOVER); - openInstallDialog(matched); - }, [queryInstallable, agents]); - - const fetchAgents = async () => { - setLoading(true); - setError(null); - - try { - const params = new URLSearchParams(); - if (searchQuery) params.append('q', searchQuery); - if (category !== 'all') params.append('category', category); - params.append('registry', 'commonly-official'); - params.append('verified', 'true'); - - const response = await axios.get(`/api/registry/agents?${params.toString()}`, { - headers: getAuthHeaders(), - }); - const fetchedAgents = response.data.agents || []; - - // Check for new agent type names (commonly-summarizer, openclaw) - const hasSummarizer = fetchedAgents.some((agent) => agent.name === 'commonly-summarizer'); - const hasOpenclaw = fetchedAgents.some((agent) => agent.name === 'openclaw'); - const shouldSeedDefaults = !searchQuery && category === 'all' && (!hasSummarizer || !hasOpenclaw); - - if (fetchedAgents.length === 0 || shouldSeedDefaults) { - // Auto-seed if registry is empty or missing Commonly Bot (default view only) - await seedAgents(params); - } else { - setAgents(fetchedAgents); - } - } catch (err) { - console.error('Error fetching agents:', err); - setError('Failed to load agents'); - setAgents([]); - } finally { - setLoading(false); - } - }; - - const fetchTemplates = async () => { - try { - const response = await axios.get('/api/registry/templates', { - headers: getAuthHeaders(), - }); - setTemplates(response.data.templates || []); - } catch (err) { - console.error('Error fetching agent templates:', err); - setTemplates([]); - } - }; - - const fetchAgentPresets = async () => { - setPresetsLoading(true); - setPresetsError(''); - try { - const response = await axios.get('/api/registry/presets', { - headers: getAuthHeaders(), - }); - setPresets(response.data?.presets || []); - setPresetCapabilities(response.data?.capabilities || null); - setPresetRuntimeSkills(response.data?.runtimeSkills || null); - setPresetDockerCapabilities(response.data?.dockerCapabilities || null); - } catch (err) { - console.error('Error fetching agent presets:', err); - setPresets([]); - setPresetCapabilities(null); - setPresetRuntimeSkills(null); - setPresetDockerCapabilities(null); - setPresetsError(err.response?.data?.error || 'Failed to load presets'); - } finally { - setPresetsLoading(false); - } - }; - - const seedAgents = async (params = new URLSearchParams()) => { - setSeeding(true); - try { - await axios.post('/api/registry/seed', {}, { - headers: getAuthHeaders(), - }); - // Re-fetch after seeding - const response = await axios.get(`/api/registry/agents?${params.toString()}`, { - headers: getAuthHeaders(), - }); - setAgents(response.data.agents || []); - } catch (err) { - console.error('Error seeding agents:', err); - setAgents([]); - } finally { - setSeeding(false); - } - }; - - const fetchInstalledAgents = async () => { - try { - const response = await axios.get(`/api/registry/pods/${selectedPodId}/agents`, { - headers: getAuthHeaders(), - }); - setInstalledAgents(response.data.agents || []); - } catch (err) { - console.error('Error fetching installed agents:', err); - } - }; - - const fetchAdminInstallations = async () => { - if (!isGlobalAdmin) return; - setAdminLoading(true); - setAdminError(''); - try { - const params = new URLSearchParams(); - if (adminSearch.trim()) params.append('q', adminSearch.trim()); - params.append('status', adminShowAll ? 'all' : 'active'); - params.append('limit', '200'); - params.append('offset', '0'); - const response = await axios.get(`/api/registry/admin/installations?${params.toString()}`, { - headers: getAuthHeaders(), - }); - setAdminInstallations(response.data.installations || []); - setAdminTotal(response.data.total || 0); - } catch (err) { - console.error('Error fetching admin installations:', err); - setAdminError('Failed to load installations.'); - } finally { - setAdminLoading(false); - } - }; - - const fetchClawdbotStatus = async () => { - try { - const response = await axios.get('/api/health/clawdbot', { - headers: getAuthHeaders(), - }); - const data = response.data; - setClawdbotGatewayStatus({ - connected: data.status === 'connected', - channels: data.channels || [], - gateway: data.gateway, - version: data.version, - }); - } catch (err) { - console.error('Error fetching Clawdbot status:', err); - setClawdbotGatewayStatus({ connected: false, channels: [] }); - } - }; - - useEffect(() => { - if (!isGlobalAdmin && activeTab > TAB_INSTALLED) { - setActiveTab(TAB_DISCOVER); - } - }, [isGlobalAdmin, activeTab]); - - useEffect(() => { - if (!isGlobalAdmin) return; - if (activeTab !== adminTabIndex) return; - fetchAdminInstallations(); - }, [isGlobalAdmin, activeTab, adminTabIndex, adminShowAll]); - - useEffect(() => { - if (!isGlobalAdmin) return; - if (activeTab !== adminTabIndex) return; - const handle = setTimeout(() => { - fetchAdminInstallations(); - }, 350); - return () => clearTimeout(handle); - }, [isGlobalAdmin, activeTab, adminTabIndex, adminSearch]); - - const openInstallDialog = (agent, options: any = {}) => { - // A local wrapper is CONNECTED, not installed: the BYO flow carries the - // token handoff, the "answers only while your session runs" honesty - // (#943/#945/#947), and the awaiting-seat truth. The old dialog offered - // it moltbot gateway fields instead β€” the broken install UI Sam flagged. - const agentKey = (agent?.agentName || agent?.name || '').toLowerCase(); - if (agentKey === 'claude-code') { - navigate('/v2/agents/byo'); - return; - } - const defaultPodId = selectedPodId - || accessiblePods[0]?._id - || userPods[0]?._id - || null; - setInstallAgent(agent); - setInstallPresetContext(options.presetContext || null); - setInstallInstanceName(agent?.displayName || agent?.name || ''); - setInstallInstanceId(''); - setInstallPodIds(defaultPodId ? [defaultPodId] : []); - setInstallGatewayMode('shared'); - setInstallGatewayId(''); - setInstallGatewayToken(''); - setInstallLlmMode('default'); - setInstallLlmKeys({ google: '', anthropic: '', openai: '' }); - setInstallDialogOpen(true); - }; - - const handleInstallPreset = async (preset) => { - if (!preset?.agentName) return; - setPresetInstallLoadingId(preset.id); - try { - const response = await axios.get(`/api/registry/agents/${preset.agentName}`, { - headers: getAuthHeaders(), - }); - const agent = { - name: response.data?.name || preset.agentName, - displayName: response.data?.displayName || preset.title, - description: response.data?.description || preset.description, - version: response.data?.version || '', - verified: response.data?.verified, - categories: response.data?.categories || [], - stats: response.data?.stats || {}, - iconUrl: response.data?.iconUrl || '', - }; - openInstallDialog(agent, { - presetContext: { - id: preset.id, - title: preset.title, - defaultSkills: (preset.defaultSkills || []).map((skill) => skill.id).filter(Boolean), - installHints: preset.installHints || {}, - }, - }); - setInstallInstanceName(`${preset.title}`); - } catch (err) { - console.error('Error loading preset agent type:', err); - alert(err.response?.data?.error || 'Failed to load preset agent type'); - } finally { - setPresetInstallLoadingId(''); - } - }; - - const openAdminTokensDialog = (installation) => { - setAdminTokensInstallation(installation); - setAdminTokensOpen(true); - }; - - const closeAdminTokensDialog = () => { - setAdminTokensOpen(false); - setAdminTokensInstallation(null); - }; - - const handleAdminRevokeToken = async (tokenId) => { - if (!adminTokensInstallation?.id) return; - setAdminRevokeLoadingId(tokenId); - try { - await axios.delete( - `/api/registry/admin/installations/${adminTokensInstallation.id}/runtime-tokens/${tokenId}`, - { headers: getAuthHeaders() }, - ); - setAdminInstallations((prev) => prev.map((item) => { - if (item.id !== adminTokensInstallation.id) return item; - return { - ...item, - runtimeTokens: (item.runtimeTokens || []).filter((token) => token.id !== tokenId), - }; - })); - setAdminTokensInstallation((prev) => { - if (!prev) return prev; - return { - ...prev, - runtimeTokens: (prev.runtimeTokens || []).filter((token) => token.id !== tokenId), - }; - }); - } catch (err) { - console.error('Error revoking admin runtime token:', err); - setAdminError('Failed to revoke runtime token.'); - } finally { - setAdminRevokeLoadingId(null); - } - }; - - const openAdminUninstallDialog = (installation) => { - setAdminUninstallTarget(installation); - }; - - const closeAdminUninstallDialog = () => { - setAdminUninstallTarget(null); - }; - - const handleAdminUninstallConfirm = async () => { - if (!adminUninstallTarget?.id) return; - setAdminUninstallLoading(true); - try { - await axios.delete(`/api/registry/admin/installations/${adminUninstallTarget.id}`, { - headers: getAuthHeaders(), - }); - setAdminInstallations((prev) => { - if (!adminShowAll) { - return prev.filter((item) => item.id !== adminUninstallTarget.id); - } - return prev.map((item) => ( - item.id === adminUninstallTarget.id - ? { ...item, status: 'uninstalled' } - : item - )); - }); - closeAdminUninstallDialog(); - } catch (err) { - console.error('Error uninstalling admin installation:', err); - setAdminError('Failed to uninstall installation.'); - } finally { - setAdminUninstallLoading(false); - } - }; - - const handleRunThemedAutonomy = async () => { - if (!isGlobalAdmin) return; - setAdminAutonomyLoading(true); - setAdminError(''); - setAdminAutonomyResult(null); - try { - const parsedHours = Number(adminAutonomyHours); - const parsedMinMatches = Number(adminAutonomyMinMatches); - const hours = Number.isFinite(parsedHours) && parsedHours > 0 ? parsedHours : 12; - const minMatches = Number.isFinite(parsedMinMatches) && parsedMinMatches > 0 ? parsedMinMatches : 4; - const response = await axios.post( - '/api/admin/agents/autonomy/themed-pods/run', - { - hours, - minMatches, - }, - { headers: getAuthHeaders() }, - ); - const result = response.data?.result || {}; - setAdminAutonomyResult({ - scannedPosts: result.scannedPosts || 0, - createdPods: (result.createdPods || []).length, - triggeredPods: (result.triggeredPods || []).length, - }); - fetchAdminInstallations(); - } catch (err) { - console.error('Error running themed autonomy:', err); - setAdminError(err?.response?.data?.error || 'Failed to run themed autonomy.'); - } finally { - setAdminAutonomyLoading(false); - } - }; - - const handleForceReprovisionAll = async () => { - if (!isGlobalAdmin) return; - setAdminReprovisionLoading(true); - setAdminReprovisionResult(null); - setAdminError(''); - try { - const response = await axios.post( - '/api/registry/admin/installations/reprovision-all', - {}, - { headers: getAuthHeaders() }, - ); - setAdminReprovisionResult({ - attempted: response.data?.attempted || 0, - succeeded: response.data?.succeeded || 0, - failed: response.data?.failed || 0, - }); - fetchAdminInstallations(); - } catch (err) { - console.error('Error running bulk reprovision:', err); - setAdminError(err?.response?.data?.error || 'Failed to reprovision all agent runtimes.'); - } finally { - setAdminReprovisionLoading(false); - } - }; - - const closeInstallDialog = () => { - setInstallDialogOpen(false); - setInstallAgent(null); - setInstallPresetContext(null); - setInstallPodIds([]); - setInstallInstanceName(''); - setInstallInstanceId(''); - setInstallGatewayMode('shared'); - setInstallGatewayId(''); - setInstallGatewayToken(''); - setInstallLlmMode('default'); - setInstallLlmKeys({ google: '', anthropic: '', openai: '' }); - setInstallWebhookUrl(''); - setInstallWebhookSecret(''); - setInstallGatewayCreateOpen(false); - setInstallGatewayCreateError(''); - setInstallGatewayCreateLoading(false); - setInstallGatewayForm({ - name: '', - slug: '', - mode: 'k8s', - namespace: '', - }); - setExistingAgentInfo(null); - setCheckingExisting(false); - }; - - const openInstallGatewayCreateDialog = () => { - if (!isGlobalAdmin) return; - setInstallGatewayCreateError(''); - setInstallGatewayToken(''); - setInstallGatewayForm({ - name: '', - slug: '', - mode: 'k8s', - namespace: '', - }); - setInstallGatewayCreateOpen(true); - }; - - const closeInstallGatewayCreateDialog = () => { - setInstallGatewayCreateOpen(false); - }; - - const handleInstallGatewayCreate = async () => { - if (!isGlobalAdmin) return; - if (!installGatewayForm.name.trim()) { - setInstallGatewayCreateError('Gateway name is required.'); - return; - } - setInstallGatewayCreateLoading(true); - setInstallGatewayCreateError(''); - try { - const payload = { - name: installGatewayForm.name.trim(), - slug: installGatewayForm.slug.trim() || undefined, - mode: 'k8s', - metadata: {} as Record, - }; - if (installGatewayForm.namespace.trim()) { - payload.metadata.namespace = installGatewayForm.namespace.trim(); - } - const response = await axios.post('/api/gateways', payload, { headers: getAuthHeaders() }); - const newGateway = response.data?.gateway; - const token = response.data?.gatewayToken || ''; - if (newGateway) { - setRuntimeGatewayList((prev) => { - const next = prev.filter((gateway) => gateway?._id !== newGateway._id); - next.push(newGateway); - return next; - }); - setInstallGatewayMode('custom'); - setInstallGatewayId(newGateway._id); - } - setInstallGatewayToken(token); - closeInstallGatewayCreateDialog(); - } catch (err) { - console.error('Error creating gateway:', err); - setInstallGatewayCreateError(err.response?.data?.error || 'Failed to create gateway'); - } finally { - setInstallGatewayCreateLoading(false); - } - }; - - // Unwired 2026-08-21 with the New Agent button (template flow deprecated); - // kept because the create dialog it opens still exists, dormant, until - // Phase 1 retires this component whole. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const openCreateDialog = () => { - setCreateAgentType(''); - setCreateAgentName(''); - setCreateAgentDescription(''); - setCreateAgentVisibility('private'); - setCreateAgentAvatarFile(null); - setCreateAgentAvatarPreview(''); - setCreateDialogOpen(true); - }; - - const closeCreateDialog = () => { - setCreateDialogOpen(false); - setCreateAgentType(''); - setCreateAgentName(''); - setCreateAgentDescription(''); - setCreateAgentVisibility('private'); - setCreateAgentAvatarFile(null); - setCreateAgentAvatarPreview(''); - }; - - const handleCreateTemplateAvatarChange = (event) => { - const file = event.target.files?.[0]; - if (!file) return; - setCreateAgentAvatarFile(file); - setCreateAgentAvatarPreview(URL.createObjectURL(file)); - }; - - const handleAvatarGenerated = (avatarDataUri, metadata) => { - if (!avatarDataUri || typeof avatarDataUri !== 'string' || !avatarDataUri.startsWith('data:image/')) { - alert('Invalid generated avatar data. Please try again.'); - return; - } - - // Convert data URI to blob for upload - const byteString = atob(avatarDataUri.split(',')[1]); - const mimeString = avatarDataUri.split(',')[0].split(':')[1].split(';')[0]; - const ab = new ArrayBuffer(byteString.length); - const ia = new Uint8Array(ab); - for (let i = 0; i < byteString.length; i++) { - ia[i] = byteString.charCodeAt(i); - } - const blob = new Blob([ab], { type: mimeString }); - const extensionByMime = { - 'image/png': 'png', - 'image/jpeg': 'jpg', - 'image/jpg': 'jpg', - 'image/webp': 'webp', - 'image/gif': 'gif', - 'image/svg+xml': 'svg', - }; - const extension = extensionByMime[mimeString] || 'png'; - const file = new File([blob], `avatar.${extension}`, { type: mimeString }); - - setCreateAgentAvatarFile(file); - setCreateAgentAvatarPreview(avatarDataUri); - }; - - /** - * Derive instanceId from displayName for consistent agent identity across pods. - * Matches backend logic in registry.js deriveInstanceId() - */ - const deriveInstanceId = (displayName, agentName) => { - if (!displayName) return 'default'; - const slug = String(displayName) - .trim() - .toLowerCase() - .replace(/[^a-z0-9-]/g, '-') - .replace(/-+/g, '-') - .replace(/^-+|-+$/g, ''); - if (!slug || slug === agentName?.toLowerCase()) { - return 'default'; - } - return slug; - }; - - /** - * Check if an agent with the derived instanceId already exists in other pods. - * Called when displayName changes to detect shared agent identity. - */ - const checkExistingAgentInstance = async (agentName, instanceId) => { - if (!agentName || !instanceId) { - setExistingAgentInfo(null); - return; - } - setCheckingExisting(true); - try { - const response = await axios.get( - `/api/registry/agents/${agentName}/instances/${encodeURIComponent(instanceId)}`, - { headers: getAuthHeaders() }, - ); - if (response.data.exists) { - setExistingAgentInfo(response.data); - } else { - setExistingAgentInfo(null); - } - } catch (err) { - console.error('Error checking existing agent instance:', err); - setExistingAgentInfo(null); - } finally { - setCheckingExisting(false); - } - }; - - // Check for existing agent when displayName changes - useEffect(() => { - if (!installDialogOpen || !installAgent) return; - const agentName = installAgent.agentName || installAgent.name; - const derivedId = deriveInstanceId(installInstanceName, agentName); - // Debounce the check - const timeoutId = setTimeout(() => { - checkExistingAgentInstance(agentName, derivedId); - }, 300); - return () => clearTimeout(timeoutId); - }, [installDialogOpen, installAgent, installInstanceName]); - - const buildInstallAuthProfiles = () => { - if (installLlmMode !== 'custom') return null; - const profiles = {}; - const googleKey = installLlmKeys.google.trim(); - const anthropicKey = installLlmKeys.anthropic.trim(); - const openaiKey = installLlmKeys.openai.trim(); - if (googleKey) { - profiles['google:default'] = { type: 'api_key', provider: 'google', key: googleKey }; - } - if (anthropicKey) { - profiles['anthropic:default'] = { type: 'api_key', provider: 'anthropic', key: anthropicKey }; - } - if (openaiKey) { - profiles['openai:default'] = { type: 'api_key', provider: 'openai', key: openaiKey }; - } - return Object.keys(profiles).length ? profiles : null; - }; - - const handleInstall = async () => { - if (!installAgent) return; - if (installPodIds.length === 0) { - alert('Select at least one pod to install this agent.'); - return; - } - - const selectedGatewayId = installGatewayMode === 'custom' ? installGatewayId : ''; - if (installGatewayMode === 'custom' && !selectedGatewayId) { - alert('Select a custom gateway before installing.'); - return; - } - - setInstallSaving(true); - try { - const agentName = installAgent.agentName || installAgent.name; - const agentDetails = await axios.get(`/api/registry/agents/${agentName}`, { - headers: getAuthHeaders(), - }); - const requiredScopes = agentDetails.data?.manifest?.context?.required || []; - const presetScopes = Array.isArray(installPresetContext?.installHints?.scopes) - ? installPresetContext.installHints.scopes - : []; - const installScopes = Array.from(new Set([ - ...(requiredScopes.length > 0 ? requiredScopes : ['context:read']), - ...presetScopes, - ])); - let resolvedInstanceId = installInstanceId?.trim() || ''; - if (resolvedInstanceId) { - resolvedInstanceId = resolvedInstanceId - .toLowerCase() - .replace(/[^a-z0-9-]/g, '-') - .replace(/-+/g, '-') - .replace(/^-+|-+$/g, '') || ''; - if (resolvedInstanceId === agentName.toLowerCase()) { - resolvedInstanceId = 'default'; - } - } - - const authProfiles = buildInstallAuthProfiles(); - const presetRuntime = installPresetContext?.installHints?.runtime || ''; - const isExternalRuntime = ['webhook', 'claude-code'].includes(presetRuntime); - - const results = await Promise.allSettled( - installPodIds.map((podId) => ( - (() => { - const config: Record = {}; - if (authProfiles) { - config.runtime = { authProfiles }; - } - if (isExternalRuntime && installWebhookUrl.trim()) { - config.runtime = { - ...config.runtime, - webhookUrl: installWebhookUrl.trim(), - ...(installWebhookSecret.trim() ? { webhookSecret: installWebhookSecret.trim() } : {}), - }; - } - const presetSkills = Array.isArray(installPresetContext?.defaultSkills) - ? installPresetContext.defaultSkills - : []; - if (agentName.toLowerCase() === 'openclaw') { - if (presetSkills.length > 0) { - config.skillSync = { - mode: 'selected', - allPods: false, - podIds: [podId], - skillNames: presetSkills, - }; - } - } - return axios.post('/api/registry/install', { - agentName, - podId, - scopes: installScopes, - instanceId: resolvedInstanceId || undefined, - displayName: installInstanceName || agentDetails.data?.displayName || agentName, - gatewayId: selectedGatewayId || undefined, - config: Object.keys(config).length ? config : undefined, - }, { - headers: getAuthHeaders(), - }); - })() - )), - ); - - const failures = results.filter((result) => result.status === 'rejected'); - if (failures.length > 0) { - const firstData = failures[0].reason?.response?.data; - // Cloud-gate refusal β€” route the user to the BYO flow instead of a - // dead-end "try again". The gate returns { code, message } with no - // `error` field, so detect on the code and prefer its message. - if (firstData?.code === 'cloud_agents_not_entitled') { - const msg = firstData.message - || 'Hosted agents require an upgrade or admin β€” connect your own agent instead.'; - if (window.confirm(`${msg}\n\nConnect your own local agent now?`)) { - navigate('/v2/agents/byo'); - return; - } - } else { - alert(firstData?.error || `Failed to install on ${failures.length} pod(s).`); - } - } - - if (installPodIds.includes(selectedPodId)) { - fetchInstalledAgents(); - } - - // Sprint B2 β€” Post-install handoff. The 60-second wedge is - // "install your first agent β†’ talk to it." On successful install - // (β‰₯1 pod succeeded), open the agent-room (1:1 DM) for the first - // installed pod so the user lands in a conversation surface, not - // back on the catalog. Detect v2 context by URL prefix; v1 uses - // its own `/pods/agent-room/:id` route. Failures degrade gracefully - // β€” we close the dialog and let the user navigate manually. - const successPodIds = results - .map((r, i) => (r.status === 'fulfilled' ? installPodIds[i] : null)) - .filter((id) => id); - const handoffPodId = successPodIds[0]; - const isV2Context = typeof window !== 'undefined' && window.location.pathname.startsWith('/v2'); - if (handoffPodId && !installAgent?.skipHandoff) { - try { - const roomRes = await axios.post('/api/agents/runtime/room', { - agentName, - instanceId: resolvedInstanceId || undefined, - podId: handoffPodId, - }, { - headers: getAuthHeaders(), - }); - const roomPodId = roomRes.data?.room?._id; - if (roomPodId) { - closeInstallDialog(); - navigate(isV2Context ? `/v2/pods/${roomPodId}` : `/pods/agent-room/${roomPodId}`); - return; - } - } catch (handoffErr) { - console.warn('[install] post-install agent-room handoff failed:', handoffErr); - // Fall through to closeInstallDialog without navigation. - } - } - - closeInstallDialog(); - } catch (err) { - console.error('Error installing agent:', err); - const data = err.response?.data; - if (data?.code === 'cloud_agents_not_entitled') { - const msg = data.message - || 'Hosted agents require an upgrade or admin β€” connect your own agent instead.'; - if (window.confirm(`${msg}\n\nConnect your own local agent now?`)) { - navigate('/v2/agents/byo'); - } - } else { - alert(data?.error || 'Failed to install agent'); - } - } finally { - setInstallSaving(false); - } - }; - - const handleCreateTemplate = async () => { - if (!createAgentType || !createAgentName) { - alert('Select an agent type and enter a name.'); - return; - } - setCreateSaving(true); - try { - let iconUrl = ''; - if (createAgentAvatarFile) { - const formData = new FormData(); - formData.append('image', createAgentAvatarFile); - const uploadRes = await axios.post('/api/uploads', formData, { - headers: { - ...getAuthHeaders(), - 'Content-Type': 'multipart/form-data', - }, - }); - iconUrl = uploadRes.data?.url || ''; - } - await axios.post('/api/registry/templates', { - agentName: createAgentType, - displayName: createAgentName, - description: createAgentDescription, - visibility: createAgentVisibility, - iconUrl, - }, { - headers: getAuthHeaders(), - }); - await fetchTemplates(); - closeCreateDialog(); - } catch (err) { - console.error('Error creating agent template:', err); - const message = err.response?.data?.error || err.message || 'Failed to create agent'; - const status = err.response?.status; - alert(status ? `${message} (status ${status})` : message); - } finally { - setCreateSaving(false); - } - }; - - const openEditTemplateDialog = (template) => { - setEditTemplate({ - ...template, - displayName: template.displayName || '', - description: template.description || '', - visibility: template.visibility || 'private', - iconUrl: template.iconUrl || '', - }); - setEditTemplateAvatarFile(null); - setEditTemplateAvatarPreview(''); - setEditTemplateOpen(true); - }; - - const closeEditTemplateDialog = () => { - setEditTemplateOpen(false); - setEditTemplate(null); - setEditTemplateAvatarFile(null); - setEditTemplateAvatarPreview(''); - }; - - const handleEditTemplateAvatarChange = (event) => { - const file = event.target.files?.[0]; - if (!file) return; - setEditTemplateAvatarFile(file); - setEditTemplateAvatarPreview(URL.createObjectURL(file)); - }; - - const handleUpdateTemplate = async () => { - if (!editTemplate) return; - setEditTemplateSaving(true); - try { - let iconUrl = editTemplate.iconUrl || ''; - if (editTemplateAvatarFile) { - const formData = new FormData(); - formData.append('image', editTemplateAvatarFile); - const uploadRes = await axios.post('/api/uploads', formData, { - headers: { - ...getAuthHeaders(), - 'Content-Type': 'multipart/form-data', - }, - }); - iconUrl = uploadRes.data?.url || ''; - } - await axios.patch(`/api/registry/templates/${editTemplate.templateId || editTemplate.id}`, { - displayName: editTemplate.displayName, - description: editTemplate.description, - visibility: editTemplate.visibility, - iconUrl, - }, { - headers: getAuthHeaders(), - }); - await fetchTemplates(); - closeEditTemplateDialog(); - } catch (err) { - console.error('Error updating agent template:', err); - alert(err.response?.data?.error || 'Failed to update agent'); - } finally { - setEditTemplateSaving(false); - } - }; - - const handleDeleteTemplate = async () => { - if (!editTemplate) return; - const confirmed = window.confirm('Delete this agent? This cannot be undone.'); - if (!confirmed) return; - try { - await axios.delete(`/api/registry/templates/${editTemplate.templateId || editTemplate.id}`, { - headers: getAuthHeaders(), - }); - await fetchTemplates(); - closeEditTemplateDialog(); - } catch (err) { - console.error('Error deleting agent template:', err); - alert(err.response?.data?.error || 'Failed to delete agent'); - } - }; - - const handleRemove = async (agent) => { - if (!selectedPodId) return; - - try { - const resolved = resolveInstalledAgent(agent); - if (!resolved.instanceId && isInstalled(agent?.name)) { - alert('Multiple installations found. Open the Installed tab to remove the correct instance.'); - return; - } - const instanceId = resolved.instanceId || 'default'; - await axios.delete( - `/api/registry/agents/${agent.name}/pods/${selectedPodId}?instanceId=${encodeURIComponent(instanceId)}`, - { - headers: getAuthHeaders(), - }, - ); - fetchInstalledAgents(); - } catch (err) { - console.error('Error removing agent:', err); - alert(err.response?.data?.error || 'Failed to remove agent'); - } - }; - - const handleMessageAgent = async (agent) => { - try { - const response = await axios.post('/api/agents/runtime/dm', { - agentName: agent?.name || agent?.agentName, - instanceId: agent?.instanceId || 'default', - podId: selectedPodId || undefined, - }, { - headers: getAuthHeaders(), - }); - const dmPodId = response.data?.dmPod?._id; - if (!dmPodId) { - throw new Error('DM pod not returned'); - } - // Stay in the shell that launched this β€” a v2 card must not eject the - // user into the legacy ChatRoom. Mirrors the install handoff above. - const isV2Context = typeof window !== 'undefined' && window.location.pathname.startsWith('/v2'); - navigate(isV2Context ? `/v2/pods/${dmPodId}` : `/pods/agent-admin/${dmPodId}`); - } catch (err) { - setError(err.response?.data?.message || 'Failed to open agent DM'); - } - }; - - const handleTalkToAgent = async (agent) => { - try { - const response = await axios.post('/api/agents/runtime/room', { - agentName: agent?.name || agent?.agentName, - instanceId: agent?.instanceId || 'default', - podId: selectedPodId || undefined, - }, { - headers: getAuthHeaders(), - }); - const roomPodId = response.data?.room?._id; - if (!roomPodId) { - throw new Error('Agent room not returned'); - } - // Stay in the shell that launched this β€” a v2 card must not eject the - // user into the legacy ChatRoom. Mirrors the install handoff above. - const isV2Context = typeof window !== 'undefined' && window.location.pathname.startsWith('/v2'); - navigate(isV2Context ? `/v2/pods/${roomPodId}` : `/pods/agent-room/${roomPodId}`); - } catch (err) { - setError(err.response?.data?.message || 'Failed to open agent room'); - } - }; - - const currentPodId = selectedPodId; - - const modelOptions = [ - { value: '', label: 'Default (global routing policy)' }, - { value: 'openai-codex/gpt-5.4', label: 'GPT-5.4 Codex (OAuth)' }, - { value: 'openai-codex/gpt-5.3-codex', label: 'GPT-5.3 Codex (OAuth, legacy)' }, - { value: 'google/gemini-2.5-flash', label: 'Gemini 2.5 Flash' }, - { value: 'google/gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash Lite' }, - { value: 'google/gemini-2.0-flash', label: 'Gemini 2.0 Flash' }, - { value: 'openrouter/nvidia/nemotron-3-super-120b-a12b:free', label: 'Nemotron 120B (OpenRouter free)' }, - { value: 'openrouter/arcee-ai/trinity-large-preview:free', label: 'Trinity Large (OpenRouter free)' }, - ]; - - const personaToneOptions = [ - 'friendly', - 'professional', - 'casual', - 'formal', - 'technical', - ]; - const toolPolicyHelper = 'Common: commonly (all), commonly_search, commonly_context, commonly_write.'; - - const DEFAULT_HEARTBEAT_CHECKLIST = [ - '- If the `commonly` skill is available, read and follow `./skills/commonly/SKILL.md` in this agent workspace.', - '- If `commonly` skill is missing, use HTTP APIs directly (do not run `commonly --help`): context via `/api/agents/runtime/pods/:podId/context` with runtime token, or `/api/pods/:podId/context` with user token.', - '- Fetch last 20 chat messages and 10 recent posts via user-token routes: `/api/messages/:podId?limit=20` and `/api/posts?podId=:podId&limit=10`.', - '- If there is something new, post a conversational, high-signal update to the pod chat and reply to relevant posts/threads.', - '- Do not post housekeeping-only status updates (for example: "no new posts" or "most recent post is ..."). If no meaningful new signal exists, reply HEARTBEAT_OK.', - '- Log short-term notes in memory/YYYY-MM-DD.md with message/post ids. Promote durable, agent-specific notes to MEMORY.md.', - '- If nothing new, reply HEARTBEAT_OK.', - ].join('\n'); - - - const formatCommaList = (items = []) => items.filter(Boolean).join(', '); - const parseCommaList = (value = '') => value - .split(',') - .map((item) => item.trim()) - .filter(Boolean); - const clampNumber = (value, fallback, min = 1, max = 1440) => { - const parsed = Number(value); - if (!Number.isFinite(parsed)) return fallback; - return Math.min(Math.max(parsed, min), max); - }; - const parseHeartbeatMinutes = (heartbeat) => { - if (!heartbeat) return 60; - if (Number.isFinite(heartbeat.everyMinutes)) return heartbeat.everyMinutes; - const raw = String(heartbeat.every || '').trim().toLowerCase(); - if (!raw) return 60; - const match = raw.match(/([0-9]+)\s*m/); - if (match) return Number(match[1]); - const value = Number(raw); - return Number.isFinite(value) ? value : 60; - }; - - const isInstalled = (agentName) => { - return installedAgents.some((a) => a.name === agentName); - }; - - const getInstallation = (agentName, instanceId = 'default') => { - return installedAgents.find((a) => a.name === agentName && a.instanceId === instanceId) || null; - }; - - const getAgentTargetInstanceId = (agent) => { - if (!agent) return 'default'; - if (agent.instanceId) return agent.instanceId; - const baseName = agent.agentName || agent.name; - if (agent.templateId) { - return deriveInstanceId(agent.displayName || baseName, baseName); - } - return 'default'; - }; - - const isInstalledAgent = (agent) => { - if (!agent) return false; - const baseName = agent.agentName || agent.name; - const targetInstanceId = getAgentTargetInstanceId(agent); - if (agent.templateId) { - return Boolean(getInstallation(baseName, targetInstanceId)); - } - return isInstalled(baseName); - }; - - const canRemoveAgent = (agent) => { - if (!agent) return false; - if (canRemoveInSelectedPod) return true; - const baseName = agent.agentName || agent.name; - const targetInstanceId = getAgentTargetInstanceId(agent); - return getInstallation(baseName, targetInstanceId)?.installedBy === currentUserId; - }; - - const resolveInstalledAgent = (agent) => { - if (!agent || agent.instanceId) return agent; - const baseName = agent.agentName || agent.name; - if (agent.templateId) { - const targetInstanceId = getAgentTargetInstanceId(agent); - const match = getInstallation(baseName, targetInstanceId); - if (match) { - return { - ...agent, - instanceId: match.instanceId, - installedBy: match.installedBy, - iconUrl: agent.iconUrl || match.iconUrl, - profile: match.profile, - config: match.config, - runtime: match.runtime, - displayName: agent.displayName || match.displayName || baseName, - }; - } - return agent; - } - const matches = installedAgents.filter((installed) => installed.name === agent.name); - if (matches.length === 1) { - const match = matches[0]; - return { - ...agent, - instanceId: match.instanceId, - installedBy: match.installedBy, - iconUrl: agent.iconUrl || match.iconUrl, - profile: match.profile, - config: match.config, - runtime: match.runtime, - displayName: agent.displayName || match.displayName || agent.name, - }; - } - return agent; - }; - - const selectedPod = (accessiblePods.length > 0 ? accessiblePods : userPods) - .find((pod) => pod._id === selectedPodId) || null; - const canRemoveInSelectedPod = selectedPod ? isPodAdmin(selectedPod) : false; - const canManageInstalledAgent = (agent) => { - if (!agent) return false; - if (isGlobalAdmin || canRemoveInSelectedPod) return true; - const installerId = String(agent.installedBy?._id || agent.installedBy || ''); - return Boolean(currentUserId && installerId && installerId === String(currentUserId)); - }; - - const closeAgentOverviewDialog = () => { - setAgentOverviewOpen(false); - setAgentOverview(null); - setAgentOverviewError(''); - setAgentOverviewLoading(false); - }; - // Template rows no longer surface in Discover (Sam, 2026-08-21: the flow is - // deprecated, and the rows were 11 pieces of internal junk β€” parked moltbot - // "team agents", FakeSam, test personas β€” advertised beside the curated - // catalog). The create/edit machinery below stays dormant until Phase 1 of - // the persona plan retires this component whole. - const allAgents = [...agents]; - const filteredAgents = category === 'all' - ? allAgents - : allAgents.filter((agent) => (agent.categories || []).includes(category)); - const presetCategories = useMemo(() => { - const ids = Array.from( - new Set( - (presets || []) - .map((preset) => String(preset?.category || '').trim()) - .filter(Boolean), - ), - ).sort((a, b) => a.localeCompare(b)); - return ['all', ...ids]; - }, [presets]); - const filteredPresets = useMemo(() => { - if (presetCategory === 'all') return presets; - return (presets || []).filter((preset) => String(preset?.category || '') === presetCategory); - }, [presets, presetCategory]); - - useEffect(() => { - if (!presetCategories.includes(presetCategory)) { - setPresetCategory('all'); - } - }, [presetCategories, presetCategory]); - - const applyConfigDialogState = (resolved) => { - setConfigAgent(resolved); - setConfigModel(resolved?.profile?.modelPreferences?.preferred || ''); - const persona = resolved?.profile?.persona || {}; - const toolPolicy = resolved?.profile?.toolPolicy || {}; - const heartbeatConfig = resolved?.config?.heartbeat || null; - const heartbeatChecklist = resolved?.config?.heartbeatChecklist || ''; - const autonomyConfig = resolved?.config?.autonomy || {}; - const errorRoutingConfig = resolved?.config?.errorRouting || {}; - const skillSyncConfig = resolved?.config?.skillSync || {}; - const currentScopes = Array.isArray(resolved?.scopes) ? resolved.scopes : []; - const normalizedCurrentScopes = Array.from( - new Set(currentScopes.map(normalizeInstallationScope).filter(Boolean)), - ); - const knownScopeIds = new Set(installationScopeOptions.map((scope) => scope.id)); - setConfigSelectedScopes(normalizedCurrentScopes.filter((scope) => knownScopeIds.has(scope as string))); - setConfigExtraScopes(normalizedCurrentScopes.filter((scope) => !knownScopeIds.has(scope as string))); - setConfigAutoJoinAgentOwnedPods(Boolean(autonomyConfig.autoJoinAgentOwnedPods)); - setConfigOwnerDmErrorRouting(Boolean(errorRoutingConfig.ownerDm)); - setConfigInstructions(resolved?.profile?.instructions || ''); - setConfigPersonaTone(persona.tone || 'friendly'); - setConfigPersonaSpecialties(formatCommaList(persona.specialties || [])); - setConfigPersonaBoundaries(formatCommaList(persona.boundaries || [])); - setConfigPersonaCustomInstructions(persona.customInstructions || ''); - setToolPolicyAllowed(formatCommaList(toolPolicy.allowed || ['commonly'])); - setToolPolicyBlocked(formatCommaList(toolPolicy.blocked || [])); - setToolPolicyRequireApproval(formatCommaList(toolPolicy.requireApproval || [])); - setConfigHeartbeatEnabled(heartbeatConfig?.enabled !== false); - setConfigHeartbeatInterval(parseHeartbeatMinutes(heartbeatConfig)); - setConfigHeartbeatChecklist( - heartbeatChecklist && heartbeatChecklist.trim() - ? heartbeatChecklist - : DEFAULT_HEARTBEAT_CHECKLIST, - ); - const hasCustomAuthProfiles = Boolean(resolved?.runtime?.hasCustomAuthProfiles); - setConfigAuthMode(hasCustomAuthProfiles ? 'custom' : 'default'); - setConfigAuthKeys({ google: '', anthropic: '', openai: '' }); - setConfigSkillEnvJson(''); - setConfigSkillEnvError(''); - setSkillSyncMode(skillSyncConfig.mode === 'selected' ? 'selected' : 'all'); - setSkillSyncAllPods(skillSyncConfig.allPods !== false); - setSkillSyncPods(Array.isArray(skillSyncConfig.podIds) ? skillSyncConfig.podIds : []); - setSkillSyncSkills(Array.isArray(skillSyncConfig.skillNames) ? skillSyncConfig.skillNames : []); - setConfigIdentityContent(''); - setRuntimeTokenValue(''); - setRuntimeTokenLabel('Local dev'); - setUserTokenValue(''); - setUserTokenScopes([]); - setUserTokenMeta({ hasToken: false, createdAt: null, scopeMode: 'none' }); - setProvisionForceReprovision(false); - setConfigOpen(true); - }; - - const openConfigDialog = async (agent) => { - const resolved = resolveInstalledAgent(agent); - if (!resolved.instanceId && isInstalled(agent?.name)) { - alert('Multiple installations found. Open the Installed tab to configure the correct instance.'); - return; - } - applyConfigDialogState(resolved); - // Fetch Clawdbot gateway status if opening clawdbot-bridge config - if (agent?.name === 'clawdbot-bridge') { - fetchClawdbotStatus(); - } - if (!selectedPodId || !resolved?.name) return; - const isOpenClaw = (resolved?.name || resolved?.agentName) === 'openclaw'; - let latestAgent = resolved; - try { - const instanceId = resolved.instanceId || 'default'; - const response = await axios.get( - `/api/registry/pods/${selectedPodId}/agents/${resolved.name}?instanceId=${encodeURIComponent(instanceId)}`, - { headers: getAuthHeaders() }, - ); - const latest = response.data?.agent || null; - if (latest) { - applyConfigDialogState(latest); - latestAgent = latest; - } - } catch (error) { - console.warn('Failed to refresh latest installation config:', error); - } - - if (!isOpenClaw) return; - - try { - const heartbeatInstanceId = latestAgent?.instanceId || resolved?.instanceId || 'default'; - const heartbeatResponse = await axios.get( - `/api/registry/pods/${selectedPodId}/agents/${resolved.name}/heartbeat-file?instanceId=${encodeURIComponent(heartbeatInstanceId)}`, - { headers: getAuthHeaders() }, - ); - const liveContent = heartbeatResponse?.data?.content || ''; - const configChecklist = latestAgent?.config?.heartbeatChecklist || resolved?.config?.heartbeatChecklist || ''; - setConfigHeartbeatChecklist(resolveHeartbeatEditorContent({ - liveContent, - configChecklist, - fallback: DEFAULT_HEARTBEAT_CHECKLIST, - })); - } catch (error) { - console.warn('Failed to load live HEARTBEAT.md content:', error); - } - - try { - const identityInstanceId = latestAgent?.instanceId || resolved?.instanceId || 'default'; - const identityResponse = await axios.get( - `/api/registry/pods/${selectedPodId}/agents/${resolved.name}/identity-file?instanceId=${encodeURIComponent(identityInstanceId)}`, - { headers: getAuthHeaders() }, - ); - setConfigIdentityContent(identityResponse?.data?.content || ''); - } catch (error) { - console.warn('Failed to load live IDENTITY.md content:', error); - } - }; - - const openAgentOverviewDialog = async (agent) => { - const resolved = resolveInstalledAgent(agent); - setAgentOverviewOpen(true); - setAgentOverview(resolved); - setAgentOverviewError(''); - - if (!selectedPodId || !resolved?.name) return; - setAgentOverviewLoading(true); - try { - const instanceId = resolved.instanceId || 'default'; - const response = await axios.get( - `/api/registry/pods/${selectedPodId}/agents/${resolved.name}?instanceId=${encodeURIComponent(instanceId)}`, - { headers: getAuthHeaders() }, - ); - const latest = response.data?.agent || null; - if (latest) { - setAgentOverview(latest); - } - } catch (error) { - setAgentOverviewError(error?.response?.data?.error || 'Failed to load latest agent details'); - } finally { - setAgentOverviewLoading(false); - } - }; - - const handleAutoPersona = async () => { - if (!selectedPodId || !configAgent) { - alert('Select a pod and open an agent configuration first.'); - return; - } - const resolved = resolveInstalledAgent(configAgent); - const instanceId = resolved.instanceId || 'default'; - const targetKey = `${resolved.name || resolved.agentName || resolved.name}:${instanceId}`; - setAutoPersonaLoading(true); - setAutoPersonaTarget(targetKey); - - try { - const response = await axios.post( - `/api/registry/pods/${selectedPodId}/agents/${resolved.name || resolved.agentName || resolved.name}/persona/generate`, - { instanceId }, - { headers: getAuthHeaders() }, - ); - const persona = response.data?.persona || {}; - const exampleInstructions = response.data?.exampleInstructions || ''; - - setConfigPersonaTone(persona.tone || 'friendly'); - setConfigPersonaSpecialties(formatCommaList(persona.specialties || [])); - setConfigPersonaBoundaries(formatCommaList(persona.boundaries || [])); - setConfigPersonaCustomInstructions(persona.customInstructions || ''); - setConfigInstructions((prev) => (prev && prev.trim() ? prev : exampleInstructions)); - } catch (error) { - console.error('Failed to generate persona:', error); - alert(error.response?.data?.error || 'Failed to generate persona.'); - } finally { - setAutoPersonaLoading(false); - setAutoPersonaTarget(''); - } - }; - - const fetchSkillSyncPods = async () => { - if (!configAgent || (configAgent.name || configAgent.agentName) !== 'openclaw') return; - setSkillSyncPodsLoading(true); - try { - const instanceId = configAgent.instanceId || 'default'; - const response = await axios.get( - `/api/registry/agents/${configAgent.name}/installations?instanceId=${encodeURIComponent(instanceId)}`, - { headers: getAuthHeaders() }, - ); - const pods = response.data?.installations || []; - setSkillSyncPodOptions(pods); - } catch (error) { - console.error('Failed to load skill sync pods:', error); - setSkillSyncPodOptions([]); - } finally { - setSkillSyncPodsLoading(false); - } - }; - - const fetchSkillSyncSkills = async (podIds) => { - const targets = Array.isArray(podIds) ? podIds.filter(Boolean) : []; - if (!targets.length) { - setSkillSyncSkillOptions([]); - return; - } - setSkillSyncSkillLoading(true); - try { - const responses = await Promise.all( - targets.map((podId) => axios.get(`/api/skills/pods/${podId}/imported`, { - headers: getAuthHeaders(), - params: { scope: 'pod' }, - })), - ); - const items = responses.flatMap((res) => res.data?.items || []); - const unique = new Map(); - items.forEach((item) => { - if (!item?.name) return; - const key = item.name.toLowerCase(); - if (!unique.has(key)) { - unique.set(key, item); - } - }); - setSkillSyncSkillOptions(Array.from(unique.values())); - } catch (error) { - console.error('Failed to load skill sync skills:', error); - setSkillSyncSkillOptions([]); - } finally { - setSkillSyncSkillLoading(false); - } - }; - - const openSkillsCatalog = async () => { - setSkillsCatalogOpen(true); - setSkillsCatalogLoading(true); - setSkillsCatalogError(''); - setSkillsImportNotice(''); - try { - const response = await axios.get('/api/skills/catalog?source=awesome', { - headers: getAuthHeaders(), - }); - setSkillsCatalogItems(response.data?.items || []); - } catch (error) { - console.error('Failed to load skills catalog:', error); - setSkillsCatalogError(error.response?.data?.error || 'Failed to load catalog'); - setSkillsCatalogItems([]); - } finally { - setSkillsCatalogLoading(false); - } - }; - - const closeSkillsCatalog = () => { - setSkillsCatalogOpen(false); - }; - - const beginSkillImport = (item) => { - setSkillsImportNotice(''); - setSkillsImportState({ - name: item?.name || '', - content: item?.content || '', - tags: (item?.tags || []).join(', '), - sourceUrl: item?.sourceUrl || '', - license: item?.license?.name || item?.license || '', - description: item?.description || '', - }); - if (!item?.content) { - setSkillsImportNotice( - 'This catalog entry does not include full skill content yet. Paste content manually or open the source link.', - ); - } - requestAnimationFrame(() => { - skillsSelectedRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); - }); - }; - - const openLicenseDialog = (item) => { - const license = item?.license || {}; - const title = license.name || 'License'; - const text = license.text || 'No license text available.'; - const path = license.path || ''; - setLicenseState({ title, text, path }); - setLicenseOpen(true); - }; - - const closeLicenseDialog = () => { - setLicenseOpen(false); - }; - - const handleImportSkill = async () => { - if (!selectedPodId || !configAgent?.name) return; - const payload = { - podId: selectedPodId, - name: skillsImportState.name, - content: skillsImportState.content, - tags: skillsImportState.tags - ? skillsImportState.tags.split(',').map((t) => t.trim()).filter(Boolean) - : [], - sourceUrl: skillsImportState.sourceUrl, - license: skillsImportState.license, - scope: 'agent', - agentName: configAgent?.name, - instanceId: configAgent?.instanceId || 'default', - description: skillsImportState.description, - }; - - try { - await axios.post('/api/skills/import', payload, { - headers: getAuthHeaders(), - }); - setSkillsCatalogOpen(false); - } catch (error) { - console.error('Failed to import skill:', error); - alert(error.response?.data?.error || 'Failed to import skill'); - } - }; - - const closeConfigDialog = () => { - setConfigOpen(false); - setConfigAgent(null); - setRuntimeTokens([]); - setRuntimeTokenValue(''); - setUserTokenValue(''); - setUserTokenScopes([]); - setUserTokenMeta({ hasToken: false, createdAt: null, scopeMode: 'none' }); - setProvisionLoading(false); - setProvisionResult(null); - setProvisionError(''); - setRuntimeStatus(null); - setRuntimeStatusLoading(false); - setRuntimeStatusError(''); - setRuntimeSessionResetResult(null); - setRuntimeLogsOpen(false); - setRuntimeLogsLoading(false); - setRuntimeLogsContent(''); - setRuntimeLogsError(''); - setRuntimeLogsAutoRefresh(true); - setRuntimeLogsFilter(''); - setConfigInstructions(''); - setConfigPersonaTone('friendly'); - setConfigPersonaSpecialties(''); - setConfigPersonaBoundaries(''); - setConfigPersonaCustomInstructions(''); - setConfigAuthMode('default'); - setConfigAuthKeys({ google: '', anthropic: '', openai: '' }); - setConfigSkillEnvJson(''); - setConfigSkillEnvError(''); - setConfigOwnerDmErrorRouting(false); - setToolPolicyAllowed('commonly'); - setToolPolicyBlocked(''); - setToolPolicyRequireApproval(''); - }; - - useEffect(() => { - const fetchRuntimeTokens = async () => { - if (!configOpen || !configAgent || !selectedPodId) return; - setRuntimeTokenLoading(true); - try { - const instanceId = configAgent.instanceId || 'default'; - const response = await axios.get( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/runtime-tokens?instanceId=${encodeURIComponent(instanceId)}`, - { headers: getAuthHeaders() }, - ); - setRuntimeTokens(response.data.tokens || []); - } catch (err) { - console.error('Error fetching runtime tokens:', err); - alert(err.response?.data?.error || 'Failed to load runtime tokens'); - setRuntimeTokens([]); - } finally { - setRuntimeTokenLoading(false); - } - }; - fetchRuntimeTokens(); - }, [configOpen, configAgent, selectedPodId]); - - useEffect(() => { - if (!configOpen || !configAgent || !selectedPodId) return; - fetchRuntimeStatus(); - }, [configOpen, configAgent, selectedPodId, runtimeGatewayMode, runtimeGatewayId]); - - useEffect(() => { - if (!configOpen || !configAgent) return; - const nextFilter = configAgent.instanceId || 'default'; - setRuntimeLogsFilter(nextFilter); - }, [configOpen, configAgent]); - - useEffect(() => { - if (!configOpen || !configAgent) return; - if ((configAgent.name || configAgent.agentName) !== 'openclaw') return; - fetchSkillSyncPods(); - }, [configOpen, configAgent]); - - useEffect(() => { - if (!configOpen || !configAgent) return; - if ((configAgent.name || configAgent.agentName) !== 'openclaw') return; - const availablePods = skillSyncPodOptions.map((pod) => pod.podId); - const targetPods = skillSyncAllPods ? availablePods : skillSyncPods; - fetchSkillSyncSkills(targetPods); - }, [configOpen, configAgent, skillSyncAllPods, skillSyncPods, skillSyncPodOptions]); - - useEffect(() => { - if (!configOpen || !configAgent) return; - const configuredGatewayId = configAgent?.runtime?.gatewayId || ''; - if (configuredGatewayId) { - setRuntimeGatewayMode('custom'); - setRuntimeGatewayId(configuredGatewayId); - } else { - setRuntimeGatewayMode('shared'); - setRuntimeGatewayId(''); - } - }, [configOpen, configAgent]); - - - useEffect(() => { - if (!runtimeLogsOpen || !runtimeLogsAutoRefresh) return () => {}; - const interval = setInterval(() => { - fetchRuntimeLogs(); - }, 4000); - return () => clearInterval(interval); - }, [runtimeLogsOpen, runtimeLogsAutoRefresh, runtimeLogsLines, configAgent, selectedPodId, runtimeGatewayMode, runtimeGatewayId]); - - useEffect(() => { - if (!isGlobalAdmin || (!configOpen && !installDialogOpen)) return; - let cancelled = false; - const fetchGateways = async () => { - setRuntimeGatewayLoading(true); - setRuntimeGatewayError(''); - try { - const response = await axios.get('/api/gateways', { headers: getAuthHeaders() }); - const gateways = response.data?.gateways || []; - const active = gateways.filter((gateway) => gateway?.status !== 'disabled'); - if (cancelled) return; - setRuntimeGatewayList(active); - } catch (error) { - if (cancelled) return; - console.warn('Failed to load gateways:', error); - setRuntimeGatewayError(error.response?.data?.error || 'Failed to load gateways'); - setRuntimeGatewayList([]); - } finally { - if (!cancelled) { - setRuntimeGatewayLoading(false); - } - } - }; - fetchGateways(); - return () => { - cancelled = true; - }; - }, [configOpen, installDialogOpen, isGlobalAdmin]); - - useEffect(() => { - if (runtimeGatewayMode !== 'custom') return; - if (runtimeGatewayId) return; - const firstGateway = runtimeGatewayList.find((gateway) => gateway.mode === 'k8s' && gateway.slug !== 'default') - || runtimeGatewayList.find((gateway) => gateway.mode === 'k8s') - || runtimeGatewayList[0]; - if (firstGateway?._id) { - setRuntimeGatewayId(firstGateway._id); - } - }, [runtimeGatewayMode, runtimeGatewayId, runtimeGatewayList]); - - useEffect(() => { - if (!installDialogOpen) return; - if (installGatewayMode !== 'custom') { - if (installGatewayId) setInstallGatewayId(''); - return; - } - if (installGatewayId) return; - const firstGateway = runtimeGatewayList.find((gateway) => gateway.mode === 'k8s' && gateway.slug !== 'default') - || runtimeGatewayList.find((gateway) => gateway.mode === 'k8s') - || runtimeGatewayList[0]; - if (firstGateway?._id) { - setInstallGatewayId(firstGateway._id); - } - }, [installDialogOpen, installGatewayMode, installGatewayId, runtimeGatewayList]); - - useEffect(() => { - const fetchUserTokenMeta = async () => { - if (!configOpen || !configAgent || !selectedPodId) return; - setUserTokenLoading(true); - try { - const instanceId = configAgent.instanceId || 'default'; - const response = await axios.get( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/user-token?instanceId=${encodeURIComponent(instanceId)}`, - { headers: getAuthHeaders() }, - ); - setUserTokenMeta({ - hasToken: !!response.data?.hasToken, - createdAt: response.data?.createdAt || null, - scopeMode: response.data?.scopeMode || ((response.data?.scopes || []).length > 0 ? 'scoped' : 'all'), - }); - setUserTokenScopes(response.data?.scopes || []); - } catch (err) { - console.error('Error fetching user token metadata:', err); - alert(err.response?.data?.error || 'Failed to load user token'); - setUserTokenMeta({ hasToken: false, createdAt: null, scopeMode: 'none' }); - setUserTokenScopes([]); - } finally { - setUserTokenLoading(false); - } - }; - fetchUserTokenMeta(); - }, [configOpen, configAgent, selectedPodId]); - - const handleGenerateRuntimeToken = async () => { - if (!configAgent || !selectedPodId) return; - setRuntimeTokenLoading(true); - try { - const instanceId = configAgent.instanceId || 'default'; - const response = await axios.post( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/runtime-tokens`, - { label: runtimeTokenLabel, instanceId }, - { headers: getAuthHeaders() }, - ); - setRuntimeTokenValue(response.data.token || ''); - const refresh = await axios.get( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/runtime-tokens?instanceId=${encodeURIComponent(instanceId)}`, - { headers: getAuthHeaders() }, - ); - setRuntimeTokens(refresh.data.tokens || []); - } catch (err) { - console.error('Error issuing runtime token:', err); - alert(err.response?.data?.error || 'Failed to issue runtime token'); - } finally { - setRuntimeTokenLoading(false); - } - }; - - const handleProvisionRuntime = async () => { - if (!selectedPodId || !configAgent) return; - const normalizedAgent = String(configAgent.name || configAgent.agentName || '').toLowerCase(); - if (normalizedAgent === 'commonly-bot' && !isGlobalAdmin) { - setProvisionError('Only global admins can provision commonly-bot runtime.'); - return; - } - setProvisionLoading(true); - setProvisionError(''); - setProvisionResult(null); - try { - const resolved = resolveInstalledAgent(configAgent); - const instanceId = resolved.instanceId || 'default'; - const gatewayId = resolveRuntimeGatewayId(); - const payload: Record = { - instanceId, - includeUserToken: provisionIncludeUserToken, - label: 'Provisioned runtime', - force: provisionForceReprovision, - }; - if (gatewayId) { - payload.gatewayId = gatewayId; - } - const response = await axios.post( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/provision`, - payload, - { headers: getAuthHeaders() }, - ); - setProvisionResult(response.data); - if (response.data.runtimeToken) { - setRuntimeTokenValue(response.data.runtimeToken); - } - if (response.data.userToken) { - setUserTokenValue(response.data.userToken); - } - const refresh = await axios.get( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/runtime-tokens?instanceId=${encodeURIComponent(instanceId)}`, - { headers: getAuthHeaders() }, - ); - setRuntimeTokens(refresh.data.tokens || []); - fetchInstalledAgents(); - } catch (err) { - console.error('Error provisioning runtime:', err); - setProvisionError(err.response?.data?.error || 'Failed to provision runtime'); - } finally { - setProvisionLoading(false); - } - }; - - const fetchRuntimeStatus = async () => { - if (!selectedPodId || !configAgent) return; - setRuntimeStatusLoading(true); - setRuntimeStatusError(''); - setRuntimeSessionResetResult(null); - try { - const instanceId = configAgent.instanceId || 'default'; - const gatewayId = resolveRuntimeGatewayId(); - const params: Record = { instanceId }; - if (gatewayId) { - params.gatewayId = gatewayId; - } - const response = await axios.get( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/runtime-status`, - { headers: getAuthHeaders(), params }, - ); - setRuntimeStatus(response.data || null); - } catch (err) { - console.error('Error fetching runtime status:', err); - setRuntimeStatusError(err.response?.data?.error || 'Failed to fetch runtime status'); - setRuntimeStatus(null); - } finally { - setRuntimeStatusLoading(false); - } - }; - - const handleStartRuntime = async () => { - if (!selectedPodId || !configAgent) return; - setRuntimeStatusLoading(true); - setRuntimeSessionResetResult(null); - try { - const instanceId = configAgent.instanceId || 'default'; - const gatewayId = resolveRuntimeGatewayId(); - const payload: Record = { instanceId }; - if (gatewayId) { - payload.gatewayId = gatewayId; - } - await axios.post( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/runtime-start`, - payload, - { headers: getAuthHeaders() }, - ); - await fetchRuntimeStatus(); - } catch (err) { - console.error('Error starting runtime:', err); - setRuntimeStatusError(err.response?.data?.error || 'Failed to start runtime'); - } finally { - setRuntimeStatusLoading(false); - } - }; - - const handleStopRuntime = async () => { - if (!selectedPodId || !configAgent) return; - setRuntimeStatusLoading(true); - setRuntimeSessionResetResult(null); - try { - const instanceId = configAgent.instanceId || 'default'; - const gatewayId = resolveRuntimeGatewayId(); - const payload: Record = { instanceId }; - if (gatewayId) { - payload.gatewayId = gatewayId; - } - await axios.post( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/runtime-stop`, - payload, - { headers: getAuthHeaders() }, - ); - await fetchRuntimeStatus(); - } catch (err) { - console.error('Error stopping runtime:', err); - setRuntimeStatusError(err.response?.data?.error || 'Failed to stop runtime'); - } finally { - setRuntimeStatusLoading(false); - } - }; - - const handleRestartRuntime = async () => { - if (!selectedPodId || !configAgent) return; - setRuntimeStatusLoading(true); - setRuntimeSessionResetResult(null); - try { - const instanceId = configAgent.instanceId || 'default'; - const gatewayId = resolveRuntimeGatewayId(); - const payload: Record = { instanceId }; - if (gatewayId) { - payload.gatewayId = gatewayId; - } - await axios.post( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/runtime-restart`, - payload, - { headers: getAuthHeaders() }, - ); - await fetchRuntimeStatus(); - } catch (err) { - console.error('Error restarting runtime:', err); - setRuntimeStatusError(err.response?.data?.error || 'Failed to restart runtime'); - } finally { - setRuntimeStatusLoading(false); - } - }; - - const handleClearRuntimeSessions = async () => { - if (!selectedPodId || !configAgent) return; - const confirmed = window.confirm('Clear all runtime session state for this agent instance and restart runtime?'); - if (!confirmed) return; - setRuntimeStatusLoading(true); - setRuntimeStatusError(''); - setRuntimeSessionResetResult(null); - try { - const instanceId = configAgent.instanceId || 'default'; - const gatewayId = resolveRuntimeGatewayId(); - const payload: Record = { instanceId, restart: true }; - if (gatewayId) { - payload.gatewayId = gatewayId; - } - const response = await axios.post( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/runtime-clear-sessions`, - payload, - { headers: getAuthHeaders() }, - ); - setRuntimeSessionResetResult(response.data || null); - await fetchRuntimeStatus(); - } catch (err) { - console.error('Error clearing runtime sessions:', err); - setRuntimeStatusError(err.response?.data?.error || 'Failed to clear runtime sessions'); - } finally { - setRuntimeStatusLoading(false); - } - }; - - const fetchRuntimeLogs = async () => { - if (!selectedPodId || !configAgent) return; - setRuntimeLogsLoading(true); - setRuntimeLogsError(''); - try { - const inputEl = runtimeLogsInputRef.current; - if (inputEl) { - const { scrollTop, scrollHeight, clientHeight } = inputEl; - runtimeLogsScrollRef.current = { - top: scrollTop, - atBottom: scrollTop + clientHeight >= scrollHeight - 8, - }; - } - const instanceId = configAgent.instanceId || 'default'; - const gatewayId = resolveRuntimeGatewayId(); - const params: Record = { lines: runtimeLogsLines, instanceId }; - if (gatewayId) { - params.gatewayId = gatewayId; - } - const response = await axios.get( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/runtime-logs`, - { headers: getAuthHeaders(), params }, - ); - setRuntimeLogsContent(response.data?.logs || ''); - } catch (err) { - console.error('Error fetching runtime logs:', err); - setRuntimeLogsError(err.response?.data?.error || 'Failed to fetch runtime logs'); - setRuntimeLogsContent(''); - } finally { - setRuntimeLogsLoading(false); - } - }; - - useEffect(() => { - if (!runtimeLogsOpen) return; - const inputEl = runtimeLogsInputRef.current; - if (!inputEl) return; - const { top, atBottom } = runtimeLogsScrollRef.current || {}; - requestAnimationFrame(() => { - if (!inputEl) return; - if (atBottom) { - inputEl.scrollTop = inputEl.scrollHeight; - return; - } - if (typeof top === 'number') { - inputEl.scrollTop = top; - } - }); - }, [runtimeLogsContent, runtimeLogsFilter, runtimeLogsOpen]); - - const handleRevokeRuntimeToken = async (tokenId) => { - if (!configAgent || !selectedPodId || !tokenId) return; - setRuntimeTokenRevokingId(tokenId); - try { - const instanceId = configAgent.instanceId || 'default'; - await axios.delete( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/runtime-tokens/${tokenId}?instanceId=${encodeURIComponent(instanceId)}`, - { headers: getAuthHeaders() }, - ); - const refresh = await axios.get( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/runtime-tokens?instanceId=${encodeURIComponent(instanceId)}`, - { headers: getAuthHeaders() }, - ); - setRuntimeTokens(refresh.data.tokens || []); - } catch (err) { - console.error('Error revoking runtime token:', err); - alert(err.response?.data?.error || 'Failed to revoke runtime token'); - } finally { - setRuntimeTokenRevokingId(null); - } - }; - - const handleToggleUserScope = (scopeId) => { - setUserTokenScopes((prev) => ( - prev.includes(scopeId) ? prev.filter((scope) => scope !== scopeId) : [...prev, scopeId] - )); - }; - - const handleGenerateUserToken = async () => { - if (!configAgent || !selectedPodId) return; - setUserTokenLoading(true); - try { - const instanceId = configAgent.instanceId || 'default'; - const response = await axios.post( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/user-token`, - { - scopes: userTokenScopes, - instanceId, - displayName: configAgent.displayName || configAgent.profile?.displayName, - }, - { headers: getAuthHeaders() }, - ); - setUserTokenValue(response.data?.token || ''); - setUserTokenMeta({ - hasToken: true, - createdAt: response.data?.createdAt || null, - scopeMode: response.data?.scopeMode || (userTokenScopes.length > 0 ? 'scoped' : 'all'), - }); - } catch (err) { - console.error('Error issuing agent user token:', err); - alert(err.response?.data?.error || 'Failed to issue user token'); - } finally { - setUserTokenLoading(false); - } - }; - - const handleRevokeUserToken = async () => { - if (!configAgent || !selectedPodId) return; - setUserTokenRevoking(true); - try { - const instanceId = configAgent.instanceId || 'default'; - await axios.delete( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/user-token?instanceId=${encodeURIComponent(instanceId)}`, - { headers: getAuthHeaders() }, - ); - setUserTokenValue(''); - setUserTokenMeta({ hasToken: false, createdAt: null, scopeMode: 'none' }); - setUserTokenScopes([]); - } catch (err) { - console.error('Error revoking agent user token:', err); - alert(err.response?.data?.error || 'Failed to revoke user token'); - } finally { - setUserTokenRevoking(false); - } - }; - - const handleCopyToken = async () => { - if (!runtimeTokenValue) return; - try { - await navigator.clipboard.writeText(runtimeTokenValue); - } catch (err) { - console.warn('Clipboard copy failed:', err); - } - }; - - const userTokenIsFullAccess = userTokenMeta.hasToken && userTokenMeta.scopeMode === 'all'; - - const handleCopyInstallGatewayToken = async () => { - if (!installGatewayToken) return; - try { - await navigator.clipboard.writeText(installGatewayToken); - } catch (err) { - console.warn('Clipboard copy failed:', err); - } - }; - - const provisionGatewayMissing = runtimeGatewayMode === 'custom' && !runtimeGatewayId; - - const buildConfigAuthProfiles = () => { - if (configAuthMode !== 'custom') return null; - const profiles = {}; - const googleKey = configAuthKeys.google.trim(); - const anthropicKey = configAuthKeys.anthropic.trim(); - const openaiKey = configAuthKeys.openai.trim(); - if (googleKey) { - profiles['google:default'] = { type: 'api_key', provider: 'google', key: googleKey }; - } - if (anthropicKey) { - profiles['anthropic:default'] = { type: 'api_key', provider: 'anthropic', key: anthropicKey }; - } - if (openaiKey) { - profiles['openai:default'] = { type: 'api_key', provider: 'openai', key: openaiKey }; - } - return Object.keys(profiles).length ? profiles : null; - }; - - const parseSkillEnvJson = () => { - const raw = configSkillEnvJson.trim(); - if (!raw) return null; - try { - const parsed = JSON.parse(raw); - if (!parsed || typeof parsed !== 'object') { - return { error: 'Skill token JSON must be an object.' }; - } - return { value: parsed }; - } catch (error) { - return { error: 'Skill token JSON is invalid.' }; - } - }; - - const saveConfig = async () => { - if (!configAgent || !selectedPodId) { - return; - } - const skillEnvParsed = parseSkillEnvJson(); - if (skillEnvParsed?.error) { - setConfigSkillEnvError(skillEnvParsed.error); - return; - } - setConfigSkillEnvError(''); - setConfigSaving(true); - try { - const instanceId = configAgent.instanceId || 'default'; - const isOpenClaw = (configAgent.name || configAgent.agentName) === 'openclaw'; - const authProfiles = buildConfigAuthProfiles(); - const scopes = Array.from(new Set([ - ...configExtraScopes, - ...configSelectedScopes.map(normalizeInstallationScope).filter(Boolean), - ])); - await axios.patch(`/api/registry/pods/${selectedPodId}/agents/${configAgent.name}`, { - config: { - heartbeat: { - enabled: configHeartbeatEnabled, - everyMinutes: clampNumber(configHeartbeatInterval, 10), - }, - autonomy: { - autoJoinAgentOwnedPods: configAutoJoinAgentOwnedPods, - }, - errorRouting: { - ownerDm: configOwnerDmErrorRouting, - }, - heartbeatChecklist: configHeartbeatChecklist || '', - runtime: { - authProfiles: authProfiles ?? null, - skillEnv: skillEnvParsed?.value ?? null, - }, - ...(isOpenClaw - ? { - skillSync: { - mode: skillSyncMode, - allPods: skillSyncAllPods, - podIds: skillSyncPods, - skillNames: skillSyncSkills, - }, - } - : {}), - }, - scopes, - modelPreferences: { preferred: configModel }, - instanceId, - instructions: configInstructions, - persona: { - tone: configPersonaTone, - specialties: parseCommaList(configPersonaSpecialties), - boundaries: parseCommaList(configPersonaBoundaries), - customInstructions: configPersonaCustomInstructions, - }, - toolPolicy: { - allowed: parseCommaList(toolPolicyAllowed), - blocked: parseCommaList(toolPolicyBlocked), - requireApproval: parseCommaList(toolPolicyRequireApproval), - }, - }, { - headers: getAuthHeaders(), - }); - - if (isOpenClaw) { - await axios.post( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name || configAgent.agentName}/heartbeat-file`, - { - instanceId, - content: configHeartbeatChecklist || '', - }, - { headers: getAuthHeaders() }, - ); - if (configIdentityContent.trim()) { - setIdentityFileSaving(true); - try { - await axios.post( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name || configAgent.agentName}/identity-file`, - { instanceId, content: configIdentityContent }, - { headers: getAuthHeaders() }, - ); - } catch (identityErr) { - console.warn('Failed to save IDENTITY.md:', identityErr); - } finally { - setIdentityFileSaving(false); - } - } - } - if (provisionForceReprovision) { - const gatewayId = resolveRuntimeGatewayId(); - const provisionPayload: Record = { - instanceId, - includeUserToken: provisionIncludeUserToken, - label: 'Provisioned runtime', - force: true, - }; - if (gatewayId) { - provisionPayload.gatewayId = gatewayId; - } - try { - const provisionResponse = await axios.post( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name}/provision`, - provisionPayload, - { headers: getAuthHeaders() }, - ); - setProvisionResult(provisionResponse.data); - } catch (provisionErr) { - console.warn('Provision after save failed:', provisionErr); - } - } - await fetchInstalledAgents(); - closeConfigDialog(); - } catch (err) { - console.error('Error updating agent model preferences:', err); - alert(err.response?.data?.error || 'Failed to update agent configuration'); - } finally { - setConfigSaving(false); - } - }; - - const handleResetHeartbeatChecklist = async () => { - if (!configAgent || !selectedPodId) return; - const isOpenClaw = (configAgent.name || configAgent.agentName) === 'openclaw'; - if (!isOpenClaw) return; - const instanceId = configAgent.instanceId || 'default'; - setHeartbeatResetLoading(true); - try { - setConfigHeartbeatChecklist(DEFAULT_HEARTBEAT_CHECKLIST); - await axios.post( - `/api/registry/pods/${selectedPodId}/agents/${configAgent.name || configAgent.agentName}/heartbeat-file`, - { - instanceId, - content: DEFAULT_HEARTBEAT_CHECKLIST, - reset: true, - }, - { headers: getAuthHeaders() }, - ); - } catch (error) { - console.error('Failed to reset heartbeat checklist:', error); - alert(error.response?.data?.error || 'Failed to reset heartbeat checklist.'); - } finally { - setHeartbeatResetLoading(false); - } - }; - - return ( - - - {/* The v2 shell renders its own page header, so hide this legacy - title to avoid a duplicated heading echo. */} - {!v2Embedded && ( - - - Agent Hub - - - Discover, install, and manage pod-native agents. - - - )} - - - {/* "New Agent" (template creator) removed 2026-08-21: the template - flow is deprecated and its rows no longer render in Discover β€” - a creator whose output is invisible is a trap, not a feature. */} - - - - - - - - setSearchQuery(e.target.value)} - InputProps={{ - startAdornment: ( - - - - ), - }} - sx={{ - '& .MuiOutlinedInput-root': { - borderRadius: 2, - backgroundColor: theme.palette.background.paper, - }, - }} - /> - - Category - - - - Install to Pod - - - - - - {allAgents.length} agents β€’ {installedAgents.length} installed - - {seeding && ( - - )} - - - - {/* Tabs */} - - setActiveTab(v)}> - - 0 ? `(${installedAgents.length})` : ''}`} /> - {isGlobalAdmin && ( - 0 ? `(${adminTotal})` : ''}`} /> - )} - - - - {error && ( - - {error} - - )} - - {/* Discover Tab */} - {activeTab === TAB_DISCOVER && ( - <> - - - - - {searchQuery ? `Results for "${searchQuery}"` : 'All Agents'} - - - {searchQuery - ? 'Matching agents from the catalog.' - : 'Agents you can install into the selected pod.'} - - - - - - {loading - ? [1, 2, 3, 4, 5, 6].map((i) => ( - - - - )) - : filteredAgents.map((agent) => ( - - - - ))} - - {!loading && allAgents.length === 0 && ( - - No agents found - - )} - - - )} - - {/* Installed Tab */} - {/* Presets panel removed with the tab β€” see TAB_PRESETS note near the top. */} - - {activeTab === TAB_INSTALLED && ( - - {!currentPodId ? ( - Select a pod to view installed agents - ) : installedAgents.length === 0 ? ( - - - - - - No agents installed in this pod yet - - - Install an agent to give this pod automation, summarization, or collaborative AI partners. - - setActiveTab(TAB_DISCOVER)} - underline="hover" - sx={{ fontWeight: 600 }} - > - Browse the Discover tab β†’ - - - ) : ( - - {installedAgents.map((agent) => { - const canManage = canManageInstalledAgent(agent); - return ( - - (canManage ? openConfigDialog(target) : openAgentOverviewDialog(target))} - onRemove={handleRemove} - onMessage={handleMessageAgent} - onTalkTo={handleTalkToAgent} - canConfigure={true} - installedActionLabel={canManage ? 'Configure' : 'View'} - canRemove={canManage} - /> - - ); - })} - - )} - - )} - - {isGlobalAdmin && activeTab === adminTabIndex && ( - - - setAdminView(value)} - > - - - - - - {adminView === ADMIN_VIEW_INSTALLATIONS && ( - <> - - Themed Pod Autonomy - - setAdminAutonomyHours(Number(e.target.value))} - inputProps={{ min: 1, max: 168 }} - sx={{ width: { xs: '100%', sm: 140 } }} - /> - setAdminAutonomyMinMatches(Number(e.target.value))} - inputProps={{ min: 1, max: 50 }} - sx={{ width: { xs: '100%', sm: 160 } }} - /> - - - {adminAutonomyResult && ( - - Themed autonomy run complete. Scanned {adminAutonomyResult.scannedPosts} posts, created{' '} - {adminAutonomyResult.createdPods} pod(s), triggered {adminAutonomyResult.triggeredPods} pod(s). - - )} - - - - Runtime Reprovision - - - Force reprovision all active agent installations and rotate shared runtime tokens per agent instance. - - - - {adminReprovisionResult && ( - - Reprovision complete. Attempted {adminReprovisionResult.attempted}, succeeded{' '} - {adminReprovisionResult.succeeded}, failed {adminReprovisionResult.failed}. - - )} - - - - - setAdminSearch(e.target.value)} - InputProps={{ - startAdornment: ( - - - - ), - }} - sx={{ minWidth: { xs: '100%', sm: 320 } }} - /> - setAdminShowAll(e.target.checked)} - /> - )} - label="Include uninstalled" - /> - - - - - {adminTotal} total installations - - - - {adminError && ( - - {adminError} - - )} - - - - - - Agent - Instance - Pod - Status - Tokens - Last Used - Installed By - Actions - - - - {adminLoading && ( - - - - - - )} - {!adminLoading && adminInstallations.length === 0 && ( - - - No installations found. - - - )} - {!adminLoading && adminInstallations.map((install) => ( - - - - {install.displayName || install.agentName} - - - {install.agentName} - - - - - {install.instanceId || 'default'} - - - - - {install.pod?.name || 'Unknown pod'} - - - {install.pod?.id || 'β€”'} - - - {renderAdminStatusChip(install.status)} - - - - {(install.runtimeTokens || []).length} - - - - - - - {formatDateTime(install.usage?.lastUsedAt)} - - - - - {install.installedBy?.username || 'Unknown'} - - - {install.installedBy?.email || 'β€”'} - - - - - - - ))} - -
-
- - )} - - {adminView === ADMIN_VIEW_EVENTS && ( - - )} -
- )} - - - - Agent Overview - {agentOverview?.displayName ? ` β€’ ${agentOverview.displayName}` : ''} - - - {agentOverviewError && ( - - {agentOverviewError} - - )} - {agentOverviewLoading && ( - - - - )} - {agentOverview && ( - - - - {(agentOverview as any).config?.presetId && ( - - )} - {((agentOverview as any).config?.customizations?.soul || (agentOverview as any).config?.customizations?.heartbeat) && ( - - )} - - - {agentOverview.profile?.purpose || agentOverview.description || 'No description provided.'} - - Usage - - {(agentOverview.usage?.messagesProcessed || 0).toLocaleString()} messages processed β€’ {(agentOverview.usage?.podsJoined || 0).toLocaleString()} pods joined - - Instructions - - - Only the installer, pod admin, or a global admin can change configuration or runtime controls. - - - )} - - - - {agentOverview && canManageInstalledAgent(agentOverview) && ( - - )} - - - - - - Agent Settings - {configAgent?.displayName ? ` β€’ ${configAgent.displayName}` : ''} - - - {configAgent?.instanceId && ( - - Instance ID: {configAgent.instanceId} - - )} - {((configAgent as any)?.config?.presetId || (configAgent as any)?.config?.customizations?.soul || (configAgent as any)?.config?.customizations?.heartbeat) && ( - - {(configAgent as any).config?.presetId && ( - - )} - {((configAgent as any).config?.customizations?.soul || (configAgent as any).config?.customizations?.heartbeat) && ( - - )} - - )} - - Model Override - - - "Default" follows the global routing policy (dev vs community). Override only to pin this agent to a specific model. - - - - - - - LLM Credentials - - - Provide custom keys for this agent or use the gateway defaults. Keys are write-only. - - - setConfigAuthMode(event.target.value)} - > - } label="Use gateway defaults" /> - } label="Provide custom keys" /> - - - {configAuthMode === 'custom' && ( - <> - setConfigAuthKeys((prev) => ({ ...prev, google: event.target.value }))} - size="small" - type="password" - sx={{ mb: 2 }} - /> - setConfigAuthKeys((prev) => ({ ...prev, anthropic: event.target.value }))} - size="small" - type="password" - sx={{ mb: 2 }} - /> - setConfigAuthKeys((prev) => ({ ...prev, openai: event.target.value }))} - size="small" - type="password" - sx={{ mb: 2 }} - /> - - )} - {configAgent?.runtime?.hasCustomAuthProfiles && ( - - This agent already has custom keys on the gateway. Saving with defaults will reset it. - - )} - - - Skill API Tokens - - - Provide JSON mapping of skill names to env/apiKey entries. Example: - - setConfigSkillEnvJson(event.target.value)} - placeholder={`{\n "notion": { "env": { "NOTION_API_KEY": "..." } },\n "tavily": { "apiKey": "..." }\n}`} - sx={{ mb: 2 }} - /> - {configSkillEnvError && ( - - {configSkillEnvError} - - )} - - Apply changes by provisioning runtime (or restarting the gateway) after saving. - - - - - - Persona & Instructions - - - - - setConfigInstructions(e.target.value)} - multiline - minRows={3} - sx={{ mb: 2 }} - /> - - Tone - - - setConfigPersonaSpecialties(e.target.value)} - sx={{ mb: 2 }} - /> - setConfigPersonaBoundaries(e.target.value)} - sx={{ mb: 2 }} - /> - setConfigPersonaCustomInstructions(e.target.value)} - multiline - minRows={2} - sx={{ mb: 2 }} - /> - - - - - Tool Access Policy - - setToolPolicyAllowed(e.target.value)} - helperText={toolPolicyHelper} - sx={{ mb: 2 }} - /> - setToolPolicyBlocked(e.target.value)} - helperText="Blocklist always wins, even if allowed." - sx={{ mb: 2 }} - /> - setToolPolicyRequireApproval(e.target.value)} - helperText="Tools that should request human approval." - sx={{ mb: 2 }} - /> - - - - - Integration Autonomy - - - Configure installation scopes for runtime reads/writes. - - - {installationScopeOptions.map((scope) => ( - { - setConfigSelectedScopes((prev) => { - if (e.target.checked) return Array.from(new Set([...prev, scope.id])); - return prev.filter((item) => item !== scope.id); - }); - }} - /> - )} - label={`${scope.label} (${scope.id})`} - /> - ))} - - {configExtraScopes.length > 0 && ( - - Preserving legacy scopes: {configExtraScopes.join(', ')} - - )} - setConfigAutoJoinAgentOwnedPods(e.target.checked)} - /> - )} - label="Allow auto-join into pods owned by bot users" - sx={{ mb: 2 }} - /> - - Auto-join applies only to active installations and uses scheduler/admin autonomy flows. - - setConfigOwnerDmErrorRouting(e.target.checked)} - /> - )} - label="Route error-like agent messages to installer debug DM" - sx={{ mb: 1 }} - /> - - Opt-in per installation. When enabled, detected error/debug outputs are moved to agent-admin DM and - source chat gets a short system notice. - - - {(configAgent?.name === 'openclaw' || configAgent?.agentName === 'openclaw') && ( - <> - - - Identity (IDENTITY.md) - - - Loaded at every session start. Defines name, vibe, and domain. Auto-generated from persona fields above when saved β€” or edit directly here. - - setConfigIdentityContent(e.target.value)} - multiline - minRows={5} - sx={{ mb: 1 }} - helperText="Stored in the agent workspace IDENTITY.md. Saved when you click Save." - disabled={identityFileSaving} - /> - - )} - - - - - Heartbeat - - {(configAgent?.name === 'openclaw' || configAgent?.agentName === 'openclaw') && ( - setConfigHeartbeatChecklist(e.target.value)} - multiline - minRows={4} - sx={{ mb: 2 }} - helperText="Stored in the agent workspace HEARTBEAT.md. Keep this short." - /> - )} - {(configAgent?.name === 'openclaw' || configAgent?.agentName === 'openclaw') && ( - - - - )} - setConfigHeartbeatEnabled(e.target.checked)} - /> - )} - label="Enable heartbeat (auto-check pod messages)" - sx={{ mb: 1 }} - /> - setConfigHeartbeatInterval(Number(e.target.value))} - inputProps={{ min: 1 }} - sx={{ mb: 2 }} - /> - - - - - {(configAgent?.name === 'openclaw' || configAgent?.agentName === 'openclaw') && ( - - - Workspace Skills - - - Sync imported pod skills into this agent’s workspace `skills/` directory. Saving overwrites the - directory to avoid conflicts. - - setSkillSyncAllPods(event.target.checked)} - /> - )} - label="Use all pods where this agent is installed" - sx={{ mb: 2 }} - /> - {!skillSyncAllPods && ( - - Pods to sync - - - )} - - Sync mode - - - {skillSyncMode === 'selected' && ( - - Skills to sync - - - )} - {(skillSyncPodsLoading || skillSyncSkillLoading) && ( - Loading skill sync data... - )} - - )} - - - - {/* Clawdbot-specific configuration panel */} - {configAgent?.name === 'clawdbot-bridge' ? ( - - ) : ( - <> - - Provision Runtime - - - Create tokens and write runtime config for this agent instance on the shared gateway or a custom gateway. - - - { - const nextValue = event.target.value; - setRuntimeGatewayMode(nextValue); - if (nextValue === 'shared') { - setRuntimeGatewayId(''); - } - }} - > - } label="Shared gateway (default)" /> - } - label="Custom gateway" - disabled={!isGlobalAdmin} - /> - - - {!isGlobalAdmin && ( - - Gateway selection is available to global admins. - - )} - {runtimeGatewayMode === 'custom' && ( - <> - - Gateway - - - {runtimeGatewayLoading && ( - - Loading gateways... - - )} - {runtimeGatewayError && ( - - {runtimeGatewayError} - - )} - {!runtimeGatewayLoading && runtimeGatewayOptions.length === 0 && ( - - No custom gateways available yet. - - )} - - )} - setProvisionIncludeUserToken(event.target.checked)} - disabled={String(configAgent?.name || configAgent?.agentName || '').toLowerCase() === 'commonly-bot' && !isGlobalAdmin} - /> - )} - label="Include bot user token (recommended for OpenClaw)" - sx={{ mb: 2 }} - /> - setProvisionForceReprovision(event.target.checked)} - /> - )} - label="Force reprovision (rotate runtime token)" - sx={{ mb: 2 }} - /> - {String(configAgent?.name || configAgent?.agentName || '').toLowerCase() === 'commonly-bot' && !isGlobalAdmin && ( - - Provisioning commonly-bot is restricted to global admins. - - )} - - {provisionGatewayMissing && ( - - Select a custom gateway before provisioning. - - )} - {provisionError && ( - - {provisionError} - - )} - {provisionResult && ( - - {provisionResult.restartRequired - ? (provisionResult.runtimeRestarted - ? 'Runtime config written. Gateway restarted.' - : 'Runtime config written. Gateway restart required.') - : 'Runtime config written.'} - {provisionResult.runtimeStarted - ? ' Runtime started.' - : ' Runtime not started.'} - {provisionResult.runtimeRestartError - ? ` Restart error: ${provisionResult.runtimeRestartError}` - : ''} - - )} - - - Runtime Controls - - - Start/stop and inspect the runtime (gateway-managed in Kubernetes). - - - - - - - - - - {runtimeStatusError && ( - - {runtimeStatusError} - - )} - {runtimeStatus && ( - - Status: {runtimeStatus.status?.status || runtimeStatus.status?.state || runtimeStatus.status} - {runtimeStatus.status?.service ? ` β€’ Service ${runtimeStatus.status.service}` : ''} - {(() => { - const gatewayLabel = runtimeStatus.gatewaySlug - || selectedRuntimeGateway?.name - || selectedRuntimeGateway?.slug - || (runtimeGatewayMode === 'custom' ? 'Custom gateway' : 'Shared gateway'); - return gatewayLabel ? ` β€’ Gateway ${gatewayLabel}` : ''; - })()} - - )} - {runtimeSessionResetResult?.cleared && ( - - Cleared session state for account {runtimeSessionResetResult.accountId || 'unknown'}. - {runtimeSessionResetResult.restarted?.restarted - ? ' Runtime restarted.' - : ''} - - )} - - - - - Runtime Token - - - Generate a token for external agent runtimes (shown once). - - setRuntimeTokenLabel(e.target.value)} - size="small" - sx={{ mb: 2 }} - /> - - {runtimeTokenValue && ( - - - - - - - - - )} - {runtimeTokens.length > 0 && ( - - - Issued tokens: {runtimeTokens.length} - - - )} - {runtimeTokens.length > 0 && ( - - {runtimeTokens.map((token) => ( - - - - {token.label || 'Runtime token'} - - - Issued {token.createdAt ? new Date(token.createdAt).toLocaleString() : 'unknown'} - {token.lastUsedAt ? ` β€’ Last used ${new Date(token.lastUsedAt).toLocaleString()}` : ''} - - - - - ))} - - )} - - - - - Designated User Token - - - Use a bot user API token for MCP skills or direct REST access. Leave all - permissions unchecked for full access. Generating a new token rotates the old one. - - - - {agentUserTokenScopes.map((scope) => ( - handleToggleUserScope(scope.id)} - /> - )} - label={scope.label} - /> - ))} - - - {userTokenMeta.hasToken && ( - - Token issued {userTokenMeta.createdAt ? new Date(userTokenMeta.createdAt).toLocaleString() : 'recently'}. - {' '} - {userTokenIsFullAccess ? 'Mode: Full access (all API permissions).' : 'Mode: Scoped permissions.'} - {' '} - Revoke to rotate. - - )} - - - - {userTokenValue && ( - - - - navigator.clipboard.writeText(userTokenValue)}> - - - - - )} - - - - )} - - - - - - - - - - Runtime Tokens - {adminTokensInstallation?.displayName - ? ` β€’ ${adminTokensInstallation.displayName}` - : adminTokensInstallation?.agentName - } - - - {adminTokensInstallation?.runtimeTokens?.length ? ( - - {adminTokensInstallation.runtimeTokens.map((token) => ( - - - - - {token.label || 'Untitled token'} - - - Created {formatDateTime(token.createdAt)} β€’ Last used {formatDateTime(token.lastUsedAt)} - - - - - - ))} - - ) : ( - No runtime tokens for this installation. - )} - - - - - - - - Uninstall agent instance - - - This will remove the agent from the pod and revoke access for this installation. - - {adminUninstallTarget && ( - - {adminUninstallTarget.displayName || adminUninstallTarget.agentName} - {' β€’ '} - {adminUninstallTarget.instanceId || 'default'} - {' β€’ '} - {adminUninstallTarget.pod?.name || 'Unknown pod'} - - )} - - - - - - - - setRuntimeLogsOpen(false)} fullWidth maxWidth="md"> - Runtime Logs - - - setRuntimeLogsLines(Number(event.target.value) || 200)} - size="small" - sx={{ width: 120 }} - /> - setRuntimeLogsFilter(event.target.value)} - size="small" - sx={{ minWidth: 220 }} - placeholder={configAgent?.instanceId || 'default'} - /> - - setRuntimeLogsAutoRefresh(event.target.checked)} - /> - )} - label="Live" - /> - - - {runtimeLogsError && ( - {runtimeLogsError} - )} - { - if (runtimeLogsLoading) return 'Loading logs...'; - if (!runtimeLogsFilter) return runtimeLogsContent; - const needle = runtimeLogsFilter.trim(); - if (!needle) return runtimeLogsContent; - return runtimeLogsContent - .split('\n') - .filter((line) => line.includes(`[${needle}]`) || line.includes(needle)) - .join('\n'); - })()} - InputProps={{ readOnly: true }} - inputRef={runtimeLogsInputRef} - /> - - - - - - - - Skills Catalog - - {skillsCatalogLoading && Loading catalog...} - {skillsCatalogError && {skillsCatalogError}} - {!skillsCatalogLoading && skillsCatalogItems.length === 0 && ( - No skills available yet. - )} - - {skillsCatalogItems.map((item) => ( - - - - {item.name} - - {item.description || 'No description'} - - {item.license && ( - openLicenseDialog(item)} - /> - )} - {item.tags?.length ? ( - - {item.tags.map((tag) => ( - - ))} - - ) : null} - - - - {item.license && ( - - )} - - - - - ))} - - - - - - Selected Skill - - {skillsImportNotice && ( - - {skillsImportNotice} - - )} - setSkillsImportState((prev) => ({ ...prev, name: e.target.value }))} - sx={{ mb: 2 }} - /> - setSkillsImportState((prev) => ({ ...prev, description: e.target.value }))} - sx={{ mb: 2 }} - /> - setSkillsImportState((prev) => ({ ...prev, content: e.target.value }))} - multiline - minRows={4} - sx={{ mb: 2 }} - /> - setSkillsImportState((prev) => ({ ...prev, tags: e.target.value }))} - sx={{ mb: 2 }} - /> - setSkillsImportState((prev) => ({ ...prev, sourceUrl: e.target.value }))} - sx={{ mb: 2 }} - /> - setSkillsImportState((prev) => ({ ...prev, license: e.target.value }))} - sx={{ mb: 2 }} - /> - - - - - - - - - {licenseState.title} - - {licenseState.path && ( - - {licenseState.path} - - )} - - - - - - - - - Select pods for install - - setInstallInstanceName(e.target.value)} - size="small" - sx={{ mb: 2 }} - helperText="Give this bot a unique name (e.g., Cuz, Como). Use @name to mention in chat." - /> - {/* Show existing agent info if found */} - {checkingExisting && ( - - Checking for existing agent... - - )} - {existingAgentInfo && !checkingExisting && ( - - - This agent already exists in {existingAgentInfo.installations?.length || 0} pod(s) - - - {existingAgentInfo.installations?.map((i) => i.podName).join(', ')} - - - Installing to new pods will share the same agent identity and memory. - {existingAgentInfo.hasRuntimeToken && ' Runtime token already provisioned.'} - - - )} - setInstallInstanceId(e.target.value)} - size="small" - sx={{ mb: 2 }} - helperText={`Derived from name: "${deriveInstanceId(installInstanceName, installAgent?.agentName || installAgent?.name)}"`} - /> - {/* Keyed on the AGENT, not the removed presets flow β€” the old - condition read installPresetContext, which nothing sets since the - Presets tab retired, so webhook installs lost their URL fields. */} - {((installAgent?.agentName || installAgent?.name || '').toLowerCase() === 'webhook') && ( - <> - - - Webhook endpoint - - - Commonly will POST events (mentions, messages) to this URL. The agent posts replies back via the bot API using its token. - - setInstallWebhookUrl(e.target.value)} - size="small" - sx={{ mb: 2 }} - helperText="Leave blank to use pull-mode polling instead" - /> - setInstallWebhookSecret(e.target.value)} - size="small" - type="password" - sx={{ mb: 2 }} - helperText="Commonly signs payloads as X-Commonly-Signature: sha256=" - /> - - )} - {/* The gateway-mode and per-agent LLM-key sections that lived here - were moltbot-era config (#1050 parked that tier); no catalog row - reaches them anymore, and rendering them for native/BYO installs - was exactly the broken config UI Sam flagged. Phase 1 of the - persona plan replaces this dialog whole. */} - {(installAgent?.agentName || installAgent?.name || '').toLowerCase() === 'openclaw' && ( - <> - - - Workspace skill sync - - - This installation syncs imported pod skills into the agent workspace. - - - )} - {accessiblePods.length === 0 && userPods.length === 0 ? ( - Join or create a pod to install agents. - ) : ( - - {(accessiblePods.length > 0 ? accessiblePods : userPods).map((pod) => ( - { - setInstallPodIds((prev) => { - if (e.target.checked) { - return Array.from(new Set([...prev, pod._id])); - } - return prev.filter((id) => id !== pod._id); - }); - }} - /> - )} - label={`${pod.name}${isPodAdmin(pod) ? ' (Admin)' : ''}`} - /> - ))} - - )} - - - - - - - - - Create gateway - - {installGatewayCreateError && ( - - {installGatewayCreateError} - - )} - setInstallGatewayForm((prev) => ({ ...prev, name: event.target.value }))} - size="small" - sx={{ mb: 2 }} - /> - setInstallGatewayForm((prev) => ({ ...prev, slug: event.target.value }))} - size="small" - sx={{ mb: 2 }} - helperText="Defaults to a slugified version of the name." - /> - - setInstallGatewayForm((prev) => ({ ...prev, namespace: event.target.value }))} - size="small" - /> - - - - - - - - - Create Agent - - - Agent Type - - - setCreateAgentName(e.target.value)} - size="small" - sx={{ mb: 2 }} - helperText="This shows as the card title." - /> - setCreateAgentDescription(e.target.value)} - size="small" - multiline - rows={3} - sx={{ mb: 2 }} - /> - - - - - - - {!createAgentName && ( - - Enter an agent name first to generate an AI avatar - - )} - - Visibility - - - - - - - - - - - Edit Agent - - setEditTemplate((prev) => ({ ...prev, displayName: e.target.value }))} - size="small" - sx={{ mb: 2 }} - /> - setEditTemplate((prev) => ({ ...prev, description: e.target.value }))} - size="small" - multiline - rows={3} - sx={{ mb: 2 }} - /> - - - - - - Visibility - - - - - - - - - - - {/* AI Avatar Generator Modal */} -
- ); -}; - -export default AgentsHub; diff --git a/frontend/src/components/agents/AvatarGenerator.tsx b/frontend/src/components/agents/AvatarGenerator.tsx deleted file mode 100644 index 7dd7478c23..0000000000 --- a/frontend/src/components/agents/AvatarGenerator.tsx +++ /dev/null @@ -1,455 +0,0 @@ -import React, { useState } from 'react'; -import { - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Button, - Select, - MenuItem, - FormControl, - InputLabel, - TextField, - CircularProgress, - Box, - Typography, - IconButton, - Grid, - Paper, - ToggleButtonGroup, - ToggleButton, -} from '@mui/material'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import axios from 'axios'; - -interface StyleOption { - value: string; - label: string; - description: string; -} - -interface PersonalityOption { - value: string; - label: string; - emoji: string; -} - -interface GenderOption { - value: string; - label: string; -} - -interface ColorSchemeOption { - value: string; - label: string; - description: string; -} - -interface GenerationInfo { - source?: string; - model?: string; - fallbackUsed?: boolean; -} - -interface AvatarSelection { - style: string; - personality: string; - colorScheme: string; - gender: string; - customPrompt: string; - generationInfo: GenerationInfo | null; -} - -interface AvatarGeneratorProps { - open: boolean; - onClose: () => void; - onSelect: (avatar: string | null, selection: AvatarSelection) => void; - agentName?: string; - targetType?: 'agent' | 'user'; -} - -const AvatarGenerator: React.FC = ({ - open, - onClose, - onSelect, - agentName, - targetType = 'agent', -}) => { - const [loading, setLoading] = useState(false); - const [avatar, setAvatar] = useState(null); - const [style, setStyle] = useState('realistic'); - const [personality, setPersonality] = useState('friendly'); - const [colorScheme, setColorScheme] = useState('vibrant'); - const [gender, setGender] = useState('neutral'); - const [promptPreset, setPromptPreset] = useState('professional'); - const [customPrompt, setCustomPrompt] = useState('professional profile portrait, natural lighting, clean studio backdrop'); - const [generationInfo, setGenerationInfo] = useState(null); - - const styles: StyleOption[] = [ - { - value: 'realistic', - label: 'Photo Realistic', - description: 'Natural profile-photo look', - }, - { - value: 'anime', - label: 'Anime Portrait', - description: 'Stylized character portrait', - }, - { - value: 'cartoon', - label: 'Illustrated', - description: 'Friendly drawn portrait', - }, - { - value: 'game', - label: 'Game Character', - description: 'Game-inspired character portrait', - }, - { - value: 'minimalist', - label: 'Minimal Portrait', - description: 'Simple clean portrait', - }, - ]; - - const personalities: PersonalityOption[] = [ - { value: 'friendly', label: 'Friendly', emoji: '😊' }, - { value: 'professional', label: 'Professional', emoji: 'πŸ’Ό' }, - { value: 'playful', label: 'Playful', emoji: 'πŸŽ‰' }, - { value: 'wise', label: 'Wise', emoji: 'πŸ¦‰' }, - { value: 'creative', label: 'Creative', emoji: '🎨' }, - ]; - - const genders: GenderOption[] = [ - { value: 'neutral', label: 'Neutral' }, - { value: 'female', label: 'Female' }, - { value: 'male', label: 'Male' }, - ]; - - const colorSchemes: ColorSchemeOption[] = [ - { - value: 'vibrant', - label: 'Vibrant', - description: 'Bright, saturated colors', - }, - { - value: 'pastel', - label: 'Pastel', - description: 'Soft, muted tones', - }, - { - value: 'monochrome', - label: 'Monochrome', - description: 'Black, white, and grays', - }, - { - value: 'neon', - label: 'Neon', - description: 'Electric glowing colors', - }, - ]; - - const promptPresets: Record = { - professional: 'professional profile portrait, natural lighting, clean studio backdrop', - friendly: 'friendly approachable portrait, soft smile, warm lighting, clean background', - creator: 'creative modern portrait, expressive but natural look, soft cinematic light', - executive: 'confident executive portrait, neat attire, premium neutral background', - }; - - const handlePresetChange = (_: React.MouseEvent, value: string): void => { - if (!value) return; - setPromptPreset(value); - setCustomPrompt(promptPresets[value]); - }; - - const generateAvatar = async (): Promise => { - setLoading(true); - try { - const response = await axios.post('/api/registry/generate-avatar', { - agentName, - style, - personality, - colorScheme, - gender, - customPrompt, - }); - setAvatar(response.data.avatar); - setGenerationInfo(response.data.metadata || null); - } catch (error) { - console.error('Failed to generate avatar:', error); - const err = error as { response?: { data?: { error?: string } } }; - alert( - err.response?.data?.error - || 'Failed to generate avatar. Please try again.', - ); - } finally { - setLoading(false); - } - }; - - const handleConfirm = (): void => { - onSelect(avatar, { - style, - personality, - colorScheme, - gender, - customPrompt, - generationInfo, - }); - onClose(); - }; - - const handleClose = (): void => { - setAvatar(null); - setGenerationInfo(null); - onClose(); - }; - - return ( - - - {targetType === 'user' ? 'Generate Your AI Avatar' : 'Generate Agent AI Avatar'} - - - - - - - Professional - Friendly - Creator - Executive - - - - {/* Style Selector */} - - - Portrait Style - - - - - {/* Personality Selector */} - - - Personality - - - - - {/* Gender Selector */} - - - Gender - - - - - {/* Color Scheme Selector */} - - - Color Scheme - - - - - - setCustomPrompt(e.target.value)} - helperText="Base human-portrait instructions are always applied. Add hairstyle, clothing, mood, background, and vibe." - /> - - - - {/* Avatar Preview */} - - {loading ? ( - - - - Generating your unique avatar... - - - ) : avatar ? ( - - - Generated avatar - - - - Love it? Use it! Want a different style? - - - - - - {generationInfo ? ( - - Source: {generationInfo.source || 'unknown'} - {generationInfo.model ? ` (${generationInfo.model})` : ''} - {generationInfo.fallbackUsed ? ' - fallback mode' : ''} - - ) : null} - - ) : ( - - - Ready to create? - - - Choose a portrait preset and click Generate. - - - )} - - - - - - - - - - ); -}; - -export default AvatarGenerator; diff --git a/frontend/src/components/agents/ClawdbotConfigPanel.tsx b/frontend/src/components/agents/ClawdbotConfigPanel.tsx deleted file mode 100644 index cb0728d532..0000000000 --- a/frontend/src/components/agents/ClawdbotConfigPanel.tsx +++ /dev/null @@ -1,449 +0,0 @@ -import React, { useState } from 'react'; -import { - Box, - Typography, - Button, - TextField, - IconButton, - Tooltip, - Chip, - Paper, - Divider, - Collapse, - Alert, - FormGroup, - FormControlLabel, - Checkbox, -} from '@mui/material'; -import { - ContentCopy as CopyIcon, - ExpandMore as ExpandMoreIcon, - ExpandLess as ExpandLessIcon, - CheckCircle as ConnectedIcon, - Cancel as DisconnectedIcon, - Refresh as RefreshIcon, -} from '@mui/icons-material'; -import { alpha, useTheme } from '@mui/material/styles'; - -interface RuntimeToken { - id: string; - label?: string; - createdAt: string; - lastUsedAt?: string; -} - -interface GatewayStatus { - connected?: boolean; - channels?: string[]; -} - -interface UserTokenMeta { - hasToken?: boolean; - createdAt?: string; -} - -interface ClawdbotConfigPanelProps { - runtimeTokens?: RuntimeToken[]; - runtimeTokenValue?: string; - onGenerateToken?: () => void; - onRevokeToken?: (id: string) => void; - tokenLoading?: boolean; - gatewayStatus?: GatewayStatus; - onRefreshStatus?: () => void; - userTokenValue?: string; - userTokenMeta?: UserTokenMeta; - userTokenScopes?: string[]; - userTokenLoading?: boolean; - userTokenRevoking?: boolean; - onToggleUserScope?: (scopeId: string) => void; - onGenerateUserToken?: () => void; - onRevokeUserToken?: () => void; -} - -/** - * ClawdbotConfigPanel - Configuration panel for Clawdbot integration - * - * Shows: - * 1. Commonly-hosted Clawdbot status - * 2. "Connect your own Clawdbot" with token + config snippet - * 3. Channel status (future) - */ -const ClawdbotConfigPanel: React.FC = ({ - runtimeTokens = [], - runtimeTokenValue, - onGenerateToken, - onRevokeToken, - tokenLoading, - gatewayStatus, - onRefreshStatus, - userTokenValue, - userTokenMeta, - userTokenScopes = [], - userTokenLoading, - userTokenRevoking, - onToggleUserScope, - onGenerateUserToken, - onRevokeUserToken, -}) => { - const theme = useTheme(); - const [copied, setCopied] = useState(false); - const [configCopied, setConfigCopied] = useState(false); - const [showConfigSnippet, setShowConfigSnippet] = useState(false); - - const copyToClipboard = async (text: string, setCopiedFn: React.Dispatch>): Promise => { - try { - await navigator.clipboard.writeText(text); - setCopiedFn(true); - setTimeout(() => setCopiedFn(false), 2000); - } catch (err) { - console.error('Failed to copy:', err); - } - }; - - const generateConfigSnippet = (runtimeToken: string | undefined, userToken: string | undefined): string => { - return `{ - "channels": { - "commonly": { - "enabled": true, - "baseUrl": "${window.location.origin}", - "runtimeToken": "${runtimeToken || ''}", - "userToken": "${userToken || ''}", - "agentName": "openclaw", - "instanceId": "default" - } - } -}`; - }; - - const activeToken = runtimeTokenValue || (runtimeTokens.length > 0 ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : null); - const hasUserToken = userTokenMeta?.hasToken; - - return ( - - {/* Section Header */} - - Clawdbot Integration - - - {/* Commonly-hosted Clawdbot Status */} - - - - Commonly-hosted Clawdbot - - - {gatewayStatus?.connected ? ( - } - label="Connected" - size="small" - color="success" - variant="outlined" - /> - ) : ( - } - label="Not configured" - size="small" - color="default" - variant="outlined" - /> - )} - - - - - - - - - Shared AI assistant powered by Clawdbot. Responds to events in this pod. - - - {gatewayStatus?.channels && gatewayStatus.channels.length > 0 && ( - - {gatewayStatus.channels.map((channel) => ( - - ))} - - )} - - - - - {/* Connect Your Own Clawdbot */} - - Connect Your Own Clawdbot - - - Run Clawdbot on your machine with your personal channels (Gmail, Discord, etc.) - and connect it to this pod. - - - {/* Runtime Token Section */} - {activeToken ? ( - - - Your Runtime Token - - - - {runtimeTokenValue || '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022'} - - {runtimeTokenValue && ( - - copyToClipboard(runtimeTokenValue, setCopied)} - > - - - - )} - - - {runtimeTokenValue && ( - - - Save this token now - it will not be shown again. - - - )} - - ) : ( - - )} - - {/* Config Snippet */} - - - - - - - - - copyToClipboard( - generateConfigSnippet(runtimeTokenValue, userTokenValue), - setConfigCopied, - ) - } - sx={{ position: 'absolute', top: 8, right: 8 }} - > - - - - - - Add this to your ~/.clawdbot/moltbot.json to connect your Clawdbot to Commonly. - Runtime token is required; user token is optional for tools/context access. - - - - - {/* Existing Tokens */} - {runtimeTokens.length > 0 && ( - - - Active Connections ({runtimeTokens.length}) - - {runtimeTokens.map((token) => ( - - - - {token.label || 'Unnamed'} - - - Created {new Date(token.createdAt).toLocaleDateString()} - {token.lastUsedAt && ` \u2022 Last used ${new Date(token.lastUsedAt).toLocaleDateString()}`} - - - - - ))} - - )} - - - - - Designated User Token - - - Issue a bot user token with optional permissions. Leave all permissions unchecked for full access. - Generating a new token rotates the old one. - - - - {[ - { id: 'agent:events:read', label: 'Read agent events' }, - { id: 'agent:events:ack', label: 'Acknowledge agent events' }, - { id: 'agent:context:read', label: 'Read pod context' }, - { id: 'agent:messages:read', label: 'Read pod messages' }, - { id: 'agent:messages:write', label: 'Post pod messages' }, - ].map((scope) => ( - onToggleUserScope?.(scope.id)} - size="small" - /> - )} - label={{scope.label}} - /> - ))} - - - {hasUserToken && ( - - - Token issued {userTokenMeta?.createdAt ? new Date(userTokenMeta.createdAt).toLocaleString() : 'recently'}. - Revoke to rotate. - - - )} - - - - {userTokenValue && ( - - - User Token (shown once) - - - - {userTokenValue} - - - copyToClipboard(userTokenValue, setCopied)} - > - - - - - - )} - - - - ); -}; - -export default ClawdbotConfigPanel; diff --git a/frontend/src/components/agents/PersonalityBuilder.tsx b/frontend/src/components/agents/PersonalityBuilder.tsx deleted file mode 100644 index 1ad8fa8650..0000000000 --- a/frontend/src/components/agents/PersonalityBuilder.tsx +++ /dev/null @@ -1,503 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { - Box, - Typography, - FormControl, - InputLabel, - Select, - MenuItem, - TextField, - Chip, - Button, - Paper, - Grid, - Stack, - Divider, - Alert, - Accordion, - AccordionSummary, - AccordionDetails, - CircularProgress, -} from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'; - -interface ToneOption { - value: string; - label: string; - description: string; - example: string; -} - -interface BehaviorOption { - value: string; - label: string; - description: string; - icon: string; -} - -interface ResponseStyleOption { - value: string; - label: string; - description: string; - icon: string; -} - -interface PersonalityConfig { - tone: string; - interests: string[]; - behavior: string; - responseStyle: string; - specialties: string[]; - boundaries: string[]; - customInstructions: string; -} - -interface GeneratedConfig { - tone?: string; - specialties?: string[]; - boundaries?: string[]; - customInstructions?: string; -} - -interface PersonalityBuilderProps { - initialConfig?: Partial; - onSave: (config: PersonalityConfig) => void; - agentName?: string; - onAutoGenerate?: () => Promise; - autoGenerating?: boolean; -} - -/** - * Enhanced PersonalityBuilder Component - * - * Provides an intuitive UI for configuring agent personality with: - * - Tone selection with descriptions - * - Interest tags - * - Behavior patterns - * - Response styles - * - Auto-generation option - */ -const PersonalityBuilder: React.FC = ({ - initialConfig = {}, - onSave, - agentName, - onAutoGenerate, - autoGenerating = false, -}) => { - const [config, setConfig] = useState({ - tone: 'friendly', - interests: [], - behavior: 'reactive', - responseStyle: 'conversational', - specialties: [], - boundaries: [], - customInstructions: '', - ...initialConfig, - }); - - const [interestInput, setInterestInput] = useState(''); - const [specialtyInput, setSpecialtyInput] = useState(''); - const [boundaryInput, setBoundaryInput] = useState(''); - - useEffect(() => { - setConfig({ ...config, ...initialConfig }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialConfig]); - - const tones: ToneOption[] = [ - { - value: 'friendly', - label: 'Friendly 😊', - description: 'Warm, welcoming, and approachable. Uses casual language and emojis occasionally.', - example: '"Hey! I\'d love to help you with that. Let me know what you need!"', - }, - { - value: 'professional', - label: 'Professional πŸ’Ό', - description: 'Polite, formal, and competent. Provides well-structured, business-appropriate responses.', - example: '"I would be happy to assist you with this matter. Please provide the relevant details."', - }, - { - value: 'sarcastic', - label: 'Sarcastic 😏', - description: 'Witty and humorous with a sarcastic edge. Keeps things entertaining while being helpful.', - example: '"Oh sure, because that\'s definitely going to work... just kidding! Let me show you the right way."', - }, - { - value: 'educational', - label: 'Educational πŸ“š', - description: 'Knowledgeable teacher style. Explains concepts clearly with examples and context.', - example: '"Let me break this down for you. Think of it like... Now you try applying that concept."', - }, - { - value: 'humorous', - label: 'Humorous πŸ˜‚', - description: 'Funny and entertaining. Makes people laugh while providing helpful information.', - example: '"Why did the function go to therapy? It had too many issues! But seriously, here\'s the fix..."', - }, - ]; - - const behaviors: BehaviorOption[] = [ - { - value: 'reactive', - label: 'Reactive', - description: 'Only responds when directly mentioned or asked a question. Waits for user input.', - icon: '⏸️', - }, - { - value: 'proactive', - label: 'Proactive', - description: 'Actively participates in discussions and shares relevant insights without being asked.', - icon: '⚑', - }, - { - value: 'balanced', - label: 'Balanced', - description: 'Responds to mentions and occasionally contributes to relevant discussions.', - icon: 'βš–οΈ', - }, - ]; - - const responseStyles: ResponseStyleOption[] = [ - { - value: 'concise', - label: 'Concise', - description: 'Brief and to the point (1-2 sentences)', - icon: 'πŸ“', - }, - { - value: 'detailed', - label: 'Detailed', - description: 'Comprehensive, well-explained responses', - icon: 'πŸ“–', - }, - { - value: 'conversational', - label: 'Conversational', - description: 'Natural, friendly conversational style', - icon: 'πŸ’¬', - }, - ]; - - const addInterest = (): void => { - if (interestInput && !config.interests.includes(interestInput)) { - setConfig({ - ...config, - interests: [...config.interests, interestInput], - }); - setInterestInput(''); - } - }; - - const removeInterest = (interest: string): void => { - setConfig({ - ...config, - interests: config.interests.filter((i) => i !== interest), - }); - }; - - const addSpecialty = (): void => { - if (specialtyInput && !config.specialties.includes(specialtyInput)) { - setConfig({ - ...config, - specialties: [...config.specialties, specialtyInput], - }); - setSpecialtyInput(''); - } - }; - - const removeSpecialty = (specialty: string): void => { - setConfig({ - ...config, - specialties: config.specialties.filter((s) => s !== specialty), - }); - }; - - const addBoundary = (): void => { - if (boundaryInput && !config.boundaries.includes(boundaryInput)) { - setConfig({ - ...config, - boundaries: [...config.boundaries, boundaryInput], - }); - setBoundaryInput(''); - } - }; - - const removeBoundary = (boundary: string): void => { - setConfig({ - ...config, - boundaries: config.boundaries.filter((b) => b !== boundary), - }); - }; - - const handleAutoGenerate = async (): Promise => { - if (onAutoGenerate) { - const generated = await onAutoGenerate(); - if (generated) { - setConfig({ - ...config, - tone: generated.tone || config.tone, - specialties: generated.specialties || config.specialties, - boundaries: generated.boundaries || config.boundaries, - customInstructions: generated.customInstructions || config.customInstructions, - }); - } - } - }; - - const selectedTone = tones.find((t) => t.value === config.tone); - - return ( - - - Configure Personality - {onAutoGenerate && ( - - )} - - - - {/* Tone Selector */} - - - - Communication Tone - - - Tone - - - {selectedTone && ( - - - {selectedTone.description} - - - - Example: {selectedTone.example} - - - - )} - - - - {/* Behavior & Response Style */} - - - - Behavior Pattern - - - - - - - - - - - Response Style - - - - - - - - {/* Interests & Topics */} - - - - Interests & Topics - - - Topics this agent cares about and enjoys discussing - - - {config.interests.map((interest) => ( - removeInterest(interest)} - color="primary" - variant="outlined" - /> - ))} - - - setInterestInput(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && addInterest()} - /> - - - - - - {/* Advanced Configuration (Accordion) */} - - - }> - - Advanced Configuration - - - - - {/* Specialties */} - - - Specialties (What the agent excels at) - - - {config.specialties.map((specialty) => ( - removeSpecialty(specialty)} - color="success" - size="small" - /> - ))} - - - setSpecialtyInput(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && addSpecialty()} - /> - - - - - - - {/* Boundaries */} - - - Boundaries (What the agent won't do) - - - {config.boundaries.map((boundary) => ( - removeBoundary(boundary)} - color="error" - size="small" - variant="outlined" - /> - ))} - - - setBoundaryInput(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && addBoundary()} - /> - - - - - - - {/* Custom Instructions */} - - - Custom Instructions - - - setConfig({ ...config, customInstructions: e.target.value }) - } - variant="outlined" - /> - - - - - - - - - - - - ); -}; - -export default PersonalityBuilder; diff --git a/frontend/src/components/agents/index.ts b/frontend/src/components/agents/index.ts deleted file mode 100644 index 4b7deddfba..0000000000 --- a/frontend/src/components/agents/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { default as AgentsHub } from './AgentsHub'; -export { default as AgentCard } from './AgentCard'; -export { default as ClawdbotConfigPanel } from './ClawdbotConfigPanel'; -export { default as AgentEnsemblePanel } from './AgentEnsemblePanel'; diff --git a/frontend/src/components/common/UnifiedComposer.tsx b/frontend/src/components/common/UnifiedComposer.tsx deleted file mode 100644 index 1f78927277..0000000000 --- a/frontend/src/components/common/UnifiedComposer.tsx +++ /dev/null @@ -1,470 +0,0 @@ -import React, { useState, useRef, useCallback } from 'react'; -import { - Box, - TextField, - IconButton, - Button, - Paper, - List, - ListItem, - ListItemAvatar, - ListItemText, - Avatar, - Typography, - Chip, - Popper, - ClickAwayListener, - Divider, - alpha, - useTheme, -} from '@mui/material'; -import { - Send as SendIcon, - AttachFile as AttachIcon, - EmojiEmotions as EmojiIcon, - SmartToy as AgentIcon, - Code as CodeIcon, - Search as SearchIcon, - Summarize as SummarizeIcon, - CheckCircle as CheckIcon, -} from '@mui/icons-material'; -import { SvgIconComponent } from '@mui/icons-material'; - -interface QuickAction { - command: string; - label: string; - description: string; - icon: SvgIconComponent; - agentRequired: boolean; -} - -const quickActions: QuickAction[] = [ - { - command: '/summarize', - label: 'Summarize', - description: 'Summarize recent discussion', - icon: SummarizeIcon, - agentRequired: true, - }, - { - command: '/search', - label: 'Search', - description: 'Search pod memory', - icon: SearchIcon, - agentRequired: true, - }, - { - command: '/review', - label: 'Review', - description: 'Request code review', - icon: CodeIcon, - agentRequired: true, - }, - { - command: '/task', - label: 'Create Task', - description: 'Create a task from message', - icon: CheckIcon, - agentRequired: false, - }, -]; - -interface Member { - id?: string; - username?: string; - name?: string; - [key: string]: unknown; -} - -interface Agent { - id?: string; - name?: string; - displayName?: string; - [key: string]: unknown; -} - -type Participant = - | (Member & { type: 'human' }) - | (Agent & { type: 'agent'; name: string }); - -interface SendPayload { - content: string; - mentions: (Participant | undefined)[]; - command: { name: string; label: string } | null; -} - -interface UnifiedComposerProps { - onSend: (payload: SendPayload) => void; - onFileUpload?: () => void; - members?: Member[]; - agents?: Agent[]; - placeholder?: string; - disabled?: boolean; - showFileUpload?: boolean; - showEmoji?: boolean; -} - -const UnifiedComposer: React.FC = ({ - onSend, - onFileUpload, - members = [], - agents = [], - placeholder = 'Type a message... or @mention', - disabled = false, - showFileUpload = true, - showEmoji = true, -}) => { - const theme = useTheme(); - const inputRef = useRef(null); - const [message, setMessage] = useState(''); - const [mentionAnchor, setMentionAnchor] = useState(null); - const [mentionQuery, setMentionQuery] = useState(''); - const [mentionType, setMentionType] = useState<'mention' | 'command' | null>(null); - const [selectedIndex, setSelectedIndex] = useState(0); - - const allParticipants: Participant[] = [ - ...members.map((m): Participant => ({ ...m, type: 'human' as const })), - ...agents.map((a): Participant => ({ ...a, type: 'agent' as const, name: a.displayName || a.name || '' })), - ]; - - const filteredParticipants = mentionQuery - ? allParticipants.filter((p) => - ((p.name as string | undefined) || (p as Member).username || '').toLowerCase().includes(mentionQuery.toLowerCase()) - ) - : allParticipants; - - const filteredActions = mentionQuery - ? quickActions.filter( - (a) => - a.command.toLowerCase().includes(mentionQuery.toLowerCase()) || - a.label.toLowerCase().includes(mentionQuery.toLowerCase()) - ) - : quickActions; - - const handleChange = (e: React.ChangeEvent): void => { - const value = e.target.value; - setMessage(value); - - const atMatch = value.match(/@(\w*)$/); - if (atMatch) { - setMentionAnchor(inputRef.current); - setMentionQuery(atMatch[1]); - setMentionType('mention'); - setSelectedIndex(0); - return; - } - - const slashMatch = value.match(/\/(\w*)$/); - if (slashMatch && (value === slashMatch[0] || value.endsWith(' ' + slashMatch[0]))) { - setMentionAnchor(inputRef.current); - setMentionQuery(slashMatch[1]); - setMentionType('command'); - setSelectedIndex(0); - return; - } - - setMentionAnchor(null); - setMentionType(null); - }; - - const handleSelect = useCallback((item: Participant | QuickAction): void => { - if (mentionType === 'mention') { - const p = item as Participant; - const name = (p as Member).username || p.name; - const newMessage = message.replace(/@\w*$/, `@${name} `); - setMessage(newMessage); - } else if (mentionType === 'command') { - const action = item as QuickAction; - const newMessage = message.replace(/\/\w*$/, `${action.command} `); - setMessage(newMessage); - } - - setMentionAnchor(null); - setMentionType(null); - (inputRef.current?.querySelector('textarea') as HTMLTextAreaElement | null)?.focus(); - }, [mentionType, message]); - - const handleSend = useCallback((): void => { - if (!message.trim() || disabled) return; - - const mentionMatches = message.match(/@(\w+)/g) || []; - const mentions = mentionMatches - .map((m) => { - const username = m.slice(1); - return allParticipants.find( - (p) => ((p as Member).username || p.name)?.toLowerCase() === username.toLowerCase() - ); - }) - .filter(Boolean); - - const commandMatch = message.match(/^\/(\w+)/); - const command = commandMatch ? quickActions.find((a) => a.command === `/${commandMatch[1]}`) : null; - - onSend({ - content: message, - mentions, - command: command ? { name: command.command, label: command.label } : null, - }); - - setMessage(''); - }, [message, disabled, allParticipants, onSend]); - - const handleKeyDown = (e: React.KeyboardEvent): void => { - if (mentionAnchor) { - const items: (Participant | QuickAction)[] = - mentionType === 'mention' ? filteredParticipants : filteredActions; - - if (e.key === 'ArrowDown') { - e.preventDefault(); - setSelectedIndex((prev) => (prev + 1) % items.length); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - setSelectedIndex((prev) => (prev - 1 + items.length) % items.length); - } else if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - if (items[selectedIndex]) { - handleSelect(items[selectedIndex]); - } - } else if (e.key === 'Escape') { - setMentionAnchor(null); - setMentionType(null); - } - } else if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSend(); - } - }; - - return ( - - {/* Main composer */} - - {showFileUpload && ( - - - - )} - - - - {showEmoji && ( - - - - )} - - - - - {/* Quick actions hint */} - {agents.length > 0 && ( - - - Quick: - - {quickActions.slice(0, 3).map((action) => ( - setMessage(action.command + ' ')} - sx={{ - height: 20, - fontSize: '0.7rem', - cursor: 'pointer', - '&:hover': { - backgroundColor: alpha(theme.palette.primary.main, 0.08), - }, - }} - /> - ))} - - )} - - {/* Mention/Command popup */} - - setMentionAnchor(null)}> - - {mentionType === 'mention' && ( - <> - - Mention someone - - - - {filteredParticipants.length === 0 ? ( - - - - ) : ( - filteredParticipants.map((participant, index) => ( - handleSelect(participant)} - sx={{ borderRadius: 1, mx: 0.5, my: 0.25 }} - > - - - {participant.type === 'agent' - ? 'πŸ€–' - : (participant.name || (participant as Member).username)?.charAt(0)} - - - - {participant.type === 'agent' && ( - - )} - - )) - )} - - - )} - - {mentionType === 'command' && ( - <> - - Quick actions - - - - {filteredActions.length === 0 ? ( - - - - ) : ( - filteredActions.map((action, index) => ( - handleSelect(action)} - sx={{ borderRadius: 1, mx: 0.5, my: 0.25 }} - > - - - - - - - {action.agentRequired && ( - } - label="Agent" - size="small" - sx={{ - height: 18, - fontSize: '0.625rem', - '& .MuiChip-icon': { ml: 0.5 }, - }} - /> - )} - - )) - )} - - - )} - - - - - ); -}; - -export default UnifiedComposer; diff --git a/frontend/src/components/common/index.ts b/frontend/src/components/common/index.ts deleted file mode 100644 index 8e9785b602..0000000000 --- a/frontend/src/components/common/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { default as AgentIndicator } from './AgentIndicator'; -export { - AgentBadge, - AgentAvatar, - AgentName, - AgentTypeIndicator, - ActivitySourceBadge, - isAgentUsername, - getAgentInfo, -} from './AgentIndicator'; - -export { default as UnifiedComposer } from './UnifiedComposer'; diff --git a/frontend/src/components/pods/PodHeader.tsx b/frontend/src/components/pods/PodHeader.tsx deleted file mode 100644 index a9006f1fee..0000000000 --- a/frontend/src/components/pods/PodHeader.tsx +++ /dev/null @@ -1,382 +0,0 @@ -/** - * PodHeader Component - * - * Header for a pod showing members (humans + agents), stats, and actions. - * Key element of the hybrid social experience. - */ - -import React, { useState } from 'react'; -import { - Box, - Typography, - Avatar, - AvatarGroup, - Button, - IconButton, - Chip, - Menu, - MenuItem, - ListItemIcon, - ListItemText, - Tooltip, - alpha, - useTheme, -} from '@mui/material'; -import { - Settings as SettingsIcon, - Add as AddIcon, - SmartToy as AgentIcon, - Person as PersonIcon, - MoreVert as MoreIcon, - Link as LinkIcon, - Memory as MemoryIcon, - AutoAwesome as SkillIcon, - Article as SummaryIcon, - Share as ShareIcon, - Notifications as NotificationsIcon, - NotificationsOff as MuteIcon, -} from '@mui/icons-material'; - -interface PodData { - name: string; - description?: string; - type: string; - icon?: string; -} - -interface PodMember { - id: string; - name?: string; - type?: string; - online?: boolean; - color?: string; -} - -interface PodAgent { - id: string; - name?: string; - status?: string; -} - -interface PodStats { - memoryCount?: number; - skillCount?: number; - summaryCount?: number; - linkedPods?: number; -} - -interface PodHeaderProps { - pod: PodData; - members?: PodMember[]; - agents?: PodAgent[]; - stats?: PodStats; - onAddAgent?: () => void; - onInviteMember?: () => void; - onSettings?: () => void; - onLinkPod?: () => void; -} - -interface StatItemProps { - icon: React.ReactNode; - label: string; - value: number; - unit?: string; - action?: React.ReactNode; -} - -const PodHeader: React.FC = ({ - pod, - members = [], - agents = [], - stats = {}, - onAddAgent, - onInviteMember, - onSettings, - onLinkPod, -}) => { - const theme = useTheme(); - const [moreAnchor, setMoreAnchor] = useState(null); - - const { - name, - description, - type, - icon = 'πŸ“¦', - } = pod; - - const humanMembers = members.filter((m) => m.type === 'human'); - const onlineHumans = humanMembers.filter((m) => m.online); - const activeAgents = agents.filter((a) => a.status === 'active'); - - return ( - - {/* Top row: Icon, Name, Actions */} - - {/* Pod icon */} - - {icon} - - - {/* Name and description */} - - - - {name} - - - - - {description} - - - - {/* Actions */} - - - - - - - - - - - - setMoreAnchor(e.currentTarget)}> - - - - - - {/* Members row */} - - {/* Humans */} - - - - - {humanMembers.length} members - - {onlineHumans.length > 0 && ( - - )} - - - {humanMembers.slice(0, 5).map((member) => ( - - - {member.name?.charAt(0)} - - - ))} - - - - - - - {/* Agents */} - - - - - {agents.length} agents - - {activeAgents.length > 0 && ( - - )} - - - {agents.slice(0, 4).map((agent) => ( - - πŸ€– - - ))} - - - - - - {/* Stats row */} - - } - label="Memory" - value={stats.memoryCount || 0} - unit="entries" - /> - } - label="Skills" - value={stats.skillCount || 0} - unit="" - /> - } - label="Summaries" - value={stats.summaryCount || 0} - unit="this week" - /> - } - label="Linked Pods" - value={stats.linkedPods || 0} - unit="" - action={ - - } - /> - - - {/* More menu */} - setMoreAnchor(null)} - > - { setMoreAnchor(null); onLinkPod?.(); }}> - - - - Link to another pod - - - - - - Share pod - - - - - - Mute notifications - - - - ); -}; - -// Stat item component -const StatItem: React.FC = ({ icon, label, value, unit, action }) => { - return ( - - {icon} - - - - {value.toLocaleString()} - - {unit && ( - - {unit} - - )} - - - {label} - - - {action} - - ); -}; - -export default PodHeader; diff --git a/frontend/src/components/pods/index.ts b/frontend/src/components/pods/index.ts deleted file mode 100644 index be3588d11c..0000000000 --- a/frontend/src/components/pods/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as PodHeader } from './PodHeader'; diff --git a/frontend/src/theme/commonlyTheme.ts b/frontend/src/theme/commonlyTheme.ts deleted file mode 100644 index 293ce1c868..0000000000 --- a/frontend/src/theme/commonlyTheme.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { createTheme, alpha, Theme } from '@mui/material/styles'; - -interface TokenColors { - main: string; - light: string; - dark: string; - contrast: string; -} - -interface Tokens { - primary: TokenColors; - secondary: TokenColors; - agents: Record; - integrations: Record; - neutral: Record; - success: string; - warning: string; - error: string; - info: string; - background: { default: string; paper: string; elevated: string; subtle: string }; - shadows: { - sm: string; - md: string; - lg: string; - xl: string; - glow: (color: string) => string; - }; - fonts: { display: string; body: string; mono: string }; - spacing: Record; - radius: Record; - transitions: Record; -} - -const tokens: Tokens = { - primary: { main: '#0d9488', light: '#14b8a6', dark: '#0f766e', contrast: '#ffffff' }, - secondary: { main: '#f59e0b', light: '#fbbf24', dark: '#d97706', contrast: '#000000' }, - agents: { personal: '#8b5cf6', utility: '#06b6d4', analytics: '#ec4899', security: '#ef4444', productivity: '#22c55e' }, - integrations: { discord: '#5865F2', slack: '#4A154B', telegram: '#229ED9', whatsapp: '#25D366', github: '#333333' }, - neutral: { 50: '#fafafa', 100: '#f4f4f5', 200: '#e4e4e7', 300: '#d4d4d8', 400: '#a1a1aa', 500: '#71717a', 600: '#52525b', 700: '#3f3f46', 800: '#27272a', 900: '#18181b', 950: '#09090b' }, - success: '#22c55e', - warning: '#f59e0b', - error: '#ef4444', - info: '#3b82f6', - background: { default: '#fafafa', paper: '#ffffff', elevated: '#ffffff', subtle: '#f4f4f5' }, - shadows: { - sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)', - md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', - lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', - xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)', - glow: (color: string) => `0 0 20px ${alpha(color, 0.3)}`, - }, - fonts: { - display: '"Cal Sans", "Inter", system-ui, sans-serif', - body: '"Inter", system-ui, -apple-system, sans-serif', - mono: '"JetBrains Mono", "Fira Code", monospace', - }, - spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48 }, - radius: { sm: 6, md: 10, lg: 16, xl: 24, full: 9999 }, - transitions: { fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)', normal: '200ms cubic-bezier(0.4, 0, 0.2, 1)', slow: '300ms cubic-bezier(0.4, 0, 0.2, 1)', bounce: '500ms cubic-bezier(0.34, 1.56, 0.64, 1)' }, -}; - -const commonlyTheme: Theme = createTheme({ - palette: { - mode: 'light', - primary: tokens.primary, - secondary: tokens.secondary, - success: { main: tokens.success }, - warning: { main: tokens.warning }, - error: { main: tokens.error }, - info: { main: tokens.info }, - background: tokens.background, - text: { primary: tokens.neutral[900], secondary: tokens.neutral[600], disabled: tokens.neutral[400] }, - divider: tokens.neutral[200], - } as unknown as Theme['palette'], - - typography: { - fontFamily: tokens.fonts.body, - h1: { fontFamily: tokens.fonts.display, fontWeight: 700, fontSize: '2.5rem', lineHeight: 1.2, letterSpacing: '-0.02em' }, - h2: { fontFamily: tokens.fonts.display, fontWeight: 700, fontSize: '2rem', lineHeight: 1.25, letterSpacing: '-0.01em' }, - h3: { fontFamily: tokens.fonts.display, fontWeight: 600, fontSize: '1.5rem', lineHeight: 1.3 }, - h4: { fontFamily: tokens.fonts.display, fontWeight: 600, fontSize: '1.25rem', lineHeight: 1.4 }, - h5: { fontWeight: 600, fontSize: '1.125rem', lineHeight: 1.4 }, - h6: { fontWeight: 600, fontSize: '1rem', lineHeight: 1.5 }, - body1: { fontSize: '1rem', lineHeight: 1.6 }, - body2: { fontSize: '0.875rem', lineHeight: 1.5 }, - caption: { fontSize: '0.75rem', lineHeight: 1.4, color: tokens.neutral[500] }, - button: { fontWeight: 600, textTransform: 'none', letterSpacing: '0.01em' }, - }, - - shape: { borderRadius: tokens.radius.md }, - - shadows: [ - 'none', tokens.shadows.sm, tokens.shadows.sm, tokens.shadows.md, tokens.shadows.md, - tokens.shadows.md, tokens.shadows.lg, tokens.shadows.lg, tokens.shadows.lg, tokens.shadows.lg, - tokens.shadows.xl, tokens.shadows.xl, tokens.shadows.xl, tokens.shadows.xl, tokens.shadows.xl, - tokens.shadows.xl, tokens.shadows.xl, tokens.shadows.xl, tokens.shadows.xl, tokens.shadows.xl, - tokens.shadows.xl, tokens.shadows.xl, tokens.shadows.xl, tokens.shadows.xl, tokens.shadows.xl, - ], - - components: { - MuiCssBaseline: { styleOverrides: { body: { backgroundColor: tokens.background.default, scrollBehavior: 'smooth' }, '::selection': { backgroundColor: alpha(tokens.primary.main, 0.2) }, '::-webkit-scrollbar': { width: 8, height: 8 }, '::-webkit-scrollbar-track': { background: tokens.neutral[100] }, '::-webkit-scrollbar-thumb: { background: tokens.neutral[300], borderRadius: 4 }': {} } }, - MuiButton: { - defaultProps: { disableElevation: true }, - styleOverrides: { - root: { borderRadius: tokens.radius.lg, padding: '10px 20px', fontWeight: 600, transition: tokens.transitions.normal, '&:hover': { transform: 'translateY(-1px)' }, '&:active': { transform: 'translateY(0)' } }, - contained: { '&:hover': { boxShadow: tokens.shadows.md } }, - containedPrimary: { background: `linear-gradient(135deg, ${tokens.primary.main} 0%, ${tokens.primary.dark} 100%)`, '&:hover': { background: `linear-gradient(135deg, ${tokens.primary.light} 0%, ${tokens.primary.main} 100%)` } }, - outlined: { borderWidth: 2, '&:hover': { borderWidth: 2, backgroundColor: alpha(tokens.primary.main, 0.04) } }, - text: { '&:hover': { backgroundColor: alpha(tokens.primary.main, 0.08) } }, - }, - }, - MuiCard: { defaultProps: { elevation: 0 }, styleOverrides: { root: { borderRadius: tokens.radius.lg, border: `1px solid ${tokens.neutral[200]}`, transition: tokens.transitions.normal, '&:hover': { borderColor: tokens.neutral[300], boxShadow: tokens.shadows.md } } } }, - MuiPaper: { defaultProps: { elevation: 0 }, styleOverrides: { root: { backgroundImage: 'none' }, rounded: { borderRadius: tokens.radius.lg }, outlined: { borderColor: tokens.neutral[200] } } }, - MuiChip: { styleOverrides: { root: { borderRadius: tokens.radius.md, fontWeight: 500, transition: tokens.transitions.fast }, filled: { '&:hover': { transform: 'scale(1.02)' } }, outlined: { borderWidth: 1.5 }, colorPrimary: { background: alpha(tokens.primary.main, 0.1), color: tokens.primary.dark, '&:hover': { background: alpha(tokens.primary.main, 0.2) } } } }, - MuiAvatar: { styleOverrides: { root: { fontWeight: 600, fontSize: '1rem' }, rounded: { borderRadius: tokens.radius.md }, colorDefault: { backgroundColor: tokens.primary.main, color: tokens.primary.contrast } } }, - MuiTextField: { defaultProps: { variant: 'outlined', size: 'medium' }, styleOverrides: { root: { '& .MuiOutlinedInput-root': { borderRadius: tokens.radius.md, transition: tokens.transitions.normal, '&:hover .MuiOutlinedInput-notchedOutline': { borderColor: tokens.neutral[400] }, '&.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: tokens.primary.main, borderWidth: 2 } } } } }, - MuiTab: { styleOverrides: { root: { textTransform: 'none', fontWeight: 600, fontSize: '0.9375rem', minHeight: 48, transition: tokens.transitions.normal, '&.Mui-selected': { color: tokens.primary.main } } } }, - MuiTabs: { styleOverrides: { indicator: { height: 3, borderRadius: '3px 3px 0 0' } } }, - MuiListItem: { styleOverrides: { root: { borderRadius: tokens.radius.md, transition: tokens.transitions.fast, '&:hover': { backgroundColor: alpha(tokens.primary.main, 0.04) } } } }, - MuiTooltip: { styleOverrides: { tooltip: { backgroundColor: tokens.neutral[900], fontSize: '0.8125rem', fontWeight: 500, borderRadius: tokens.radius.sm, padding: '8px 12px' }, arrow: { color: tokens.neutral[900] } } }, - MuiBadge: { styleOverrides: { badge: { fontWeight: 600, fontSize: '0.75rem' }, dot: { width: 10, height: 10, borderRadius: '50%' } } }, - MuiDialog: { styleOverrides: { paper: { borderRadius: tokens.radius.xl, boxShadow: tokens.shadows.xl } } }, - MuiMenu: { styleOverrides: { paper: { borderRadius: tokens.radius.lg, boxShadow: tokens.shadows.lg, border: `1px solid ${tokens.neutral[200]}`, marginTop: 8 } } }, - MuiMenuItem: { styleOverrides: { root: { borderRadius: tokens.radius.sm, margin: '2px 8px', padding: '8px 12px', transition: tokens.transitions.fast } } }, - MuiSkeleton: { styleOverrides: { root: { backgroundColor: tokens.neutral[200] }, rounded: { borderRadius: tokens.radius.md } } }, - MuiLinearProgress: { styleOverrides: { root: { borderRadius: tokens.radius.full, height: 6 }, bar: { borderRadius: tokens.radius.full } } }, - MuiSwitch: { styleOverrides: { root: { width: 52, height: 32, padding: 0 }, switchBase: { padding: 4, '&.Mui-checked': { transform: 'translateX(20px)', '& + .MuiSwitch-track': { backgroundColor: tokens.primary.main, opacity: 1 } } }, thumb: { width: 24, height: 24, boxShadow: tokens.shadows.sm }, track: { borderRadius: tokens.radius.full, backgroundColor: tokens.neutral[300], opacity: 1 } } }, - }, -}); - -export { tokens }; -export default commonlyTheme; diff --git a/frontend/src/utils/heartbeatUtils.test.tsx b/frontend/src/utils/heartbeatUtils.test.tsx deleted file mode 100644 index 3b00a4f137..0000000000 --- a/frontend/src/utils/heartbeatUtils.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// @ts-nocheck -import { resolveHeartbeatEditorContent } from './heartbeatUtils'; - -describe('resolveHeartbeatEditorContent', () => { - it('prefers live heartbeat content from workspace', () => { - const value = resolveHeartbeatEditorContent({ - liveContent: '# HEARTBEAT.md\n\nlive', - configChecklist: 'config', - fallback: 'fallback', - }); - expect(value).toBe('# HEARTBEAT.md\n\nlive'); - }); - - it('falls back to saved config checklist when live content is empty', () => { - const value = resolveHeartbeatEditorContent({ - liveContent: ' ', - configChecklist: 'saved config value', - fallback: 'fallback', - }); - expect(value).toBe('saved config value'); - }); - - it('uses fallback when neither live nor saved content is available', () => { - const value = resolveHeartbeatEditorContent({ - liveContent: '', - configChecklist: '', - fallback: 'fallback value', - }); - expect(value).toBe('fallback value'); - }); -}); - diff --git a/frontend/src/utils/heartbeatUtils.ts b/frontend/src/utils/heartbeatUtils.ts deleted file mode 100644 index 0c49d7afa6..0000000000 --- a/frontend/src/utils/heartbeatUtils.ts +++ /dev/null @@ -1,19 +0,0 @@ -interface HeartbeatEditorContentOptions { - liveContent?: string; - configChecklist?: string; - fallback?: string; -} - -export const resolveHeartbeatEditorContent = ({ - liveContent = '', - configChecklist = '', - fallback = '', -}: HeartbeatEditorContentOptions = {}): string => { - const live = String(liveContent || '').trim(); - if (live) return live; - - const config = String(configChecklist || '').trim(); - if (config) return config; - - return fallback; -}; diff --git a/frontend/src/utils/styleUtils.test.tsx b/frontend/src/utils/styleUtils.test.tsx deleted file mode 100644 index e29a45e5c6..0000000000 --- a/frontend/src/utils/styleUtils.test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -// @ts-nocheck -import { forceReflow, applyBodyClass, applyStylesToElements, reloadStylesheets, forceImmediateStyleApplication } from './styleUtils'; - -describe('styleUtils', () => { - test('forceReflow reads offsetHeight', () => { - const el = document.createElement('div'); - - // Mock offsetHeight as a getter - let offsetHeightCalled = false; - Object.defineProperty(el, 'offsetHeight', { - get: jest.fn(() => { - offsetHeightCalled = true; - return 100; - }), - configurable: true - }); - - forceReflow(el); - expect(offsetHeightCalled).toBe(true); - }); - - test('applyBodyClass adds and removes class on body', () => { - const cleanup = applyBodyClass('test-class'); - expect(document.body.classList.contains('test-class')).toBe(true); - cleanup(); - expect(document.body.classList.contains('test-class')).toBe(false); - }); - - test('applyStylesToElements applies class', () => { - const el1 = document.createElement('div'); - el1.className = 't'; - const el2 = document.createElement('div'); - el2.className = 't'; - document.body.appendChild(el1); - document.body.appendChild(el2); - applyStylesToElements('.t'); - expect(el1.classList.contains('style-applied')).toBe(true); - expect(el2.classList.contains('style-applied')).toBe(true); - el1.remove(); - el2.remove(); - }); - - test('reloadStylesheets replaces links', () => { - jest.useFakeTimers(); - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = 'style.css'; - document.head.appendChild(link); - reloadStylesheets(); - const newLink = document.head.querySelector('link[rel="stylesheet"]:not([href="style.css"])'); - expect(newLink).toBeTruthy(); - jest.runAllTimers(); - expect(document.head.querySelector('link[href="style.css"]')).toBeNull(); - newLink.remove(); - jest.useRealTimers(); - }); - - test('forceImmediateStyleApplication hides body temporarily', () => { - jest.useFakeTimers(); - forceImmediateStyleApplication(); - expect(document.body.style.visibility).toBe('hidden'); - jest.runAllTimers(); - expect(document.body.style.visibility).toBe(''); - jest.useRealTimers(); - }); -}); diff --git a/frontend/src/utils/styleUtils.ts b/frontend/src/utils/styleUtils.ts deleted file mode 100644 index 58cb63c070..0000000000 --- a/frontend/src/utils/styleUtils.ts +++ /dev/null @@ -1,62 +0,0 @@ -export const forceReflow = (element: HTMLElement | null | undefined): void => { - if (element) { - void element.offsetHeight; - } -}; - -export const reloadStylesheets = (): void => { - const links = document.querySelectorAll('link[rel="stylesheet"]'); - links.forEach((link) => { - const href = link.getAttribute('href'); - if (href) { - const newHref = href.includes('?') - ? `${href.split('?')[0]}?t=${new Date().getTime()}` - : `${href}?t=${new Date().getTime()}`; - - const newLink = document.createElement('link'); - newLink.rel = 'stylesheet'; - newLink.href = newHref; - document.head.appendChild(newLink); - - setTimeout(() => { - try { - link.parentNode?.removeChild(link); - } catch (e) { - console.error('Error removing old stylesheet:', e); - } - }, 100); - } - }); -}; - -export const applyStylesToElements = (selector: string): void => { - const elements = document.querySelectorAll(selector); - elements.forEach((el) => { - if (el) { - forceReflow(el); - el.classList.add('style-applied'); - } - }); -}; - -export const applyBodyClass = (className: string): () => void => { - document.body.classList.add(className); - return () => document.body.classList.remove(className); -}; - -export const forceImmediateStyleApplication = (): void => { - document.body.style.visibility = 'hidden'; - setTimeout(() => { - document.body.style.visibility = ''; - }, 0); -}; - -const styleUtils = { - forceReflow, - reloadStylesheets, - applyStylesToElements, - applyBodyClass, - forceImmediateStyleApplication, -}; - -export default styleUtils; diff --git a/frontend/src/v2/__tests__/V2PodsSidebarCreate.test.tsx b/frontend/src/v2/__tests__/V2PodsSidebarCreate.test.tsx index 30f932805f..ed68f56f08 100644 --- a/frontend/src/v2/__tests__/V2PodsSidebarCreate.test.tsx +++ b/frontend/src/v2/__tests__/V2PodsSidebarCreate.test.tsx @@ -36,14 +36,6 @@ jest.mock('../hooks/useV2Api', () => ({ }), })); -jest.mock('../hooks/useV2Unread', () => ({ - useV2Unread: () => ({ - isUnread: () => false, - bumpLatest: jest.fn(), - seedFromExisting: jest.fn(), - }), -})); - jest.mock('../../context/SocketContext', () => ({ useSocket: () => ({ socket: null, connected: false, joinPod: jest.fn() }), })); diff --git a/frontend/src/v2/hooks/useV2Unread.ts b/frontend/src/v2/hooks/useV2Unread.ts deleted file mode 100644 index 5f4348b28f..0000000000 --- a/frontend/src/v2/hooks/useV2Unread.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; - -// Per-pod "last seen" timestamp persisted to localStorage. We can't lean on -// the backend yet β€” there's no read-receipts model β€” so this is a frontend- -// only approximation: a row is "unread" when its latest message arrived -// after the user last had that pod selected. Survives reload, scoped per- -// browser, accepted limitation. - -const STORAGE_KEY = 'v2.lastSeen'; - -const readMap = (): Record => { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return {}; - const parsed = JSON.parse(raw); - return typeof parsed === 'object' && parsed ? parsed as Record : {}; - } catch { - return {}; - } -}; - -const writeMap = (map: Record): void => { - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(map)); - } catch { - /* ignore quota / private-mode errors */ - } -}; - -export interface UseV2UnreadResult { - isUnread: (podId: string, lastMessageAt: string | undefined | null) => boolean; - markRead: (podId: string) => void; - // Bumps the perceived "latest message" timestamp for a pod without marking - // it read. Used when a `newMessage` socket event lands while the pod is not - // selected β€” keeps the unread badge sticky until the user actually opens - // the pod, even if the server-side `lastMessage` field hasn't been refetched. - bumpLatest: (podId: string, ts: string) => void; - // Seed lastSeen for pods we've never observed before. Without this, every - // pod looks unread on first load β€” which it isn't, the user just hasn't - // opened this device before. Called once with the current pod list and - // their newest-message timestamps; only fills in pods missing from the - // map, never moves a real lastSeen backwards. - seedFromExisting: (entries: Array<{ podId: string; lastMessageAt?: string | null }>) => void; -} - -export const useV2Unread = (selectedPodId: string | null): UseV2UnreadResult => { - const [seenMap, setSeenMap] = useState>(() => readMap()); - const [latestMap, setLatestMap] = useState>({}); - - // When the user navigates to a pod, mark it read. Persist to localStorage - // so a refresh on the same pod stays read. - useEffect(() => { - if (!selectedPodId) return; - const now = Date.now(); - setSeenMap((prev) => { - const next = { ...prev, [selectedPodId]: now }; - writeMap(next); - return next; - }); - }, [selectedPodId]); - - const isUnread = useCallback((podId: string, lastMessageAt: string | undefined | null): boolean => { - if (!podId || podId === selectedPodId) return false; - const seen = seenMap[podId] || 0; - const fromList = lastMessageAt ? new Date(lastMessageAt).getTime() : 0; - const fromSocket = latestMap[podId] || 0; - const latest = Math.max(fromList, fromSocket); - return Number.isFinite(latest) && latest > seen; - }, [seenMap, latestMap, selectedPodId]); - - const markRead = useCallback((podId: string) => { - if (!podId) return; - setSeenMap((prev) => { - const next = { ...prev, [podId]: Date.now() }; - writeMap(next); - return next; - }); - }, []); - - const bumpLatest = useCallback((podId: string, ts: string) => { - if (!podId || !ts) return; - const t = new Date(ts).getTime(); - if (!Number.isFinite(t)) return; - setLatestMap((prev) => (prev[podId] && prev[podId] >= t ? prev : { ...prev, [podId]: t })); - }, []); - - const seedFromExisting = useCallback((entries: Array<{ podId: string; lastMessageAt?: string | null }>) => { - if (!entries || entries.length === 0) return; - setSeenMap((prev) => { - let changed = false; - const next = { ...prev }; - for (const { podId, lastMessageAt } of entries) { - if (!podId || podId in next) continue; - // Treat the existing newest-message timestamp as already seen β€” the - // user can't have unread "from before they opened this device". If - // there's no message yet, fall back to now. - const t = lastMessageAt ? new Date(lastMessageAt).getTime() : Date.now(); - next[podId] = Number.isFinite(t) ? t : Date.now(); - changed = true; - } - if (!changed) return prev; - writeMap(next); - return next; - }); - }, []); - - return { isUnread, markRead, bumpLatest, seedFromExisting }; -}; diff --git a/scripts/test-discord.sh b/scripts/test-discord.sh index 7680582142..c5046c430e 100755 --- a/scripts/test-discord.sh +++ b/scripts/test-discord.sh @@ -31,7 +31,7 @@ echo "πŸ“‹ Next Steps:" echo "1. Follow the setup guide: docs/discord/DISCORD_SETUP.md" echo "2. Create a Discord bot and get your credentials" echo "3. Update your .env file with Discord credentials" -echo "4. Run the test script: node backend/test-discord-integration.js" +echo "4. Run Discord service tests in CI: backend/__tests__/service/discord.test.js" echo "5. Test the integration in the frontend" echo "" echo "πŸ”— Useful URLs:"