From 9e7d98a47a6df88d40aa41f1157aa8e6d9cf44df Mon Sep 17 00:00:00 2001 From: Chaim Date: Tue, 7 Jul 2026 15:48:26 +0000 Subject: [PATCH] =?UTF-8?q?feat(agents-tab):=20=D7=94=D7=95=D7=A8=D7=90?= =?UTF-8?q?=D7=94=20=D7=9C=D7=9E=D7=A2=D7=9C=D7=94=20+=20=D7=A4=D7=A2?= =?UTF-8?q?=D7=99=D7=9C=D7=95=D7=AA=20=D7=9E=D7=A7=D7=95=D7=A4=D7=9C=D7=AA?= =?UTF-8?q?=20+=20=D7=91=D7=95=D7=A8=D7=A8-=D7=99=D7=A2=D7=93=20=D7=A2?= =?UTF-8?q?=D7=9D=20"=D7=94=D7=A8=D7=A6=D7=94=20=D7=97=D7=93=D7=A9=D7=94"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit טאב הסוכנים בדף-התיק — מימוש מוקאפ X17 מאושר (18i3), פותר את התלונה החוזרת שהיו"ר נחת בתחתית העמוד, שדה-ההוראה למטה, והטיוטה החדשה נבלעה בגלילה. Frontend (web-ui/agent-activity-feed.tsx): - **שדה-ההוראה עלה לראש הטאב** (Composer), מעל הפעילות — הכי זמין, בלי גלילה. בוטל ה-auto-scroll-to-bottom (endRef/useEffect הוסרו). - **כל הפעילות מקופלת כברירת-מחדל** — קבוצות-המשימה וגם "ממתין לתשובתך" (defaultOpen=false); היו"ר פותח כדי להסתכל. סדר החדש-למעלה (תג "החדש למעלה"). - **בורר-יעד** (TargetSelector): מציג לאיזו משימה ההוראה תלך, בוחר משימה פעילה, מסמן משימות סגורות כמנוטרלות ("שליחה אליהן לא תעיר סוכן"), אזהרת יעד-סגור עם כפתור-שליחה מנוטרל, ואופציית **"פתח הרצה חדשה"**. מחשב את יעד-ברירת-המחדל בצד-לקוח במראָה ל-pick_default_comment_target. Backend: - `open_ceo_run` (paperclip_client) — פותח הרצת-CEO טרייה דרך פרימיטיב ה-CEO-child (#227): יוצר issue-ילד בבעלות CEO ומעיר עליו, ועוקף את issue_assignee_changed על issue בבעלות-אדם (הבאג של #228). נחשף דרך ה-Port כ-`pc_open_ceo_run`. - `/agents/comment` מקבל `new_run:bool`; `PaperclipIssue` מחזיר `parent_id` (לבורר בצד-לקוח). Invariants: G1 (נרמול-במקור), G2 (אין מסלול-ניתוב מקביל), G12 (מגע-Paperclip דרך ה-Port). מקדם TaskMaster #228 (מנתב הרצה-חדשה דרך פרימיטיב ה-CEO-child). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/cases/agent-activity-feed.tsx | 403 ++++++++++++++---- web-ui/src/lib/api/agents.ts | 12 +- web/agent_platform_port.py | 5 + web/app.py | 30 +- web/paperclip_client.py | 87 ++++ 5 files changed, 457 insertions(+), 80 deletions(-) diff --git a/web-ui/src/components/cases/agent-activity-feed.tsx b/web-ui/src/components/cases/agent-activity-feed.tsx index 4609d99..ab11c9a 100644 --- a/web-ui/src/components/cases/agent-activity-feed.tsx +++ b/web-ui/src/components/cases/agent-activity-feed.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRef, useState, useEffect, useMemo } from "react"; +import { useState, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; @@ -34,6 +34,9 @@ import { HelpCircle, ChevronDown, Ban, + Target, + Plus, + AlertTriangle, } from "lucide-react"; /* ── Role → color mapping ────────────────────────────────────── */ @@ -818,6 +821,312 @@ function IssueGroup({ ); } +/* ── Compose: directive box + target selector (top of tab) ────── */ + +const isOpenStatus = (s: string) => s !== "done" && s !== "cancelled"; + +/** Client mirror of the server's pick_default_comment_target + * (web/paperclip_client.py): prefer the newest OPEN top-level issue (the live + * CEO main issue), then any open issue, then any top-level, else newest. + * `issues` arrives oldest→newest, so the last match is the newest. */ +function pickDefaultTarget(issues: PaperclipIssue[]): PaperclipIssue | null { + if (!issues.length) return null; + const isTop = (i: PaperclipIssue) => i.parent_id == null; + const preds: ((i: PaperclipIssue) => boolean)[] = [ + (i) => isOpenStatus(i.status) && isTop(i), + (i) => isOpenStatus(i.status), + isTop, + ]; + for (const p of preds) { + const m = issues.filter(p); + if (m.length) return m[m.length - 1]; + } + return issues[issues.length - 1]; +} + +const STATUS_TONE: Record = { + in_progress: "bg-emerald-100 text-emerald-700", + in_review: "bg-amber-100 text-amber-700", + todo: "bg-emerald-100 text-emerald-700", + backlog: "bg-emerald-100 text-emerald-700", + blocked: "bg-red-100 text-red-700", + done: "bg-gray-100 text-gray-500", + cancelled: "bg-red-50 text-red-600", +}; +function statusTone(s: string) { + return STATUS_TONE[s] ?? "bg-gray-100 text-gray-600"; +} + +/** Where a chair instruction is routed. `auto` lets the server pick the live + * CEO main issue; `issue` targets an explicit one; `new_run` opens a fresh + * CEO-owned run (bypasses the human-owned-issue cancellation). */ +type Target = + | { kind: "auto" } + | { kind: "issue"; id: string } + | { kind: "new_run" }; + +function TargetSelector({ + issues, + target, + onChange, +}: { + issues: PaperclipIssue[]; + target: Target; + onChange: (t: Target) => void; +}) { + const [open, setOpen] = useState(false); + const defaultIssue = useMemo(() => pickDefaultTarget(issues), [issues]); + const activeIssues = issues.filter((i) => isOpenStatus(i.status)); + const closedIssues = issues.filter((i) => !isOpenStatus(i.status)); + + // The issue the pill currently represents (explicit pick, or the auto default). + const effectiveIssue = + target.kind === "issue" + ? issues.find((i) => i.id === target.id) ?? null + : target.kind === "auto" + ? defaultIssue + : null; + + return ( +
+ + + {open && ( + <> +
setOpen(false)} + aria-hidden + /> +
+
+ משימות פעילות +
+ {activeIssues.map((i) => { + const selected = + (target.kind === "issue" && target.id === i.id) || + (target.kind === "auto" && defaultIssue?.id === i.id); + return ( + + ); + })} + + {closedIssues.length > 0 && ( + <> +
+
+ משימות סגורות · שליחה אליהן לא תעיר סוכן +
+ {closedIssues.map((i) => ( +
+ + + {i.identifier} + + + {shortIssueTitle(i.title)} + + + {issueStatusLabel(i.status)} + +
+ ))} + + )} + +
+ +
+ + )} +
+ ); +} + +function Composer({ + caseNumber, + issues, +}: { + caseNumber: string; + issues: PaperclipIssue[]; +}) { + const sendComment = useSendComment(caseNumber); + const [body, setBody] = useState(""); + const [target, setTarget] = useState({ kind: "auto" }); + + const selectedIssue = + target.kind === "issue" ? issues.find((i) => i.id === target.id) ?? null : null; + const closedTarget = !!selectedIssue && !isOpenStatus(selectedIssue.status); + + const handleSend = () => { + if (!body.trim() || closedTarget) return; + const vars = + target.kind === "new_run" + ? { body: body.trim(), new_run: true } + : target.kind === "issue" + ? { body: body.trim(), issue_id: target.id } + : { body: body.trim() }; + sendComment.mutate(vars, { + onSuccess: (res) => { + setBody(""); + setTarget({ kind: "auto" }); + toast.success( + res.new_run + ? `נפתחה הרצה חדשה — ${res.issue_identifier}` + : `נשלח ל-${res.issue_identifier}`, + ); + }, + onError: () => toast.error("שגיאה בשליחת ההודעה"), + }); + }; + + return ( +
+
+ +

הוראה לסוכנים

+ + Ctrl+Enter לשליחה + +
+ +
+ + שליחה אל: + + +
+ + {closedTarget && ( +
+ +
+ יעד סגור. שליחה למשימה סגורה לא תעיר סוכן — + ההוראה לא תטופל. בחר משימה פעילה או{" "} + + . +
+
+ )} + +