Steer each agent from its modal; drop the Pro upsell there

This commit is contained in:
Jonathan Singer
2026-07-20 17:20:09 -04:00
parent 47ded9b93c
commit 09ff9aa783
6 changed files with 229 additions and 202 deletions
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-Cm9zWfr2.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CHD1Rail.css">
<script type="module" crossorigin src="./assets/index-S0W-37yp.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DIFG4xqP.css">
</head>
<body>
<div id="root"></div>
+7 -54
View File
@@ -15,7 +15,6 @@ import {
Radio,
ArrowUpRight,
History,
Send,
} from "lucide-react";
import type { Vulnerability, VulnerabilitySeverity } from "@/types/issues";
import { SEVERITY_COLORS } from "@/types/issues";
@@ -26,6 +25,7 @@ import { IssueSeveritySummary } from "@/components/IssueSeveritySummary";
import AgentGraph from "@/components/live/AgentGraph";
import { buildGraphAgents } from "@/components/live/AgentTranscript";
import AgentDetailModal from "@/components/live/AgentDetailModal";
import { AgentSteerInput } from "@/components/live/AgentSteerInput";
import { severityCounts, type ParsedRunSummary } from "@/lib/local-run-parser";
import {
fetchAll,
@@ -36,13 +36,12 @@ import {
fetchTranscript,
fetchVulnerabilities,
forgetAuth,
steerAgent,
type AuthStatus,
type LoadedRun,
type RunsPayload,
type TranscriptAgent,
} from "@/data/serverSource";
import { SIGNUP_URL, ctaUrl, track, trackCta } from "@/lib/cta";
import { SIGNUP_URL, ctaUrl, trackCta } from "@/lib/cta";
import { runTitle } from "@/lib/target-utils";
import Sidebar from "@/components/Sidebar";
import PastRunsView from "@/components/PastRunsView";
@@ -802,6 +801,7 @@ function AgentsTab({ run, canSteer }: { run: LoadedRun; canSteer: boolean }) {
<AgentDetailModal
agent={selectedAgent}
events={events}
steerable={steerable}
onClose={() => setSelectedId(null)}
/>
)}
@@ -819,31 +819,10 @@ function SteerComposer({
agents: TranscriptAgent[];
selectedId: string | null;
}) {
const [message, setMessage] = useState("");
const [sending, setSending] = useState(false);
const [feedback, setFeedback] = useState<string | null>(null);
const rootAgent = agents.find((a) => !a.parent_id) ?? agents[0] ?? null;
const target =
(selectedId ? (agents.find((a) => a.id === selectedId) ?? null) : null) ?? rootAgent;
const send = useCallback(async () => {
const text = message.trim();
if (!text || sending || !target) return;
setSending(true);
setFeedback(null);
const res = await steerAgent(target.id, text);
setSending(false);
if (res.ok) {
setMessage("");
setFeedback(`Sent to ${target.name}`);
track("agent_steered");
} else if (res.error === "not_delivered") {
setFeedback("Could not reach that agent (it may have finished).");
} else {
setFeedback("Could not send that message. Try again.");
}
}, [message, sending, target]);
if (!target) return null;
return (
<div className="rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5">
@@ -852,36 +831,10 @@ function SteerComposer({
<h2 className="text-sm font-semibold text-white">Steer the agents</h2>
</div>
<p className="mt-1 mb-3 text-xs text-[#666]">
Send a live instruction to{" "}
<span className="text-[#aaa]">{target ? target.name : "the agent"}</span>
{selectedId ? " (selected)" : " (root)"}. Click an agent in the graph to steer it directly.
Send a live instruction to <span className="text-[#aaa]">{target.name}</span>
{selectedId ? " (selected)" : " (root)"}. Open an agent to steer it directly.
</p>
<div className="flex items-center gap-2">
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
void send();
}
}}
placeholder="e.g. Focus on the authentication flow next"
maxLength={4000}
disabled={sending || !target}
className="flex-1 min-w-0 rounded-lg border border-[#2a2a2a] bg-[rgba(255,255,255,0.03)] px-3 py-2 text-sm text-white placeholder:text-[#555] focus:outline-none focus:border-[#3a3a3a] disabled:opacity-50"
/>
<button
onClick={() => void send()}
disabled={sending || !message.trim() || !target}
className="inline-flex items-center gap-1.5 rounded-lg bg-white px-3 py-2 text-sm font-medium text-black transition-opacity hover:opacity-90 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
>
<Send className="w-3.5 h-3.5" aria-hidden="true" />
{sending ? "Sending" : "Send"}
</button>
</div>
{feedback && <p className="mt-2 text-xs text-[#888]">{feedback}</p>}
<AgentSteerInput key={target.id} agentId={target.id} agentName={target.name} />
</div>
);
}
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef } from "react";
import { X } from "lucide-react";
import { Radio, X } from "lucide-react";
import { AgentTranscript } from "./AgentTranscript";
import { SIGNUP_URL, ctaUrl, trackCta } from "@/lib/cta";
import { AgentSteerInput } from "./AgentSteerInput";
import type { TranscriptAgent, TranscriptEvent } from "@/data/serverSource";
/** Status -> the small leading dot color, matching the graph node styling. */
@@ -27,10 +27,12 @@ const NEAR_BOTTOM_PX = 80;
export function AgentDetailModal({
agent,
events,
steerable,
onClose,
}: {
agent: TranscriptAgent;
events: TranscriptEvent[];
steerable: boolean;
onClose: () => void;
}) {
const scrollRef = useRef<HTMLDivElement>(null);
@@ -98,18 +100,15 @@ export function AgentDetailModal({
<AgentTranscript agent={agent} events={events} showHeader={false} />
</div>
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-[#222] px-5 py-3">
<span className="text-xs text-[#888]">Steer agents live from your terminal.</span>
<a
href={ctaUrl(SIGNUP_URL, "steer_agent")}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta("steer_agent", "agents")}
className="text-xs font-medium text-[#60a5fa] hover:underline"
>
Upgrade to Pro to steer agents from the web &rarr;
</a>
</div>
{steerable && (
<div className="border-t border-[#222] px-5 py-3">
<div className="mb-2 flex items-center gap-1.5">
<Radio className="h-3.5 w-3.5 text-[#888]" aria-hidden="true" />
<span className="text-xs font-medium text-[#aaa]">Steer this agent</span>
</div>
<AgentSteerInput agentId={agent.id} agentName={agent.name} />
</div>
)}
</div>
</div>
);
@@ -0,0 +1,75 @@
import { useCallback, useState } from "react";
import { Send } from "lucide-react";
import { steerAgent } from "@/data/serverSource";
import { track } from "@/lib/cta";
/**
* Send a live instruction to one specific running agent. Only rendered when the
* viewer runs in-process during a live scan (steering available). Reused by the
* Agents graph composer and the per-agent detail modal, so both steer through
* the same path.
*/
export function AgentSteerInput({
agentId,
agentName,
placeholder = "e.g. Focus on the authentication flow next",
}: {
agentId: string;
agentName: string;
placeholder?: string;
}) {
const [message, setMessage] = useState("");
const [sending, setSending] = useState(false);
const [feedback, setFeedback] = useState<string | null>(null);
const send = useCallback(async () => {
const text = message.trim();
if (!text || sending) return;
setSending(true);
setFeedback(null);
const res = await steerAgent(agentId, text);
setSending(false);
if (res.ok) {
setMessage("");
setFeedback(`Sent to ${agentName}`);
track("agent_steered");
} else if (res.error === "not_delivered") {
setFeedback("Could not reach that agent (it may have finished).");
} else {
setFeedback("Could not send that message. Try again.");
}
}, [message, sending, agentId, agentName]);
return (
<div className="w-full">
<div className="flex items-center gap-2">
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
void send();
}
}}
placeholder={placeholder}
maxLength={4000}
disabled={sending}
className="flex-1 min-w-0 rounded-lg border border-[#2a2a2a] bg-[rgba(255,255,255,0.03)] px-3 py-2 text-sm text-white placeholder:text-[#555] focus:outline-none focus:border-[#3a3a3a] disabled:opacity-50"
/>
<button
onClick={() => void send()}
disabled={sending || !message.trim()}
className="inline-flex items-center gap-1.5 rounded-lg bg-white px-3 py-2 text-sm font-medium text-black transition-opacity hover:opacity-90 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
>
<Send className="w-3.5 h-3.5" aria-hidden="true" />
{sending ? "Sending" : "Send"}
</button>
</div>
{feedback && <p className="mt-2 text-xs text-[#888]">{feedback}</p>}
</div>
);
}
export default AgentSteerInput;