From 4c42f53cae22420bcce73fbe20990d1eabad083d Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Fri, 2 May 2025 23:31:50 -0600 Subject: [PATCH] Agent Multi-Role Milestone 2 --- Data/Agent/borealis-agent.py | 113 ++++--- .../WebUI/src/nodes/Agents/Node_Agent.jsx | 92 +++--- .../Agents/Node_Agent_Role_Screenshot.jsx | 309 +++++++++--------- 3 files changed, 276 insertions(+), 238 deletions(-) diff --git a/Data/Agent/borealis-agent.py b/Data/Agent/borealis-agent.py index 4f19ef0..cc8974a 100644 --- a/Data/Agent/borealis-agent.py +++ b/Data/Agent/borealis-agent.py @@ -36,7 +36,6 @@ class ConfigManager: self.load() def load(self): - # load or initialize if not os.path.exists(self.path): self.data = DEFAULT_CONFIG.copy() self._write() @@ -44,12 +43,10 @@ class ConfigManager: try: with open(self.path, 'r') as f: loaded = json.load(f) - # merge defaults self.data = {**DEFAULT_CONFIG, **loaded} except Exception as e: print(f"[WARN] Failed to parse config: {e}") self.data = DEFAULT_CONFIG.copy() - # track mtime try: self._last_mtime = os.path.getmtime(self.path) except Exception: @@ -74,14 +71,12 @@ class ConfigManager: return False CONFIG = ConfigManager(CONFIG_PATH) -# Purge saved regions on startup (fresh run) CONFIG.data['regions'] = {} CONFIG._write() # ////////////////////////////////////////////////////////////////////////// # END CORE SECTION: CONFIG MANAGER # ////////////////////////////////////////////////////////////////////////// -# Assign or persist agent_id host = socket.gethostname().lower() stored_id = CONFIG.data.get('agent_id') if stored_id: @@ -108,7 +103,6 @@ async def connect(): @sio.event async def disconnect(): print("[WebSocket] Disconnected from Borealis server.") - # reset tasks and overlays for task in list(role_tasks.values()): task.cancel() role_tasks.clear() @@ -116,50 +110,50 @@ async def disconnect(): try: widget.close() except: pass overlay_widgets.clear() - # purge regions on intentional disconnect CONFIG.data['regions'].clear() CONFIG._write() - # reload settings CONFIG.load() @sio.on('agent_config') async def on_agent_config(cfg): print(f"[CONNECTED] Received config with {len(cfg.get('roles',[]))} roles.") - # determine removed roles new_ids = {r.get('node_id') for r in cfg.get('roles', []) if r.get('node_id')} old_ids = set(role_tasks.keys()) removed = old_ids - new_ids + + # Cancel removed roles for rid in removed: - # remove region config if rid in CONFIG.data['regions']: CONFIG.data['regions'].pop(rid, None) - # close overlay w = overlay_widgets.pop(rid, None) if w: try: w.close() except: pass + if removed: CONFIG._write() - # cancel existing and start new + + # Cancel all existing to ensure clean state for task in list(role_tasks.values()): task.cancel() role_tasks.clear() + + # Restart everything to ensure roles are re-applied for role_cfg in cfg.get('roles', []): + nid = role_cfg.get('node_id') if role_cfg.get('role') == 'screenshot': - nid = role_cfg.get('node_id') - t = asyncio.create_task(screenshot_task(role_cfg)) - role_tasks[nid] = t + task = asyncio.create_task(screenshot_task(role_cfg)) + role_tasks[nid] = task # ////////////////////////////////////////////////////////////////////////// # END CORE SECTION: WEBSOCKET SETUP & HANDLERS # ////////////////////////////////////////////////////////////////////////// -# ---------------- Overlay Widget ---------------- class ScreenshotRegion(QtWidgets.QWidget): def __init__(self, node_id, x=100, y=100, w=300, h=200): super().__init__() self.node_id = node_id self.setGeometry(x, y, w, h) - self.setWindowFlags(QtCore.Qt.FramelessWindowHint|QtCore.Qt.WindowStaysOnTopHint) + self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.drag_offset = None self.resizing = False @@ -168,7 +162,7 @@ class ScreenshotRegion(QtWidgets.QWidget): self.label = QtWidgets.QLabel(self) self.label.setText(f"{node_id[:8]}") self.label.setStyleSheet("color: lime; background: transparent; font-size: 10px;") - self.label.move(8,4) + self.label.move(8, 4) self.setMouseTracking(True) def paintEvent(self, event): @@ -183,51 +177,72 @@ class ScreenshotRegion(QtWidgets.QWidget): def mousePressEvent(self, e): if e.button()==QtCore.Qt.LeftButton: - x,y = e.pos().x(), e.pos().y() - if x>self.width()-self.resize_handle_size and y>self.height()-self.resize_handle_size: - self.resizing=True + x, y = e.pos().x(), e.pos().y() + if x > self.width() - self.resize_handle_size and y > self.height() - self.resize_handle_size: + self.resizing = True else: self.drag_offset = e.globalPos() - self.frameGeometry().topLeft() def mouseMoveEvent(self, e): if self.resizing: - nw = max(e.pos().x(),100) - nh = max(e.pos().y(),80) - self.resize(nw,nh) - elif e.buttons()&QtCore.Qt.LeftButton and self.drag_offset: - self.move(e.globalPos()-self.drag_offset) + nw = max(e.pos().x(), 100) + nh = max(e.pos().y(), 80) + self.resize(nw, nh) + elif e.buttons() & QtCore.Qt.LeftButton and self.drag_offset: + self.move(e.globalPos() - self.drag_offset) - def mouseReleaseEvent(self,e): - self.resizing=False; self.drag_offset=None + def mouseReleaseEvent(self, e): + self.resizing = False + self.drag_offset = None def get_geometry(self): - g=self.geometry(); return (g.x(),g.y(),g.width(),g.height()) + g = self.geometry() + return (g.x(), g.y(), g.width(), g.height()) # ---------------- Screenshot Task ---------------- async def screenshot_task(cfg): - nid = cfg.get('node_id'); - # initial region from config or payload + nid = cfg.get('node_id') + # If existing region in config, honor that r = CONFIG.data['regions'].get(nid) - region = (r['x'],r['y'],r['w'],r['h']) if r else (cfg.get('x',100),cfg.get('y',100),cfg.get('w',300),cfg.get('h',200)) + if r: + region = (r['x'], r['y'], r['w'], r['h']) + else: + region = (cfg.get('x', 100), cfg.get('y', 100), cfg.get('w', 300), cfg.get('h', 200)) + CONFIG.data['regions'][nid] = { + 'x': region[0], 'y': region[1], 'w': region[2], 'h': region[3] + } + CONFIG._write() + if nid not in overlay_widgets: - widget = ScreenshotRegion(nid,*region) - overlay_widgets[nid] = widget; widget.show() - interval = cfg.get('interval',1000)/1000.0 + widget = ScreenshotRegion(nid, *region) + overlay_widgets[nid] = widget + widget.show() + + interval = cfg.get('interval', 1000) / 1000.0 loop = asyncio.get_event_loop() - executor = concurrent.futures.ThreadPoolExecutor(max_workers=CONFIG.data.get('max_task_workers',DEFAULT_CONFIG['max_task_workers'])) + executor = concurrent.futures.ThreadPoolExecutor( + max_workers=CONFIG.data.get('max_task_workers', DEFAULT_CONFIG['max_task_workers']) + ) + try: while True: - x,y,w,h = overlay_widgets[nid].get_geometry() - # persist if changed + x, y, w, h = overlay_widgets[nid].get_geometry() prev = CONFIG.data['regions'].get(nid) - if prev!={'x':x,'y':y,'w':w,'h':h}: - CONFIG.data['regions'][nid]={'x':x,'y':y,'w':w,'h':h} + new_geom = {'x': x, 'y': y, 'w': w, 'h': h} + if prev != new_geom: + CONFIG.data['regions'][nid] = new_geom CONFIG._write() - grab = partial(ImageGrab.grab,bbox=(x,y,x+w,y+h)) - img = await loop.run_in_executor(executor,grab) - buf = BytesIO(); img.save(buf,format='PNG') + grab = partial(ImageGrab.grab, bbox=(x, y, x+w, y+h)) + img = await loop.run_in_executor(executor, grab) + buf = BytesIO() + img.save(buf, format='PNG') encoded = base64.b64encode(buf.getvalue()).decode('utf-8') - await sio.emit('agent_screenshot_task',{'agent_id':AGENT_ID,'node_id':nid,'image_base64':encoded}) + await sio.emit('agent_screenshot_task', { + 'agent_id': AGENT_ID, + 'node_id': nid, + 'image_base64': encoded, + 'x': x, 'y': y, 'w': w, 'h': h # Bi-directional live-sync + }) await asyncio.sleep(interval) except asyncio.CancelledError: return @@ -238,24 +253,24 @@ async def screenshot_task(cfg): async def config_watcher(): while True: if CONFIG.watch(): pass - await asyncio.sleep(CONFIG.data.get('config_file_watcher_interval',DEFAULT_CONFIG['config_file_watcher_interval'])) + await asyncio.sleep(CONFIG.data.get('config_file_watcher_interval', DEFAULT_CONFIG['config_file_watcher_interval'])) # ////////////////////////////////////////////////////////////////////////// # CORE SECTION: MAIN & EVENT LOOP (do not modify unless you know what you’re doing) # ////////////////////////////////////////////////////////////////////////// async def connect_loop(): - retry=5 + retry = 5 while True: try: - url=CONFIG.data.get('borealis_server_url',DEFAULT_CONFIG['borealis_server_url']) + url = CONFIG.data.get('borealis_server_url', DEFAULT_CONFIG['borealis_server_url']) print(f"[WebSocket] Connecting to {url}...") - await sio.connect(url,transports=['websocket']) + await sio.connect(url, transports=['websocket']) break except: print(f"[WebSocket] Server unavailable, retrying in {retry}s...") await asyncio.sleep(retry) -if __name__=='__main__': +if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) loop = QEventLoop(app) asyncio.set_event_loop(loop) diff --git a/Data/Server/WebUI/src/nodes/Agents/Node_Agent.jsx b/Data/Server/WebUI/src/nodes/Agents/Node_Agent.jsx index dfdc001..f634546 100644 --- a/Data/Server/WebUI/src/nodes/Agents/Node_Agent.jsx +++ b/Data/Server/WebUI/src/nodes/Agents/Node_Agent.jsx @@ -1,6 +1,6 @@ ////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/nodes/Agent/Node_Agent.jsx -import React, { useEffect, useState, useCallback, useMemo } from "react"; +import React, { useEffect, useState, useCallback, useMemo, useRef } from "react"; import { Handle, Position, useReactFlow, useStore } from "reactflow"; const BorealisAgentNode = ({ id, data }) => { @@ -9,18 +9,20 @@ const BorealisAgentNode = ({ id, data }) => { const [agents, setAgents] = useState({}); const [selectedAgent, setSelectedAgent] = useState(data.agent_id || ""); const [isConnected, setIsConnected] = useState(false); + const prevRolesRef = useRef([]); - // Build a normalized list [{id, status}, ...] const agentList = useMemo(() => { - if (Array.isArray(agents)) { - return agents.map((a) => ({ id: a.id, status: a.status })); - } else if (agents && typeof agents === "object") { - return Object.entries(agents).map(([aid, info]) => ({ id: aid, status: info.status })); - } - return []; + if (!agents || typeof agents !== "object") return []; + return Object.entries(agents) + .map(([aid, info]) => ({ + id: aid, + status: info?.status || "offline", + last_seen: info?.last_seen || 0 + })) + .filter(({ status }) => status !== "offline") + .sort((a, b) => b.last_seen - a.last_seen); }, [agents]); - // Fetch agents from backend useEffect(() => { const fetchAgents = () => { fetch("/api/agents") @@ -29,11 +31,10 @@ const BorealisAgentNode = ({ id, data }) => { .catch(() => {}); }; fetchAgents(); - const interval = setInterval(fetchAgents, 5000); + const interval = setInterval(fetchAgents, 4000); return () => clearInterval(interval); }, []); - // Persist selectedAgent and reset connection on change useEffect(() => { setNodes((nds) => nds.map((n) => @@ -43,7 +44,6 @@ const BorealisAgentNode = ({ id, data }) => { setIsConnected(false); }, [selectedAgent]); - // Compute attached role node IDs for this agent node const attachedRoleIds = useMemo( () => edges @@ -52,7 +52,6 @@ const BorealisAgentNode = ({ id, data }) => { [edges, id] ); - // Build role payloads using the instruction registry const getAttachedRoles = useCallback(() => { const allNodes = getNodes(); return attachedRoleIds @@ -63,42 +62,54 @@ const BorealisAgentNode = ({ id, data }) => { .filter((r) => r); }, [attachedRoleIds, getNodes]); - // Connect: send roles to server - const handleConnect = useCallback(() => { - if (!selectedAgent) return; - const roles = getAttachedRoles(); + const provisionRoles = useCallback((roles) => { + if (!selectedAgent || roles.length === 0) return; fetch("/api/agent/provision", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ agent_id: selectedAgent, roles }), + body: JSON.stringify({ agent_id: selectedAgent, roles }) }) - .then(() => setIsConnected(true)) + .then(() => { + setIsConnected(true); + prevRolesRef.current = roles; + }) .catch(() => {}); - }, [selectedAgent, getAttachedRoles]); + }, [selectedAgent]); + + const handleConnect = useCallback(() => { + const roles = getAttachedRoles(); + provisionRoles(roles); + }, [getAttachedRoles, provisionRoles]); - // Disconnect: clear roles on server const handleDisconnect = useCallback(() => { if (!selectedAgent) return; fetch("/api/agent/provision", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ agent_id: selectedAgent, roles: [] }), + body: JSON.stringify({ agent_id: selectedAgent, roles: [] }) }) - .then(() => setIsConnected(false)) + .then(() => { + setIsConnected(false); + prevRolesRef.current = []; + }) .catch(() => {}); }, [selectedAgent]); - // Hot-update roles when attachedRoleIds change useEffect(() => { - if (isConnected) { - const roles = getAttachedRoles(); - fetch("/api/agent/provision", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ agent_id: selectedAgent, roles }), - }).catch(() => {}); + const newRoles = getAttachedRoles(); + const prevSerialized = JSON.stringify(prevRolesRef.current || []); + const newSerialized = JSON.stringify(newRoles); + if (isConnected && newSerialized !== prevSerialized) { + provisionRoles(newRoles); } - }, [attachedRoleIds]); + }, [attachedRoleIds, isConnected]); + + const selectedAgentStatus = useMemo(() => { + if (!selectedAgent) return "Unassigned"; + const agent = agents[selectedAgent]; + if (!agent) return "Reconnecting..."; + return agent.status === "provisioned" ? "Connected" : "Available"; + }, [agents, selectedAgent]); return (
@@ -119,14 +130,11 @@ const BorealisAgentNode = ({ id, data }) => { style={{ width: "100%", marginBottom: "6px", fontSize: "9px" }} > - {agentList.map(({ id: aid, status }) => { - const labelText = status === "provisioned" ? "(Connected)" : "(Ready to Connect)"; - return ( - - ); - })} + {agentList.map(({ id: aid, status }) => ( + + ))} {isConnected ? ( @@ -149,7 +157,9 @@ const BorealisAgentNode = ({ id, data }) => {
- Attach Agent Role Nodes to define roles for this agent. Roles will be provisioned automatically. + Status: {selectedAgentStatus} +
+ Attach Agent Role Nodes to define live behavior.
diff --git a/Data/Server/WebUI/src/nodes/Agents/Node_Agent_Role_Screenshot.jsx b/Data/Server/WebUI/src/nodes/Agents/Node_Agent_Role_Screenshot.jsx index 6e86279..bb09880 100644 --- a/Data/Server/WebUI/src/nodes/Agents/Node_Agent_Role_Screenshot.jsx +++ b/Data/Server/WebUI/src/nodes/Agents/Node_Agent_Role_Screenshot.jsx @@ -6,181 +6,194 @@ import ShareIcon from "@mui/icons-material/Share"; import IconButton from "@mui/material/IconButton"; if (!window.BorealisValueBus) { - window.BorealisValueBus = {}; + window.BorealisValueBus = {}; } if (!window.BorealisUpdateRate) { - window.BorealisUpdateRate = 100; + window.BorealisUpdateRate = 100; } const ScreenshotInstructionNode = ({ id, data }) => { - const { setNodes, getNodes } = useReactFlow(); - const edges = useStore(state => state.edges); + const { setNodes, getNodes } = useReactFlow(); + const edges = useStore(state => state.edges); - const [interval, setInterval] = useState(data?.interval || 1000); - const [region, setRegion] = useState({ - x: data?.x ?? 250, - y: data?.y ?? 100, - w: data?.w ?? 300, - h: data?.h ?? 200, - }); - const [visible, setVisible] = useState(data?.visible ?? true); - const [alias, setAlias] = useState(data?.alias || ""); - const [imageBase64, setImageBase64] = useState(""); + const [interval, setInterval] = useState(data?.interval || 1000); + const [region, setRegion] = useState({ + x: data?.x ?? 250, + y: data?.y ?? 100, + w: data?.w ?? 300, + h: data?.h ?? 200, + }); + const [visible, setVisible] = useState(data?.visible ?? true); + const [alias, setAlias] = useState(data?.alias || ""); + const [imageBase64, setImageBase64] = useState(""); - const base64Ref = useRef(""); + const base64Ref = useRef(""); + const regionRef = useRef(region); - const handleCopyLiveViewLink = () => { - const agentEdge = edges.find(e => e.target === id && e.sourceHandle === "provisioner"); - if (!agentEdge) { - alert("No upstream agent connection found."); - return; + // Push current state into BorealisValueBus at intervals + useEffect(() => { + const intervalId = setInterval(() => { + const val = base64Ref.current; + if (val) { + window.BorealisValueBus[id] = val; + setNodes(nds => + nds.map(n => + n.id === id ? { ...n, data: { ...n.data, value: val } } : n + ) + ); + } + }, window.BorealisUpdateRate || 100); + return () => clearInterval(intervalId); + }, [id, setNodes]); + + // Listen for agent screenshot + overlay updates + useEffect(() => { + const socket = window.BorealisSocket; + if (!socket) return; + + const handleScreenshot = (payload) => { + if (payload?.node_id !== id || !payload.image_base64) return; + + base64Ref.current = payload.image_base64; + setImageBase64(payload.image_base64); + window.BorealisValueBus[id] = payload.image_base64; + + // If geometry changed from agent side, sync into UI + const { x, y, w, h } = payload; + if (x !== undefined && y !== undefined && w !== undefined && h !== undefined) { + const newRegion = { x, y, w, h }; + const prev = regionRef.current; + const changed = Object.entries(newRegion).some(([k, v]) => prev[k] !== v); + + if (changed) { + regionRef.current = newRegion; + setRegion(newRegion); + setNodes(nds => + nds.map(n => + n.id === id ? { ...n, data: { ...n.data, ...newRegion } } : n + ) + ); } - - const agentNode = getNodes().find(n => n.id === agentEdge.source); - const selectedAgentId = agentNode?.data?.agent_id; - - if (!selectedAgentId) { - alert("Upstream agent node does not have a selected agent."); - return; - } - - const liveUrl = `${window.location.origin}/api/agent/${selectedAgentId}/node/${id}/screenshot/live`; - navigator.clipboard.writeText(liveUrl) - .then(() => console.log(`[Clipboard] Copied Live View URL: ${liveUrl}`)) - .catch(err => console.error("Clipboard copy failed:", err)); + } }; - useEffect(() => { - const intervalId = setInterval(() => { - const val = base64Ref.current; + socket.on("agent_screenshot_task", handleScreenshot); + return () => socket.off("agent_screenshot_task", handleScreenshot); + }, [id, setNodes]); - console.log(`[Screenshot Node] setInterval update. Current base64 length: ${val?.length || 0}`); + // Bi-directional instruction export + window.__BorealisInstructionNodes = window.__BorealisInstructionNodes || {}; + window.__BorealisInstructionNodes[id] = () => ({ + node_id: id, + role: "screenshot", + interval, + visible, + alias, + ...regionRef.current + }); - if (!val) return; + // Manual live view copy + const handleCopyLiveViewLink = () => { + const agentEdge = edges.find(e => e.target === id && e.sourceHandle === "provisioner"); + const agentNode = getNodes().find(n => n.id === agentEdge?.source); + const selectedAgentId = agentNode?.data?.agent_id; - window.BorealisValueBus[id] = val; - setNodes(nds => - nds.map(n => - n.id === id - ? { ...n, data: { ...n.data, value: val } } - : n - ) - ); - }, window.BorealisUpdateRate || 100); + if (!selectedAgentId) { + alert("No valid agent connection found."); + return; + } - return () => clearInterval(intervalId); - }, [id, setNodes]); + const liveUrl = `${window.location.origin}/api/agent/${selectedAgentId}/node/${id}/screenshot/live`; + navigator.clipboard.writeText(liveUrl) + .then(() => console.log(`[Clipboard] Live View URL copied: ${liveUrl}`)) + .catch(err => console.error("Clipboard copy failed:", err)); + }; - useEffect(() => { - const socket = window.BorealisSocket || null; - if (!socket) { - console.warn("[Screenshot Node] BorealisSocket not available"); - return; - } + return ( +
+ + - console.log(`[Screenshot Node] Listening for agent_screenshot_task with node_id: ${id}`); +
Agent Role: Screenshot
+
+ + setInterval(Number(e.target.value))} + style={{ width: "100%", marginBottom: "4px" }} + /> - const handleScreenshot = (payload) => { - console.log("[Screenshot Node] Received payload:", payload); - - if (payload?.node_id === id && payload?.image_base64) { - base64Ref.current = payload.image_base64; - setImageBase64(payload.image_base64); - window.BorealisValueBus[id] = payload.image_base64; - - console.log(`[Screenshot Node] Updated base64Ref and ValueBus for ${id}, length: ${payload.image_base64.length}`); - } else { - console.log(`[Screenshot Node] Ignored payload for mismatched node_id (${payload?.node_id})`); - } - }; - - socket.on("agent_screenshot_task", handleScreenshot); - return () => socket.off("agent_screenshot_task", handleScreenshot); - }, [id]); - - window.__BorealisInstructionNodes = window.__BorealisInstructionNodes || {}; - window.__BorealisInstructionNodes[id] = () => ({ - node_id: id, - role: "screenshot", - interval, - visible, - alias, - ...region - }); - - return ( -
- - - -
Agent Role: Screenshot
-
- - setInterval(Number(e.target.value))} - style={{ width: "100%", marginBottom: "4px" }} - /> - - -
- setRegion({ ...region, x: Number(e.target.value) })} style={{ width: "25%" }} /> - setRegion({ ...region, y: Number(e.target.value) })} style={{ width: "25%" }} /> - setRegion({ ...region, w: Number(e.target.value) })} style={{ width: "25%" }} /> - setRegion({ ...region, h: Number(e.target.value) })} style={{ width: "25%" }} /> -
- -
- -
- - - setAlias(e.target.value)} - placeholder="Label (optional)" - style={{ width: "100%", fontSize: "9px", marginBottom: "6px" }} - /> - -
- {imageBase64 - ? `Last image: ${Math.round(imageBase64.length / 1024)} KB` - : "Awaiting Screenshot Data..."} -
-
- -
- - - -
+ +
+ { + const x = Number(e.target.value); + const updated = { ...region, x }; setRegion(updated); regionRef.current = updated; + }} style={{ width: "25%" }} /> + { + const y = Number(e.target.value); + const updated = { ...region, y }; setRegion(updated); regionRef.current = updated; + }} style={{ width: "25%" }} /> + { + const w = Number(e.target.value); + const updated = { ...region, w }; setRegion(updated); regionRef.current = updated; + }} style={{ width: "25%" }} /> + { + const h = Number(e.target.value); + const updated = { ...region, h }; setRegion(updated); regionRef.current = updated; + }} style={{ width: "25%" }} />
- ); + +
+ +
+ + + setAlias(e.target.value)} + placeholder="Label (optional)" + style={{ width: "100%", fontSize: "9px", marginBottom: "6px" }} + /> + +
+ {imageBase64 + ? `Last image: ${Math.round(imageBase64.length / 1024)} KB` + : "Awaiting Screenshot Data..."} +
+
+ +
+ + + +
+
+ ); }; export default { - type: "Agent_Role_Screenshot", - label: "Agent Role: Screenshot", - description: ` + type: "Agent_Role_Screenshot", + label: "Agent Role: Screenshot", + description: ` Agent Role Node: Screenshot Region - Defines a single region capture role - Allows custom update interval and overlay - Emits captured base64 PNG data from agent `.trim(), - content: "Capture screenshot region via agent", - component: ScreenshotInstructionNode + content: "Capture screenshot region via agent", + component: ScreenshotInstructionNode };