apteva 0.4.56 → 0.7.0

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 (142) hide show
  1. package/README.md +216 -54
  2. package/cli.js +35 -0
  3. package/install.js +92 -0
  4. package/package.json +12 -79
  5. package/LICENSE +0 -63
  6. package/bin/apteva.js +0 -196
  7. package/dist/ActivityPage.kxzzb4yc.js +0 -3
  8. package/dist/ApiDocsPage.zq998hbm.js +0 -4
  9. package/dist/App.55rea8mn.js +0 -61
  10. package/dist/App.5ywb23z4.js +0 -53
  11. package/dist/App.6thds120.js +0 -4
  12. package/dist/App.9tctxzqm.js +0 -8
  13. package/dist/App.a8r8ttaz.js +0 -4
  14. package/dist/App.agsv5bje.js +0 -4
  15. package/dist/App.cepapqmx.js +0 -4
  16. package/dist/App.dp041gb3.js +0 -221
  17. package/dist/App.fds72zb5.js +0 -4
  18. package/dist/App.fg9qj2dq.js +0 -4
  19. package/dist/App.ndfejbm9.js +0 -4
  20. package/dist/App.nxmfmq1h.js +0 -13
  21. package/dist/App.qdfyt8ba.js +0 -4
  22. package/dist/App.x2d0ygt6.js +0 -4
  23. package/dist/App.yt9p4nr3.js +0 -20
  24. package/dist/App.zn4mw16t.js +0 -1
  25. package/dist/ConnectionsPage.8r96ryw7.js +0 -3
  26. package/dist/McpPage.3cwh0gnd.js +0 -3
  27. package/dist/SettingsPage.ykgdh5ev.js +0 -3
  28. package/dist/SkillsPage.4np1s65b.js +0 -3
  29. package/dist/TasksPage.4g08t7p6.js +0 -3
  30. package/dist/TelemetryPage.72w9pwcp.js +0 -3
  31. package/dist/TestsPage.z4fk3r7r.js +0 -3
  32. package/dist/ThreadsPage.63tcajeh.js +0 -3
  33. package/dist/apteva-kit.css +0 -1
  34. package/dist/icon.png +0 -0
  35. package/dist/index.html +0 -16
  36. package/dist/styles.css +0 -1
  37. package/scripts/postinstall.mjs +0 -102
  38. package/src/auth/index.ts +0 -394
  39. package/src/auth/middleware.ts +0 -213
  40. package/src/binary.ts +0 -536
  41. package/src/channels/index.ts +0 -40
  42. package/src/channels/telegram.ts +0 -311
  43. package/src/crypto.ts +0 -301
  44. package/src/db-tests.ts +0 -174
  45. package/src/db.ts +0 -3133
  46. package/src/integrations/agentdojo.ts +0 -559
  47. package/src/integrations/composio.ts +0 -437
  48. package/src/integrations/index.ts +0 -87
  49. package/src/integrations/skillsmp.ts +0 -318
  50. package/src/mcp-client.ts +0 -605
  51. package/src/mcp-handler.ts +0 -394
  52. package/src/mcp-platform.ts +0 -2370
  53. package/src/openapi.ts +0 -2410
  54. package/src/providers.ts +0 -597
  55. package/src/routes/api/agent-utils.ts +0 -890
  56. package/src/routes/api/agents.ts +0 -916
  57. package/src/routes/api/api-keys.ts +0 -95
  58. package/src/routes/api/channels.ts +0 -182
  59. package/src/routes/api/helpers.ts +0 -12
  60. package/src/routes/api/integrations.ts +0 -639
  61. package/src/routes/api/mcp.ts +0 -574
  62. package/src/routes/api/meta-agent.ts +0 -195
  63. package/src/routes/api/projects.ts +0 -112
  64. package/src/routes/api/providers.ts +0 -424
  65. package/src/routes/api/skills.ts +0 -537
  66. package/src/routes/api/system.ts +0 -333
  67. package/src/routes/api/telemetry.ts +0 -203
  68. package/src/routes/api/tests.ts +0 -148
  69. package/src/routes/api/triggers.ts +0 -518
  70. package/src/routes/api/users.ts +0 -148
  71. package/src/routes/api/webhooks.ts +0 -171
  72. package/src/routes/api.ts +0 -53
  73. package/src/routes/auth.ts +0 -251
  74. package/src/routes/share.ts +0 -86
  75. package/src/routes/static.ts +0 -131
  76. package/src/server.ts +0 -642
  77. package/src/test-runner.ts +0 -598
  78. package/src/triggers/agentdojo.ts +0 -253
  79. package/src/triggers/composio.ts +0 -264
  80. package/src/triggers/index.ts +0 -71
  81. package/src/tui/AgentList.tsx +0 -145
  82. package/src/tui/App.tsx +0 -102
  83. package/src/tui/Login.tsx +0 -104
  84. package/src/tui/api.ts +0 -72
  85. package/src/tui/index.tsx +0 -7
  86. package/src/web/App.tsx +0 -455
  87. package/src/web/components/activity/ActivityPage.tsx +0 -314
  88. package/src/web/components/activity/index.ts +0 -1
  89. package/src/web/components/agents/AgentCard.tsx +0 -189
  90. package/src/web/components/agents/AgentPanel.tsx +0 -2244
  91. package/src/web/components/agents/AgentsView.tsx +0 -180
  92. package/src/web/components/agents/CreateAgentModal.tsx +0 -475
  93. package/src/web/components/agents/index.ts +0 -4
  94. package/src/web/components/api/ApiDocsPage.tsx +0 -842
  95. package/src/web/components/auth/CreateAccountStep.tsx +0 -176
  96. package/src/web/components/auth/LoginPage.tsx +0 -91
  97. package/src/web/components/auth/index.ts +0 -2
  98. package/src/web/components/common/Icons.tsx +0 -250
  99. package/src/web/components/common/LoadingSpinner.tsx +0 -44
  100. package/src/web/components/common/Modal.tsx +0 -199
  101. package/src/web/components/common/Select.tsx +0 -97
  102. package/src/web/components/common/index.ts +0 -20
  103. package/src/web/components/connections/ConnectionsPage.tsx +0 -54
  104. package/src/web/components/connections/IntegrationsTab.tsx +0 -170
  105. package/src/web/components/connections/OverviewTab.tsx +0 -137
  106. package/src/web/components/connections/TriggersTab.tsx +0 -1346
  107. package/src/web/components/dashboard/Dashboard.tsx +0 -572
  108. package/src/web/components/dashboard/index.ts +0 -1
  109. package/src/web/components/index.ts +0 -21
  110. package/src/web/components/layout/ErrorBanner.tsx +0 -18
  111. package/src/web/components/layout/Header.tsx +0 -332
  112. package/src/web/components/layout/Sidebar.tsx +0 -231
  113. package/src/web/components/layout/index.ts +0 -3
  114. package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
  115. package/src/web/components/mcp/McpPage.tsx +0 -2515
  116. package/src/web/components/mcp/index.ts +0 -1
  117. package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
  118. package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
  119. package/src/web/components/onboarding/index.ts +0 -1
  120. package/src/web/components/settings/SettingsPage.tsx +0 -2776
  121. package/src/web/components/settings/index.ts +0 -1
  122. package/src/web/components/skills/SkillsPage.tsx +0 -1200
  123. package/src/web/components/tasks/TasksPage.tsx +0 -1116
  124. package/src/web/components/tasks/index.ts +0 -1
  125. package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
  126. package/src/web/components/tests/TestsPage.tsx +0 -594
  127. package/src/web/components/threads/ThreadsPage.tsx +0 -315
  128. package/src/web/context/AuthContext.tsx +0 -242
  129. package/src/web/context/ProjectContext.tsx +0 -214
  130. package/src/web/context/TelemetryContext.tsx +0 -299
  131. package/src/web/context/ThemeContext.tsx +0 -90
  132. package/src/web/context/UIModeContext.tsx +0 -49
  133. package/src/web/context/index.ts +0 -12
  134. package/src/web/hooks/index.ts +0 -3
  135. package/src/web/hooks/useAgents.ts +0 -115
  136. package/src/web/hooks/useOnboarding.ts +0 -20
  137. package/src/web/hooks/useProviders.ts +0 -75
  138. package/src/web/icon.png +0 -0
  139. package/src/web/index.html +0 -16
  140. package/src/web/styles.css +0 -118
  141. package/src/web/themes.ts +0 -162
  142. package/src/web/types.ts +0 -298
@@ -1,1116 +0,0 @@
1
- import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
2
- import { TasksIcon, CloseIcon, RecurringIcon, ScheduledIcon, TaskOnceIcon } from "../common/Icons";
3
- import { Select } from "../common/Select";
4
- import { useConfirm } from "../common/Modal";
5
- import { useAuth, useProjects } from "../../context";
6
- import { useTelemetry } from "../../context/TelemetryContext";
7
- import type { Task, TaskTrajectoryStep, ToolUseBlock, ToolResultBlock, Agent } from "../../types";
8
-
9
- interface TasksPageProps {
10
- onSelectAgent?: (agentId: string) => void;
11
- }
12
-
13
- export function TasksPage({ onSelectAgent }: TasksPageProps) {
14
- const { authFetch } = useAuth();
15
- const { currentProjectId } = useProjects();
16
- const [tasks, setTasks] = useState<Task[]>([]);
17
- const [loading, setLoading] = useState(true);
18
- const [filter, setFilter] = useState<string>("all");
19
- const [agentFilter, setAgentFilter] = useState<string>("all");
20
- const [selectedTask, setSelectedTask] = useState<Task | null>(null);
21
- const [loadingTask, setLoadingTask] = useState(false);
22
- const [showCreateModal, setShowCreateModal] = useState(false);
23
- const lastProcessedEventRef = useRef<string | null>(null);
24
-
25
- // Subscribe to task telemetry events for real-time updates
26
- const { events: taskEvents } = useTelemetry({ category: "TASK" });
27
-
28
- const fetchTasks = useCallback(async () => {
29
- try {
30
- let url = `/api/tasks?status=${filter}`;
31
- if (currentProjectId !== null) {
32
- url += `&project_id=${encodeURIComponent(currentProjectId)}`;
33
- }
34
- const res = await authFetch(url);
35
- const data = await res.json();
36
- setTasks(data.tasks || []);
37
- } catch (e) {
38
- console.error("Failed to fetch tasks:", e);
39
- } finally {
40
- setLoading(false);
41
- }
42
- }, [authFetch, filter, currentProjectId]);
43
-
44
- // Initial fetch
45
- useEffect(() => {
46
- fetchTasks();
47
- }, [fetchTasks]);
48
-
49
- // Handle real-time task events from telemetry - use as trigger to refetch
50
- // since telemetry data is incomplete (missing id, agentId, status, etc.)
51
- useEffect(() => {
52
- if (!taskEvents.length) return;
53
-
54
- const latestEvent = taskEvents[0];
55
- if (!latestEvent || latestEvent.id === lastProcessedEventRef.current) return;
56
-
57
- // Only react to task mutation events
58
- const eventType = latestEvent.type;
59
- if (eventType === "task_created" || eventType === "task_updated" || eventType === "task_deleted") {
60
- lastProcessedEventRef.current = latestEvent.id;
61
- console.log("[TasksPage] Telemetry event:", eventType);
62
- // Refetch to get complete task data
63
- fetchTasks();
64
- }
65
- }, [taskEvents, fetchTasks]);
66
-
67
- // Fetch full task details (including trajectory) when selecting a task
68
- const selectTask = useCallback(async (task: Task) => {
69
- // Set task immediately for quick feedback
70
- setSelectedTask(task);
71
- setLoadingTask(true);
72
-
73
- try {
74
- const res = await authFetch(`/api/tasks/${task.agentId}/${task.id}`);
75
- console.log("[TasksPage] Fetch task response status:", res.status);
76
- if (res.ok) {
77
- const data = await res.json();
78
- console.log("[TasksPage] Task data:", data);
79
- console.log("[TasksPage] Has trajectory:", !!data.task?.trajectory, "Length:", data.task?.trajectory?.length);
80
- if (data.task) {
81
- // Merge with agentId/agentName since API might not include them
82
- setSelectedTask({ ...data.task, agentId: task.agentId, agentName: task.agentName });
83
- }
84
- } else {
85
- console.error("[TasksPage] Failed to fetch task:", res.status, await res.text());
86
- }
87
- } catch (e) {
88
- console.error("Failed to fetch task details:", e);
89
- } finally {
90
- setLoadingTask(false);
91
- }
92
- }, [authFetch]);
93
-
94
- // Extract unique agents from tasks for the agent filter
95
- const uniqueAgents = useMemo(() => {
96
- const map = new Map<string, string>();
97
- for (const t of tasks) {
98
- const id = t.agentId || (t as any).agent_id;
99
- const name = t.agentName || (t as any).agent_name;
100
- if (id && name && !map.has(id)) {
101
- map.set(id, name);
102
- }
103
- }
104
- return Array.from(map.entries()).sort((a, b) => a[1].localeCompare(b[1]));
105
- }, [tasks]);
106
-
107
- // Sort tasks: running first, then pending by next execution (soonest first), then completed/failed by date
108
- const sortedTasks = useMemo(() => {
109
- const filtered = agentFilter === "all" ? tasks : tasks.filter(t => (t.agentId || (t as any).agent_id) === agentFilter);
110
- return [...filtered].sort((a, b) => {
111
- // Running tasks first
112
- if (a.status === "running" && b.status !== "running") return -1;
113
- if (b.status === "running" && a.status !== "running") return 1;
114
- // Pending tasks next
115
- const aIsPending = a.status === "pending";
116
- const bIsPending = b.status === "pending";
117
- if (aIsPending && !bIsPending) return -1;
118
- if (bIsPending && !aIsPending) return 1;
119
- // For running/pending: sort by next execution time (soonest first)
120
- if (aIsPending && bIsPending || a.status === "running" && b.status === "running") {
121
- const aTime = a.next_run || a.execute_at || null;
122
- const bTime = b.next_run || b.execute_at || null;
123
- const aTs = aTime ? new Date(aTime).getTime() : Infinity;
124
- const bTs = bTime ? new Date(bTime).getTime() : Infinity;
125
- return aTs - bTs;
126
- }
127
- // For completed/failed: most recent first
128
- const aDate = a.completed_at || a.executed_at || a.created_at;
129
- const bDate = b.completed_at || b.executed_at || b.created_at;
130
- return new Date(bDate).getTime() - new Date(aDate).getTime();
131
- });
132
- }, [tasks, agentFilter]);
133
-
134
- const statusColors: Record<string, string> = {
135
- pending: "bg-yellow-500/20 text-yellow-400",
136
- running: "bg-blue-500/20 text-blue-400",
137
- completed: "bg-green-500/20 text-green-400",
138
- failed: "bg-red-500/20 text-red-400",
139
- cancelled: "bg-gray-500/20 text-gray-400",
140
- };
141
-
142
- const filterOptions = [
143
- { value: "all", label: "All" },
144
- { value: "pending", label: "Pending" },
145
- { value: "running", label: "Running" },
146
- { value: "completed", label: "Completed" },
147
- { value: "failed", label: "Failed" },
148
- ];
149
-
150
- return (
151
- <div className="flex-1 flex overflow-hidden">
152
- {/* Task List */}
153
- <div className={`flex-1 p-4 md:p-6 overflow-auto ${selectedTask ? 'hidden md:block md:w-1/2 lg:w-2/3' : ''}`}>
154
- <div className="max-w-4xl">
155
- <div className="mb-6">
156
- <div className="mb-4 flex items-start justify-between">
157
- <div>
158
- <h1 className="text-xl md:text-2xl font-semibold mb-1">Tasks</h1>
159
- <p className="text-sm text-[var(--color-text-muted)]">
160
- View tasks from all running agents
161
- </p>
162
- </div>
163
- <button
164
- onClick={() => setShowCreateModal(true)}
165
- className="px-3 py-1.5 rounded text-sm bg-[var(--color-accent)] text-black hover:opacity-90 transition flex items-center gap-1.5 flex-shrink-0"
166
- >
167
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>
168
- Create Task
169
- </button>
170
- </div>
171
- <div className="flex items-center gap-3 flex-wrap pb-1">
172
- <div className="flex gap-2 overflow-x-auto scrollbar-hide">
173
- {filterOptions.map(opt => (
174
- <button
175
- key={opt.value}
176
- onClick={() => setFilter(opt.value)}
177
- className={`px-3 py-1.5 rounded text-sm transition whitespace-nowrap ${
178
- filter === opt.value
179
- ? "bg-[var(--color-accent)] text-black"
180
- : "bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)]"
181
- }`}
182
- >
183
- {opt.label}
184
- </button>
185
- ))}
186
- </div>
187
- {uniqueAgents.length > 0 && (
188
- <div className="w-48">
189
- <Select
190
- value={agentFilter}
191
- onChange={setAgentFilter}
192
- placeholder="All agents"
193
- compact
194
- options={[
195
- { value: "all", label: "All agents" },
196
- ...uniqueAgents.map(([id, name]) => ({ value: id, label: name })),
197
- ]}
198
- />
199
- </div>
200
- )}
201
- </div>
202
- </div>
203
-
204
- {loading ? (
205
- <div className="text-center py-12 text-[var(--color-text-muted)]">Loading tasks...</div>
206
- ) : sortedTasks.length === 0 ? (
207
- <div className="text-center py-12">
208
- <TasksIcon className="w-12 h-12 mx-auto mb-4 text-[var(--color-border-light)]" />
209
- <p className="text-[var(--color-text-muted)]">No tasks found</p>
210
- <p className="text-sm text-[var(--color-text-faint)] mt-1">
211
- Tasks will appear here when agents create them
212
- </p>
213
- </div>
214
- ) : (
215
- <div className="space-y-3">
216
- {sortedTasks.map(task => (
217
- <div
218
- key={`${task.agentId}-${task.id}`}
219
- onClick={() => selectTask(task)}
220
- className={`bg-[var(--color-surface)] border rounded-lg p-4 cursor-pointer transition ${
221
- selectedTask?.id === task.id && selectedTask?.agentId === task.agentId
222
- ? "border-[var(--color-accent)]"
223
- : "border-[var(--color-border)] hover:border-[var(--color-border-light)]"
224
- }`}
225
- >
226
- <div className="flex items-start justify-between mb-2">
227
- <div className="flex-1">
228
- <h3 className="font-medium">{task.title}</h3>
229
- <p className="text-sm text-[var(--color-text-muted)]">{task.agentName}</p>
230
- </div>
231
- <span className={`px-2 py-1 rounded text-xs font-medium ${statusColors[task.status] || statusColors.pending}`}>
232
- {task.status}
233
- </span>
234
- </div>
235
-
236
- {task.description && (
237
- <p className="text-sm text-[var(--color-text-secondary)] mb-2 line-clamp-2">
238
- {task.description}
239
- </p>
240
- )}
241
-
242
- <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-[var(--color-text-faint)]">
243
- <span className="flex items-center gap-1">
244
- {task.type === "recurring"
245
- ? <RecurringIcon className="w-3.5 h-3.5" />
246
- : task.execute_at
247
- ? <ScheduledIcon className="w-3.5 h-3.5" />
248
- : <TaskOnceIcon className="w-3.5 h-3.5" />
249
- }
250
- {task.type === "recurring" && task.recurrence ? formatCron(task.recurrence) : task.type}
251
- </span>
252
- <span>Priority: {task.priority}</span>
253
- {task.next_run && (
254
- <span className="text-[var(--color-accent)]">{formatRelativeTime(task.next_run)}</span>
255
- )}
256
- {!task.next_run && task.execute_at && (
257
- <span className="text-[var(--color-accent)]">{formatRelativeTime(task.execute_at)}</span>
258
- )}
259
- <span>Created: {new Date(task.created_at).toLocaleDateString()}</span>
260
- </div>
261
- </div>
262
- ))}
263
- </div>
264
- )}
265
- </div>
266
- </div>
267
-
268
- {/* Task Detail Panel */}
269
- {selectedTask && (
270
- <TaskDetailPanel
271
- task={selectedTask}
272
- statusColors={statusColors}
273
- onClose={() => setSelectedTask(null)}
274
- onSelectAgent={onSelectAgent}
275
- loading={loadingTask}
276
- authFetch={authFetch}
277
- onRefresh={() => { fetchTasks(); setSelectedTask(null); }}
278
- />
279
- )}
280
-
281
- {/* Create Task Modal */}
282
- {showCreateModal && (
283
- <CreateTaskModal
284
- authFetch={authFetch}
285
- currentProjectId={currentProjectId}
286
- onClose={() => setShowCreateModal(false)}
287
- onCreated={() => { fetchTasks(); setShowCreateModal(false); }}
288
- />
289
- )}
290
- </div>
291
- );
292
- }
293
-
294
- export interface TaskDetailPanelProps {
295
- task: Task;
296
- statusColors: Record<string, string>;
297
- onClose: () => void;
298
- onSelectAgent?: (agentId: string) => void;
299
- loading?: boolean;
300
- authFetch?: (url: string, options?: RequestInit) => Promise<Response>;
301
- onRefresh?: () => void;
302
- }
303
-
304
- export function TaskDetailPanel({ task, statusColors, onClose, onSelectAgent, loading, authFetch, onRefresh }: TaskDetailPanelProps) {
305
- const [executing, setExecuting] = useState(false);
306
- const [deleting, setDeleting] = useState(false);
307
- const [editing, setEditing] = useState(false);
308
- const [saving, setSaving] = useState(false);
309
- const [editForm, setEditForm] = useState({
310
- title: task.title,
311
- description: task.description || "",
312
- type: task.type as "once" | "recurring",
313
- priority: task.priority,
314
- execute_at: task.execute_at ? new Date(task.execute_at).toISOString().slice(0, 16) : "",
315
- recurrence: task.recurrence || "",
316
- });
317
- const { confirm, ConfirmDialog } = useConfirm();
318
-
319
- // Reset edit form when task changes
320
- useEffect(() => {
321
- setEditForm({
322
- title: task.title,
323
- description: task.description || "",
324
- type: task.type as "once" | "recurring",
325
- priority: task.priority,
326
- execute_at: task.execute_at ? new Date(task.execute_at).toISOString().slice(0, 16) : "",
327
- recurrence: task.recurrence || "",
328
- });
329
- setEditing(false);
330
- }, [task.id, task.agentId]);
331
-
332
- const handleSave = async () => {
333
- if (!authFetch || saving) return;
334
- setSaving(true);
335
- try {
336
- const body: Record<string, unknown> = {
337
- title: editForm.title.trim(),
338
- description: editForm.description.trim() || undefined,
339
- type: editForm.type,
340
- priority: editForm.priority,
341
- };
342
- if (editForm.type === "once" && editForm.execute_at) {
343
- body.execute_at = new Date(editForm.execute_at).toISOString();
344
- }
345
- if (editForm.type === "recurring" && editForm.recurrence.trim()) {
346
- body.recurrence = editForm.recurrence.trim();
347
- }
348
- const res = await authFetch(`/api/tasks/${task.agentId}/${task.id}`, {
349
- method: "PUT",
350
- headers: { "Content-Type": "application/json" },
351
- body: JSON.stringify(body),
352
- });
353
- if (res.ok) {
354
- setEditing(false);
355
- onRefresh?.();
356
- }
357
- } catch (e) {
358
- console.error("Failed to update task:", e);
359
- } finally {
360
- setSaving(false);
361
- }
362
- };
363
-
364
- const handleExecute = async () => {
365
- if (!authFetch || executing) return;
366
- setExecuting(true);
367
- try {
368
- await authFetch(`/api/tasks/${task.agentId}/${task.id}/execute`, { method: "POST" });
369
- onRefresh?.();
370
- } catch (e) {
371
- console.error("Failed to execute task:", e);
372
- } finally {
373
- setExecuting(false);
374
- }
375
- };
376
-
377
- const handleDelete = async () => {
378
- if (!authFetch || deleting) return;
379
- const ok = await confirm(`Are you sure you want to delete "${task.title}"?`, {
380
- title: "Delete Task",
381
- confirmText: "Delete",
382
- confirmVariant: "danger",
383
- });
384
- if (!ok) return;
385
- setDeleting(true);
386
- try {
387
- await authFetch(`/api/tasks/${task.agentId}/${task.id}`, { method: "DELETE" });
388
- onRefresh?.();
389
- } catch (e) {
390
- console.error("Failed to delete task:", e);
391
- } finally {
392
- setDeleting(false);
393
- }
394
- };
395
-
396
- 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)]";
397
-
398
- return (
399
- <div className="w-full md:w-1/2 lg:w-1/3 border-l border-[var(--color-border)] bg-[var(--color-bg)] flex flex-col overflow-hidden">
400
- {ConfirmDialog}
401
- {/* Header */}
402
- <div className="flex items-center justify-between p-4 border-b border-[var(--color-border)]">
403
- <h2 className="font-medium truncate pr-2">{editing ? "Edit Task" : "Task Details"}</h2>
404
- <div className="flex items-center gap-2">
405
- {authFetch && !editing && (task.status === "pending" || task.status === "completed" || task.status === "failed") && (
406
- <button
407
- onClick={() => setEditing(true)}
408
- title="Edit task"
409
- className="text-[var(--color-text-muted)] hover:text-[var(--color-accent)] transition"
410
- >
411
- <svg className="w-5 h-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>
412
- </button>
413
- )}
414
- {authFetch && !editing && (task.status === "pending" || task.status === "completed") && (
415
- <button
416
- onClick={handleExecute}
417
- disabled={executing}
418
- title="Execute now"
419
- className="text-[var(--color-accent)] hover:opacity-80 transition disabled:opacity-50"
420
- >
421
- <svg className="w-5 h-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>
422
- </button>
423
- )}
424
- {authFetch && !editing && (
425
- <button
426
- onClick={handleDelete}
427
- disabled={deleting}
428
- title="Delete task"
429
- className="text-red-400 hover:text-red-300 transition disabled:opacity-50"
430
- >
431
- <svg className="w-5 h-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>
432
- </button>
433
- )}
434
- {editing && (
435
- <>
436
- <button
437
- onClick={() => {
438
- setEditing(false);
439
- setEditForm({
440
- title: task.title,
441
- description: task.description || "",
442
- type: task.type as "once" | "recurring",
443
- priority: task.priority,
444
- execute_at: task.execute_at ? new Date(task.execute_at).toISOString().slice(0, 16) : "",
445
- recurrence: task.recurrence || "",
446
- });
447
- }}
448
- className="text-[var(--color-text-muted)] hover:text-[var(--color-text)] text-sm transition"
449
- >
450
- Cancel
451
- </button>
452
- <button
453
- onClick={handleSave}
454
- disabled={saving || !editForm.title.trim()}
455
- className="px-3 py-1 rounded text-sm bg-[var(--color-accent)] text-black hover:opacity-90 transition disabled:opacity-50"
456
- >
457
- {saving ? "Saving..." : "Save"}
458
- </button>
459
- </>
460
- )}
461
- {!editing && (
462
- <button onClick={onClose} className="text-[var(--color-text-muted)] hover:text-[var(--color-text)] transition">
463
- <CloseIcon />
464
- </button>
465
- )}
466
- </div>
467
- </div>
468
-
469
- {/* Content */}
470
- <div className="flex-1 overflow-auto p-4 space-y-4">
471
- {/* Title & Status */}
472
- <div>
473
- <div className="flex items-start justify-between gap-2 mb-2">
474
- {editing ? (
475
- <input
476
- type="text"
477
- value={editForm.title}
478
- onChange={e => setEditForm({ ...editForm, title: e.target.value })}
479
- className={`${inputClass} text-lg font-medium`}
480
- placeholder="Task title"
481
- />
482
- ) : (
483
- <h3 className="text-lg font-medium">{task.title}</h3>
484
- )}
485
- {!editing && (
486
- <span className={`px-2 py-1 rounded text-xs font-medium flex-shrink-0 ${statusColors[task.status]}`}>
487
- {task.status}
488
- </span>
489
- )}
490
- </div>
491
- {!editing && (
492
- <button
493
- onClick={() => onSelectAgent?.(task.agentId)}
494
- className="text-sm text-[var(--color-accent)] hover:underline"
495
- >
496
- {task.agentName}
497
- </button>
498
- )}
499
- </div>
500
-
501
- {/* Description */}
502
- {editing ? (
503
- <div>
504
- <h4 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1">Description</h4>
505
- <textarea
506
- value={editForm.description}
507
- onChange={e => setEditForm({ ...editForm, description: e.target.value })}
508
- className={`${inputClass} resize-none`}
509
- rows={3}
510
- placeholder="Task description..."
511
- />
512
- </div>
513
- ) : task.description ? (
514
- <div>
515
- <h4 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1">Description</h4>
516
- <p className="text-sm text-[var(--color-text-secondary)] whitespace-pre-wrap">{task.description}</p>
517
- </div>
518
- ) : null}
519
-
520
- {/* Metadata */}
521
- {editing ? (
522
- <div className="grid grid-cols-2 gap-3">
523
- <div>
524
- <label className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1 block">Type</label>
525
- <select
526
- value={editForm.type}
527
- onChange={e => setEditForm({ ...editForm, type: e.target.value as "once" | "recurring" })}
528
- className={inputClass}
529
- >
530
- <option value="once">One-time</option>
531
- <option value="recurring">Recurring</option>
532
- </select>
533
- </div>
534
- <div>
535
- <label className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1 block">Priority</label>
536
- <input
537
- type="number"
538
- min={1}
539
- max={10}
540
- value={editForm.priority}
541
- onChange={e => setEditForm({ ...editForm, priority: Number(e.target.value) })}
542
- className={inputClass}
543
- />
544
- </div>
545
- </div>
546
- ) : (
547
- <div className="grid grid-cols-2 gap-3 text-sm">
548
- <div>
549
- <span className="text-[var(--color-text-muted)]">Type</span>
550
- <p className="capitalize">{task.type}</p>
551
- </div>
552
- <div>
553
- <span className="text-[var(--color-text-muted)]">Priority</span>
554
- <p>{task.priority}</p>
555
- </div>
556
- <div>
557
- <span className="text-[var(--color-text-muted)]">Source</span>
558
- <p className="capitalize">{task.source}</p>
559
- </div>
560
- {task.recurrence && (
561
- <div>
562
- <span className="text-[var(--color-text-muted)]">Recurrence</span>
563
- <p>{formatCron(task.recurrence)}</p>
564
- <p className="text-xs text-[var(--color-text-faint)] mt-0.5 font-mono">{task.recurrence}</p>
565
- </div>
566
- )}
567
- </div>
568
- )}
569
-
570
- {/* Schedule (edit mode) */}
571
- {editing && editForm.type === "once" && (
572
- <div>
573
- <label className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1 block">Schedule</label>
574
- <input
575
- type="datetime-local"
576
- value={editForm.execute_at}
577
- onChange={e => setEditForm({ ...editForm, execute_at: e.target.value })}
578
- className={inputClass}
579
- />
580
- <p className="text-xs text-[var(--color-text-faint)] mt-1">Leave empty for manual execution</p>
581
- </div>
582
- )}
583
- {editing && editForm.type === "recurring" && (
584
- <div>
585
- <label className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1 block">Cron Schedule</label>
586
- <input
587
- type="text"
588
- value={editForm.recurrence}
589
- onChange={e => setEditForm({ ...editForm, recurrence: e.target.value })}
590
- className={`${inputClass} font-mono`}
591
- placeholder="*/30 * * * *"
592
- />
593
- <p className="text-xs text-[var(--color-text-faint)] mt-1">e.g. */30 * * * * = every 30 min</p>
594
- </div>
595
- )}
596
-
597
- {/* Timestamps (view mode only) */}
598
- {!editing && (
599
- <div className="space-y-2 text-sm">
600
- <div className="flex justify-between">
601
- <span className="text-[var(--color-text-muted)]">Created</span>
602
- <span>{new Date(task.created_at).toLocaleString()}</span>
603
- </div>
604
- {task.execute_at && (
605
- <div className="flex justify-between">
606
- <span className="text-[var(--color-text-muted)]">Scheduled</span>
607
- <span className="text-[var(--color-accent)]">{formatRelativeTime(task.execute_at)}</span>
608
- </div>
609
- )}
610
- {task.executed_at && (
611
- <div className="flex justify-between">
612
- <span className="text-[var(--color-text-muted)]">Started</span>
613
- <span>{new Date(task.executed_at).toLocaleString()}</span>
614
- </div>
615
- )}
616
- {task.completed_at && (
617
- <div className="flex justify-between">
618
- <span className="text-[var(--color-text-muted)]">Completed</span>
619
- <span>{new Date(task.completed_at).toLocaleString()}</span>
620
- </div>
621
- )}
622
- {task.next_run && (
623
- <div className="flex justify-between">
624
- <span className="text-[var(--color-text-muted)]">Next Run</span>
625
- <span className="text-[var(--color-accent)]">{formatRelativeTime(task.next_run)}</span>
626
- </div>
627
- )}
628
- </div>
629
- )}
630
-
631
- {/* Error */}
632
- {task.status === "failed" && task.error && (
633
- <div className="min-w-0">
634
- <h4 className="text-xs text-red-400 uppercase tracking-wider mb-1">Error</h4>
635
- <div className="bg-red-500/10 border border-red-500/20 rounded p-3 overflow-x-auto">
636
- <pre className="text-sm text-red-400 whitespace-pre-wrap break-words">{task.error}</pre>
637
- </div>
638
- </div>
639
- )}
640
-
641
- {/* Result */}
642
- {task.status === "completed" && task.result && (
643
- <div className="min-w-0">
644
- <h4 className="text-xs text-green-400 uppercase tracking-wider mb-1">Result</h4>
645
- <div className="bg-green-500/10 border border-green-500/20 rounded p-3 overflow-x-auto">
646
- <pre className="text-sm text-green-400 whitespace-pre-wrap break-words">
647
- {typeof task.result === "string" ? task.result : JSON.stringify(task.result, null, 2)}
648
- </pre>
649
- </div>
650
- </div>
651
- )}
652
-
653
- {/* Trajectory */}
654
- {loading && !task.trajectory && (
655
- <div>
656
- <h4 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-2">Trajectory</h4>
657
- <div className="text-sm text-[var(--color-text-faint)]">Loading trajectory...</div>
658
- </div>
659
- )}
660
- {task.trajectory && task.trajectory.length > 0 && (
661
- <div>
662
- <h4 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-2">
663
- Trajectory ({task.trajectory.length} steps)
664
- </h4>
665
- <TrajectoryView trajectory={task.trajectory} />
666
- </div>
667
- )}
668
- </div>
669
- </div>
670
- );
671
- }
672
-
673
- export function TrajectoryView({ trajectory }: { trajectory: TaskTrajectoryStep[] }) {
674
- const [expanded, setExpanded] = useState<Set<string>>(new Set());
675
-
676
- const toggleStep = (id: string) => {
677
- setExpanded(prev => {
678
- const next = new Set(prev);
679
- if (next.has(id)) {
680
- next.delete(id);
681
- } else {
682
- next.add(id);
683
- }
684
- return next;
685
- });
686
- };
687
-
688
- const roleStyles = {
689
- user: { bg: "bg-blue-500/10", text: "text-blue-400", icon: "👤", label: "User" },
690
- assistant: { bg: "bg-purple-500/10", text: "text-purple-400", icon: "🤖", label: "Assistant" },
691
- };
692
-
693
- // Render content which can be string or array of blocks
694
- const renderContent = (step: TaskTrajectoryStep) => {
695
- const content = step.content;
696
-
697
- // String content (text message)
698
- if (typeof content === "string") {
699
- const isLong = content.length > 200;
700
- const isExpanded = expanded.has(step.id);
701
-
702
- return (
703
- <div>
704
- <p className={`text-sm text-[var(--color-text)] whitespace-pre-wrap break-words ${!isExpanded && isLong ? 'line-clamp-4' : ''}`}>
705
- {content}
706
- </p>
707
- {isLong && (
708
- <button
709
- onClick={() => toggleStep(step.id)}
710
- className="text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] mt-1"
711
- >
712
- {isExpanded ? "Show less" : "Show more..."}
713
- </button>
714
- )}
715
- </div>
716
- );
717
- }
718
-
719
- // Array content (tool_use or tool_result blocks)
720
- return (
721
- <div className="space-y-2">
722
- {content.map((block, idx) => {
723
- if (block.type === "tool_use") {
724
- const inputStr = JSON.stringify(block.input, null, 2);
725
- const isLong = inputStr.length > 150;
726
- const blockId = `${step.id}-${idx}`;
727
- const isExpanded = expanded.has(blockId);
728
-
729
- return (
730
- <div key={idx} className="bg-orange-500/10 border border-orange-500/20 rounded p-2">
731
- <div className="flex items-center gap-2 mb-1">
732
- <span className="text-orange-400">🔧</span>
733
- <span className="text-xs font-medium text-orange-400">Tool Call</span>
734
- <span className="text-xs text-[var(--color-text-secondary)]">{block.name}</span>
735
- </div>
736
- <pre className={`text-xs text-[var(--color-text-secondary)] overflow-x-auto ${!isExpanded && isLong ? 'line-clamp-3' : ''}`}>
737
- {inputStr}
738
- </pre>
739
- {isLong && (
740
- <button
741
- onClick={() => toggleStep(blockId)}
742
- className="text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] mt-1"
743
- >
744
- {isExpanded ? "Show less" : "Show more..."}
745
- </button>
746
- )}
747
- </div>
748
- );
749
- }
750
-
751
- if (block.type === "tool_result") {
752
- const isError = block.is_error;
753
- const blockId = `${step.id}-${idx}`;
754
- const isExpanded = expanded.has(blockId);
755
- const isLong = block.content.length > 150;
756
-
757
- return (
758
- <div
759
- key={idx}
760
- className={`${isError ? 'bg-red-500/10 border-red-500/20' : 'bg-teal-500/10 border-teal-500/20'} border rounded p-2`}
761
- >
762
- <div className="flex items-center gap-2 mb-1">
763
- <span>{isError ? "❌" : "📋"}</span>
764
- <span className={`text-xs font-medium ${isError ? 'text-red-400' : 'text-teal-400'}`}>
765
- Tool Result
766
- </span>
767
- </div>
768
- <pre className={`text-xs text-[var(--color-text-secondary)] overflow-x-auto whitespace-pre-wrap break-words ${!isExpanded && isLong ? 'line-clamp-3' : ''}`}>
769
- {block.content}
770
- </pre>
771
- {isLong && (
772
- <button
773
- onClick={() => toggleStep(blockId)}
774
- className="text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)] mt-1"
775
- >
776
- {isExpanded ? "Show less" : "Show more..."}
777
- </button>
778
- )}
779
- </div>
780
- );
781
- }
782
-
783
- return null;
784
- })}
785
- </div>
786
- );
787
- };
788
-
789
- return (
790
- <div className="space-y-2">
791
- {trajectory.map((step) => {
792
- const style = roleStyles[step.role] || roleStyles.assistant;
793
-
794
- return (
795
- <div
796
- key={step.id}
797
- className={`${style.bg} border border-[var(--color-border)] rounded overflow-hidden p-3`}
798
- >
799
- <div className="flex items-center gap-2 mb-2">
800
- <span>{style.icon}</span>
801
- <span className={`text-xs font-medium ${style.text}`}>{style.label}</span>
802
- {step.model && (
803
- <span className="text-xs text-[var(--color-text-faint)]">· {step.model}</span>
804
- )}
805
- <span className="text-xs text-[var(--color-text-faint)]">
806
- · {new Date(step.created_at).toLocaleTimeString()}
807
- </span>
808
- </div>
809
- {renderContent(step)}
810
- </div>
811
- );
812
- })}
813
- </div>
814
- );
815
- }
816
-
817
- // --- Create Task Modal ---
818
-
819
- interface CreateTaskModalProps {
820
- authFetch: (url: string, options?: RequestInit) => Promise<Response>;
821
- currentProjectId: string | null;
822
- onClose: () => void;
823
- onCreated: () => void;
824
- }
825
-
826
- function CreateTaskModal({ authFetch, currentProjectId, onClose, onCreated }: CreateTaskModalProps) {
827
- const [agents, setAgents] = useState<{ id: string; name: string }[]>([]);
828
- const [agentId, setAgentId] = useState("");
829
- const [title, setTitle] = useState("");
830
- const [description, setDescription] = useState("");
831
- const [type, setType] = useState<"once" | "recurring">("once");
832
- const [priority, setPriority] = useState(5);
833
- const [executeAt, setExecuteAt] = useState("");
834
- const [recurrence, setRecurrence] = useState("");
835
- const [creating, setCreating] = useState(false);
836
- const [error, setError] = useState("");
837
-
838
- useEffect(() => {
839
- authFetch("/api/agents").then(r => r.json()).then(data => {
840
- const running = (data.agents || []).filter((a: Agent) => a.status === "running" && a.features?.tasks);
841
- setAgents(running.map((a: Agent) => ({ id: a.id, name: a.name })));
842
- if (running.length === 1) setAgentId(running[0].id);
843
- }).catch(() => {});
844
- }, [authFetch]);
845
-
846
- const handleSubmit = async (e: React.FormEvent) => {
847
- e.preventDefault();
848
- if (!agentId || !title.trim()) return;
849
- setCreating(true);
850
- setError("");
851
-
852
- const body: Record<string, unknown> = {
853
- title: title.trim(),
854
- description: description.trim() || undefined,
855
- type,
856
- priority,
857
- };
858
- if (type === "once" && executeAt) {
859
- body.execute_at = new Date(executeAt).toISOString();
860
- }
861
- if (type === "recurring" && recurrence.trim()) {
862
- body.recurrence = recurrence.trim();
863
- }
864
-
865
- try {
866
- const res = await authFetch(`/api/tasks/${agentId}`, {
867
- method: "POST",
868
- headers: { "Content-Type": "application/json" },
869
- body: JSON.stringify(body),
870
- });
871
- if (!res.ok) {
872
- const data = await res.json().catch(() => ({}));
873
- setError(data.error || `HTTP ${res.status}`);
874
- return;
875
- }
876
- onCreated();
877
- } catch (err) {
878
- setError(String(err));
879
- } finally {
880
- setCreating(false);
881
- }
882
- };
883
-
884
- return (
885
- <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={onClose}>
886
- <div className="bg-[var(--color-surface)] card w-full max-w-md" onClick={e => e.stopPropagation()}>
887
- <div className="flex items-center justify-between p-4 border-b border-[var(--color-border)]">
888
- <h2 className="font-medium">Create Task</h2>
889
- <button onClick={onClose} className="text-[var(--color-text-muted)] hover:text-[var(--color-text)] transition">
890
- <CloseIcon />
891
- </button>
892
- </div>
893
- <form onSubmit={handleSubmit} className="p-4 space-y-4">
894
- {/* Agent */}
895
- <div>
896
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Agent</label>
897
- {agents.length === 0 ? (
898
- <p className="text-sm text-[var(--color-text-faint)]">No running agents with tasks enabled</p>
899
- ) : (
900
- <select
901
- value={agentId}
902
- onChange={e => setAgentId(e.target.value)}
903
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border)] rounded px-3 py-2 text-sm"
904
- required
905
- >
906
- <option value="">Select agent...</option>
907
- {agents.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
908
- </select>
909
- )}
910
- </div>
911
-
912
- {/* Title */}
913
- <div>
914
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Title</label>
915
- <input
916
- type="text"
917
- value={title}
918
- onChange={e => setTitle(e.target.value)}
919
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border)] rounded px-3 py-2 text-sm"
920
- placeholder="e.g. Check email for new orders"
921
- required
922
- />
923
- </div>
924
-
925
- {/* Description */}
926
- <div>
927
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Description</label>
928
- <textarea
929
- value={description}
930
- onChange={e => setDescription(e.target.value)}
931
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border)] rounded px-3 py-2 text-sm resize-none"
932
- rows={2}
933
- placeholder="Optional instructions for the agent..."
934
- />
935
- </div>
936
-
937
- {/* Type & Priority */}
938
- <div className="grid grid-cols-2 gap-3">
939
- <div>
940
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Type</label>
941
- <select
942
- value={type}
943
- onChange={e => setType(e.target.value as "once" | "recurring")}
944
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border)] rounded px-3 py-2 text-sm"
945
- >
946
- <option value="once">One-time</option>
947
- <option value="recurring">Recurring</option>
948
- </select>
949
- </div>
950
- <div>
951
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Priority</label>
952
- <input
953
- type="number"
954
- min={1}
955
- max={10}
956
- value={priority}
957
- onChange={e => setPriority(Number(e.target.value))}
958
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border)] rounded px-3 py-2 text-sm"
959
- />
960
- </div>
961
- </div>
962
-
963
- {/* Schedule */}
964
- {type === "once" && (
965
- <div>
966
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Schedule (optional)</label>
967
- <input
968
- type="datetime-local"
969
- value={executeAt}
970
- onChange={e => setExecuteAt(e.target.value)}
971
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border)] rounded px-3 py-2 text-sm"
972
- />
973
- <p className="text-xs text-[var(--color-text-faint)] mt-1">Leave empty to execute immediately</p>
974
- </div>
975
- )}
976
-
977
- {type === "recurring" && (
978
- <div>
979
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">Cron Schedule</label>
980
- <input
981
- type="text"
982
- value={recurrence}
983
- onChange={e => setRecurrence(e.target.value)}
984
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border)] rounded px-3 py-2 text-sm font-mono"
985
- placeholder="*/30 * * * *"
986
- required
987
- />
988
- <p className="text-xs text-[var(--color-text-faint)] mt-1">e.g. */30 * * * * = every 30 min, 0 9 * * 1-5 = weekdays at 9am</p>
989
- </div>
990
- )}
991
-
992
- {error && (
993
- <p className="text-sm text-red-400">{error}</p>
994
- )}
995
-
996
- <div className="flex justify-end gap-2 pt-2">
997
- <button
998
- type="button"
999
- onClick={onClose}
1000
- className="px-4 py-2 rounded text-sm bg-[var(--color-surface-raised)] hover:bg-[var(--color-border)] transition"
1001
- >
1002
- Cancel
1003
- </button>
1004
- <button
1005
- type="submit"
1006
- disabled={creating || !agentId || !title.trim() || agents.length === 0}
1007
- className="px-4 py-2 rounded text-sm bg-[var(--color-accent)] text-black hover:opacity-90 transition disabled:opacity-50"
1008
- >
1009
- {creating ? "Creating..." : "Create Task"}
1010
- </button>
1011
- </div>
1012
- </form>
1013
- </div>
1014
- </div>
1015
- );
1016
- }
1017
-
1018
- // --- Schedule formatting helpers ---
1019
-
1020
- const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1021
-
1022
- export function formatCron(cron: string): string {
1023
- try {
1024
- const parts = cron.trim().split(/\s+/);
1025
- if (parts.length !== 5) return cron;
1026
- const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
1027
-
1028
- // Every N minutes: */N * * * *
1029
- if (minute.startsWith("*/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
1030
- const n = parseInt(minute.slice(2));
1031
- if (n === 1) return "Every minute";
1032
- return `Every ${n} minutes`;
1033
- }
1034
-
1035
- // Every hour: 0 * * * *
1036
- if (minute !== "*" && !minute.includes("/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
1037
- return "Every hour";
1038
- }
1039
-
1040
- // Every N hours: 0 */N * * *
1041
- if (hour.startsWith("*/") && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
1042
- const n = parseInt(hour.slice(2));
1043
- if (n === 1) return "Every hour";
1044
- return `Every ${n} hours`;
1045
- }
1046
-
1047
- const formatTime = (h: string, m: string): string => {
1048
- const hr = parseInt(h);
1049
- const mn = parseInt(m);
1050
- if (isNaN(hr)) return "";
1051
- const ampm = hr >= 12 ? "PM" : "AM";
1052
- const h12 = hr === 0 ? 12 : hr > 12 ? hr - 12 : hr;
1053
- return `${h12}:${mn.toString().padStart(2, "0")} ${ampm}`;
1054
- };
1055
-
1056
- if (hour !== "*" && !hour.includes("/") && dayOfMonth === "*" && month === "*") {
1057
- const timeStr = formatTime(hour, minute);
1058
-
1059
- if (dayOfWeek === "*") return `Daily at ${timeStr}`;
1060
-
1061
- const days = dayOfWeek.split(",").map(d => {
1062
- const num = parseInt(d.trim());
1063
- return DAY_NAMES[num] || d;
1064
- });
1065
-
1066
- if (days.length === 7) return `Daily at ${timeStr}`;
1067
- if (days.length === 5 && !days.includes("Sat") && !days.includes("Sun")) {
1068
- return `Weekdays at ${timeStr}`;
1069
- }
1070
- if (days.length === 1) return `Weekly on ${days[0]} at ${timeStr}`;
1071
- return `${days.join(" & ")} at ${timeStr}`;
1072
- }
1073
-
1074
- return cron;
1075
- } catch {
1076
- return cron;
1077
- }
1078
- }
1079
-
1080
- export function formatRelativeTime(dateStr: string): string {
1081
- const date = new Date(dateStr);
1082
- const now = new Date();
1083
- const diffMs = date.getTime() - now.getTime();
1084
- const absDiffMs = Math.abs(diffMs);
1085
- const isFuture = diffMs > 0;
1086
-
1087
- const minutes = Math.floor(absDiffMs / 60000);
1088
- const hours = Math.floor(absDiffMs / 3600000);
1089
- const days = Math.floor(absDiffMs / 86400000);
1090
-
1091
- const timeStr = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
1092
-
1093
- const isToday = date.toDateString() === now.toDateString();
1094
- const tomorrow = new Date(now);
1095
- tomorrow.setDate(tomorrow.getDate() + 1);
1096
- const isTomorrow = date.toDateString() === tomorrow.toDateString();
1097
- const yesterday = new Date(now);
1098
- yesterday.setDate(yesterday.getDate() - 1);
1099
- const isYesterday = date.toDateString() === yesterday.toDateString();
1100
-
1101
- if (isToday) {
1102
- if (minutes < 1) return isFuture ? "now" : "just now";
1103
- if (minutes < 60) return isFuture ? `in ${minutes} min (${timeStr})` : `${minutes} min ago`;
1104
- return isFuture ? `in ${hours}h (${timeStr})` : `${hours}h ago`;
1105
- }
1106
-
1107
- if (isTomorrow) return `Tomorrow at ${timeStr}`;
1108
- if (isYesterday) return `Yesterday at ${timeStr}`;
1109
-
1110
- if (days < 7) {
1111
- const dayName = DAY_NAMES[date.getDay()];
1112
- return `${dayName} at ${timeStr}`;
1113
- }
1114
-
1115
- return date.toLocaleDateString([], { month: "short", day: "numeric" }) + ` at ${timeStr}`;
1116
- }