apteva 0.4.16 → 0.4.18

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 (62) hide show
  1. package/dist/ActivityPage.yv28a2vj.js +3 -0
  2. package/dist/ApiDocsPage.4ccwjjbk.js +4 -0
  3. package/dist/App.155wke5v.js +4 -0
  4. package/dist/App.2e19nvn4.js +13 -0
  5. package/dist/App.2ye1b5n0.js +4 -0
  6. package/dist/App.4da4ycbe.js +4 -0
  7. package/dist/App.b6wtzd1j.js +4 -0
  8. package/dist/App.fjrh28tf.js +4 -0
  9. package/dist/App.htc36cy8.js +4 -0
  10. package/dist/App.me6reaa6.js +4 -0
  11. package/dist/App.n5q6p960.js +4 -0
  12. package/dist/App.nft7h9jt.js +4 -0
  13. package/dist/App.np463xvy.js +4 -0
  14. package/dist/App.nps62kvt.js +4 -0
  15. package/dist/App.q8ws33cc.js +181 -0
  16. package/dist/App.tb0y0jmt.js +40 -0
  17. package/dist/ConnectionsPage.52evzrp7.js +3 -0
  18. package/dist/McpPage.bjqrp0n2.js +3 -0
  19. package/dist/SettingsPage.es76hnj2.js +3 -0
  20. package/dist/SkillsPage.06h8yf0h.js +3 -0
  21. package/dist/TasksPage.99df66mk.js +3 -0
  22. package/dist/TelemetryPage.bmdnxhq7.js +3 -0
  23. package/dist/TestsPage.denxrg8c.js +3 -0
  24. package/dist/index.html +1 -1
  25. package/dist/styles.css +1 -1
  26. package/package.json +1 -1
  27. package/src/auth/middleware.ts +2 -0
  28. package/src/db.ts +162 -11
  29. package/src/mcp-platform.ts +41 -1
  30. package/src/routes/api/agent-utils.ts +38 -2
  31. package/src/routes/api/agents.ts +65 -2
  32. package/src/routes/api/projects.ts +19 -2
  33. package/src/routes/api/system.ts +26 -12
  34. package/src/routes/api/triggers.ts +458 -0
  35. package/src/routes/api/webhooks.ts +171 -0
  36. package/src/routes/api.ts +4 -0
  37. package/src/routes/static.ts +12 -3
  38. package/src/server.ts +6 -4
  39. package/src/triggers/agentdojo.ts +248 -0
  40. package/src/triggers/composio.ts +264 -0
  41. package/src/triggers/index.ts +71 -0
  42. package/src/web/App.tsx +20 -12
  43. package/src/web/components/agents/AgentCard.tsx +14 -7
  44. package/src/web/components/agents/AgentPanel.tsx +105 -115
  45. package/src/web/components/common/Icons.tsx +8 -0
  46. package/src/web/components/common/index.ts +1 -0
  47. package/src/web/components/connections/ConnectionsPage.tsx +54 -0
  48. package/src/web/components/connections/IntegrationsTab.tsx +144 -0
  49. package/src/web/components/connections/OverviewTab.tsx +183 -0
  50. package/src/web/components/connections/TriggersTab.tsx +690 -0
  51. package/src/web/components/index.ts +1 -0
  52. package/src/web/components/layout/Sidebar.tsx +7 -1
  53. package/src/web/components/mcp/IntegrationsPanel.tsx +19 -3
  54. package/src/web/components/mcp/McpPage.tsx +9 -3
  55. package/src/web/components/settings/SettingsPage.tsx +96 -2
  56. package/src/web/components/tasks/TasksPage.tsx +2 -2
  57. package/src/web/components/tests/TestsPage.tsx +1 -2
  58. package/src/web/context/TelemetryContext.tsx +14 -1
  59. package/src/web/context/index.ts +1 -1
  60. package/src/web/hooks/useAgents.ts +15 -11
  61. package/src/web/types.ts +1 -1
  62. package/dist/App.2194efgj.js +0 -228
package/src/web/App.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useMemo } from "react";
1
+ import React, { useState, useEffect, useMemo, lazy, Suspense } from "react";
2
2
  import { createRoot } from "react-dom/client";
3
3
  import "@apteva/apteva-kit/styles.css";
4
4
 
@@ -7,38 +7,42 @@ import type { Agent, Provider, Route, NewAgentForm } from "./types";
7
7
  import { DEFAULT_FEATURES } from "./types";
8
8
 
9
9
  // Context
10
- import { TelemetryProvider, AuthProvider, ProjectProvider, useAuth, useProjects, useAgentStatusChange } from "./context";
10
+ import { TelemetryProvider, AuthProvider, ProjectProvider, useAuth, useProjects, useAgentStatusChange, useTaskChange } from "./context";
11
11
 
12
12
  // Hooks
13
13
  import { useAgents, useProviders, useOnboarding } from "./hooks";
14
14
 
15
- // Components
15
+ // Core components (always needed)
16
16
  import {
17
17
  LoadingSpinner,
18
18
  Header,
19
19
  Sidebar,
20
20
  ErrorBanner,
21
21
  OnboardingWizard,
22
- SettingsPage,
23
22
  CreateAgentModal,
24
23
  AgentsView,
25
24
  Dashboard,
26
- ActivityPage,
27
- TasksPage,
28
- McpPage,
29
- SkillsPage,
30
- TestsPage,
31
- TelemetryPage,
32
25
  LoginPage,
33
26
  } from "./components";
34
- import { ApiDocsPage } from "./components/api/ApiDocsPage";
35
27
  import { MetaAgentProvider, MetaAgentPanel } from "./components/meta-agent/MetaAgent";
36
28
 
29
+ // Lazy-loaded page components (only loaded when navigated to)
30
+ const SettingsPage = lazy(() => import("./components/settings/SettingsPage").then(m => ({ default: m.SettingsPage })));
31
+ const ActivityPage = lazy(() => import("./components/activity/ActivityPage").then(m => ({ default: m.ActivityPage })));
32
+ const TasksPage = lazy(() => import("./components/tasks/TasksPage").then(m => ({ default: m.TasksPage })));
33
+ const McpPage = lazy(() => import("./components/mcp/McpPage").then(m => ({ default: m.McpPage })));
34
+ const SkillsPage = lazy(() => import("./components/skills/SkillsPage").then(m => ({ default: m.SkillsPage })));
35
+ const TestsPage = lazy(() => import("./components/tests/TestsPage").then(m => ({ default: m.TestsPage })));
36
+ const TelemetryPage = lazy(() => import("./components/telemetry/TelemetryPage").then(m => ({ default: m.TelemetryPage })));
37
+ const ConnectionsPage = lazy(() => import("./components/connections/ConnectionsPage").then(m => ({ default: m.ConnectionsPage })));
38
+ const ApiDocsPage = lazy(() => import("./components/api/ApiDocsPage").then(m => ({ default: m.ApiDocsPage })));
39
+
37
40
  function AppContent() {
38
41
  // Auth state
39
42
  const { isAuthenticated, isLoading: authLoading, hasUsers, accessToken, checkAuth } = useAuth();
40
43
  const { currentProjectId, refreshProjects } = useProjects();
41
44
  const statusChangeCounter = useAgentStatusChange();
45
+ const taskChangeCounter = useTaskChange();
42
46
 
43
47
  // Onboarding state
44
48
  const { isComplete: onboardingComplete, setIsComplete: setOnboardingComplete } = useOnboarding();
@@ -107,7 +111,7 @@ function AppContent() {
107
111
  };
108
112
 
109
113
  fetchTaskCount();
110
- }, [shouldFetchData, accessToken, currentProjectId, agents, statusChangeCounter]);
114
+ }, [shouldFetchData, accessToken, currentProjectId, agents, statusChangeCounter, taskChangeCounter]);
111
115
 
112
116
  // Form state
113
117
  const [newAgent, setNewAgent] = useState<NewAgentForm>({
@@ -253,6 +257,7 @@ function AppContent() {
253
257
  />
254
258
 
255
259
  <main className="flex-1 overflow-hidden flex">
260
+ <Suspense fallback={<LoadingSpinner />}>
256
261
  {route === "settings" && <SettingsPage />}
257
262
 
258
263
  {route === "activity" && (
@@ -291,6 +296,8 @@ function AppContent() {
291
296
 
292
297
  {route === "tasks" && <TasksPage />}
293
298
 
299
+ {route === "connections" && <ConnectionsPage />}
300
+
294
301
  {route === "mcp" && <McpPage />}
295
302
 
296
303
  {route === "skills" && <SkillsPage />}
@@ -300,6 +307,7 @@ function AppContent() {
300
307
  {route === "telemetry" && <TelemetryPage />}
301
308
 
302
309
  {route === "api" && <ApiDocsPage />}
310
+ </Suspense>
303
311
  </main>
304
312
  </div>
305
313
 
@@ -121,13 +121,16 @@ export function AgentCard({ agent, selected, onSelect, onToggle, showProject }:
121
121
 
122
122
  <button
123
123
  onClick={onToggle}
124
+ disabled={agent.status === "starting" || agent.status === "stopping"}
124
125
  className={`w-full px-3 py-1.5 rounded text-sm font-medium transition mt-auto ${
125
- agent.status === "running"
126
- ? "bg-[#f97316]/20 text-[#f97316] hover:bg-[#f97316]/30"
127
- : "bg-[#3b82f6]/20 text-[#3b82f6] hover:bg-[#3b82f6]/30"
126
+ agent.status === "starting" || agent.status === "stopping"
127
+ ? "bg-[#333] text-[#666] cursor-wait"
128
+ : agent.status === "running"
129
+ ? "bg-[#f97316]/20 text-[#f97316] hover:bg-[#f97316]/30"
130
+ : "bg-[#3b82f6]/20 text-[#3b82f6] hover:bg-[#3b82f6]/30"
128
131
  }`}
129
132
  >
130
- {agent.status === "running" ? "Stop" : "Start"}
133
+ {agent.status === "starting" ? "Starting..." : agent.status === "stopping" ? "Stopping..." : agent.status === "running" ? "Stop" : "Start"}
131
134
  </button>
132
135
  </div>
133
136
  );
@@ -142,12 +145,16 @@ function StatusBadge({ status, isActive, activityType }: { status: Agent["status
142
145
  );
143
146
  }
144
147
 
148
+ const isTransitioning = status === "starting" || status === "stopping";
149
+
145
150
  return (
146
151
  <span
147
152
  className={`px-2 py-1 rounded text-xs font-medium ${
148
- status === "running"
149
- ? "bg-[#3b82f6]/20 text-[#3b82f6]"
150
- : "bg-[#333] text-[#666]"
153
+ isTransitioning
154
+ ? "bg-yellow-500/20 text-yellow-400 animate-pulse"
155
+ : status === "running"
156
+ ? "bg-[#3b82f6]/20 text-[#3b82f6]"
157
+ : "bg-[#333] text-[#666]"
151
158
  }`}
152
159
  >
153
160
  {status}
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { Chat } from "@apteva/apteva-kit";
3
- import { CloseIcon, MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon } from "../common/Icons";
3
+ import { CloseIcon, MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon, RecurringIcon, ScheduledIcon, TaskOnceIcon } from "../common/Icons";
4
+ import { formatCron, formatRelativeTime } from "../tasks/TasksPage";
4
5
  import { Select } from "../common/Select";
5
6
  import { useConfirm } from "../common/Modal";
6
7
  import { useTelemetry } from "../../context";
@@ -152,7 +153,7 @@ function ThreadsTab({ agent }: { agent: Agent }) {
152
153
  const [loading, setLoading] = useState(true);
153
154
  const [error, setError] = useState<string | null>(null);
154
155
  const [selectedThread, setSelectedThread] = useState<string | null>(null);
155
- const [messages, setMessages] = useState<Array<{ role: string; content: string; created_at: string }>>([]);
156
+ const [initialMessages, setInitialMessages] = useState<any[]>([]);
156
157
  const [loadingMessages, setLoadingMessages] = useState(false);
157
158
  const { confirm, ConfirmDialog } = useConfirm();
158
159
 
@@ -160,7 +161,6 @@ function ThreadsTab({ agent }: { agent: Agent }) {
160
161
  useEffect(() => {
161
162
  setThreads([]);
162
163
  setSelectedThread(null);
163
- setMessages([]);
164
164
  setError(null);
165
165
  setLoading(true);
166
166
  }, [agent.id]);
@@ -188,19 +188,29 @@ function ThreadsTab({ agent }: { agent: Agent }) {
188
188
  fetchThreads();
189
189
  }, [agent.id, agent.status]);
190
190
 
191
- const loadMessages = async (threadId: string) => {
192
- setSelectedThread(threadId);
191
+ const openThread = async (threadId: string) => {
193
192
  setLoadingMessages(true);
193
+ setSelectedThread(threadId);
194
194
  try {
195
195
  const res = await fetch(`/api/agents/${agent.id}/threads/${threadId}/messages`);
196
- if (!res.ok) throw new Error("Failed to fetch messages");
197
- const data = await res.json();
198
- setMessages(data.messages || []);
196
+ if (res.ok) {
197
+ const data = await res.json();
198
+ const msgs = (data.messages || [])
199
+ .filter((m: any) => typeof m.content === "string")
200
+ .map((m: any) => ({
201
+ id: m.id,
202
+ role: m.role,
203
+ content: m.content,
204
+ timestamp: new Date(m.created_at),
205
+ }));
206
+ setInitialMessages(msgs);
207
+ } else {
208
+ setInitialMessages([]);
209
+ }
199
210
  } catch {
200
- setMessages([]);
201
- } finally {
202
- setLoadingMessages(false);
211
+ setInitialMessages([]);
203
212
  }
213
+ setLoadingMessages(false);
204
214
  };
205
215
 
206
216
  const deleteThread = async (threadId: string, e: React.MouseEvent) => {
@@ -213,7 +223,6 @@ function ThreadsTab({ agent }: { agent: Agent }) {
213
223
  setThreads(prev => prev.filter(t => t.id !== threadId));
214
224
  if (selectedThread === threadId) {
215
225
  setSelectedThread(null);
216
- setMessages([]);
217
226
  }
218
227
  } catch {
219
228
  // Ignore errors
@@ -244,7 +253,7 @@ function ThreadsTab({ agent }: { agent: Agent }) {
244
253
  );
245
254
  }
246
255
 
247
- // Show messages view when a thread is selected
256
+ // Show live chat for selected thread
248
257
  if (selectedThread) {
249
258
  const selectedThreadData = threads.find(t => t.id === selectedThread);
250
259
  return (
@@ -252,15 +261,15 @@ function ThreadsTab({ agent }: { agent: Agent }) {
252
261
  {ConfirmDialog}
253
262
  <div className="flex-1 flex flex-col overflow-hidden">
254
263
  {/* Header with back button */}
255
- <div className="flex items-center gap-3 px-4 py-3 border-b border-[#1a1a1a]">
264
+ <div className="flex items-center gap-3 px-4 py-2 border-b border-[#1a1a1a] flex-shrink-0">
256
265
  <button
257
- onClick={() => { setSelectedThread(null); setMessages([]); }}
266
+ onClick={() => { setSelectedThread(null); setInitialMessages([]); }}
258
267
  className="text-[#666] hover:text-[#e0e0e0] transition text-lg"
259
268
  >
260
269
 
261
270
  </button>
262
- <div className="flex-1">
263
- <p className="text-sm font-medium">
271
+ <div className="flex-1 min-w-0">
272
+ <p className="text-sm font-medium truncate">
264
273
  {selectedThreadData?.title || `Thread ${selectedThread.slice(0, 8)}`}
265
274
  </p>
266
275
  <p className="text-xs text-[#666]">
@@ -275,54 +284,22 @@ function ThreadsTab({ agent }: { agent: Agent }) {
275
284
  </button>
276
285
  </div>
277
286
 
278
- {/* Messages */}
279
- <div className="flex-1 overflow-auto p-4">
280
- {loadingMessages ? (
281
- <p className="text-[#666]">Loading messages...</p>
282
- ) : messages.length === 0 ? (
283
- <p className="text-[#666]">No messages in this thread</p>
284
- ) : (
285
- <div className="space-y-4">
286
- {messages.map((msg, i) => (
287
- <div key={i} className={`${msg.role === "user" ? "text-right" : ""}`}>
288
- <div
289
- className={`inline-block max-w-[80%] p-3 rounded ${
290
- msg.role === "user"
291
- ? "bg-[#f97316]/20 text-[#f97316]"
292
- : "bg-[#1a1a1a] text-[#e0e0e0]"
293
- }`}
294
- >
295
- <div className="text-sm whitespace-pre-wrap">
296
- {typeof msg.content === "string"
297
- ? msg.content
298
- : Array.isArray(msg.content)
299
- ? msg.content.map((block: any, j: number) => (
300
- <div key={j}>
301
- {block.type === "text" && block.text}
302
- {block.type === "tool_use" && (
303
- <div className="bg-[#222] p-2 rounded mt-1 text-xs text-[#888]">
304
- 🔧 Tool: {block.name}
305
- </div>
306
- )}
307
- {block.type === "tool_result" && (
308
- <div className="bg-[#222] p-2 rounded mt-1 text-xs text-[#888]">
309
- 📋 Result: {typeof block.content === "string" ? block.content.slice(0, 200) : "..."}
310
- </div>
311
- )}
312
- </div>
313
- ))
314
- : JSON.stringify(msg.content)
315
- }
316
- </div>
317
- <p className="text-xs text-[#666] mt-1">
318
- {new Date(msg.created_at).toLocaleTimeString()}
319
- </p>
320
- </div>
321
- </div>
322
- ))}
323
- </div>
324
- )}
325
- </div>
287
+ {/* Live chat in this thread */}
288
+ {loadingMessages ? (
289
+ <div className="flex-1 flex items-center justify-center text-[#666]">Loading messages...</div>
290
+ ) : (
291
+ <Chat
292
+ key={selectedThread}
293
+ agentId="default"
294
+ apiUrl={`/api/agents/${agent.id}`}
295
+ threadId={selectedThread}
296
+ initialMessages={initialMessages}
297
+ placeholder="Continue this conversation..."
298
+ context={agent.systemPrompt}
299
+ variant="terminal"
300
+ showHeader={false}
301
+ />
302
+ )}
326
303
  </div>
327
304
  </>
328
305
  );
@@ -342,7 +319,7 @@ function ThreadsTab({ agent }: { agent: Agent }) {
342
319
  {threads.map(thread => (
343
320
  <div
344
321
  key={thread.id}
345
- onClick={() => loadMessages(thread.id)}
322
+ onClick={() => openThread(thread.id)}
346
323
  className="p-4 cursor-pointer hover:bg-[#111] transition flex items-center justify-between"
347
324
  >
348
325
  <div className="flex-1 min-w-0">
@@ -369,24 +346,11 @@ function ThreadsTab({ agent }: { agent: Agent }) {
369
346
  );
370
347
  }
371
348
 
372
- interface Task {
373
- id: string;
374
- name: string;
375
- description?: string;
376
- status: "pending" | "running" | "completed" | "failed";
377
- created_at: string;
378
- updated_at?: string;
379
- scheduled_at?: string;
380
- completed_at?: string;
381
- result?: string;
382
- error?: string;
383
- }
384
-
385
349
  function TasksTab({ agent }: { agent: Agent }) {
386
- const [tasks, setTasks] = useState<Task[]>([]);
350
+ const [tasks, setTasks] = useState<any[]>([]);
387
351
  const [loading, setLoading] = useState(true);
388
352
  const [error, setError] = useState<string | null>(null);
389
- const [filter, setFilter] = useState<"all" | "pending" | "running" | "completed">("all");
353
+ const [filter, setFilter] = useState<string>("all");
390
354
  const { events } = useTelemetry({ agent_id: agent.id, category: "task" });
391
355
 
392
356
  // Reset state when agent changes
@@ -421,14 +385,12 @@ function TasksTab({ agent }: { agent: Agent }) {
421
385
  fetchTasks();
422
386
  }, [agent.id, agent.status, filter, events.length]);
423
387
 
424
- const getStatusColor = (status: Task["status"]) => {
425
- switch (status) {
426
- case "pending": return "bg-yellow-500/20 text-yellow-400";
427
- case "running": return "bg-blue-500/20 text-blue-400";
428
- case "completed": return "bg-green-500/20 text-green-400";
429
- case "failed": return "bg-red-500/20 text-red-400";
430
- default: return "bg-[#222] text-[#666]";
431
- }
388
+ const statusColors: Record<string, string> = {
389
+ pending: "bg-yellow-500/20 text-yellow-400",
390
+ running: "bg-blue-500/20 text-blue-400",
391
+ completed: "bg-green-500/20 text-green-400",
392
+ failed: "bg-red-500/20 text-red-400",
393
+ cancelled: "bg-gray-500/20 text-gray-400",
432
394
  };
433
395
 
434
396
  if (agent.status !== "running") {
@@ -466,63 +428,91 @@ function TasksTab({ agent }: { agent: Agent }) {
466
428
  );
467
429
  }
468
430
 
431
+ const filterOptions = [
432
+ { value: "all", label: "All" },
433
+ { value: "pending", label: "Pending" },
434
+ { value: "running", label: "Running" },
435
+ { value: "completed", label: "Completed" },
436
+ { value: "failed", label: "Failed" },
437
+ ];
438
+
469
439
  return (
470
440
  <div className="flex-1 overflow-auto p-4">
471
441
  {/* Filter tabs */}
472
442
  <div className="flex gap-2 mb-4">
473
- {(["all", "pending", "running", "completed"] as const).map(status => (
443
+ {filterOptions.map(opt => (
474
444
  <button
475
- key={status}
476
- onClick={() => setFilter(status)}
477
- className={`px-3 py-1 text-xs rounded transition ${
478
- filter === status
445
+ key={opt.value}
446
+ onClick={() => setFilter(opt.value)}
447
+ className={`px-3 py-1.5 rounded text-sm transition ${
448
+ filter === opt.value
479
449
  ? "bg-[#f97316] text-black"
480
- : "bg-[#1a1a1a] text-[#666] hover:text-[#888]"
450
+ : "bg-[#1a1a1a] hover:bg-[#222]"
481
451
  }`}
482
452
  >
483
- {status.charAt(0).toUpperCase() + status.slice(1)}
453
+ {opt.label}
484
454
  </button>
485
455
  ))}
486
456
  </div>
487
457
 
488
458
  {tasks.length === 0 ? (
489
- <div className="text-center py-10 text-[#666]">
490
- <p>No {filter === "all" ? "" : filter + " "}tasks</p>
459
+ <div className="text-center py-10">
460
+ <TasksIcon className="w-10 h-10 mx-auto mb-3 text-[#333]" />
461
+ <p className="text-[#666]">No {filter === "all" ? "" : filter + " "}tasks</p>
462
+ <p className="text-sm text-[#444] mt-1">Tasks will appear here when created</p>
491
463
  </div>
492
464
  ) : (
493
465
  <div className="space-y-3">
494
466
  {tasks.map(task => (
495
- <div key={task.id} className="bg-[#111] border border-[#1a1a1a] rounded p-3">
496
- <div className="flex items-start justify-between">
467
+ <div key={task.id} className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4">
468
+ <div className="flex items-start justify-between mb-2">
497
469
  <div className="flex-1 min-w-0">
498
- <p className="text-sm font-medium text-[#e0e0e0]">{task.name}</p>
499
- {task.description && (
500
- <p className="text-xs text-[#666] mt-1 line-clamp-2">{task.description}</p>
501
- )}
470
+ <h3 className="font-medium">{task.title || task.name}</h3>
502
471
  </div>
503
- <span className={`text-xs px-2 py-0.5 rounded ml-2 ${getStatusColor(task.status)}`}>
472
+ <span className={`px-2 py-1 rounded text-xs font-medium ml-2 ${statusColors[task.status] || statusColors.pending}`}>
504
473
  {task.status}
505
474
  </span>
506
475
  </div>
507
476
 
508
- <div className="flex items-center gap-4 mt-2 text-xs text-[#666]">
509
- <span>Created: {new Date(task.created_at).toLocaleString()}</span>
510
- {task.scheduled_at && (
511
- <span>Scheduled: {new Date(task.scheduled_at).toLocaleString()}</span>
477
+ {task.description && (
478
+ <p className="text-sm text-[#888] mb-2 line-clamp-2">{task.description}</p>
479
+ )}
480
+
481
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-[#555]">
482
+ <span className="flex items-center gap-1">
483
+ {task.type === "recurring"
484
+ ? <RecurringIcon className="w-3.5 h-3.5" />
485
+ : task.execute_at
486
+ ? <ScheduledIcon className="w-3.5 h-3.5" />
487
+ : <TaskOnceIcon className="w-3.5 h-3.5" />
488
+ }
489
+ {task.type === "recurring" && task.recurrence ? formatCron(task.recurrence) : task.type || "once"}
490
+ </span>
491
+ {task.priority !== undefined && (
492
+ <span>Priority: {task.priority}</span>
493
+ )}
494
+ {task.next_run && (
495
+ <span className="text-[#f97316]">{formatRelativeTime(task.next_run)}</span>
496
+ )}
497
+ {!task.next_run && task.execute_at && (
498
+ <span className="text-[#f97316]">{formatRelativeTime(task.execute_at)}</span>
512
499
  )}
500
+ <span>Created: {new Date(task.created_at).toLocaleDateString()}</span>
513
501
  </div>
514
502
 
515
503
  {task.status === "completed" && task.result && (
516
- <div className="mt-2 p-2 bg-[#0a0a0a] rounded text-xs text-[#888]">
517
- <p className="text-[#666] mb-1">Result:</p>
518
- <p className="whitespace-pre-wrap">{task.result}</p>
504
+ <div className="mt-3 bg-green-500/10 border border-green-500/20 rounded p-3">
505
+ <h4 className="text-xs text-green-400 uppercase tracking-wider mb-1">Result</h4>
506
+ <pre className="text-sm text-green-400 whitespace-pre-wrap break-words">
507
+ {typeof task.result === "string" ? task.result : JSON.stringify(task.result, null, 2)}
508
+ </pre>
519
509
  </div>
520
510
  )}
521
511
 
522
512
  {task.status === "failed" && task.error && (
523
- <div className="mt-2 p-2 bg-red-500/10 rounded text-xs text-red-400">
524
- <p className="text-red-400/70 mb-1">Error:</p>
525
- <p className="whitespace-pre-wrap">{task.error}</p>
513
+ <div className="mt-3 bg-red-500/10 border border-red-500/20 rounded p-3">
514
+ <h4 className="text-xs text-red-400 uppercase tracking-wider mb-1">Error</h4>
515
+ <pre className="text-sm text-red-400 whitespace-pre-wrap break-words">{task.error}</pre>
526
516
  </div>
527
517
  )}
528
518
  </div>
@@ -182,6 +182,14 @@ export function TestsIcon({ className = "w-4 h-4" }: IconProps) {
182
182
  );
183
183
  }
184
184
 
185
+ export function ConnectionsIcon({ className = "w-5 h-5" }: IconProps) {
186
+ return (
187
+ <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
188
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
189
+ </svg>
190
+ );
191
+ }
192
+
185
193
  export function ActivityIcon({ className = "w-5 h-5" }: IconProps) {
186
194
  return (
187
195
  <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -16,4 +16,5 @@ export {
16
16
  RealtimeIcon,
17
17
  TelemetryIcon,
18
18
  ActivityIcon,
19
+ ConnectionsIcon,
19
20
  } from "./Icons";
@@ -0,0 +1,54 @@
1
+ import React, { useState } from "react";
2
+ import { OverviewTab } from "./OverviewTab";
3
+ import { TriggersTab } from "./TriggersTab";
4
+ import { IntegrationsTab } from "./IntegrationsTab";
5
+
6
+ type Tab = "overview" | "triggers" | "integrations";
7
+
8
+ export function ConnectionsPage() {
9
+ const [activeTab, setActiveTab] = useState<Tab>("overview");
10
+
11
+ const tabs: { id: Tab; label: string }[] = [
12
+ { id: "overview", label: "Overview" },
13
+ { id: "triggers", label: "Triggers" },
14
+ { id: "integrations", label: "Integrations" },
15
+ ];
16
+
17
+ return (
18
+ <div className="flex-1 overflow-auto p-6">
19
+ <div className="max-w-6xl">
20
+ {/* Header */}
21
+ <div className="flex items-center justify-between mb-6">
22
+ <div>
23
+ <h1 className="text-2xl font-semibold mb-1">Connections</h1>
24
+ <p className="text-[#666]">
25
+ Manage external app connections, triggers, and webhooks.
26
+ </p>
27
+ </div>
28
+ </div>
29
+
30
+ {/* Tabs */}
31
+ <div className="flex gap-1 mb-6 bg-[#111] border border-[#1a1a1a] rounded-lg p-1 w-fit">
32
+ {tabs.map(tab => (
33
+ <button
34
+ key={tab.id}
35
+ onClick={() => setActiveTab(tab.id)}
36
+ className={`px-4 py-2 rounded text-sm font-medium transition ${
37
+ activeTab === tab.id
38
+ ? "bg-[#1a1a1a] text-white"
39
+ : "text-[#666] hover:text-[#888]"
40
+ }`}
41
+ >
42
+ {tab.label}
43
+ </button>
44
+ ))}
45
+ </div>
46
+
47
+ {/* Tab Content */}
48
+ {activeTab === "overview" && <OverviewTab />}
49
+ {activeTab === "triggers" && <TriggersTab />}
50
+ {activeTab === "integrations" && <IntegrationsTab />}
51
+ </div>
52
+ </div>
53
+ );
54
+ }