apteva 0.4.44 → 0.4.48

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 (83) hide show
  1. package/dist/{ActivityPage.c48n83h2.js → ActivityPage.sw9p594m.js} +1 -1
  2. package/dist/{ApiDocsPage.yzcxx5ax.js → ApiDocsPage.90e03bz7.js} +1 -1
  3. package/dist/App.0ws87fpx.js +53 -0
  4. package/dist/App.3vnrera5.js +4 -0
  5. package/dist/App.94x6mh7f.js +20 -0
  6. package/dist/{App.qzbx5wtj.js → App.9sryp183.js} +1 -1
  7. package/dist/App.d9tny4t0.js +221 -0
  8. package/dist/{App.r5serxkt.js → App.jhb45d7r.js} +1 -1
  9. package/dist/App.p7jjw1zf.js +4 -0
  10. package/dist/App.pfbdzrhh.js +4 -0
  11. package/dist/App.stgng5bx.js +13 -0
  12. package/dist/{App.152mbs1r.js → App.tm3k7h4b.js} +1 -1
  13. package/dist/App.vkg121c6.js +4 -0
  14. package/dist/App.wghtdzsk.js +1 -0
  15. package/dist/App.xf7wsckg.js +4 -0
  16. package/dist/App.xva0tfzh.js +4 -0
  17. package/dist/App.ysxy7akk.js +61 -0
  18. package/dist/App.yzkh4gq2.js +4 -0
  19. package/dist/ConnectionsPage.q5f9fd37.js +3 -0
  20. package/dist/McpPage.f3ccrezb.js +3 -0
  21. package/dist/SettingsPage.3sqx6wm4.js +3 -0
  22. package/dist/SkillsPage.whxnez67.js +3 -0
  23. package/dist/TasksPage.zp4jfevw.js +3 -0
  24. package/dist/TelemetryPage.a9fmxq87.js +3 -0
  25. package/dist/TestsPage.18krj0d1.js +3 -0
  26. package/dist/ThreadsPage.nnphgy98.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 +5 -4
  31. package/src/db.ts +42 -4
  32. package/src/providers.ts +14 -9
  33. package/src/routes/api/agent-utils.ts +25 -3
  34. package/src/routes/api/telemetry.ts +20 -2
  35. package/src/server.ts +52 -1
  36. package/src/web/App.tsx +1 -1
  37. package/src/web/components/agents/AgentCard.tsx +9 -7
  38. package/src/web/components/agents/AgentPanel.tsx +205 -44
  39. package/src/web/components/agents/CreateAgentModal.tsx +5 -5
  40. package/src/web/components/auth/LoginPage.tsx +2 -2
  41. package/src/web/components/common/LoadingSpinner.tsx +1 -1
  42. package/src/web/components/common/Modal.tsx +6 -6
  43. package/src/web/components/common/Select.tsx +2 -2
  44. package/src/web/components/connections/ConnectionsPage.tsx +1 -1
  45. package/src/web/components/connections/IntegrationsTab.tsx +3 -3
  46. package/src/web/components/connections/OverviewTab.tsx +3 -3
  47. package/src/web/components/connections/TriggersTab.tsx +8 -8
  48. package/src/web/components/dashboard/Dashboard.tsx +2 -2
  49. package/src/web/components/layout/Header.tsx +3 -3
  50. package/src/web/components/layout/Sidebar.tsx +3 -2
  51. package/src/web/components/mcp/McpPage.tsx +13 -13
  52. package/src/web/components/onboarding/OnboardingWizard.tsx +2 -2
  53. package/src/web/components/settings/SettingsPage.tsx +55 -22
  54. package/src/web/components/skills/SkillsPage.tsx +7 -7
  55. package/src/web/components/tasks/TasksPage.tsx +212 -36
  56. package/src/web/components/telemetry/TelemetryPage.tsx +278 -9
  57. package/src/web/components/tests/TestsPage.tsx +2 -2
  58. package/src/web/components/threads/ThreadsPage.tsx +2 -2
  59. package/src/web/context/ThemeContext.tsx +31 -10
  60. package/src/web/index.html +1 -6
  61. package/src/web/styles.css +47 -0
  62. package/src/web/themes.ts +68 -5
  63. package/dist/App.09yb8t0b.js +0 -1
  64. package/dist/App.3a67nx9w.js +0 -4
  65. package/dist/App.9epx6785.js +0 -4
  66. package/dist/App.d8955awp.js +0 -4
  67. package/dist/App.drwb57jq.js +0 -4
  68. package/dist/App.gssbmajb.js +0 -4
  69. package/dist/App.qw70pc29.js +0 -53
  70. package/dist/App.tpmp9020.js +0 -20
  71. package/dist/App.v2wb4d7d.js +0 -61
  72. package/dist/App.vxmaaj0m.js +0 -13
  73. package/dist/App.w4p2tda9.js +0 -4
  74. package/dist/App.wv2ng55q.js +0 -221
  75. package/dist/App.yncnrn0f.js +0 -4
  76. package/dist/ConnectionsPage.k6cspyqq.js +0 -3
  77. package/dist/McpPage.cdxm48xj.js +0 -3
  78. package/dist/SettingsPage.evpv7c2y.js +0 -3
  79. package/dist/SkillsPage.pvzp6c1a.js +0 -3
  80. package/dist/TasksPage.6jnvbpsy.js +0 -3
  81. package/dist/TelemetryPage.t7vk24zc.js +0 -3
  82. package/dist/TestsPage.5x6658aa.js +0 -3
  83. package/dist/ThreadsPage.3fvhtevh.js +0 -3
package/src/server.ts CHANGED
@@ -357,7 +357,7 @@ const server = Bun.serve({
357
357
  development: false, // Suppress "Started server" message
358
358
  idleTimeout: 255, // Max value - prevents SSE connections from timing out
359
359
 
360
- async fetch(req: Request): Promise<Response> {
360
+ async fetch(req: Request, bunServer: Server): Promise<Response | undefined> {
361
361
  const url = new URL(req.url);
362
362
  const path = url.pathname;
363
363
 
@@ -367,6 +367,19 @@ const server = Bun.serve({
367
367
  console.log(`[${req.method}] ${path}${params}`);
368
368
  }
369
369
 
370
+ // WebSocket upgrade: /api/agents/:id/voice → proxy to agent binary
371
+ const voiceMatch = path.match(/^\/api\/agents\/([^/]+)\/voice$/);
372
+ if (voiceMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
373
+ const agentId = voiceMatch[1];
374
+ const agent = AgentDB.findById(agentId);
375
+ if (!agent || agent.status !== "running" || !agent.port) {
376
+ return new Response("Agent not available", { status: 400 });
377
+ }
378
+ const upgraded = bunServer.upgrade(req, { data: { agentId: agent.id, agentPort: agent.port } });
379
+ if (upgraded) return undefined;
380
+ return new Response("WebSocket upgrade failed", { status: 500 });
381
+ }
382
+
370
383
  // CORS headers - configurable origins
371
384
  const origin = req.headers.get("Origin") || "";
372
385
  const allowedOrigins = process.env.CORS_ORIGINS?.split(",") || [];
@@ -436,6 +449,44 @@ const server = Bun.serve({
436
449
  // Serve static files (React app)
437
450
  return serveStatic(req, path);
438
451
  },
452
+
453
+ // WebSocket proxy for agent voice/realtime
454
+ websocket: {
455
+ open(ws: any) {
456
+ const { agentId, agentPort } = ws.data;
457
+ const agentWs = new WebSocket(`ws://localhost:${agentPort}/voice`);
458
+
459
+ agentWs.onopen = () => {
460
+ console.log(`[WS] Voice proxy connected: agent=${agentId} port=${agentPort}`);
461
+ };
462
+ agentWs.onmessage = (event: MessageEvent) => {
463
+ try { ws.send(event.data); } catch {}
464
+ };
465
+ agentWs.onclose = (event: CloseEvent) => {
466
+ console.log(`[WS] Agent disconnected: agent=${agentId} code=${event.code}`);
467
+ ws.close(event.code, event.reason);
468
+ };
469
+ agentWs.onerror = () => {
470
+ ws.close(1011, "Agent WebSocket error");
471
+ };
472
+
473
+ // Store agent WS on the client WS for message/close handlers
474
+ ws.data.agentWs = agentWs;
475
+ },
476
+ message(ws: any, message: string | Buffer) {
477
+ const agentWs = ws.data.agentWs as WebSocket;
478
+ if (agentWs?.readyState === WebSocket.OPEN) {
479
+ agentWs.send(message);
480
+ }
481
+ },
482
+ close(ws: any, code: number, reason: string) {
483
+ const agentWs = ws.data.agentWs as WebSocket;
484
+ if (agentWs && agentWs.readyState !== WebSocket.CLOSED) {
485
+ agentWs.close(code, reason);
486
+ }
487
+ console.log(`[WS] Client disconnected: agent=${ws.data.agentId} code=${code}`);
488
+ },
489
+ },
439
490
  });
440
491
 
441
492
  const serverUrl = `http://localhost:${PORT}`;
package/src/web/App.tsx CHANGED
@@ -241,7 +241,7 @@ function AppContent() {
241
241
  }
242
242
 
243
243
  return (
244
- <div className="h-screen font-mono flex flex-col overflow-hidden" style={{ backgroundColor: "var(--color-bg)", color: "var(--color-text)" }}>
244
+ <div className="h-screen flex flex-col overflow-hidden" style={{ backgroundColor: "var(--color-bg)", color: "var(--color-text)" }}>
245
245
  <Header onMenuClick={() => setMobileMenuOpen(true)} agents={agents} />
246
246
 
247
247
  {startError && (
@@ -34,10 +34,10 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
34
34
  return (
35
35
  <div
36
36
  onClick={onSelect}
37
- className={`bg-[var(--color-surface)] rounded p-5 border transition cursor-pointer flex flex-col h-full ${
37
+ className={`bg-[var(--color-surface)] card p-5 transition cursor-pointer flex flex-col h-full ${
38
38
  selected
39
- ? 'border-[var(--color-accent)]'
40
- : 'border-[var(--color-border)] hover:border-[var(--color-border-light)]'
39
+ ? '!border-[var(--color-accent)]'
40
+ : 'hover:border-[var(--color-border-light)]'
41
41
  }`}
42
42
  >
43
43
  <div className="flex items-start justify-between mb-3">
@@ -62,7 +62,8 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
62
62
  {enabledFeatures.map(({ key, icon: Icon, label }) => (
63
63
  <span
64
64
  key={key}
65
- className="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-[var(--color-accent-10)] text-[var(--color-accent-70)] text-xs"
65
+ className="inline-flex items-center gap-1 px-2 py-0.5 bg-[var(--color-accent-10)] text-[var(--color-accent-70)] text-xs"
66
+ style={{ borderRadius: "var(--radius-badge)" }}
66
67
  title={label}
67
68
  >
68
69
  <Icon className="w-3 h-3" />
@@ -143,7 +144,7 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
143
144
  <button
144
145
  onClick={onToggle}
145
146
  disabled={agent.status === "starting" || agent.status === "stopping"}
146
- className={`w-full px-3 py-1.5 rounded text-sm font-medium transition mt-auto ${
147
+ className={`w-full px-3 py-1.5 btn text-sm font-medium transition mt-auto ${
147
148
  agent.status === "starting" || agent.status === "stopping"
148
149
  ? "bg-[var(--color-surface-raised)] text-[var(--color-text-muted)] cursor-wait"
149
150
  : agent.status === "running"
@@ -160,7 +161,7 @@ export const AgentCard = React.memo(function AgentCard({ agent, selected, onSele
160
161
  function StatusBadge({ status, isActive, activityLabel }: { status: Agent["status"]; isActive?: boolean; activityLabel?: string }) {
161
162
  if (status === "running" && isActive && activityLabel) {
162
163
  return (
163
- <span className="px-2 py-1 rounded text-xs font-medium bg-green-500/20 text-green-400 animate-pulse">
164
+ <span className="px-2 py-1 text-xs font-medium bg-green-500/20 text-green-400 animate-pulse" style={{ borderRadius: "var(--radius-badge)" }}>
164
165
  {activityLabel}
165
166
  </span>
166
167
  );
@@ -170,13 +171,14 @@ function StatusBadge({ status, isActive, activityLabel }: { status: Agent["statu
170
171
 
171
172
  return (
172
173
  <span
173
- className={`px-2 py-1 rounded text-xs font-medium ${
174
+ className={`px-2 py-1 text-xs font-medium ${
174
175
  isTransitioning
175
176
  ? "bg-yellow-500/20 text-yellow-400 animate-pulse"
176
177
  : status === "running"
177
178
  ? "bg-[#3b82f6]/20 text-[#3b82f6]"
178
179
  : "bg-[var(--color-surface-raised)] text-[var(--color-text-muted)]"
179
180
  }`}
181
+ style={{ borderRadius: "var(--radius-badge)" }}
180
182
  >
181
183
  {status}
182
184
  </span>
@@ -123,6 +123,7 @@ function ChatTab({ agent, onStartAgent }: { agent: Agent; onStartAgent: (e?: Rea
123
123
  variant="terminal"
124
124
  theme={theme.id as "light" | "dark"}
125
125
  headerTitle={agent.name}
126
+ enableVoice={!!agent.features.realtime}
126
127
  />
127
128
  );
128
129
  }
@@ -328,6 +329,9 @@ function TasksTab({ agent }: { agent: Agent }) {
328
329
  const [showCreateForm, setShowCreateForm] = useState(false);
329
330
  const [executing, setExecuting] = useState(false);
330
331
  const [deleting, setDeleting] = useState(false);
332
+ const [editing, setEditing] = useState(false);
333
+ const [saving, setSaving] = useState(false);
334
+ const [editForm, setEditForm] = useState({ title: "", description: "", type: "once" as "once" | "recurring", priority: 5, execute_at: "", recurrence: "" });
331
335
  const { confirm, ConfirmDialog } = useConfirm();
332
336
  const { events } = useTelemetry({ agent_id: agent.id, category: "task" });
333
337
 
@@ -428,6 +432,51 @@ function TasksTab({ agent }: { agent: Agent }) {
428
432
  }
429
433
  };
430
434
 
435
+ const startEditing = (task: Task) => {
436
+ setEditForm({
437
+ title: task.title,
438
+ description: task.description || "",
439
+ type: task.type as "once" | "recurring",
440
+ priority: task.priority,
441
+ execute_at: task.execute_at ? new Date(task.execute_at).toISOString().slice(0, 16) : "",
442
+ recurrence: task.recurrence || "",
443
+ });
444
+ setEditing(true);
445
+ };
446
+
447
+ const handleUpdateTask = async () => {
448
+ if (!selectedTask || saving) return;
449
+ setSaving(true);
450
+ try {
451
+ const body: Record<string, unknown> = {
452
+ title: editForm.title.trim(),
453
+ description: editForm.description.trim() || undefined,
454
+ type: editForm.type,
455
+ priority: editForm.priority,
456
+ };
457
+ if (editForm.type === "once" && editForm.execute_at) {
458
+ body.execute_at = new Date(editForm.execute_at).toISOString();
459
+ }
460
+ if (editForm.type === "recurring" && editForm.recurrence.trim()) {
461
+ body.recurrence = editForm.recurrence.trim();
462
+ }
463
+ const res = await authFetch(`/api/tasks/${agent.id}/${selectedTask.id}`, {
464
+ method: "PUT",
465
+ headers: { "Content-Type": "application/json" },
466
+ body: JSON.stringify(body),
467
+ });
468
+ if (res.ok) {
469
+ setEditing(false);
470
+ setSelectedTask(null);
471
+ fetchTasks();
472
+ }
473
+ } catch (e) {
474
+ console.error("Failed to update task:", e);
475
+ } finally {
476
+ setSaving(false);
477
+ }
478
+ };
479
+
431
480
  // Refetch when agent changes, filter changes, or task telemetry arrives
432
481
  useEffect(() => {
433
482
  setLoading(true);
@@ -493,73 +542,182 @@ function TasksTab({ agent }: { agent: Agent }) {
493
542
  {/* Back button + actions */}
494
543
  <div className="px-4 pt-3 pb-2 border-b border-[var(--color-border)] shrink-0 flex items-center justify-between">
495
544
  <button
496
- onClick={() => setSelectedTask(null)}
545
+ onClick={() => { setSelectedTask(null); setEditing(false); }}
497
546
  className="text-sm text-[var(--color-text-muted)] hover:text-[var(--color-text)] transition flex items-center gap-1"
498
547
  >
499
- <span>←</span> Back to tasks
548
+ <span>←</span> {editing ? "Cancel" : "Back to tasks"}
500
549
  </button>
501
550
  <div className="flex items-center gap-2">
502
- {(selectedTask.status === "pending" || selectedTask.status === "completed") && (
503
- <button
504
- onClick={handleExecuteTask}
505
- disabled={executing}
506
- title="Execute now"
507
- className="text-[var(--color-accent)] hover:opacity-80 transition disabled:opacity-50"
508
- >
509
- <svg className="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
510
- </button>
551
+ {editing ? (
552
+ <>
553
+ <button
554
+ onClick={() => setEditing(false)}
555
+ className="text-[var(--color-text-muted)] hover:text-[var(--color-text)] text-sm transition"
556
+ >
557
+ Cancel
558
+ </button>
559
+ <button
560
+ onClick={handleUpdateTask}
561
+ disabled={saving || !editForm.title.trim()}
562
+ className="px-3 py-1 rounded text-sm bg-[var(--color-accent)] text-black hover:opacity-90 transition disabled:opacity-50"
563
+ >
564
+ {saving ? "Saving..." : "Save"}
565
+ </button>
566
+ </>
567
+ ) : (
568
+ <>
569
+ {(selectedTask.status === "pending" || selectedTask.status === "completed" || selectedTask.status === "failed") && (
570
+ <button
571
+ onClick={() => startEditing(selectedTask)}
572
+ title="Edit task"
573
+ className="text-[var(--color-text-muted)] hover:text-[var(--color-accent)] transition"
574
+ >
575
+ <svg className="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
576
+ </button>
577
+ )}
578
+ {(selectedTask.status === "pending" || selectedTask.status === "completed") && (
579
+ <button
580
+ onClick={handleExecuteTask}
581
+ disabled={executing}
582
+ title="Execute now"
583
+ className="text-[var(--color-accent)] hover:opacity-80 transition disabled:opacity-50"
584
+ >
585
+ <svg className="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
586
+ </button>
587
+ )}
588
+ <button
589
+ onClick={handleDeleteTask}
590
+ disabled={deleting}
591
+ title="Delete task"
592
+ className="text-red-400 hover:text-red-300 transition disabled:opacity-50"
593
+ >
594
+ <svg className="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
595
+ </button>
596
+ </>
511
597
  )}
512
- <button
513
- onClick={handleDeleteTask}
514
- disabled={deleting}
515
- title="Delete task"
516
- className="text-red-400 hover:text-red-300 transition disabled:opacity-50"
517
- >
518
- <svg className="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
519
- </button>
520
598
  </div>
521
599
  </div>
522
600
 
523
601
  {/* Task detail content */}
524
602
  <div className="flex-1 overflow-auto p-4 space-y-4">
603
+ {(() => {
604
+ const inputClass = "w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-2 py-1.5 text-sm focus:outline-none focus:border-[var(--color-accent)] text-[var(--color-text)]";
605
+ return <>
525
606
  {/* Title & Status */}
526
607
  <div>
527
608
  <div className="flex items-start justify-between gap-2 mb-1">
528
- <h3 className="text-lg font-medium">{selectedTask.title}</h3>
529
- <span className={`px-2 py-1 rounded text-xs font-medium flex-shrink-0 ${statusColors[selectedTask.status]}`}>
530
- {selectedTask.status}
531
- </span>
609
+ {editing ? (
610
+ <input
611
+ type="text"
612
+ value={editForm.title}
613
+ onChange={e => setEditForm({ ...editForm, title: e.target.value })}
614
+ className={`${inputClass} text-lg font-medium`}
615
+ placeholder="Task title"
616
+ />
617
+ ) : (
618
+ <h3 className="text-lg font-medium">{selectedTask.title}</h3>
619
+ )}
620
+ {!editing && (
621
+ <span className={`px-2 py-1 rounded text-xs font-medium flex-shrink-0 ${statusColors[selectedTask.status]}`}>
622
+ {selectedTask.status}
623
+ </span>
624
+ )}
532
625
  </div>
533
626
  </div>
534
627
 
535
628
  {/* Description */}
536
- {selectedTask.description && (
629
+ {editing ? (
630
+ <div>
631
+ <h4 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1">Description</h4>
632
+ <textarea
633
+ value={editForm.description}
634
+ onChange={e => setEditForm({ ...editForm, description: e.target.value })}
635
+ className={`${inputClass} resize-none`}
636
+ rows={3}
637
+ placeholder="Task description..."
638
+ />
639
+ </div>
640
+ ) : selectedTask.description ? (
537
641
  <div>
538
642
  <h4 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1">Description</h4>
539
643
  <p className="text-sm text-[var(--color-text-secondary)] whitespace-pre-wrap">{selectedTask.description}</p>
540
644
  </div>
541
- )}
645
+ ) : null}
542
646
 
543
647
  {/* Metadata */}
544
- <div className="grid grid-cols-2 gap-3 text-sm">
648
+ {editing ? (
649
+ <div className="grid grid-cols-2 gap-3">
650
+ <div>
651
+ <label className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1 block">Type</label>
652
+ <select
653
+ value={editForm.type}
654
+ onChange={e => setEditForm({ ...editForm, type: e.target.value as "once" | "recurring" })}
655
+ className={inputClass}
656
+ >
657
+ <option value="once">One-time</option>
658
+ <option value="recurring">Recurring</option>
659
+ </select>
660
+ </div>
661
+ <div>
662
+ <label className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1 block">Priority</label>
663
+ <input
664
+ type="number"
665
+ min={1}
666
+ max={10}
667
+ value={editForm.priority}
668
+ onChange={e => setEditForm({ ...editForm, priority: Number(e.target.value) })}
669
+ className={inputClass}
670
+ />
671
+ </div>
672
+ </div>
673
+ ) : (
674
+ <div className="grid grid-cols-2 gap-3 text-sm">
675
+ <div>
676
+ <span className="text-[var(--color-text-muted)]">Type</span>
677
+ <p className="capitalize">{selectedTask.type}</p>
678
+ </div>
679
+ <div>
680
+ <span className="text-[var(--color-text-muted)]">Priority</span>
681
+ <p>{selectedTask.priority}</p>
682
+ </div>
683
+ {selectedTask.recurrence && (
684
+ <div>
685
+ <span className="text-[var(--color-text-muted)]">Recurrence</span>
686
+ <p>{formatCron(selectedTask.recurrence)}</p>
687
+ <p className="text-xs text-[var(--color-text-faint)] mt-0.5 font-mono">{selectedTask.recurrence}</p>
688
+ </div>
689
+ )}
690
+ </div>
691
+ )}
692
+
693
+ {/* Schedule (edit mode) */}
694
+ {editing && editForm.type === "once" && (
545
695
  <div>
546
- <span className="text-[var(--color-text-muted)]">Type</span>
547
- <p className="capitalize">{selectedTask.type}</p>
696
+ <label className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1 block">Schedule</label>
697
+ <input
698
+ type="datetime-local"
699
+ value={editForm.execute_at}
700
+ onChange={e => setEditForm({ ...editForm, execute_at: e.target.value })}
701
+ className={inputClass}
702
+ />
548
703
  </div>
704
+ )}
705
+ {editing && editForm.type === "recurring" && (
549
706
  <div>
550
- <span className="text-[var(--color-text-muted)]">Priority</span>
551
- <p>{selectedTask.priority}</p>
707
+ <label className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1 block">Cron Schedule</label>
708
+ <input
709
+ type="text"
710
+ value={editForm.recurrence}
711
+ onChange={e => setEditForm({ ...editForm, recurrence: e.target.value })}
712
+ className={`${inputClass} font-mono`}
713
+ placeholder="*/30 * * * *"
714
+ />
715
+ <p className="text-xs text-[var(--color-text-faint)] mt-1">e.g. */30 * * * * = every 30 min</p>
552
716
  </div>
553
- {selectedTask.recurrence && (
554
- <div>
555
- <span className="text-[var(--color-text-muted)]">Recurrence</span>
556
- <p>{formatCron(selectedTask.recurrence)}</p>
557
- <p className="text-xs text-[var(--color-text-faint)] mt-0.5 font-mono">{selectedTask.recurrence}</p>
558
- </div>
559
- )}
560
- </div>
717
+ )}
561
718
 
562
- {/* Timestamps */}
719
+ {/* Timestamps (view mode only) */}
720
+ {!editing && (
563
721
  <div className="space-y-2 text-sm">
564
722
  <div className="flex justify-between">
565
723
  <span className="text-[var(--color-text-muted)]">Created</span>
@@ -590,9 +748,10 @@ function TasksTab({ agent }: { agent: Agent }) {
590
748
  </div>
591
749
  )}
592
750
  </div>
751
+ )}
593
752
 
594
753
  {/* Error */}
595
- {selectedTask.status === "failed" && selectedTask.error && (
754
+ {!editing && selectedTask.status === "failed" && selectedTask.error && (
596
755
  <div className="min-w-0">
597
756
  <h4 className="text-xs text-red-400 uppercase tracking-wider mb-1">Error</h4>
598
757
  <div className="bg-red-500/10 border border-red-500/20 rounded p-3 overflow-x-auto">
@@ -602,7 +761,7 @@ function TasksTab({ agent }: { agent: Agent }) {
602
761
  )}
603
762
 
604
763
  {/* Result */}
605
- {selectedTask.status === "completed" && selectedTask.result && (
764
+ {!editing && selectedTask.status === "completed" && selectedTask.result && (
606
765
  <div className="min-w-0">
607
766
  <h4 className="text-xs text-green-400 uppercase tracking-wider mb-1">Result</h4>
608
767
  <div className="bg-green-500/10 border border-green-500/20 rounded p-3 overflow-x-auto">
@@ -614,13 +773,13 @@ function TasksTab({ agent }: { agent: Agent }) {
614
773
  )}
615
774
 
616
775
  {/* Trajectory */}
617
- {loadingTask && !selectedTask.trajectory && (
776
+ {!editing && loadingTask && !selectedTask.trajectory && (
618
777
  <div>
619
778
  <h4 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-2">Trajectory</h4>
620
779
  <div className="text-sm text-[var(--color-text-faint)]">Loading trajectory...</div>
621
780
  </div>
622
781
  )}
623
- {selectedTask.trajectory && selectedTask.trajectory.length > 0 && (
782
+ {!editing && selectedTask.trajectory && selectedTask.trajectory.length > 0 && (
624
783
  <div>
625
784
  <h4 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-2">
626
785
  Trajectory ({selectedTask.trajectory.length} steps)
@@ -628,6 +787,8 @@ function TasksTab({ agent }: { agent: Agent }) {
628
787
  <TrajectoryView trajectory={selectedTask.trajectory} />
629
788
  </div>
630
789
  )}
790
+ </>;
791
+ })()}
631
792
  </div>
632
793
  </div>
633
794
  );
@@ -681,7 +842,7 @@ function TasksTab({ agent }: { agent: Agent }) {
681
842
  <div
682
843
  key={task.id}
683
844
  onClick={() => selectTask(task)}
684
- className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg p-4 cursor-pointer hover:border-[var(--color-border-light)] transition"
845
+ className="bg-[var(--color-surface)] card p-4 cursor-pointer hover:border-[var(--color-border-light)] transition"
685
846
  >
686
847
  <div className="flex items-start justify-between mb-2">
687
848
  <div className="flex-1 min-w-0">
@@ -202,7 +202,7 @@ export function CreateAgentModal({
202
202
  key={key}
203
203
  type="button"
204
204
  onClick={() => toggleFeature(key)}
205
- className={`flex items-center gap-3 p-3 rounded border text-left transition ${
205
+ className={`flex items-center gap-3 p-3 btn border text-left transition ${
206
206
  isEnabled
207
207
  ? "border-[var(--color-accent)] bg-[var(--color-accent-10)]"
208
208
  : "border-[var(--color-border-light)] hover:border-[var(--color-border-light)]"
@@ -237,7 +237,7 @@ export function CreateAgentModal({
237
237
  },
238
238
  },
239
239
  })}
240
- className={`flex items-center gap-2 px-3 py-2 rounded border transition ${
240
+ className={`flex items-center gap-2 px-3 py-2 btn border transition ${
241
241
  form.features.builtinTools?.webSearch
242
242
  ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]"
243
243
  : "border-[var(--color-border-light)] hover:border-[var(--color-border-light)] text-[var(--color-text-secondary)]"
@@ -260,7 +260,7 @@ export function CreateAgentModal({
260
260
  },
261
261
  },
262
262
  })}
263
- className={`flex items-center gap-2 px-3 py-2 rounded border transition ${
263
+ className={`flex items-center gap-2 px-3 py-2 btn border transition ${
264
264
  form.features.builtinTools?.webFetch
265
265
  ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]"
266
266
  : "border-[var(--color-border-light)] hover:border-[var(--color-border-light)] text-[var(--color-text-secondary)]"
@@ -282,14 +282,14 @@ export function CreateAgentModal({
282
282
  <div className="flex gap-3 mt-6">
283
283
  <button
284
284
  onClick={onClose}
285
- className="flex-1 border border-[var(--color-border-light)] hover:border-[var(--color-accent)] hover:text-[var(--color-accent)] px-4 py-2 rounded font-medium transition"
285
+ className="flex-1 border border-[var(--color-border-light)] hover:border-[var(--color-accent)] hover:text-[var(--color-accent)] px-4 py-2 btn font-medium transition"
286
286
  >
287
287
  Cancel
288
288
  </button>
289
289
  <button
290
290
  onClick={onCreate}
291
291
  disabled={!form.name}
292
- className="flex-1 bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 text-black px-4 py-2 rounded font-medium transition"
292
+ className="flex-1 bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 text-black px-4 py-2 btn font-medium transition"
293
293
  >
294
294
  Create
295
295
  </button>
@@ -23,7 +23,7 @@ export function LoginPage() {
23
23
  };
24
24
 
25
25
  return (
26
- <div className="min-h-screen bg-[var(--color-bg)] text-[var(--color-text)] font-mono flex items-center justify-center p-8">
26
+ <div className="min-h-screen bg-[var(--color-bg)] text-[var(--color-text)] flex items-center justify-center p-8">
27
27
  <div className="w-full max-w-md">
28
28
  {/* Logo */}
29
29
  <div className="text-center mb-8">
@@ -34,7 +34,7 @@ export function LoginPage() {
34
34
  <p className="text-[var(--color-text-muted)]">Run AI agents locally</p>
35
35
  </div>
36
36
 
37
- <div className="bg-[var(--color-surface)] rounded-lg border border-[var(--color-border)] p-8">
37
+ <div className="bg-[var(--color-surface)] card p-8">
38
38
  <h2 className="text-2xl font-semibold mb-2">Welcome back</h2>
39
39
  <p className="text-[var(--color-text-muted)] mb-6">Sign in to continue to apteva</p>
40
40
 
@@ -30,7 +30,7 @@ export function LoadingSpinner({ message = "Loading...", fullScreen = false }: L
30
30
 
31
31
  if (fullScreen) {
32
32
  return (
33
- <div className="min-h-screen bg-[var(--color-bg)] text-[var(--color-text)] font-mono flex items-center justify-center">
33
+ <div className="min-h-screen bg-[var(--color-bg)] text-[var(--color-text)] flex items-center justify-center">
34
34
  {content}
35
35
  </div>
36
36
  );
@@ -8,7 +8,7 @@ interface ModalProps {
8
8
  export function Modal({ children, onClose }: ModalProps) {
9
9
  return (
10
10
  <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
11
- <div className="bg-[var(--color-surface)] rounded p-6 w-full max-w-xl lg:max-w-2xl border border-[var(--color-border)] max-h-[90vh] overflow-y-auto">
11
+ <div className="bg-[var(--color-surface)] card p-6 w-full max-w-xl lg:max-w-2xl max-h-[90vh] overflow-y-auto">
12
12
  {children}
13
13
  </div>
14
14
  </div>
@@ -37,19 +37,19 @@ export function ConfirmModal({
37
37
  }: ConfirmModalProps) {
38
38
  return (
39
39
  <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
40
- <div className="bg-[var(--color-surface)] border border-[var(--color-border-light)] rounded-lg p-6 w-full max-w-sm">
40
+ <div className="bg-[var(--color-surface)] card p-6 w-full max-w-sm">
41
41
  {title && <h3 className="font-medium mb-2">{title}</h3>}
42
42
  <p className="text-sm text-[var(--color-text)] mb-4">{message}</p>
43
43
  <div className="flex gap-2">
44
44
  <button
45
45
  onClick={onCancel}
46
- className="flex-1 text-sm bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] border border-[var(--color-border-light)] px-4 py-2 rounded transition"
46
+ className="flex-1 text-sm bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] border border-[var(--color-border-light)] btn px-4 py-2 transition"
47
47
  >
48
48
  {cancelText}
49
49
  </button>
50
50
  <button
51
51
  onClick={onConfirm}
52
- className={`flex-1 text-sm text-white px-4 py-2 rounded transition ${
52
+ className={`flex-1 text-sm text-white px-4 py-2 btn transition ${
53
53
  confirmVariant === "danger"
54
54
  ? "bg-red-500 hover:bg-red-600"
55
55
  : "bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)]"
@@ -93,7 +93,7 @@ export function AlertModal({
93
93
 
94
94
  return (
95
95
  <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
96
- <div className="bg-[var(--color-surface)] border border-[var(--color-border-light)] rounded-lg p-6 w-full max-w-sm text-center">
96
+ <div className="bg-[var(--color-surface)] card p-6 w-full max-w-sm text-center">
97
97
  <div
98
98
  className={`w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3 ${iconColors[variant]}`}
99
99
  >
@@ -103,7 +103,7 @@ export function AlertModal({
103
103
  <p className="text-sm text-[var(--color-text)] mb-4">{message}</p>
104
104
  <button
105
105
  onClick={onClose}
106
- className="w-full text-sm bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] border border-[var(--color-border-light)] px-4 py-2 rounded transition"
106
+ className="w-full text-sm bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] border border-[var(--color-border-light)] px-4 py-2 btn transition"
107
107
  >
108
108
  {buttonText}
109
109
  </button>
@@ -35,7 +35,7 @@ export function Select({ value, options, onChange, placeholder = "Select...", co
35
35
  <button
36
36
  type="button"
37
37
  onClick={() => setIsOpen(!isOpen)}
38
- className={`w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded ${compact ? "px-2.5 py-1.5 text-sm" : "px-3 py-2"} text-left flex items-center justify-between focus:outline-none focus:border-[var(--color-accent)] text-[var(--color-text)] hover:border-[var(--color-border-light)] transition`}
38
+ className={`w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] btn ${compact ? "px-2.5 py-1.5 text-sm" : "px-3 py-2"} text-left flex items-center justify-between focus:outline-none focus:border-[var(--color-accent)] text-[var(--color-text)] hover:border-[var(--color-border-light)] transition`}
39
39
  >
40
40
  <span className={selectedOption ? "text-[var(--color-text)]" : "text-[var(--color-text-muted)]"}>
41
41
  {selectedOption ? (
@@ -51,7 +51,7 @@ export function Select({ value, options, onChange, placeholder = "Select...", co
51
51
  </button>
52
52
 
53
53
  {isOpen && (
54
- <div className="absolute z-50 w-full min-w-max mt-1 bg-[var(--color-surface)] border border-[var(--color-border-light)] rounded shadow-lg max-h-60 overflow-y-auto scrollbar-hide">
54
+ <div className="absolute z-50 w-full min-w-max mt-1 bg-[var(--color-surface)] border border-[var(--color-border-light)] shadow-lg max-h-60 overflow-y-auto scrollbar-hide" style={{ borderRadius: "var(--radius-button)" }}>
55
55
  {options.map((option) => (
56
56
  <button
57
57
  key={option.value}
@@ -28,7 +28,7 @@ export function ConnectionsPage() {
28
28
  </div>
29
29
 
30
30
  {/* Tabs */}
31
- <div className="flex gap-1 mb-6 bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg p-1 w-fit">
31
+ <div className="flex gap-1 mb-6 bg-[var(--color-surface)] card p-1 w-fit">
32
32
  {tabs.map(tab => (
33
33
  <button
34
34
  key={tab.id}