apteva 0.4.32 → 0.4.44

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 (113) hide show
  1. package/dist/ActivityPage.c48n83h2.js +3 -0
  2. package/dist/ApiDocsPage.yzcxx5ax.js +4 -0
  3. package/dist/App.09yb8t0b.js +1 -0
  4. package/dist/App.152mbs1r.js +4 -0
  5. package/dist/App.3a67nx9w.js +4 -0
  6. package/dist/App.9epx6785.js +4 -0
  7. package/dist/App.d8955awp.js +4 -0
  8. package/dist/App.drwb57jq.js +4 -0
  9. package/dist/App.gssbmajb.js +4 -0
  10. package/dist/App.qw70pc29.js +53 -0
  11. package/dist/App.qzbx5wtj.js +4 -0
  12. package/dist/App.r5serxkt.js +8 -0
  13. package/dist/App.tpmp9020.js +20 -0
  14. package/dist/App.v2wb4d7d.js +61 -0
  15. package/dist/App.vxmaaj0m.js +13 -0
  16. package/dist/App.w4p2tda9.js +4 -0
  17. package/dist/App.wv2ng55q.js +221 -0
  18. package/dist/App.yncnrn0f.js +4 -0
  19. package/dist/ConnectionsPage.k6cspyqq.js +3 -0
  20. package/dist/McpPage.cdxm48xj.js +3 -0
  21. package/dist/SettingsPage.evpv7c2y.js +3 -0
  22. package/dist/SkillsPage.pvzp6c1a.js +3 -0
  23. package/dist/TasksPage.6jnvbpsy.js +3 -0
  24. package/dist/TelemetryPage.t7vk24zc.js +3 -0
  25. package/dist/TestsPage.5x6658aa.js +3 -0
  26. package/dist/ThreadsPage.3fvhtevh.js +3 -0
  27. package/dist/apteva-kit.css +1 -1
  28. package/dist/index.html +1 -1
  29. package/dist/styles.css +1 -1
  30. package/package.json +10 -9
  31. package/src/crypto.ts +4 -3
  32. package/src/db.ts +171 -36
  33. package/src/integrations/agentdojo.ts +95 -12
  34. package/src/integrations/index.ts +7 -0
  35. package/src/mcp-platform.ts +870 -142
  36. package/src/openapi.ts +96 -0
  37. package/src/providers.ts +60 -34
  38. package/src/routes/api/agent-utils.ts +59 -47
  39. package/src/routes/api/agents.ts +71 -2
  40. package/src/routes/api/integrations.ts +11 -5
  41. package/src/routes/api/mcp.ts +5 -4
  42. package/src/routes/api/meta-agent.ts +37 -1
  43. package/src/routes/api/projects.ts +3 -3
  44. package/src/routes/api/providers.ts +121 -30
  45. package/src/routes/api/skills.ts +2 -3
  46. package/src/routes/api/system.ts +98 -14
  47. package/src/routes/api/telemetry.ts +19 -1
  48. package/src/routes/share.ts +85 -0
  49. package/src/server.ts +43 -32
  50. package/src/triggers/agentdojo.ts +2 -2
  51. package/src/web/App.tsx +107 -21
  52. package/src/web/components/activity/ActivityPage.tsx +242 -389
  53. package/src/web/components/agents/AgentCard.tsx +19 -27
  54. package/src/web/components/agents/AgentPanel.tsx +358 -198
  55. package/src/web/components/agents/AgentsView.tsx +4 -4
  56. package/src/web/components/agents/CreateAgentModal.tsx +21 -79
  57. package/src/web/components/api/ApiDocsPage.tsx +66 -66
  58. package/src/web/components/auth/CreateAccountStep.tsx +16 -16
  59. package/src/web/components/auth/LoginPage.tsx +10 -10
  60. package/src/web/components/common/Icons.tsx +8 -0
  61. package/src/web/components/common/LoadingSpinner.tsx +2 -2
  62. package/src/web/components/common/Modal.tsx +8 -8
  63. package/src/web/components/common/Select.tsx +11 -10
  64. package/src/web/components/connections/ConnectionsPage.tsx +4 -4
  65. package/src/web/components/connections/IntegrationsTab.tsx +18 -18
  66. package/src/web/components/connections/OverviewTab.tsx +13 -13
  67. package/src/web/components/connections/TriggersTab.tsx +99 -99
  68. package/src/web/components/dashboard/Dashboard.tsx +177 -52
  69. package/src/web/components/index.ts +1 -1
  70. package/src/web/components/layout/Header.tsx +50 -34
  71. package/src/web/components/layout/Sidebar.tsx +41 -16
  72. package/src/web/components/mcp/IntegrationsPanel.tsx +160 -69
  73. package/src/web/components/mcp/McpPage.tsx +218 -209
  74. package/src/web/components/meta-agent/MetaAgent.tsx +15 -11
  75. package/src/web/components/onboarding/OnboardingWizard.tsx +25 -25
  76. package/src/web/components/settings/SettingsPage.tsx +389 -221
  77. package/src/web/components/skills/SkillsPage.tsx +88 -88
  78. package/src/web/components/tasks/TasksPage.tsx +385 -68
  79. package/src/web/components/telemetry/TelemetryPage.tsx +294 -39
  80. package/src/web/components/tests/TestsPage.tsx +50 -50
  81. package/src/web/components/threads/ThreadsPage.tsx +315 -0
  82. package/src/web/context/AuthContext.tsx +3 -3
  83. package/src/web/context/ProjectContext.tsx +8 -3
  84. package/src/web/context/TelemetryContext.tsx +24 -6
  85. package/src/web/context/ThemeContext.tsx +69 -0
  86. package/src/web/context/index.ts +3 -1
  87. package/src/web/styles.css +25 -7
  88. package/src/web/themes.ts +99 -0
  89. package/src/web/types.ts +4 -7
  90. package/dist/ActivityPage.41nbye4r.js +0 -3
  91. package/dist/ApiDocsPage.4smnt8m3.js +0 -4
  92. package/dist/App.0sbax9et.js +0 -4
  93. package/dist/App.0ws427h8.js +0 -4
  94. package/dist/App.6q6bar8b.js +0 -4
  95. package/dist/App.80301vdb.js +0 -4
  96. package/dist/App.af2wg84v.js +0 -267
  97. package/dist/App.ca1rz1ph.js +0 -4
  98. package/dist/App.ensa6z0r.js +0 -4
  99. package/dist/App.f8g7tych.js +0 -13
  100. package/dist/App.mvtqv6qc.js +0 -20
  101. package/dist/App.ncgc9cxy.js +0 -4
  102. package/dist/App.p02f4ret.js +0 -1
  103. package/dist/App.p0fb1pds.js +0 -4
  104. package/dist/App.pmaq48sj.js +0 -4
  105. package/dist/App.yv87t9m5.js +0 -4
  106. package/dist/App.zjmfm8p6.js +0 -4
  107. package/dist/ConnectionsPage.anb3rv9a.js +0 -3
  108. package/dist/McpPage.y396h6fy.js +0 -3
  109. package/dist/SettingsPage.p1hc60gk.js +0 -3
  110. package/dist/SkillsPage.yj3xdsay.js +0 -3
  111. package/dist/TasksPage.sjv0khtv.js +0 -3
  112. package/dist/TelemetryPage.2qm4w16r.js +0 -3
  113. package/dist/TestsPage.zzs4qfj8.js +0 -3
@@ -271,7 +271,7 @@ export function TestsPage() {
271
271
  running: "bg-blue-900/50 text-blue-400",
272
272
  };
273
273
  return (
274
- <span className={`px-2 py-0.5 rounded text-xs font-medium ${colors[status] || "bg-[#222] text-[#666]"}`}>
274
+ <span className={`px-2 py-0.5 rounded text-xs font-medium ${colors[status] || "bg-[var(--color-surface-raised)] text-[var(--color-text-muted)]"}`}>
275
275
  {status.toUpperCase()}
276
276
  </span>
277
277
  );
@@ -297,7 +297,7 @@ export function TestsPage() {
297
297
  <div className="flex items-center justify-between mb-6">
298
298
  <div>
299
299
  <h1 className="text-xl font-bold">Tests</h1>
300
- <p className="text-sm text-[#666] mt-1">
300
+ <p className="text-sm text-[var(--color-text-muted)] mt-1">
301
301
  Describe behavior, AI handles the rest
302
302
  </p>
303
303
  </div>
@@ -306,14 +306,14 @@ export function TestsPage() {
306
306
  <button
307
307
  onClick={handleRunAll}
308
308
  disabled={runningAll}
309
- className="px-4 py-2 bg-[#1a1a1a] hover:bg-[#222] text-[#e0e0e0] rounded text-sm font-medium transition disabled:opacity-50"
309
+ className="px-4 py-2 bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] text-[var(--color-text)] rounded text-sm font-medium transition disabled:opacity-50"
310
310
  >
311
311
  {runningAll ? "Running..." : "Run All"}
312
312
  </button>
313
313
  )}
314
314
  <button
315
315
  onClick={openCreate}
316
- className="px-4 py-2 bg-[#f97316] hover:bg-[#fb923c] text-white rounded text-sm font-medium transition"
316
+ className="px-4 py-2 bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-white rounded text-sm font-medium transition"
317
317
  >
318
318
  + New Test
319
319
  </button>
@@ -322,19 +322,19 @@ export function TestsPage() {
322
322
 
323
323
  {/* Test list */}
324
324
  {loading ? (
325
- <div className="text-[#666] text-sm">Loading...</div>
325
+ <div className="text-[var(--color-text-muted)] text-sm">Loading...</div>
326
326
  ) : tests.length === 0 ? (
327
327
  <div className="text-center py-16">
328
- <div className="text-[#333] text-4xl mb-4">
328
+ <div className="text-[var(--color-border-light)] text-4xl mb-4">
329
329
  <svg className="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
330
330
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
331
331
  </svg>
332
332
  </div>
333
- <p className="text-[#666] mb-2">No tests yet</p>
334
- <p className="text-xs text-[#555] mb-4">Describe what your agents should do and let AI verify it</p>
333
+ <p className="text-[var(--color-text-muted)] mb-2">No tests yet</p>
334
+ <p className="text-xs text-[var(--color-text-faint)] mb-4">Describe what your agents should do and let AI verify it</p>
335
335
  <button
336
336
  onClick={openCreate}
337
- className="px-4 py-2 bg-[#f97316] hover:bg-[#fb923c] text-white rounded text-sm font-medium transition"
337
+ className="px-4 py-2 bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-white rounded text-sm font-medium transition"
338
338
  >
339
339
  Create your first test
340
340
  </button>
@@ -342,7 +342,7 @@ export function TestsPage() {
342
342
  ) : (
343
343
  <div className="space-y-3">
344
344
  {tests.map(tc => (
345
- <div key={tc.id} className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4">
345
+ <div key={tc.id} className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg p-4">
346
346
  <div className="flex items-start justify-between">
347
347
  <div className="flex-1 min-w-0">
348
348
  <div className="flex items-center gap-2 mb-1">
@@ -352,18 +352,18 @@ export function TestsPage() {
352
352
  : tc.last_run && (<>
353
353
  {statusBadge(tc.last_run.status)}
354
354
  {tc.last_run.score != null && (
355
- <span className="text-xs text-[#888] font-mono">{tc.last_run.score}/10</span>
355
+ <span className="text-xs text-[var(--color-text-secondary)] font-mono">{tc.last_run.score}/10</span>
356
356
  )}
357
357
  </>)
358
358
  }
359
359
  </div>
360
360
  {tc.behavior && (
361
- <p className="text-xs text-[#888] mb-1.5 line-clamp-2">{tc.behavior}</p>
361
+ <p className="text-xs text-[var(--color-text-secondary)] mb-1.5 line-clamp-2">{tc.behavior}</p>
362
362
  )}
363
- <div className="text-xs text-[#666] space-y-0.5">
363
+ <div className="text-xs text-[var(--color-text-muted)] space-y-0.5">
364
364
  <div>
365
365
  Agent:{" "}
366
- <span className="text-[#888]">
366
+ <span className="text-[var(--color-text-secondary)]">
367
367
  {tc.agent_name || (tc.last_run?.selected_agent_name
368
368
  ? `${tc.last_run.selected_agent_name} (auto-selected)`
369
369
  : "Auto (AI picks)")}
@@ -371,18 +371,18 @@ export function TestsPage() {
371
371
  </div>
372
372
  {tc.last_run?.generated_message && (
373
373
  <div className="truncate">
374
- Message: <span className="text-[#888]">"{tc.last_run.generated_message}"</span>
374
+ Message: <span className="text-[var(--color-text-secondary)]">"{tc.last_run.generated_message}"</span>
375
375
  </div>
376
376
  )}
377
377
  {tc.input_message && !tc.last_run?.generated_message && (
378
378
  <div className="truncate">
379
- Message: <span className="text-[#888]">"{tc.input_message}"</span>
379
+ Message: <span className="text-[var(--color-text-secondary)]">"{tc.input_message}"</span>
380
380
  </div>
381
381
  )}
382
382
  {tc.last_run && (
383
383
  <div>
384
384
  Last run:{" "}
385
- <span className="text-[#888]">
385
+ <span className="text-[var(--color-text-secondary)]">
386
386
  {tc.last_run.duration_ms ? `${(tc.last_run.duration_ms / 1000).toFixed(1)}s` : "---"}
387
387
  {tc.last_run.judge_reasoning && ` --- "${tc.last_run.judge_reasoning.slice(0, 80)}${tc.last_run.judge_reasoning.length > 80 ? "..." : ""}"`}
388
388
  </span>
@@ -393,7 +393,7 @@ export function TestsPage() {
393
393
  <div className="flex items-center gap-1 ml-3 shrink-0">
394
394
  <button
395
395
  onClick={() => viewRuns(tc.id)}
396
- className="px-2 py-1 text-xs text-[#666] hover:text-[#888] hover:bg-[#1a1a1a] rounded transition"
396
+ className="px-2 py-1 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)] rounded transition"
397
397
  title="View run history"
398
398
  >
399
399
  History
@@ -401,19 +401,19 @@ export function TestsPage() {
401
401
  <button
402
402
  onClick={() => handleRun(tc.id)}
403
403
  disabled={runningTests.has(tc.id)}
404
- className="px-3 py-1 text-xs bg-[#1a1a1a] hover:bg-[#222] text-[#e0e0e0] rounded transition disabled:opacity-50"
404
+ className="px-3 py-1 text-xs bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] text-[var(--color-text)] rounded transition disabled:opacity-50"
405
405
  >
406
406
  {runningTests.has(tc.id) ? "Running..." : "Run"}
407
407
  </button>
408
408
  <button
409
409
  onClick={() => openEdit(tc)}
410
- className="px-2 py-1 text-xs text-[#666] hover:text-[#888] hover:bg-[#1a1a1a] rounded transition"
410
+ className="px-2 py-1 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)] rounded transition"
411
411
  >
412
412
  Edit
413
413
  </button>
414
414
  <button
415
415
  onClick={() => handleDelete(tc.id)}
416
- className="px-2 py-1 text-xs text-[#666] hover:text-red-400 hover:bg-[#1a1a1a] rounded transition"
416
+ className="px-2 py-1 text-xs text-[var(--color-text-muted)] hover:text-red-400 hover:bg-[var(--color-surface-raised)] rounded transition"
417
417
  >
418
418
  Delete
419
419
  </button>
@@ -428,50 +428,50 @@ export function TestsPage() {
428
428
  {selectedRuns && (
429
429
  <div className="mt-6">
430
430
  <div className="flex items-center justify-between mb-3">
431
- <h2 className="text-sm font-bold text-[#888]">Run History</h2>
431
+ <h2 className="text-sm font-bold text-[var(--color-text-secondary)]">Run History</h2>
432
432
  <button
433
433
  onClick={() => { setSelectedRuns(null); setExpandedRun(null); }}
434
- className="text-xs text-[#666] hover:text-[#888]"
434
+ className="text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
435
435
  >
436
436
  Close
437
437
  </button>
438
438
  </div>
439
439
  {selectedRuns.runs.length === 0 ? (
440
- <p className="text-sm text-[#666]">No runs yet</p>
440
+ <p className="text-sm text-[var(--color-text-muted)]">No runs yet</p>
441
441
  ) : (
442
442
  <div className="space-y-2">
443
443
  {selectedRuns.runs.map(run => (
444
- <div key={run.id} className="bg-[#0d0d0d] border border-[#1a1a1a] rounded p-3">
444
+ <div key={run.id} className="bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded p-3">
445
445
  <div
446
446
  className="flex items-center justify-between cursor-pointer"
447
447
  onClick={() => setExpandedRun(expandedRun === run.id ? null : run.id)}
448
448
  >
449
449
  <div className="flex items-center gap-3">
450
450
  {statusBadge(run.status)}
451
- <span className="text-xs text-[#666]">
451
+ <span className="text-xs text-[var(--color-text-muted)]">
452
452
  {run.duration_ms ? `${(run.duration_ms / 1000).toFixed(1)}s` : "---"}
453
453
  </span>
454
454
  {run.score != null && (
455
- <span className="text-xs text-[#888] font-mono">{run.score}/10</span>
455
+ <span className="text-xs text-[var(--color-text-secondary)] font-mono">{run.score}/10</span>
456
456
  )}
457
457
  {run.selected_agent_name && (
458
- <span className="text-xs text-[#555]">
458
+ <span className="text-xs text-[var(--color-text-faint)]">
459
459
  Agent: {run.selected_agent_name}
460
460
  </span>
461
461
  )}
462
- <span className="text-xs text-[#555]">
462
+ <span className="text-xs text-[var(--color-text-faint)]">
463
463
  {new Date(run.created_at).toLocaleString()}
464
464
  </span>
465
465
  </div>
466
- <span className="text-xs text-[#555]">{expandedRun === run.id ? "---" : "+"}</span>
466
+ <span className="text-xs text-[var(--color-text-faint)]">{expandedRun === run.id ? "---" : "+"}</span>
467
467
  </div>
468
468
  {expandedRun === run.id && (
469
469
  <div className="mt-3 space-y-2">
470
470
  {run.planner_reasoning && (
471
471
  <div>
472
- <div className="text-xs text-[#666] mb-1">Planner:</div>
473
- <div className="text-sm text-[#aaa] bg-[#0a0a0a] p-2 rounded">
474
- {run.selected_agent_name && <span className="text-[#f97316]">{run.selected_agent_name}</span>}
472
+ <div className="text-xs text-[var(--color-text-muted)] mb-1">Planner:</div>
473
+ <div className="text-sm text-[var(--color-text-secondary)] bg-[var(--color-bg)] p-2 rounded">
474
+ {run.selected_agent_name && <span className="text-[var(--color-accent)]">{run.selected_agent_name}</span>}
475
475
  {run.selected_agent_name && " --- "}
476
476
  {run.planner_reasoning}
477
477
  </div>
@@ -479,26 +479,26 @@ export function TestsPage() {
479
479
  )}
480
480
  {run.generated_message && (
481
481
  <div>
482
- <div className="text-xs text-[#666] mb-1">Generated Message:</div>
483
- <div className="text-sm text-[#aaa] bg-[#0a0a0a] p-2 rounded">"{run.generated_message}"</div>
482
+ <div className="text-xs text-[var(--color-text-muted)] mb-1">Generated Message:</div>
483
+ <div className="text-sm text-[var(--color-text-secondary)] bg-[var(--color-bg)] p-2 rounded">"{run.generated_message}"</div>
484
484
  </div>
485
485
  )}
486
486
  {run.judge_reasoning && (
487
487
  <div>
488
- <div className="text-xs text-[#666] mb-1">Judge:</div>
489
- <div className="text-sm text-[#aaa] bg-[#0a0a0a] p-2 rounded">{run.judge_reasoning}</div>
488
+ <div className="text-xs text-[var(--color-text-muted)] mb-1">Judge:</div>
489
+ <div className="text-sm text-[var(--color-text-secondary)] bg-[var(--color-bg)] p-2 rounded">{run.judge_reasoning}</div>
490
490
  </div>
491
491
  )}
492
492
  {run.error && (
493
493
  <div>
494
494
  <div className="text-xs text-red-400 mb-1">Error:</div>
495
- <div className="text-sm text-red-300 bg-[#0a0a0a] p-2 rounded">{run.error}</div>
495
+ <div className="text-sm text-red-300 bg-[var(--color-bg)] p-2 rounded">{run.error}</div>
496
496
  </div>
497
497
  )}
498
498
  {run.agent_response && (
499
499
  <div>
500
- <div className="text-xs text-[#666] mb-1">Agent Response (Thread):</div>
501
- <pre className="text-xs text-[#888] bg-[#0a0a0a] p-2 rounded overflow-auto max-h-64">
500
+ <div className="text-xs text-[var(--color-text-muted)] mb-1">Agent Response (Thread):</div>
501
+ <pre className="text-xs text-[var(--color-text-secondary)] bg-[var(--color-bg)] p-2 rounded overflow-auto max-h-64">
502
502
  {run.agent_response}
503
503
  </pre>
504
504
  </div>
@@ -515,34 +515,34 @@ export function TestsPage() {
515
515
  {/* Create/Edit Form Modal */}
516
516
  {showForm && (
517
517
  <div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center" onClick={() => setShowForm(false)}>
518
- <div className="bg-[#111] border border-[#1a1a1a] rounded-lg w-full max-w-lg mx-4 p-6" onClick={e => e.stopPropagation()}>
518
+ <div className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg w-full max-w-lg mx-4 p-6" onClick={e => e.stopPropagation()}>
519
519
  <h2 className="text-lg font-bold mb-4">{editingTest ? "Edit Test" : "New Test"}</h2>
520
520
 
521
521
  <div className="space-y-4">
522
522
  <div>
523
- <label className="block text-xs text-[#666] mb-1">Name</label>
523
+ <label className="block text-xs text-[var(--color-text-muted)] mb-1">Name</label>
524
524
  <input
525
525
  value={formName}
526
526
  onChange={e => setFormName(e.target.value)}
527
527
  placeholder="e.g. Social Media Posting"
528
- className="w-full bg-[#0a0a0a] border border-[#222] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#f97316]"
528
+ className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--color-accent)]"
529
529
  />
530
530
  </div>
531
531
 
532
532
  <div>
533
- <label className="block text-xs text-[#666] mb-1">Behavior</label>
533
+ <label className="block text-xs text-[var(--color-text-muted)] mb-1">Behavior</label>
534
534
  <textarea
535
535
  value={formBehavior}
536
536
  onChange={e => setFormBehavior(e.target.value)}
537
537
  placeholder="Describe what should happen, e.g. 'When asked to post on social media, the agent creates a proper post with relevant hashtags and confirms it was published'"
538
538
  rows={3}
539
- className="w-full bg-[#0a0a0a] border border-[#222] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#f97316] resize-none"
539
+ className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 text-sm focus:outline-none focus:border-[var(--color-accent)] resize-none"
540
540
  />
541
- <p className="text-xs text-[#555] mt-1">AI will generate the test message and evaluate results based on this</p>
541
+ <p className="text-xs text-[var(--color-text-faint)] mt-1">AI will generate the test message and evaluate results based on this</p>
542
542
  </div>
543
543
 
544
544
  <div>
545
- <label className="block text-xs text-[#666] mb-1">Agent</label>
545
+ <label className="block text-xs text-[var(--color-text-muted)] mb-1">Agent</label>
546
546
  <Select
547
547
  value={formAgentId}
548
548
  onChange={setFormAgentId}
@@ -552,21 +552,21 @@ export function TestsPage() {
552
552
  label: `${a.name} (${a.status})`,
553
553
  }))}
554
554
  />
555
- <p className="text-xs text-[#555] mt-1">Leave empty to let AI choose the right agent</p>
555
+ <p className="text-xs text-[var(--color-text-faint)] mt-1">Leave empty to let AI choose the right agent</p>
556
556
  </div>
557
557
  </div>
558
558
 
559
559
  <div className="flex justify-end gap-2 mt-6">
560
560
  <button
561
561
  onClick={() => setShowForm(false)}
562
- className="px-4 py-2 text-sm text-[#888] hover:text-[#e0e0e0] transition"
562
+ className="px-4 py-2 text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition"
563
563
  >
564
564
  Cancel
565
565
  </button>
566
566
  <button
567
567
  onClick={handleSave}
568
568
  disabled={!formName || !formBehavior}
569
- className="px-4 py-2 bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 text-white rounded text-sm font-medium transition"
569
+ className="px-4 py-2 bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 text-white rounded text-sm font-medium transition"
570
570
  >
571
571
  {editingTest ? "Save" : "Create"}
572
572
  </button>
@@ -0,0 +1,315 @@
1
+ import React, { useState, useEffect, useCallback, useMemo } from "react";
2
+ import { Chat, convertApiMessages } from "@apteva/apteva-kit";
3
+ import { useAgentActivity, useAuth, useProjects, useTelemetryContext, useTheme } from "../../context";
4
+ import type { TelemetryEvent } from "../../context";
5
+ import type { Agent, Route } from "../../types";
6
+
7
+ interface Thread {
8
+ id: string;
9
+ title?: string;
10
+ created_at: string;
11
+ updated_at: string;
12
+ message_count?: number;
13
+ agent_id: string;
14
+ agent_name: string;
15
+ }
16
+
17
+ interface ThreadsPageProps {
18
+ agents: Agent[];
19
+ onNavigate?: (route: Route) => void;
20
+ }
21
+
22
+ export function ThreadsPage({ agents, onNavigate }: ThreadsPageProps) {
23
+ const { theme } = useTheme();
24
+ const { authFetch } = useAuth();
25
+ const { currentProjectId } = useProjects();
26
+ const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
27
+
28
+ const [threads, setThreads] = useState<Thread[]>([]);
29
+ const [selectedThread, setSelectedThread] = useState<Thread | null>(null);
30
+ const [newChatAgent, setNewChatAgent] = useState<Agent | null>(null);
31
+ const [initialMessages, setInitialMessages] = useState<any[]>([]);
32
+ const [loadingThreads, setLoadingThreads] = useState(true);
33
+ const [loadingMessages, setLoadingMessages] = useState(false);
34
+ const [historicalActivities, setHistoricalActivities] = useState<TelemetryEvent[]>([]);
35
+ const [showAgentPicker, setShowAgentPicker] = useState(false);
36
+ const [newChatKey, setNewChatKey] = useState(0);
37
+
38
+ const filteredAgents = useMemo(() => {
39
+ if (currentProjectId === null) return agents;
40
+ if (currentProjectId === "unassigned") return agents.filter(a => !a.projectId);
41
+ return agents.filter(a => a.projectId === currentProjectId);
42
+ }, [agents, currentProjectId]);
43
+
44
+ const runningAgents = useMemo(() => filteredAgents.filter(a => a.status === "running"), [filteredAgents]);
45
+ const agentIds = useMemo(() => new Set(filteredAgents.map(a => a.id)), [filteredAgents]);
46
+
47
+ // Fetch consolidated threads
48
+ const fetchThreads = useCallback(async () => {
49
+ try {
50
+ const projectParam = currentProjectId ? `?project_id=${encodeURIComponent(currentProjectId)}` : "";
51
+ const [threadsRes, activityRes] = await Promise.all([
52
+ authFetch(`/api/threads${projectParam}`).catch(() => null),
53
+ authFetch(`/api/telemetry/events?type=thread_activity&limit=100${projectParam ? `&${projectParam}` : ""}`).catch(() => null),
54
+ ]);
55
+ if (threadsRes?.ok) {
56
+ const data = await threadsRes.json();
57
+ setThreads(data.threads || []);
58
+ }
59
+ if (activityRes?.ok) {
60
+ const data = await activityRes.json();
61
+ setHistoricalActivities(data.events || []);
62
+ }
63
+ } catch (e) {
64
+ console.error("Failed to fetch threads:", e);
65
+ } finally {
66
+ setLoadingThreads(false);
67
+ }
68
+ }, [authFetch, currentProjectId]);
69
+
70
+ useEffect(() => { fetchThreads(); }, [fetchThreads, statusChangeCounter]);
71
+
72
+ useEffect(() => {
73
+ const interval = setInterval(fetchThreads, 15000);
74
+ return () => clearInterval(interval);
75
+ }, [fetchThreads]);
76
+
77
+ // Open an existing thread
78
+ const openThread = useCallback(async (thread: Thread) => {
79
+ setNewChatAgent(null);
80
+ setLoadingMessages(true);
81
+ setSelectedThread(thread);
82
+ try {
83
+ const res = await authFetch(`/api/agents/${thread.agent_id}/threads/${thread.id}/messages`);
84
+ if (res.ok) {
85
+ const data = await res.json();
86
+ setInitialMessages(convertApiMessages(data.messages || []));
87
+ } else {
88
+ setInitialMessages([]);
89
+ }
90
+ } catch {
91
+ setInitialMessages([]);
92
+ }
93
+ setLoadingMessages(false);
94
+ }, [authFetch]);
95
+
96
+ // Start a new conversation with an agent
97
+ const startNewChat = (agent: Agent) => {
98
+ setSelectedThread(null);
99
+ setInitialMessages([]);
100
+ setNewChatAgent(agent);
101
+ setNewChatKey(k => k + 1);
102
+ setShowAgentPicker(false);
103
+ };
104
+
105
+ // Merge real-time + historical activity
106
+ const activities = useMemo(() => {
107
+ const realtimeThreadEvents = realtimeEvents.filter(e => e.type === "thread_activity" && !e.data?.parent_id);
108
+ const seen = new Set(realtimeThreadEvents.map(e => e.id));
109
+ const merged = [...realtimeThreadEvents];
110
+ for (const evt of historicalActivities) {
111
+ if (!seen.has(evt.id) && !evt.data?.parent_id) { merged.push(evt); seen.add(evt.id); }
112
+ }
113
+ return merged
114
+ .filter(e => agentIds.has(e.agent_id))
115
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
116
+ .slice(0, 100);
117
+ }, [realtimeEvents, historicalActivities, agentIds]);
118
+
119
+ // Group activities by thread_id
120
+ const activityByThread = useMemo(() => {
121
+ const map = new Map<string, TelemetryEvent[]>();
122
+ for (const evt of activities) {
123
+ const tid = evt.thread_id || evt.data?.thread_id as string;
124
+ if (tid) {
125
+ if (!map.has(tid)) map.set(tid, []);
126
+ map.get(tid)!.push(evt);
127
+ }
128
+ }
129
+ return map;
130
+ }, [activities]);
131
+
132
+ const runningCount = runningAgents.length;
133
+
134
+ // What's currently shown in chat
135
+ const chatAgentId = selectedThread?.agent_id || newChatAgent?.id;
136
+ const chatAgentName = selectedThread?.agent_name || newChatAgent?.name;
137
+ const chatThreadId = selectedThread?.id;
138
+ const chatKey = selectedThread
139
+ ? `${selectedThread.agent_id}-${selectedThread.id}`
140
+ : newChatAgent
141
+ ? `new-${newChatAgent.id}-${newChatKey}`
142
+ : null;
143
+
144
+ return (
145
+ <div className="flex-1 flex flex-col overflow-hidden">
146
+ {/* Header */}
147
+ <div className="px-6 pt-6 pb-4 shrink-0">
148
+ <div className="flex items-center justify-between">
149
+ <h1 className="text-xl font-semibold">Threads</h1>
150
+ <span className="text-sm text-[var(--color-text-muted)]">
151
+ {threads.length} threads from {runningCount} running agents
152
+ </span>
153
+ </div>
154
+ </div>
155
+
156
+ {/* Messenger layout: 1/4 threads | 3/4 chat */}
157
+ <div className="flex-1 flex min-h-0 overflow-hidden">
158
+ {/* Thread list — 1/4 */}
159
+ <div className="w-1/4 min-w-[260px] max-w-[360px] flex flex-col overflow-hidden">
160
+ {/* New conversation button */}
161
+ <div className="p-2 shrink-0">
162
+ <div className="relative">
163
+ <button
164
+ onClick={() => setShowAgentPicker(!showAgentPicker)}
165
+ disabled={runningAgents.length === 0}
166
+ className="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg bg-[var(--color-accent-10)] text-[var(--color-accent)] text-sm font-medium hover:bg-[var(--color-accent-20)] transition disabled:opacity-30 disabled:cursor-not-allowed"
167
+ >
168
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
169
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
170
+ </svg>
171
+ New conversation
172
+ </button>
173
+
174
+ {/* Agent picker dropdown */}
175
+ {showAgentPicker && (
176
+ <>
177
+ <div className="fixed inset-0 z-40" onClick={() => setShowAgentPicker(false)} />
178
+ <div className="absolute top-full left-0 right-0 mt-1 bg-[var(--color-surface)] border border-[var(--color-border-light)] rounded-lg shadow-xl z-50 max-h-60 overflow-auto">
179
+ {runningAgents.map(agent => (
180
+ <button
181
+ key={agent.id}
182
+ onClick={() => startNewChat(agent)}
183
+ className="w-full text-left px-3 py-2.5 hover:bg-[var(--color-surface-raised)] transition"
184
+ >
185
+ <p className="text-sm font-medium truncate">{agent.name}</p>
186
+ <p className="text-[10px] text-[var(--color-text-faint)]">{agent.provider} · {agent.model}</p>
187
+ </button>
188
+ ))}
189
+ </div>
190
+ </>
191
+ )}
192
+ </div>
193
+ </div>
194
+
195
+ <div className="flex-1 overflow-auto px-2 pb-2">
196
+ {loadingThreads ? (
197
+ <div className="p-6 text-center text-[var(--color-text-faint)] text-sm">Loading threads...</div>
198
+ ) : threads.length === 0 ? (
199
+ <div className="p-6 text-center text-[var(--color-text-faint)] text-sm">
200
+ <p>No threads yet</p>
201
+ <p className="mt-1 text-[var(--color-text-faint)]">Start a conversation or wait for agents</p>
202
+ </div>
203
+ ) : (
204
+ <div className="space-y-0.5">
205
+ {threads.map(thread => (
206
+ <ThreadRow
207
+ key={`${thread.agent_id}-${thread.id}`}
208
+ thread={thread}
209
+ selected={selectedThread?.id === thread.id && selectedThread?.agent_id === thread.agent_id}
210
+ activities={activityByThread.get(thread.id) || []}
211
+ onSelect={() => openThread(thread)}
212
+ />
213
+ ))}
214
+ </div>
215
+ )}
216
+ </div>
217
+ </div>
218
+
219
+ {/* Chat — 3/4 */}
220
+ <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
221
+ {chatAgentId && chatKey ? (
222
+ loadingMessages ? (
223
+ <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">Loading messages...</div>
224
+ ) : (
225
+ <Chat
226
+ key={chatKey}
227
+ agentId="default"
228
+ apiUrl={`/api/agents/${chatAgentId}`}
229
+ threadId={chatThreadId}
230
+ initialMessages={initialMessages}
231
+ placeholder={`Message ${chatAgentName}...`}
232
+ headerTitle={chatAgentName}
233
+ variant="terminal"
234
+ theme={theme.id as "light" | "dark"}
235
+ showHeader={true}
236
+ />
237
+ )
238
+ ) : (
239
+ <div className="flex-1 flex items-center justify-center">
240
+ <div className="text-center text-[var(--color-text-faint)]">
241
+ <svg className="w-12 h-12 mx-auto mb-3 text-[var(--color-border-light)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
242
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
243
+ </svg>
244
+ <p className="text-sm">Select a thread or start a new conversation</p>
245
+ <p className="text-xs text-[var(--color-text-faint)] mt-1">Chat with any running agent</p>
246
+ </div>
247
+ </div>
248
+ )}
249
+ </div>
250
+ </div>
251
+ </div>
252
+ );
253
+ }
254
+
255
+ // --- Thread Row ---
256
+
257
+ function ThreadRow({ thread, selected, activities, onSelect }: {
258
+ thread: Thread;
259
+ selected: boolean;
260
+ activities: TelemetryEvent[];
261
+ onSelect: () => void;
262
+ }) {
263
+ const { isActive } = useAgentActivity(thread.agent_id);
264
+ const latestActivity = activities[0];
265
+ const activityText = latestActivity?.data?.activity as string | undefined;
266
+
267
+ return (
268
+ <button
269
+ onClick={onSelect}
270
+ className={`w-full text-left px-3 py-2.5 rounded-lg transition ${
271
+ selected
272
+ ? "bg-[var(--color-accent-10)]"
273
+ : "hover:bg-[var(--color-bg-secondary)]"
274
+ }`}
275
+ >
276
+ <div className="flex items-center justify-between gap-2 mb-1">
277
+ <span className="text-sm font-medium truncate">
278
+ {thread.title || `Thread ${thread.id.slice(0, 8)}`}
279
+ </span>
280
+ <span className="text-[10px] text-[var(--color-text-faint)] shrink-0">{timeAgo(thread.updated_at)}</span>
281
+ </div>
282
+ <div className="flex items-center gap-1.5">
283
+ <span
284
+ className={`w-1.5 h-1.5 rounded-full shrink-0 ${
285
+ isActive ? "bg-green-400 animate-pulse" : "bg-[var(--color-scrollbar)]"
286
+ }`}
287
+ />
288
+ <span className="text-[11px] text-[var(--color-accent)]">{thread.agent_name}</span>
289
+ {thread.message_count != null && (
290
+ <>
291
+ <span className="text-[var(--color-border-light)]">&middot;</span>
292
+ <span className="text-[10px] text-[var(--color-text-faint)]">{thread.message_count} msgs</span>
293
+ </>
294
+ )}
295
+ </div>
296
+ {activityText && (
297
+ <p className="text-[11px] text-[var(--color-text-faint)] truncate mt-1">{activityText}</p>
298
+ )}
299
+ </button>
300
+ );
301
+ }
302
+
303
+ // --- Helpers ---
304
+
305
+ function timeAgo(timestamp: string): string {
306
+ const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
307
+ if (seconds < 5) return "just now";
308
+ if (seconds < 60) return `${seconds}s ago`;
309
+ const minutes = Math.floor(seconds / 60);
310
+ if (minutes < 60) return `${minutes}m ago`;
311
+ const hours = Math.floor(minutes / 60);
312
+ if (hours < 24) return `${hours}h ago`;
313
+ const days = Math.floor(hours / 24);
314
+ return `${days}d ago`;
315
+ }
@@ -1,4 +1,4 @@
1
- import React, { createContext, useContext, useState, useEffect, useCallback, useRef, type ReactNode } from "react";
1
+ import React, { createContext, useContext, useState, useEffect, useCallback, useMemo, useRef, type ReactNode } from "react";
2
2
 
3
3
  interface User {
4
4
  id: string;
@@ -216,7 +216,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
216
216
  return () => clearInterval(refreshInterval);
217
217
  }, [accessToken, refreshTokenInternal]);
218
218
 
219
- const value: AuthContextValue = {
219
+ const value = useMemo<AuthContextValue>(() => ({
220
220
  user,
221
221
  isAuthenticated: !!user,
222
222
  isLoading,
@@ -230,7 +230,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
230
230
  refreshToken,
231
231
  checkAuth,
232
232
  authFetch,
233
- };
233
+ }), [user, isLoading, hasUsers, isDev, accessToken, onboardingComplete, setOnboardingComplete, login, logout, refreshToken, checkAuth, authFetch]);
234
234
 
235
235
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
236
236
  }