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:
parent
961c064864
commit
0cf01906ca
|
|
@ -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.
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue