feat(awooop): surface callback replies on run list
All checks were successful
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m25s
CD Pipeline / build-and-deploy (push) Successful in 3m35s
CD Pipeline / post-deploy-checks (push) Successful in 1m50s

This commit is contained in:
Your Name
2026-05-18 15:24:39 +08:00
parent 584bd4b31b
commit 20d62ee0cf
5 changed files with 356 additions and 4 deletions

View File

@@ -20,6 +20,7 @@ import {
Cpu,
ListChecks,
SearchCheck,
Send,
ShieldCheck,
TriangleAlert,
} from "lucide-react";
@@ -40,6 +41,13 @@ type RunState =
| "timeout";
type RunLane = "intake" | "diagnosis" | "approval" | "execution" | "done" | "manual";
type CallbackReplyStatus =
| "no_callback"
| "sent"
| "fallback_sent"
| "rescue_sent"
| "failed"
| "observed";
type RemediationStatus =
| "no_evidence"
| "mcp_observed"
@@ -74,6 +82,22 @@ interface RemediationSummary {
latest_mcp_server?: string | null;
}
interface CallbackReplySummary {
schema_version?: string;
status?: CallbackReplyStatus | string;
total?: number;
sent?: number;
fallback_sent?: number;
rescue_sent?: number;
failed?: number;
needs_human?: boolean;
latest_status?: string | null;
latest_action?: string | null;
latest_incident_id?: string | null;
latest_at?: string | null;
latest_provider_message_id?: string | null;
}
interface Run {
run_id: string;
project_id: string;
@@ -84,6 +108,7 @@ interface Run {
step_count: number;
created_at: string;
remediation_summary?: RemediationSummary | null;
callback_reply_summary?: CallbackReplySummary | null;
}
interface Tenant {
@@ -282,6 +307,46 @@ const REMEDIATION_FILTER_OPTIONS: RemediationStatus[] = [
"no_evidence",
];
const CALLBACK_REPLY_CONFIG: Record<
CallbackReplyStatus,
{
labelKey: string;
detailKey: string;
className: string;
}
> = {
no_callback: {
labelKey: "statuses.noCallback",
detailKey: "details.noCallback",
className: "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]",
},
sent: {
labelKey: "statuses.sent",
detailKey: "details.sent",
className: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
},
fallback_sent: {
labelKey: "statuses.fallbackSent",
detailKey: "details.fallbackSent",
className: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
},
rescue_sent: {
labelKey: "statuses.rescueSent",
detailKey: "details.rescueSent",
className: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
},
failed: {
labelKey: "statuses.failed",
detailKey: "details.failed",
className: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]",
},
observed: {
labelKey: "statuses.observed",
detailKey: "details.observed",
className: "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]",
},
};
function getRunLane(state: RunState): RunLane {
if (state === "pending") return "intake";
if (state === "waiting_tool") return "diagnosis";
@@ -355,6 +420,20 @@ function normalizeRemediationStatus(summary?: RemediationSummary | null): Remedi
return "no_evidence";
}
function normalizeCallbackReplyStatus(summary?: CallbackReplySummary | null): CallbackReplyStatus {
const statusValue = summary?.status;
if (
statusValue === "sent" ||
statusValue === "fallback_sent" ||
statusValue === "rescue_sent" ||
statusValue === "failed" ||
statusValue === "observed"
) {
return statusValue;
}
return "no_callback";
}
function RemediationEvidenceCell({ summary }: { summary?: RemediationSummary | null }) {
const t = useTranslations("awooop.listEvidence");
const status = normalizeRemediationStatus(summary);
@@ -399,6 +478,56 @@ function RemediationEvidenceCell({ summary }: { summary?: RemediationSummary | n
);
}
function CallbackReplyCell({ summary }: { summary?: CallbackReplySummary | null }) {
const t = useTranslations("awooop.callbackReply");
const status = normalizeCallbackReplyStatus(summary);
const config = CALLBACK_REPLY_CONFIG[status];
const total = summary?.total ?? 0;
const latestAction = summary?.latest_action ? String(summary.latest_action) : null;
const latestIncidentId = summary?.latest_incident_id ? String(summary.latest_incident_id) : null;
const countText = total > 0
? t("count", {
total,
failed: summary?.failed ?? 0,
fallback: (summary?.fallback_sent ?? 0) + (summary?.rescue_sent ?? 0),
})
: t("emptyShort");
const latestText = latestAction || latestIncidentId
? t("latest", {
action: latestAction ?? "--",
incidentId: latestIncidentId ?? "--",
})
: null;
return (
<div className="flex min-w-[210px] flex-col gap-1">
<span
className={cn(
"inline-flex w-fit items-center gap-1.5 border px-2 py-0.5 text-xs font-semibold",
config.className
)}
title={t(config.detailKey)}
>
<Send className="h-3.5 w-3.5" aria-hidden="true" />
{t(config.labelKey)}
</span>
<span className={cn("text-xs leading-5", total > 0 ? "text-[#5f5b52]" : "text-[#77736a]")}>
{countText}
</span>
{latestText && (
<span className="truncate font-mono text-xs text-[#77736a]">
{latestText}
</span>
)}
{summary?.needs_human && (
<span className="text-xs font-semibold text-[#9f2f25]">
{t("needsHuman")}
</span>
)}
</div>
);
}
function linkedIncidentIds(summary?: RemediationSummary | null): string[] {
const rawIds = summary?.incident_ids ?? [];
return Array.from(
@@ -492,6 +621,9 @@ function RunRow({ run }: { run: Run }) {
<td className="px-4 py-3">
<RemediationEvidenceCell summary={run.remediation_summary} />
</td>
<td className="px-4 py-3">
<CallbackReplyCell summary={run.callback_reply_summary} />
</td>
<td className="px-4 py-3">
<ShadowBadge isShadow={run.is_shadow} />
</td>
@@ -707,6 +839,12 @@ export default function RunsPage() {
(run) => normalizeRemediationStatus(run.remediation_summary) === "no_evidence"
).length,
manualGate: runs.filter((run) => run.remediation_summary?.human_gate_open).length,
callbackObserved: runs.filter(
(run) => normalizeCallbackReplyStatus(run.callback_reply_summary) !== "no_callback"
).length,
callbackFailed: runs.filter(
(run) => normalizeCallbackReplyStatus(run.callback_reply_summary) === "failed"
).length,
};
}, [runs]);
@@ -762,7 +900,7 @@ export default function RunsPage() {
})}
</section>
<section className="grid gap-px border border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-5">
<section className="grid gap-px border border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-6">
{[
{
label: tEvidence("summary.mcpObserved"),
@@ -792,6 +930,17 @@ export default function RunsPage() {
icon: TriangleAlert,
className: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]",
},
{
label: tEvidence("summary.callbackObserved"),
value: evidenceSummary.callbackObserved,
detail: tEvidence("summary.callbackObservedDetail", {
failed: evidenceSummary.callbackFailed,
}),
icon: Send,
className: evidenceSummary.callbackFailed > 0
? "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"
: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
},
{
label: tEvidence("summary.noEvidence"),
value: evidenceSummary.noEvidence,
@@ -940,6 +1089,9 @@ export default function RunsPage() {
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
{tEvidence("column")}
</th>
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
{tEvidence("callbackColumn")}
</th>
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
Shadow
</th>
@@ -955,7 +1107,7 @@ export default function RunsPage() {
{loading ? (
Array.from({ length: 8 }).map((_, i) => (
<tr key={i} className="border-b border-border">
{Array.from({ length: 10 }).map((_, j) => (
{Array.from({ length: 11 }).map((_, j) => (
<td key={j} className="px-4 py-3">
<div className="h-5 bg-muted animate-pulse rounded w-20" />
</td>
@@ -964,7 +1116,7 @@ export default function RunsPage() {
))
) : runs.length === 0 && !error ? (
<tr>
<td colSpan={10} className="px-4 py-16 text-center">
<td colSpan={11} className="px-4 py-16 text-center">
<Activity className="w-10 h-10 text-muted-foreground/30 mx-auto mb-3" aria-hidden="true" />
<p className="text-sm text-muted-foreground"> Run </p>
</td>