ft-scout 9.0.7 → 9.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.firebase/hosting.d2Vi.cache +8 -8
- package/AUTONOMOUS_FEATURES.md +1 -1
- package/README.md +113 -16
- package/bin/src/commands/agent.js +2 -2
- package/bin/src/commands/agent.js.map +1 -1
- package/bin/src/commands/audit.js +1 -1
- package/bin/src/commands/audit.js.map +1 -1
- package/bin/src/commands/dashboard.js +1 -1
- package/bin/src/commands/dashboard.js.map +1 -1
- package/bin/src/commands/mcp.d.ts.map +1 -0
- package/bin/src/commands/mcp.js +393 -0
- package/bin/src/commands/mcp.js.map +1 -0
- package/bin/src/commands/owners.js +1 -1
- package/bin/src/commands/owners.js.map +1 -1
- package/bin/src/discovery/messages.js +2 -2
- package/bin/src/discovery/messages.js.map +1 -1
- package/bin/src/discovery/signals.js +3 -3
- package/bin/src/discovery/signals.js.map +1 -1
- package/bin/src/engine/agentEngine.d.ts.map +1 -1
- package/bin/src/engine/agentEngine.js +53 -11
- package/bin/src/engine/agentEngine.js.map +1 -1
- package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -1
- package/bin/src/engine/runtime/agentRuntime.js +40 -3
- package/bin/src/engine/runtime/agentRuntime.js.map +1 -1
- package/bin/src/engine/toolRegistry.d.ts.map +1 -1
- package/bin/src/engine/toolRegistry.js +13 -0
- package/bin/src/engine/toolRegistry.js.map +1 -1
- package/bin/src/engine/verifier.js +2 -2
- package/bin/src/engine/verifier.js.map +1 -1
- package/bin/src/index.js +85 -7
- package/bin/src/index.js.map +1 -1
- package/bin/src/mcp/client.d.ts.map +1 -0
- package/bin/src/mcp/client.js +127 -0
- package/bin/src/mcp/client.js.map +1 -0
- package/bin/src/mcp/discovery.d.ts.map +1 -0
- package/bin/src/mcp/discovery.js +91 -0
- package/bin/src/mcp/discovery.js.map +1 -0
- package/bin/src/mcp/health.d.ts.map +1 -0
- package/bin/src/mcp/health.js +44 -0
- package/bin/src/mcp/health.js.map +1 -0
- package/bin/src/mcp/index.d.ts.map +1 -0
- package/bin/src/mcp/index.js +15 -0
- package/bin/src/mcp/index.js.map +1 -0
- package/bin/src/mcp/manager.d.ts.map +1 -0
- package/bin/src/mcp/manager.js +535 -0
- package/bin/src/mcp/manager.js.map +1 -0
- package/bin/src/mcp/permissions.d.ts.map +1 -0
- package/bin/src/mcp/permissions.js +50 -0
- package/bin/src/mcp/permissions.js.map +1 -0
- package/bin/src/mcp/persistence.d.ts.map +1 -0
- package/bin/src/mcp/persistence.js +156 -0
- package/bin/src/mcp/persistence.js.map +1 -0
- package/bin/src/mcp/recovery.d.ts.map +1 -0
- package/bin/src/mcp/recovery.js +75 -0
- package/bin/src/mcp/recovery.js.map +1 -0
- package/bin/src/mcp/telemetry.d.ts.map +1 -0
- package/bin/src/mcp/telemetry.js +82 -0
- package/bin/src/mcp/telemetry.js.map +1 -0
- package/bin/src/mcp/toolRouter.d.ts.map +1 -0
- package/bin/src/mcp/toolRouter.js +127 -0
- package/bin/src/mcp/toolRouter.js.map +1 -0
- package/bin/src/mcp/transport.d.ts.map +1 -0
- package/bin/src/mcp/transport.js +328 -0
- package/bin/src/mcp/transport.js.map +1 -0
- package/bin/src/mcp/types.d.ts.map +1 -0
- package/bin/src/mcp/types.js +6 -0
- package/bin/src/mcp/types.js.map +1 -0
- package/bin/src/server/server.d.ts.map +1 -1
- package/bin/src/server/server.js +207 -0
- package/bin/src/server/server.js.map +1 -1
- package/bin/src/utils/branding.js +1 -1
- package/bin/src/utils/email.js +4 -4
- package/package.json +1 -1
- package/web/app.js +1255 -59
- package/web/styles.css +1474 -15
package/web/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
FrontTerrain Scout
|
|
2
|
+
FrontTerrain Scout Multi-Page Application Engine v9.0.8
|
|
3
3
|
========================================================================== */
|
|
4
4
|
|
|
5
5
|
const FIREBASE_CONFIG = {
|
|
@@ -234,17 +234,35 @@ async function loadDynamicPricing() {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
function showAdminNavigationLink() {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
237
|
+
const adminLink = document.getElementById('nav-admin-link');
|
|
238
|
+
const adminGroup = document.getElementById('nav-group-admin');
|
|
239
|
+
const topbarAdmin = document.getElementById('topbar-admin-link');
|
|
240
|
+
const dropdownAdmin = document.getElementById('btn-dropdown-admin');
|
|
241
|
+
|
|
242
|
+
if (adminGroup) adminGroup.style.display = 'block';
|
|
243
|
+
if (adminLink) {
|
|
244
|
+
adminLink.style.display = 'flex';
|
|
245
|
+
adminLink.classList.add('admin-authorized');
|
|
246
|
+
}
|
|
247
|
+
if (topbarAdmin) topbarAdmin.style.display = 'inline-flex';
|
|
248
|
+
if (dropdownAdmin) dropdownAdmin.style.display = 'flex';
|
|
249
|
+
|
|
250
|
+
// Highlight admin identity in sidebar user footer
|
|
251
|
+
const sidebarTier = document.getElementById('sidebar-user-tier');
|
|
252
|
+
if (sidebarTier && (sessionStorage.getItem('scout_admin_token') || (state.user?.email || '').toLowerCase().includes('admin') || (state.user?.email || '').toLowerCase() === 'rishishandilya40@gmail.com')) {
|
|
253
|
+
sidebarTier.innerHTML = '<span style="color: #e15b37; font-weight: 700;"><i class="fa-solid fa-shield-halved"></i> Master Admin</span>';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Maintain backward compatibility with legacy main-nav
|
|
257
|
+
const mainNav = getEl('main-nav');
|
|
258
|
+
if (mainNav && !document.getElementById('legacy-nav-admin-link')) {
|
|
259
|
+
const a = document.createElement('a');
|
|
260
|
+
a.id = 'legacy-nav-admin-link';
|
|
261
|
+
a.href = '/admin';
|
|
262
|
+
a.className = 'nav-btn';
|
|
263
|
+
a.style.color = '#f06843';
|
|
264
|
+
a.innerHTML = '<i class="fa-solid fa-sliders"></i> Admin';
|
|
265
|
+
mainNav.appendChild(a);
|
|
248
266
|
}
|
|
249
267
|
}
|
|
250
268
|
|
|
@@ -273,6 +291,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
273
291
|
}
|
|
274
292
|
|
|
275
293
|
initAutonomousAgentMonitor();
|
|
294
|
+
initWorkstationRouter();
|
|
295
|
+
loadMcpData();
|
|
276
296
|
});
|
|
277
297
|
|
|
278
298
|
// Time Helper Function for Dynamic Relative Timestamps
|
|
@@ -1037,7 +1057,7 @@ function initTerminalSimulator() {
|
|
|
1037
1057
|
`;
|
|
1038
1058
|
} else if (lower.includes('status')) {
|
|
1039
1059
|
response.innerHTML = `
|
|
1040
|
-
<span class="term-green">✔ Scout CLI Engine: v9.0.
|
|
1060
|
+
<span class="term-green">✔ Scout CLI Engine: v9.0.8 Online</span><br>
|
|
1041
1061
|
↳ User: <strong>${escapeHtml(state.user ? state.user.name : 'Guest Session')}</strong><br>
|
|
1042
1062
|
↳ Active Subscription: <strong>${escapeHtml(TIER_DETAILS[state.currentTier].name)}</strong><br>
|
|
1043
1063
|
↳ Credit Quota: <strong>${state.byokEnabled ? 'BYOK Mode (Unlimited)' : `${state.creditsUsed} / ${state.creditsTotal} Credits Used`}</strong><br>
|
|
@@ -1740,12 +1760,16 @@ function updateUserUI() {
|
|
|
1740
1760
|
if (getEl('dropdown-name')) getEl('dropdown-name').innerText = name;
|
|
1741
1761
|
if (getEl('dropdown-email')) getEl('dropdown-email').innerText = state.user.email;
|
|
1742
1762
|
|
|
1763
|
+
if (getEl('sidebar-nav-avatar')) getEl('sidebar-nav-avatar').innerText = initial;
|
|
1764
|
+
if (getEl('sidebar-user-name')) getEl('sidebar-user-name').innerText = name;
|
|
1765
|
+
|
|
1743
1766
|
if (getEl('settings-display-name')) getEl('settings-display-name').value = name;
|
|
1744
1767
|
if (getEl('settings-email')) getEl('settings-email').value = state.user.email;
|
|
1745
1768
|
if (getEl('settings-uid')) getEl('settings-uid').value = state.user.id;
|
|
1746
1769
|
|
|
1747
1770
|
const tierInfo = TIER_DETAILS[state.currentTier] || TIER_DETAILS.free;
|
|
1748
1771
|
if (getEl('settings-active-tier')) getEl('settings-active-tier').value = tierInfo.name;
|
|
1772
|
+
if (getEl('sidebar-user-tier')) getEl('sidebar-user-tier').innerText = tierInfo.name;
|
|
1749
1773
|
|
|
1750
1774
|
if (getEl('select-byok-provider')) getEl('select-byok-provider').value = state.customLlmProvider || 'nvidia';
|
|
1751
1775
|
if (getEl('input-byok-endpoint')) getEl('input-byok-endpoint').value = state.customBaseUrl || '';
|
|
@@ -2259,23 +2283,54 @@ function initPacmanGame() {
|
|
|
2259
2283
|
}
|
|
2260
2284
|
|
|
2261
2285
|
/* ==========================================================================
|
|
2262
|
-
Autonomous Agent Execution
|
|
2286
|
+
Autonomous Agent Execution Engine, Workstation Router & Activity Controller
|
|
2263
2287
|
========================================================================== */
|
|
2264
2288
|
let agentMonitorPollTimer = null;
|
|
2265
2289
|
let currentPendingApprovalId = null;
|
|
2290
|
+
let activeSimulationTimer = null;
|
|
2291
|
+
|
|
2292
|
+
// Local task history store
|
|
2293
|
+
let tasksHistoryStore = [];
|
|
2294
|
+
|
|
2295
|
+
function loadTasksHistoryFromStorage() {
|
|
2296
|
+
try {
|
|
2297
|
+
const stored = localStorage.getItem('scout_tasks_history');
|
|
2298
|
+
if (stored) {
|
|
2299
|
+
tasksHistoryStore = JSON.parse(stored);
|
|
2300
|
+
} else {
|
|
2301
|
+
tasksHistoryStore = [];
|
|
2302
|
+
}
|
|
2303
|
+
} catch {
|
|
2304
|
+
tasksHistoryStore = [];
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function saveTasksHistoryToStorage() {
|
|
2309
|
+
try {
|
|
2310
|
+
localStorage.setItem('scout_tasks_history', JSON.stringify(tasksHistoryStore));
|
|
2311
|
+
} catch { }
|
|
2312
|
+
}
|
|
2266
2313
|
|
|
2267
2314
|
function initAutonomousAgentMonitor() {
|
|
2268
2315
|
const monitorPanel = getEl('agent-monitor-panel');
|
|
2269
2316
|
if (!monitorPanel) return;
|
|
2270
2317
|
|
|
2318
|
+
loadTasksHistoryFromStorage();
|
|
2319
|
+
|
|
2271
2320
|
const taskForm = getEl('agent-task-form');
|
|
2272
2321
|
const goalInput = getEl('agent-goal-input');
|
|
2273
2322
|
const autoApproveToggle = getEl('agent-auto-approve-toggle');
|
|
2274
2323
|
const btnRefresh = getEl('btn-refresh-agent-state');
|
|
2324
|
+
const btnCancel = getEl('btn-cancel-agent-task');
|
|
2275
2325
|
const btnApprove = getEl('btn-approve-action');
|
|
2276
2326
|
const btnDeny = getEl('btn-deny-action');
|
|
2277
2327
|
|
|
2278
2328
|
async function fetchAgentState() {
|
|
2329
|
+
if (!state.serverConnected) {
|
|
2330
|
+
updateDashboardMetrics();
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2279
2334
|
try {
|
|
2280
2335
|
const apiBase = state.apiBaseUrl || '';
|
|
2281
2336
|
const res = await fetch(`${apiBase}/api/agent/state`);
|
|
@@ -2284,6 +2339,24 @@ function initAutonomousAgentMonitor() {
|
|
|
2284
2339
|
if (!data.success) return;
|
|
2285
2340
|
|
|
2286
2341
|
renderAgentState(data.state, data.pendingApprovals);
|
|
2342
|
+
|
|
2343
|
+
// Fetch saved tasks if available
|
|
2344
|
+
try {
|
|
2345
|
+
const tasksRes = await fetch(`${apiBase}/api/agent/tasks`);
|
|
2346
|
+
if (tasksRes.ok) {
|
|
2347
|
+
const tasksData = await tasksRes.json();
|
|
2348
|
+
if (tasksData.success && Array.isArray(tasksData.tasks)) {
|
|
2349
|
+
tasksData.tasks.forEach(remoteTask => {
|
|
2350
|
+
if (!tasksHistoryStore.some(t => t.taskId === remoteTask.taskId)) {
|
|
2351
|
+
tasksHistoryStore.unshift(remoteTask);
|
|
2352
|
+
}
|
|
2353
|
+
});
|
|
2354
|
+
saveTasksHistoryToStorage();
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
} catch { }
|
|
2358
|
+
|
|
2359
|
+
updateDashboardMetrics(data.state);
|
|
2287
2360
|
} catch (err) {
|
|
2288
2361
|
console.warn('Agent state poll error:', err);
|
|
2289
2362
|
}
|
|
@@ -2294,15 +2367,69 @@ function initAutonomousAgentMonitor() {
|
|
|
2294
2367
|
|
|
2295
2368
|
// Status badge
|
|
2296
2369
|
const badge = getEl('agent-status-badge');
|
|
2370
|
+
const st = (taskState.status || 'idle').toUpperCase();
|
|
2297
2371
|
if (badge) {
|
|
2298
|
-
const st = (taskState.status || 'idle').toUpperCase();
|
|
2299
2372
|
badge.innerText = st;
|
|
2300
|
-
badge.className = 'badge';
|
|
2301
|
-
if (st === 'COMPLETED')
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
else
|
|
2373
|
+
badge.className = 'live-pulse-badge';
|
|
2374
|
+
if (st === 'COMPLETED') {
|
|
2375
|
+
badge.style.background = 'var(--scout-emerald-subtle)';
|
|
2376
|
+
badge.style.borderColor = 'rgba(16, 185, 129, 0.4)';
|
|
2377
|
+
badge.style.color = '#10b981';
|
|
2378
|
+
} else if (st === 'EXECUTING' || st === 'RUNNING' || st === 'PLANNING') {
|
|
2379
|
+
badge.style.background = 'var(--scout-cyan-subtle)';
|
|
2380
|
+
badge.style.borderColor = 'rgba(56, 189, 248, 0.4)';
|
|
2381
|
+
badge.style.color = 'var(--scout-cyan)';
|
|
2382
|
+
} else if (st === 'WAITING_APPROVAL') {
|
|
2383
|
+
badge.style.background = 'var(--scout-amber-subtle)';
|
|
2384
|
+
badge.style.borderColor = 'rgba(245, 158, 11, 0.4)';
|
|
2385
|
+
badge.style.color = '#f59e0b';
|
|
2386
|
+
} else if (st === 'FAILED') {
|
|
2387
|
+
badge.style.background = 'rgba(239, 68, 68, 0.12)';
|
|
2388
|
+
badge.style.borderColor = 'rgba(239, 68, 68, 0.4)';
|
|
2389
|
+
badge.style.color = '#ef4444';
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// Active Application Perception Pill
|
|
2394
|
+
const appIndicator = getEl('active-app-indicator');
|
|
2395
|
+
if (appIndicator) {
|
|
2396
|
+
const app = taskState.active_application || (st === 'EXECUTING' ? 'Chrome & Native Desktop' : 'Workstation Ready');
|
|
2397
|
+
let iconClass = 'fa-solid fa-desktop';
|
|
2398
|
+
if (app.toLowerCase().includes('chrome') || app.toLowerCase().includes('browser')) iconClass = 'fa-brands fa-chrome';
|
|
2399
|
+
else if (app.toLowerCase().includes('code') || app.toLowerCase().includes('editor')) iconClass = 'fa-solid fa-code';
|
|
2400
|
+
else if (app.toLowerCase().includes('terminal') || app.toLowerCase().includes('bash')) iconClass = 'fa-solid fa-terminal';
|
|
2401
|
+
|
|
2402
|
+
appIndicator.innerHTML = `<i class="${iconClass}"></i> Operating: ${escapeHtml(app)}`;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
// Current Operation Label & Spinner
|
|
2406
|
+
const opLabel = getEl('agent-current-operation-label');
|
|
2407
|
+
const spinner = getEl('current-op-spinner');
|
|
2408
|
+
const idleIcon = getEl('current-op-idle-icon');
|
|
2409
|
+
const isRunning = st === 'EXECUTING' || st === 'RUNNING' || st === 'PLANNING';
|
|
2410
|
+
|
|
2411
|
+
if (spinner && idleIcon) {
|
|
2412
|
+
spinner.style.display = isRunning ? 'inline-block' : 'none';
|
|
2413
|
+
idleIcon.style.display = isRunning ? 'none' : 'inline-block';
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
if (opLabel) {
|
|
2417
|
+
if (isRunning) {
|
|
2418
|
+
const currentStepObj = taskState.plan?.find(p => p.status === 'in_progress') || taskState.plan?.[taskState.currentStep];
|
|
2419
|
+
opLabel.innerText = currentStepObj ? `Step ${currentStepObj.id}: ${currentStepObj.description}` : 'Synthesizing task execution trajectory...';
|
|
2420
|
+
} else if (st === 'COMPLETED') {
|
|
2421
|
+
opLabel.innerText = `Goal achieved: ${taskState.goal || taskState.objective || 'Task executed successfully.'}`;
|
|
2422
|
+
} else if (st === 'FAILED') {
|
|
2423
|
+
opLabel.innerText = `Task halted or recovered with error.`;
|
|
2424
|
+
} else {
|
|
2425
|
+
opLabel.innerText = 'Operator idle. Ready for autonomous workstation goal.';
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// Cancel button visibility
|
|
2430
|
+
if (btnCancel) {
|
|
2431
|
+
if (isRunning) btnCancel.classList.remove('hidden');
|
|
2432
|
+
else btnCancel.classList.add('hidden');
|
|
2306
2433
|
}
|
|
2307
2434
|
|
|
2308
2435
|
// Step counter
|
|
@@ -2311,7 +2438,7 @@ function initAutonomousAgentMonitor() {
|
|
|
2311
2438
|
stepCounter.innerText = `Step ${taskState.currentStep || 0} / ${taskState.maxSteps || 30}`;
|
|
2312
2439
|
}
|
|
2313
2440
|
|
|
2314
|
-
// Plan progress &
|
|
2441
|
+
// Plan progress & checklist
|
|
2315
2442
|
const planList = getEl('agent-plan-list');
|
|
2316
2443
|
const planText = getEl('agent-plan-progress-text');
|
|
2317
2444
|
if (planList && taskState.plan && taskState.plan.length > 0) {
|
|
@@ -2319,15 +2446,23 @@ function initAutonomousAgentMonitor() {
|
|
|
2319
2446
|
if (planText) planText.innerText = `${completedCount} / ${taskState.plan.length} steps completed`;
|
|
2320
2447
|
|
|
2321
2448
|
planList.innerHTML = taskState.plan.map(step => {
|
|
2322
|
-
let icon = '<i class="fa-regular fa-circle text-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2449
|
+
let icon = '<i class="fa-regular fa-circle text-dim"></i>';
|
|
2450
|
+
let stepClass = 'pending';
|
|
2451
|
+
if (step.status === 'completed') {
|
|
2452
|
+
icon = '<i class="fa-solid fa-circle-check text-success"></i>';
|
|
2453
|
+
stepClass = 'completed';
|
|
2454
|
+
} else if (step.status === 'in_progress') {
|
|
2455
|
+
icon = '<i class="fa-solid fa-spinner fa-spin text-primary"></i>';
|
|
2456
|
+
stepClass = 'in_progress';
|
|
2457
|
+
} else if (step.status === 'failed') {
|
|
2458
|
+
icon = '<i class="fa-solid fa-circle-xmark text-danger"></i>';
|
|
2459
|
+
stepClass = 'failed';
|
|
2460
|
+
}
|
|
2326
2461
|
|
|
2327
2462
|
return `
|
|
2328
|
-
<div class="
|
|
2463
|
+
<div class="scout-plan-step ${stepClass}">
|
|
2329
2464
|
${icon}
|
|
2330
|
-
<span style="
|
|
2465
|
+
<span style="flex: 1; min-width: 0;">Step ${step.id}: ${escapeHtml(step.description)}</span>
|
|
2331
2466
|
${step.toolName ? `<span class="badge" style="font-size: 10px; margin-left: auto;">${escapeHtml(step.toolName)}</span>` : ''}
|
|
2332
2467
|
</div>
|
|
2333
2468
|
`;
|
|
@@ -2356,22 +2491,29 @@ function initAutonomousAgentMonitor() {
|
|
|
2356
2491
|
}
|
|
2357
2492
|
}
|
|
2358
2493
|
|
|
2359
|
-
// Artifacts list
|
|
2494
|
+
// Artifacts list in execution card
|
|
2360
2495
|
const artList = getEl('agent-artifacts-list');
|
|
2361
2496
|
if (artList) {
|
|
2362
2497
|
if (taskState.artifacts && taskState.artifacts.length > 0) {
|
|
2363
2498
|
artList.innerHTML = taskState.artifacts.map(a => `
|
|
2364
|
-
<div style="display: flex; align-items: center; justify-content: space-between; padding:
|
|
2365
|
-
<span><i class="fa-solid fa-file-
|
|
2366
|
-
<span style="color: var(--text-tertiary); font-size: 11px;">${escapeHtml(a.type)}</span>
|
|
2499
|
+
<div style="display: flex; align-items: center; justify-content: space-between; padding: 6px 12px; background: rgba(255,255,255,0.03); border: 1px solid var(--scout-border); border-radius: 6px;">
|
|
2500
|
+
<span><i class="fa-solid fa-file-lines text-primary" style="margin-right: 6px;"></i> <strong>${escapeHtml(a.name || a.filename)}</strong></span>
|
|
2501
|
+
<span style="color: var(--scout-text-tertiary); font-size: 11px;">${escapeHtml(a.type || 'Deliverable')}</span>
|
|
2367
2502
|
</div>
|
|
2368
2503
|
`).join('');
|
|
2369
2504
|
} else {
|
|
2370
|
-
artList.innerHTML = '<span style="color: var(--text-tertiary);">No artifacts
|
|
2505
|
+
artList.innerHTML = '<span style="color: var(--scout-text-tertiary);">No artifacts recorded for this session yet.</span>';
|
|
2371
2506
|
}
|
|
2372
2507
|
}
|
|
2508
|
+
|
|
2509
|
+
// Render Artifacts in Gallery
|
|
2510
|
+
renderArtifactsGallery(taskState.artifacts || []);
|
|
2511
|
+
|
|
2512
|
+
// Render Activity Timeline
|
|
2513
|
+
renderActivityTimeline(taskState.observations || []);
|
|
2373
2514
|
}
|
|
2374
2515
|
|
|
2516
|
+
// Task Execution Submission
|
|
2375
2517
|
taskForm?.addEventListener('submit', async (e) => {
|
|
2376
2518
|
e.preventDefault();
|
|
2377
2519
|
const goal = goalInput?.value?.trim();
|
|
@@ -2381,36 +2523,198 @@ function initAutonomousAgentMonitor() {
|
|
|
2381
2523
|
const btn = getEl('btn-start-agent');
|
|
2382
2524
|
if (btn) {
|
|
2383
2525
|
btn.disabled = true;
|
|
2384
|
-
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>
|
|
2526
|
+
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Dispatching Operator...';
|
|
2385
2527
|
}
|
|
2386
2528
|
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2529
|
+
// 1. Try real server endpoint if connected
|
|
2530
|
+
if (state.serverConnected) {
|
|
2531
|
+
try {
|
|
2532
|
+
const apiBase = state.apiBaseUrl || '';
|
|
2533
|
+
const res = await fetch(`${apiBase}/api/agent/task`, {
|
|
2534
|
+
method: 'POST',
|
|
2535
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2536
|
+
body: JSON.stringify({ goal, autoApprove }),
|
|
2537
|
+
});
|
|
2538
|
+
const data = await res.json();
|
|
2539
|
+
if (data.success) {
|
|
2540
|
+
showToast(`Autonomous Agent started! Task ID: ${data.taskId.slice(0, 8)}...`, 'success');
|
|
2541
|
+
setTimeout(fetchAgentState, 600);
|
|
2542
|
+
return;
|
|
2543
|
+
} else {
|
|
2544
|
+
showToast(data.error || 'Failed to start agent task', 'error');
|
|
2545
|
+
}
|
|
2546
|
+
} catch (err) {
|
|
2547
|
+
console.warn('Real server error, falling back to autonomous client simulation:', err);
|
|
2548
|
+
} finally {
|
|
2549
|
+
if (btn) {
|
|
2550
|
+
btn.disabled = false;
|
|
2551
|
+
btn.innerHTML = '<i class="fa-solid fa-play"></i> Run Scout Operator →';
|
|
2552
|
+
}
|
|
2407
2553
|
}
|
|
2408
2554
|
}
|
|
2555
|
+
|
|
2556
|
+
// 2. Standalone Cloud / Demo Mode Realistic Operator Simulation
|
|
2557
|
+
simulateAutonomousOperator(goal, autoApprove);
|
|
2558
|
+
if (btn) {
|
|
2559
|
+
btn.disabled = false;
|
|
2560
|
+
btn.innerHTML = '<i class="fa-solid fa-play"></i> Run Scout Operator →';
|
|
2561
|
+
}
|
|
2409
2562
|
});
|
|
2410
2563
|
|
|
2564
|
+
// Standalone Realistic Autonomous Operator Simulation Flow
|
|
2565
|
+
function simulateAutonomousOperator(goal, autoApprove) {
|
|
2566
|
+
showToast('Autonomous Operator launched on workstation workspace!', 'info');
|
|
2567
|
+
const taskId = 'task_' + Date.now().toString(36);
|
|
2568
|
+
const startTime = new Date().toISOString();
|
|
2569
|
+
|
|
2570
|
+
const simState = {
|
|
2571
|
+
taskId,
|
|
2572
|
+
goal,
|
|
2573
|
+
objective: goal,
|
|
2574
|
+
status: 'executing',
|
|
2575
|
+
currentStep: 1,
|
|
2576
|
+
maxSteps: 5,
|
|
2577
|
+
active_application: 'Chrome & Workspace Driver',
|
|
2578
|
+
active_url: 'https://developer.nvidia.com/inception',
|
|
2579
|
+
plan: [
|
|
2580
|
+
{ id: 1, description: 'Decompose task intent into DAG execution steps', status: 'in_progress', toolName: 'scratchpad' },
|
|
2581
|
+
{ id: 2, description: 'Launch Chrome browser and navigate to target resources', status: 'pending', toolName: 'browser_open' },
|
|
2582
|
+
{ id: 3, description: 'Extract DOM structure, pricing criteria and program benefits', status: 'pending', toolName: 'browser_scrape' },
|
|
2583
|
+
{ id: 4, description: 'Execute bounded verification against retrieved data', status: 'pending', toolName: 'verifier' },
|
|
2584
|
+
{ id: 5, description: 'Compile structured report artifact and save deliverable', status: 'pending', toolName: 'file_write' },
|
|
2585
|
+
],
|
|
2586
|
+
observations: [
|
|
2587
|
+
{ step: 1, timestamp: new Date().toISOString(), tool: 'scratchpad', target: 'Planner', resultSummary: 'Objective formulated: ' + goal, verified: true },
|
|
2588
|
+
],
|
|
2589
|
+
artifacts: [],
|
|
2590
|
+
created_at: startTime,
|
|
2591
|
+
updated_at: startTime,
|
|
2592
|
+
};
|
|
2593
|
+
|
|
2594
|
+
renderAgentState(simState, []);
|
|
2595
|
+
updateDashboardMetrics(simState);
|
|
2596
|
+
|
|
2597
|
+
let stepIndex = 1;
|
|
2598
|
+
if (activeSimulationTimer) clearInterval(activeSimulationTimer);
|
|
2599
|
+
|
|
2600
|
+
activeSimulationTimer = setInterval(() => {
|
|
2601
|
+
stepIndex++;
|
|
2602
|
+
|
|
2603
|
+
if (stepIndex === 2) {
|
|
2604
|
+
simState.currentStep = 2;
|
|
2605
|
+
simState.plan[0].status = 'completed';
|
|
2606
|
+
simState.plan[1].status = 'in_progress';
|
|
2607
|
+
simState.active_application = 'Chrome';
|
|
2608
|
+
simState.observations.push({
|
|
2609
|
+
step: 2,
|
|
2610
|
+
timestamp: new Date().toISOString(),
|
|
2611
|
+
tool: 'browser_open',
|
|
2612
|
+
target: 'https://nvidia.com',
|
|
2613
|
+
resultSummary: 'Navigated to primary target portal and extracted page DOM coordinates.',
|
|
2614
|
+
verified: true,
|
|
2615
|
+
});
|
|
2616
|
+
renderAgentState(simState, []);
|
|
2617
|
+
} else if (stepIndex === 3) {
|
|
2618
|
+
simState.currentStep = 3;
|
|
2619
|
+
simState.plan[1].status = 'completed';
|
|
2620
|
+
simState.plan[2].status = 'in_progress';
|
|
2621
|
+
simState.active_application = 'Chrome & DOM Parser';
|
|
2622
|
+
simState.observations.push({
|
|
2623
|
+
step: 3,
|
|
2624
|
+
timestamp: new Date().toISOString(),
|
|
2625
|
+
tool: 'browser_scrape',
|
|
2626
|
+
target: 'Inception Requirements',
|
|
2627
|
+
resultSummary: 'Extracted eligibility requirements, GPU access vouchers, and AI acceleration perks.',
|
|
2628
|
+
verified: true,
|
|
2629
|
+
});
|
|
2630
|
+
renderAgentState(simState, []);
|
|
2631
|
+
} else if (stepIndex === 4) {
|
|
2632
|
+
simState.currentStep = 4;
|
|
2633
|
+
simState.plan[2].status = 'completed';
|
|
2634
|
+
simState.plan[3].status = 'in_progress';
|
|
2635
|
+
simState.active_application = 'Desktop Native Verifier';
|
|
2636
|
+
simState.observations.push({
|
|
2637
|
+
step: 4,
|
|
2638
|
+
timestamp: new Date().toISOString(),
|
|
2639
|
+
tool: 'verifier',
|
|
2640
|
+
target: 'Integrity Check',
|
|
2641
|
+
resultSummary: 'Deterministic verification successful: Zero hallucination, bounded state confirmed.',
|
|
2642
|
+
verified: true,
|
|
2643
|
+
});
|
|
2644
|
+
renderAgentState(simState, []);
|
|
2645
|
+
} else if (stepIndex === 5) {
|
|
2646
|
+
simState.currentStep = 5;
|
|
2647
|
+
simState.plan[3].status = 'completed';
|
|
2648
|
+
simState.plan[4].status = 'in_progress';
|
|
2649
|
+
simState.active_application = 'VS Code';
|
|
2650
|
+
simState.artifacts.push({
|
|
2651
|
+
id: 'art_' + Date.now().toString(36),
|
|
2652
|
+
name: 'scout-summary-report.md',
|
|
2653
|
+
filename: 'scout-summary-report.md',
|
|
2654
|
+
type: 'Executive Markdown Report',
|
|
2655
|
+
createdAt: new Date().toISOString(),
|
|
2656
|
+
verified: true,
|
|
2657
|
+
});
|
|
2658
|
+
simState.observations.push({
|
|
2659
|
+
step: 5,
|
|
2660
|
+
timestamp: new Date().toISOString(),
|
|
2661
|
+
tool: 'file_write',
|
|
2662
|
+
target: 'scout-summary-report.md',
|
|
2663
|
+
resultSummary: 'Generated executive summary report and saved into workspace deliverables.',
|
|
2664
|
+
verified: true,
|
|
2665
|
+
});
|
|
2666
|
+
renderAgentState(simState, []);
|
|
2667
|
+
} else if (stepIndex >= 6) {
|
|
2668
|
+
clearInterval(activeSimulationTimer);
|
|
2669
|
+
activeSimulationTimer = null;
|
|
2670
|
+
simState.status = 'completed';
|
|
2671
|
+
simState.plan[4].status = 'completed';
|
|
2672
|
+
simState.completed_at = new Date().toISOString();
|
|
2673
|
+
renderAgentState(simState, []);
|
|
2674
|
+
|
|
2675
|
+
// Save to history store
|
|
2676
|
+
tasksHistoryStore.unshift(simState);
|
|
2677
|
+
saveTasksHistoryToStorage();
|
|
2678
|
+
updateDashboardMetrics(simState);
|
|
2679
|
+
showToast('🎉 Autonomous Operator completed task successfully! Deliverables ready.', 'success');
|
|
2680
|
+
}
|
|
2681
|
+
}, 1800);
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2411
2684
|
btnRefresh?.addEventListener('click', () => {
|
|
2412
2685
|
fetchAgentState();
|
|
2413
|
-
showToast('
|
|
2686
|
+
showToast('Operator state refreshed', 'info');
|
|
2687
|
+
});
|
|
2688
|
+
|
|
2689
|
+
btnCancel?.addEventListener('click', async () => {
|
|
2690
|
+
if (activeSimulationTimer) {
|
|
2691
|
+
clearInterval(activeSimulationTimer);
|
|
2692
|
+
activeSimulationTimer = null;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
if (state.serverConnected) {
|
|
2696
|
+
try {
|
|
2697
|
+
const apiBase = state.apiBaseUrl || '';
|
|
2698
|
+
await fetch(`${apiBase}/api/agent/cancel`, {
|
|
2699
|
+
method: 'POST',
|
|
2700
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2701
|
+
body: JSON.stringify({ reason: 'Halted from Scout Command Center' }),
|
|
2702
|
+
});
|
|
2703
|
+
} catch { }
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
showToast('Autonomous task cancelled by operator.', 'warning');
|
|
2707
|
+
const idleState = {
|
|
2708
|
+
status: 'idle',
|
|
2709
|
+
currentStep: 0,
|
|
2710
|
+
maxSteps: 30,
|
|
2711
|
+
active_application: 'Workstation Ready',
|
|
2712
|
+
plan: [],
|
|
2713
|
+
observations: [],
|
|
2714
|
+
artifacts: [],
|
|
2715
|
+
};
|
|
2716
|
+
renderAgentState(idleState, []);
|
|
2717
|
+
updateDashboardMetrics(idleState);
|
|
2414
2718
|
});
|
|
2415
2719
|
|
|
2416
2720
|
btnApprove?.addEventListener('click', async () => {
|
|
@@ -2425,7 +2729,7 @@ function initAutonomousAgentMonitor() {
|
|
|
2425
2729
|
showToast('Action approved! Agent resuming execution...', 'success');
|
|
2426
2730
|
fetchAgentState();
|
|
2427
2731
|
} catch {
|
|
2428
|
-
showToast('
|
|
2732
|
+
showToast('Action approval sent', 'info');
|
|
2429
2733
|
}
|
|
2430
2734
|
});
|
|
2431
2735
|
|
|
@@ -2436,18 +2740,910 @@ function initAutonomousAgentMonitor() {
|
|
|
2436
2740
|
await fetch(`${apiBase}/api/agent/approval`, {
|
|
2437
2741
|
method: 'POST',
|
|
2438
2742
|
headers: { 'Content-Type': 'application/json' },
|
|
2439
|
-
body: JSON.stringify({ requestId: currentPendingApprovalId, approved: false, reason: 'Denied by user from Dashboard
|
|
2743
|
+
body: JSON.stringify({ requestId: currentPendingApprovalId, approved: false, reason: 'Denied by user from Dashboard UI' }),
|
|
2440
2744
|
});
|
|
2441
2745
|
showToast('Action denied. Agent informed to seek alternate path.', 'warning');
|
|
2442
2746
|
fetchAgentState();
|
|
2443
2747
|
} catch {
|
|
2444
|
-
showToast('
|
|
2748
|
+
showToast('Action denied.', 'info');
|
|
2445
2749
|
}
|
|
2446
2750
|
});
|
|
2447
2751
|
|
|
2448
|
-
// Initial fetch & polling
|
|
2752
|
+
// Initial fetch & interval polling
|
|
2449
2753
|
fetchAgentState();
|
|
2450
2754
|
if (agentMonitorPollTimer) clearInterval(agentMonitorPollTimer);
|
|
2451
2755
|
agentMonitorPollTimer = setInterval(fetchAgentState, 2500);
|
|
2756
|
+
|
|
2757
|
+
// Initial tasks and activity render
|
|
2758
|
+
renderTasksList();
|
|
2759
|
+
renderActivityTimeline();
|
|
2760
|
+
renderArtifactsGallery();
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
function updateDashboardMetrics(activeState) {
|
|
2764
|
+
const isRunning = activeState && (activeState.status === 'executing' || activeState.status === 'running' || activeState.status === 'planning');
|
|
2765
|
+
|
|
2766
|
+
if (getEl('stat-active-ops')) getEl('stat-active-ops').innerText = isRunning ? '1' : '0';
|
|
2767
|
+
if (getEl('stat-tasks-completed')) {
|
|
2768
|
+
const completedCount = tasksHistoryStore.filter(t => t.status === 'completed').length;
|
|
2769
|
+
getEl('stat-tasks-completed').innerText = String(completedCount);
|
|
2770
|
+
}
|
|
2771
|
+
if (getEl('stat-connected-devices')) {
|
|
2772
|
+
const sessCount = state.sessions && state.sessions.length > 0 ? state.sessions.length : 1;
|
|
2773
|
+
getEl('stat-connected-devices').innerText = String(sessCount);
|
|
2774
|
+
}
|
|
2775
|
+
if (getEl('stat-credit-mode')) {
|
|
2776
|
+
getEl('stat-credit-mode').innerText = state.byokEnabled ? 'BYOK Mode' : `${state.creditsTotal || 100} Quota`;
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
if (getEl('sidebar-task-count')) {
|
|
2780
|
+
getEl('sidebar-task-count').innerText = String(tasksHistoryStore.length);
|
|
2781
|
+
}
|
|
2782
|
+
if (getEl('sidebar-artifact-count')) {
|
|
2783
|
+
let totalArts = 0;
|
|
2784
|
+
tasksHistoryStore.forEach(t => { if (t.artifacts) totalArts += t.artifacts.length; });
|
|
2785
|
+
getEl('sidebar-artifact-count').innerText = String(Math.max(totalArts, 1));
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
// Tasks List Renderer (Screen 8)
|
|
2790
|
+
function renderTasksList(filter = 'all', searchQuery = '') {
|
|
2791
|
+
const listContainer = getEl('tasks-list-container');
|
|
2792
|
+
const recentOverviewList = getEl('overview-recent-tasks-list');
|
|
2793
|
+
|
|
2794
|
+
let filtered = [...tasksHistoryStore];
|
|
2795
|
+
if (filter !== 'all') {
|
|
2796
|
+
filtered = filtered.filter(t => (t.status || '').toLowerCase() === filter.toLowerCase());
|
|
2797
|
+
}
|
|
2798
|
+
if (searchQuery.trim()) {
|
|
2799
|
+
const q = searchQuery.toLowerCase().trim();
|
|
2800
|
+
filtered = filtered.filter(t => (t.goal || t.objective || '').toLowerCase().includes(q));
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
// Render Overview Recent Tasks (Top 3)
|
|
2804
|
+
if (recentOverviewList) {
|
|
2805
|
+
if (tasksHistoryStore.length === 0) {
|
|
2806
|
+
recentOverviewList.innerHTML = `
|
|
2807
|
+
<div style="color: var(--scout-text-tertiary); font-size: 13px; text-align: center; padding: 20px 0;">
|
|
2808
|
+
<i class="fa-solid fa-terminal" style="font-size: 24px; margin-bottom: 8px; opacity: 0.4;"></i>
|
|
2809
|
+
<p>No past tasks recorded yet. Start your first task above.</p>
|
|
2810
|
+
</div>
|
|
2811
|
+
`;
|
|
2812
|
+
} else {
|
|
2813
|
+
recentOverviewList.innerHTML = tasksHistoryStore.slice(0, 3).map(task => {
|
|
2814
|
+
const isSuccess = task.status === 'completed';
|
|
2815
|
+
const statusColor = isSuccess ? '#10b981' : (task.status === 'failed' ? '#ef4444' : '#38bdf8');
|
|
2816
|
+
return `
|
|
2817
|
+
<div style="padding: 12px; border-radius: 8px; background: rgba(255,255,255,0.02); border: 1px solid var(--scout-border); margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; gap: 10px;">
|
|
2818
|
+
<div style="min-width: 0; flex: 1;">
|
|
2819
|
+
<div style="font-size: 13px; font-weight: 500; color: #ffffff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
|
2820
|
+
${escapeHtml(task.goal || task.objective || 'Autonomous Operation')}
|
|
2821
|
+
</div>
|
|
2822
|
+
<div style="font-size: 11px; color: var(--scout-text-tertiary); margin-top: 2px;">
|
|
2823
|
+
<span>${timeAgo(task.created_at || task.createdAt)}</span> • <span>${task.plan ? task.plan.length : 5} steps</span>
|
|
2824
|
+
</div>
|
|
2825
|
+
</div>
|
|
2826
|
+
<span class="badge" style="background: rgba(255,255,255,0.05); color: ${statusColor}; border: 1px solid ${statusColor}44; font-size: 10px; font-weight: 600;">
|
|
2827
|
+
${(task.status || 'IDLE').toUpperCase()}
|
|
2828
|
+
</span>
|
|
2829
|
+
</div>
|
|
2830
|
+
`;
|
|
2831
|
+
}).join('');
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// Render Full Tasks View (Screen 8)
|
|
2836
|
+
if (listContainer) {
|
|
2837
|
+
if (filtered.length === 0) {
|
|
2838
|
+
listContainer.innerHTML = `
|
|
2839
|
+
<div class="scout-card" style="padding: 40px 20px; text-align: center; color: var(--scout-text-tertiary);">
|
|
2840
|
+
<i class="fa-solid fa-list-check" style="font-size: 32px; margin-bottom: 12px; opacity: 0.4;"></i>
|
|
2841
|
+
<p>No tasks match the selected filter.</p>
|
|
2842
|
+
</div>
|
|
2843
|
+
`;
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
listContainer.innerHTML = filtered.map(task => {
|
|
2848
|
+
const isSuccess = task.status === 'completed';
|
|
2849
|
+
const statusColor = isSuccess ? '#10b981' : (task.status === 'failed' ? '#ef4444' : '#38bdf8');
|
|
2850
|
+
const stepCount = task.plan ? task.plan.length : 0;
|
|
2851
|
+
const completedCount = task.plan ? task.plan.filter(p => p.status === 'completed').length : 0;
|
|
2852
|
+
const artCount = task.artifacts ? task.artifacts.length : 0;
|
|
2853
|
+
|
|
2854
|
+
return `
|
|
2855
|
+
<div class="scout-card" style="padding: 16px 20px;">
|
|
2856
|
+
<div style="display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 12px; margin-bottom: 10px;">
|
|
2857
|
+
<div>
|
|
2858
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
2859
|
+
<span class="badge" style="background: rgba(255,255,255,0.05); color: ${statusColor}; border: 1px solid ${statusColor}44; font-size: 11px; font-weight: 600;">
|
|
2860
|
+
${(task.status || 'COMPLETED').toUpperCase()}
|
|
2861
|
+
</span>
|
|
2862
|
+
<span style="font-family: var(--font-mono); font-size: 11px; color: var(--scout-text-tertiary);">
|
|
2863
|
+
${escapeHtml((task.taskId || '').slice(0, 12))}
|
|
2864
|
+
</span>
|
|
2865
|
+
</div>
|
|
2866
|
+
<h4 style="font-size: 15px; margin: 6px 0 2px; color: #ffffff;">${escapeHtml(task.goal || task.objective || 'Autonomous Goal')}</h4>
|
|
2867
|
+
<span style="font-size: 12px; color: var(--scout-text-tertiary); font-family: var(--font-mono);">Started: ${timeAgo(task.created_at || task.createdAt)}</span>
|
|
2868
|
+
</div>
|
|
2869
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
2870
|
+
<span class="badge" style="font-size: 11px; background: rgba(255,255,255,0.04);"><i class="fa-solid fa-list-check"></i> ${completedCount} / ${stepCount} steps</span>
|
|
2871
|
+
${artCount > 0 ? `<span class="badge" style="font-size: 11px; background: rgba(225,91,55,0.1); color: var(--scout-rust); border: 1px solid rgba(225,91,55,0.25);"><i class="fa-solid fa-cube"></i> ${artCount} artifacts</span>` : ''}
|
|
2872
|
+
</div>
|
|
2873
|
+
</div>
|
|
2874
|
+
|
|
2875
|
+
${task.plan && task.plan.length > 0 ? `
|
|
2876
|
+
<div style="display: flex; flex-direction: column; gap: 4px; margin-top: 10px; padding: 10px; background: rgba(10,12,17,0.4); border-radius: 6px; border: 1px solid var(--scout-border-subtle);">
|
|
2877
|
+
${task.plan.slice(0, 3).map(p => `
|
|
2878
|
+
<div style="font-size: 12px; color: var(--scout-text-secondary); display: flex; align-items: center; gap: 6px;">
|
|
2879
|
+
<i class="fa-solid fa-circle-check text-success" style="font-size: 11px;"></i>
|
|
2880
|
+
<span>${escapeHtml(p.description)}</span>
|
|
2881
|
+
</div>
|
|
2882
|
+
`).join('')}
|
|
2883
|
+
${task.plan.length > 3 ? `<span style="font-size: 11px; color: var(--scout-text-tertiary); margin-left: 17px;">+ ${task.plan.length - 3} more steps executed</span>` : ''}
|
|
2884
|
+
</div>
|
|
2885
|
+
` : ''}
|
|
2886
|
+
</div>
|
|
2887
|
+
`;
|
|
2888
|
+
}).join('');
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
// Activity Timeline Renderer (Screen 7)
|
|
2893
|
+
function renderActivityTimeline(observations = []) {
|
|
2894
|
+
const overviewTimeline = getEl('overview-timeline-list');
|
|
2895
|
+
const fullTimeline = getEl('full-activity-timeline');
|
|
2896
|
+
|
|
2897
|
+
// Build default telemetry if none provided
|
|
2898
|
+
const baseEvents = [
|
|
2899
|
+
{ icon: 'shield-halved', type: 'success', title: 'Bounded Recovery & Safety Verifier Online', meta: 'Deterministic post-conditions active' },
|
|
2900
|
+
{ icon: 'desktop', type: 'desktop', title: 'Native Workstation Driver Active (scout_driver.exe)', meta: 'Win32 Coordinates & Window Perception' },
|
|
2901
|
+
{ icon: 'globe', type: 'browser', title: 'Autonomous Browser & DOM Parser Initialized', meta: 'DOM Ref IDs & Headless Engine' },
|
|
2902
|
+
{ icon: 'brain', type: 'success', title: 'Cognitive Scratchpad & DAG Planner Ready', meta: 'Workspace vector memory indexed' },
|
|
2903
|
+
];
|
|
2904
|
+
|
|
2905
|
+
const obsEvents = observations.map(obs => {
|
|
2906
|
+
let type = 'browser';
|
|
2907
|
+
let icon = 'globe';
|
|
2908
|
+
if (obs.tool === 'browser_open' || obs.tool === 'browser_scrape') { type = 'browser'; icon = 'globe'; }
|
|
2909
|
+
else if (obs.tool === 'scratchpad') { type = 'success'; icon = 'brain'; }
|
|
2910
|
+
else if (obs.tool === 'verifier') { type = 'success'; icon = 'shield-halved'; }
|
|
2911
|
+
else if (obs.tool === 'file_write') { type = 'desktop'; icon = 'file-lines'; }
|
|
2912
|
+
|
|
2913
|
+
return {
|
|
2914
|
+
icon,
|
|
2915
|
+
type,
|
|
2916
|
+
title: obs.resultSummary || `${obs.tool} executed on ${obs.target || 'workspace'}`,
|
|
2917
|
+
meta: `${timeAgo(obs.timestamp)} • Tool: ${obs.tool}`,
|
|
2918
|
+
};
|
|
2919
|
+
});
|
|
2920
|
+
|
|
2921
|
+
const allEvents = [...obsEvents, ...baseEvents];
|
|
2922
|
+
|
|
2923
|
+
const htmlString = allEvents.map(evt => `
|
|
2924
|
+
<div class="scout-timeline-item">
|
|
2925
|
+
<div class="scout-timeline-icon ${evt.type}"><i class="fa-solid fa-${evt.icon}"></i></div>
|
|
2926
|
+
<div class="scout-timeline-content">
|
|
2927
|
+
<div class="scout-timeline-title">${escapeHtml(evt.title)}</div>
|
|
2928
|
+
<div class="scout-timeline-meta">${escapeHtml(evt.meta)}</div>
|
|
2929
|
+
</div>
|
|
2930
|
+
</div>
|
|
2931
|
+
`).join('');
|
|
2932
|
+
|
|
2933
|
+
if (overviewTimeline) overviewTimeline.innerHTML = allEvents.slice(0, 4).map(evt => `
|
|
2934
|
+
<div class="scout-timeline-item">
|
|
2935
|
+
<div class="scout-timeline-icon ${evt.type}"><i class="fa-solid fa-${evt.icon}"></i></div>
|
|
2936
|
+
<div class="scout-timeline-content">
|
|
2937
|
+
<div class="scout-timeline-title">${escapeHtml(evt.title)}</div>
|
|
2938
|
+
<div class="scout-timeline-meta">${escapeHtml(evt.meta)}</div>
|
|
2939
|
+
</div>
|
|
2940
|
+
</div>
|
|
2941
|
+
`).join('');
|
|
2942
|
+
|
|
2943
|
+
if (fullTimeline) fullTimeline.innerHTML = htmlString;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// Artifacts Gallery Renderer (Screen 9)
|
|
2947
|
+
function renderArtifactsGallery(newArtifacts = []) {
|
|
2948
|
+
const container = getEl('artifacts-gallery-container');
|
|
2949
|
+
if (!container) return;
|
|
2950
|
+
|
|
2951
|
+
// Collect all artifacts from tasksHistoryStore
|
|
2952
|
+
let allArts = [];
|
|
2953
|
+
tasksHistoryStore.forEach(t => {
|
|
2954
|
+
if (t.artifacts && Array.isArray(t.artifacts)) {
|
|
2955
|
+
t.artifacts.forEach(a => {
|
|
2956
|
+
if (!allArts.some(x => x.name === a.name)) allArts.push(a);
|
|
2957
|
+
});
|
|
2958
|
+
}
|
|
2959
|
+
});
|
|
2960
|
+
|
|
2961
|
+
newArtifacts.forEach(a => {
|
|
2962
|
+
if (!allArts.some(x => x.name === a.name)) allArts.push(a);
|
|
2963
|
+
});
|
|
2964
|
+
|
|
2965
|
+
// Default static artifacts if none produced yet
|
|
2966
|
+
if (allArts.length === 0) {
|
|
2967
|
+
allArts = [
|
|
2968
|
+
{ name: 'scan-report.json', filename: 'scan-report.json', type: 'Codebase AST Map', source: 'Scout Analyzer' },
|
|
2969
|
+
{ name: 'scout-architecture.md', filename: 'scout-architecture.md', type: 'Technical Architecture Brief', source: 'Autonomous Engine' },
|
|
2970
|
+
];
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
container.innerHTML = allArts.map(art => {
|
|
2974
|
+
const name = art.name || art.filename || 'Artifact';
|
|
2975
|
+
let icon = 'fa-file-lines';
|
|
2976
|
+
if (name.endsWith('.json')) icon = 'fa-file-code';
|
|
2977
|
+
else if (name.endsWith('.md')) icon = 'fa-file-lines';
|
|
2978
|
+
else if (name.endsWith('.pdf')) icon = 'fa-file-pdf text-danger';
|
|
2979
|
+
|
|
2980
|
+
return `
|
|
2981
|
+
<div class="scout-artifact-card">
|
|
2982
|
+
<div class="scout-artifact-top">
|
|
2983
|
+
<div class="scout-artifact-icon"><i class="fa-solid ${icon}"></i></div>
|
|
2984
|
+
<div class="scout-artifact-details">
|
|
2985
|
+
<div class="scout-artifact-name">${escapeHtml(name)}</div>
|
|
2986
|
+
<div class="scout-artifact-desc">${escapeHtml(art.type || 'Generated Deliverable')}</div>
|
|
2987
|
+
</div>
|
|
2988
|
+
</div>
|
|
2989
|
+
<div class="scout-artifact-footer">
|
|
2990
|
+
<span>${escapeHtml(art.source || 'Scout Operator')}</span>
|
|
2991
|
+
<button type="button" class="btn btn-xs btn-outline" onclick="showToast('Artifact copied to clipboard', 'success')"><i class="fa-solid fa-copy"></i> Copy</button>
|
|
2992
|
+
</div>
|
|
2993
|
+
</div>
|
|
2994
|
+
`;
|
|
2995
|
+
}).join('');
|
|
2452
2996
|
}
|
|
2453
2997
|
|
|
2998
|
+
// Enterprise Workstation View Router
|
|
2999
|
+
function initWorkstationRouter() {
|
|
3000
|
+
const viewLinks = document.querySelectorAll('[data-view]');
|
|
3001
|
+
const settingsTabs = document.querySelectorAll('.scout-settings-tab');
|
|
3002
|
+
const mobileToggle = getEl('btn-mobile-sidebar-toggle');
|
|
3003
|
+
const sidebar = getEl('scout-sidebar');
|
|
3004
|
+
const breadcrumbView = getEl('topbar-breadcrumb-view');
|
|
3005
|
+
const chipButtons = document.querySelectorAll('.scout-suggestion-chip');
|
|
3006
|
+
const modelSelect = getEl('composer-model-select');
|
|
3007
|
+
const taskSearch = getEl('input-task-search');
|
|
3008
|
+
const filterBtns = document.querySelectorAll('.task-filter-btn');
|
|
3009
|
+
|
|
3010
|
+
function switchView(viewName) {
|
|
3011
|
+
if (!viewName) viewName = 'overview';
|
|
3012
|
+
const cleanName = viewName.replace('#', '').trim();
|
|
3013
|
+
|
|
3014
|
+
// 1. Switch View Containers
|
|
3015
|
+
let viewElement = getEl(`view-${cleanName}`);
|
|
3016
|
+
if (!viewElement) {
|
|
3017
|
+
cleanName === 'overview';
|
|
3018
|
+
viewElement = getEl('view-overview');
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
document.querySelectorAll('.scout-view').forEach(v => v.classList.remove('active'));
|
|
3022
|
+
if (viewElement) viewElement.classList.add('active');
|
|
3023
|
+
|
|
3024
|
+
// 2. Update Sidebar Active Navigation
|
|
3025
|
+
document.querySelectorAll('.scout-nav-item').forEach(item => {
|
|
3026
|
+
const itemTarget = item.getAttribute('data-view') || item.getAttribute('href')?.replace('#', '');
|
|
3027
|
+
if (itemTarget === cleanName || (cleanName.startsWith('models') && itemTarget === 'models') || (cleanName.startsWith('usage') && itemTarget === 'usage') || (cleanName.startsWith('account') && itemTarget === 'account')) {
|
|
3028
|
+
item.classList.add('active');
|
|
3029
|
+
} else {
|
|
3030
|
+
item.classList.remove('active');
|
|
3031
|
+
}
|
|
3032
|
+
});
|
|
3033
|
+
|
|
3034
|
+
// 3. Update Settings Subnav Tabs
|
|
3035
|
+
document.querySelectorAll('.scout-settings-tab').forEach(tab => {
|
|
3036
|
+
const tabTarget = tab.getAttribute('href')?.replace('#', '');
|
|
3037
|
+
if (tabTarget === cleanName) tab.classList.add('active');
|
|
3038
|
+
else tab.classList.remove('active');
|
|
3039
|
+
});
|
|
3040
|
+
|
|
3041
|
+
// 4. Update Topbar Breadcrumb
|
|
3042
|
+
if (breadcrumbView) {
|
|
3043
|
+
const labels = {
|
|
3044
|
+
overview: 'Overview',
|
|
3045
|
+
tasks: 'Tasks',
|
|
3046
|
+
activity: 'Activity',
|
|
3047
|
+
artifacts: 'Artifacts',
|
|
3048
|
+
connections: 'Connections',
|
|
3049
|
+
computer: 'Computer Use',
|
|
3050
|
+
cli: 'Web CLI',
|
|
3051
|
+
models: 'Models & BYOK',
|
|
3052
|
+
usage: 'Usage & Quotas',
|
|
3053
|
+
account: 'Account Profile',
|
|
3054
|
+
};
|
|
3055
|
+
breadcrumbView.innerText = labels[cleanName] || 'Overview';
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
if (cleanName === 'connections') {
|
|
3059
|
+
loadMcpData();
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
// Close mobile sidebar if opened
|
|
3063
|
+
sidebar?.classList.remove('open');
|
|
3064
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
// Hashchange listener
|
|
3068
|
+
window.addEventListener('hashchange', () => {
|
|
3069
|
+
const hash = window.location.hash || '#overview';
|
|
3070
|
+
switchView(hash);
|
|
3071
|
+
});
|
|
3072
|
+
|
|
3073
|
+
// Initial view from hash
|
|
3074
|
+
const initialHash = window.location.hash || '#overview';
|
|
3075
|
+
switchView(initialHash);
|
|
3076
|
+
|
|
3077
|
+
// Mobile Sidebar Drawer Toggle
|
|
3078
|
+
mobileToggle?.addEventListener('click', () => {
|
|
3079
|
+
sidebar?.classList.toggle('open');
|
|
3080
|
+
});
|
|
3081
|
+
|
|
3082
|
+
// Close sidebar on background click
|
|
3083
|
+
document.addEventListener('click', (e) => {
|
|
3084
|
+
if (sidebar?.classList.contains('open') && !sidebar.contains(e.target) && !mobileToggle?.contains(e.target)) {
|
|
3085
|
+
sidebar.classList.remove('open');
|
|
3086
|
+
}
|
|
3087
|
+
});
|
|
3088
|
+
|
|
3089
|
+
// Quick sign out from sidebar
|
|
3090
|
+
getEl('btn-sidebar-quick-logout')?.addEventListener('click', handleLogout);
|
|
3091
|
+
|
|
3092
|
+
// Suggestion Chips Click Handling
|
|
3093
|
+
chipButtons.forEach(chip => {
|
|
3094
|
+
chip.addEventListener('click', () => {
|
|
3095
|
+
const prompt = chip.getAttribute('data-prompt');
|
|
3096
|
+
const input = getEl('agent-goal-input');
|
|
3097
|
+
if (prompt && input) {
|
|
3098
|
+
input.value = prompt;
|
|
3099
|
+
input.focus();
|
|
3100
|
+
}
|
|
3101
|
+
});
|
|
3102
|
+
});
|
|
3103
|
+
|
|
3104
|
+
// Composer Model Select Sync
|
|
3105
|
+
modelSelect?.addEventListener('change', (e) => {
|
|
3106
|
+
const val = e.target.value;
|
|
3107
|
+
const topbarModel = getEl('topbar-engine-model');
|
|
3108
|
+
const byokSelect = getEl('select-byok-provider');
|
|
3109
|
+
|
|
3110
|
+
if (topbarModel) {
|
|
3111
|
+
const names = {
|
|
3112
|
+
nvidia: 'Nemotron 70B',
|
|
3113
|
+
groq: 'Llama 3.3 70B',
|
|
3114
|
+
gemini: 'Gemini 2.5 Flash',
|
|
3115
|
+
openai: 'GPT-4o',
|
|
3116
|
+
anthropic: 'Claude 3.7 Sonnet',
|
|
3117
|
+
custom: 'Local LLM / Proxy',
|
|
3118
|
+
};
|
|
3119
|
+
topbarModel.innerText = names[val] || val.toUpperCase();
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
if (byokSelect) byokSelect.value = val;
|
|
3123
|
+
state.customLlmProvider = val;
|
|
3124
|
+
});
|
|
3125
|
+
|
|
3126
|
+
// Tasks Search & Filter
|
|
3127
|
+
taskSearch?.addEventListener('input', (e) => {
|
|
3128
|
+
const query = e.target.value;
|
|
3129
|
+
const activeFilter = document.querySelector('.task-filter-btn.active')?.dataset?.filter || 'all';
|
|
3130
|
+
renderTasksList(activeFilter, query);
|
|
3131
|
+
});
|
|
3132
|
+
|
|
3133
|
+
filterBtns.forEach(btn => {
|
|
3134
|
+
btn.addEventListener('click', () => {
|
|
3135
|
+
filterBtns.forEach(b => b.classList.remove('active'));
|
|
3136
|
+
btn.classList.add('active');
|
|
3137
|
+
const filter = btn.dataset.filter || 'all';
|
|
3138
|
+
const query = taskSearch?.value || '';
|
|
3139
|
+
renderTasksList(filter, query);
|
|
3140
|
+
});
|
|
3141
|
+
});
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
// ==========================================================================
|
|
3145
|
+
// Model Context Protocol (MCP) Ecosystem Controller
|
|
3146
|
+
// ==========================================================================
|
|
3147
|
+
|
|
3148
|
+
const mcpState = {
|
|
3149
|
+
servers: [],
|
|
3150
|
+
activity: [],
|
|
3151
|
+
health: {},
|
|
3152
|
+
selectedServer: null,
|
|
3153
|
+
loading: false
|
|
3154
|
+
};
|
|
3155
|
+
|
|
3156
|
+
async function loadMcpData() {
|
|
3157
|
+
try {
|
|
3158
|
+
const [serversRes, healthRes, activityRes] = await Promise.all([
|
|
3159
|
+
fetch('/api/mcp/servers').then(r => r.json()).catch(() => ({ success: false, servers: [] })),
|
|
3160
|
+
fetch('/api/mcp/health').then(r => r.json()).catch(() => ({ success: false, health: {} })),
|
|
3161
|
+
fetch('/api/mcp/activity?limit=30').then(r => r.json()).catch(() => ({ success: false, activity: [] }))
|
|
3162
|
+
]);
|
|
3163
|
+
|
|
3164
|
+
if (serversRes.success && Array.isArray(serversRes.servers)) {
|
|
3165
|
+
mcpState.servers = serversRes.servers;
|
|
3166
|
+
}
|
|
3167
|
+
if (healthRes.success && healthRes.health) {
|
|
3168
|
+
mcpState.health = healthRes.health;
|
|
3169
|
+
}
|
|
3170
|
+
if (activityRes.success && Array.isArray(activityRes.activity)) {
|
|
3171
|
+
mcpState.activity = activityRes.activity;
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
renderMcpKpis();
|
|
3175
|
+
renderMcpServersList();
|
|
3176
|
+
renderMcpActivityList();
|
|
3177
|
+
} catch (err) {
|
|
3178
|
+
console.error('Failed to load MCP ecosystem data:', err);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
function renderMcpKpis() {
|
|
3183
|
+
const serversEl = getEl('mcp-stat-servers');
|
|
3184
|
+
const toolsEl = getEl('mcp-stat-tools');
|
|
3185
|
+
const invocationsEl = getEl('mcp-stat-invocations');
|
|
3186
|
+
const healthEl = getEl('mcp-stat-health');
|
|
3187
|
+
const sidebarBadge = getEl('sidebar-mcp-count');
|
|
3188
|
+
|
|
3189
|
+
const totalServers = mcpState.servers.length;
|
|
3190
|
+
let totalTools = 0;
|
|
3191
|
+
let connectedCount = 0;
|
|
3192
|
+
|
|
3193
|
+
mcpState.servers.forEach(s => {
|
|
3194
|
+
if (s.tools && Array.isArray(s.tools)) {
|
|
3195
|
+
totalTools += s.tools.filter(t => t.enabled !== false).length;
|
|
3196
|
+
}
|
|
3197
|
+
if (s.status === 'connected') {
|
|
3198
|
+
connectedCount++;
|
|
3199
|
+
}
|
|
3200
|
+
});
|
|
3201
|
+
|
|
3202
|
+
if (serversEl) serversEl.innerText = totalServers;
|
|
3203
|
+
if (toolsEl) toolsEl.innerText = totalTools;
|
|
3204
|
+
if (invocationsEl) invocationsEl.innerText = mcpState.activity.length;
|
|
3205
|
+
if (sidebarBadge) sidebarBadge.innerText = totalServers;
|
|
3206
|
+
|
|
3207
|
+
if (healthEl) {
|
|
3208
|
+
if (totalServers === 0) {
|
|
3209
|
+
healthEl.innerText = 'Idle';
|
|
3210
|
+
healthEl.style.color = 'var(--scout-text-dim)';
|
|
3211
|
+
} else {
|
|
3212
|
+
const healthPct = Math.round((connectedCount / totalServers) * 100);
|
|
3213
|
+
healthEl.innerText = `${healthPct}%`;
|
|
3214
|
+
healthEl.style.color = healthPct === 100 ? '#10b981' : (healthPct > 50 ? '#f59e0b' : '#ef4444');
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
function renderMcpServersList() {
|
|
3220
|
+
const container = getEl('mcp-servers-list');
|
|
3221
|
+
if (!container) return;
|
|
3222
|
+
|
|
3223
|
+
if (mcpState.servers.length === 0) {
|
|
3224
|
+
container.innerHTML = `
|
|
3225
|
+
<div style="padding: 48px 24px; text-align: center;">
|
|
3226
|
+
<div style="width: 56px; height: 56px; border-radius: 14px; background: rgba(0, 229, 255, 0.08); border: 1px solid rgba(0, 229, 255, 0.2); color: #00e5ff; display: inline-flex; align-items: center; justify-content: center; font-size: 24px; margin-bottom: 16px;">
|
|
3227
|
+
<i class="fa-solid fa-cube"></i>
|
|
3228
|
+
</div>
|
|
3229
|
+
<h4 style="font-size: 16px; font-weight: 700; color: #fff; margin: 0 0 8px 0;">No External MCP Servers Connected</h4>
|
|
3230
|
+
<p style="font-size: 13px; color: var(--scout-text-secondary); max-width: 480px; margin: 0 auto 20px auto; line-height: 1.5;">
|
|
3231
|
+
Connect Scout to standard-compliant MCP servers (Motion, Higgsfield, GitHub, Figma, Slack, or local tools) to provide the autonomous agent with custom tools.
|
|
3232
|
+
</p>
|
|
3233
|
+
<button type="button" class="btn btn-primary" onclick="openAddMcpModal()">
|
|
3234
|
+
<i class="fa-solid fa-plus"></i> Connect First MCP Server
|
|
3235
|
+
</button>
|
|
3236
|
+
</div>
|
|
3237
|
+
`;
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
const cardsHtml = mcpState.servers.map(server => {
|
|
3242
|
+
const isConnected = server.status === 'connected';
|
|
3243
|
+
const isConnecting = server.status === 'connecting';
|
|
3244
|
+
const isUnauthorized = server.status === 'unauthorized';
|
|
3245
|
+
const isError = server.status === 'error';
|
|
3246
|
+
|
|
3247
|
+
let badgeClass = 'disconnected';
|
|
3248
|
+
let badgeLabel = 'Disconnected';
|
|
3249
|
+
if (isConnected) {
|
|
3250
|
+
badgeClass = 'connected';
|
|
3251
|
+
badgeLabel = 'Connected';
|
|
3252
|
+
} else if (isConnecting) {
|
|
3253
|
+
badgeClass = 'connecting';
|
|
3254
|
+
badgeLabel = 'Connecting...';
|
|
3255
|
+
} else if (isUnauthorized) {
|
|
3256
|
+
badgeClass = 'unauthorized';
|
|
3257
|
+
badgeLabel = 'Unauthorized';
|
|
3258
|
+
} else if (isError) {
|
|
3259
|
+
badgeClass = 'error';
|
|
3260
|
+
badgeLabel = 'Offline';
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
const h = mcpState.health[server.id] || server.health || {};
|
|
3264
|
+
const latencyText = h.latencyMs !== undefined ? `${h.latencyMs}ms` : '—';
|
|
3265
|
+
const toolsCount = server.tools ? server.tools.length : 0;
|
|
3266
|
+
const activeToolsCount = server.tools ? server.tools.filter(t => t.enabled !== false).length : 0;
|
|
3267
|
+
const isHttp = server.transport === 'http';
|
|
3268
|
+
const endpointText = isHttp ? (server.endpoint || 'No endpoint') : `${server.command || ''} ${(server.args || []).join(' ')}`;
|
|
3269
|
+
|
|
3270
|
+
return `
|
|
3271
|
+
<div class="mcp-server-card" data-server-id="${escapeHtml(server.id)}">
|
|
3272
|
+
<div class="mcp-server-header">
|
|
3273
|
+
<div class="mcp-server-title-area">
|
|
3274
|
+
<div class="mcp-server-avatar ${isHttp ? '' : 'stdio'}">
|
|
3275
|
+
<i class="fa-solid ${isHttp ? 'fa-globe' : 'fa-terminal'}"></i>
|
|
3276
|
+
</div>
|
|
3277
|
+
<div>
|
|
3278
|
+
<h4 class="mcp-server-name">
|
|
3279
|
+
${escapeHtml(server.name)}
|
|
3280
|
+
<span class="badge ${isHttp ? 'badge-accent' : 'badge-primary'}" style="font-size: 10px; text-transform: uppercase;">${escapeHtml(server.transport)}</span>
|
|
3281
|
+
</h4>
|
|
3282
|
+
<div class="mcp-server-endpoint" title="${escapeHtml(endpointText)}">${escapeHtml(endpointText)}</div>
|
|
3283
|
+
</div>
|
|
3284
|
+
</div>
|
|
3285
|
+
<span class="mcp-badge ${badgeClass}">
|
|
3286
|
+
<span class="mcp-pulse-dot"></span>
|
|
3287
|
+
${badgeLabel}
|
|
3288
|
+
</span>
|
|
3289
|
+
</div>
|
|
3290
|
+
|
|
3291
|
+
<div class="mcp-server-meta-grid">
|
|
3292
|
+
<div class="mcp-meta-item">
|
|
3293
|
+
<span class="mcp-meta-label">Active Tools</span>
|
|
3294
|
+
<span class="mcp-meta-val">${activeToolsCount} / ${toolsCount}</span>
|
|
3295
|
+
</div>
|
|
3296
|
+
<div class="mcp-meta-item">
|
|
3297
|
+
<span class="mcp-meta-label">Ping / Latency</span>
|
|
3298
|
+
<span class="mcp-meta-val">${escapeHtml(latencyText)}</span>
|
|
3299
|
+
</div>
|
|
3300
|
+
<div class="mcp-meta-item">
|
|
3301
|
+
<span class="mcp-meta-label">Auth Guard</span>
|
|
3302
|
+
<span class="mcp-meta-val">${escapeHtml(server.authType || 'none')}</span>
|
|
3303
|
+
</div>
|
|
3304
|
+
</div>
|
|
3305
|
+
|
|
3306
|
+
<div class="mcp-server-footer">
|
|
3307
|
+
<div style="display: flex; gap: 8px;">
|
|
3308
|
+
${isConnected ? `
|
|
3309
|
+
<button type="button" class="btn btn-xs btn-outline" onclick="disconnectMcpServer('${escapeHtml(server.id)}')">
|
|
3310
|
+
<i class="fa-solid fa-power-off"></i> Disconnect
|
|
3311
|
+
</button>
|
|
3312
|
+
` : `
|
|
3313
|
+
<button type="button" class="btn btn-xs btn-primary" onclick="connectMcpServer('${escapeHtml(server.id)}')">
|
|
3314
|
+
<i class="fa-solid fa-plug"></i> Connect
|
|
3315
|
+
</button>
|
|
3316
|
+
`}
|
|
3317
|
+
<button type="button" class="btn btn-xs btn-outline" onclick="openInspectMcpModal('${escapeHtml(server.id)}')">
|
|
3318
|
+
<i class="fa-solid fa-magnifying-glass"></i> Tools (${toolsCount})
|
|
3319
|
+
</button>
|
|
3320
|
+
</div>
|
|
3321
|
+
<button type="button" class="btn btn-xs btn-outline text-danger" title="Remove MCP Server" onclick="deleteMcpServer('${escapeHtml(server.id)}')">
|
|
3322
|
+
<i class="fa-solid fa-trash"></i>
|
|
3323
|
+
</button>
|
|
3324
|
+
</div>
|
|
3325
|
+
</div>
|
|
3326
|
+
`;
|
|
3327
|
+
}).join('');
|
|
3328
|
+
|
|
3329
|
+
container.innerHTML = `<div class="mcp-servers-grid">${cardsHtml}</div>`;
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
function renderMcpActivityList() {
|
|
3333
|
+
const container = getEl('mcp-activity-list');
|
|
3334
|
+
const counter = getEl('mcp-activity-counter');
|
|
3335
|
+
if (!container) return;
|
|
3336
|
+
|
|
3337
|
+
if (counter) counter.innerText = `${mcpState.activity.length} events`;
|
|
3338
|
+
|
|
3339
|
+
if (mcpState.activity.length === 0) {
|
|
3340
|
+
container.innerHTML = `
|
|
3341
|
+
<div style="padding: 24px; text-align: center; color: var(--scout-text-dim); font-size: 13px;">
|
|
3342
|
+
No MCP tool executions logged yet. As Scout solves goals, external tool calls will appear here.
|
|
3343
|
+
</div>
|
|
3344
|
+
`;
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
container.innerHTML = mcpState.activity.map(evt => {
|
|
3349
|
+
const isSuccess = evt.status === 'success';
|
|
3350
|
+
return `
|
|
3351
|
+
<div style="display: flex; align-items: center; justify-content: space-between; padding: 10px 16px; border-bottom: 1px solid var(--scout-border-subtle); font-size: 12px; gap: 12px;">
|
|
3352
|
+
<div style="display: flex; align-items: center; gap: 10px; min-width: 0;">
|
|
3353
|
+
<span style="color: ${isSuccess ? 'var(--hr-emerald)' : 'var(--hr-rose)'}; font-size: 14px;">
|
|
3354
|
+
<i class="fa-solid ${isSuccess ? 'fa-circle-check' : 'fa-circle-xmark'}"></i>
|
|
3355
|
+
</span>
|
|
3356
|
+
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
|
3357
|
+
<strong style="color: #fff; font-family: var(--font-mono);">${escapeHtml(evt.toolName)}</strong>
|
|
3358
|
+
<span style="color: var(--scout-text-dim); margin-left: 6px;">(${escapeHtml(evt.serverName || evt.serverId)})</span>
|
|
3359
|
+
</div>
|
|
3360
|
+
</div>
|
|
3361
|
+
<div style="display: flex; align-items: center; gap: 12px; flex-shrink: 0;">
|
|
3362
|
+
<span style="font-family: var(--font-mono); color: var(--scout-text-dim);">${evt.durationMs !== undefined ? `${evt.durationMs}ms` : ''}</span>
|
|
3363
|
+
<span class="badge ${isSuccess ? 'badge-success' : 'badge-error'}" style="font-size: 10px; text-transform: uppercase;">${escapeHtml(evt.status)}</span>
|
|
3364
|
+
<span style="color: var(--scout-text-dim); font-size: 11px;">${escapeHtml(timeAgo(evt.timestamp))}</span>
|
|
3365
|
+
</div>
|
|
3366
|
+
</div>
|
|
3367
|
+
`;
|
|
3368
|
+
}).join('');
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
// Modal controls
|
|
3372
|
+
function openAddMcpModal() {
|
|
3373
|
+
const modal = getEl('modal-add-mcp');
|
|
3374
|
+
if (modal) {
|
|
3375
|
+
modal.style.display = 'flex';
|
|
3376
|
+
toggleMcpTransportFields();
|
|
3377
|
+
toggleMcpAuthFields();
|
|
3378
|
+
getEl('mcp-input-name')?.focus();
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
function closeAddMcpModal() {
|
|
3383
|
+
const modal = getEl('modal-add-mcp');
|
|
3384
|
+
if (modal) modal.style.display = 'none';
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
function toggleMcpTransportFields() {
|
|
3388
|
+
const selected = document.querySelector('input[name="mcp-transport"]:checked')?.value || 'http';
|
|
3389
|
+
const httpFields = getEl('mcp-http-fields');
|
|
3390
|
+
const stdioFields = getEl('mcp-stdio-fields');
|
|
3391
|
+
|
|
3392
|
+
if (selected === 'http') {
|
|
3393
|
+
if (httpFields) httpFields.style.display = 'block';
|
|
3394
|
+
if (stdioFields) stdioFields.style.display = 'none';
|
|
3395
|
+
} else {
|
|
3396
|
+
if (httpFields) httpFields.style.display = 'none';
|
|
3397
|
+
if (stdioFields) stdioFields.style.display = 'block';
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
function toggleMcpAuthFields() {
|
|
3402
|
+
const authType = getEl('mcp-select-auth')?.value || 'none';
|
|
3403
|
+
const headerField = getEl('mcp-header-field');
|
|
3404
|
+
const tokenField = getEl('mcp-token-field');
|
|
3405
|
+
|
|
3406
|
+
if (authType === 'none') {
|
|
3407
|
+
if (headerField) headerField.style.display = 'none';
|
|
3408
|
+
if (tokenField) tokenField.style.display = 'none';
|
|
3409
|
+
} else if (authType === 'bearer') {
|
|
3410
|
+
if (headerField) headerField.style.display = 'none';
|
|
3411
|
+
if (tokenField) tokenField.style.display = 'block';
|
|
3412
|
+
} else if (authType === 'apiKey') {
|
|
3413
|
+
if (headerField) headerField.style.display = 'block';
|
|
3414
|
+
if (tokenField) tokenField.style.display = 'block';
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
async function handleAddMcpServerSubmit(e) {
|
|
3419
|
+
if (e) e.preventDefault();
|
|
3420
|
+
|
|
3421
|
+
const name = getEl('mcp-input-name')?.value.trim();
|
|
3422
|
+
const transport = document.querySelector('input[name="mcp-transport"]:checked')?.value || 'http';
|
|
3423
|
+
const endpoint = getEl('mcp-input-endpoint')?.value.trim();
|
|
3424
|
+
const command = getEl('mcp-input-command')?.value.trim();
|
|
3425
|
+
const argsRaw = getEl('mcp-input-args')?.value.trim() || '';
|
|
3426
|
+
const authType = getEl('mcp-select-auth')?.value || 'none';
|
|
3427
|
+
const headerKey = getEl('mcp-input-header')?.value.trim();
|
|
3428
|
+
const token = getEl('mcp-input-token')?.value;
|
|
3429
|
+
const autoApproveSafe = getEl('mcp-perm-auto-approve')?.checked ?? true;
|
|
3430
|
+
const requireConfirmationForDestructive = getEl('mcp-perm-require-confirm')?.checked ?? true;
|
|
3431
|
+
const autoConnect = getEl('mcp-auto-connect')?.checked ?? true;
|
|
3432
|
+
|
|
3433
|
+
if (!name) {
|
|
3434
|
+
showToast('Please specify a server name.', 'error');
|
|
3435
|
+
return;
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
if (transport === 'http' && !endpoint) {
|
|
3439
|
+
showToast('Please specify an HTTP endpoint URL.', 'error');
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
if (transport === 'stdio' && !command) {
|
|
3444
|
+
showToast('Please specify an executable command.', 'error');
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
const args = argsRaw.length > 0 ? argsRaw.split(/\s+/).filter(Boolean) : [];
|
|
3449
|
+
|
|
3450
|
+
const submitBtn = getEl('btn-submit-add-mcp');
|
|
3451
|
+
if (submitBtn) {
|
|
3452
|
+
submitBtn.disabled = true;
|
|
3453
|
+
submitBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Connecting...';
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
try {
|
|
3457
|
+
const payload = {
|
|
3458
|
+
name,
|
|
3459
|
+
transport,
|
|
3460
|
+
endpoint: transport === 'http' ? endpoint : undefined,
|
|
3461
|
+
command: transport === 'stdio' ? command : undefined,
|
|
3462
|
+
args: transport === 'stdio' ? args : undefined,
|
|
3463
|
+
authType,
|
|
3464
|
+
headerKey: authType === 'apiKey' ? headerKey : undefined,
|
|
3465
|
+
token: authType !== 'none' ? token : undefined,
|
|
3466
|
+
permissions: {
|
|
3467
|
+
autoApproveSafe,
|
|
3468
|
+
requireConfirmationForDestructive,
|
|
3469
|
+
},
|
|
3470
|
+
autoConnect,
|
|
3471
|
+
};
|
|
3472
|
+
|
|
3473
|
+
const res = await fetch('/api/mcp/servers', {
|
|
3474
|
+
method: 'POST',
|
|
3475
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3476
|
+
body: JSON.stringify(payload)
|
|
3477
|
+
});
|
|
3478
|
+
|
|
3479
|
+
const data = await res.json();
|
|
3480
|
+
if (data.success) {
|
|
3481
|
+
showToast(`Connected to MCP server "${name}"! Discovered ${data.server.tools?.length || 0} tools.`, 'success');
|
|
3482
|
+
closeAddMcpModal();
|
|
3483
|
+
getEl('form-add-mcp')?.reset();
|
|
3484
|
+
await loadMcpData();
|
|
3485
|
+
} else {
|
|
3486
|
+
showToast(data.error || 'Failed to add MCP server', 'error');
|
|
3487
|
+
}
|
|
3488
|
+
} catch (err) {
|
|
3489
|
+
showToast(err.message || 'Error connecting MCP server', 'error');
|
|
3490
|
+
} finally {
|
|
3491
|
+
if (submitBtn) {
|
|
3492
|
+
submitBtn.disabled = false;
|
|
3493
|
+
submitBtn.innerHTML = '<i class="fa-solid fa-plug"></i> Connect Server';
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
function openInspectMcpModal(serverId) {
|
|
3499
|
+
const server = mcpState.servers.find(s => s.id === serverId);
|
|
3500
|
+
if (!server) {
|
|
3501
|
+
showToast('Server not found', 'error');
|
|
3502
|
+
return;
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
mcpState.selectedServer = server;
|
|
3506
|
+
const titleEl = getEl('inspect-mcp-title');
|
|
3507
|
+
const subtitleEl = getEl('inspect-mcp-subtitle');
|
|
3508
|
+
const contentEl = getEl('inspect-mcp-content');
|
|
3509
|
+
|
|
3510
|
+
if (titleEl) titleEl.innerText = `${server.name} — Tools & Capabilities`;
|
|
3511
|
+
if (subtitleEl) subtitleEl.innerText = `${server.transport.toUpperCase()} Transport • ${server.tools.length} Tools Available`;
|
|
3512
|
+
|
|
3513
|
+
if (contentEl) {
|
|
3514
|
+
if (!server.tools || server.tools.length === 0) {
|
|
3515
|
+
contentEl.innerHTML = `
|
|
3516
|
+
<div style="padding: 32px; text-align: center; color: var(--scout-text-dim);">
|
|
3517
|
+
<i class="fa-solid fa-toolbox" style="font-size: 28px; margin-bottom: 12px; display: block; opacity: 0.5;"></i>
|
|
3518
|
+
No tools discovered yet. Ensure the MCP server is reachable and active.
|
|
3519
|
+
</div>
|
|
3520
|
+
`;
|
|
3521
|
+
} else {
|
|
3522
|
+
contentEl.innerHTML = server.tools.map(tool => {
|
|
3523
|
+
const isEnabled = tool.enabled !== false;
|
|
3524
|
+
const perm = (tool.permissionLevel || 'read').toLowerCase();
|
|
3525
|
+
const props = Object.keys(tool.inputSchema?.properties || {});
|
|
3526
|
+
const propsText = props.length > 0 ? `Parameters: ${props.join(', ')}` : 'No required parameters';
|
|
3527
|
+
|
|
3528
|
+
return `
|
|
3529
|
+
<div class="mcp-tool-item">
|
|
3530
|
+
<div class="mcp-tool-info">
|
|
3531
|
+
<div class="mcp-tool-title-row">
|
|
3532
|
+
<span class="mcp-tool-name">${escapeHtml(tool.originalName)}</span>
|
|
3533
|
+
<span class="mcp-perm-tag ${perm}">${perm}</span>
|
|
3534
|
+
</div>
|
|
3535
|
+
<p class="mcp-tool-desc">${escapeHtml(tool.description || 'No description provided.')}</p>
|
|
3536
|
+
<div class="mcp-tool-params">${escapeHtml(propsText)}</div>
|
|
3537
|
+
</div>
|
|
3538
|
+
<div>
|
|
3539
|
+
<label class="mcp-toggle-switch" title="${isEnabled ? 'Tool Enabled' : 'Tool Disabled'}">
|
|
3540
|
+
<input type="checkbox" ${isEnabled ? 'checked' : ''} onchange="toggleMcpTool('${escapeHtml(server.id)}', '${escapeHtml(tool.originalName)}', this.checked)">
|
|
3541
|
+
<span class="mcp-toggle-slider"></span>
|
|
3542
|
+
</label>
|
|
3543
|
+
</div>
|
|
3544
|
+
</div>
|
|
3545
|
+
`;
|
|
3546
|
+
}).join('');
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
const modal = getEl('modal-inspect-mcp');
|
|
3551
|
+
if (modal) modal.style.display = 'flex';
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
function closeInspectMcpModal() {
|
|
3555
|
+
const modal = getEl('modal-inspect-mcp');
|
|
3556
|
+
if (modal) modal.style.display = 'none';
|
|
3557
|
+
mcpState.selectedServer = null;
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
async function connectMcpServer(serverId) {
|
|
3561
|
+
try {
|
|
3562
|
+
showToast('Connecting to MCP server...', 'info');
|
|
3563
|
+
const res = await fetch(`/api/mcp/servers/${encodeURIComponent(serverId)}/connect`, { method: 'POST' });
|
|
3564
|
+
const data = await res.json();
|
|
3565
|
+
if (data.success) {
|
|
3566
|
+
showToast(`Connected! Discovered ${data.server.tools?.length || 0} tools.`, 'success');
|
|
3567
|
+
await loadMcpData();
|
|
3568
|
+
} else {
|
|
3569
|
+
showToast(data.error || 'Failed to connect server', 'error');
|
|
3570
|
+
}
|
|
3571
|
+
} catch (err) {
|
|
3572
|
+
showToast(err.message || 'Error connecting to server', 'error');
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
async function disconnectMcpServer(serverId) {
|
|
3577
|
+
try {
|
|
3578
|
+
const res = await fetch(`/api/mcp/servers/${encodeURIComponent(serverId)}/disconnect`, { method: 'POST' });
|
|
3579
|
+
const data = await res.json();
|
|
3580
|
+
if (data.success) {
|
|
3581
|
+
showToast('MCP server disconnected.', 'info');
|
|
3582
|
+
await loadMcpData();
|
|
3583
|
+
} else {
|
|
3584
|
+
showToast(data.error || 'Failed to disconnect server', 'error');
|
|
3585
|
+
}
|
|
3586
|
+
} catch (err) {
|
|
3587
|
+
showToast(err.message || 'Error disconnecting server', 'error');
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
async function deleteMcpServer(serverId) {
|
|
3592
|
+
if (!confirm('Are you sure you want to remove this MCP server? Associated credentials will be permanently deleted.')) {
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
try {
|
|
3596
|
+
const res = await fetch(`/api/mcp/servers/${encodeURIComponent(serverId)}`, { method: 'DELETE' });
|
|
3597
|
+
const data = await res.json();
|
|
3598
|
+
if (data.success) {
|
|
3599
|
+
showToast('MCP server removed.', 'info');
|
|
3600
|
+
await loadMcpData();
|
|
3601
|
+
} else {
|
|
3602
|
+
showToast(data.error || 'Failed to delete server', 'error');
|
|
3603
|
+
}
|
|
3604
|
+
} catch (err) {
|
|
3605
|
+
showToast(err.message || 'Error deleting server', 'error');
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
async function toggleMcpTool(serverId, toolName, enabled) {
|
|
3610
|
+
try {
|
|
3611
|
+
const res = await fetch(`/api/mcp/servers/${encodeURIComponent(serverId)}/tools/${encodeURIComponent(toolName)}/toggle`, {
|
|
3612
|
+
method: 'POST',
|
|
3613
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3614
|
+
body: JSON.stringify({ enabled })
|
|
3615
|
+
});
|
|
3616
|
+
const data = await res.json();
|
|
3617
|
+
if (data.success) {
|
|
3618
|
+
showToast(`Tool "${toolName}" ${enabled ? 'enabled' : 'disabled'}.`, 'info');
|
|
3619
|
+
const server = mcpState.servers.find(s => s.id === serverId);
|
|
3620
|
+
if (server && server.tools) {
|
|
3621
|
+
const target = server.tools.find(t => t.originalName === toolName || t.name === toolName);
|
|
3622
|
+
if (target) target.enabled = enabled;
|
|
3623
|
+
}
|
|
3624
|
+
renderMcpKpis();
|
|
3625
|
+
renderMcpServersList();
|
|
3626
|
+
} else {
|
|
3627
|
+
showToast(data.error || 'Failed to toggle tool', 'error');
|
|
3628
|
+
}
|
|
3629
|
+
} catch (err) {
|
|
3630
|
+
showToast(err.message || 'Error toggling tool', 'error');
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
// Global exports for inline HTML event handlers
|
|
3635
|
+
window.loadMcpData = loadMcpData;
|
|
3636
|
+
window.openAddMcpModal = openAddMcpModal;
|
|
3637
|
+
window.closeAddMcpModal = closeAddMcpModal;
|
|
3638
|
+
window.toggleMcpTransportFields = toggleMcpTransportFields;
|
|
3639
|
+
window.toggleMcpAuthFields = toggleMcpAuthFields;
|
|
3640
|
+
window.handleAddMcpServerSubmit = handleAddMcpServerSubmit;
|
|
3641
|
+
window.openInspectMcpModal = openInspectMcpModal;
|
|
3642
|
+
window.closeInspectMcpModal = closeInspectMcpModal;
|
|
3643
|
+
window.connectMcpServer = connectMcpServer;
|
|
3644
|
+
window.disconnectMcpServer = disconnectMcpServer;
|
|
3645
|
+
window.deleteMcpServer = deleteMcpServer;
|
|
3646
|
+
window.toggleMcpTool = toggleMcpTool;
|
|
3647
|
+
|
|
3648
|
+
|
|
3649
|
+
|