Files
legal-ai/web-ui/src/components/cases/agent-status-widget.tsx
Chaim 82844a63c2
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
feat(agents): reset button + endpoint to clear stuck agent error state
Adds a one-click 'reset agents' action for cases where writer/QA agents
are stuck in Paperclip's error state (triggered by recovery loop or
failed run). Addresses the root cause documented in
reference_paperclip_recovery_loops: reassigns open issues from agents
back to the chair user, and calls reset_agent_session for each error
agent to clear wedged runtime sessions.

Changes:
- paperclip_client.py: reset_case_agents() — reassigns stuck issues +
  clears error status in Paperclip DB + calls reset_agent_session
- agent_platform_port.py: exports pc_reset_case_agents (G12 gate)
- app.py: POST /api/cases/{case_number}/agents/reset endpoint
- agents.ts: useResetCaseAgents mutation hook + AgentResetResult type
- agent-status-widget.tsx: 'אפס' button (shown only when error agents
  exist) with Dialog confirmation + loading state + toast feedback

Invariants: G12 (Paperclip only via port), G2 (no parallel path —
uses existing reset_agent_session + pc_get_case_issues).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 14:53:50 +00:00

165 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { useAgentActivity, useResetCaseAgents } from "@/lib/api/agents";
import type { PaperclipAgentStatus } from "@/lib/api/agents";
import { Bot, RotateCcw, Loader2 } from "lucide-react";
import { toast } from "sonner";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
/* ── Status dot colors ───────────────────────────────────────── */
const STATUS_DOT: Record<string, string> = {
active: "bg-emerald-500 shadow-[0_0_6px_rgba(16,185,129,0.6)]",
running: "bg-emerald-500 shadow-[0_0_6px_rgba(16,185,129,0.6)]",
idle: "bg-gray-300",
error: "bg-red-500",
};
const STATUS_LABEL: Record<string, string> = {
active: "פעיל",
running: "פעיל",
idle: "ממתין",
error: "שגיאה",
};
function statusDot(status: string) {
return STATUS_DOT[status] ?? STATUS_DOT.idle;
}
/* ── Agent row ───────────────────────────────────────────────── */
function AgentRow({ agent }: { agent: PaperclipAgentStatus }) {
return (
<div className="flex items-center gap-2 py-1">
<span
className={`w-2 h-2 rounded-full flex-shrink-0 ${statusDot(agent.status)}`}
/>
<span className="text-xs text-ink truncate">{agent.name}</span>
<span className="text-[10px] text-ink-faint ms-auto">
{STATUS_LABEL[agent.status] ?? agent.status}
</span>
</div>
);
}
/* ── Reset button ─────────────────────────────────────────────── */
function ResetAgentsButton({ caseNumber }: { caseNumber: string }) {
const [open, setOpen] = useState(false);
const reset = useResetCaseAgents(caseNumber);
function handleConfirm() {
reset.mutate(undefined, {
onSuccess: (result) => {
setOpen(false);
const agentNames = result.reset_agents
.filter((a) => a.ok)
.map((a) => a.name)
.join(", ");
const msg = agentNames
? `סוכנים אופסו: ${agentNames}`
: "אופסו בהצלחה";
toast.success(msg);
},
onError: () => {
setOpen(false);
toast.error("שגיאה בביצוע האיפוס");
},
});
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<button
className="text-[10px] text-ink-faint hover:text-red-600 flex items-center gap-0.5 transition-colors"
title="אפס סוכנים תקועים"
>
<RotateCcw className="w-3 h-3" />
<span>אפס</span>
</button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>איפוס סוכנים</DialogTitle>
<DialogDescription>
פעולה זו תאפס את כל הסוכנים במצב שגיאה ותחזיר issues פתוחים לניהול ידני.
פעולה הפיכה ניתן להפעיל את הסוכנים מחדש אחרי האיפוס.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)} disabled={reset.isPending}>
ביטול
</Button>
<Button
variant="destructive"
onClick={handleConfirm}
disabled={reset.isPending}
>
{reset.isPending ? (
<>
<Loader2 className="w-3.5 h-3.5 ms-1.5 animate-spin" />
מאפס...
</>
) : (
"אפס סוכנים"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
/* ── Widget ───────────────────────────────────────────────────── */
export function AgentStatusWidget({
caseNumber,
}: {
caseNumber: string;
}) {
const { data } = useAgentActivity(caseNumber);
// Don't render if no Paperclip project yet
if (!data?.issues?.length) return null;
const agents = data.agents ?? [];
const activeCount = agents.filter((a) => a.status === "active" || a.status === "running").length;
const hasErrors = agents.some((a) => a.status === "error");
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-xs font-medium text-navy">
<Bot className="w-3.5 h-3.5" />
<span>סוכנים</span>
</div>
<div className="flex items-center gap-2">
{hasErrors && <ResetAgentsButton caseNumber={caseNumber} />}
{agents.length > 0 && (
<span className="text-[10px] text-ink-faint">
{activeCount} פעילים מתוך {agents.length}
</span>
)}
</div>
</div>
<div className="space-y-0.5">
{agents.map((agent) => (
<AgentRow key={agent.id} agent={agent} />
))}
</div>
</div>
);
}