Files
fusion/packages/dashboard/app/components/AgentRunHistory.tsx
gsxdsm 1e49494bac feat(i18n): full-sweep string migration — 5,930 keys across 5 locales (#1352)
Migration (multi-agent sweep over 216 files, 60 batches):
- Every user-visible dashboard + TUI string moved to t() with the exact
  English inline default (en rendering byte-identical)
- Catalogs merged from per-batch fragments: en/zh-CN/zh-TW/fr/es now
  carry ~5,930 keys each across common/app/errors/cli namespaces;
  CLI bundles regenerated (6 locales incl. ko)

Integration fixes:
- 18 type errors: reserved {{count}} interpolations renamed, malformed
  plural call, hand-rolled t-param types replaced with TFunction<"app">
- 23 lint errors: superseded label constants/helpers removed
- ExecutorStatusBar hook-order violation (keyboard-open early return
  moved below hooks)
- TUI tests wrapped in I18nextProvider (uninitialized fallback renders
  literal {{placeholders}}); dashboard vitest.setup boots a minimal en
  i18next instance for the same reason

Known WIP (next commits): ~457 residual strings across 50 batches,
Korean drafts for swept keys, and a dashboard test-suite pass that is
still being stabilized (~283 failures under investigation — fake-timer
waitFor interaction, likely stale node_modules vs merged lockfile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 19:06:53 -07:00

133 lines
4.7 KiB
TypeScript

import { useState, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { CheckCircle, XCircle, Loader2, Square, Clock } from "lucide-react";
import type { AgentHeartbeatRun } from "../api";
import { fetchAgentRuns, stopAgentRun } from "../api";
import { useConfirm } from "../hooks/useConfirm";
interface AgentRunHistoryProps {
agentId: string;
projectId?: string;
/** Optional callback when a run row is clicked */
onRunClick?: (runId: string) => void;
}
const STATUS_ICONS: Record<string, { icon: typeof CheckCircle; color: string }> = {
completed: { icon: CheckCircle, color: "var(--color-success, #3fb950)" },
failed: { icon: XCircle, color: "var(--color-error, #f85149)" },
active: { icon: Loader2, color: "var(--in-progress, #bc8cff)" },
terminated: { icon: Square, color: "var(--text-muted, #8b949e)" },
};
export function AgentRunHistory({ agentId, projectId, onRunClick }: AgentRunHistoryProps) {
const { t } = useTranslation("app");
const [runs, setRuns] = useState<AgentHeartbeatRun[]>([]);
const { confirm } = useConfirm();
const [isLoading, setIsLoading] = useState(true);
const loadRuns = useCallback(async () => {
setIsLoading(true);
try {
const data = await fetchAgentRuns(agentId, 50, projectId);
setRuns(data);
} catch {
setRuns([]);
} finally {
setIsLoading(false);
}
}, [agentId, projectId]);
useEffect(() => {
void loadRuns();
}, [loadRuns]);
const handleStop = useCallback(async () => {
const shouldStop = await confirm({
title: t("agents.runs.stopTitle", "Stop Run"),
message: t("agents.runs.stopMessage", "Stop this run?"),
danger: true,
});
if (!shouldStop) {
return;
}
try {
await stopAgentRun(agentId, projectId);
await loadRuns();
} catch {
// No-op: keep history view usable even if stop fails.
}
}, [agentId, projectId, loadRuns, confirm]);
if (isLoading) {
return <div className="agent-run-loading"><Loader2 className="animate-spin" size={20} /> {t("agents.runs.loading", "Loading runs...")}</div>;
}
if (runs.length === 0) {
return <div className="agent-run-empty">{t("agents.runs.empty", "No runs yet")}</div>;
}
return (
<div className="agent-run-history">
{runs.map(run => {
const statusInfo = STATUS_ICONS[run.status] ?? STATUS_ICONS.terminated;
const StatusIcon = statusInfo.icon;
const duration = run.endedAt
? Math.round((new Date(run.endedAt).getTime() - new Date(run.startedAt).getTime()) / 1000)
: null;
const usage = run.usageJson;
return (
<div
key={run.id}
className={onRunClick ? "agent-run-row agent-run-row--clickable" : "agent-run-row"}
onClick={onRunClick ? () => onRunClick(run.id) : undefined}
role={onRunClick ? "button" : undefined}
tabIndex={onRunClick ? 0 : undefined}
aria-label={onRunClick ? `Run ${run.id.slice(0, 8)}, ${run.status}` : undefined}
onKeyDown={onRunClick ? (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onRunClick(run.id);
}
} : undefined}
>
<StatusIcon size={16} style={{ color: statusInfo.color }} className={run.status === "active" ? "animate-spin" : ""} />
<div className="agent-run-info">
<span className="agent-run-id">{run.id}</span>
<span className="text-secondary">{new Date(run.startedAt).toLocaleString()}</span>
</div>
<div className="agent-run-meta">
{duration !== null && (
<span className="badge"><Clock size={12} /> {duration}s</span>
)}
{usage && (
<span className="badge text-secondary">
{((usage.inputTokens + usage.outputTokens) / 1000).toFixed(1)}k tokens
</span>
)}
{run.triggerDetail && (
<span className="badge text-secondary">{run.triggerDetail}</span>
)}
</div>
{run.status === "active" && (
<button
className="btn btn--sm btn--danger"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
void handleStop();
}}
aria-label={t("agents.runs.stopAriaLabel", "Stop run")}
style={{ marginLeft: "8px" }}
>
<Square size={12} /> {t("agents.runs.stop", "Stop")}
</button>
)}
</div>
);
})}
</div>
);
}