feat: implement comment posting functionality and notify lead on task unassignment
This commit is contained in:
@@ -277,6 +277,65 @@ def _notify_lead_on_task_create(
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _notify_lead_on_task_unassigned(
|
||||||
|
*,
|
||||||
|
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 = (
|
||||||
|
"TASK BACK IN INBOX\n"
|
||||||
|
+ "\n".join(details)
|
||||||
|
+ "\n\nTake action: assign a new owner or adjust the plan."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
asyncio.run(
|
||||||
|
_send_lead_task_message(
|
||||||
|
session_key=lead.openclaw_session_id,
|
||||||
|
config=config,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
record_activity(
|
||||||
|
session,
|
||||||
|
event_type="task.lead_unassigned_notified",
|
||||||
|
message=f"Lead notified task returned to inbox: {task.title}.",
|
||||||
|
agent_id=lead.id,
|
||||||
|
task_id=task.id,
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
except OpenClawGatewayError as exc:
|
||||||
|
record_activity(
|
||||||
|
session,
|
||||||
|
event_type="task.lead_unassigned_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,
|
||||||
@@ -509,6 +568,15 @@ def update_task(
|
|||||||
agent_id=actor.agent.id if actor.actor_type == "agent" and actor.agent else None,
|
agent_id=actor.agent.id if actor.actor_type == "agent" and actor.agent else None,
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
if task.status == "inbox" and task.assigned_agent_id is None:
|
||||||
|
if previous_status != "inbox" or previous_assigned is not None:
|
||||||
|
board = session.get(Board, task.board_id) if task.board_id else None
|
||||||
|
if board:
|
||||||
|
_notify_lead_on_task_unassigned(
|
||||||
|
session=session,
|
||||||
|
board=board,
|
||||||
|
task=task,
|
||||||
|
)
|
||||||
if task.assigned_agent_id and task.assigned_agent_id != previous_assigned:
|
if task.assigned_agent_id and task.assigned_agent_id != previous_assigned:
|
||||||
if (
|
if (
|
||||||
actor.actor_type == "agent"
|
actor.actor_type == "agent"
|
||||||
|
|||||||
@@ -130,6 +130,9 @@ export default function BoardDetailPage() {
|
|||||||
const [comments, setComments] = useState<TaskComment[]>([]);
|
const [comments, setComments] = useState<TaskComment[]>([]);
|
||||||
const [isCommentsLoading, setIsCommentsLoading] = useState(false);
|
const [isCommentsLoading, setIsCommentsLoading] = useState(false);
|
||||||
const [commentsError, setCommentsError] = useState<string | null>(null);
|
const [commentsError, setCommentsError] = useState<string | null>(null);
|
||||||
|
const [newComment, setNewComment] = useState("");
|
||||||
|
const [isPostingComment, setIsPostingComment] = useState(false);
|
||||||
|
const [postCommentError, setPostCommentError] = useState<string | null>(null);
|
||||||
const [isDetailOpen, setIsDetailOpen] = useState(false);
|
const [isDetailOpen, setIsDetailOpen] = useState(false);
|
||||||
const tasksRef = useRef<Task[]>([]);
|
const tasksRef = useRef<Task[]>([]);
|
||||||
const approvalsRef = useRef<Approval[]>([]);
|
const approvalsRef = useRef<Approval[]>([]);
|
||||||
@@ -777,9 +780,48 @@ export default function BoardDetailPage() {
|
|||||||
setSelectedTask(null);
|
setSelectedTask(null);
|
||||||
setComments([]);
|
setComments([]);
|
||||||
setCommentsError(null);
|
setCommentsError(null);
|
||||||
|
setNewComment("");
|
||||||
|
setPostCommentError(null);
|
||||||
setIsEditDialogOpen(false);
|
setIsEditDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePostComment = async () => {
|
||||||
|
if (!selectedTask || !boardId || !isSignedIn) return;
|
||||||
|
const trimmed = newComment.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
setPostCommentError("Write a message before sending.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsPostingComment(true);
|
||||||
|
setPostCommentError(null);
|
||||||
|
try {
|
||||||
|
const token = await getToken();
|
||||||
|
const response = await fetch(
|
||||||
|
`${apiBase}/api/v1/boards/${boardId}/tasks/${selectedTask.id}/comments`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: token ? `Bearer ${token}` : "",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ message: trimmed }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Unable to send message.");
|
||||||
|
}
|
||||||
|
const created = (await response.json()) as TaskComment;
|
||||||
|
setComments((prev) => [created, ...prev]);
|
||||||
|
setNewComment("");
|
||||||
|
} catch (err) {
|
||||||
|
setPostCommentError(
|
||||||
|
err instanceof Error ? err.message : "Unable to send message.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsPostingComment(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleTaskSave = async (closeOnSuccess = false) => {
|
const handleTaskSave = async (closeOnSuccess = false) => {
|
||||||
if (!selectedTask || !isSignedIn || !boardId) return;
|
if (!selectedTask || !isSignedIn || !boardId) return;
|
||||||
const trimmedTitle = editTitle.trim();
|
const trimmedTitle = editTitle.trim();
|
||||||
@@ -1489,6 +1531,26 @@ 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">
|
||||||
Comments
|
Comments
|
||||||
</p>
|
</p>
|
||||||
|
<div className="space-y-2 rounded-xl border border-slate-200 bg-slate-50 p-3">
|
||||||
|
<Textarea
|
||||||
|
value={newComment}
|
||||||
|
onChange={(event) => setNewComment(event.target.value)}
|
||||||
|
placeholder="Write a message for the assigned agent…"
|
||||||
|
className="min-h-[80px] bg-white"
|
||||||
|
/>
|
||||||
|
{postCommentError ? (
|
||||||
|
<p className="text-xs text-rose-600">{postCommentError}</p>
|
||||||
|
) : null}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handlePostComment}
|
||||||
|
disabled={isPostingComment || !newComment.trim()}
|
||||||
|
>
|
||||||
|
{isPostingComment ? "Sending…" : "Send message"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{isCommentsLoading ? (
|
{isCommentsLoading ? (
|
||||||
<p className="text-sm text-slate-500">Loading comments…</p>
|
<p className="text-sm text-slate-500">Loading comments…</p>
|
||||||
) : commentsError ? (
|
) : commentsError ? (
|
||||||
|
|||||||
@@ -68,10 +68,11 @@ curl -s "$BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks?status=inbox&unassigned=
|
|||||||
|
|
||||||
5) If you do NOT have an in_progress task:
|
5) If you do NOT have an in_progress task:
|
||||||
- If you have **assigned inbox** tasks, move one to in_progress and add a markdown comment describing the update.
|
- If you have **assigned inbox** tasks, move one to in_progress and add a markdown comment describing the update.
|
||||||
- Else if there are **unassigned inbox** tasks, claim one and move it to in_progress with a comment.
|
- If there are **unassigned inbox** tasks, do **not** claim them. Wait for the board lead to assign work.
|
||||||
|
|
||||||
6) Work the task:
|
6) Work the task:
|
||||||
- Post progress comments as you go.
|
- Post progress comments as you go.
|
||||||
|
- Before working, fetch the latest task comments and respond in the task thread if the human asked a question.
|
||||||
- Completion is a two‑step sequence:
|
- Completion is a two‑step sequence:
|
||||||
6a) Post the full response as a markdown comment using:
|
6a) Post the full response as a markdown comment using:
|
||||||
POST $BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks/{TASK_ID}/comments
|
POST $BASE_URL/api/v1/agent/boards/{BOARD_ID}/tasks/{TASK_ID}/comments
|
||||||
|
|||||||
Reference in New Issue
Block a user