apteva 0.4.3 → 0.4.5

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.
@@ -1,7 +1,8 @@
1
- import React, { useState, useEffect, useCallback } from "react";
1
+ import React, { useState, useEffect, useCallback, useRef } from "react";
2
2
  import { TasksIcon, CloseIcon } from "../common/Icons";
3
3
  import { useAuth } from "../../context";
4
- import type { Task, TaskTrajectoryStep } from "../../types";
4
+ import { useTelemetry } from "../../context/TelemetryContext";
5
+ import type { Task, TaskTrajectoryStep, ToolUseBlock, ToolResultBlock } from "../../types";
5
6
 
6
7
  interface TasksPageProps {
7
8
  onSelectAgent?: (agentId: string) => void;
@@ -13,6 +14,11 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
13
14
  const [loading, setLoading] = useState(true);
14
15
  const [filter, setFilter] = useState<string>("all");
15
16
  const [selectedTask, setSelectedTask] = useState<Task | null>(null);
17
+ const [loadingTask, setLoadingTask] = useState(false);
18
+ const lastProcessedEventRef = useRef<string | null>(null);
19
+
20
+ // Subscribe to task telemetry events for real-time updates
21
+ const { events: taskEvents } = useTelemetry({ category: "TASK" });
16
22
 
17
23
  const fetchTasks = useCallback(async () => {
18
24
  try {
@@ -26,13 +32,56 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
26
32
  }
27
33
  }, [authFetch, filter]);
28
34
 
35
+ // Initial fetch
29
36
  useEffect(() => {
30
37
  fetchTasks();
31
- // Refresh every 10 seconds
32
- const interval = setInterval(fetchTasks, 10000);
33
- return () => clearInterval(interval);
34
38
  }, [fetchTasks]);
35
39
 
40
+ // Handle real-time task events from telemetry - use as trigger to refetch
41
+ // since telemetry data is incomplete (missing id, agentId, status, etc.)
42
+ useEffect(() => {
43
+ if (!taskEvents.length) return;
44
+
45
+ const latestEvent = taskEvents[0];
46
+ if (!latestEvent || latestEvent.id === lastProcessedEventRef.current) return;
47
+
48
+ // Only react to task mutation events
49
+ const eventType = latestEvent.type;
50
+ if (eventType === "task_created" || eventType === "task_updated" || eventType === "task_deleted") {
51
+ lastProcessedEventRef.current = latestEvent.id;
52
+ console.log("[TasksPage] Telemetry event:", eventType);
53
+ // Refetch to get complete task data
54
+ fetchTasks();
55
+ }
56
+ }, [taskEvents, fetchTasks]);
57
+
58
+ // Fetch full task details (including trajectory) when selecting a task
59
+ const selectTask = useCallback(async (task: Task) => {
60
+ // Set task immediately for quick feedback
61
+ setSelectedTask(task);
62
+ setLoadingTask(true);
63
+
64
+ try {
65
+ const res = await authFetch(`/api/tasks/${task.agentId}/${task.id}`);
66
+ console.log("[TasksPage] Fetch task response status:", res.status);
67
+ if (res.ok) {
68
+ const data = await res.json();
69
+ console.log("[TasksPage] Task data:", data);
70
+ console.log("[TasksPage] Has trajectory:", !!data.task?.trajectory, "Length:", data.task?.trajectory?.length);
71
+ if (data.task) {
72
+ // Merge with agentId/agentName since API might not include them
73
+ setSelectedTask({ ...data.task, agentId: task.agentId, agentName: task.agentName });
74
+ }
75
+ } else {
76
+ console.error("[TasksPage] Failed to fetch task:", res.status, await res.text());
77
+ }
78
+ } catch (e) {
79
+ console.error("Failed to fetch task details:", e);
80
+ } finally {
81
+ setLoadingTask(false);
82
+ }
83
+ }, [authFetch]);
84
+
36
85
  const statusColors: Record<string, string> = {
37
86
  pending: "bg-yellow-500/20 text-yellow-400",
38
87
  running: "bg-blue-500/20 text-blue-400",
@@ -93,7 +142,7 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
93
142
  {tasks.map(task => (
94
143
  <div
95
144
  key={`${task.agentId}-${task.id}`}
96
- onClick={() => setSelectedTask(task)}
145
+ onClick={() => selectTask(task)}
97
146
  className={`bg-[#111] border rounded-lg p-4 cursor-pointer transition ${
98
147
  selectedTask?.id === task.id && selectedTask?.agentId === task.agentId
99
148
  ? "border-[#f97316]"
@@ -143,6 +192,7 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
143
192
  statusColors={statusColors}
144
193
  onClose={() => setSelectedTask(null)}
145
194
  onSelectAgent={onSelectAgent}
195
+ loading={loadingTask}
146
196
  />
147
197
  )}
148
198
  </div>
@@ -154,9 +204,10 @@ interface TaskDetailPanelProps {
154
204
  statusColors: Record<string, string>;
155
205
  onClose: () => void;
156
206
  onSelectAgent?: (agentId: string) => void;
207
+ loading?: boolean;
157
208
  }
158
209
 
159
- function TaskDetailPanel({ task, statusColors, onClose, onSelectAgent }: TaskDetailPanelProps) {
210
+ function TaskDetailPanel({ task, statusColors, onClose, onSelectAgent, loading }: TaskDetailPanelProps) {
160
211
  return (
161
212
  <div className="w-full md:w-1/2 lg:w-1/3 border-l border-[#1a1a1a] bg-[#0a0a0a] flex flex-col overflow-hidden">
162
213
  {/* Header */}
@@ -249,27 +300,33 @@ function TaskDetailPanel({ task, statusColors, onClose, onSelectAgent }: TaskDet
249
300
 
250
301
  {/* Error */}
251
302
  {task.status === "failed" && task.error && (
252
- <div>
303
+ <div className="min-w-0">
253
304
  <h4 className="text-xs text-red-400 uppercase tracking-wider mb-1">Error</h4>
254
- <div className="bg-red-500/10 border border-red-500/20 rounded p-3">
255
- <p className="text-sm text-red-400 whitespace-pre-wrap">{task.error}</p>
305
+ <div className="bg-red-500/10 border border-red-500/20 rounded p-3 overflow-x-auto">
306
+ <pre className="text-sm text-red-400 whitespace-pre-wrap break-words">{task.error}</pre>
256
307
  </div>
257
308
  </div>
258
309
  )}
259
310
 
260
311
  {/* Result */}
261
312
  {task.status === "completed" && task.result && (
262
- <div>
313
+ <div className="min-w-0">
263
314
  <h4 className="text-xs text-green-400 uppercase tracking-wider mb-1">Result</h4>
264
- <div className="bg-green-500/10 border border-green-500/20 rounded p-3">
265
- <p className="text-sm text-green-400 whitespace-pre-wrap">
315
+ <div className="bg-green-500/10 border border-green-500/20 rounded p-3 overflow-x-auto">
316
+ <pre className="text-sm text-green-400 whitespace-pre-wrap break-words">
266
317
  {typeof task.result === "string" ? task.result : JSON.stringify(task.result, null, 2)}
267
- </p>
318
+ </pre>
268
319
  </div>
269
320
  </div>
270
321
  )}
271
322
 
272
323
  {/* Trajectory */}
324
+ {loading && !task.trajectory && (
325
+ <div>
326
+ <h4 className="text-xs text-[#666] uppercase tracking-wider mb-2">Trajectory</h4>
327
+ <div className="text-sm text-[#555]">Loading trajectory...</div>
328
+ </div>
329
+ )}
273
330
  {task.trajectory && task.trajectory.length > 0 && (
274
331
  <div>
275
332
  <h4 className="text-xs text-[#666] uppercase tracking-wider mb-2">
@@ -284,70 +341,142 @@ function TaskDetailPanel({ task, statusColors, onClose, onSelectAgent }: TaskDet
284
341
  }
285
342
 
286
343
  function TrajectoryView({ trajectory }: { trajectory: TaskTrajectoryStep[] }) {
287
- const [expanded, setExpanded] = useState<Set<number>>(new Set());
344
+ const [expanded, setExpanded] = useState<Set<string>>(new Set());
288
345
 
289
- const toggleStep = (index: number) => {
346
+ const toggleStep = (id: string) => {
290
347
  setExpanded(prev => {
291
348
  const next = new Set(prev);
292
- if (next.has(index)) {
293
- next.delete(index);
349
+ if (next.has(id)) {
350
+ next.delete(id);
294
351
  } else {
295
- next.add(index);
352
+ next.add(id);
296
353
  }
297
354
  return next;
298
355
  });
299
356
  };
300
357
 
301
- const stepColors: Record<string, { bg: string; text: string; icon: string }> = {
302
- thought: { bg: "bg-purple-500/10", text: "text-purple-400", icon: "💭" },
303
- action: { bg: "bg-blue-500/10", text: "text-blue-400", icon: "" },
304
- observation: { bg: "bg-green-500/10", text: "text-green-400", icon: "👁" },
305
- tool_call: { bg: "bg-orange-500/10", text: "text-orange-400", icon: "🔧" },
306
- tool_result: { bg: "bg-teal-500/10", text: "text-teal-400", icon: "📋" },
307
- message: { bg: "bg-gray-500/10", text: "text-gray-400", icon: "💬" },
358
+ const roleStyles = {
359
+ user: { bg: "bg-blue-500/10", text: "text-blue-400", icon: "👤", label: "User" },
360
+ assistant: { bg: "bg-purple-500/10", text: "text-purple-400", icon: "🤖", label: "Assistant" },
308
361
  };
309
362
 
310
- return (
311
- <div className="space-y-2">
312
- {trajectory.map((step, index) => {
313
- const colors = stepColors[step.type] || stepColors.message;
314
- const isExpanded = expanded.has(index);
315
- const isLong = step.content.length > 150;
363
+ // Render content which can be string or array of blocks
364
+ const renderContent = (step: TaskTrajectoryStep) => {
365
+ const content = step.content;
316
366
 
317
- return (
318
- <div
319
- key={index}
320
- className={`${colors.bg} border border-[#1a1a1a] rounded overflow-hidden`}
321
- >
367
+ // String content (text message)
368
+ if (typeof content === "string") {
369
+ const isLong = content.length > 200;
370
+ const isExpanded = expanded.has(step.id);
371
+
372
+ return (
373
+ <div>
374
+ <p className={`text-sm text-[#ccc] whitespace-pre-wrap break-words ${!isExpanded && isLong ? 'line-clamp-4' : ''}`}>
375
+ {content}
376
+ </p>
377
+ {isLong && (
322
378
  <button
323
- onClick={() => isLong && toggleStep(index)}
324
- className={`w-full p-2 text-left flex items-start gap-2 ${isLong ? 'cursor-pointer' : ''}`}
379
+ onClick={() => toggleStep(step.id)}
380
+ className="text-xs text-[#666] hover:text-[#888] mt-1"
325
381
  >
326
- <span className="flex-shrink-0">{colors.icon}</span>
327
- <div className="flex-1 min-w-0">
382
+ {isExpanded ? "Show less" : "Show more..."}
383
+ </button>
384
+ )}
385
+ </div>
386
+ );
387
+ }
388
+
389
+ // Array content (tool_use or tool_result blocks)
390
+ return (
391
+ <div className="space-y-2">
392
+ {content.map((block, idx) => {
393
+ if (block.type === "tool_use") {
394
+ const inputStr = JSON.stringify(block.input, null, 2);
395
+ const isLong = inputStr.length > 150;
396
+ const blockId = `${step.id}-${idx}`;
397
+ const isExpanded = expanded.has(blockId);
398
+
399
+ return (
400
+ <div key={idx} className="bg-orange-500/10 border border-orange-500/20 rounded p-2">
328
401
  <div className="flex items-center gap-2 mb-1">
329
- <span className={`text-xs font-medium ${colors.text} capitalize`}>
330
- {step.type.replace("_", " ")}
331
- </span>
332
- {step.tool && (
333
- <span className="text-xs text-[#666]">· {step.tool}</span>
334
- )}
335
- {step.timestamp && (
336
- <span className="text-xs text-[#555]">
337
- · {new Date(step.timestamp).toLocaleTimeString()}
338
- </span>
339
- )}
402
+ <span className="text-orange-400">🔧</span>
403
+ <span className="text-xs font-medium text-orange-400">Tool Call</span>
404
+ <span className="text-xs text-[#888]">{block.name}</span>
340
405
  </div>
341
- <p className={`text-sm text-[#888] whitespace-pre-wrap ${!isExpanded && isLong ? 'line-clamp-3' : ''}`}>
342
- {step.content}
343
- </p>
406
+ <pre className={`text-xs text-[#888] overflow-x-auto ${!isExpanded && isLong ? 'line-clamp-3' : ''}`}>
407
+ {inputStr}
408
+ </pre>
344
409
  {isLong && (
345
- <span className="text-xs text-[#666] mt-1 inline-block">
346
- {isExpanded ? "Click to collapse" : "Click to expand..."}
410
+ <button
411
+ onClick={() => toggleStep(blockId)}
412
+ className="text-xs text-[#666] hover:text-[#888] mt-1"
413
+ >
414
+ {isExpanded ? "Show less" : "Show more..."}
415
+ </button>
416
+ )}
417
+ </div>
418
+ );
419
+ }
420
+
421
+ if (block.type === "tool_result") {
422
+ const isError = block.is_error;
423
+ const blockId = `${step.id}-${idx}`;
424
+ const isExpanded = expanded.has(blockId);
425
+ const isLong = block.content.length > 150;
426
+
427
+ return (
428
+ <div
429
+ key={idx}
430
+ className={`${isError ? 'bg-red-500/10 border-red-500/20' : 'bg-teal-500/10 border-teal-500/20'} border rounded p-2`}
431
+ >
432
+ <div className="flex items-center gap-2 mb-1">
433
+ <span>{isError ? "❌" : "📋"}</span>
434
+ <span className={`text-xs font-medium ${isError ? 'text-red-400' : 'text-teal-400'}`}>
435
+ Tool Result
347
436
  </span>
437
+ </div>
438
+ <pre className={`text-xs text-[#888] overflow-x-auto whitespace-pre-wrap break-words ${!isExpanded && isLong ? 'line-clamp-3' : ''}`}>
439
+ {block.content}
440
+ </pre>
441
+ {isLong && (
442
+ <button
443
+ onClick={() => toggleStep(blockId)}
444
+ className="text-xs text-[#666] hover:text-[#888] mt-1"
445
+ >
446
+ {isExpanded ? "Show less" : "Show more..."}
447
+ </button>
348
448
  )}
349
449
  </div>
350
- </button>
450
+ );
451
+ }
452
+
453
+ return null;
454
+ })}
455
+ </div>
456
+ );
457
+ };
458
+
459
+ return (
460
+ <div className="space-y-2">
461
+ {trajectory.map((step) => {
462
+ const style = roleStyles[step.role] || roleStyles.assistant;
463
+
464
+ return (
465
+ <div
466
+ key={step.id}
467
+ className={`${style.bg} border border-[#1a1a1a] rounded overflow-hidden p-3`}
468
+ >
469
+ <div className="flex items-center gap-2 mb-2">
470
+ <span>{style.icon}</span>
471
+ <span className={`text-xs font-medium ${style.text}`}>{style.label}</span>
472
+ {step.model && (
473
+ <span className="text-xs text-[#555]">· {step.model}</span>
474
+ )}
475
+ <span className="text-xs text-[#555]">
476
+ · {new Date(step.created_at).toLocaleTimeString()}
477
+ </span>
478
+ </div>
479
+ {renderContent(step)}
351
480
  </div>
352
481
  );
353
482
  })}
@@ -19,6 +19,7 @@ interface TelemetryContextValue {
19
19
  events: TelemetryEvent[];
20
20
  lastActivityByAgent: Record<string, { timestamp: string; category: string; type: string }>;
21
21
  activeAgents: Record<string, { type: string; expiresAt: number }>;
22
+ statusChangeCounter: number;
22
23
  clearEvents: () => void;
23
24
  }
24
25
 
@@ -31,6 +32,7 @@ export function TelemetryProvider({ children }: { children: React.ReactNode }) {
31
32
  const [events, setEvents] = useState<TelemetryEvent[]>([]);
32
33
  const [lastActivityByAgent, setLastActivityByAgent] = useState<Record<string, { timestamp: string; category: string; type: string }>>({});
33
34
  const [activeAgents, setActiveAgents] = useState<Record<string, { type: string; expiresAt: number }>>({});
35
+ const [statusChangeCounter, setStatusChangeCounter] = useState(0);
34
36
  const eventSourceRef = useRef<EventSource | null>(null);
35
37
  const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
36
38
 
@@ -110,6 +112,11 @@ export function TelemetryProvider({ children }: { children: React.ReactNode }) {
110
112
  }
111
113
  return updated;
112
114
  });
115
+
116
+ // Detect agent status change events (system-emitted)
117
+ if (data.some((e: TelemetryEvent) => e.category === "system" && (e.type === "agent_started" || e.type === "agent_stopped"))) {
118
+ setStatusChangeCounter(c => c + 1);
119
+ }
113
120
  }
114
121
  } catch {
115
122
  // Ignore parse errors (likely keepalive or empty message)
@@ -155,7 +162,7 @@ export function TelemetryProvider({ children }: { children: React.ReactNode }) {
155
162
  }, []);
156
163
 
157
164
  return (
158
- <TelemetryContext.Provider value={{ connected, events, lastActivityByAgent, activeAgents, clearEvents }}>
165
+ <TelemetryContext.Provider value={{ connected, events, lastActivityByAgent, activeAgents, statusChangeCounter, clearEvents }}>
159
166
  {children}
160
167
  </TelemetryContext.Provider>
161
168
  );
@@ -222,3 +229,9 @@ export function useAgentActivity(agentId: string) {
222
229
  type: activity?.type,
223
230
  };
224
231
  }
232
+
233
+ // Hook to trigger agent list refetch on status changes (started/stopped/crashed)
234
+ export function useAgentStatusChange(): number {
235
+ const { statusChangeCounter } = useTelemetryContext();
236
+ return statusChangeCounter;
237
+ }
@@ -1,6 +1,7 @@
1
1
  import { useState, useEffect, useCallback } from "react";
2
2
  import type { Agent, AgentFeatures } from "../types";
3
3
  import { useAuth } from "../context";
4
+ import { useAgentStatusChange } from "../context/TelemetryContext";
4
5
 
5
6
  export function useAgents(enabled: boolean) {
6
7
  const { accessToken } = useAuth();
@@ -22,6 +23,14 @@ export function useAgents(enabled: boolean) {
22
23
  setLoading(false);
23
24
  }, [getHeaders]);
24
25
 
26
+ // Auto-refetch when agents start/stop/crash (via SSE telemetry)
27
+ const statusChangeCounter = useAgentStatusChange();
28
+ useEffect(() => {
29
+ if (enabled && statusChangeCounter > 0) {
30
+ fetchAgents();
31
+ }
32
+ }, [enabled, statusChangeCounter, fetchAgents]);
33
+
25
34
  useEffect(() => {
26
35
  if (enabled) {
27
36
  fetchAgents();
package/src/web/types.ts CHANGED
@@ -145,11 +145,29 @@ export interface OnboardingStatus {
145
145
 
146
146
  export type Route = "dashboard" | "agents" | "tasks" | "mcp" | "skills" | "telemetry" | "settings" | "api";
147
147
 
148
- export interface TaskTrajectoryStep {
149
- type: "thought" | "action" | "observation" | "tool_call" | "tool_result" | "message";
148
+ // Tool use content block in trajectory
149
+ export interface ToolUseBlock {
150
+ type: "tool_use";
151
+ id: string;
152
+ name: string;
153
+ input: Record<string, unknown>;
154
+ }
155
+
156
+ // Tool result content block in trajectory
157
+ export interface ToolResultBlock {
158
+ type: "tool_result";
159
+ tool_use_id: string;
150
160
  content: string;
151
- tool?: string;
152
- timestamp?: string;
161
+ is_error?: boolean;
162
+ }
163
+
164
+ // Trajectory step from the agent API (chat message format)
165
+ export interface TaskTrajectoryStep {
166
+ id: string;
167
+ role: "user" | "assistant";
168
+ content: string | Array<ToolUseBlock | ToolResultBlock>;
169
+ created_at: string;
170
+ model?: string;
153
171
  }
154
172
 
155
173
  export interface Task {