Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-dev"
version = "0.0.48"
version = "0.0.49"
description = "UiPath Developer Console"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
143 changes: 86 additions & 57 deletions src/uipath/dev/server/frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useEffect } from "react";
import { useCallback, useEffect, useRef } from "react";
import { useRunStore } from "./store/useRunStore";
import { useWebSocket } from "./store/useWebSocket";
import { listRuns, listEntrypoints, getRun } from "./api/client";
import type { RunDetail } from "./types/run";
import { useHashRoute } from "./hooks/useHashRoute";
import Sidebar from "./components/layout/Sidebar";
import NewRunPanel from "./components/runs/NewRunPanel";
Expand Down Expand Up @@ -41,79 +42,109 @@ export default function App() {
.catch(console.error);
}, [setRuns, setEntrypoints]);

const selectedRun = selectedRunId ? runs[selectedRunId] : null;

// Shared helper: apply a full run detail response to the store
const applyRunDetail = useCallback((runId: string, detail: RunDetail) => {
upsertRun(detail);
setTraces(runId, detail.traces);
setLogs(runId, detail.logs);
// Convert messages to chat format (server uses camelCase aliases)
const chatMsgs = (detail.messages as unknown as Record<string, unknown>[]).map((m: Record<string, unknown>) => {
const parts = ((m.contentParts ?? m.content_parts) as Array<Record<string, unknown>>) ?? [];
const toolCalls = ((m.toolCalls ?? m.tool_calls) as Array<Record<string, unknown>>) ?? [];
return {
message_id: ((m.messageId ?? m.message_id) as string),
role: (m.role as string) ?? "assistant",
content:
parts
.filter((p) => {
const mime = ((p.mimeType ?? p.mime_type) as string) ?? "";
return mime.startsWith("text/") || mime === "application/json";
})
.map((p) => {
const data = p.data as Record<string, unknown>;
return (data?.inline as string) ?? "";
})
.join("\n")
.trim() ?? "",
tool_calls: toolCalls.length > 0
? toolCalls.map((tc) => ({
name: (tc.name as string) ?? "",
has_result: !!tc.result,
}))
: undefined,
};
});
setChatMessages(runId, chatMsgs);
// Cache graph data per run (persists across reloads)
if (detail.graph && detail.graph.nodes.length > 0) {
setGraphCache(runId, detail.graph);
}
// Load persisted state events
if (detail.states && detail.states.length > 0) {
setStateEvents(
runId,
detail.states.map((s) => ({
node_name: s.node_name,
qualified_node_name: s.qualified_node_name,
phase: s.phase,
timestamp: new Date(s.timestamp).getTime(),
payload: s.payload,
})),
);
}
}, [upsertRun, setTraces, setLogs, setChatMessages, setStateEvents, setGraphCache]);

// Subscribe to selected run
useEffect(() => {
if (!selectedRunId) return;
ws.subscribe(selectedRunId);

const applyRunDetail = (detail: Awaited<ReturnType<typeof getRun>>) => {
upsertRun(detail);
setTraces(selectedRunId, detail.traces);
setLogs(selectedRunId, detail.logs);
// Convert messages to chat format (server uses camelCase aliases)
const chatMsgs = (detail.messages as unknown as Record<string, unknown>[]).map((m: Record<string, unknown>) => {
const parts = ((m.contentParts ?? m.content_parts) as Array<Record<string, unknown>>) ?? [];
const toolCalls = ((m.toolCalls ?? m.tool_calls) as Array<Record<string, unknown>>) ?? [];
return {
message_id: ((m.messageId ?? m.message_id) as string),
role: (m.role as string) ?? "assistant",
content:
parts
.filter((p) => {
const mime = ((p.mimeType ?? p.mime_type) as string) ?? "";
return mime.startsWith("text/") || mime === "application/json";
})
.map((p) => {
const data = p.data as Record<string, unknown>;
return (data?.inline as string) ?? "";
})
.join("\n")
.trim() ?? "",
tool_calls: toolCalls.length > 0
? toolCalls.map((tc) => ({
name: (tc.name as string) ?? "",
has_result: !!tc.result,
}))
: undefined,
};
});
setChatMessages(selectedRunId, chatMsgs);
// Cache graph data per run (persists across reloads)
if (detail.graph && detail.graph.nodes.length > 0) {
setGraphCache(selectedRunId, detail.graph);
}
// Load persisted state events
if (detail.states && detail.states.length > 0) {
setStateEvents(
selectedRunId,
detail.states.map((s) => ({
node_name: s.node_name,
qualified_node_name: s.qualified_node_name,
phase: s.phase,
timestamp: new Date(s.timestamp).getTime(),
payload: s.payload,
})),
);
}
};

// Fetch full run details (includes fresh status in case we missed run.updated events)
getRun(selectedRunId).then(applyRunDetail).catch(console.error);
getRun(selectedRunId).then((d) => applyRunDetail(selectedRunId, d)).catch(console.error);

// Safety net: re-fetch if run is still in progress after WS subscribe + initial fetch.
// Covers the race where the run completes before WS subscription is processed.
const retryTimer = setTimeout(() => {
const run = useRunStore.getState().runs[selectedRunId];
if (run && (run.status === "pending" || run.status === "running")) {
getRun(selectedRunId).then(applyRunDetail).catch(console.error);
getRun(selectedRunId).then((d) => applyRunDetail(selectedRunId, d)).catch(console.error);
}
}, 2000);

return () => {
clearTimeout(retryTimer);
ws.unsubscribe(selectedRunId);
};
}, [selectedRunId, ws, upsertRun, setTraces, setLogs, setChatMessages, setStateEvents, setGraphCache]);
}, [selectedRunId, ws, applyRunDetail]);

// Refetch full details when run reaches terminal status, but only if WS events were missed
const prevStatusRef = useRef<string | null>(null);
useEffect(() => {
if (!selectedRunId) return;
const status = selectedRun?.status;
const prev = prevStatusRef.current;
prevStatusRef.current = status ?? null;

if (
status &&
(status === "completed" || status === "failed") &&
prev !== status
) {
// Compare what we received via WS against the counts in the run summary.
// Only refetch if something was missed — avoids unnecessary re-renders / flicker.
const state = useRunStore.getState();
const haveTraces = state.traces[selectedRunId]?.length ?? 0;
const haveLogs = state.logs[selectedRunId]?.length ?? 0;
const expectedTraces = selectedRun?.trace_count ?? 0;
const expectedLogs = selectedRun?.log_count ?? 0;

if (haveTraces < expectedTraces || haveLogs < expectedLogs) {
getRun(selectedRunId).then((d) => applyRunDetail(selectedRunId, d)).catch(console.error);
}
}
}, [selectedRunId, selectedRun?.status, applyRunDetail]);

const handleRunCreated = (runId: string) => {
navigate(`#/runs/${runId}/traces`);
Expand All @@ -129,8 +160,6 @@ export default function App() {
navigate("#/new");
};

const selectedRun = selectedRunId ? runs[selectedRunId] : null;

return (
<div className="flex h-screen w-screen">
<Sidebar
Expand Down
12 changes: 10 additions & 2 deletions src/uipath/dev/server/frontend/src/api/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,20 @@ export class WsClient {
};

this.ws.onmessage = (event) => {
let msg: ServerMessage;
try {
const msg: ServerMessage = JSON.parse(event.data);
this.handlers.forEach((h) => h(msg));
msg = JSON.parse(event.data);
} catch {
console.warn("[ws] failed to parse message", event.data);
return;
}
this.handlers.forEach((h) => {
try {
h(msg);
} catch (e) {
console.error("[ws] handler error", e);
}
});
};

this.ws.onclose = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,10 @@ export default function GraphPanel({ entrypoint, runId, breakpointNode, breakpoi
0%, 100% { box-shadow: 0 0 4px var(--success); }
50% { box-shadow: 0 0 10px var(--success); }
}
@keyframes node-pulse-red {
0%, 100% { box-shadow: 0 0 4px var(--error); }
50% { box-shadow: 0 0 10px var(--error); }
}
`}</style>
<ReactFlow
nodes={nodes}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function DefaultNode({ data }: NodeProps) {
const isExecutingNode = data.isExecutingNode as boolean | undefined;

const borderColor = isPausedHere
? "var(--accent)"
? "var(--error)"
: isExecutingNode
? "var(--success)"
: isActiveNode
Expand All @@ -23,7 +23,7 @@ export default function DefaultNode({ data }: NodeProps) {
? "var(--error)"
: "var(--node-border)";

const glowColor = isExecutingNode ? "var(--success)" : "var(--accent)";
const glowColor = isPausedHere ? "var(--error)" : isExecutingNode ? "var(--success)" : "var(--accent)";

return (
<div
Expand All @@ -34,7 +34,7 @@ export default function DefaultNode({ data }: NodeProps) {
color: "var(--text-primary)",
border: `2px solid ${borderColor}`,
boxShadow: isPausedHere || isActiveNode || isExecutingNode ? `0 0 4px ${glowColor}` : undefined,
animation: (isActiveNode || isExecutingNode) && !isPausedHere ? `node-pulse-${isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
animation: isPausedHere || isActiveNode || isExecutingNode ? `node-pulse-${isPausedHere ? "red" : isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
}}
title={label}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function EndNode({ data }: NodeProps) {
const isExecutingNode = data.isExecutingNode as boolean | undefined;

const borderColor = isPausedHere
? "var(--accent)"
? "var(--error)"
: isExecutingNode
? "var(--success)"
: isActiveNode
Expand All @@ -31,7 +31,7 @@ export default function EndNode({ data }: NodeProps) {
? "var(--error)"
: "var(--node-border)";

const glowColor = isExecutingNode ? "var(--success)" : "var(--accent)";
const glowColor = isPausedHere ? "var(--error)" : isExecutingNode ? "var(--success)" : "var(--accent)";

return (
<div
Expand All @@ -42,7 +42,7 @@ export default function EndNode({ data }: NodeProps) {
color: "var(--text-primary)",
border: `2px solid ${borderColor}`,
boxShadow: isPausedHere || isActiveNode || isExecutingNode ? `0 0 4px ${glowColor}` : undefined,
animation: (isActiveNode || isExecutingNode) && !isPausedHere ? `node-pulse-${isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
animation: isPausedHere || isActiveNode || isExecutingNode ? `node-pulse-${isPausedHere ? "red" : isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
}}
title={label}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function GroupNode({ data }: NodeProps) {
const isExecutingNode = data.isExecutingNode as boolean | undefined;

const borderColor = isPausedHere
? "var(--accent)"
? "var(--error)"
: isExecutingNode
? "var(--success)"
: isActiveNode
Expand All @@ -33,7 +33,7 @@ export default function GroupNode({ data }: NodeProps) {
? "var(--error)"
: "var(--bg-tertiary)";

const glowColor = isExecutingNode ? "var(--success)" : "var(--accent)";
const glowColor = isPausedHere ? "var(--error)" : isExecutingNode ? "var(--success)" : "var(--accent)";

return (
<div
Expand All @@ -45,7 +45,7 @@ export default function GroupNode({ data }: NodeProps) {
border: `1.5px ${isPausedHere || isActiveNode || isExecutingNode ? "solid" : "dashed"} ${borderColor}`,
borderRadius: 8,
boxShadow: isPausedHere || isActiveNode || isExecutingNode ? `0 0 4px ${glowColor}` : undefined,
animation: (isActiveNode || isExecutingNode) && !isPausedHere ? `node-pulse-${isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
animation: isPausedHere || isActiveNode || isExecutingNode ? `node-pulse-${isPausedHere ? "red" : isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
}}
>
{hasBreakpoint && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function ModelNode({ data }: NodeProps) {
const isExecutingNode = data.isExecutingNode as boolean | undefined;

const borderColor = isPausedHere
? "var(--accent)"
? "var(--error)"
: isExecutingNode
? "var(--success)"
: isActiveNode
Expand All @@ -34,7 +34,7 @@ export default function ModelNode({ data }: NodeProps) {
? "var(--error)"
: "var(--node-border)";

const glowColor = isExecutingNode ? "var(--success)" : "var(--accent)";
const glowColor = isPausedHere ? "var(--error)" : isExecutingNode ? "var(--success)" : "var(--accent)";

return (
<div
Expand All @@ -45,7 +45,7 @@ export default function ModelNode({ data }: NodeProps) {
color: "var(--text-primary)",
border: `2px solid ${borderColor}`,
boxShadow: isPausedHere || isActiveNode || isExecutingNode ? `0 0 4px ${glowColor}` : undefined,
animation: (isActiveNode || isExecutingNode) && !isPausedHere ? `node-pulse-${isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
animation: isPausedHere || isActiveNode || isExecutingNode ? `node-pulse-${isPausedHere ? "red" : isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
}}
title={modelName ? `${label}\n${modelName}` : label}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function StartNode({ data }: NodeProps) {
const isExecutingNode = data.isExecutingNode as boolean | undefined;

const borderColor = isPausedHere
? "var(--accent)"
? "var(--error)"
: isExecutingNode
? "var(--success)"
: isActiveNode
Expand All @@ -31,7 +31,7 @@ export default function StartNode({ data }: NodeProps) {
? "var(--warning)"
: "var(--node-border)";

const glowColor = isExecutingNode ? "var(--success)" : "var(--accent)";
const glowColor = isPausedHere ? "var(--error)" : isExecutingNode ? "var(--success)" : "var(--accent)";

return (
<div
Expand All @@ -42,7 +42,7 @@ export default function StartNode({ data }: NodeProps) {
color: "var(--text-primary)",
border: `2px solid ${borderColor}`,
boxShadow: isPausedHere || isActiveNode || isExecutingNode ? `0 0 4px ${glowColor}` : undefined,
animation: (isActiveNode || isExecutingNode) && !isPausedHere ? `node-pulse-${isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
animation: isPausedHere || isActiveNode || isExecutingNode ? `node-pulse-${isPausedHere ? "red" : isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
}}
title={label}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function ToolNode({ data }: NodeProps) {
const isExecutingNode = data.isExecutingNode as boolean | undefined;

const borderColor = isPausedHere
? "var(--accent)"
? "var(--error)"
: isExecutingNode
? "var(--success)"
: isActiveNode
Expand All @@ -37,7 +37,7 @@ export default function ToolNode({ data }: NodeProps) {
? "var(--error)"
: "var(--node-border)";

const glowColor = isExecutingNode ? "var(--success)" : "var(--accent)";
const glowColor = isPausedHere ? "var(--error)" : isExecutingNode ? "var(--success)" : "var(--accent)";

const visibleTools = toolNames?.slice(0, MAX_VISIBLE_TOOLS) ?? [];
const remaining = (toolCount ?? toolNames?.length ?? 0) - visibleTools.length;
Expand All @@ -51,7 +51,7 @@ export default function ToolNode({ data }: NodeProps) {
color: "var(--text-primary)",
border: `2px solid ${borderColor}`,
boxShadow: isPausedHere || isActiveNode || isExecutingNode ? `0 0 4px ${glowColor}` : undefined,
animation: (isActiveNode || isExecutingNode) && !isPausedHere ? `node-pulse-${isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
animation: isPausedHere || isActiveNode || isExecutingNode ? `node-pulse-${isPausedHere ? "red" : isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
}}
title={toolNames?.length ? `${label}\n\n${toolNames.join("\n")}` : label}
>
Expand Down
Loading