apteva 0.4.57 → 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.
- package/README.md +216 -54
- package/cli.js +35 -0
- package/install.js +92 -0
- package/package.json +12 -79
- package/LICENSE +0 -63
- package/bin/apteva.js +0 -196
- package/dist/ActivityPage.kxzzb4yc.js +0 -3
- package/dist/ApiDocsPage.zq998hbm.js +0 -4
- package/dist/App.55rea8mn.js +0 -61
- package/dist/App.5ywb23z4.js +0 -53
- package/dist/App.6thds120.js +0 -4
- package/dist/App.9tctxzqm.js +0 -8
- package/dist/App.a8r8ttaz.js +0 -4
- package/dist/App.agsv5bje.js +0 -4
- package/dist/App.cepapqmx.js +0 -4
- package/dist/App.dp041gb3.js +0 -221
- package/dist/App.fds72zb5.js +0 -4
- package/dist/App.fg9qj2dq.js +0 -4
- package/dist/App.ndfejbm9.js +0 -4
- package/dist/App.nxmfmq1h.js +0 -13
- package/dist/App.qdfyt8ba.js +0 -4
- package/dist/App.x2d0ygt6.js +0 -4
- package/dist/App.yt9p4nr3.js +0 -20
- package/dist/App.zn4mw16t.js +0 -1
- package/dist/ConnectionsPage.8r96ryw7.js +0 -3
- package/dist/McpPage.3cwh0gnd.js +0 -3
- package/dist/SettingsPage.ykgdh5ev.js +0 -3
- package/dist/SkillsPage.4np1s65b.js +0 -3
- package/dist/TasksPage.4g08t7p6.js +0 -3
- package/dist/TelemetryPage.72w9pwcp.js +0 -3
- package/dist/TestsPage.z4fk3r7r.js +0 -3
- package/dist/ThreadsPage.63tcajeh.js +0 -3
- package/dist/apteva-kit.css +0 -1
- package/dist/icon.png +0 -0
- package/dist/index.html +0 -16
- package/dist/styles.css +0 -1
- package/scripts/postinstall.mjs +0 -102
- package/src/auth/index.ts +0 -394
- package/src/auth/middleware.ts +0 -213
- package/src/binary.ts +0 -536
- package/src/channels/index.ts +0 -40
- package/src/channels/telegram.ts +0 -311
- package/src/crypto.ts +0 -301
- package/src/db-tests.ts +0 -174
- package/src/db.ts +0 -3133
- package/src/integrations/agentdojo.ts +0 -559
- package/src/integrations/composio.ts +0 -437
- package/src/integrations/index.ts +0 -87
- package/src/integrations/skillsmp.ts +0 -318
- package/src/mcp-client.ts +0 -605
- package/src/mcp-handler.ts +0 -394
- package/src/mcp-platform.ts +0 -2403
- package/src/openapi.ts +0 -2410
- package/src/providers.ts +0 -597
- package/src/routes/api/agent-utils.ts +0 -890
- package/src/routes/api/agents.ts +0 -916
- package/src/routes/api/api-keys.ts +0 -95
- package/src/routes/api/channels.ts +0 -182
- package/src/routes/api/helpers.ts +0 -12
- package/src/routes/api/integrations.ts +0 -639
- package/src/routes/api/mcp.ts +0 -574
- package/src/routes/api/meta-agent.ts +0 -195
- package/src/routes/api/projects.ts +0 -112
- package/src/routes/api/providers.ts +0 -424
- package/src/routes/api/skills.ts +0 -537
- package/src/routes/api/system.ts +0 -333
- package/src/routes/api/telemetry.ts +0 -203
- package/src/routes/api/tests.ts +0 -148
- package/src/routes/api/triggers.ts +0 -518
- package/src/routes/api/users.ts +0 -148
- package/src/routes/api/webhooks.ts +0 -171
- package/src/routes/api.ts +0 -53
- package/src/routes/auth.ts +0 -251
- package/src/routes/share.ts +0 -86
- package/src/routes/static.ts +0 -131
- package/src/server.ts +0 -642
- package/src/test-runner.ts +0 -598
- package/src/triggers/agentdojo.ts +0 -253
- package/src/triggers/composio.ts +0 -264
- package/src/triggers/index.ts +0 -71
- package/src/tui/AgentList.tsx +0 -145
- package/src/tui/App.tsx +0 -102
- package/src/tui/Login.tsx +0 -104
- package/src/tui/api.ts +0 -72
- package/src/tui/index.tsx +0 -7
- package/src/web/App.tsx +0 -455
- package/src/web/components/activity/ActivityPage.tsx +0 -314
- package/src/web/components/activity/index.ts +0 -1
- package/src/web/components/agents/AgentCard.tsx +0 -189
- package/src/web/components/agents/AgentPanel.tsx +0 -2244
- package/src/web/components/agents/AgentsView.tsx +0 -180
- package/src/web/components/agents/CreateAgentModal.tsx +0 -475
- package/src/web/components/agents/index.ts +0 -4
- package/src/web/components/api/ApiDocsPage.tsx +0 -842
- package/src/web/components/auth/CreateAccountStep.tsx +0 -176
- package/src/web/components/auth/LoginPage.tsx +0 -91
- package/src/web/components/auth/index.ts +0 -2
- package/src/web/components/common/Icons.tsx +0 -250
- package/src/web/components/common/LoadingSpinner.tsx +0 -44
- package/src/web/components/common/Modal.tsx +0 -199
- package/src/web/components/common/Select.tsx +0 -97
- package/src/web/components/common/index.ts +0 -20
- package/src/web/components/connections/ConnectionsPage.tsx +0 -54
- package/src/web/components/connections/IntegrationsTab.tsx +0 -170
- package/src/web/components/connections/OverviewTab.tsx +0 -137
- package/src/web/components/connections/TriggersTab.tsx +0 -1346
- package/src/web/components/dashboard/Dashboard.tsx +0 -572
- package/src/web/components/dashboard/index.ts +0 -1
- package/src/web/components/index.ts +0 -21
- package/src/web/components/layout/ErrorBanner.tsx +0 -18
- package/src/web/components/layout/Header.tsx +0 -332
- package/src/web/components/layout/Sidebar.tsx +0 -231
- package/src/web/components/layout/index.ts +0 -3
- package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
- package/src/web/components/mcp/McpPage.tsx +0 -2515
- package/src/web/components/mcp/index.ts +0 -1
- package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
- package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
- package/src/web/components/onboarding/index.ts +0 -1
- package/src/web/components/settings/SettingsPage.tsx +0 -2776
- package/src/web/components/settings/index.ts +0 -1
- package/src/web/components/skills/SkillsPage.tsx +0 -1200
- package/src/web/components/tasks/TasksPage.tsx +0 -1116
- package/src/web/components/tasks/index.ts +0 -1
- package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
- package/src/web/components/tests/TestsPage.tsx +0 -594
- package/src/web/components/threads/ThreadsPage.tsx +0 -315
- package/src/web/context/AuthContext.tsx +0 -242
- package/src/web/context/ProjectContext.tsx +0 -214
- package/src/web/context/TelemetryContext.tsx +0 -299
- package/src/web/context/ThemeContext.tsx +0 -90
- package/src/web/context/UIModeContext.tsx +0 -49
- package/src/web/context/index.ts +0 -12
- package/src/web/hooks/index.ts +0 -3
- package/src/web/hooks/useAgents.ts +0 -115
- package/src/web/hooks/useOnboarding.ts +0 -20
- package/src/web/hooks/useProviders.ts +0 -75
- package/src/web/icon.png +0 -0
- package/src/web/index.html +0 -16
- package/src/web/styles.css +0 -118
- package/src/web/themes.ts +0 -162
- 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
|
-
}
|