Implemented Live Macro Window Detection

This commit is contained in:
2025-06-07 00:05:21 -06:00
parent 5927403f12
commit c9e3e67f20
4 changed files with 427 additions and 86 deletions

View File

@ -43,13 +43,27 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti
return config.map((field, index) => {
const value = nodeData?.[field.key] || "";
return (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: "#ccc", mb: 0.5 }}>
{field.label || field.key}
</Typography>
// ---- DYNAMIC DROPDOWN SUPPORT ----
if (field.type === "select") {
let options = field.options || [];
{field.type === "select" ? (
// Handle dynamic options for things like Target Window
if (field.dynamicOptions && nodeData?.windowList && Array.isArray(nodeData.windowList)) {
options = nodeData.windowList
.map(win => ({
value: String(win.handle),
label: `${win.title} (${win.handle})`
}))
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
} else {
options = options.map(opt => ({ value: opt, label: opt }));
}
return (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: "#ccc", mb: 0.5 }}>
{field.label || field.key}
</Typography>
<TextField
select
fullWidth
@ -112,42 +126,57 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti
}
}}
>
{(field.options || []).map((opt, idx) => (
<MenuItem key={idx} value={opt}>
{opt}
{options.length === 0 ? (
<MenuItem disabled value="">
{field.label === "Target Window"
? "No windows detected"
: "No options"}
</MenuItem>
))}
) : (
options.map((opt, idx) => (
<MenuItem key={idx} value={opt.value}>
{opt.label}
</MenuItem>
))
)}
</TextField>
</Box>
);
}
// ---- END DYNAMIC DROPDOWN SUPPORT ----
) : (
<TextField
variant="outlined"
size="small"
fullWidth
value={value}
onChange={(e) => {
const newValue = e.target.value;
if (!nodeId) return;
effectiveSetNodes((nds) =>
nds.map((n) =>
n.id === nodeId
? { ...n, data: { ...n.data, [field.key]: newValue } }
: n
)
);
window.BorealisValueBus[nodeId] = newValue;
}}
InputProps={{
sx: {
backgroundColor: "#1e1e1e",
color: "#ccc",
"& fieldset": { borderColor: "#444" },
"&:hover fieldset": { borderColor: "#666" },
"&.Mui-focused fieldset": { borderColor: "#58a6ff" }
}
}}
/>
)}
return (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: "#ccc", mb: 0.5 }}>
{field.label || field.key}
</Typography>
<TextField
variant="outlined"
size="small"
fullWidth
value={value}
onChange={(e) => {
const newValue = e.target.value;
if (!nodeId) return;
effectiveSetNodes((nds) =>
nds.map((n) =>
n.id === nodeId
? { ...n, data: { ...n.data, [field.key]: newValue } }
: n
)
);
window.BorealisValueBus[nodeId] = newValue;
}}
InputProps={{
sx: {
backgroundColor: "#1e1e1e",
color: "#ccc",
"& fieldset": { borderColor: "#444" },
"&:hover fieldset": { borderColor: "#666" },
"&.Mui-focused fieldset": { borderColor: "#58a6ff" }
}
}}
/>
</Box>
);
});

View File

@ -17,21 +17,130 @@ const OPERATION_MODES = [
"Trigger-Continuous"
];
const MACRO_TYPES = [
"keypress",
"typed_text"
];
const statusColors = {
idle: "#333",
running: "#00d18c",
error: "#ff4f4f",
success: "#00d18c"
};
const MacroKeyPressNode = ({ id, data }) => {
const { setNodes, getNodes } = useReactFlow();
const edges = useStore((state) => state.edges);
const [windowList, setWindowList] = useState([]);
const [status, setStatus] = useState({ state: "idle", message: "" });
const socketRef = useRef(null);
// Determine if agent is connected
const agentEdge = edges.find((e) => e.target === id && e.targetHandle === "agent");
const agentNode = agentEdge && getNodes().find((n) => n.id === agentEdge.source);
const agentConnection = !!(agentNode && agentNode.data && agentNode.data.agent_id);
const agent_id = agentNode && agentNode.data && agentNode.data.agent_id;
// Macro run/trigger state (sidebar sets this via config, but node UI just shows status)
const running = data?.active === true || data?.active === "true";
// Node UI (no config fields, only status)
// Store for last macro error/status
const [lastMacroStatus, setLastMacroStatus] = useState({ success: true, message: "", timestamp: null });
// Setup WebSocket for agent macro status updates
useEffect(() => {
if (!window.BorealisSocket) return;
const socket = window.BorealisSocket;
socketRef.current = socket;
function handleMacroStatus(payload) {
if (
payload &&
payload.agent_id === agent_id &&
payload.node_id === id
) {
setLastMacroStatus({
success: !!payload.success,
message: payload.message || "",
timestamp: payload.timestamp || Date.now()
});
setStatus({
state: payload.success ? "success" : "error",
message: payload.message || (payload.success ? "Success" : "Error")
});
}
}
socket.on("macro_status", handleMacroStatus);
return () => {
socket.off("macro_status", handleMacroStatus);
};
}, [agent_id, id]);
// Auto-refresh window list from agent
useEffect(() => {
let intervalId = null;
async function fetchWindows() {
if (window.BorealisSocket && agentConnection) {
window.BorealisSocket.emit("list_agent_windows", {
agent_id
});
}
}
fetchWindows();
intervalId = setInterval(fetchWindows, WINDOW_LIST_REFRESH_MS);
// Listen for agent_window_list updates
function handleAgentWindowList(payload) {
if (payload?.agent_id === agent_id && Array.isArray(payload.windows)) {
setWindowList(payload.windows);
// Store windowList in node data for sidebar dynamic dropdowns
setNodes(nds =>
nds.map(n =>
n.id === id
? { ...n, data: { ...n.data, windowList: payload.windows } }
: n
)
);
}
}
if (window.BorealisSocket) {
window.BorealisSocket.on("agent_window_list", handleAgentWindowList);
}
return () => {
clearInterval(intervalId);
if (window.BorealisSocket) {
window.BorealisSocket.off("agent_window_list", handleAgentWindowList);
}
};
}, [agent_id, agentConnection, setNodes, id]);
// UI: Start/Pause Button
const handleToggleMacro = () => {
setNodes(nds =>
nds.map(n =>
n.id === id
? {
...n,
data: {
...n.data,
active: n.data?.active === true || n.data?.active === "true" ? "false" : "true"
}
}
: n
)
);
};
// Optional: Show which window is targeted by name
const selectedWindow = (windowList || []).find(w => String(w.handle) === String(data?.window_handle));
// Node UI (no config fields, only status + window list)
return (
<div className="borealis-node" style={{ minWidth: 240, position: "relative" }}>
<div className="borealis-node" style={{ minWidth: 280, position: "relative" }}>
{/* --- INPUT LABELS & HANDLES --- */}
<div style={{
position: "absolute",
@ -85,16 +194,105 @@ const MacroKeyPressNode = ({ id, data }) => {
transform: "translateY(-50%)",
height: "10px",
borderRadius: "50%",
backgroundColor: running ? "#00d18c" : "#333",
backgroundColor:
status.state === "error"
? statusColors.error
: running
? statusColors.running
: statusColors.idle,
border: "1px solid #222"
}}
/>
</div>
<div className="borealis-node-content">
<strong>Status</strong>: {running ? "Running" : "Idle"}
<strong>Status</strong>:{" "}
{status.state === "error"
? (
<span style={{ color: "#ff4f4f" }}>
Error{lastMacroStatus.message ? `: ${lastMacroStatus.message}` : ""}
</span>
)
: running
? (
<span style={{ color: "#00d18c" }}>
Running{lastMacroStatus.message ? ` (${lastMacroStatus.message})` : ""}
</span>
)
: "Idle"}
<br />
<strong>Agent Connection</strong>: {agentConnection ? "Connected" : "Not Connected"}
<br />
<strong>Target Window</strong>:{" "}
{selectedWindow
? `${selectedWindow.title} (${selectedWindow.handle})`
: data?.window_handle
? `Handle: ${data.window_handle}`
: <span style={{ color: "#888" }}>Not set</span>}
<br />
<strong>Mode</strong>: {data?.operation_mode || DEFAULT_OPERATION_MODE}
<br />
<strong>Macro Type</strong>: {data?.macro_type || "keypress"}
<br />
<button
onClick={handleToggleMacro}
style={{
marginTop: 8,
padding: "4px 10px",
background: running ? "#3a3a3a" : "#0475c2",
color: running ? "#fff" : "#fff",
border: "1px solid #0475c2",
borderRadius: 3,
fontSize: "11px",
cursor: "pointer"
}}
>
{running ? "Pause Macro" : "Start Macro"}
</button>
<br />
<span style={{ fontSize: "9px", color: "#aaa" }}>
{lastMacroStatus.timestamp
? `Last event: ${new Date(lastMacroStatus.timestamp).toLocaleTimeString()}`
: ""}
</span>
</div>
{/* Show available windows for debug (hidden from sidebar, but helpful for quick dropdown) */}
<div style={{ marginTop: 8, fontSize: "9px", color: "#58a6ff" }}>
<b>Windows:</b>{" "}
{windowList.length === 0
? <span style={{ color: "#999" }}>No windows detected.</span>
: windowList.map((w) =>
<span
key={w.handle}
style={{
background:
String(w.handle) === String(data?.window_handle)
? "#333"
: "transparent",
borderRadius: 3,
padding: "1px 4px",
marginRight: 4,
border:
String(w.handle) === String(data?.window_handle)
? "1px solid #58a6ff"
: "1px solid #222",
cursor: "pointer"
}}
onClick={() =>
setNodes(nds =>
nds.map(n =>
n.id === id
? { ...n, data: { ...n.data, window_handle: w.handle } }
: n
)
)
}
title={w.title}
>
{w.title}
</span>
)}
</div>
</div>
);
@ -111,17 +309,18 @@ Supports manual, continuous, trigger, and one-shot modes for automation and even
content: "Send Key Press or Typed Text to Window via Agent",
component: MacroKeyPressNode,
config: [
{ key: "window_handle", label: "Target Window", type: "text", defaultValue: "" },
{ key: "macro_type", label: "Macro Type", type: "select", options: ["keypress", "typed_text"], defaultValue: "keypress" },
{ key: "key", label: "Key", type: "text", defaultValue: "" },
{ key: "text", label: "Typed Text", type: "text", defaultValue: "" },
{ key: "interval_ms", label: "Interval (ms)", type: "text", defaultValue: "1000" },
{ key: "randomize_interval", label: "Randomize Interval", type: "select", options: ["true", "false"], defaultValue: "false" },
{ key: "random_min", label: "Random Min (ms)", type: "text", defaultValue: "750" },
{ key: "random_max", label: "Random Max (ms)", type: "text", defaultValue: "950" },
{ key: "operation_mode", label: "Operation Mode", type: "select", options: OPERATION_MODES, defaultValue: "Continuous" },
{ key: "active", label: "Macro Enabled", type: "select", options: ["true", "false"], defaultValue: "false" }
],
{ key: "window_handle", label: "Target Window", type: "select", dynamicOptions: true, defaultValue: "" },
{ key: "macro_type", label: "Macro Type", type: "select", options: ["keypress", "typed_text"], defaultValue: "keypress" },
{ key: "key", label: "Key", type: "text", defaultValue: "" },
{ key: "text", label: "Typed Text", type: "text", defaultValue: "" },
{ key: "interval_ms", label: "Interval (ms)", type: "text", defaultValue: "1000" },
{ key: "randomize_interval", label: "Randomize Interval", type: "select", options: ["true", "false"], defaultValue: "false" },
{ key: "random_min", label: "Random Min (ms)", type: "text", defaultValue: "750" },
{ key: "random_max", label: "Random Max (ms)", type: "text", defaultValue: "950" },
{ key: "operation_mode", label: "Operation Mode", type: "select", options: OPERATION_MODES, defaultValue: "Continuous" },
{ key: "active", label: "Macro Enabled", type: "select", options: ["true", "false"], defaultValue: "false" },
{ key: "trigger", label: "Trigger Value", type: "text", defaultValue: "0" }
],
usage_documentation: `
### Agent Role: Macro
@ -141,6 +340,9 @@ Supports manual, continuous, trigger, and one-shot modes for automation and even
**Event-Driven Support:**
- Chain with other Borealis nodes (text recognition, event triggers, etc).
**Live Status:**
- Displays last agent macro event and error feedback in node.
---
`.trim()
};

View File

@ -256,6 +256,40 @@ def receive_screenshot(data):
def on_disconnect():
print("[WebSocket] Connection Disconnected")
# Macro Websocket Handlers
@socketio.on("macro_status")
def receive_macro_status(data):
"""
Receives macro status/errors from agent and relays to all clients
Expected payload: {
"agent_id": ...,
"node_id": ...,
"success": True/False,
"message": "...",
"timestamp": ...
}
"""
print(f"[Macro Status] Agent {data.get('agent_id')} Node {data.get('node_id')} Success: {data.get('success')} Msg: {data.get('message')}")
emit("macro_status", data, broadcast=True)
@socketio.on("list_agent_windows")
def handle_list_agent_windows(data):
"""
Forwards list_agent_windows event to all agents (or filter for a specific agent_id).
"""
agent_id = data.get("agent_id")
# You can target a specific agent if you track rooms/sessions.
# For now, broadcast to all agents so the correct one can reply.
emit("list_agent_windows", data, broadcast=True)
@socketio.on("agent_window_list")
def handle_agent_window_list(data):
"""
Relay the list of windows from the agent back to all connected clients.
"""
emit("agent_window_list", data, broadcast=True)
# ---------------------------------------------
# Server Launch
# ---------------------------------------------