import { useEffect, useMemo, useRef } from 'react';
import type { AgentEvent, JobState, RunResult } from '../types';
import { useI18n, type MsgKey, type TFn } from '../i18n';
const STEPS = ['plan', 'implement', 'verify', 'fix', 'evaluate'] as const;
function runSummary(r: RunResult, t: TFn, pct: (v: number | null | undefined) => string): string {
if (r.timed_out) return t('flow.timeout');
if (!r.compiled) return t('flow.compileErr');
return t('flow.runSummary', { p: r.passed, t: r.total, cov: pct(r.coverage.percent) });
}
function Item({ e }: { e: AgentEvent }) {
const { t, pct, num, evTitle, pick } = useI18n();
const d = e.data;
const title = evTitle(e);
if (e.type === 'phase') {
const n = STEPS.findIndex((s) => s === e.phase) + 1;
return (
{n || '•'}
{title}
{d.agent &&
{t('flow.agent', { a: pick(d.agent_i18n, d.agent) })}
}
{d.reason &&
{t('flow.reason', { r: pick(d.reason_i18n, d.reason) })}
}
);
}
if (e.type === 'tool_call' && d.tool === 'write_tests') {
return (
✎
write_tests — {t('flow.wrote', { n: d.lines })}
{t('flow.showCode')}
{d.code}
);
}
if (e.type === 'tool_call' && d.tool === 'report_bug') {
const ok = d.accepted !== false;
return (
{ok ? '!' : '⊘'}
report_bug — {ok ? t('flow.bugOk') : t('flow.bugRejected', { by: t(d.by === 'kural' ? 'flow.by.rule' : 'flow.by.judge') })}
{d.test_name}: {d.reason}
{d.verdict &&
{t('flow.verdict')} {d.verdict}
}
);
}
if (e.type === 'tool_call') {
const bad = d.tool === 'report_bug';
return (
{bad ? '!' : '›'}
{title}
{bad &&
{d.test_name}: {d.reason}
}
{d.summary &&
{d.summary}
}
);
}
if (e.type === 'tool_result' && d.run) {
const r: RunResult = d.run;
const failing = r.cases.filter((c) => c.status === 'failed' || c.status === 'error');
return (
{d.accepted ? '✓' : '✕'}
{t('flow.roundResult', { n: d.iteration, summary: runSummary(r, t, pct) })}
{t('flow.sandboxSec', { s: num(r.duration_s, 1) })}
{(!r.compiled || failing.length > 0) && (
{!r.compiled ? t('flow.compilerOut') : t('flow.nFailed', { n: failing.length })}
{!r.compiled ? r.compile_output : failing.map((c) => `✕ ${c.name}\n${c.message}`).join('\n\n')}
)}
);
}
if (e.type === 'thought') {
return (
…
{title}
{d.text &&
{t('flow.showText')}
{d.text}}
);
}
if (e.type === 'result') {
return (
✓
{title}
{d.analysis?.test_plan?.length > 0 &&
{t('flow.planItems', { n: d.analysis.test_plan.length })}
}
);
}
if (e.type === 'llm_progress') {
return (
…
{t('flow.generating')}
{d.seconds ? t('flow.genProgress', { s: Math.round(d.seconds), c: d.chunks }) : t('flow.genLoading')}
);
}
if (e.type === 'mutant') {
return (
μ
{t('flow.mutants', { d: d.done, t: d.total })}
);
}
if (e.type === 'error') {
return (
!
{t('flow.error')}
{d.message}
{d.trace &&
{t('flow.details')}
{d.trace}}
);
}
if (e.type === 'done' || e.type === 'status') {
return (
{e.type === 'done' ? '■' : '▶'}
{title}
{pick(d.stop_reason_i18n, d.stop_reason || '') || (d.model ? t('flow.model', { m: d.model }) : '')}
);
}
return null;
}
export default function AgentFlow({ events, job, running }: { events: AgentEvent[]; job: JobState | null; running: boolean }) {
const { t, pct, num } = useI18n();
const endRef = useRef(null);
const { visible, active, seen, llm, runs } = useMemo(() => {
let lastMutant = -1, lastProgress = -1, lastLlm = -1;
events.forEach((e, i) => {
if (e.type === 'mutant') lastMutant = i;
if (e.type === 'llm_progress') lastProgress = i;
if (e.type === 'llm' || e.type === 'error' || e.type === 'done') lastLlm = i;
});
// Model hâlâ yanıt üretiyorsa yalnızca en son ilerleme satırını göster
const showProgress = lastProgress > lastLlm ? lastProgress : -1;
const seen = new Set();
let active: string | null = null;
const runs: Array<{ it: number; r: RunResult }> = [];
let llm = { calls: 0, tin: 0, tout: 0, secs: 0 };
for (const e of events) {
if (e.type === 'phase' && e.phase) { seen.add(e.phase); active = e.phase; }
if (e.type === 'llm') llm = { calls: llm.calls + 1, tin: llm.tin + (e.data.tokens_in || 0), tout: llm.tout + (e.data.tokens_out || 0), secs: llm.secs + (e.data.seconds || 0) };
if (e.type === 'tool_result' && e.data.run) runs.push({ it: e.data.iteration, r: e.data.run });
}
const visible = events.filter((e, i) => e.type !== 'llm'
&& (e.type !== 'mutant' || i === lastMutant)
&& (e.type !== 'llm_progress' || i === showProgress));
return { visible, active, seen, llm, runs };
}, [events]);
useEffect(() => { if (running) endRef.current?.scrollIntoView({ block: 'end' }); }, [visible.length, running]);
return (
{t('flow.title')}
{job &&
{t(`status.${running ? 'running' : job.status}` as MsgKey)}}
{STEPS.map((s) => {
const isActive = running && active === s;
return (
{t(`flow.step.${s}` as MsgKey)}
);
})}
{events.length === 0 ? (
{t('flow.emptyTitle')}
{t('flow.emptyText')}
) : (
{visible.map((e) => )}
)}
{runs.length > 0 && (
{runs.map(({ it, r }) => (
{t('flow.round', { n: it })}
{r.compiled ? pct(r.coverage.percent) : t('flow.compileShort')}
))}
{llm.calls > 0 && (
{t('flow.llmStats', { c: llm.calls, tin: num(llm.tin), tout: num(llm.tout), s: num(llm.secs) })}
)}
)}
);
}