feat: add lead notification for new task creation and improve comment ordering

This commit is contained in:
Abhimanyu Saharan
2026-02-05 19:48:30 +05:30
parent 28b4a5bba8
commit 77e37f73b3
2 changed files with 107 additions and 7 deletions

View File

@@ -21,9 +21,16 @@ from app.api.deps import (
) )
from app.core.auth import AuthContext from app.core.auth import AuthContext
from app.db.session import engine, get_session 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.activity_events import ActivityEvent
from app.models.agents import Agent from app.models.agents import Agent
from app.models.boards import Board from app.models.boards import Board
from app.models.gateways import Gateway
from app.models.tasks import Task from app.models.tasks import Task
from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate
from app.services.activity_log import record_activity 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") 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") @router.get("/stream")
async def stream_tasks( async def stream_tasks(
request: Request, request: Request,
@@ -214,6 +299,7 @@ def create_task(
message=f"Task created: {task.title}.", message=f"Task created: {task.title}.",
) )
session.commit() session.commit()
_notify_lead_on_task_create(session=session, board=board, task=task)
return task return task

View File

@@ -358,6 +358,14 @@ export default function BoardDetailPage() {
[tasks, assigneeById], [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( const boardAgents = useMemo(
() => agents.filter((agent) => !boardId || agent.board_id === boardId), () => agents.filter((agent) => !boardId || agent.board_id === boardId),
[agents, boardId], [agents, boardId],
@@ -621,7 +629,7 @@ export default function BoardDetailPage() {
) : null} ) : null}
<aside <aside
className={cn( className={cn(
"fixed right-0 top-0 z-50 h-full w-[420px] max-w-[92vw] transform bg-white shadow-2xl transition-transform", "fixed right-0 top-0 z-50 h-full w-[760px] max-w-[99vw] transform bg-white shadow-2xl transition-transform",
isDetailOpen ? "translate-x-0" : "translate-x-full", isDetailOpen ? "translate-x-0" : "translate-x-full",
)} )}
> >
@@ -648,7 +656,7 @@ export default function BoardDetailPage() {
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500"> <p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
Description Description
</p> </p>
<p className="text-sm text-slate-700"> <p className="text-sm text-slate-700 whitespace-pre-wrap break-words">
{selectedTask?.description || "No description provided."} {selectedTask?.description || "No description provided."}
</p> </p>
</div> </div>
@@ -666,7 +674,7 @@ export default function BoardDetailPage() {
<p className="text-sm text-slate-500">No comments yet.</p> <p className="text-sm text-slate-500">No comments yet.</p>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{comments.map((comment) => ( {orderedComments.map((comment) => (
<div <div
key={comment.id} key={comment.id}
className="rounded-xl border border-slate-200 bg-white p-3" className="rounded-xl border border-slate-200 bg-white p-3"
@@ -681,20 +689,26 @@ export default function BoardDetailPage() {
<span>{formatCommentTimestamp(comment.created_at)}</span> <span>{formatCommentTimestamp(comment.created_at)}</span>
</div> </div>
{comment.message?.trim() ? ( {comment.message?.trim() ? (
<div className="mt-2 text-sm text-slate-900"> <div className="mt-2 text-sm text-slate-900 whitespace-pre-wrap break-words">
<ReactMarkdown <ReactMarkdown
components={{ components={{
p: ({ ...props }) => ( p: ({ ...props }) => (
<p className="text-sm text-slate-900" {...props} /> <p
className="text-sm text-slate-900 whitespace-pre-wrap break-words"
{...props}
/>
), ),
ul: ({ ...props }) => ( ul: ({ ...props }) => (
<ul <ul
className="list-disc pl-5 text-sm text-slate-900" className="list-disc pl-5 text-sm text-slate-900 whitespace-pre-wrap break-words"
{...props} {...props}
/> />
), ),
li: ({ ...props }) => ( li: ({ ...props }) => (
<li className="mb-1 text-sm text-slate-900" {...props} /> <li
className="mb-1 text-sm text-slate-900 whitespace-pre-wrap break-words"
{...props}
/>
), ),
strong: ({ ...props }) => ( strong: ({ ...props }) => (
<strong <strong