feat: make agent reports collapsible (#132)

Show explicit session, actor, and source attribution in the session audit trail so report provenance is easier to scan in Session Detail.
This commit is contained in:
harshitsinghbhandari 2026-04-17 21:39:37 +05:30
parent 961c064864
commit 0cf01906ca
3 changed files with 167 additions and 54 deletions

View File

@ -0,0 +1,5 @@
---
"@aoagents/ao-web": patch
---
Make session detail agent reports collapsible and add explicit audit attribution for the session, actor, and report source command.

View File

@ -231,68 +231,126 @@ function formatAuditTimestamp(isoDate: string): string {
return new Date(parsed).toLocaleString();
}
function getAuditCommandLabel(entry: DashboardAgentReportAuditEntry): string {
return entry.source === "acknowledge" ? "ao acknowledge" : `ao report ${entry.reportState}`;
}
function SessionReportAuditPanel({
sessionId,
entries,
}: {
sessionId: string;
entries: DashboardAgentReportAuditEntry[];
}) {
const [isExpanded, setIsExpanded] = useState(true);
if (entries.length === 0) {
return null;
}
return (
<section className="mb-4 rounded-[20px] border border-[var(--color-border-muted)] bg-[var(--color-bg-panel)] px-4 py-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-[var(--color-text-muted)]">
Agent Report Audit
</p>
<div className="mt-3 space-y-3">
{entries.map((entry, index) => (
<div
key={`${entry.timestamp}-${entry.reportState}-${entry.actor}-${entry.source}-${String(entry.accepted)}-${index}`}
className="rounded-[16px] border border-[var(--color-border-muted)] bg-[var(--color-bg-base)] px-3 py-3"
<button
type="button"
className="flex w-full items-center justify-between gap-3 text-left"
aria-expanded={isExpanded}
aria-controls="session-report-audit-panel"
onClick={() => setIsExpanded((current) => !current)}
>
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-[var(--color-text-muted)]">
Agent Reports
</p>
<p className="mt-1 text-[12px] text-[var(--color-text-secondary)]">
{entries.length} audit {entries.length === 1 ? "entry" : "entries"}
</p>
</div>
<span
className="flex h-7 w-7 items-center justify-center rounded-full border border-[var(--color-border-muted)] bg-[var(--color-bg-base)] text-[var(--color-text-secondary)]"
aria-hidden="true"
>
<svg
className={cn(
"h-4 w-4 transition-transform duration-200",
isExpanded ? "rotate-180" : "rotate-0",
)}
fill="none"
stroke="currentColor"
strokeWidth="2"
viewBox="0 0 24 24"
>
<div className="flex flex-wrap items-center gap-2">
<span
className="rounded-full px-2 py-0.5 text-[11px] font-medium"
style={{
background: entry.accepted
? "color-mix(in srgb, var(--color-status-ready) 14%, transparent)"
: "color-mix(in srgb, var(--color-status-error) 14%, transparent)",
color: entry.accepted
? "var(--color-status-ready)"
: "var(--color-status-error)",
}}
>
{entry.accepted ? "Accepted" : "Rejected"}
</span>
<span className="text-[12px] font-medium text-[var(--color-text-primary)]">
{entry.source === "acknowledge" ? "ao acknowledge" : `ao report ${entry.reportState}`}
</span>
<span className="text-[12px] text-[var(--color-text-secondary)]">
by {entry.actor}
</span>
<span className="text-[11px] text-[var(--color-text-tertiary)]">
{formatAuditTimestamp(entry.timestamp)}
</span>
<path d="m6 9 6 6 6-6" />
</svg>
</span>
</button>
{isExpanded ? (
<div id="session-report-audit-panel" className="mt-3 space-y-3">
{entries.map((entry, index) => (
<div
key={`${entry.timestamp}-${entry.reportState}-${entry.actor}-${entry.source}-${String(entry.accepted)}-${index}`}
className="rounded-[16px] border border-[var(--color-border-muted)] bg-[var(--color-bg-base)] px-3 py-3"
>
<div className="flex flex-wrap items-center gap-2">
<span
className="rounded-full px-2 py-0.5 text-[11px] font-medium"
style={{
background: entry.accepted
? "color-mix(in srgb, var(--color-status-ready) 14%, transparent)"
: "color-mix(in srgb, var(--color-status-error) 14%, transparent)",
color: entry.accepted
? "var(--color-status-ready)"
: "var(--color-status-error)",
}}
>
{entry.accepted ? "Accepted" : "Rejected"}
</span>
<span className="text-[11px] text-[var(--color-text-tertiary)]">
{formatAuditTimestamp(entry.timestamp)}
</span>
</div>
<div className="mt-3 grid gap-2 text-[12px] text-[var(--color-text-secondary)] sm:grid-cols-3">
<div className="rounded-[12px] border border-[var(--color-border-muted)] px-2.5 py-2">
<div className="text-[10px] uppercase tracking-[0.16em] text-[var(--color-text-muted)]">
Session
</div>
<div className="mt-1 font-[var(--font-mono)] text-[var(--color-text-primary)]">
{sessionId}
</div>
</div>
<div className="rounded-[12px] border border-[var(--color-border-muted)] px-2.5 py-2">
<div className="text-[10px] uppercase tracking-[0.16em] text-[var(--color-text-muted)]">
Actor
</div>
<div className="mt-1 text-[var(--color-text-primary)]">{entry.actor}</div>
</div>
<div className="rounded-[12px] border border-[var(--color-border-muted)] px-2.5 py-2">
<div className="text-[10px] uppercase tracking-[0.16em] text-[var(--color-text-muted)]">
Source
</div>
<div className="mt-1 font-[var(--font-mono)] text-[var(--color-text-primary)]">
{getAuditCommandLabel(entry)}
</div>
</div>
</div>
<div className="mt-3 text-[12px] text-[var(--color-text-secondary)]">
{entry.before.legacyStatus} / {entry.before.sessionState}
{" -> "}
{entry.after.legacyStatus} / {entry.after.sessionState}
</div>
{entry.note ? (
<p className="mt-2 text-[12px] text-[var(--color-text-secondary)]">
Note: {entry.note}
</p>
) : null}
{entry.rejectionReason ? (
<p className="mt-2 text-[12px] text-[var(--color-status-error)]">
Rejection: {entry.rejectionReason}
</p>
) : null}
</div>
<div className="mt-2 text-[12px] text-[var(--color-text-secondary)]">
{entry.before.legacyStatus} / {entry.before.sessionState}
{" -> "}
{entry.after.legacyStatus} / {entry.after.sessionState}
</div>
{entry.note ? (
<p className="mt-2 text-[12px] text-[var(--color-text-secondary)]">
Note: {entry.note}
</p>
) : null}
{entry.rejectionReason ? (
<p className="mt-2 text-[12px] text-[var(--color-status-error)]">
Rejection: {entry.rejectionReason}
</p>
) : null}
</div>
))}
</div>
))}
</div>
) : null}
</section>
);
}
@ -814,7 +872,10 @@ export function SessionDetail({
{!isOrchestrator ? <SessionTruthPanel session={session} /> : null}
{!isOrchestrator ? (
<SessionReportAuditPanel entries={session.agentReportAudit ?? []} />
<SessionReportAuditPanel
sessionId={session.id}
entries={session.agentReportAudit ?? []}
/>
) : null}
<section className="session-detail-terminal-wrap">

View File

@ -147,12 +147,59 @@ describe("SessionDetail desktop layout", () => {
expect(screen.getByText(/Unresolved Comments/i)).toBeInTheDocument();
expect(screen.getByText("Tighten the copy")).toBeInTheDocument();
expect(screen.getByText("The empty state text needs to be shorter.")).toBeInTheDocument();
expect(screen.getByText("Agent Report Audit")).toBeInTheDocument();
expect(screen.getByText(/ao report working/i)).toBeInTheDocument();
expect(screen.getByText(/by codex/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Agent Reports/i })).toHaveAttribute(
"aria-expanded",
"true",
);
expect(screen.getAllByText("worker-desktop").length).toBeGreaterThanOrEqual(1);
expect(screen.getByText("ao report working")).toBeInTheDocument();
expect(screen.getByText("codex")).toBeInTheDocument();
expect(screen.getByText("Live Terminal")).toBeInTheDocument();
});
it("toggles the agent reports section", () => {
render(
<SessionDetail
session={makeSession({
id: "worker-audit-toggle",
projectId: "my-app",
agentReportAudit: [
{
timestamp: "2025-01-01T10:00:00.000Z",
actor: "codex",
source: "acknowledge",
reportState: "started",
accepted: true,
before: {
legacyStatus: "spawning",
sessionState: "spawning",
sessionReason: "agent_spawned",
lastTransitionAt: "2025-01-01T09:55:00.000Z",
},
after: {
legacyStatus: "working",
sessionState: "working",
sessionReason: "agent_acknowledged",
lastTransitionAt: "2025-01-01T10:00:00.000Z",
},
},
],
})}
/>,
);
const toggle = screen.getByRole("button", { name: /Agent Reports/i });
expect(screen.getByText("ao acknowledge")).toBeInTheDocument();
fireEvent.click(toggle);
expect(toggle).toHaveAttribute("aria-expanded", "false");
expect(screen.queryByText("ao acknowledge")).not.toBeInTheDocument();
fireEvent.click(toggle);
expect(toggle).toHaveAttribute("aria-expanded", "true");
expect(screen.getByText("ao acknowledge")).toBeInTheDocument();
});
it("sends unresolved comments back to the agent and shows sent state", async () => {
vi.useFakeTimers();
@ -236,6 +283,6 @@ describe("SessionDetail desktop layout", () => {
expect(screen.queryByRole("link", { name: "Orchestrator" })).not.toBeInTheDocument();
expect(screen.getByText("orchestrator")).toBeInTheDocument();
expect(screen.queryByText("Lifecycle Truth")).not.toBeInTheDocument();
expect(screen.queryByText("Agent Report Audit")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /Agent Reports/i })).not.toBeInTheDocument();
});
});