From bb83ddc611fc5ce7bb976171d9f008560d0dc13d Mon Sep 17 00:00:00 2001 From: Chaim Date: Tue, 30 Jun 2026 19:37:01 +0000 Subject: [PATCH] =?UTF-8?q?feat(case-ui):=20agents=20tab=20=E2=80=94=20ful?= =?UTF-8?q?l=2018i=20(rich=20roster=20+=20surfaced=20pending=20forms)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes mockup 18i beyond the earlier minimal version: - AgentRoster — a 5-up grid of agents, each with a status dot, name, role label and its current activity (derived from the agent's most recent comment). Replaces the simple AgentStatusWidget list at the top of the feed (reset still lives in the banner status-rail popover). - "ממתין לתשובתך" now renders the actual pending InteractionCards (the approve/reject/answer forms) at the top, not just a count pointer — and those pending interactions are excluded from the timeline below so they aren't shown twice. All interaction logic / mutations preserved. tsc --noEmit + eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/cases/agent-activity-feed.tsx | 144 ++++++++++++++++-- 1 file changed, 129 insertions(+), 15 deletions(-) diff --git a/web-ui/src/components/cases/agent-activity-feed.tsx b/web-ui/src/components/cases/agent-activity-feed.tsx index a7ebe5d..b546b1b 100644 --- a/web-ui/src/components/cases/agent-activity-feed.tsx +++ b/web-ui/src/components/cases/agent-activity-feed.tsx @@ -5,7 +5,6 @@ import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; import { Markdown } from "@/components/ui/markdown"; -import { AgentStatusWidget } from "@/components/cases/agent-status-widget"; import { useAgentActivity, useSendComment, @@ -17,6 +16,7 @@ import type { InteractionQuestion, InteractionTask, PaperclipComment, + PaperclipAgentStatus, } from "@/lib/api/agents"; import { formatRelative } from "@/lib/format-date"; import { toast } from "sonner"; @@ -82,6 +82,104 @@ function issueStatusLabel(status: string) { return ISSUE_STATUS_LABELS[status] ?? status; } +/* ── Agent roster (mockup 18i) — who's doing what right now ────── */ + +const AGENT_STATUS_LABEL: Record = { + active: "פעיל", + running: "פעיל", + idle: "ממתין", + error: "שגיאה", +}; + +function agentIsActive(s: string) { + return s === "active" || s === "running"; +} + +function agentStatusDot(s: string) { + return agentIsActive(s) + ? "bg-success shadow-[0_0_6px_rgba(74,124,89,0.6)]" + : s === "error" + ? "bg-danger" + : "bg-rule"; +} + +function agentStatusTone(s: string) { + return agentIsActive(s) + ? "bg-success-bg text-success" + : s === "error" + ? "bg-danger-bg text-danger" + : "bg-rule-soft text-ink-muted"; +} + +function AgentRoster({ + agents, + comments, +}: { + agents: PaperclipAgentStatus[]; + comments: PaperclipComment[]; +}) { + if (!agents.length) return null; + + // "current activity" per agent = its most recent comment body (by name/role). + const latest = new Map(); + for (const a of agents) { + let best: PaperclipComment | null = null; + let bestAt = -1; + for (const c of comments) { + const mine = + (c.agent_name && c.agent_name === a.name) || + (c.agent_role && c.agent_role === a.role); + if (!mine) continue; + const at = c.created_at ? new Date(c.created_at).getTime() : 0; + if (at >= bestAt) { + bestAt = at; + best = c; + } + } + if (best) latest.set(a.id, best.body); + } + + const activeCount = agents.filter((a) => agentIsActive(a.status)).length; + + return ( +
+
+ +

סוכני התיק

+ + {activeCount} פעילים מתוך {agents.length} + +
+
+ {agents.map((a) => { + const active = agentIsActive(a.status); + const activity = latest.get(a.id); + return ( +
+
+ + {a.name} + + {AGENT_STATUS_LABEL[a.status] ?? a.status} + +
+
{roleLabel(a.role)}
+
+ {activity || "—"} +
+
+ ); + })} +
+
+ ); +} + /* ── Time formatting ─────────────────────────────────────────── */ function timeAgo(iso: string | null): string { @@ -690,7 +788,9 @@ export function AgentActivityFeed({ const comments = data.comments ?? []; const interactions = data.interactions ?? []; - const pendingCount = interactions.filter((i) => i.status === "pending").length; + // Pending interactions are surfaced at the top (awaiting-you, mockup 18i) and + // NOT repeated in the timeline below. + const pending = interactions.filter((i) => i.status === "pending"); // Unified, time-sorted feed: comments + interactions interleaved. type FeedItem = @@ -703,11 +803,13 @@ export function AgentActivityFeed({ at: c.created_at ? new Date(c.created_at).getTime() : 0, comment: c, })), - ...interactions.map((i) => ({ - kind: "interaction", - at: i.created_at ? new Date(i.created_at).getTime() : 0, - interaction: i, - })), + ...interactions + .filter((i) => i.status !== "pending") + .map((i) => ({ + kind: "interaction", + at: i.created_at ? new Date(i.created_at).getTime() : 0, + interaction: i, + })), ].sort((a, b) => a.at - b.at); // An issue is "active" if it's not done/cancelled. When everything is closed @@ -719,15 +821,27 @@ export function AgentActivityFeed({ return (
{/* agent roster — who's doing what right now (mockup 18i) */} -
- -
+ - {/* pending interactions surfaced to the top (mockup 18i — awaiting you) */} - {pendingCount > 0 && ( -
- - ממתין לתשובתך — {pendingCount} {pendingCount === 1 ? "בקשה" : "בקשות"} בהמשך הזרם + {/* pending interactions surfaced to the top — the actual forms, not a + pointer (mockup 18i — awaiting you). Excluded from the timeline. */} + {pending.length > 0 && ( +
+
+ + ממתין לתשובתך + + {pending.length} + +
+ {pending.map((i) => ( + + ))}
)}