agent-ecosystem/src/renderer/components/team/schedule/ScheduleRunRow.tsx
iliya 1d3080f0f9 feat: add scheduled tasks with cron execution and rich log viewer
- Cron-based task scheduling with SchedulerService (create, pause, resume, delete)
- One-shot executor using `claude -p` with stream-json output for rich log display
- CLAUDECODE env var stripped to prevent nested session detection
- JsonScheduleRepository for persistent schedule/run/log storage
- Full IPC pipeline: handlers, preload bridge, API types, HttpClient stubs
- ScheduleSection UI with create/edit dialog, run history, status badges
- ScheduleRunLogDialog with CliLogsRichView (thinking blocks, tool cards, markdown)
- Real-time run status updates via store subscription
- Retry logic with configurable max retries and auto-pause on consecutive failures
- CronScheduleInput with human-readable descriptions via cronstrue
- 3 test suites: SchedulerService, ScheduledTaskExecutor, JsonScheduleRepository
2026-03-08 00:58:07 +02:00

107 lines
3.4 KiB
TypeScript

import React from 'react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { Clock } from 'lucide-react';
import { RunStatusBadge } from './ScheduleStatusBadge';
import type { ScheduleRun } from '@shared/types';
// =============================================================================
// Props
// =============================================================================
interface ScheduleRunRowProps {
run: ScheduleRun;
onClick?: (run: ScheduleRun) => void;
}
// =============================================================================
// Helpers
// =============================================================================
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`;
const seconds = Math.floor(ms / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m`;
}
function formatTime(isoString: string): string {
try {
return new Date(isoString).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
} catch {
return isoString;
}
}
// =============================================================================
// Component
// =============================================================================
export const ScheduleRunRow = ({ run, onClick }: ScheduleRunRowProps): React.JSX.Element => (
<div
className={`flex items-center gap-2 border-t border-[var(--color-border)] px-2 py-1.5 text-[11px]${
onClick ? 'cursor-pointer transition-colors hover:bg-[var(--color-surface-raised)]' : ''
}`}
onClick={onClick ? () => onClick(run) : undefined}
role={onClick ? 'button' : undefined}
tabIndex={onClick ? 0 : undefined}
onKeyDown={
onClick
? (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick(run);
}
}
: undefined
}
>
<RunStatusBadge status={run.status} />
<span className="flex items-center gap-1 text-[var(--color-text-muted)]">
<Clock className="size-3" />
{formatTime(run.startedAt)}
</span>
{run.durationMs != null ? (
<span className="text-[var(--color-text-muted)]">{formatDuration(run.durationMs)}</span>
) : null}
{run.summary ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="min-w-0 flex-1 truncate text-[var(--color-text-secondary)]">
{run.summary.slice(0, 80)}
</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-sm">
<p className="whitespace-pre-wrap text-xs">{run.summary.slice(0, 500)}</p>
</TooltipContent>
</Tooltip>
) : null}
{run.error ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="min-w-0 flex-1 truncate text-red-400">{run.error.slice(0, 60)}</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-sm">
<p className="whitespace-pre-wrap text-xs text-red-300">{run.error.slice(0, 500)}</p>
</TooltipContent>
</Tooltip>
) : null}
</div>
);