From 77e37f73b31abcb3762162a57ce3c50ec2ebcfb2 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 5 Feb 2026 19:48:30 +0530 Subject: [PATCH] feat: add lead notification for new task creation and improve comment ordering --- backend/app/api/tasks.py | 86 ++++++++++++++++++++++ frontend/src/app/boards/[boardId]/page.tsx | 28 +++++-- 2 files changed, 107 insertions(+), 7 deletions(-) diff --git a/backend/app/api/tasks.py b/backend/app/api/tasks.py index c8116430..9e861aa8 100644 --- a/backend/app/api/tasks.py +++ b/backend/app/api/tasks.py @@ -21,9 +21,16 @@ from app.api.deps import ( ) from app.core.auth import AuthContext from app.db.session import engine, get_session +from app.integrations.openclaw_gateway import ( + GatewayConfig as GatewayClientConfig, + OpenClawGatewayError, + ensure_session, + send_message, +) from app.models.activity_events import ActivityEvent from app.models.agents import Agent from app.models.boards import Board +from app.models.gateways import Gateway from app.models.tasks import Task from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate from app.services.activity_log import record_activity @@ -124,6 +131,84 @@ def _serialize_comment(event: ActivityEvent) -> dict[str, object]: return TaskCommentRead.model_validate(event).model_dump(mode="json") +def _gateway_config(session: Session, board: Board) -> GatewayClientConfig | None: + if not board.gateway_id: + return None + gateway = session.get(Gateway, board.gateway_id) + if gateway is None or not gateway.url: + return None + return GatewayClientConfig(url=gateway.url, token=gateway.token) + + +async def _send_lead_task_message( + *, + session_key: str, + config: GatewayClientConfig, + message: str, +) -> None: + await ensure_session(session_key, config=config, label="Lead Agent") + await send_message(message, session_key=session_key, config=config, deliver=False) + + +def _notify_lead_on_task_create( + *, + session: Session, + board: Board, + task: Task, +) -> None: + lead = session.exec( + select(Agent) + .where(Agent.board_id == board.id) + .where(Agent.is_board_lead.is_(True)) + ).first() + if lead is None or not lead.openclaw_session_id: + return + config = _gateway_config(session, board) + if config is None: + return + description = (task.description or "").strip() + if len(description) > 500: + description = f"{description[:497]}..." + details = [ + f"Board: {board.name}", + f"Task: {task.title}", + f"Task ID: {task.id}", + f"Status: {task.status}", + ] + if description: + details.append(f"Description: {description}") + message = ( + "NEW TASK ADDED\n" + + "\n".join(details) + + "\n\nTake action: triage, assign, or plan next steps." + ) + try: + asyncio.run( + _send_lead_task_message( + session_key=lead.openclaw_session_id, + config=config, + message=message, + ) + ) + record_activity( + session, + event_type="task.lead_notified", + message=f"Lead agent notified for task: {task.title}.", + agent_id=lead.id, + task_id=task.id, + ) + session.commit() + except OpenClawGatewayError as exc: + record_activity( + session, + event_type="task.lead_notify_failed", + message=f"Lead notify failed: {exc}", + agent_id=lead.id, + task_id=task.id, + ) + session.commit() + + @router.get("/stream") async def stream_tasks( request: Request, @@ -214,6 +299,7 @@ def create_task( message=f"Task created: {task.title}.", ) session.commit() + _notify_lead_on_task_create(session=session, board=board, task=task) return task diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index e9302568..7a4380d1 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -358,6 +358,14 @@ export default function BoardDetailPage() { [tasks, assigneeById], ); + const orderedComments = useMemo(() => { + return [...comments].sort((a, b) => { + const aTime = new Date(a.created_at).getTime(); + const bTime = new Date(b.created_at).getTime(); + return bTime - aTime; + }); + }, [comments]); + const boardAgents = useMemo( () => agents.filter((agent) => !boardId || agent.board_id === boardId), [agents, boardId], @@ -621,7 +629,7 @@ export default function BoardDetailPage() { ) : null}