agent-ecosystem/src/renderer/components/team/schedule/ScheduleRunLogDialog.tsx
Artem Rootman da58917032
perf: offload heavy I/O to worker thread, reduce renderer re-renders
Main process — worker thread for team data:
- New team-data-worker thread handles getTeamData and findLogsForTask,
  isolating heavy file I/O (scanning 300+ subagent JSONL files) from
  Electron's main event loop. getTeamData dropped from ~2000ms on the
  main thread to ~110ms via the worker.
- Worker-side dedup and 10s result cache for findLogsForTask prevents
  redundant scans when the same task is queried multiple times.
- Discovery cache TTL raised from 5s to 30s — avoids re-scanning the
  entire project directory on every call.
- Message cap at 200 in TeamDataService to keep IPC payloads under 1MB
  (was sending 2200+ messages / ~3MB, stalling Chromium IPC serialization).
- IPC handlers fall back to main-thread execution if the worker is
  unavailable (graceful degradation).

Renderer — useShallow and memoization (55 files):
- Added useShallow to store selectors across 55 renderer files. Batched
  individual useStore() calls (e.g. 17 calls in ExtensionStoreView,
  10 in ConnectionSection) into single useShallow selectors, cutting
  unnecessary re-render checks on every store update.
- MemberLogsTab: three 5-second polling intervals now pause when the
  parent tab is hidden (display:none). Previously 5 hidden tabs × 3
  intervals = 15 polling timers firing continuously.
- KanbanColumn wrapped in React.memo to skip re-renders when props
  haven't changed.
- MemberList: memoized activeMembers/removedMembers/colorMap; replaced
  O(n×m) per-member task scan with a pre-computed reviewer map.
- Bounded timer Maps in store initialization to prevent unbounded growth
  of debounce/throttle tracking maps during long sessions.
2026-04-05 16:21:05 +00:00

245 lines
7.7 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { api } from '@renderer/api';
import { Button } from '@renderer/components/ui/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@renderer/components/ui/dialog';
import { useStore } from '@renderer/store';
import { useShallow } from 'zustand/react/shallow';
import { AlertTriangle, Clock, Loader2, Terminal } from 'lucide-react';
import { CliLogsRichView } from '../CliLogsRichView';
import { RunStatusBadge } from './ScheduleStatusBadge';
import type { ScheduleRun } from '@shared/types';
// =============================================================================
// Props
// =============================================================================
interface ScheduleRunLogDialogProps {
open: boolean;
/** Initial run snapshot — used to identify the run; live data comes from store */
run: ScheduleRun | null;
scheduleId: string;
onClose: () => void;
}
// =============================================================================
// Helpers
// =============================================================================
function formatTime(isoString: string): string {
try {
return new Date(isoString).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
} catch {
return isoString;
}
}
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`;
}
// =============================================================================
// Component
// =============================================================================
export const ScheduleRunLogDialog = ({
open,
run: initialRun,
scheduleId,
onClose,
}: ScheduleRunLogDialogProps): React.JSX.Element => {
// Read live run data from store — falls back to initial prop if not found
const liveRun = useStore(
useShallow((s) => {
if (!initialRun) return null;
const runs = s.scheduleRuns[scheduleId] ?? [];
return runs.find((r) => r.id === initialRun.id) ?? initialRun;
})
);
const run = liveRun ?? initialRun;
const [logs, setLogs] = useState<{ stdout: string; stderr: string } | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const runStatus = run?.status;
const runId = run?.id;
useEffect(() => {
if (!open || !run) {
setLogs(null);
setError(null);
return;
}
// Only fetch logs for completed/failed runs (not running/pending)
const hasLogs =
runStatus === 'completed' || runStatus === 'failed' || runStatus === 'failed_interrupted';
if (!hasLogs) return;
let cancelled = false;
setLoading(true);
setError(null);
void (async () => {
try {
const result = await api.schedules.getRunLogs(scheduleId, runId!);
if (!cancelled) {
setLogs(result);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err.message : 'Failed to load logs');
}
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, [open, runId, runStatus, scheduleId]);
if (!run) return <></>;
const isRunning =
run.status === 'running' ||
run.status === 'warming_up' ||
run.status === 'warm' ||
run.status === 'pending';
const hasStderr = logs?.stderr && logs.stderr.trim().length > 0;
return (
<Dialog
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) onClose();
}}
>
<DialogContent className="max-h-[85vh] max-w-4xl overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-sm">
<Terminal className="size-4" />
Run Log
</DialogTitle>
</DialogHeader>
{/* Metadata */}
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px]">
<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.exitCode != null ? (
<span
className={`font-mono ${run.exitCode === 0 ? 'text-emerald-400' : 'text-red-400'}`}
>
exit {run.exitCode}
</span>
) : null}
{run.retryCount > 0 ? (
<span className="text-[var(--color-text-muted)]">retry {run.retryCount}/2</span>
) : null}
</div>
{/* Content */}
<div className="space-y-3">
{/* Running state */}
{isRunning ? (
<div className="flex items-center gap-2 rounded border border-[var(--color-border)] bg-[var(--color-surface)] p-4 text-xs text-[var(--color-text-secondary)]">
<Loader2 className="size-4 animate-spin" />
Task is still running...
{run.summary ? (
<span className="ml-2 truncate text-[var(--color-text-muted)]">{run.summary}</span>
) : null}
</div>
) : null}
{/* Loading */}
{loading ? (
<div className="flex items-center justify-center py-6 text-xs text-[var(--color-text-muted)]">
<Loader2 className="mr-2 size-4 animate-spin" />
Loading logs...
</div>
) : null}
{/* Error loading logs */}
{error ? (
<div className="flex items-start gap-2 rounded border border-red-500/40 bg-red-500/10 p-3 text-xs text-red-300">
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
<span>{error}</span>
</div>
) : null}
{/* Stdout — rich stream-json view (falls back to plain text for old logs) */}
{logs ? (
<>
<CliLogsRichView
cliLogsTail={logs.stdout}
order="oldest-first"
className="max-h-[400px]"
/>
{/* Stderr */}
{hasStderr ? (
<div>
<div className="mb-1 text-[11px] font-medium text-red-400">Errors</div>
<pre className="max-h-[200px] overflow-auto whitespace-pre-wrap rounded border border-red-500/30 bg-red-500/5 p-3 font-mono text-xs leading-relaxed text-red-300">
{logs.stderr}
</pre>
</div>
) : null}
</>
) : null}
{/* Run error message (from ScheduleRun.error) */}
{!isRunning && run.error && !logs ? (
<div className="flex items-start gap-2 rounded border border-red-500/40 bg-red-500/10 p-3 text-xs text-red-300">
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
<span className="whitespace-pre-wrap">{run.error}</span>
</div>
) : null}
</div>
<DialogFooter className="pt-2">
<Button variant="outline" size="sm" onClick={onClose}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};