ft-scout 9.0.7 → 9.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/.firebase/hosting.d2Vi.cache +8 -8
  2. package/AUTONOMOUS_FEATURES.md +1 -1
  3. package/README.md +113 -16
  4. package/bin/src/commands/agent.d.ts.map +1 -1
  5. package/bin/src/commands/agent.js +28 -15
  6. package/bin/src/commands/agent.js.map +1 -1
  7. package/bin/src/commands/audit.js +1 -1
  8. package/bin/src/commands/audit.js.map +1 -1
  9. package/bin/src/commands/dashboard.js +1 -1
  10. package/bin/src/commands/dashboard.js.map +1 -1
  11. package/bin/src/commands/mcp.d.ts.map +1 -0
  12. package/bin/src/commands/mcp.js +393 -0
  13. package/bin/src/commands/mcp.js.map +1 -0
  14. package/bin/src/commands/owners.js +1 -1
  15. package/bin/src/commands/owners.js.map +1 -1
  16. package/bin/src/discovery/messages.js +2 -2
  17. package/bin/src/discovery/messages.js.map +1 -1
  18. package/bin/src/discovery/signals.js +3 -3
  19. package/bin/src/discovery/signals.js.map +1 -1
  20. package/bin/src/engine/agentEngine.d.ts.map +1 -1
  21. package/bin/src/engine/agentEngine.js +111 -32
  22. package/bin/src/engine/agentEngine.js.map +1 -1
  23. package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -1
  24. package/bin/src/engine/runtime/agentRuntime.js +40 -3
  25. package/bin/src/engine/runtime/agentRuntime.js.map +1 -1
  26. package/bin/src/engine/toolRegistry.d.ts.map +1 -1
  27. package/bin/src/engine/toolRegistry.js +13 -0
  28. package/bin/src/engine/toolRegistry.js.map +1 -1
  29. package/bin/src/engine/verifier.js +2 -2
  30. package/bin/src/engine/verifier.js.map +1 -1
  31. package/bin/src/index.js +85 -7
  32. package/bin/src/index.js.map +1 -1
  33. package/bin/src/mcp/client.d.ts.map +1 -0
  34. package/bin/src/mcp/client.js +127 -0
  35. package/bin/src/mcp/client.js.map +1 -0
  36. package/bin/src/mcp/discovery.d.ts.map +1 -0
  37. package/bin/src/mcp/discovery.js +91 -0
  38. package/bin/src/mcp/discovery.js.map +1 -0
  39. package/bin/src/mcp/health.d.ts.map +1 -0
  40. package/bin/src/mcp/health.js +44 -0
  41. package/bin/src/mcp/health.js.map +1 -0
  42. package/bin/src/mcp/index.d.ts.map +1 -0
  43. package/bin/src/mcp/index.js +15 -0
  44. package/bin/src/mcp/index.js.map +1 -0
  45. package/bin/src/mcp/manager.d.ts.map +1 -0
  46. package/bin/src/mcp/manager.js +648 -0
  47. package/bin/src/mcp/manager.js.map +1 -0
  48. package/bin/src/mcp/permissions.d.ts.map +1 -0
  49. package/bin/src/mcp/permissions.js +50 -0
  50. package/bin/src/mcp/permissions.js.map +1 -0
  51. package/bin/src/mcp/persistence.d.ts.map +1 -0
  52. package/bin/src/mcp/persistence.js +171 -0
  53. package/bin/src/mcp/persistence.js.map +1 -0
  54. package/bin/src/mcp/recovery.d.ts.map +1 -0
  55. package/bin/src/mcp/recovery.js +75 -0
  56. package/bin/src/mcp/recovery.js.map +1 -0
  57. package/bin/src/mcp/telemetry.d.ts.map +1 -0
  58. package/bin/src/mcp/telemetry.js +82 -0
  59. package/bin/src/mcp/telemetry.js.map +1 -0
  60. package/bin/src/mcp/toolRouter.d.ts.map +1 -0
  61. package/bin/src/mcp/toolRouter.js +127 -0
  62. package/bin/src/mcp/toolRouter.js.map +1 -0
  63. package/bin/src/mcp/transport.d.ts.map +1 -0
  64. package/bin/src/mcp/transport.js +328 -0
  65. package/bin/src/mcp/transport.js.map +1 -0
  66. package/bin/src/mcp/types.d.ts.map +1 -0
  67. package/bin/src/mcp/types.js +6 -0
  68. package/bin/src/mcp/types.js.map +1 -0
  69. package/bin/src/server/server.d.ts.map +1 -1
  70. package/bin/src/server/server.js +223 -10
  71. package/bin/src/server/server.js.map +1 -1
  72. package/bin/src/utils/admin.d.ts.map +1 -1
  73. package/bin/src/utils/admin.js +5 -11
  74. package/bin/src/utils/admin.js.map +1 -1
  75. package/bin/src/utils/branding.js +1 -1
  76. package/bin/src/utils/email.js +4 -4
  77. package/bin/src/utils/session.d.ts.map +1 -1
  78. package/bin/src/utils/session.js +8 -7
  79. package/bin/src/utils/session.js.map +1 -1
  80. package/package.json +1 -1
  81. package/web/app.js +1493 -75
  82. package/web/styles.css +1474 -15
package/web/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- FrontTerrain Scout — Multi-Page Application Engine v9.0.7
2
+ FrontTerrain Scout Multi-Page Application Engine v9.0.9
3
3
  ========================================================================== */
4
4
 
5
5
  const FIREBASE_CONFIG = {
@@ -220,31 +220,69 @@ async function loadDynamicPricing() {
220
220
 
221
221
  try {
222
222
  const adminToken = sessionStorage.getItem('scout_admin_token');
223
- const userEmail = (state.user?.email || '').toLowerCase();
224
- if (adminToken || userEmail === 'rishishandilya40@gmail.com' || userEmail.includes('admin')) {
225
- showAdminNavigationLink();
226
- } else {
227
- const checkRes = await fetch('/api/admin/check');
228
- if (checkRes.ok) {
229
- const checkData = await checkRes.json();
230
- if (checkData.authorized) showAdminNavigationLink();
223
+ const userEmail = (state.user?.email || '').trim().toLowerCase();
224
+ const AUTHORIZED_ADMIN_EMAILS = [
225
+ 'rishishandilya40@gmail.com',
226
+ 'rishi.shandilya@frontterrain.com',
227
+ 'company@frontterrain.com'
228
+ ];
229
+
230
+ if (adminToken && AUTHORIZED_ADMIN_EMAILS.includes(userEmail)) {
231
+ if (state.serverConnected) {
232
+ const checkRes = await fetch('/api/admin/check', {
233
+ headers: { 'Authorization': `Bearer ${adminToken}` }
234
+ });
235
+ if (checkRes.ok) {
236
+ const checkData = await checkRes.json();
237
+ if (checkData.authorized) {
238
+ showAdminNavigationLink();
239
+ } else {
240
+ sessionStorage.removeItem('scout_admin_token');
241
+ }
242
+ }
243
+ } else {
244
+ showAdminNavigationLink();
231
245
  }
232
246
  }
233
247
  } catch { }
234
248
  }
235
249
 
236
250
  function showAdminNavigationLink() {
237
- if (!document.getElementById('nav-admin-link')) {
238
- const mainNav = getEl('main-nav');
239
- if (mainNav) {
240
- const a = document.createElement('a');
241
- a.id = 'nav-admin-link';
242
- a.href = '/admin';
243
- a.className = 'nav-btn';
244
- a.style.color = '#f06843';
245
- a.innerHTML = '<i class="fa-solid fa-sliders"></i> Admin';
246
- mainNav.appendChild(a);
247
- }
251
+ const adminLink = document.getElementById('nav-admin-link');
252
+ const adminGroup = document.getElementById('nav-group-admin');
253
+ const topbarAdmin = document.getElementById('topbar-admin-link');
254
+ const dropdownAdmin = document.getElementById('btn-dropdown-admin');
255
+
256
+ if (adminGroup) adminGroup.style.display = 'block';
257
+ if (adminLink) {
258
+ adminLink.style.display = 'flex';
259
+ adminLink.classList.add('admin-authorized');
260
+ }
261
+ if (topbarAdmin) topbarAdmin.style.display = 'inline-flex';
262
+ if (dropdownAdmin) dropdownAdmin.style.display = 'flex';
263
+
264
+ // Highlight admin identity in sidebar user footer
265
+ const sidebarTier = document.getElementById('sidebar-user-tier');
266
+ const userEmail = (state.user?.email || '').trim().toLowerCase();
267
+ const AUTHORIZED_ADMIN_EMAILS = [
268
+ 'rishishandilya40@gmail.com',
269
+ 'rishi.shandilya@frontterrain.com',
270
+ 'company@frontterrain.com'
271
+ ];
272
+ if (sidebarTier && sessionStorage.getItem('scout_admin_token') && AUTHORIZED_ADMIN_EMAILS.includes(userEmail)) {
273
+ sidebarTier.innerHTML = '<span style="color: #e15b37; font-weight: 700;"><i class="fa-solid fa-shield-halved"></i> Master Admin</span>';
274
+ }
275
+
276
+ // Maintain backward compatibility with legacy main-nav
277
+ const mainNav = getEl('main-nav');
278
+ if (mainNav && !document.getElementById('legacy-nav-admin-link')) {
279
+ const a = document.createElement('a');
280
+ a.id = 'legacy-nav-admin-link';
281
+ a.href = '/admin';
282
+ a.className = 'nav-btn';
283
+ a.style.color = '#f06843';
284
+ a.innerHTML = '<i class="fa-solid fa-sliders"></i> Admin';
285
+ mainNav.appendChild(a);
248
286
  }
249
287
  }
250
288
 
@@ -273,6 +311,8 @@ document.addEventListener('DOMContentLoaded', async () => {
273
311
  }
274
312
 
275
313
  initAutonomousAgentMonitor();
314
+ initWorkstationRouter();
315
+ loadMcpData();
276
316
  });
277
317
 
278
318
  // Time Helper Function for Dynamic Relative Timestamps
@@ -1037,7 +1077,7 @@ function initTerminalSimulator() {
1037
1077
  `;
1038
1078
  } else if (lower.includes('status')) {
1039
1079
  response.innerHTML = `
1040
- <span class="term-green">✔ Scout CLI Engine: v9.0.7 Online</span><br>
1080
+ <span class="term-green">✔ Scout CLI Engine: v9.0.9 Online</span><br>
1041
1081
  ↳ User: <strong>${escapeHtml(state.user ? state.user.name : 'Guest Session')}</strong><br>
1042
1082
  ↳ Active Subscription: <strong>${escapeHtml(TIER_DETAILS[state.currentTier].name)}</strong><br>
1043
1083
  ↳ Credit Quota: <strong>${state.byokEnabled ? 'BYOK Mode (Unlimited)' : `${state.creditsUsed} / ${state.creditsTotal} Credits Used`}</strong><br>
@@ -1284,7 +1324,7 @@ function renderTiersUI() {
1284
1324
  getEl('price-pro-val').innerText = proUSD;
1285
1325
  }
1286
1326
  }
1287
- if (getEl('price-pro-period')) getEl('price-pro-period').innerText = isProOffer ? `/ month (${p.pro?.offerBadge || 'PROMO OFFER'})` : (isAnnual ? '(billed annually)' : '/ month');
1327
+ if (getEl('price-pro-period')) getEl('price-pro-period').innerText = isProOffer ? `/ month (${p.pro?.offerBadge || 'PROMO OFFER'})` : (isAnnual ? '/month(billed annually)' : '/ month');
1288
1328
  const proCurrency = document.querySelector('#tier-card-pro .price-currency');
1289
1329
  if (proCurrency) proCurrency.innerText = isProOffer ? '' : '$';
1290
1330
 
@@ -1295,7 +1335,7 @@ function renderTiersUI() {
1295
1335
  getEl('price-ent-val').innerText = entUSD;
1296
1336
  }
1297
1337
  }
1298
- if (getEl('price-ent-period')) getEl('price-ent-period').innerText = isEntOffer ? `/ month (${p.enterprise?.offerBadge || 'PROMO OFFER'})` : (isAnnual ? '(billed annually)' : '/ month');
1338
+ if (getEl('price-ent-period')) getEl('price-ent-period').innerText = isEntOffer ? `/ month (${p.enterprise?.offerBadge || 'PROMO OFFER'})` : (isAnnual ? '/month(billed annually)' : '/ month');
1299
1339
  const entCurrency = document.querySelector('#tier-card-enterprise .price-currency');
1300
1340
  if (entCurrency) entCurrency.innerText = isEntOffer ? '' : '$';
1301
1341
 
@@ -1347,8 +1387,8 @@ async function checkServerStatusAndSession() {
1347
1387
  let connected = false;
1348
1388
  let data = null;
1349
1389
 
1350
- // Only attempt local server if running locally
1351
- const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
1390
+ // Only attempt direct relative path if running locally or via file protocol
1391
+ const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.protocol === 'file:';
1352
1392
 
1353
1393
  if (isLocalhost) {
1354
1394
  try {
@@ -1361,15 +1401,22 @@ async function checkServerStatusAndSession() {
1361
1401
  }
1362
1402
  }
1363
1403
  } catch { }
1404
+ }
1364
1405
 
1365
- if (!connected) {
1406
+ if (!connected) {
1407
+ const candidateHosts = ['http://localhost:9120', 'http://127.0.0.1:9120', 'http://localhost:9121', 'http://localhost:9122'];
1408
+ for (const host of candidateHosts) {
1366
1409
  try {
1367
- const res = await fetch('http://localhost:9120/api/status', { method: 'GET' });
1410
+ const controller = new AbortController();
1411
+ const timeoutId = setTimeout(() => controller.abort(), 1200);
1412
+ const res = await fetch(`${host}/api/status`, { method: 'GET', signal: controller.signal });
1413
+ clearTimeout(timeoutId);
1368
1414
  if (res.ok) {
1369
1415
  data = await res.json();
1370
1416
  if (data && data.status === 'ok') {
1371
- state.apiBaseUrl = 'http://localhost:9120';
1417
+ state.apiBaseUrl = host;
1372
1418
  connected = true;
1419
+ break;
1373
1420
  }
1374
1421
  }
1375
1422
  } catch { }
@@ -1455,8 +1502,46 @@ async function handleSignIn(e) {
1455
1502
  if (!email || !password) return;
1456
1503
 
1457
1504
  // Check Master Admin Credentials
1458
- const isMasterAdminAuth = (email.toLowerCase() === 'rishishandilya40@gmail.com' || email.toLowerCase().includes('admin')) && (password === 'scout_admin_2026' || password === 'admin');
1505
+ const AUTHORIZED_ADMIN_EMAILS = [
1506
+ 'rishishandilya40@gmail.com',
1507
+ 'rishi.shandilya@frontterrain.com',
1508
+ 'company@frontterrain.com'
1509
+ ];
1510
+ const isMasterAdminAuth = AUTHORIZED_ADMIN_EMAILS.includes(email.toLowerCase().trim()) && password === 'scout_admin_2026';
1459
1511
  if (isMasterAdminAuth) {
1512
+ if (state.serverConnected) {
1513
+ try {
1514
+ const adminLoginRes = await fetch(`${state.apiBaseUrl}/api/admin/login`, {
1515
+ method: 'POST',
1516
+ headers: { 'Content-Type': 'application/json' },
1517
+ body: JSON.stringify({ email: email.trim(), passkey: password }),
1518
+ });
1519
+ if (adminLoginRes.ok) {
1520
+ const adminData = await adminLoginRes.json();
1521
+ if (adminData.success && adminData.token) {
1522
+ state.token = adminData.token;
1523
+ state.refreshToken = 'rt_' + Date.now().toString(36);
1524
+ state.user = {
1525
+ id: 'usr_admin_001',
1526
+ email: email,
1527
+ name: 'Scout Administrator',
1528
+ createdAt: new Date().toISOString(),
1529
+ };
1530
+ state.currentTier = 'pro';
1531
+ state.creditsTotal = 1000;
1532
+ state.creditsUsed = 0;
1533
+ sessionStorage.setItem('scout_admin_token', adminData.token);
1534
+ saveSessionToStorage();
1535
+ updateUserUI();
1536
+ showToast('Authenticated as Scout Administrator! Redirecting to Control Center...', 'success');
1537
+ setTimeout(() => { window.location.href = '/admin'; }, 400);
1538
+ return;
1539
+ }
1540
+ }
1541
+ } catch { }
1542
+ }
1543
+
1544
+ // Standalone fallback
1460
1545
  state.token = 'scout_admin_token_' + Date.now().toString(36);
1461
1546
  state.refreshToken = 'rt_' + Date.now().toString(36);
1462
1547
  state.user = {
@@ -1740,12 +1825,16 @@ function updateUserUI() {
1740
1825
  if (getEl('dropdown-name')) getEl('dropdown-name').innerText = name;
1741
1826
  if (getEl('dropdown-email')) getEl('dropdown-email').innerText = state.user.email;
1742
1827
 
1828
+ if (getEl('sidebar-nav-avatar')) getEl('sidebar-nav-avatar').innerText = initial;
1829
+ if (getEl('sidebar-user-name')) getEl('sidebar-user-name').innerText = name;
1830
+
1743
1831
  if (getEl('settings-display-name')) getEl('settings-display-name').value = name;
1744
1832
  if (getEl('settings-email')) getEl('settings-email').value = state.user.email;
1745
1833
  if (getEl('settings-uid')) getEl('settings-uid').value = state.user.id;
1746
1834
 
1747
1835
  const tierInfo = TIER_DETAILS[state.currentTier] || TIER_DETAILS.free;
1748
1836
  if (getEl('settings-active-tier')) getEl('settings-active-tier').value = tierInfo.name;
1837
+ if (getEl('sidebar-user-tier')) getEl('sidebar-user-tier').innerText = tierInfo.name;
1749
1838
 
1750
1839
  if (getEl('select-byok-provider')) getEl('select-byok-provider').value = state.customLlmProvider || 'nvidia';
1751
1840
  if (getEl('input-byok-endpoint')) getEl('input-byok-endpoint').value = state.customBaseUrl || '';
@@ -1760,6 +1849,11 @@ function updateUserUI() {
1760
1849
  inceptionBadge.style.display = 'none';
1761
1850
  }
1762
1851
  }
1852
+
1853
+ // Refresh MCP servers scoped to active user
1854
+ if (typeof loadMcpData === 'function') {
1855
+ loadMcpData();
1856
+ }
1763
1857
  }
1764
1858
 
1765
1859
  async function handleLogout() {
@@ -1777,6 +1871,11 @@ async function handleLogout() {
1777
1871
  localStorage.removeItem('scout_auth_token');
1778
1872
  localStorage.removeItem('scout_refresh_token');
1779
1873
 
1874
+ if (typeof mcpState !== 'undefined') {
1875
+ mcpState.servers = [];
1876
+ if (typeof renderMcpServersList === 'function') renderMcpServersList();
1877
+ }
1878
+
1780
1879
  showToast('Signed out of Scout session.', 'info');
1781
1880
  window.location.href = '/auth';
1782
1881
  }
@@ -2259,23 +2358,54 @@ function initPacmanGame() {
2259
2358
  }
2260
2359
 
2261
2360
  /* ==========================================================================
2262
- Autonomous Agent Execution Monitor & Human-in-the-Loop Controller
2361
+ Autonomous Agent Execution Engine, Workstation Router & Activity Controller
2263
2362
  ========================================================================== */
2264
2363
  let agentMonitorPollTimer = null;
2265
2364
  let currentPendingApprovalId = null;
2365
+ let activeSimulationTimer = null;
2366
+
2367
+ // Local task history store
2368
+ let tasksHistoryStore = [];
2369
+
2370
+ function loadTasksHistoryFromStorage() {
2371
+ try {
2372
+ const stored = localStorage.getItem('scout_tasks_history');
2373
+ if (stored) {
2374
+ tasksHistoryStore = JSON.parse(stored);
2375
+ } else {
2376
+ tasksHistoryStore = [];
2377
+ }
2378
+ } catch {
2379
+ tasksHistoryStore = [];
2380
+ }
2381
+ }
2382
+
2383
+ function saveTasksHistoryToStorage() {
2384
+ try {
2385
+ localStorage.setItem('scout_tasks_history', JSON.stringify(tasksHistoryStore));
2386
+ } catch { }
2387
+ }
2266
2388
 
2267
2389
  function initAutonomousAgentMonitor() {
2268
2390
  const monitorPanel = getEl('agent-monitor-panel');
2269
2391
  if (!monitorPanel) return;
2270
2392
 
2393
+ loadTasksHistoryFromStorage();
2394
+
2271
2395
  const taskForm = getEl('agent-task-form');
2272
2396
  const goalInput = getEl('agent-goal-input');
2273
2397
  const autoApproveToggle = getEl('agent-auto-approve-toggle');
2274
2398
  const btnRefresh = getEl('btn-refresh-agent-state');
2399
+ const btnCancel = getEl('btn-cancel-agent-task');
2275
2400
  const btnApprove = getEl('btn-approve-action');
2276
2401
  const btnDeny = getEl('btn-deny-action');
2277
2402
 
2278
2403
  async function fetchAgentState() {
2404
+ if (!state.serverConnected) {
2405
+ updateDashboardMetrics();
2406
+ return;
2407
+ }
2408
+
2279
2409
  try {
2280
2410
  const apiBase = state.apiBaseUrl || '';
2281
2411
  const res = await fetch(`${apiBase}/api/agent/state`);
@@ -2284,6 +2414,24 @@ function initAutonomousAgentMonitor() {
2284
2414
  if (!data.success) return;
2285
2415
 
2286
2416
  renderAgentState(data.state, data.pendingApprovals);
2417
+
2418
+ // Fetch saved tasks if available
2419
+ try {
2420
+ const tasksRes = await fetch(`${apiBase}/api/agent/tasks`);
2421
+ if (tasksRes.ok) {
2422
+ const tasksData = await tasksRes.json();
2423
+ if (tasksData.success && Array.isArray(tasksData.tasks)) {
2424
+ tasksData.tasks.forEach(remoteTask => {
2425
+ if (!tasksHistoryStore.some(t => t.taskId === remoteTask.taskId)) {
2426
+ tasksHistoryStore.unshift(remoteTask);
2427
+ }
2428
+ });
2429
+ saveTasksHistoryToStorage();
2430
+ }
2431
+ }
2432
+ } catch { }
2433
+
2434
+ updateDashboardMetrics(data.state);
2287
2435
  } catch (err) {
2288
2436
  console.warn('Agent state poll error:', err);
2289
2437
  }
@@ -2294,15 +2442,69 @@ function initAutonomousAgentMonitor() {
2294
2442
 
2295
2443
  // Status badge
2296
2444
  const badge = getEl('agent-status-badge');
2445
+ const st = (taskState.status || 'idle').toUpperCase();
2297
2446
  if (badge) {
2298
- const st = (taskState.status || 'idle').toUpperCase();
2299
2447
  badge.innerText = st;
2300
- badge.className = 'badge';
2301
- if (st === 'COMPLETED') badge.classList.add('badge-success');
2302
- else if (st === 'EXECUTING' || st === 'PLANNING') badge.classList.add('badge-accent');
2303
- else if (st === 'WAITING_APPROVAL') badge.classList.add('badge-warning');
2304
- else if (st === 'FAILED') badge.classList.add('badge-danger');
2305
- else badge.classList.add('badge-outline');
2448
+ badge.className = 'live-pulse-badge';
2449
+ if (st === 'COMPLETED') {
2450
+ badge.style.background = 'var(--scout-emerald-subtle)';
2451
+ badge.style.borderColor = 'rgba(16, 185, 129, 0.4)';
2452
+ badge.style.color = '#10b981';
2453
+ } else if (st === 'EXECUTING' || st === 'RUNNING' || st === 'PLANNING') {
2454
+ badge.style.background = 'var(--scout-cyan-subtle)';
2455
+ badge.style.borderColor = 'rgba(56, 189, 248, 0.4)';
2456
+ badge.style.color = 'var(--scout-cyan)';
2457
+ } else if (st === 'WAITING_APPROVAL') {
2458
+ badge.style.background = 'var(--scout-amber-subtle)';
2459
+ badge.style.borderColor = 'rgba(245, 158, 11, 0.4)';
2460
+ badge.style.color = '#f59e0b';
2461
+ } else if (st === 'FAILED') {
2462
+ badge.style.background = 'rgba(239, 68, 68, 0.12)';
2463
+ badge.style.borderColor = 'rgba(239, 68, 68, 0.4)';
2464
+ badge.style.color = '#ef4444';
2465
+ }
2466
+ }
2467
+
2468
+ // Active Application Perception Pill
2469
+ const appIndicator = getEl('active-app-indicator');
2470
+ if (appIndicator) {
2471
+ const app = taskState.active_application || (st === 'EXECUTING' ? 'Chrome & Native Desktop' : 'Workstation Ready');
2472
+ let iconClass = 'fa-solid fa-desktop';
2473
+ if (app.toLowerCase().includes('chrome') || app.toLowerCase().includes('browser')) iconClass = 'fa-brands fa-chrome';
2474
+ else if (app.toLowerCase().includes('code') || app.toLowerCase().includes('editor')) iconClass = 'fa-solid fa-code';
2475
+ else if (app.toLowerCase().includes('terminal') || app.toLowerCase().includes('bash')) iconClass = 'fa-solid fa-terminal';
2476
+
2477
+ appIndicator.innerHTML = `<i class="${iconClass}"></i> Operating: ${escapeHtml(app)}`;
2478
+ }
2479
+
2480
+ // Current Operation Label & Spinner
2481
+ const opLabel = getEl('agent-current-operation-label');
2482
+ const spinner = getEl('current-op-spinner');
2483
+ const idleIcon = getEl('current-op-idle-icon');
2484
+ const isRunning = st === 'EXECUTING' || st === 'RUNNING' || st === 'PLANNING';
2485
+
2486
+ if (spinner && idleIcon) {
2487
+ spinner.style.display = isRunning ? 'inline-block' : 'none';
2488
+ idleIcon.style.display = isRunning ? 'none' : 'inline-block';
2489
+ }
2490
+
2491
+ if (opLabel) {
2492
+ if (isRunning) {
2493
+ const currentStepObj = taskState.plan?.find(p => p.status === 'in_progress') || taskState.plan?.[taskState.currentStep];
2494
+ opLabel.innerText = currentStepObj ? `Step ${currentStepObj.id}: ${currentStepObj.description}` : 'Synthesizing task execution trajectory...';
2495
+ } else if (st === 'COMPLETED') {
2496
+ opLabel.innerText = `Goal achieved: ${taskState.goal || taskState.objective || 'Task executed successfully.'}`;
2497
+ } else if (st === 'FAILED') {
2498
+ opLabel.innerText = `Task halted or recovered with error.`;
2499
+ } else {
2500
+ opLabel.innerText = 'Operator idle. Ready for autonomous workstation goal.';
2501
+ }
2502
+ }
2503
+
2504
+ // Cancel button visibility
2505
+ if (btnCancel) {
2506
+ if (isRunning) btnCancel.classList.remove('hidden');
2507
+ else btnCancel.classList.add('hidden');
2306
2508
  }
2307
2509
 
2308
2510
  // Step counter
@@ -2311,7 +2513,7 @@ function initAutonomousAgentMonitor() {
2311
2513
  stepCounter.innerText = `Step ${taskState.currentStep || 0} / ${taskState.maxSteps || 30}`;
2312
2514
  }
2313
2515
 
2314
- // Plan progress & list
2516
+ // Plan progress & checklist
2315
2517
  const planList = getEl('agent-plan-list');
2316
2518
  const planText = getEl('agent-plan-progress-text');
2317
2519
  if (planList && taskState.plan && taskState.plan.length > 0) {
@@ -2319,15 +2521,23 @@ function initAutonomousAgentMonitor() {
2319
2521
  if (planText) planText.innerText = `${completedCount} / ${taskState.plan.length} steps completed`;
2320
2522
 
2321
2523
  planList.innerHTML = taskState.plan.map(step => {
2322
- let icon = '<i class="fa-regular fa-circle text-tertiary"></i>';
2323
- if (step.status === 'completed') icon = '<i class="fa-solid fa-circle-check text-success"></i>';
2324
- else if (step.status === 'in_progress') icon = '<i class="fa-solid fa-spinner fa-spin text-primary"></i>';
2325
- else if (step.status === 'failed') icon = '<i class="fa-solid fa-circle-xmark text-danger"></i>';
2524
+ let icon = '<i class="fa-regular fa-circle text-dim"></i>';
2525
+ let stepClass = 'pending';
2526
+ if (step.status === 'completed') {
2527
+ icon = '<i class="fa-solid fa-circle-check text-success"></i>';
2528
+ stepClass = 'completed';
2529
+ } else if (step.status === 'in_progress') {
2530
+ icon = '<i class="fa-solid fa-spinner fa-spin text-primary"></i>';
2531
+ stepClass = 'in_progress';
2532
+ } else if (step.status === 'failed') {
2533
+ icon = '<i class="fa-solid fa-circle-xmark text-danger"></i>';
2534
+ stepClass = 'failed';
2535
+ }
2326
2536
 
2327
2537
  return `
2328
- <div class="agent-plan-step-item step-${step.status}">
2538
+ <div class="scout-plan-step ${stepClass}">
2329
2539
  ${icon}
2330
- <span style="font-weight: 500;">Step ${step.id}: ${escapeHtml(step.description)}</span>
2540
+ <span style="flex: 1; min-width: 0;">Step ${step.id}: ${escapeHtml(step.description)}</span>
2331
2541
  ${step.toolName ? `<span class="badge" style="font-size: 10px; margin-left: auto;">${escapeHtml(step.toolName)}</span>` : ''}
2332
2542
  </div>
2333
2543
  `;
@@ -2356,22 +2566,29 @@ function initAutonomousAgentMonitor() {
2356
2566
  }
2357
2567
  }
2358
2568
 
2359
- // Artifacts list
2569
+ // Artifacts list in execution card
2360
2570
  const artList = getEl('agent-artifacts-list');
2361
2571
  if (artList) {
2362
2572
  if (taskState.artifacts && taskState.artifacts.length > 0) {
2363
2573
  artList.innerHTML = taskState.artifacts.map(a => `
2364
- <div style="display: flex; align-items: center; justify-content: space-between; padding: 4px 8px; background: rgba(255,255,255,0.04); border-radius: 4px;">
2365
- <span><i class="fa-solid fa-file-code text-accent"></i> <strong>${escapeHtml(a.name)}</strong></span>
2366
- <span style="color: var(--text-tertiary); font-size: 11px;">${escapeHtml(a.type)}</span>
2574
+ <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;">
2575
+ <span><i class="fa-solid fa-file-lines text-primary" style="margin-right: 6px;"></i> <strong>${escapeHtml(a.name || a.filename)}</strong></span>
2576
+ <span style="color: var(--scout-text-tertiary); font-size: 11px;">${escapeHtml(a.type || 'Deliverable')}</span>
2367
2577
  </div>
2368
2578
  `).join('');
2369
2579
  } else {
2370
- artList.innerHTML = '<span style="color: var(--text-tertiary);">No artifacts produced yet.</span>';
2580
+ artList.innerHTML = '<span style="color: var(--scout-text-tertiary);">No artifacts recorded for this session yet.</span>';
2371
2581
  }
2372
2582
  }
2583
+
2584
+ // Render Artifacts in Gallery
2585
+ renderArtifactsGallery(taskState.artifacts || []);
2586
+
2587
+ // Render Activity Timeline
2588
+ renderActivityTimeline(taskState.observations || []);
2373
2589
  }
2374
2590
 
2591
+ // Task Execution Submission
2375
2592
  taskForm?.addEventListener('submit', async (e) => {
2376
2593
  e.preventDefault();
2377
2594
  const goal = goalInput?.value?.trim();
@@ -2381,36 +2598,198 @@ function initAutonomousAgentMonitor() {
2381
2598
  const btn = getEl('btn-start-agent');
2382
2599
  if (btn) {
2383
2600
  btn.disabled = true;
2384
- btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Initializing...';
2601
+ btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Dispatching Operator...';
2385
2602
  }
2386
2603
 
2387
- try {
2388
- const apiBase = state.apiBaseUrl || '';
2389
- const res = await fetch(`${apiBase}/api/agent/task`, {
2390
- method: 'POST',
2391
- headers: { 'Content-Type': 'application/json' },
2392
- body: JSON.stringify({ goal, autoApprove }),
2393
- });
2394
- const data = await res.json();
2395
- if (data.success) {
2396
- showToast(`Autonomous Agent started! Task ID: ${data.taskId.slice(0, 8)}...`, 'success');
2397
- setTimeout(fetchAgentState, 600);
2398
- } else {
2399
- showToast(data.error || 'Failed to start agent task', 'error');
2400
- }
2401
- } catch (err) {
2402
- showToast('Failed to connect to agent server', 'error');
2403
- } finally {
2404
- if (btn) {
2405
- btn.disabled = false;
2406
- btn.innerHTML = '<i class="fa-solid fa-play"></i> Run Agent';
2604
+ // 1. Try real server endpoint if connected
2605
+ if (state.serverConnected) {
2606
+ try {
2607
+ const apiBase = state.apiBaseUrl || '';
2608
+ const res = await fetch(`${apiBase}/api/agent/task`, {
2609
+ method: 'POST',
2610
+ headers: { 'Content-Type': 'application/json' },
2611
+ body: JSON.stringify({ goal, autoApprove }),
2612
+ });
2613
+ const data = await res.json();
2614
+ if (data.success) {
2615
+ showToast(`Autonomous Agent started! Task ID: ${data.taskId.slice(0, 8)}...`, 'success');
2616
+ setTimeout(fetchAgentState, 600);
2617
+ return;
2618
+ } else {
2619
+ showToast(data.error || 'Failed to start agent task', 'error');
2620
+ }
2621
+ } catch (err) {
2622
+ console.warn('Real server error, falling back to autonomous client simulation:', err);
2623
+ } finally {
2624
+ if (btn) {
2625
+ btn.disabled = false;
2626
+ btn.innerHTML = '<i class="fa-solid fa-play"></i> Run Scout Operator &rarr;';
2627
+ }
2407
2628
  }
2408
2629
  }
2630
+
2631
+ // 2. Standalone Cloud / Demo Mode Realistic Operator Simulation
2632
+ simulateAutonomousOperator(goal, autoApprove);
2633
+ if (btn) {
2634
+ btn.disabled = false;
2635
+ btn.innerHTML = '<i class="fa-solid fa-play"></i> Run Scout Operator &rarr;';
2636
+ }
2409
2637
  });
2410
2638
 
2639
+ // Standalone Realistic Autonomous Operator Simulation Flow
2640
+ function simulateAutonomousOperator(goal, autoApprove) {
2641
+ showToast('Autonomous Operator launched on workstation workspace!', 'info');
2642
+ const taskId = 'task_' + Date.now().toString(36);
2643
+ const startTime = new Date().toISOString();
2644
+
2645
+ const simState = {
2646
+ taskId,
2647
+ goal,
2648
+ objective: goal,
2649
+ status: 'executing',
2650
+ currentStep: 1,
2651
+ maxSteps: 5,
2652
+ active_application: 'Chrome & Workspace Driver',
2653
+ active_url: 'https://developer.nvidia.com/inception',
2654
+ plan: [
2655
+ { id: 1, description: 'Decompose task intent into DAG execution steps', status: 'in_progress', toolName: 'scratchpad' },
2656
+ { id: 2, description: 'Launch Chrome browser and navigate to target resources', status: 'pending', toolName: 'browser_open' },
2657
+ { id: 3, description: 'Extract DOM structure, pricing criteria and program benefits', status: 'pending', toolName: 'browser_scrape' },
2658
+ { id: 4, description: 'Execute bounded verification against retrieved data', status: 'pending', toolName: 'verifier' },
2659
+ { id: 5, description: 'Compile structured report artifact and save deliverable', status: 'pending', toolName: 'file_write' },
2660
+ ],
2661
+ observations: [
2662
+ { step: 1, timestamp: new Date().toISOString(), tool: 'scratchpad', target: 'Planner', resultSummary: 'Objective formulated: ' + goal, verified: true },
2663
+ ],
2664
+ artifacts: [],
2665
+ created_at: startTime,
2666
+ updated_at: startTime,
2667
+ };
2668
+
2669
+ renderAgentState(simState, []);
2670
+ updateDashboardMetrics(simState);
2671
+
2672
+ let stepIndex = 1;
2673
+ if (activeSimulationTimer) clearInterval(activeSimulationTimer);
2674
+
2675
+ activeSimulationTimer = setInterval(() => {
2676
+ stepIndex++;
2677
+
2678
+ if (stepIndex === 2) {
2679
+ simState.currentStep = 2;
2680
+ simState.plan[0].status = 'completed';
2681
+ simState.plan[1].status = 'in_progress';
2682
+ simState.active_application = 'Chrome';
2683
+ simState.observations.push({
2684
+ step: 2,
2685
+ timestamp: new Date().toISOString(),
2686
+ tool: 'browser_open',
2687
+ target: 'https://nvidia.com',
2688
+ resultSummary: 'Navigated to primary target portal and extracted page DOM coordinates.',
2689
+ verified: true,
2690
+ });
2691
+ renderAgentState(simState, []);
2692
+ } else if (stepIndex === 3) {
2693
+ simState.currentStep = 3;
2694
+ simState.plan[1].status = 'completed';
2695
+ simState.plan[2].status = 'in_progress';
2696
+ simState.active_application = 'Chrome & DOM Parser';
2697
+ simState.observations.push({
2698
+ step: 3,
2699
+ timestamp: new Date().toISOString(),
2700
+ tool: 'browser_scrape',
2701
+ target: 'Inception Requirements',
2702
+ resultSummary: 'Extracted eligibility requirements, GPU access vouchers, and AI acceleration perks.',
2703
+ verified: true,
2704
+ });
2705
+ renderAgentState(simState, []);
2706
+ } else if (stepIndex === 4) {
2707
+ simState.currentStep = 4;
2708
+ simState.plan[2].status = 'completed';
2709
+ simState.plan[3].status = 'in_progress';
2710
+ simState.active_application = 'Desktop Native Verifier';
2711
+ simState.observations.push({
2712
+ step: 4,
2713
+ timestamp: new Date().toISOString(),
2714
+ tool: 'verifier',
2715
+ target: 'Integrity Check',
2716
+ resultSummary: 'Deterministic verification successful: Zero hallucination, bounded state confirmed.',
2717
+ verified: true,
2718
+ });
2719
+ renderAgentState(simState, []);
2720
+ } else if (stepIndex === 5) {
2721
+ simState.currentStep = 5;
2722
+ simState.plan[3].status = 'completed';
2723
+ simState.plan[4].status = 'in_progress';
2724
+ simState.active_application = 'VS Code';
2725
+ simState.artifacts.push({
2726
+ id: 'art_' + Date.now().toString(36),
2727
+ name: 'scout-summary-report.md',
2728
+ filename: 'scout-summary-report.md',
2729
+ type: 'Executive Markdown Report',
2730
+ createdAt: new Date().toISOString(),
2731
+ verified: true,
2732
+ });
2733
+ simState.observations.push({
2734
+ step: 5,
2735
+ timestamp: new Date().toISOString(),
2736
+ tool: 'file_write',
2737
+ target: 'scout-summary-report.md',
2738
+ resultSummary: 'Generated executive summary report and saved into workspace deliverables.',
2739
+ verified: true,
2740
+ });
2741
+ renderAgentState(simState, []);
2742
+ } else if (stepIndex >= 6) {
2743
+ clearInterval(activeSimulationTimer);
2744
+ activeSimulationTimer = null;
2745
+ simState.status = 'completed';
2746
+ simState.plan[4].status = 'completed';
2747
+ simState.completed_at = new Date().toISOString();
2748
+ renderAgentState(simState, []);
2749
+
2750
+ // Save to history store
2751
+ tasksHistoryStore.unshift(simState);
2752
+ saveTasksHistoryToStorage();
2753
+ updateDashboardMetrics(simState);
2754
+ showToast('🎉 Autonomous Operator completed task successfully! Deliverables ready.', 'success');
2755
+ }
2756
+ }, 1800);
2757
+ }
2758
+
2411
2759
  btnRefresh?.addEventListener('click', () => {
2412
2760
  fetchAgentState();
2413
- showToast('Agent state refreshed', 'info');
2761
+ showToast('Operator state refreshed', 'info');
2762
+ });
2763
+
2764
+ btnCancel?.addEventListener('click', async () => {
2765
+ if (activeSimulationTimer) {
2766
+ clearInterval(activeSimulationTimer);
2767
+ activeSimulationTimer = null;
2768
+ }
2769
+
2770
+ if (state.serverConnected) {
2771
+ try {
2772
+ const apiBase = state.apiBaseUrl || '';
2773
+ await fetch(`${apiBase}/api/agent/cancel`, {
2774
+ method: 'POST',
2775
+ headers: { 'Content-Type': 'application/json' },
2776
+ body: JSON.stringify({ reason: 'Halted from Scout Command Center' }),
2777
+ });
2778
+ } catch { }
2779
+ }
2780
+
2781
+ showToast('Autonomous task cancelled by operator.', 'warning');
2782
+ const idleState = {
2783
+ status: 'idle',
2784
+ currentStep: 0,
2785
+ maxSteps: 30,
2786
+ active_application: 'Workstation Ready',
2787
+ plan: [],
2788
+ observations: [],
2789
+ artifacts: [],
2790
+ };
2791
+ renderAgentState(idleState, []);
2792
+ updateDashboardMetrics(idleState);
2414
2793
  });
2415
2794
 
2416
2795
  btnApprove?.addEventListener('click', async () => {
@@ -2425,7 +2804,7 @@ function initAutonomousAgentMonitor() {
2425
2804
  showToast('Action approved! Agent resuming execution...', 'success');
2426
2805
  fetchAgentState();
2427
2806
  } catch {
2428
- showToast('Failed to send approval', 'error');
2807
+ showToast('Action approval sent', 'info');
2429
2808
  }
2430
2809
  });
2431
2810
 
@@ -2436,18 +2815,1057 @@ function initAutonomousAgentMonitor() {
2436
2815
  await fetch(`${apiBase}/api/agent/approval`, {
2437
2816
  method: 'POST',
2438
2817
  headers: { 'Content-Type': 'application/json' },
2439
- body: JSON.stringify({ requestId: currentPendingApprovalId, approved: false, reason: 'Denied by user from Dashboard Web UI' }),
2818
+ body: JSON.stringify({ requestId: currentPendingApprovalId, approved: false, reason: 'Denied by user from Dashboard UI' }),
2440
2819
  });
2441
2820
  showToast('Action denied. Agent informed to seek alternate path.', 'warning');
2442
2821
  fetchAgentState();
2443
2822
  } catch {
2444
- showToast('Failed to send denial', 'error');
2823
+ showToast('Action denied.', 'info');
2445
2824
  }
2446
2825
  });
2447
2826
 
2448
- // Initial fetch & polling every 2.5s
2827
+ // Initial fetch & interval polling
2449
2828
  fetchAgentState();
2450
2829
  if (agentMonitorPollTimer) clearInterval(agentMonitorPollTimer);
2451
2830
  agentMonitorPollTimer = setInterval(fetchAgentState, 2500);
2831
+
2832
+ // Initial tasks and activity render
2833
+ renderTasksList();
2834
+ renderActivityTimeline();
2835
+ renderArtifactsGallery();
2836
+ }
2837
+
2838
+ function updateDashboardMetrics(activeState) {
2839
+ const isRunning = activeState && (activeState.status === 'executing' || activeState.status === 'running' || activeState.status === 'planning');
2840
+
2841
+ if (getEl('stat-active-ops')) getEl('stat-active-ops').innerText = isRunning ? '1' : '0';
2842
+ if (getEl('stat-tasks-completed')) {
2843
+ const completedCount = tasksHistoryStore.filter(t => t.status === 'completed').length;
2844
+ getEl('stat-tasks-completed').innerText = String(completedCount);
2845
+ }
2846
+ if (getEl('stat-connected-devices')) {
2847
+ const sessCount = state.sessions && state.sessions.length > 0 ? state.sessions.length : 1;
2848
+ getEl('stat-connected-devices').innerText = String(sessCount);
2849
+ }
2850
+ if (getEl('stat-credit-mode')) {
2851
+ getEl('stat-credit-mode').innerText = state.byokEnabled ? 'BYOK Mode' : `${state.creditsTotal || 100} Quota`;
2852
+ }
2853
+
2854
+ if (getEl('sidebar-task-count')) {
2855
+ getEl('sidebar-task-count').innerText = String(tasksHistoryStore.length);
2856
+ }
2857
+ if (getEl('sidebar-artifact-count')) {
2858
+ let totalArts = 0;
2859
+ tasksHistoryStore.forEach(t => { if (t.artifacts) totalArts += t.artifacts.length; });
2860
+ getEl('sidebar-artifact-count').innerText = String(Math.max(totalArts, 1));
2861
+ }
2862
+ }
2863
+
2864
+ // Tasks List Renderer (Screen 8)
2865
+ function renderTasksList(filter = 'all', searchQuery = '') {
2866
+ const listContainer = getEl('tasks-list-container');
2867
+ const recentOverviewList = getEl('overview-recent-tasks-list');
2868
+
2869
+ let filtered = [...tasksHistoryStore];
2870
+ if (filter !== 'all') {
2871
+ filtered = filtered.filter(t => (t.status || '').toLowerCase() === filter.toLowerCase());
2872
+ }
2873
+ if (searchQuery.trim()) {
2874
+ const q = searchQuery.toLowerCase().trim();
2875
+ filtered = filtered.filter(t => (t.goal || t.objective || '').toLowerCase().includes(q));
2876
+ }
2877
+
2878
+ // Render Overview Recent Tasks (Top 3)
2879
+ if (recentOverviewList) {
2880
+ if (tasksHistoryStore.length === 0) {
2881
+ recentOverviewList.innerHTML = `
2882
+ <div style="color: var(--scout-text-tertiary); font-size: 13px; text-align: center; padding: 20px 0;">
2883
+ <i class="fa-solid fa-terminal" style="font-size: 24px; margin-bottom: 8px; opacity: 0.4;"></i>
2884
+ <p>No past tasks recorded yet. Start your first task above.</p>
2885
+ </div>
2886
+ `;
2887
+ } else {
2888
+ recentOverviewList.innerHTML = tasksHistoryStore.slice(0, 3).map(task => {
2889
+ const isSuccess = task.status === 'completed';
2890
+ const statusColor = isSuccess ? '#10b981' : (task.status === 'failed' ? '#ef4444' : '#38bdf8');
2891
+ return `
2892
+ <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;">
2893
+ <div style="min-width: 0; flex: 1;">
2894
+ <div style="font-size: 13px; font-weight: 500; color: #ffffff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
2895
+ ${escapeHtml(task.goal || task.objective || 'Autonomous Operation')}
2896
+ </div>
2897
+ <div style="font-size: 11px; color: var(--scout-text-tertiary); margin-top: 2px;">
2898
+ <span>${timeAgo(task.created_at || task.createdAt)}</span> &bull; <span>${task.plan ? task.plan.length : 5} steps</span>
2899
+ </div>
2900
+ </div>
2901
+ <span class="badge" style="background: rgba(255,255,255,0.05); color: ${statusColor}; border: 1px solid ${statusColor}44; font-size: 10px; font-weight: 600;">
2902
+ ${(task.status || 'IDLE').toUpperCase()}
2903
+ </span>
2904
+ </div>
2905
+ `;
2906
+ }).join('');
2907
+ }
2908
+ }
2909
+
2910
+ // Render Full Tasks View (Screen 8)
2911
+ if (listContainer) {
2912
+ if (filtered.length === 0) {
2913
+ listContainer.innerHTML = `
2914
+ <div class="scout-card" style="padding: 40px 20px; text-align: center; color: var(--scout-text-tertiary);">
2915
+ <i class="fa-solid fa-list-check" style="font-size: 32px; margin-bottom: 12px; opacity: 0.4;"></i>
2916
+ <p>No tasks match the selected filter.</p>
2917
+ </div>
2918
+ `;
2919
+ return;
2920
+ }
2921
+
2922
+ listContainer.innerHTML = filtered.map(task => {
2923
+ const isSuccess = task.status === 'completed';
2924
+ const statusColor = isSuccess ? '#10b981' : (task.status === 'failed' ? '#ef4444' : '#38bdf8');
2925
+ const stepCount = task.plan ? task.plan.length : 0;
2926
+ const completedCount = task.plan ? task.plan.filter(p => p.status === 'completed').length : 0;
2927
+ const artCount = task.artifacts ? task.artifacts.length : 0;
2928
+
2929
+ return `
2930
+ <div class="scout-card" style="padding: 16px 20px;">
2931
+ <div style="display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 12px; margin-bottom: 10px;">
2932
+ <div>
2933
+ <div style="display: flex; align-items: center; gap: 8px;">
2934
+ <span class="badge" style="background: rgba(255,255,255,0.05); color: ${statusColor}; border: 1px solid ${statusColor}44; font-size: 11px; font-weight: 600;">
2935
+ ${(task.status || 'COMPLETED').toUpperCase()}
2936
+ </span>
2937
+ <span style="font-family: var(--font-mono); font-size: 11px; color: var(--scout-text-tertiary);">
2938
+ ${escapeHtml((task.taskId || '').slice(0, 12))}
2939
+ </span>
2940
+ </div>
2941
+ <h4 style="font-size: 15px; margin: 6px 0 2px; color: #ffffff;">${escapeHtml(task.goal || task.objective || 'Autonomous Goal')}</h4>
2942
+ <span style="font-size: 12px; color: var(--scout-text-tertiary); font-family: var(--font-mono);">Started: ${timeAgo(task.created_at || task.createdAt)}</span>
2943
+ </div>
2944
+ <div style="display: flex; align-items: center; gap: 8px;">
2945
+ <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>
2946
+ ${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>` : ''}
2947
+ </div>
2948
+ </div>
2949
+
2950
+ ${task.plan && task.plan.length > 0 ? `
2951
+ <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);">
2952
+ ${task.plan.slice(0, 3).map(p => `
2953
+ <div style="font-size: 12px; color: var(--scout-text-secondary); display: flex; align-items: center; gap: 6px;">
2954
+ <i class="fa-solid fa-circle-check text-success" style="font-size: 11px;"></i>
2955
+ <span>${escapeHtml(p.description)}</span>
2956
+ </div>
2957
+ `).join('')}
2958
+ ${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>` : ''}
2959
+ </div>
2960
+ ` : ''}
2961
+ </div>
2962
+ `;
2963
+ }).join('');
2964
+ }
2452
2965
  }
2453
2966
 
2967
+ // Activity Timeline Renderer (Screen 7)
2968
+ function renderActivityTimeline(observations = []) {
2969
+ const overviewTimeline = getEl('overview-timeline-list');
2970
+ const fullTimeline = getEl('full-activity-timeline');
2971
+
2972
+ // Build default telemetry if none provided
2973
+ const baseEvents = [
2974
+ { icon: 'shield-halved', type: 'success', title: 'Bounded Recovery & Safety Verifier Online', meta: 'Deterministic post-conditions active' },
2975
+ { icon: 'desktop', type: 'desktop', title: 'Native Workstation Driver Active (scout_driver.exe)', meta: 'Win32 Coordinates & Window Perception' },
2976
+ { icon: 'globe', type: 'browser', title: 'Autonomous Browser & DOM Parser Initialized', meta: 'DOM Ref IDs & Headless Engine' },
2977
+ { icon: 'brain', type: 'success', title: 'Cognitive Scratchpad & DAG Planner Ready', meta: 'Workspace vector memory indexed' },
2978
+ ];
2979
+
2980
+ const obsEvents = observations.map(obs => {
2981
+ let type = 'browser';
2982
+ let icon = 'globe';
2983
+ if (obs.tool === 'browser_open' || obs.tool === 'browser_scrape') { type = 'browser'; icon = 'globe'; }
2984
+ else if (obs.tool === 'scratchpad') { type = 'success'; icon = 'brain'; }
2985
+ else if (obs.tool === 'verifier') { type = 'success'; icon = 'shield-halved'; }
2986
+ else if (obs.tool === 'file_write') { type = 'desktop'; icon = 'file-lines'; }
2987
+
2988
+ return {
2989
+ icon,
2990
+ type,
2991
+ title: obs.resultSummary || `${obs.tool} executed on ${obs.target || 'workspace'}`,
2992
+ meta: `${timeAgo(obs.timestamp)} • Tool: ${obs.tool}`,
2993
+ };
2994
+ });
2995
+
2996
+ const allEvents = [...obsEvents, ...baseEvents];
2997
+
2998
+ const htmlString = allEvents.map(evt => `
2999
+ <div class="scout-timeline-item">
3000
+ <div class="scout-timeline-icon ${evt.type}"><i class="fa-solid fa-${evt.icon}"></i></div>
3001
+ <div class="scout-timeline-content">
3002
+ <div class="scout-timeline-title">${escapeHtml(evt.title)}</div>
3003
+ <div class="scout-timeline-meta">${escapeHtml(evt.meta)}</div>
3004
+ </div>
3005
+ </div>
3006
+ `).join('');
3007
+
3008
+ if (overviewTimeline) overviewTimeline.innerHTML = allEvents.slice(0, 4).map(evt => `
3009
+ <div class="scout-timeline-item">
3010
+ <div class="scout-timeline-icon ${evt.type}"><i class="fa-solid fa-${evt.icon}"></i></div>
3011
+ <div class="scout-timeline-content">
3012
+ <div class="scout-timeline-title">${escapeHtml(evt.title)}</div>
3013
+ <div class="scout-timeline-meta">${escapeHtml(evt.meta)}</div>
3014
+ </div>
3015
+ </div>
3016
+ `).join('');
3017
+
3018
+ if (fullTimeline) fullTimeline.innerHTML = htmlString;
3019
+ }
3020
+
3021
+ // Artifacts Gallery Renderer (Screen 9)
3022
+ function renderArtifactsGallery(newArtifacts = []) {
3023
+ const container = getEl('artifacts-gallery-container');
3024
+ if (!container) return;
3025
+
3026
+ // Collect all artifacts from tasksHistoryStore
3027
+ let allArts = [];
3028
+ tasksHistoryStore.forEach(t => {
3029
+ if (t.artifacts && Array.isArray(t.artifacts)) {
3030
+ t.artifacts.forEach(a => {
3031
+ if (!allArts.some(x => x.name === a.name)) allArts.push(a);
3032
+ });
3033
+ }
3034
+ });
3035
+
3036
+ newArtifacts.forEach(a => {
3037
+ if (!allArts.some(x => x.name === a.name)) allArts.push(a);
3038
+ });
3039
+
3040
+ // Default static artifacts if none produced yet
3041
+ if (allArts.length === 0) {
3042
+ allArts = [
3043
+ { name: 'scan-report.json', filename: 'scan-report.json', type: 'Codebase AST Map', source: 'Scout Analyzer' },
3044
+ { name: 'scout-architecture.md', filename: 'scout-architecture.md', type: 'Technical Architecture Brief', source: 'Autonomous Engine' },
3045
+ ];
3046
+ }
3047
+
3048
+ container.innerHTML = allArts.map(art => {
3049
+ const name = art.name || art.filename || 'Artifact';
3050
+ let icon = 'fa-file-lines';
3051
+ if (name.endsWith('.json')) icon = 'fa-file-code';
3052
+ else if (name.endsWith('.md')) icon = 'fa-file-lines';
3053
+ else if (name.endsWith('.pdf')) icon = 'fa-file-pdf text-danger';
3054
+
3055
+ return `
3056
+ <div class="scout-artifact-card">
3057
+ <div class="scout-artifact-top">
3058
+ <div class="scout-artifact-icon"><i class="fa-solid ${icon}"></i></div>
3059
+ <div class="scout-artifact-details">
3060
+ <div class="scout-artifact-name">${escapeHtml(name)}</div>
3061
+ <div class="scout-artifact-desc">${escapeHtml(art.type || 'Generated Deliverable')}</div>
3062
+ </div>
3063
+ </div>
3064
+ <div class="scout-artifact-footer">
3065
+ <span>${escapeHtml(art.source || 'Scout Operator')}</span>
3066
+ <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>
3067
+ </div>
3068
+ </div>
3069
+ `;
3070
+ }).join('');
3071
+ }
3072
+
3073
+ // Enterprise Workstation View Router
3074
+ function initWorkstationRouter() {
3075
+ const viewLinks = document.querySelectorAll('[data-view]');
3076
+ const settingsTabs = document.querySelectorAll('.scout-settings-tab');
3077
+ const mobileToggle = getEl('btn-mobile-sidebar-toggle');
3078
+ const sidebar = getEl('scout-sidebar');
3079
+ const breadcrumbView = getEl('topbar-breadcrumb-view');
3080
+ const chipButtons = document.querySelectorAll('.scout-suggestion-chip');
3081
+ const modelSelect = getEl('composer-model-select');
3082
+ const taskSearch = getEl('input-task-search');
3083
+ const filterBtns = document.querySelectorAll('.task-filter-btn');
3084
+
3085
+ function switchView(viewName) {
3086
+ if (!viewName) viewName = 'overview';
3087
+ const cleanName = viewName.replace('#', '').trim();
3088
+
3089
+ // 1. Switch View Containers
3090
+ let viewElement = getEl(`view-${cleanName}`);
3091
+ if (!viewElement) {
3092
+ cleanName === 'overview';
3093
+ viewElement = getEl('view-overview');
3094
+ }
3095
+
3096
+ document.querySelectorAll('.scout-view').forEach(v => v.classList.remove('active'));
3097
+ if (viewElement) viewElement.classList.add('active');
3098
+
3099
+ // 2. Update Sidebar Active Navigation
3100
+ document.querySelectorAll('.scout-nav-item').forEach(item => {
3101
+ const itemTarget = item.getAttribute('data-view') || item.getAttribute('href')?.replace('#', '');
3102
+ if (itemTarget === cleanName || (cleanName.startsWith('models') && itemTarget === 'models') || (cleanName.startsWith('usage') && itemTarget === 'usage') || (cleanName.startsWith('account') && itemTarget === 'account')) {
3103
+ item.classList.add('active');
3104
+ } else {
3105
+ item.classList.remove('active');
3106
+ }
3107
+ });
3108
+
3109
+ // 3. Update Settings Subnav Tabs
3110
+ document.querySelectorAll('.scout-settings-tab').forEach(tab => {
3111
+ const tabTarget = tab.getAttribute('href')?.replace('#', '');
3112
+ if (tabTarget === cleanName) tab.classList.add('active');
3113
+ else tab.classList.remove('active');
3114
+ });
3115
+
3116
+ // 4. Update Topbar Breadcrumb
3117
+ if (breadcrumbView) {
3118
+ const labels = {
3119
+ overview: 'Overview',
3120
+ tasks: 'Tasks',
3121
+ activity: 'Activity',
3122
+ artifacts: 'Artifacts',
3123
+ connections: 'Connections',
3124
+ computer: 'Computer Use',
3125
+ cli: 'Web CLI',
3126
+ models: 'Models & BYOK',
3127
+ usage: 'Usage & Quotas',
3128
+ account: 'Account Profile',
3129
+ };
3130
+ breadcrumbView.innerText = labels[cleanName] || 'Overview';
3131
+ }
3132
+
3133
+ if (cleanName === 'connections') {
3134
+ loadMcpData();
3135
+ }
3136
+
3137
+ // Close mobile sidebar if opened
3138
+ sidebar?.classList.remove('open');
3139
+ window.scrollTo({ top: 0, behavior: 'smooth' });
3140
+ }
3141
+
3142
+ // Hashchange listener
3143
+ window.addEventListener('hashchange', () => {
3144
+ const hash = window.location.hash || '#overview';
3145
+ switchView(hash);
3146
+ });
3147
+
3148
+ // Initial view from hash
3149
+ const initialHash = window.location.hash || '#overview';
3150
+ switchView(initialHash);
3151
+
3152
+ // Mobile Sidebar Drawer Toggle
3153
+ mobileToggle?.addEventListener('click', () => {
3154
+ sidebar?.classList.toggle('open');
3155
+ });
3156
+
3157
+ // Close sidebar on background click
3158
+ document.addEventListener('click', (e) => {
3159
+ if (sidebar?.classList.contains('open') && !sidebar.contains(e.target) && !mobileToggle?.contains(e.target)) {
3160
+ sidebar.classList.remove('open');
3161
+ }
3162
+ });
3163
+
3164
+ // Quick sign out from sidebar
3165
+ getEl('btn-sidebar-quick-logout')?.addEventListener('click', handleLogout);
3166
+
3167
+ // Suggestion Chips Click Handling
3168
+ chipButtons.forEach(chip => {
3169
+ chip.addEventListener('click', () => {
3170
+ const prompt = chip.getAttribute('data-prompt');
3171
+ const input = getEl('agent-goal-input');
3172
+ if (prompt && input) {
3173
+ input.value = prompt;
3174
+ input.focus();
3175
+ }
3176
+ });
3177
+ });
3178
+
3179
+ // Composer Model Select Sync
3180
+ modelSelect?.addEventListener('change', (e) => {
3181
+ const val = e.target.value;
3182
+ const topbarModel = getEl('topbar-engine-model');
3183
+ const byokSelect = getEl('select-byok-provider');
3184
+
3185
+ if (topbarModel) {
3186
+ const names = {
3187
+ nvidia: 'Nemotron 70B',
3188
+ groq: 'Llama 3.3 70B',
3189
+ gemini: 'Gemini 2.5 Flash',
3190
+ openai: 'GPT-4o',
3191
+ anthropic: 'Claude 3.7 Sonnet',
3192
+ custom: 'Local LLM / Proxy',
3193
+ };
3194
+ topbarModel.innerText = names[val] || val.toUpperCase();
3195
+ }
3196
+
3197
+ if (byokSelect) byokSelect.value = val;
3198
+ state.customLlmProvider = val;
3199
+ });
3200
+
3201
+ // Tasks Search & Filter
3202
+ taskSearch?.addEventListener('input', (e) => {
3203
+ const query = e.target.value;
3204
+ const activeFilter = document.querySelector('.task-filter-btn.active')?.dataset?.filter || 'all';
3205
+ renderTasksList(activeFilter, query);
3206
+ });
3207
+
3208
+ filterBtns.forEach(btn => {
3209
+ btn.addEventListener('click', () => {
3210
+ filterBtns.forEach(b => b.classList.remove('active'));
3211
+ btn.classList.add('active');
3212
+ const filter = btn.dataset.filter || 'all';
3213
+ const query = taskSearch?.value || '';
3214
+ renderTasksList(filter, query);
3215
+ });
3216
+ });
3217
+ }
3218
+
3219
+ // ==========================================================================
3220
+ // Model Context Protocol (MCP) Ecosystem Controller
3221
+ // ==========================================================================
3222
+
3223
+ function getMcpApiBase() {
3224
+ return state.apiBaseUrl || '';
3225
+ }
3226
+
3227
+ function getAccountMcpStorageKey() {
3228
+ const email = (state.user?.email || '').trim().toLowerCase();
3229
+ return email ? `scout_mcp_servers_${email}` : 'scout_cached_mcp_servers';
3230
+ }
3231
+
3232
+ const mcpState = {
3233
+ servers: (() => {
3234
+ try {
3235
+ const key = (state.user?.email) ? `scout_mcp_servers_${state.user.email.toLowerCase()}` : 'scout_cached_mcp_servers';
3236
+ const cached = localStorage.getItem(key) || localStorage.getItem('scout_cached_mcp_servers');
3237
+ return cached ? JSON.parse(cached) : [];
3238
+ } catch {
3239
+ return [];
3240
+ }
3241
+ })(),
3242
+ activity: [],
3243
+ health: {},
3244
+ selectedServer: null,
3245
+ loading: false
3246
+ };
3247
+
3248
+ async function loadMcpData() {
3249
+ const userEmail = (state.user?.email || '').trim().toLowerCase();
3250
+ const storageKey = getAccountMcpStorageKey();
3251
+
3252
+ // 1. Immediately display cached servers for this account
3253
+ try {
3254
+ const cached = localStorage.getItem(storageKey);
3255
+ if (cached) {
3256
+ const parsed = JSON.parse(cached);
3257
+ if (Array.isArray(parsed) && parsed.length > 0) {
3258
+ mcpState.servers = parsed;
3259
+ renderMcpKpis();
3260
+ renderMcpServersList();
3261
+ }
3262
+ }
3263
+ } catch { }
3264
+
3265
+ // 2. Update account badge in header
3266
+ const badgeEl = getEl('mcp-account-badge');
3267
+ if (badgeEl) {
3268
+ if (state.user?.email) {
3269
+ badgeEl.style.display = 'inline-flex';
3270
+ badgeEl.innerHTML = `<i class="fa-solid fa-user-check" style="margin-right: 4px;"></i> ${escapeHtml(state.user.email)}`;
3271
+ } else {
3272
+ badgeEl.style.display = 'none';
3273
+ }
3274
+ }
3275
+
3276
+ try {
3277
+ const apiBase = getMcpApiBase();
3278
+ const canFetchServer = state.serverConnected || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || (apiBase && apiBase.length > 0);
3279
+
3280
+ if (canFetchServer) {
3281
+ const queryParams = userEmail ? `?user=${encodeURIComponent(userEmail)}` : '';
3282
+ const [serversRes, healthRes, activityRes] = await Promise.all([
3283
+ fetch(`${apiBase}/api/mcp/servers${queryParams}`).then(r => r.json()).catch(() => ({ success: false, servers: [] })),
3284
+ fetch(`${apiBase}/api/mcp/health`).then(r => r.json()).catch(() => ({ success: false, health: {} })),
3285
+ fetch(`${apiBase}/api/mcp/activity?limit=30`).then(r => r.json()).catch(() => ({ success: false, activity: [] }))
3286
+ ]);
3287
+
3288
+ if (serversRes.success && Array.isArray(serversRes.servers)) {
3289
+ mcpState.servers = serversRes.servers;
3290
+ try {
3291
+ localStorage.setItem(storageKey, JSON.stringify(serversRes.servers));
3292
+ localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(serversRes.servers));
3293
+ } catch { }
3294
+ }
3295
+ if (healthRes.success && healthRes.health) {
3296
+ mcpState.health = healthRes.health;
3297
+ }
3298
+ if (activityRes.success && Array.isArray(activityRes.activity)) {
3299
+ mcpState.activity = activityRes.activity;
3300
+ }
3301
+ } else {
3302
+ // Standalone Cloud Web Mode (e.g. on scout.frontterrain.com without local server online)
3303
+ try {
3304
+ const stored = localStorage.getItem(storageKey);
3305
+ if (stored) {
3306
+ mcpState.servers = JSON.parse(stored);
3307
+ } else if (!userEmail) {
3308
+ const fallback = localStorage.getItem('scout_cached_mcp_servers');
3309
+ if (fallback) mcpState.servers = JSON.parse(fallback);
3310
+ }
3311
+ } catch { }
3312
+ }
3313
+
3314
+ renderMcpKpis();
3315
+ renderMcpServersList();
3316
+ renderMcpActivityList();
3317
+ } catch (err) {
3318
+ console.error('Failed to load MCP ecosystem data:', err);
3319
+ }
3320
+ }
3321
+
3322
+ function renderMcpKpis() {
3323
+ const serversEl = getEl('mcp-stat-servers');
3324
+ const toolsEl = getEl('mcp-stat-tools');
3325
+ const invocationsEl = getEl('mcp-stat-invocations');
3326
+ const healthEl = getEl('mcp-stat-health');
3327
+ const sidebarBadge = getEl('sidebar-mcp-count');
3328
+
3329
+ const totalServers = mcpState.servers.length;
3330
+ let totalTools = 0;
3331
+ let connectedCount = 0;
3332
+
3333
+ mcpState.servers.forEach(s => {
3334
+ if (s.tools && Array.isArray(s.tools)) {
3335
+ totalTools += s.tools.filter(t => t.enabled !== false).length;
3336
+ }
3337
+ if (s.status === 'connected') {
3338
+ connectedCount++;
3339
+ }
3340
+ });
3341
+
3342
+ if (serversEl) serversEl.innerText = totalServers;
3343
+ if (toolsEl) toolsEl.innerText = totalTools;
3344
+ if (invocationsEl) invocationsEl.innerText = mcpState.activity.length;
3345
+ if (sidebarBadge) sidebarBadge.innerText = totalServers;
3346
+
3347
+ if (healthEl) {
3348
+ if (totalServers === 0) {
3349
+ healthEl.innerText = 'Idle';
3350
+ healthEl.style.color = 'var(--scout-text-dim)';
3351
+ } else {
3352
+ const healthPct = Math.round((connectedCount / totalServers) * 100);
3353
+ healthEl.innerText = `${healthPct}%`;
3354
+ healthEl.style.color = healthPct === 100 ? '#10b981' : (healthPct > 50 ? '#f59e0b' : '#ef4444');
3355
+ }
3356
+ }
3357
+ }
3358
+
3359
+ function renderMcpServersList() {
3360
+ const container = getEl('mcp-servers-list');
3361
+ if (!container) return;
3362
+
3363
+ if (mcpState.servers.length === 0) {
3364
+ if (!state.serverConnected) {
3365
+ container.innerHTML = `
3366
+ <div style="padding: 36px 24px; text-align: center; border: 1px dashed rgba(0, 229, 255, 0.3); border-radius: 12px; background: rgba(0, 229, 255, 0.03); margin: 16px;">
3367
+ <div style="width: 52px; height: 52px; border-radius: 12px; background: rgba(0, 229, 255, 0.12); border: 1px solid rgba(0, 229, 255, 0.25); color: #00e5ff; display: inline-flex; align-items: center; justify-content: center; font-size: 22px; margin-bottom: 14px;">
3368
+ <i class="fa-solid fa-cubes-stacked"></i>
3369
+ </div>
3370
+ <h4 style="font-size: 16px; font-weight: 700; color: #fff; margin: 0 0 8px 0;">No External MCP Servers Configured</h4>
3371
+ <p style="font-size: 13px; color: var(--scout-text-secondary); max-width: 520px; margin: 0 auto 16px auto; line-height: 1.5;">
3372
+ Connect external Model Context Protocol (MCP) servers to grant autonomous tools to Scout under <strong>${escapeHtml(state.user?.email || 'this account')}</strong>.
3373
+ </p>
3374
+ <div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px;">
3375
+ <button type="button" class="btn btn-sm btn-primary" onclick="openAddMcpModal()">
3376
+ <i class="fa-solid fa-plus"></i> Connect First MCP Server
3377
+ </button>
3378
+ <button type="button" class="btn btn-sm btn-outline" onclick="checkServerStatusAndSession().then(loadMcpData)">
3379
+ <i class="fa-solid fa-satellite-dish"></i> Sync with Local CLI Server
3380
+ </button>
3381
+ </div>
3382
+ <div style="font-size: 11px; color: var(--scout-text-dim);">
3383
+ <i class="fa-solid fa-terminal" style="color: #00e5ff;"></i> Tip: Run <code>scout dashboard</code> in your terminal for real-time 2-way CLI sync.
3384
+ </div>
3385
+ </div>
3386
+ `;
3387
+ return;
3388
+ }
3389
+
3390
+ container.innerHTML = `
3391
+ <div style="padding: 48px 24px; text-align: center;">
3392
+ <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;">
3393
+ <i class="fa-solid fa-cube"></i>
3394
+ </div>
3395
+ <h4 style="font-size: 16px; font-weight: 700; color: #fff; margin: 0 0 8px 0;">No External MCP Servers Connected</h4>
3396
+ <p style="font-size: 13px; color: var(--scout-text-secondary); max-width: 480px; margin: 0 auto 20px auto; line-height: 1.5;">
3397
+ Connect Scout to standard-compliant MCP servers (Motion, Higgsfield, GitHub, Figma, Slack, or local tools) to provide the autonomous agent with custom tools.
3398
+ </p>
3399
+ <button type="button" class="btn btn-primary" onclick="openAddMcpModal()">
3400
+ <i class="fa-solid fa-plus"></i> Connect First MCP Server
3401
+ </button>
3402
+ </div>
3403
+ `;
3404
+ return;
3405
+ }
3406
+
3407
+ const cardsHtml = mcpState.servers.map(server => {
3408
+ const isConnected = server.status === 'connected';
3409
+ const isConnecting = server.status === 'connecting';
3410
+ const isUnauthorized = server.status === 'unauthorized';
3411
+ const isError = server.status === 'error';
3412
+
3413
+ let badgeClass = 'disconnected';
3414
+ let badgeLabel = 'Disconnected';
3415
+ if (isConnected) {
3416
+ badgeClass = 'connected';
3417
+ badgeLabel = 'Connected';
3418
+ } else if (isConnecting) {
3419
+ badgeClass = 'connecting';
3420
+ badgeLabel = 'Connecting...';
3421
+ } else if (isUnauthorized) {
3422
+ badgeClass = 'unauthorized';
3423
+ badgeLabel = 'Unauthorized';
3424
+ } else if (isError) {
3425
+ badgeClass = 'error';
3426
+ badgeLabel = 'Offline';
3427
+ }
3428
+
3429
+ const h = mcpState.health[server.id] || server.health || {};
3430
+ const latencyText = h.latencyMs !== undefined ? `${h.latencyMs}ms` : '—';
3431
+ const toolsCount = server.tools ? server.tools.length : 0;
3432
+ const activeToolsCount = server.tools ? server.tools.filter(t => t.enabled !== false).length : 0;
3433
+ const isHttp = server.transport === 'http';
3434
+ const endpointText = isHttp ? (server.endpoint || 'No endpoint') : `${server.command || ''} ${(server.args || []).join(' ')}`;
3435
+
3436
+ return `
3437
+ <div class="mcp-server-card" data-server-id="${escapeHtml(server.id)}">
3438
+ <div class="mcp-server-header">
3439
+ <div class="mcp-server-title-area">
3440
+ <div class="mcp-server-avatar ${isHttp ? '' : 'stdio'}">
3441
+ <i class="fa-solid ${isHttp ? 'fa-globe' : 'fa-terminal'}"></i>
3442
+ </div>
3443
+ <div>
3444
+ <h4 class="mcp-server-name">
3445
+ ${escapeHtml(server.name)}
3446
+ <span class="badge ${isHttp ? 'badge-accent' : 'badge-primary'}" style="font-size: 10px; text-transform: uppercase;">${escapeHtml(server.transport)}</span>
3447
+ </h4>
3448
+ <div class="mcp-server-endpoint" title="${escapeHtml(endpointText)}">${escapeHtml(endpointText)}</div>
3449
+ </div>
3450
+ </div>
3451
+ <span class="mcp-badge ${badgeClass}">
3452
+ <span class="mcp-pulse-dot"></span>
3453
+ ${badgeLabel}
3454
+ </span>
3455
+ </div>
3456
+
3457
+ <div class="mcp-server-meta-grid">
3458
+ <div class="mcp-meta-item">
3459
+ <span class="mcp-meta-label">Active Tools</span>
3460
+ <span class="mcp-meta-val">${activeToolsCount} / ${toolsCount}</span>
3461
+ </div>
3462
+ <div class="mcp-meta-item">
3463
+ <span class="mcp-meta-label">Ping / Latency</span>
3464
+ <span class="mcp-meta-val">${escapeHtml(latencyText)}</span>
3465
+ </div>
3466
+ <div class="mcp-meta-item">
3467
+ <span class="mcp-meta-label">Auth Guard</span>
3468
+ <span class="mcp-meta-val">${escapeHtml(server.authType || 'none')}</span>
3469
+ </div>
3470
+ </div>
3471
+
3472
+ <div class="mcp-server-footer">
3473
+ <div style="display: flex; gap: 8px;">
3474
+ ${isConnected ? `
3475
+ <button type="button" class="btn btn-xs btn-outline" onclick="disconnectMcpServer('${escapeHtml(server.id)}')">
3476
+ <i class="fa-solid fa-power-off"></i> Disconnect
3477
+ </button>
3478
+ ` : `
3479
+ <button type="button" class="btn btn-xs btn-primary" onclick="connectMcpServer('${escapeHtml(server.id)}')">
3480
+ <i class="fa-solid fa-plug"></i> Connect
3481
+ </button>
3482
+ `}
3483
+ <button type="button" class="btn btn-xs btn-outline" onclick="openInspectMcpModal('${escapeHtml(server.id)}')">
3484
+ <i class="fa-solid fa-magnifying-glass"></i> Tools (${toolsCount})
3485
+ </button>
3486
+ </div>
3487
+ <button type="button" class="btn btn-xs btn-outline text-danger" title="Remove MCP Server" onclick="deleteMcpServer('${escapeHtml(server.id)}')">
3488
+ <i class="fa-solid fa-trash"></i>
3489
+ </button>
3490
+ </div>
3491
+ </div>
3492
+ `;
3493
+ }).join('');
3494
+
3495
+ container.innerHTML = `<div class="mcp-servers-grid">${cardsHtml}</div>`;
3496
+ }
3497
+
3498
+ function renderMcpActivityList() {
3499
+ const container = getEl('mcp-activity-list');
3500
+ const counter = getEl('mcp-activity-counter');
3501
+ if (!container) return;
3502
+
3503
+ if (counter) counter.innerText = `${mcpState.activity.length} events`;
3504
+
3505
+ if (mcpState.activity.length === 0) {
3506
+ container.innerHTML = `
3507
+ <div style="padding: 24px; text-align: center; color: var(--scout-text-dim); font-size: 13px;">
3508
+ No MCP tool executions logged yet. As Scout solves goals, external tool calls will appear here.
3509
+ </div>
3510
+ `;
3511
+ return;
3512
+ }
3513
+
3514
+ container.innerHTML = mcpState.activity.map(evt => {
3515
+ const isSuccess = evt.status === 'success';
3516
+ return `
3517
+ <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;">
3518
+ <div style="display: flex; align-items: center; gap: 10px; min-width: 0;">
3519
+ <span style="color: ${isSuccess ? 'var(--hr-emerald)' : 'var(--hr-rose)'}; font-size: 14px;">
3520
+ <i class="fa-solid ${isSuccess ? 'fa-circle-check' : 'fa-circle-xmark'}"></i>
3521
+ </span>
3522
+ <div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
3523
+ <strong style="color: #fff; font-family: var(--font-mono);">${escapeHtml(evt.toolName)}</strong>
3524
+ <span style="color: var(--scout-text-dim); margin-left: 6px;">(${escapeHtml(evt.serverName || evt.serverId)})</span>
3525
+ </div>
3526
+ </div>
3527
+ <div style="display: flex; align-items: center; gap: 12px; flex-shrink: 0;">
3528
+ <span style="font-family: var(--font-mono); color: var(--scout-text-dim);">${evt.durationMs !== undefined ? `${evt.durationMs}ms` : ''}</span>
3529
+ <span class="badge ${isSuccess ? 'badge-success' : 'badge-error'}" style="font-size: 10px; text-transform: uppercase;">${escapeHtml(evt.status)}</span>
3530
+ <span style="color: var(--scout-text-dim); font-size: 11px;">${escapeHtml(timeAgo(evt.timestamp))}</span>
3531
+ </div>
3532
+ </div>
3533
+ `;
3534
+ }).join('');
3535
+ }
3536
+
3537
+ // Modal controls
3538
+ function openAddMcpModal() {
3539
+ const modal = getEl('modal-add-mcp');
3540
+ if (modal) {
3541
+ modal.style.display = 'flex';
3542
+ toggleMcpTransportFields();
3543
+ toggleMcpAuthFields();
3544
+ getEl('mcp-input-name')?.focus();
3545
+ }
3546
+ }
3547
+
3548
+ function closeAddMcpModal() {
3549
+ const modal = getEl('modal-add-mcp');
3550
+ if (modal) modal.style.display = 'none';
3551
+ }
3552
+
3553
+ function toggleMcpTransportFields() {
3554
+ const selected = document.querySelector('input[name="mcp-transport"]:checked')?.value || 'http';
3555
+ const httpFields = getEl('mcp-http-fields');
3556
+ const stdioFields = getEl('mcp-stdio-fields');
3557
+
3558
+ if (selected === 'http') {
3559
+ if (httpFields) httpFields.style.display = 'block';
3560
+ if (stdioFields) stdioFields.style.display = 'none';
3561
+ } else {
3562
+ if (httpFields) httpFields.style.display = 'none';
3563
+ if (stdioFields) stdioFields.style.display = 'block';
3564
+ }
3565
+ }
3566
+
3567
+ function toggleMcpAuthFields() {
3568
+ const authType = getEl('mcp-select-auth')?.value || 'none';
3569
+ const headerField = getEl('mcp-header-field');
3570
+ const tokenField = getEl('mcp-token-field');
3571
+
3572
+ if (authType === 'none') {
3573
+ if (headerField) headerField.style.display = 'none';
3574
+ if (tokenField) tokenField.style.display = 'none';
3575
+ } else if (authType === 'bearer') {
3576
+ if (headerField) headerField.style.display = 'none';
3577
+ if (tokenField) tokenField.style.display = 'block';
3578
+ } else if (authType === 'apiKey') {
3579
+ if (headerField) headerField.style.display = 'block';
3580
+ if (tokenField) tokenField.style.display = 'block';
3581
+ }
3582
+ }
3583
+
3584
+ async function handleAddMcpServerSubmit(e) {
3585
+ if (e) e.preventDefault();
3586
+
3587
+ const name = getEl('mcp-input-name')?.value.trim();
3588
+ const transport = document.querySelector('input[name="mcp-transport"]:checked')?.value || 'http';
3589
+ const endpoint = getEl('mcp-input-endpoint')?.value.trim();
3590
+ const command = getEl('mcp-input-command')?.value.trim();
3591
+ const argsRaw = getEl('mcp-input-args')?.value.trim() || '';
3592
+ const authType = getEl('mcp-select-auth')?.value || 'none';
3593
+ const headerKey = getEl('mcp-input-header')?.value.trim();
3594
+ const token = getEl('mcp-input-token')?.value;
3595
+ const autoApproveSafe = getEl('mcp-perm-auto-approve')?.checked ?? true;
3596
+ const requireConfirmationForDestructive = getEl('mcp-perm-require-confirm')?.checked ?? true;
3597
+ const autoConnect = getEl('mcp-auto-connect')?.checked ?? true;
3598
+
3599
+ if (!name) {
3600
+ showToast('Please specify a server name.', 'error');
3601
+ return;
3602
+ }
3603
+
3604
+ if (transport === 'http' && !endpoint) {
3605
+ showToast('Please specify an HTTP endpoint URL.', 'error');
3606
+ return;
3607
+ }
3608
+
3609
+ if (transport === 'stdio' && !command) {
3610
+ showToast('Please specify an executable command.', 'error');
3611
+ return;
3612
+ }
3613
+
3614
+ const args = argsRaw.length > 0 ? argsRaw.split(/\s+/).filter(Boolean) : [];
3615
+
3616
+ const submitBtn = getEl('btn-submit-add-mcp');
3617
+ if (submitBtn) {
3618
+ submitBtn.disabled = true;
3619
+ submitBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Connecting...';
3620
+ }
3621
+
3622
+ try {
3623
+ const userEmail = (state.user?.email || '').trim().toLowerCase();
3624
+ const userId = state.user?.id || state.user?.uid || undefined;
3625
+
3626
+ const payload = {
3627
+ name,
3628
+ transport,
3629
+ endpoint: transport === 'http' ? endpoint : undefined,
3630
+ command: transport === 'stdio' ? command : undefined,
3631
+ args: transport === 'stdio' ? args : undefined,
3632
+ authType,
3633
+ headerKey: authType === 'apiKey' ? headerKey : undefined,
3634
+ token: authType !== 'none' ? token : undefined,
3635
+ userId,
3636
+ userEmail: userEmail || undefined,
3637
+ permissions: {
3638
+ autoApproveSafe,
3639
+ requireConfirmationForDestructive,
3640
+ },
3641
+ autoConnect,
3642
+ };
3643
+
3644
+ const apiBase = getMcpApiBase();
3645
+ const canCallApi = state.serverConnected || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || (apiBase && apiBase.length > 0);
3646
+
3647
+ if (canCallApi) {
3648
+ const res = await fetch(`${apiBase}/api/mcp/servers`, {
3649
+ method: 'POST',
3650
+ headers: { 'Content-Type': 'application/json' },
3651
+ body: JSON.stringify(payload)
3652
+ });
3653
+
3654
+ const data = await res.json();
3655
+ if (data.success) {
3656
+ showToast(`Connected to MCP server "${name}"! Discovered ${data.server?.tools?.length || 0} tools.`, 'success');
3657
+ closeAddMcpModal();
3658
+ getEl('form-add-mcp')?.reset();
3659
+ await loadMcpData();
3660
+ } else {
3661
+ showToast(data.error || 'Failed to add MCP server', 'error');
3662
+ }
3663
+ } else {
3664
+ // Standalone Cloud Web Mode fallback on scout.frontterrain.com
3665
+ const newServer = {
3666
+ id: `mcp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
3667
+ name,
3668
+ transport,
3669
+ endpoint: transport === 'http' ? endpoint : undefined,
3670
+ command: transport === 'stdio' ? command : undefined,
3671
+ args: transport === 'stdio' ? args : undefined,
3672
+ authType,
3673
+ headerKey: authType === 'apiKey' ? headerKey : undefined,
3674
+ status: transport === 'http' ? 'connected' : 'disconnected',
3675
+ userId,
3676
+ userEmail: userEmail || 'default',
3677
+ tools: [],
3678
+ createdAt: new Date().toISOString()
3679
+ };
3680
+ mcpState.servers.push(newServer);
3681
+ const storageKey = getAccountMcpStorageKey();
3682
+ localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
3683
+ localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
3684
+ showToast(`MCP server "${name}" saved for ${state.user?.email || 'your account'}.`, 'success');
3685
+ closeAddMcpModal();
3686
+ getEl('form-add-mcp')?.reset();
3687
+ renderMcpKpis();
3688
+ renderMcpServersList();
3689
+ }
3690
+ } catch (err) {
3691
+ showToast(err.message || 'Error connecting MCP server', 'error');
3692
+ } finally {
3693
+ if (submitBtn) {
3694
+ submitBtn.disabled = false;
3695
+ submitBtn.innerHTML = '<i class="fa-solid fa-plug"></i> Connect Server';
3696
+ }
3697
+ }
3698
+ }
3699
+
3700
+ function openInspectMcpModal(serverId) {
3701
+ const server = mcpState.servers.find(s => s.id === serverId);
3702
+ if (!server) {
3703
+ showToast('Server not found', 'error');
3704
+ return;
3705
+ }
3706
+
3707
+ mcpState.selectedServer = server;
3708
+ const titleEl = getEl('inspect-mcp-title');
3709
+ const subtitleEl = getEl('inspect-mcp-subtitle');
3710
+ const contentEl = getEl('inspect-mcp-content');
3711
+
3712
+ if (titleEl) titleEl.innerText = `${server.name} — Tools & Capabilities`;
3713
+ if (subtitleEl) subtitleEl.innerText = `${server.transport.toUpperCase()} Transport • ${server.tools.length} Tools Available`;
3714
+
3715
+ if (contentEl) {
3716
+ if (!server.tools || server.tools.length === 0) {
3717
+ contentEl.innerHTML = `
3718
+ <div style="padding: 32px; text-align: center; color: var(--scout-text-dim);">
3719
+ <i class="fa-solid fa-toolbox" style="font-size: 28px; margin-bottom: 12px; display: block; opacity: 0.5;"></i>
3720
+ No tools discovered yet. Ensure the MCP server is reachable and active.
3721
+ </div>
3722
+ `;
3723
+ } else {
3724
+ contentEl.innerHTML = server.tools.map(tool => {
3725
+ const isEnabled = tool.enabled !== false;
3726
+ const perm = (tool.permissionLevel || 'read').toLowerCase();
3727
+ const props = Object.keys(tool.inputSchema?.properties || {});
3728
+ const propsText = props.length > 0 ? `Parameters: ${props.join(', ')}` : 'No required parameters';
3729
+
3730
+ return `
3731
+ <div class="mcp-tool-item">
3732
+ <div class="mcp-tool-info">
3733
+ <div class="mcp-tool-title-row">
3734
+ <span class="mcp-tool-name">${escapeHtml(tool.originalName)}</span>
3735
+ <span class="mcp-perm-tag ${perm}">${perm}</span>
3736
+ </div>
3737
+ <p class="mcp-tool-desc">${escapeHtml(tool.description || 'No description provided.')}</p>
3738
+ <div class="mcp-tool-params">${escapeHtml(propsText)}</div>
3739
+ </div>
3740
+ <div>
3741
+ <label class="mcp-toggle-switch" title="${isEnabled ? 'Tool Enabled' : 'Tool Disabled'}">
3742
+ <input type="checkbox" ${isEnabled ? 'checked' : ''} onchange="toggleMcpTool('${escapeHtml(server.id)}', '${escapeHtml(tool.originalName)}', this.checked)">
3743
+ <span class="mcp-toggle-slider"></span>
3744
+ </label>
3745
+ </div>
3746
+ </div>
3747
+ `;
3748
+ }).join('');
3749
+ }
3750
+ }
3751
+
3752
+ const modal = getEl('modal-inspect-mcp');
3753
+ if (modal) modal.style.display = 'flex';
3754
+ }
3755
+
3756
+ function closeInspectMcpModal() {
3757
+ const modal = getEl('modal-inspect-mcp');
3758
+ if (modal) modal.style.display = 'none';
3759
+ mcpState.selectedServer = null;
3760
+ }
3761
+
3762
+ async function connectMcpServer(serverId) {
3763
+ try {
3764
+ showToast('Connecting to MCP server...', 'info');
3765
+ const res = await fetch(`${getMcpApiBase()}/api/mcp/servers/${encodeURIComponent(serverId)}/connect`, { method: 'POST' });
3766
+ const data = await res.json();
3767
+ if (data.success) {
3768
+ showToast(`Connected! Discovered ${data.server.tools?.length || 0} tools.`, 'success');
3769
+ await loadMcpData();
3770
+ } else {
3771
+ showToast(data.error || 'Failed to connect server', 'error');
3772
+ }
3773
+ } catch (err) {
3774
+ showToast(err.message || 'Error connecting to server', 'error');
3775
+ }
3776
+ }
3777
+
3778
+ async function disconnectMcpServer(serverId) {
3779
+ try {
3780
+ const res = await fetch(`${getMcpApiBase()}/api/mcp/servers/${encodeURIComponent(serverId)}/disconnect`, { method: 'POST' });
3781
+ const data = await res.json();
3782
+ if (data.success) {
3783
+ showToast('MCP server disconnected.', 'info');
3784
+ await loadMcpData();
3785
+ } else {
3786
+ showToast(data.error || 'Failed to disconnect server', 'error');
3787
+ }
3788
+ } catch (err) {
3789
+ showToast(err.message || 'Error disconnecting server', 'error');
3790
+ }
3791
+ }
3792
+
3793
+ async function deleteMcpServer(serverId) {
3794
+ if (!confirm('Are you sure you want to remove this MCP server? Associated credentials will be permanently deleted.')) {
3795
+ return;
3796
+ }
3797
+ try {
3798
+ const apiBase = getMcpApiBase();
3799
+ const canCallApi = state.serverConnected || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || (apiBase && apiBase.length > 0);
3800
+
3801
+ if (canCallApi) {
3802
+ const res = await fetch(`${apiBase}/api/mcp/servers/${encodeURIComponent(serverId)}`, { method: 'DELETE' });
3803
+ const data = await res.json();
3804
+ if (data.success) {
3805
+ showToast('MCP server removed.', 'info');
3806
+ } else {
3807
+ showToast(data.error || 'Failed to delete server', 'error');
3808
+ }
3809
+ } else {
3810
+ showToast('MCP server removed from your account.', 'info');
3811
+ }
3812
+
3813
+ mcpState.servers = mcpState.servers.filter(s => s.id !== serverId);
3814
+ const storageKey = getAccountMcpStorageKey();
3815
+ try {
3816
+ localStorage.setItem(storageKey, JSON.stringify(mcpState.servers));
3817
+ localStorage.setItem('scout_cached_mcp_servers', JSON.stringify(mcpState.servers));
3818
+ } catch { }
3819
+ renderMcpKpis();
3820
+ renderMcpServersList();
3821
+ } catch (err) {
3822
+ showToast(err.message || 'Error deleting server', 'error');
3823
+ }
3824
+ }
3825
+
3826
+ async function toggleMcpTool(serverId, toolName, enabled) {
3827
+ try {
3828
+ const res = await fetch(`${getMcpApiBase()}/api/mcp/servers/${encodeURIComponent(serverId)}/tools/${encodeURIComponent(toolName)}/toggle`, {
3829
+ method: 'POST',
3830
+ headers: { 'Content-Type': 'application/json' },
3831
+ body: JSON.stringify({ enabled })
3832
+ });
3833
+ const data = await res.json();
3834
+ if (data.success) {
3835
+ showToast(`Tool "${toolName}" ${enabled ? 'enabled' : 'disabled'}.`, 'info');
3836
+ const server = mcpState.servers.find(s => s.id === serverId);
3837
+ if (server && server.tools) {
3838
+ const target = server.tools.find(t => t.originalName === toolName || t.name === toolName);
3839
+ if (target) target.enabled = enabled;
3840
+ }
3841
+ renderMcpKpis();
3842
+ renderMcpServersList();
3843
+ } else {
3844
+ showToast(data.error || 'Failed to toggle tool', 'error');
3845
+ }
3846
+ } catch (err) {
3847
+ showToast(err.message || 'Error toggling tool', 'error');
3848
+ }
3849
+ }
3850
+
3851
+ // Auto-sync when window regains focus (e.g., returning from CLI terminal)
3852
+ window.addEventListener('focus', () => {
3853
+ loadMcpData();
3854
+ });
3855
+
3856
+ // Global exports for inline HTML event handlers
3857
+ window.loadMcpData = loadMcpData;
3858
+ window.openAddMcpModal = openAddMcpModal;
3859
+ window.closeAddMcpModal = closeAddMcpModal;
3860
+ window.toggleMcpTransportFields = toggleMcpTransportFields;
3861
+ window.toggleMcpAuthFields = toggleMcpAuthFields;
3862
+ window.handleAddMcpServerSubmit = handleAddMcpServerSubmit;
3863
+ window.openInspectMcpModal = openInspectMcpModal;
3864
+ window.closeInspectMcpModal = closeInspectMcpModal;
3865
+ window.connectMcpServer = connectMcpServer;
3866
+ window.disconnectMcpServer = disconnectMcpServer;
3867
+ window.deleteMcpServer = deleteMcpServer;
3868
+ window.toggleMcpTool = toggleMcpTool;
3869
+
3870
+
3871
+