feat(agents-panel): היררכיה, סמן-כהושלם ידני, ובורר-יעד נגלל (5 תיקוני יו"ר)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 36s
Lint — undefined names / undefined-names (pull_request) Successful in 11s

טאב הסוכנים בתיק — 5 תיקונים לבקשת היו"ר, מאושרים דרך שער-העיצוב
(Claude Design X17, כרטיס 18i4):

1. היררכיה — הרצה שנפתחה תחת השורש (למשל CMP-220 תחת CMP-189) מסומנת
   בתג "↳ <parent>" גם בציר-הזמן וגם בבורר-היעד (בהזחה). הנתון parent_id
   כבר הגיע מה-API — תצוגה בלבד.
2. סמן-כהושלם / בטל ידני — כל משימה פתוחה מקבלת פעולות בכותרת. פותר
   "אין אפשרות לסמן ידנית" (superseded runs נתקעו ב-in_review בלי מנגנון
   סגירה). endpoint חדש: POST /api/cases/{n}/agents/issue-status → דרך
   ה-Port (pc_set_issue_status), סגירה loop-safe ישירה ל-DB, בלי wakeup.
3. בורר-היעד נגלל — max-height + overflow פנימי, כך שהתיבה לא בורחת
   מגבולות העמוד. פותר "לא רואים את התחתית / לא מגיעים לפתיחת הליך".
4. "פתח הרצה חדשה" עלה לראש הרשימה, מעל "משימות פעילות", ומודגש.
5. קו-הפרדה בין משימות בציר-הזמן עבה יותר (border-b-2 rule).

G12: כל מגע-פלטפורמה עובר דרך agent_platform_port. אימות: lint + tsc +
next build עוברים.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-23 12:25:36 +00:00
parent 4d227a4999
commit cb4aa703ec
5 changed files with 275 additions and 65 deletions

View File

@@ -988,6 +988,70 @@ async def escalate_issue(
}
async def set_issue_status(
issue_id: str, status: str, company_id: str = "",
) -> dict:
"""Manually close a case issue to ``done`` or ``cancelled`` from the chair UI.
The chair needs to tidy the agents board by hand: superseded "הרצה חדשה" runs
that were replaced by a later run pile up in ``in_review`` with no way to close
them — there was no manual control, so they had to be closed out-of-band. This
is the loop-safe primitive behind the board's "סמן כהושלם / בטל" actions.
Only the two **terminal** statuses are accepted (``CLOSED_ISSUE_STATUSES``) —
this tidies the board, it does not drive the workflow (agents own the
todo/in_progress/in_review transitions). Direct-DB in one transaction,
mirroring :func:`escalate_issue`: bypasses Paperclip's disposition resolver
(whose multi-PATCH/``issue.released`` behaviour *causes* the recovery loops),
sets the matching close timestamp, clears any agent assignee so no recovery
sweep re-acts on it, and records an ``author_type='system'`` audit note (inert
w.r.t. the user-comment routing sweep, so it will not wake an agent). No wakeup.
"""
if status not in CLOSED_ISSUE_STATUSES:
return {
"ok": False,
"error": f"invalid status {status!r}; expected one of {tuple(CLOSED_ISSUE_STATUSES)}",
}
# status is validated against the frozenset above, so the interpolated column
# name is one of exactly two literals — no injection surface.
ts_col = "completed_at" if status == "done" else "cancelled_at"
note = (
"✓ סומן ידנית כהושלם ע\"י היו\"ר"
if status == "done"
else "✕ בוטל ידנית ע\"י היו\"ר"
)
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
try:
async with conn.transaction():
row = await conn.fetchrow(
f"""UPDATE issues
SET status=$1, {ts_col}=now(),
assignee_agent_id=null, updated_at=now()
WHERE id=$2::uuid
RETURNING id, identifier, company_id""",
status, issue_id,
)
if not row:
return {"ok": False, "error": f"issue {issue_id} not found"}
cid = company_id or str(row["company_id"])
await conn.execute(
"""INSERT INTO issue_comments (id, company_id, issue_id, body, author_type)
VALUES ($1::uuid, $2::uuid, $3::uuid, $4, 'system')""",
str(uuid.uuid4()), cid, issue_id, note,
)
finally:
await conn.close()
logger.info("Chair set issue %s%s", issue_id, status)
return {
"ok": True,
"id": str(row["id"]),
"identifier": row["identifier"],
"status": status,
}
async def respond_to_interaction(
issue_id: str, interaction_id: str, payload: dict,
) -> dict: