Skip to content

Commit 38f71bc

Browse files
authored
Merge pull request UiPath#63 from UiPath/fix/span-details-sections-consistency
fix: span details, breakpoint edges, faulted runs, and runtime wrapping
2 parents 49a7cc3 + 94fc7ea commit 38f71bc

8 files changed

Lines changed: 162 additions & 136 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.43"
3+
version = "0.0.44"
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/components/graph/GraphPanel.tsx

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -524,13 +524,18 @@ export default function GraphPanel({ entrypoint, runId, breakpointNode, breakpoi
524524
});
525525

526526
// 2) Highlight edges
527-
setEdges((eds) =>
528-
eds.map((e) => {
527+
setEdges((eds) => {
528+
// Check if prev node actually has a direct edge into the breakpoint node.
529+
// If not (e.g. prev is a sibling child of the same parent), fall back to
530+
// highlighting all incoming edges to the breakpoint node.
531+
const prevHasDirectEdge = prevNodeIds.size === 0
532+
|| eds.some((e) => matchIds.has(e.target) && prevNodeIds.has(e.source));
533+
534+
return eds.map((e) => {
529535
let isActive: boolean;
530536
if (isPaused) {
531-
// Edge from previous node INTO breakpoint node + edges FROM breakpoint node TO next_nodes
532537
const intoBreakpoint = matchIds.has(e.target)
533-
&& (prevNodeIds.size === 0 || prevNodeIds.has(e.source));
538+
&& (prevNodeIds.size === 0 || !prevHasDirectEdge || prevNodeIds.has(e.source));
534539
isActive = intoBreakpoint
535540
|| (matchIds.has(e.source) && nextNodeIds.has(e.target));
536541
} else {
@@ -564,8 +569,8 @@ export default function GraphPanel({ entrypoint, runId, breakpointNode, breakpoi
564569
}
565570

566571
return e;
567-
}),
568-
);
572+
});
573+
});
569574

570575
// 3) Mark nodes as active
571576
// - Running: targets of highlighted edges + __start__/__end__ when matched
@@ -664,21 +669,20 @@ export default function GraphPanel({ entrypoint, runId, breakpointNode, breakpoi
664669

665670
// Build set of completed React Flow node IDs from state events
666671
const completedIds = new Set<string>();
667-
if (hasEvents) {
668-
const allNodeIds = new Set(nds.map((n) => n.id));
669-
// Fallback label→fullId map (used when no qualified name)
670-
const labelToIds = new Map<string, Set<string>>();
671-
for (const n of nds) {
672-
const label = n.data?.label as string | undefined;
673-
if (!label) continue;
674-
const plainId = n.id.includes("/") ? n.id.split("/").pop()! : n.id;
675-
for (const key of [plainId, label]) {
676-
let s = labelToIds.get(key);
677-
if (!s) { s = new Set(); labelToIds.set(key, s); }
678-
s.add(n.id);
679-
}
672+
const allNodeIds = new Set(nds.map((n) => n.id));
673+
const labelToIds = new Map<string, Set<string>>();
674+
for (const n of nds) {
675+
const label = n.data?.label as string | undefined;
676+
if (!label) continue;
677+
const plainId = n.id.includes("/") ? n.id.split("/").pop()! : n.id;
678+
for (const key of [plainId, label]) {
679+
let s = labelToIds.get(key);
680+
if (!s) { s = new Set(); labelToIds.set(key, s); }
681+
s.add(n.id);
680682
}
683+
}
681684

685+
if (hasEvents) {
682686
for (const evt of stateEvents) {
683687
let matched = false;
684688
if (evt.qualified_node_name) {
@@ -703,10 +707,18 @@ export default function GraphPanel({ entrypoint, runId, breakpointNode, breakpoi
703707
}
704708
}
705709

710+
// When run failed and no nodes were highlighted, mark the root node as failed
711+
let failedRootId: string | undefined;
712+
if (runStatus === "failed" && completedIds.size === 0) {
713+
failedRootId = nds.find((n) => !n.parentNode && n.type !== "startNode" && n.type !== "endNode" && n.type !== "groupNode")?.id;
714+
}
715+
706716
return nds.map((n) => {
707717
let status: string | undefined;
708718

709-
if (completedIds.has(n.id)) {
719+
if (n.id === failedRootId) {
720+
status = "failed";
721+
} else if (completedIds.has(n.id)) {
710722
status = "completed";
711723
} else if (n.type === "startNode") {
712724
// Top-level: completed once execution begins; subgraph: only if visited

src/uipath/dev/server/frontend/src/components/traces/SpanDetails.tsx

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -109,39 +109,8 @@ function AttributeValue({ value }: { value: unknown }) {
109109
);
110110
}
111111

112-
function IdRow({ label, value }: { label: string; value: string }) {
113-
const [copied, setCopied] = useState(false);
114-
const copy = useCallback(() => {
115-
navigator.clipboard.writeText(value).then(() => {
116-
setCopied(true);
117-
setTimeout(() => setCopied(false), 1500);
118-
});
119-
}, [value]);
120-
121-
return (
122-
<div className="flex items-center gap-2 group">
123-
<span className="text-[10px] uppercase font-semibold shrink-0 w-12" style={{ color: "var(--text-muted)" }}>
124-
{label}
125-
</span>
126-
<span
127-
className="text-[11px] font-mono truncate flex-1"
128-
style={{ color: "var(--text-secondary)" }}
129-
title={value}
130-
>
131-
{value}
132-
</span>
133-
<button
134-
onClick={copy}
135-
className="opacity-0 group-hover:opacity-100 text-[10px] cursor-pointer shrink-0"
136-
style={{ color: copied ? "var(--success)" : "var(--text-muted)" }}
137-
>
138-
{copied ? "copied" : "copy"}
139-
</button>
140-
</div>
141-
);
142-
}
143-
144112
export default function SpanDetails({ span }: Props) {
113+
const [attrsOpen, setAttrsOpen] = useState(true);
145114
const [idsOpen, setIdsOpen] = useState(false);
146115
const status = STATUS_CONFIG[span.status.toLowerCase()] ?? { ...DEFAULT_STATUS, label: span.status };
147116

@@ -188,16 +157,20 @@ export default function SpanDetails({ span }: Props) {
188157
</span>
189158
</div>
190159

191-
{/* Attributes — flat rows */}
160+
{/* Attributes — collapsible */}
192161
{attrEntries.length > 0 && (
193162
<>
194163
<div
195-
className="px-2 py-1 text-[10px] uppercase font-bold tracking-wider border-b"
164+
className="px-2 py-1 text-[10px] uppercase font-bold tracking-wider border-b cursor-pointer flex items-center"
196165
style={{ color: "var(--accent)", borderColor: "var(--border)", background: "var(--bg-secondary)" }}
166+
onClick={() => setAttrsOpen((o) => !o)}
197167
>
198-
Attributes ({attrEntries.length})
168+
<span className="flex-1">Attributes ({attrEntries.length})</span>
169+
<span style={{ color: "var(--text-muted)", transform: attrsOpen ? "rotate(0deg)" : "rotate(-90deg)" }}>
170+
&#x25BE;
171+
</span>
199172
</div>
200-
{attrEntries.map(([key, value], idx) => (
173+
{attrsOpen && attrEntries.map(([key, value], idx) => (
201174
<div
202175
key={key}
203176
className="flex gap-2 px-2 py-1 items-start border-b"
@@ -224,21 +197,37 @@ export default function SpanDetails({ span }: Props) {
224197
{/* Identifiers — collapsible */}
225198
<div
226199
className="px-2 py-1 text-[10px] uppercase font-bold tracking-wider border-b cursor-pointer flex items-center"
227-
style={{ color: "var(--info)", borderColor: "var(--border)", background: "var(--bg-secondary)" }}
200+
style={{ color: "var(--accent)", borderColor: "var(--border)", background: "var(--bg-secondary)" }}
228201
onClick={() => setIdsOpen((o) => !o)}
229202
>
230-
<span className="flex-1">Identifiers</span>
203+
<span className="flex-1">Identifiers ({ids.length})</span>
231204
<span style={{ color: "var(--text-muted)", transform: idsOpen ? "rotate(0deg)" : "rotate(-90deg)" }}>
232205
&#x25BE;
233206
</span>
234207
</div>
235-
{idsOpen && (
236-
<div className="px-2 py-1 space-y-0.5" style={{ background: "var(--bg-primary)" }}>
237-
{ids.map((id) => (
238-
<IdRow key={id.label} label={id.label} value={id.value} />
239-
))}
208+
{idsOpen && ids.map((id, idx) => (
209+
<div
210+
key={id.label}
211+
className="flex gap-2 px-2 py-1 items-start border-b"
212+
style={{
213+
borderColor: "var(--border)",
214+
background: idx % 2 === 0 ? "var(--bg-primary)" : "var(--bg-secondary)",
215+
}}
216+
>
217+
<span
218+
className="font-mono font-semibold shrink-0 pt-px truncate text-[11px]"
219+
style={{ color: "var(--info)", width: "35%" }}
220+
title={id.label}
221+
>
222+
{id.label}
223+
</span>
224+
<span className="flex-1 min-w-0">
225+
<span className="font-mono text-[11px] break-all" style={{ color: "var(--text-primary)" }}>
226+
{id.value}
227+
</span>
228+
</span>
240229
</div>
241-
)}
230+
))}
242231
</div>
243232
);
244233
}

src/uipath/dev/server/static/assets/index-C0Ai_DpI.js renamed to src/uipath/dev/server/static/assets/index-BOmQobls.js

Lines changed: 46 additions & 46 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/uipath/dev/server/static/assets/index-CQPdc1iX.css renamed to src/uipath/dev/server/static/assets/index-bgnPbWdx.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/uipath/dev/server/static/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>UiPath Developer Console</title>
77
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
8-
<script type="module" crossorigin src="/assets/index-C0Ai_DpI.js"></script>
9-
<link rel="stylesheet" crossorigin href="/assets/index-CQPdc1iX.css">
8+
<script type="module" crossorigin src="/assets/index-BOmQobls.js"></script>
9+
<link rel="stylesheet" crossorigin href="/assets/index-bgnPbWdx.css">
1010
</head>
1111
<body>
1212
<div id="root"></div>

src/uipath/dev/services/run_service.py

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,26 @@ async def execute(self, run: ExecutionRun) -> None:
164164
runtime_id=run.id,
165165
)
166166

167-
runtime: UiPathRuntimeProtocol
167+
runtime: UiPathRuntimeProtocol = new_runtime
168+
169+
if run.mode == ExecutionMode.CHAT:
170+
chat_bridge = WebChatBridge()
171+
chat_bridge.on_message = lambda evt: self._handle_chat_message_event(
172+
run, evt
173+
)
174+
chat_bridge.on_interrupt = lambda trigger: self._handle_interrupt(
175+
run, trigger
176+
)
177+
self.chat_bridges[run.id] = chat_bridge
178+
179+
# ChatRuntime handles suspend/resume internally
180+
runtime = cast(
181+
UiPathRuntimeProtocol,
182+
UiPathChatRuntime(
183+
delegate=runtime,
184+
chat_bridge=chat_bridge,
185+
),
186+
)
168187

169188
if self._debug_bridge_factory:
170189
debug_bridge = self._debug_bridge_factory(run.mode)
@@ -188,31 +207,15 @@ async def execute(self, run: ExecutionRun) -> None:
188207
self.debug_bridges[run.id] = debug_bridge
189208

190209
runtime = UiPathDebugRuntime(
191-
delegate=new_runtime,
210+
delegate=runtime,
192211
debug_bridge=debug_bridge,
193212
)
194-
else:
195-
runtime = new_runtime
196213

197214
if run.mode == ExecutionMode.CHAT:
198-
chat_bridge = WebChatBridge()
199-
chat_bridge.on_message = lambda evt: self._handle_chat_message_event(
200-
run, evt
201-
)
202-
chat_bridge.on_interrupt = lambda trigger: self._handle_interrupt(
203-
run, trigger
204-
)
205-
self.chat_bridges[run.id] = chat_bridge
206-
207-
# Wrap: ExecutionRuntime(ChatRuntime(runtime))
208-
# ChatRuntime handles suspend/resume internally,
215+
# Wrap: ExecutionRuntime(Debug(Chat(base)))
209216
# ExecutionRuntime's OTel span wraps the entire session.
210-
chat_runtime = UiPathChatRuntime(
211-
delegate=runtime,
212-
chat_bridge=chat_bridge,
213-
)
214217
execution_runtime = UiPathExecutionRuntime(
215-
delegate=cast(UiPathRuntimeProtocol, chat_runtime),
218+
delegate=runtime,
216219
trace_manager=self.trace_manager,
217220
log_handler=log_handler,
218221
execution_id=run.id,
@@ -244,6 +247,23 @@ async def execute(self, run: ExecutionRun) -> None:
244247
and result.trigger
245248
):
246249
run.status = "suspended"
250+
elif result.status == UiPathRuntimeStatus.FAULTED.value:
251+
run.status = "failed"
252+
run.error = result.error
253+
err = result.error
254+
error_state = StateData(
255+
run_id=run.id,
256+
node_name="__error__",
257+
payload={
258+
"status": "failed",
259+
"code": err.code if err else "Unknown",
260+
"title": err.title if err else "Unknown error",
261+
"detail": err.detail if err else "",
262+
},
263+
)
264+
run.states.append(error_state)
265+
if self.on_state is not None:
266+
self.on_state(error_state)
247267
else:
248268
run.status = "completed"
249269

@@ -257,7 +277,12 @@ async def execute(self, run: ExecutionRun) -> None:
257277
if run.output_data:
258278
self._add_info_log(run, f"Execution result: {run.output_data}")
259279

260-
self._add_info_log(run, "✅ Execution completed successfully")
280+
if run.status == "failed":
281+
err = run.error
282+
detail = f"{err.title}: {err.detail}" if err else "Unknown error"
283+
self._add_error_log(run, detail)
284+
else:
285+
self._add_info_log(run, "✅ Execution completed successfully")
261286
run.end_time = datetime.now()
262287

263288
except UiPathRuntimeError as e:

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)