Skip to content

Commit 93774b9

Browse files
authored
Merge pull request UiPath#70 from UiPath/fix/ws-event-delivery-and-breakpoint-glow
fix: improve WS event delivery and add breakpoint node glow
2 parents 9e0114d + 92c115f commit 93774b9

15 files changed

Lines changed: 214 additions & 161 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-dev"
3-
version = "0.0.48"
3+
version = "0.0.49"
44
description = "UiPath Developer Console"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/dev/server/frontend/src/App.tsx

Lines changed: 86 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { useEffect } from "react";
1+
import { useCallback, useEffect, useRef } from "react";
22
import { useRunStore } from "./store/useRunStore";
33
import { useWebSocket } from "./store/useWebSocket";
44
import { listRuns, listEntrypoints, getRun } from "./api/client";
5+
import type { RunDetail } from "./types/run";
56
import { useHashRoute } from "./hooks/useHashRoute";
67
import Sidebar from "./components/layout/Sidebar";
78
import NewRunPanel from "./components/runs/NewRunPanel";
@@ -41,79 +42,109 @@ export default function App() {
4142
.catch(console.error);
4243
}, [setRuns, setEntrypoints]);
4344

45+
const selectedRun = selectedRunId ? runs[selectedRunId] : null;
46+
47+
// Shared helper: apply a full run detail response to the store
48+
const applyRunDetail = useCallback((runId: string, detail: RunDetail) => {
49+
upsertRun(detail);
50+
setTraces(runId, detail.traces);
51+
setLogs(runId, detail.logs);
52+
// Convert messages to chat format (server uses camelCase aliases)
53+
const chatMsgs = (detail.messages as unknown as Record<string, unknown>[]).map((m: Record<string, unknown>) => {
54+
const parts = ((m.contentParts ?? m.content_parts) as Array<Record<string, unknown>>) ?? [];
55+
const toolCalls = ((m.toolCalls ?? m.tool_calls) as Array<Record<string, unknown>>) ?? [];
56+
return {
57+
message_id: ((m.messageId ?? m.message_id) as string),
58+
role: (m.role as string) ?? "assistant",
59+
content:
60+
parts
61+
.filter((p) => {
62+
const mime = ((p.mimeType ?? p.mime_type) as string) ?? "";
63+
return mime.startsWith("text/") || mime === "application/json";
64+
})
65+
.map((p) => {
66+
const data = p.data as Record<string, unknown>;
67+
return (data?.inline as string) ?? "";
68+
})
69+
.join("\n")
70+
.trim() ?? "",
71+
tool_calls: toolCalls.length > 0
72+
? toolCalls.map((tc) => ({
73+
name: (tc.name as string) ?? "",
74+
has_result: !!tc.result,
75+
}))
76+
: undefined,
77+
};
78+
});
79+
setChatMessages(runId, chatMsgs);
80+
// Cache graph data per run (persists across reloads)
81+
if (detail.graph && detail.graph.nodes.length > 0) {
82+
setGraphCache(runId, detail.graph);
83+
}
84+
// Load persisted state events
85+
if (detail.states && detail.states.length > 0) {
86+
setStateEvents(
87+
runId,
88+
detail.states.map((s) => ({
89+
node_name: s.node_name,
90+
qualified_node_name: s.qualified_node_name,
91+
phase: s.phase,
92+
timestamp: new Date(s.timestamp).getTime(),
93+
payload: s.payload,
94+
})),
95+
);
96+
}
97+
}, [upsertRun, setTraces, setLogs, setChatMessages, setStateEvents, setGraphCache]);
98+
4499
// Subscribe to selected run
45100
useEffect(() => {
46101
if (!selectedRunId) return;
47102
ws.subscribe(selectedRunId);
48103

49-
const applyRunDetail = (detail: Awaited<ReturnType<typeof getRun>>) => {
50-
upsertRun(detail);
51-
setTraces(selectedRunId, detail.traces);
52-
setLogs(selectedRunId, detail.logs);
53-
// Convert messages to chat format (server uses camelCase aliases)
54-
const chatMsgs = (detail.messages as unknown as Record<string, unknown>[]).map((m: Record<string, unknown>) => {
55-
const parts = ((m.contentParts ?? m.content_parts) as Array<Record<string, unknown>>) ?? [];
56-
const toolCalls = ((m.toolCalls ?? m.tool_calls) as Array<Record<string, unknown>>) ?? [];
57-
return {
58-
message_id: ((m.messageId ?? m.message_id) as string),
59-
role: (m.role as string) ?? "assistant",
60-
content:
61-
parts
62-
.filter((p) => {
63-
const mime = ((p.mimeType ?? p.mime_type) as string) ?? "";
64-
return mime.startsWith("text/") || mime === "application/json";
65-
})
66-
.map((p) => {
67-
const data = p.data as Record<string, unknown>;
68-
return (data?.inline as string) ?? "";
69-
})
70-
.join("\n")
71-
.trim() ?? "",
72-
tool_calls: toolCalls.length > 0
73-
? toolCalls.map((tc) => ({
74-
name: (tc.name as string) ?? "",
75-
has_result: !!tc.result,
76-
}))
77-
: undefined,
78-
};
79-
});
80-
setChatMessages(selectedRunId, chatMsgs);
81-
// Cache graph data per run (persists across reloads)
82-
if (detail.graph && detail.graph.nodes.length > 0) {
83-
setGraphCache(selectedRunId, detail.graph);
84-
}
85-
// Load persisted state events
86-
if (detail.states && detail.states.length > 0) {
87-
setStateEvents(
88-
selectedRunId,
89-
detail.states.map((s) => ({
90-
node_name: s.node_name,
91-
qualified_node_name: s.qualified_node_name,
92-
phase: s.phase,
93-
timestamp: new Date(s.timestamp).getTime(),
94-
payload: s.payload,
95-
})),
96-
);
97-
}
98-
};
99-
100104
// Fetch full run details (includes fresh status in case we missed run.updated events)
101-
getRun(selectedRunId).then(applyRunDetail).catch(console.error);
105+
getRun(selectedRunId).then((d) => applyRunDetail(selectedRunId, d)).catch(console.error);
102106

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

112116
return () => {
113117
clearTimeout(retryTimer);
114118
ws.unsubscribe(selectedRunId);
115119
};
116-
}, [selectedRunId, ws, upsertRun, setTraces, setLogs, setChatMessages, setStateEvents, setGraphCache]);
120+
}, [selectedRunId, ws, applyRunDetail]);
121+
122+
// Refetch full details when run reaches terminal status, but only if WS events were missed
123+
const prevStatusRef = useRef<string | null>(null);
124+
useEffect(() => {
125+
if (!selectedRunId) return;
126+
const status = selectedRun?.status;
127+
const prev = prevStatusRef.current;
128+
prevStatusRef.current = status ?? null;
129+
130+
if (
131+
status &&
132+
(status === "completed" || status === "failed") &&
133+
prev !== status
134+
) {
135+
// Compare what we received via WS against the counts in the run summary.
136+
// Only refetch if something was missed — avoids unnecessary re-renders / flicker.
137+
const state = useRunStore.getState();
138+
const haveTraces = state.traces[selectedRunId]?.length ?? 0;
139+
const haveLogs = state.logs[selectedRunId]?.length ?? 0;
140+
const expectedTraces = selectedRun?.trace_count ?? 0;
141+
const expectedLogs = selectedRun?.log_count ?? 0;
142+
143+
if (haveTraces < expectedTraces || haveLogs < expectedLogs) {
144+
getRun(selectedRunId).then((d) => applyRunDetail(selectedRunId, d)).catch(console.error);
145+
}
146+
}
147+
}, [selectedRunId, selectedRun?.status, applyRunDetail]);
117148

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

132-
const selectedRun = selectedRunId ? runs[selectedRunId] : null;
133-
134163
return (
135164
<div className="flex h-screen w-screen">
136165
<Sidebar

src/uipath/dev/server/frontend/src/api/websocket.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,20 @@ export class WsClient {
3535
};
3636

3737
this.ws.onmessage = (event) => {
38+
let msg: ServerMessage;
3839
try {
39-
const msg: ServerMessage = JSON.parse(event.data);
40-
this.handlers.forEach((h) => h(msg));
40+
msg = JSON.parse(event.data);
4141
} catch {
4242
console.warn("[ws] failed to parse message", event.data);
43+
return;
4344
}
45+
this.handlers.forEach((h) => {
46+
try {
47+
h(msg);
48+
} catch (e) {
49+
console.error("[ws] handler error", e);
50+
}
51+
});
4452
};
4553

4654
this.ws.onclose = () => {

src/uipath/dev/server/frontend/src/components/graph/GraphPanel.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,10 @@ export default function GraphPanel({ entrypoint, runId, breakpointNode, breakpoi
831831
0%, 100% { box-shadow: 0 0 4px var(--success); }
832832
50% { box-shadow: 0 0 10px var(--success); }
833833
}
834+
@keyframes node-pulse-red {
835+
0%, 100% { box-shadow: 0 0 4px var(--error); }
836+
50% { box-shadow: 0 0 10px var(--error); }
837+
}
834838
`}</style>
835839
<ReactFlow
836840
nodes={nodes}

src/uipath/dev/server/frontend/src/components/graph/nodes/DefaultNode.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default function DefaultNode({ data }: NodeProps) {
1010
const isExecutingNode = data.isExecutingNode as boolean | undefined;
1111

1212
const borderColor = isPausedHere
13-
? "var(--accent)"
13+
? "var(--error)"
1414
: isExecutingNode
1515
? "var(--success)"
1616
: isActiveNode
@@ -23,7 +23,7 @@ export default function DefaultNode({ data }: NodeProps) {
2323
? "var(--error)"
2424
: "var(--node-border)";
2525

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

2828
return (
2929
<div
@@ -34,7 +34,7 @@ export default function DefaultNode({ data }: NodeProps) {
3434
color: "var(--text-primary)",
3535
border: `2px solid ${borderColor}`,
3636
boxShadow: isPausedHere || isActiveNode || isExecutingNode ? `0 0 4px ${glowColor}` : undefined,
37-
animation: (isActiveNode || isExecutingNode) && !isPausedHere ? `node-pulse-${isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
37+
animation: isPausedHere || isActiveNode || isExecutingNode ? `node-pulse-${isPausedHere ? "red" : isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
3838
}}
3939
title={label}
4040
>

src/uipath/dev/server/frontend/src/components/graph/nodes/EndNode.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default function EndNode({ data }: NodeProps) {
2020
const isExecutingNode = data.isExecutingNode as boolean | undefined;
2121

2222
const borderColor = isPausedHere
23-
? "var(--accent)"
23+
? "var(--error)"
2424
: isExecutingNode
2525
? "var(--success)"
2626
: isActiveNode
@@ -31,7 +31,7 @@ export default function EndNode({ data }: NodeProps) {
3131
? "var(--error)"
3232
: "var(--node-border)";
3333

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

3636
return (
3737
<div
@@ -42,7 +42,7 @@ export default function EndNode({ data }: NodeProps) {
4242
color: "var(--text-primary)",
4343
border: `2px solid ${borderColor}`,
4444
boxShadow: isPausedHere || isActiveNode || isExecutingNode ? `0 0 4px ${glowColor}` : undefined,
45-
animation: (isActiveNode || isExecutingNode) && !isPausedHere ? `node-pulse-${isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
45+
animation: isPausedHere || isActiveNode || isExecutingNode ? `node-pulse-${isPausedHere ? "red" : isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
4646
}}
4747
title={label}
4848
>

src/uipath/dev/server/frontend/src/components/graph/nodes/GroupNode.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default function GroupNode({ data }: NodeProps) {
2020
const isExecutingNode = data.isExecutingNode as boolean | undefined;
2121

2222
const borderColor = isPausedHere
23-
? "var(--accent)"
23+
? "var(--error)"
2424
: isExecutingNode
2525
? "var(--success)"
2626
: isActiveNode
@@ -33,7 +33,7 @@ export default function GroupNode({ data }: NodeProps) {
3333
? "var(--error)"
3434
: "var(--bg-tertiary)";
3535

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

3838
return (
3939
<div
@@ -45,7 +45,7 @@ export default function GroupNode({ data }: NodeProps) {
4545
border: `1.5px ${isPausedHere || isActiveNode || isExecutingNode ? "solid" : "dashed"} ${borderColor}`,
4646
borderRadius: 8,
4747
boxShadow: isPausedHere || isActiveNode || isExecutingNode ? `0 0 4px ${glowColor}` : undefined,
48-
animation: (isActiveNode || isExecutingNode) && !isPausedHere ? `node-pulse-${isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
48+
animation: isPausedHere || isActiveNode || isExecutingNode ? `node-pulse-${isPausedHere ? "red" : isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
4949
}}
5050
>
5151
{hasBreakpoint && (

src/uipath/dev/server/frontend/src/components/graph/nodes/ModelNode.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default function ModelNode({ data }: NodeProps) {
2121
const isExecutingNode = data.isExecutingNode as boolean | undefined;
2222

2323
const borderColor = isPausedHere
24-
? "var(--accent)"
24+
? "var(--error)"
2525
: isExecutingNode
2626
? "var(--success)"
2727
: isActiveNode
@@ -34,7 +34,7 @@ export default function ModelNode({ data }: NodeProps) {
3434
? "var(--error)"
3535
: "var(--node-border)";
3636

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

3939
return (
4040
<div
@@ -45,7 +45,7 @@ export default function ModelNode({ data }: NodeProps) {
4545
color: "var(--text-primary)",
4646
border: `2px solid ${borderColor}`,
4747
boxShadow: isPausedHere || isActiveNode || isExecutingNode ? `0 0 4px ${glowColor}` : undefined,
48-
animation: (isActiveNode || isExecutingNode) && !isPausedHere ? `node-pulse-${isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
48+
animation: isPausedHere || isActiveNode || isExecutingNode ? `node-pulse-${isPausedHere ? "red" : isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
4949
}}
5050
title={modelName ? `${label}\n${modelName}` : label}
5151
>

src/uipath/dev/server/frontend/src/components/graph/nodes/StartNode.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default function StartNode({ data }: NodeProps) {
2020
const isExecutingNode = data.isExecutingNode as boolean | undefined;
2121

2222
const borderColor = isPausedHere
23-
? "var(--accent)"
23+
? "var(--error)"
2424
: isExecutingNode
2525
? "var(--success)"
2626
: isActiveNode
@@ -31,7 +31,7 @@ export default function StartNode({ data }: NodeProps) {
3131
? "var(--warning)"
3232
: "var(--node-border)";
3333

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

3636
return (
3737
<div
@@ -42,7 +42,7 @@ export default function StartNode({ data }: NodeProps) {
4242
color: "var(--text-primary)",
4343
border: `2px solid ${borderColor}`,
4444
boxShadow: isPausedHere || isActiveNode || isExecutingNode ? `0 0 4px ${glowColor}` : undefined,
45-
animation: (isActiveNode || isExecutingNode) && !isPausedHere ? `node-pulse-${isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
45+
animation: isPausedHere || isActiveNode || isExecutingNode ? `node-pulse-${isPausedHere ? "red" : isExecutingNode ? "green" : "accent"} 1.5s ease-in-out infinite` : undefined,
4646
}}
4747
title={label}
4848
>

src/uipath/dev/server/frontend/src/components/graph/nodes/ToolNode.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default function ToolNode({ data }: NodeProps) {
2424
const isExecutingNode = data.isExecutingNode as boolean | undefined;
2525

2626
const borderColor = isPausedHere
27-
? "var(--accent)"
27+
? "var(--error)"
2828
: isExecutingNode
2929
? "var(--success)"
3030
: isActiveNode
@@ -37,7 +37,7 @@ export default function ToolNode({ data }: NodeProps) {
3737
? "var(--error)"
3838
: "var(--node-border)";
3939

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

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

0 commit comments

Comments
 (0)