mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
Render stages tab as agent session with compact tool call rows
Replace stage list with single-stage agent session view showing system prompt, assistant messages, and grouped tool calls. Tool call/result pairs are combined into compact expandable rows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cb98825d0b
commit
854f637b60
1 changed files with 192 additions and 78 deletions
|
|
@ -1,70 +1,26 @@
|
|||
import { useState } from "react";
|
||||
import { Link, useParams } from "react-router";
|
||||
import { ChevronRightIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckCircleIcon, ArrowPathIcon, PauseCircleIcon, XCircleIcon } from "@heroicons/react/24/solid";
|
||||
import { DocumentTextIcon, MapIcon, CommandLineIcon, ChatBubbleLeftIcon, WrenchScrewdriverIcon } from "@heroicons/react/24/outline";
|
||||
import { findRun } from "../data/runs";
|
||||
import { workflowData } from "./workflow-detail";
|
||||
|
||||
export const handle = { wide: true };
|
||||
|
||||
type StageStatus = "completed" | "running" | "pending" | "failed";
|
||||
|
||||
interface StageEvent {
|
||||
time: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Stage {
|
||||
name: string;
|
||||
status: StageStatus;
|
||||
duration: string;
|
||||
events: StageEvent[];
|
||||
}
|
||||
|
||||
const stages: Stage[] = [
|
||||
{
|
||||
name: "Detect Drift",
|
||||
status: "completed",
|
||||
duration: "1m 12s",
|
||||
events: [
|
||||
{ time: "0s", description: "Loading environment configs for production and staging" },
|
||||
{ time: "8s", description: "Diffing infrastructure state files" },
|
||||
{ time: "24s", description: "Comparing application config values" },
|
||||
{ time: "45s", description: "Analyzing schema differences" },
|
||||
{ time: "1m 02s", description: "Drift detected in 3 resources" },
|
||||
{ time: "1m 12s", description: "Stage completed" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Propose Changes",
|
||||
status: "completed",
|
||||
duration: "2m 34s",
|
||||
events: [
|
||||
{ time: "0s", description: "Generating reconciliation plan" },
|
||||
{ time: "18s", description: "Computing minimal patch for Redis config" },
|
||||
{ time: "52s", description: "Computing minimal patch for IAM policies" },
|
||||
{ time: "1m 30s", description: "Computing minimal patch for env variables" },
|
||||
{ time: "2m 10s", description: "Validating proposed changes against constraints" },
|
||||
{ time: "2m 34s", description: "Stage completed — 3 patches ready" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Review Changes",
|
||||
status: "completed",
|
||||
duration: "0m 45s",
|
||||
events: [
|
||||
{ time: "0s", description: "Presenting changes for review" },
|
||||
{ time: "0m 32s", description: "All patches approved" },
|
||||
{ time: "0m 45s", description: "Stage completed" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Apply Changes",
|
||||
status: "running",
|
||||
duration: "1m 58s",
|
||||
events: [
|
||||
{ time: "0s", description: "Applying Redis config patch" },
|
||||
{ time: "22s", description: "Redis config applied successfully" },
|
||||
{ time: "35s", description: "Applying IAM policy patch" },
|
||||
{ time: "1m 10s", description: "IAM policy applied successfully" },
|
||||
{ time: "1m 24s", description: "Applying env variable patch" },
|
||||
],
|
||||
},
|
||||
{ name: "Detect Drift", status: "completed", duration: "1m 12s" },
|
||||
{ name: "Propose Changes", status: "completed", duration: "2m 34s" },
|
||||
{ name: "Review Changes", status: "completed", duration: "0m 45s" },
|
||||
{ name: "Apply Changes", status: "running", duration: "1m 58s" },
|
||||
];
|
||||
|
||||
const statusConfig: Record<StageStatus, { icon: typeof CheckCircleIcon; color: string }> = {
|
||||
|
|
@ -74,47 +30,205 @@ const statusConfig: Record<StageStatus, { icon: typeof CheckCircleIcon; color: s
|
|||
failed: { icon: XCircleIcon, color: "text-coral" },
|
||||
};
|
||||
|
||||
function StageRow({ stage }: { stage: Stage }) {
|
||||
interface ToolUse {
|
||||
toolName: string;
|
||||
args: string;
|
||||
result: string;
|
||||
}
|
||||
|
||||
type TurnType =
|
||||
| { kind: "system"; content: string }
|
||||
| { kind: "assistant"; content: string }
|
||||
| { kind: "tool"; tools: ToolUse[] };
|
||||
|
||||
const selectedStage = stages[0];
|
||||
|
||||
const turns: TurnType[] = [
|
||||
{
|
||||
kind: "system",
|
||||
content: `You are a drift detection agent. Compare the production and staging environments and identify any configuration or code drift.\n\nSource: production\nTarget: staging\nThreshold: warn`,
|
||||
},
|
||||
{
|
||||
kind: "assistant",
|
||||
content: "I'll start by loading the environment configurations for both production and staging to compare them.",
|
||||
},
|
||||
{
|
||||
kind: "tool",
|
||||
tools: [
|
||||
{
|
||||
toolName: "read_file",
|
||||
args: `{ "path": "environments/production/config.toml" }`,
|
||||
result: `[redis]\nhost = "redis-prod.internal"\nport = 6379\nmax_connections = 200\ntls = true\n\n[iam]\nrole_arn = "arn:aws:iam::123456:role/prod-api"\nsession_duration = 3600`,
|
||||
},
|
||||
{
|
||||
toolName: "read_file",
|
||||
args: `{ "path": "environments/staging/config.toml" }`,
|
||||
result: `[redis]\nhost = "redis-staging.internal"\nport = 6379\nmax_connections = 100\ntls = false\n\n[iam]\nrole_arn = "arn:aws:iam::123456:role/staging-api"\nsession_duration = 1800`,
|
||||
},
|
||||
{
|
||||
toolName: "diff_configs",
|
||||
args: `{ "source": "environments/production/config.toml", "target": "environments/staging/config.toml" }`,
|
||||
result: `3 differences found:\n redis.max_connections: 200 → 100\n redis.tls: true → false\n iam.session_duration: 3600 → 1800`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "assistant",
|
||||
content: "I've detected drift in 3 resources between production and staging:\n\n1. **redis.max_connections** — production has 200, staging has 100\n2. **redis.tls** — enabled in production, disabled in staging\n3. **iam.session_duration** — production uses 3600s, staging uses 1800s\n\nThe TLS mismatch is the most critical — staging should match production's TLS configuration for accurate testing. The connection pool and session duration differences may be intentional for cost reasons but should be verified.",
|
||||
},
|
||||
];
|
||||
|
||||
function ToolRow({ tool }: { tool: ToolUse }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const config = statusConfig[stage.status];
|
||||
const Icon = config.icon;
|
||||
|
||||
return (
|
||||
<div className="border-b border-white/[0.06] last:border-b-0">
|
||||
<div className="border-b border-white/[0.04] last:border-b-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-white/[0.02]"
|
||||
className="flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left transition-colors hover:bg-white/[0.02]"
|
||||
>
|
||||
<ChevronRightIcon
|
||||
className={`size-4 shrink-0 text-navy-600 transition-transform duration-150 ${open ? "rotate-90" : ""}`}
|
||||
/>
|
||||
<Icon className={`size-5 shrink-0 ${config.color} ${stage.status === "running" ? "animate-spin" : ""}`} />
|
||||
<span className="flex-1 text-sm text-ice-100">{stage.name}</span>
|
||||
<span className="font-mono text-xs tabular-nums text-navy-600">{stage.duration}</span>
|
||||
<ChevronRightIcon className={`size-3 shrink-0 text-navy-600 transition-transform duration-150 ${open ? "rotate-90" : ""}`} />
|
||||
<WrenchScrewdriverIcon className="size-3.5 shrink-0 text-navy-600" />
|
||||
<span className="font-mono text-xs text-ice-300">{tool.toolName}</span>
|
||||
<span className="truncate font-mono text-xs text-navy-600">{tool.args}</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="relative ml-[2.75rem] border-l border-white/[0.06] pb-3">
|
||||
{stage.events.map((event, i) => (
|
||||
<div key={i} className="relative flex gap-3 py-1.5 pl-5 pr-4">
|
||||
<span className="absolute left-[-3px] top-1/2 size-1.5 -translate-y-1/2 rounded-full bg-navy-600" />
|
||||
<span className="shrink-0 font-mono text-xs tabular-nums text-navy-600 w-16 text-right">{event.time}</span>
|
||||
<span className="text-xs text-ice-300">{event.description}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="space-y-px bg-white/[0.01] px-2.5 pb-2 pt-1">
|
||||
<div className="rounded bg-white/[0.02] px-2.5 py-2">
|
||||
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-navy-600">Args</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-ice-300">{tool.args}</pre>
|
||||
</div>
|
||||
<div className="rounded bg-white/[0.02] px-2.5 py-2">
|
||||
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-navy-600">Result</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-ice-300">{tool.result}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RunStages() {
|
||||
function ToolBlock({ tools }: { tools: ToolUse[] }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-white/[0.06] bg-navy-800/50 overflow-hidden">
|
||||
{stages.map((stage) => (
|
||||
<StageRow key={stage.name} stage={stage} />
|
||||
<div className="rounded-lg border border-white/[0.06] bg-white/[0.01] overflow-hidden">
|
||||
{tools.map((tool, i) => (
|
||||
<ToolRow key={i} tool={tool} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SystemBlock({ content }: { content: string }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-amber/10 bg-amber/5 overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-3 py-2">
|
||||
<CommandLineIcon className="size-4 shrink-0 text-amber" />
|
||||
<span className="text-xs font-medium text-ice-300">System Prompt</span>
|
||||
</div>
|
||||
<div className="border-t border-white/[0.04] px-3 py-2.5">
|
||||
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-ice-300">{content}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AssistantBlock({ content }: { content: string }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-teal-500/10 bg-teal-500/5 overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-3 py-2">
|
||||
<ChatBubbleLeftIcon className="size-4 shrink-0 text-teal-500" />
|
||||
<span className="text-xs font-medium text-ice-300">Assistant</span>
|
||||
</div>
|
||||
<div className="border-t border-white/[0.04] px-3 py-2.5">
|
||||
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-ice-300">{content}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RunStages() {
|
||||
const { id } = useParams();
|
||||
const run = findRun(id ?? "");
|
||||
const workflow = run ? workflowData[run.workflow] : undefined;
|
||||
const selectedConfig = statusConfig[selectedStage.status];
|
||||
const SelectedIcon = selectedConfig.icon;
|
||||
|
||||
return (
|
||||
<div className="flex gap-6">
|
||||
<nav className="w-56 shrink-0 space-y-6">
|
||||
<div>
|
||||
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-navy-600">Stages</h3>
|
||||
<ul className="mt-2 space-y-0.5">
|
||||
{stages.map((stage) => {
|
||||
const config = statusConfig[stage.status];
|
||||
const Icon = config.icon;
|
||||
const isSelected = stage.name === selectedStage.name;
|
||||
return (
|
||||
<li key={stage.name}>
|
||||
<Link
|
||||
to={`/runs/${id}/stages`}
|
||||
className={`flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors ${
|
||||
isSelected
|
||||
? "bg-white/[0.06] text-white"
|
||||
: "text-ice-300 hover:bg-white/[0.04] hover:text-white"
|
||||
}`}
|
||||
>
|
||||
<Icon className={`size-4 shrink-0 ${config.color} ${stage.status === "running" ? "animate-spin" : ""}`} />
|
||||
<span className="flex-1 truncate">{stage.name}</span>
|
||||
<span className="font-mono text-xs tabular-nums text-navy-600">{stage.duration}</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{workflow && (
|
||||
<div>
|
||||
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-navy-600">Workflow</h3>
|
||||
<ul className="mt-2 space-y-0.5">
|
||||
<li>
|
||||
<Link
|
||||
to={`/workflows/${run?.workflow}`}
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
|
||||
>
|
||||
<DocumentTextIcon className="size-4 shrink-0 text-navy-600" />
|
||||
Run Configuration
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to={`/workflows/${run?.workflow}/diagram`}
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
|
||||
>
|
||||
<MapIcon className="size-4 shrink-0 text-navy-600" />
|
||||
Workflow Graph
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<div className="min-w-0 flex-1 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<SelectedIcon className={`size-5 ${selectedConfig.color}`} />
|
||||
<h3 className="text-sm font-medium text-white">{selectedStage.name}</h3>
|
||||
<span className="font-mono text-xs text-navy-600">{selectedStage.duration}</span>
|
||||
</div>
|
||||
|
||||
{turns.map((turn, i) => {
|
||||
switch (turn.kind) {
|
||||
case "system":
|
||||
return <SystemBlock key={i} content={turn.content} />;
|
||||
case "assistant":
|
||||
return <AssistantBlock key={i} content={turn.content} />;
|
||||
case "tool":
|
||||
return <ToolBlock key={i} tools={turn.tools} />;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue