Open agent details in a modal

This commit is contained in:
Jonathan Singer
2026-07-20 09:51:56 -04:00
parent 46e4b16167
commit db29c77b30
6 changed files with 178 additions and 110 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -6,8 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<title>Strix Results</title>
<script type="module" crossorigin src="./assets/index-C0ezPxZx.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-nWrlYz6N.css">
<script type="module" crossorigin src="./assets/index-BQfxrWUY.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Di2TwyAB.css">
</head>
<body>
<div id="root"></div>
+13 -12
View File
@@ -16,7 +16,8 @@ import VulnerabilityDetail from "@/components/vulnerability/VulnerabilityDetail"
import { ContentSection } from "@/components/vulnerability/ContentSection";
import { IssueSeveritySummary } from "@/components/IssueSeveritySummary";
import AgentGraph from "@/components/live/AgentGraph";
import { AgentTranscript, buildGraphAgents } from "@/components/live/AgentTranscript";
import { buildGraphAgents } from "@/components/live/AgentTranscript";
import AgentDetailModal from "@/components/live/AgentDetailModal";
import { severityCounts, type ParsedRunSummary } from "@/lib/local-run-parser";
import { fetchAll, fetchRunSummary, fetchTranscript, fetchVulnerabilities, type LoadedRun } from "@/data/serverSource";
@@ -456,12 +457,10 @@ function UpsellRow() {
function AgentsTab({ run }: { run: LoadedRun }) {
const { agents, events } = run.transcript;
const graphAgents = useMemo(() => buildGraphAgents(agents, events), [agents, events]);
// Default the transcript to the root agent (no parent) so something is always
// shown; the graph selection then drives which agent's transcript renders.
const rootId = useMemo(() => agents.find((a) => !a.parent_id)?.id ?? agents[0]?.id ?? null, [agents]);
// Clicking a graph node opens the agent's transcript in a modal (matching the
// cloud app); no node selected means no modal.
const [selectedId, setSelectedId] = useState<string | null>(null);
const activeId = selectedId ?? rootId;
const activeAgent = agents.find((a) => a.id === activeId) ?? null;
const selectedAgent = selectedId ? (agents.find((a) => a.id === selectedId) ?? null) : null;
return (
<div className="space-y-5">
@@ -474,12 +473,12 @@ function AgentsTab({ run }: { run: LoadedRun }) {
</span>
</div>
<p className="mt-1 mb-4 text-xs text-[#666]">
Select an agent to view its full transcript below.
Click an agent to open its full transcript.
</p>
<div className="h-[480px] rounded-lg border border-[#1a1a1a] overflow-hidden">
<AgentGraph
agents={graphAgents}
selectedAgentId={activeId}
selectedAgentId={selectedId}
onSelectAgent={(id) => setSelectedId(id)}
eventsLoaded
eventsEmpty={graphAgents.size === 0}
@@ -488,10 +487,12 @@ function AgentsTab({ run }: { run: LoadedRun }) {
</div>
</div>
{activeAgent && (
<div className="rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5">
<AgentTranscript agent={activeAgent} events={events} />
</div>
{selectedAgent && (
<AgentDetailModal
agent={selectedAgent}
events={events}
onClose={() => setSelectedId(null)}
/>
)}
</div>
);
@@ -0,0 +1,62 @@
import { useEffect } from "react";
import { X } from "lucide-react";
import { AgentTranscript } from "./AgentTranscript";
import type { TranscriptAgent, TranscriptEvent } from "@/data/serverSource";
/**
* Overlay modal showing a single agent's full transcript. Matches the cloud
* app, where clicking a node in the agent graph opens a modal (rather than
* revealing a section beneath the graph). Closes on backdrop click, the X
* button, or Escape.
*/
export function AgentDetailModal({
agent,
events,
onClose,
}: {
agent: TranscriptAgent;
events: TranscriptEvent[];
onClose: () => void;
}) {
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", onKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prevOverflow;
};
}, [onClose]);
return (
<div
className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/70 p-4 sm:p-8"
onClick={onClose}
role="dialog"
aria-modal="true"
aria-label={`Agent ${agent.name}`}
>
<div
className="relative my-auto w-full max-w-3xl rounded-xl border border-[#222] bg-[#0a0a0a] shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<button
type="button"
onClick={onClose}
aria-label="Close"
className="absolute right-3 top-3 z-10 rounded-md p-1 text-[#888] transition-colors hover:bg-[#1a1a1a] hover:text-white"
>
<X className="h-4 w-4" />
</button>
<div className="max-h-[85vh] overflow-y-auto p-5">
<AgentTranscript agent={agent} events={events} />
</div>
</div>
</div>
);
}
export default AgentDetailModal;