apteva 0.4.44 → 0.4.51

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 (85) 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.3vnrera5.js +4 -0
  4. package/dist/App.94x6mh7f.js +20 -0
  5. package/dist/{App.qzbx5wtj.js → App.9sryp183.js} +1 -1
  6. package/dist/App.9t1zc5r7.js +53 -0
  7. package/dist/{App.r5serxkt.js → App.jhb45d7r.js} +1 -1
  8. package/dist/App.k4nmqgek.js +221 -0
  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.vza4fxg0.js +4 -0
  15. package/dist/App.wghtdzsk.js +1 -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.q1pqcc93.js +3 -0
  22. package/dist/SkillsPage.whxnez67.js +3 -0
  23. package/dist/TasksPage.zp4jfevw.js +3 -0
  24. package/dist/TelemetryPage.an0ky78c.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 +10 -9
  31. package/src/db.ts +60 -22
  32. package/src/providers.ts +14 -9
  33. package/src/routes/api/agent-utils.ts +25 -3
  34. package/src/routes/api/telemetry.ts +21 -2
  35. package/src/server.ts +53 -1
  36. package/src/web/App.tsx +2 -2
  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 +4 -4
  49. package/src/web/components/layout/Header.tsx +3 -3
  50. package/src/web/components/layout/Sidebar.tsx +6 -5
  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 +59 -26
  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 +414 -94
  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/TelemetryContext.tsx +1 -0
  60. package/src/web/context/ThemeContext.tsx +31 -10
  61. package/src/web/index.html +1 -6
  62. package/src/web/styles.css +47 -0
  63. package/src/web/themes.ts +68 -5
  64. package/src/web/types.ts +1 -1
  65. package/dist/App.09yb8t0b.js +0 -1
  66. package/dist/App.3a67nx9w.js +0 -4
  67. package/dist/App.9epx6785.js +0 -4
  68. package/dist/App.d8955awp.js +0 -4
  69. package/dist/App.drwb57jq.js +0 -4
  70. package/dist/App.gssbmajb.js +0 -4
  71. package/dist/App.qw70pc29.js +0 -53
  72. package/dist/App.tpmp9020.js +0 -20
  73. package/dist/App.v2wb4d7d.js +0 -61
  74. package/dist/App.vxmaaj0m.js +0 -13
  75. package/dist/App.w4p2tda9.js +0 -4
  76. package/dist/App.wv2ng55q.js +0 -221
  77. package/dist/App.yncnrn0f.js +0 -4
  78. package/dist/ConnectionsPage.k6cspyqq.js +0 -3
  79. package/dist/McpPage.cdxm48xj.js +0 -3
  80. package/dist/SettingsPage.evpv7c2y.js +0 -3
  81. package/dist/SkillsPage.pvzp6c1a.js +0 -3
  82. package/dist/TasksPage.6jnvbpsy.js +0 -3
  83. package/dist/TelemetryPage.t7vk24zc.js +0 -3
  84. package/dist/TestsPage.5x6658aa.js +0 -3
  85. package/dist/ThreadsPage.3fvhtevh.js +0 -3
@@ -34,6 +34,13 @@ export async function handleTelemetryRoutes(
34
34
  return json({ error: "agent_id and events are required" }, 400);
35
35
  }
36
36
 
37
+ // Debug: log raw incoming events
38
+ for (const event of body.events) {
39
+ if (event.category === "LLM") {
40
+ console.log(`[telemetry] RAW LLM event from ${body.agent_id}: ${JSON.stringify(event)}`);
41
+ }
42
+ }
43
+
37
44
  // Filter out debug events - too noisy
38
45
  const filteredEvents = body.events.filter(e => e.level !== "debug");
39
46
 
@@ -47,7 +54,16 @@ export async function handleTelemetryRoutes(
47
54
  if (event.category === "LLM" && event.data) {
48
55
  const inputTokens = (event.data.input_tokens as number) || 0;
49
56
  const outputTokens = (event.data.output_tokens as number) || 0;
50
- (event as any).cost = (inputTokens * pricing.input_cost + outputTokens * pricing.output_cost) / 1_000_000;
57
+ const cacheCreationTokens = (event.data.cache_creation_tokens as number) || 0;
58
+ const cacheReadTokens = (event.data.cache_read_tokens as number) || 0;
59
+ const reasoningTokens = (event.data.reasoning_tokens as number) || 0;
60
+ (event as any).cost = (
61
+ inputTokens * pricing.input_cost +
62
+ outputTokens * pricing.output_cost +
63
+ cacheCreationTokens * pricing.cache_creation_cost +
64
+ cacheReadTokens * pricing.cache_read_cost +
65
+ reasoningTokens * pricing.output_cost
66
+ ) / 1_000_000;
51
67
  }
52
68
  }
53
69
  }
@@ -69,6 +85,7 @@ export async function handleTelemetryRoutes(
69
85
  data: e.data,
70
86
  duration_ms: e.duration_ms,
71
87
  error: e.error,
88
+ cost: (e as any).cost as number | undefined,
72
89
  }));
73
90
  telemetryBroadcaster.broadcast(broadcastEvents);
74
91
  }
@@ -134,7 +151,7 @@ export async function handleTelemetryRoutes(
134
151
  project_id: projectIdParam === "null" ? null : projectIdParam || undefined,
135
152
  since: url.searchParams.get("since") || undefined,
136
153
  until: url.searchParams.get("until") || undefined,
137
- group_by: (url.searchParams.get("group_by") as "agent" | "day") || undefined,
154
+ group_by: (url.searchParams.get("group_by") as "agent" | "day" | "project") || undefined,
138
155
  });
139
156
  return json({ usage });
140
157
  }
@@ -147,6 +164,8 @@ export async function handleTelemetryRoutes(
147
164
  const stats = TelemetryDB.getStats({
148
165
  agentId,
149
166
  projectId: projectIdParam === "null" ? null : projectIdParam || undefined,
167
+ since: url.searchParams.get("since") || undefined,
168
+ until: url.searchParams.get("until") || undefined,
150
169
  });
151
170
  return json({ stats });
152
171
  }
package/src/server.ts CHANGED
@@ -34,6 +34,7 @@ export interface TelemetryEvent {
34
34
  data?: Record<string, unknown>;
35
35
  duration_ms?: number;
36
36
  error?: string;
37
+ cost?: number;
37
38
  }
38
39
 
39
40
  class TelemetryBroadcaster {
@@ -357,7 +358,7 @@ const server = Bun.serve({
357
358
  development: false, // Suppress "Started server" message
358
359
  idleTimeout: 255, // Max value - prevents SSE connections from timing out
359
360
 
360
- async fetch(req: Request): Promise<Response> {
361
+ async fetch(req: Request, bunServer: Server): Promise<Response | undefined> {
361
362
  const url = new URL(req.url);
362
363
  const path = url.pathname;
363
364
 
@@ -367,6 +368,19 @@ const server = Bun.serve({
367
368
  console.log(`[${req.method}] ${path}${params}`);
368
369
  }
369
370
 
371
+ // WebSocket upgrade: /api/agents/:id/voice → proxy to agent binary
372
+ const voiceMatch = path.match(/^\/api\/agents\/([^/]+)\/voice$/);
373
+ if (voiceMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
374
+ const agentId = voiceMatch[1];
375
+ const agent = AgentDB.findById(agentId);
376
+ if (!agent || agent.status !== "running" || !agent.port) {
377
+ return new Response("Agent not available", { status: 400 });
378
+ }
379
+ const upgraded = bunServer.upgrade(req, { data: { agentId: agent.id, agentPort: agent.port } });
380
+ if (upgraded) return undefined;
381
+ return new Response("WebSocket upgrade failed", { status: 500 });
382
+ }
383
+
370
384
  // CORS headers - configurable origins
371
385
  const origin = req.headers.get("Origin") || "";
372
386
  const allowedOrigins = process.env.CORS_ORIGINS?.split(",") || [];
@@ -436,6 +450,44 @@ const server = Bun.serve({
436
450
  // Serve static files (React app)
437
451
  return serveStatic(req, path);
438
452
  },
453
+
454
+ // WebSocket proxy for agent voice/realtime
455
+ websocket: {
456
+ open(ws: any) {
457
+ const { agentId, agentPort } = ws.data;
458
+ const agentWs = new WebSocket(`ws://localhost:${agentPort}/voice`);
459
+
460
+ agentWs.onopen = () => {
461
+ console.log(`[WS] Voice proxy connected: agent=${agentId} port=${agentPort}`);
462
+ };
463
+ agentWs.onmessage = (event: MessageEvent) => {
464
+ try { ws.send(event.data); } catch {}
465
+ };
466
+ agentWs.onclose = (event: CloseEvent) => {
467
+ console.log(`[WS] Agent disconnected: agent=${agentId} code=${event.code}`);
468
+ ws.close(event.code, event.reason);
469
+ };
470
+ agentWs.onerror = () => {
471
+ ws.close(1011, "Agent WebSocket error");
472
+ };
473
+
474
+ // Store agent WS on the client WS for message/close handlers
475
+ ws.data.agentWs = agentWs;
476
+ },
477
+ message(ws: any, message: string | Buffer) {
478
+ const agentWs = ws.data.agentWs as WebSocket;
479
+ if (agentWs?.readyState === WebSocket.OPEN) {
480
+ agentWs.send(message);
481
+ }
482
+ },
483
+ close(ws: any, code: number, reason: string) {
484
+ const agentWs = ws.data.agentWs as WebSocket;
485
+ if (agentWs && agentWs.readyState !== WebSocket.CLOSED) {
486
+ agentWs.close(code, reason);
487
+ }
488
+ console.log(`[WS] Client disconnected: agent=${ws.data.agentId} code=${code}`);
489
+ },
490
+ },
439
491
  });
440
492
 
441
493
  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 && (
@@ -314,7 +314,7 @@ function AppContent() {
314
314
 
315
315
  {route === "tests" && <TestsPage />}
316
316
 
317
- {route === "telemetry" && <TelemetryPage />}
317
+ {route === "analytics" && <TelemetryPage />}
318
318
 
319
319
  {route === "api" && <ApiDocsPage />}
320
320
  </Suspense>
@@ -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
  );