apteva 0.4.15 → 0.4.17

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,5 +1,5 @@
1
1
  import React, { useState, useEffect, useCallback, useRef } from "react";
2
- import { TasksIcon, CloseIcon } from "../common/Icons";
2
+ import { TasksIcon, CloseIcon, RecurringIcon, ScheduledIcon, TaskOnceIcon } from "../common/Icons";
3
3
  import { useAuth, useProjects } from "../../context";
4
4
  import { useTelemetry } from "../../context/TelemetryContext";
5
5
  import type { Task, TaskTrajectoryStep, ToolUseBlock, ToolResultBlock } from "../../types";
@@ -157,14 +157,7 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
157
157
  <div className="flex items-start justify-between mb-2">
158
158
  <div className="flex-1">
159
159
  <h3 className="font-medium">{task.title}</h3>
160
- <p className="text-sm text-[#666]">
161
- {task.agentName}
162
- {task.execute_at && (
163
- <span className="ml-2">
164
- · Scheduled: {new Date(task.execute_at).toLocaleString()}
165
- </span>
166
- )}
167
- </p>
160
+ <p className="text-sm text-[#666]">{task.agentName}</p>
168
161
  </div>
169
162
  <span className={`px-2 py-1 rounded text-xs font-medium ${statusColors[task.status] || statusColors.pending}`}>
170
163
  {task.status}
@@ -178,10 +171,23 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
178
171
  )}
179
172
 
180
173
  <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-[#555]">
181
- <span>Type: {task.type}</span>
174
+ <span className="flex items-center gap-1">
175
+ {task.type === "recurring"
176
+ ? <RecurringIcon className="w-3.5 h-3.5" />
177
+ : task.execute_at
178
+ ? <ScheduledIcon className="w-3.5 h-3.5" />
179
+ : <TaskOnceIcon className="w-3.5 h-3.5" />
180
+ }
181
+ {task.type === "recurring" && task.recurrence ? formatCron(task.recurrence) : task.type}
182
+ </span>
182
183
  <span>Priority: {task.priority}</span>
183
- {task.recurrence && <span>Recurrence: {task.recurrence}</span>}
184
- <span>Created: {new Date(task.created_at).toLocaleString()}</span>
184
+ {task.next_run && (
185
+ <span className="text-[#f97316]">{formatRelativeTime(task.next_run)}</span>
186
+ )}
187
+ {!task.next_run && task.execute_at && (
188
+ <span className="text-[#f97316]">{formatRelativeTime(task.execute_at)}</span>
189
+ )}
190
+ <span>Created: {new Date(task.created_at).toLocaleDateString()}</span>
185
191
  </div>
186
192
  </div>
187
193
  ))}
@@ -266,7 +272,8 @@ function TaskDetailPanel({ task, statusColors, onClose, onSelectAgent, loading }
266
272
  {task.recurrence && (
267
273
  <div>
268
274
  <span className="text-[#666]">Recurrence</span>
269
- <p>{task.recurrence}</p>
275
+ <p>{formatCron(task.recurrence)}</p>
276
+ <p className="text-xs text-[#444] mt-0.5 font-mono">{task.recurrence}</p>
270
277
  </div>
271
278
  )}
272
279
  </div>
@@ -280,7 +287,7 @@ function TaskDetailPanel({ task, statusColors, onClose, onSelectAgent, loading }
280
287
  {task.execute_at && (
281
288
  <div className="flex justify-between">
282
289
  <span className="text-[#666]">Scheduled</span>
283
- <span>{new Date(task.execute_at).toLocaleString()}</span>
290
+ <span className="text-[#f97316]">{formatRelativeTime(task.execute_at)}</span>
284
291
  </div>
285
292
  )}
286
293
  {task.executed_at && (
@@ -298,7 +305,7 @@ function TaskDetailPanel({ task, statusColors, onClose, onSelectAgent, loading }
298
305
  {task.next_run && (
299
306
  <div className="flex justify-between">
300
307
  <span className="text-[#666]">Next Run</span>
301
- <span>{new Date(task.next_run).toLocaleString()}</span>
308
+ <span className="text-[#f97316]">{formatRelativeTime(task.next_run)}</span>
302
309
  </div>
303
310
  )}
304
311
  </div>
@@ -488,3 +495,103 @@ function TrajectoryView({ trajectory }: { trajectory: TaskTrajectoryStep[] }) {
488
495
  </div>
489
496
  );
490
497
  }
498
+
499
+ // --- Schedule formatting helpers ---
500
+
501
+ const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
502
+
503
+ function formatCron(cron: string): string {
504
+ try {
505
+ const parts = cron.trim().split(/\s+/);
506
+ if (parts.length !== 5) return cron;
507
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
508
+
509
+ // Every N minutes: */N * * * *
510
+ if (minute.startsWith("*/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
511
+ const n = parseInt(minute.slice(2));
512
+ if (n === 1) return "Every minute";
513
+ return `Every ${n} minutes`;
514
+ }
515
+
516
+ // Every hour: 0 * * * *
517
+ if (minute !== "*" && !minute.includes("/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
518
+ return "Every hour";
519
+ }
520
+
521
+ // Every N hours: 0 */N * * *
522
+ if (hour.startsWith("*/") && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
523
+ const n = parseInt(hour.slice(2));
524
+ if (n === 1) return "Every hour";
525
+ return `Every ${n} hours`;
526
+ }
527
+
528
+ const formatTime = (h: string, m: string): string => {
529
+ const hr = parseInt(h);
530
+ const mn = parseInt(m);
531
+ if (isNaN(hr)) return "";
532
+ const ampm = hr >= 12 ? "PM" : "AM";
533
+ const h12 = hr === 0 ? 12 : hr > 12 ? hr - 12 : hr;
534
+ return `${h12}:${mn.toString().padStart(2, "0")} ${ampm}`;
535
+ };
536
+
537
+ if (hour !== "*" && !hour.includes("/") && dayOfMonth === "*" && month === "*") {
538
+ const timeStr = formatTime(hour, minute);
539
+
540
+ if (dayOfWeek === "*") return `Daily at ${timeStr}`;
541
+
542
+ const days = dayOfWeek.split(",").map(d => {
543
+ const num = parseInt(d.trim());
544
+ return DAY_NAMES[num] || d;
545
+ });
546
+
547
+ if (days.length === 7) return `Daily at ${timeStr}`;
548
+ if (days.length === 5 && !days.includes("Sat") && !days.includes("Sun")) {
549
+ return `Weekdays at ${timeStr}`;
550
+ }
551
+ if (days.length === 1) return `Weekly on ${days[0]} at ${timeStr}`;
552
+ return `${days.join(" & ")} at ${timeStr}`;
553
+ }
554
+
555
+ return cron;
556
+ } catch {
557
+ return cron;
558
+ }
559
+ }
560
+
561
+ function formatRelativeTime(dateStr: string): string {
562
+ const date = new Date(dateStr);
563
+ const now = new Date();
564
+ const diffMs = date.getTime() - now.getTime();
565
+ const absDiffMs = Math.abs(diffMs);
566
+ const isFuture = diffMs > 0;
567
+
568
+ const minutes = Math.floor(absDiffMs / 60000);
569
+ const hours = Math.floor(absDiffMs / 3600000);
570
+ const days = Math.floor(absDiffMs / 86400000);
571
+
572
+ const timeStr = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
573
+
574
+ const isToday = date.toDateString() === now.toDateString();
575
+ const tomorrow = new Date(now);
576
+ tomorrow.setDate(tomorrow.getDate() + 1);
577
+ const isTomorrow = date.toDateString() === tomorrow.toDateString();
578
+ const yesterday = new Date(now);
579
+ yesterday.setDate(yesterday.getDate() - 1);
580
+ const isYesterday = date.toDateString() === yesterday.toDateString();
581
+
582
+ if (isToday) {
583
+ if (minutes < 1) return isFuture ? "now" : "just now";
584
+ if (minutes < 60) return isFuture ? `in ${minutes} min (${timeStr})` : `${minutes} min ago`;
585
+ return isFuture ? `in ${hours}h (${timeStr})` : `${hours}h ago`;
586
+ }
587
+
588
+ if (isTomorrow) return `Tomorrow at ${timeStr}`;
589
+ if (isYesterday) return `Yesterday at ${timeStr}`;
590
+
591
+ if (days < 7) {
592
+ const dayName = DAY_NAMES[date.getDay()];
593
+ return `${dayName} at ${timeStr}`;
594
+ }
595
+
596
+ return date.toLocaleDateString([], { month: "short", day: "numeric" }) + ` at ${timeStr}`;
597
+ }
@@ -20,6 +20,7 @@ interface TelemetryContextValue {
20
20
  lastActivityByAgent: Record<string, { timestamp: string; category: string; type: string }>;
21
21
  activeAgents: Record<string, { type: string; expiresAt: number }>;
22
22
  statusChangeCounter: number;
23
+ taskChangeCounter: number;
23
24
  clearEvents: () => void;
24
25
  }
25
26
 
@@ -33,6 +34,7 @@ export function TelemetryProvider({ children }: { children: React.ReactNode }) {
33
34
  const [lastActivityByAgent, setLastActivityByAgent] = useState<Record<string, { timestamp: string; category: string; type: string }>>({});
34
35
  const [activeAgents, setActiveAgents] = useState<Record<string, { type: string; expiresAt: number }>>({});
35
36
  const [statusChangeCounter, setStatusChangeCounter] = useState(0);
37
+ const [taskChangeCounter, setTaskChangeCounter] = useState(0);
36
38
  const eventSourceRef = useRef<EventSource | null>(null);
37
39
  const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
38
40
 
@@ -117,6 +119,11 @@ export function TelemetryProvider({ children }: { children: React.ReactNode }) {
117
119
  if (data.some((e: TelemetryEvent) => e.category === "system" && (e.type === "agent_started" || e.type === "agent_stopped"))) {
118
120
  setStatusChangeCounter(c => c + 1);
119
121
  }
122
+
123
+ // Detect task change events
124
+ if (data.some((e: TelemetryEvent) => e.category === "TASK" && (e.type === "task_created" || e.type === "task_updated" || e.type === "task_deleted"))) {
125
+ setTaskChangeCounter(c => c + 1);
126
+ }
120
127
  }
121
128
  } catch {
122
129
  // Ignore parse errors (likely keepalive or empty message)
@@ -162,7 +169,7 @@ export function TelemetryProvider({ children }: { children: React.ReactNode }) {
162
169
  }, []);
163
170
 
164
171
  return (
165
- <TelemetryContext.Provider value={{ connected, events, lastActivityByAgent, activeAgents, statusChangeCounter, clearEvents }}>
172
+ <TelemetryContext.Provider value={{ connected, events, lastActivityByAgent, activeAgents, statusChangeCounter, taskChangeCounter, clearEvents }}>
166
173
  {children}
167
174
  </TelemetryContext.Provider>
168
175
  );
@@ -235,3 +242,9 @@ export function useAgentStatusChange(): number {
235
242
  const { statusChangeCounter } = useTelemetryContext();
236
243
  return statusChangeCounter;
237
244
  }
245
+
246
+ // Hook to trigger task count refetch on task mutations
247
+ export function useTaskChange(): number {
248
+ const { taskChangeCounter } = useTelemetryContext();
249
+ return taskChangeCounter;
250
+ }
@@ -1,4 +1,4 @@
1
- export { TelemetryProvider, useTelemetryContext, useTelemetry, useAgentActivity, useAgentStatusChange } from "./TelemetryContext";
1
+ export { TelemetryProvider, useTelemetryContext, useTelemetry, useAgentActivity, useAgentStatusChange, useTaskChange } from "./TelemetryContext";
2
2
  export type { TelemetryEvent } from "./TelemetryContext";
3
3
 
4
4
  export { AuthProvider, useAuth, useAuthHeaders } from "./AuthContext";
package/src/web/types.ts CHANGED
@@ -143,7 +143,7 @@ export interface OnboardingStatus {
143
143
  has_any_keys: boolean;
144
144
  }
145
145
 
146
- export type Route = "dashboard" | "agents" | "tasks" | "mcp" | "skills" | "tests" | "telemetry" | "settings" | "api";
146
+ export type Route = "dashboard" | "activity" | "agents" | "tasks" | "mcp" | "skills" | "tests" | "telemetry" | "settings" | "api";
147
147
 
148
148
  // Tool use content block in trajectory
149
149
  export interface ToolUseBlock {