diff --git a/backend/app/api/board_memory.py b/backend/app/api/board_memory.py index d60f477..e861ba1 100644 --- a/backend/app/api/board_memory.py +++ b/backend/app/api/board_memory.py @@ -65,9 +65,10 @@ async def _send_agent_message( config: GatewayClientConfig, agent_name: str, message: str, + deliver: bool = False, ) -> None: await ensure_session(session_key, config=config, label=agent_name) - await send_message(message, session_key=session_key, config=config, deliver=False) + await send_message(message, session_key=session_key, config=config, deliver=deliver) async def _fetch_memory_events( @@ -102,6 +103,31 @@ async def _notify_chat_targets( config = await _gateway_config(session, board) if config is None: return + + normalized = memory.content.strip() + command = normalized.lower() + # Special-case control commands to reach all board agents. + # These are intended to be parsed verbatim by agent runtimes. + if command in {"/pause", "/resume"}: + statement = select(Agent).where(col(Agent.board_id) == board.id) + targets = list(await session.exec(statement)) + for agent in targets: + if actor.actor_type == "agent" and actor.agent and agent.id == actor.agent.id: + continue + if not agent.openclaw_session_id: + continue + try: + await _send_agent_message( + session_key=agent.openclaw_session_id, + config=config, + agent_name=agent.name, + message=command, + deliver=True, + ) + except OpenClawGatewayError: + continue + return + mentions = extract_mentions(memory.content) statement = select(Agent).where(col(Agent.board_id) == board.id) targets: dict[str, Agent] = {} diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index b0e5f1f..84f3789 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -10,8 +10,12 @@ import { Activity, ArrowUpRight, MessageSquare, + Pause, + Plus, Pencil, + Play, Settings, + ShieldCheck, X, } from "lucide-react"; @@ -342,6 +346,15 @@ export default function BoardDetailPage() { const [chatError, setChatError] = useState(null); const chatMessagesRef = useRef([]); const chatEndRef = useRef(null); + const [isAgentsControlDialogOpen, setIsAgentsControlDialogOpen] = + useState(false); + const [agentsControlAction, setAgentsControlAction] = useState< + "pause" | "resume" + >("pause"); + const [isAgentsControlSending, setIsAgentsControlSending] = useState(false); + const [agentsControlError, setAgentsControlError] = useState( + null, + ); const [isDeletingTask, setIsDeletingTask] = useState(false); const [deleteTaskError, setDeleteTaskError] = useState(null); const [viewMode, setViewMode] = useState<"board" | "list">("board"); @@ -581,6 +594,18 @@ export default function BoardDetailPage() { return new Date(latest).toISOString(); }; + const lastAgentControlCommand = useMemo(() => { + for (let i = chatMessages.length - 1; i >= 0; i -= 1) { + const value = (chatMessages[i]?.content ?? "").trim().toLowerCase(); + if (value === "/pause" || value === "/resume") { + return value; + } + } + return null; + }, [chatMessages]); + + const isAgentsPaused = lastAgentControlCommand === "/pause"; + useEffect(() => { if (!isPageActive) return; if (!isSignedIn || !boardId || !board) return; @@ -1163,13 +1188,16 @@ export default function BoardDetailPage() { } }; - const handleSendChat = useCallback( - async (content: string): Promise => { - if (!isSignedIn || !boardId) return false; + const postBoardChatMessage = useCallback( + async ( + content: string, + ): Promise<{ ok: boolean; error: string | null }> => { + if (!isSignedIn || !boardId) { + return { ok: false, error: "Sign in to send messages." }; + } const trimmed = content.trim(); - if (!trimmed) return false; - setIsChatSending(true); - setChatError(null); + if (!trimmed) return { ok: false, error: null }; + try { const result = await createBoardMemoryApiV1BoardsBoardIdMemoryPost( boardId, @@ -1195,19 +1223,62 @@ export default function BoardDetailPage() { return next; }); } - return true; + return { ok: true, error: null }; } catch (err) { - setChatError( - err instanceof Error ? err.message : "Unable to send message.", - ); - return false; - } finally { - setIsChatSending(false); + const message = + err instanceof Error ? err.message : "Unable to send message."; + return { ok: false, error: message }; } }, [boardId, isSignedIn], ); + const handleSendChat = useCallback( + async (content: string): Promise => { + const trimmed = content.trim(); + if (!trimmed) return false; + setIsChatSending(true); + setChatError(null); + try { + const result = await postBoardChatMessage(trimmed); + if (!result.ok) { + if (result.error) { + setChatError(result.error); + } + return false; + } + return true; + } finally { + setIsChatSending(false); + } + }, + [postBoardChatMessage], + ); + + const openAgentsControlDialog = (action: "pause" | "resume") => { + setAgentsControlAction(action); + setAgentsControlError(null); + setIsAgentsControlDialogOpen(true); + }; + + const handleConfirmAgentsControl = useCallback(async () => { + const command = agentsControlAction === "pause" ? "/pause" : "/resume"; + setIsAgentsControlSending(true); + setAgentsControlError(null); + try { + const result = await postBoardChatMessage(command); + if (!result.ok) { + setAgentsControlError( + result.error ?? `Unable to send ${command} command.`, + ); + return; + } + setIsAgentsControlDialogOpen(false); + } finally { + setIsAgentsControlSending(false); + } + }, [agentsControlAction, postBoardChatMessage]); + const assigneeById = useMemo(() => { const map = new Map(); agents @@ -1869,21 +1940,48 @@ export default function BoardDetailPage() { List - + + + + + + {/* onboarding moved to board settings */} );