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.54"
version = "0.0.55"
description = "UiPath Developer Console"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
4 changes: 2 additions & 2 deletions src/uipath/dev/infrastructure/logging_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import os
import re
import threading
from datetime import datetime
from datetime import datetime, timezone
from typing import Callable, Pattern

from uipath.runtime.logging import UiPathRuntimeExecutionLogHandler
Expand Down Expand Up @@ -35,7 +35,7 @@ def emit(self, record: logging.LogRecord):
run_id=self.run_id,
level=record.levelname,
message=self.format(record),
timestamp=datetime.fromtimestamp(record.created),
timestamp=datetime.fromtimestamp(record.created, tz=timezone.utc),
)
self.callback(log_data)
except Exception:
Expand Down
8 changes: 5 additions & 3 deletions src/uipath/dev/infrastructure/tracing_exporter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Custom OpenTelemetry trace exporter for CLI UI integration."""

import logging
from datetime import datetime
from datetime import datetime, timezone
from typing import Callable, Sequence

from opentelemetry import trace
Expand Down Expand Up @@ -82,7 +82,7 @@ def _export_span(self, span: ReadableSpan):
trace_id=trace_id,
status=status,
duration_ms=duration_ms,
timestamp=datetime.fromtimestamp(start_time),
timestamp=datetime.fromtimestamp(start_time, tz=timezone.utc),
attributes=dict(span.attributes) if span.attributes else {},
)

Expand All @@ -97,7 +97,9 @@ def _export_span(self, span: ReadableSpan):
run_id=run_id_val,
level=log_level,
message=event.name,
timestamp=datetime.fromtimestamp(event.timestamp / 1_000_000_000),
timestamp=datetime.fromtimestamp(
event.timestamp / 1_000_000_000, tz=timezone.utc
),
)
self.on_log(log_data)

Expand Down
8 changes: 4 additions & 4 deletions src/uipath/dev/models/chat.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Aggregates conversation messages from conversation events."""

from datetime import datetime
from datetime import datetime, timezone
from uuid import uuid4

from uipath.core.chat import (
Expand Down Expand Up @@ -178,7 +178,7 @@ def get_timestamp(self, ev: UiPathConversationMessageEvent) -> str:
"""Choose timestamp from event if available, else fallback."""
if ev.start and ev.start.timestamp:
return ev.start.timestamp
return datetime.now().isoformat()
return datetime.now(timezone.utc).isoformat()

def get_role(self, ev: UiPathConversationMessageEvent) -> str:
"""Infer the role of the message from the event."""
Expand All @@ -189,7 +189,7 @@ def get_role(self, ev: UiPathConversationMessageEvent) -> str:

def get_user_message(user_text: str) -> UiPathConversationMessage:
"""Build a user message from text input."""
timestamp = datetime.now().isoformat()
timestamp = datetime.now(timezone.utc).isoformat()
return UiPathConversationMessage(
message_id=str(uuid4()),
created_at=timestamp,
Expand All @@ -216,7 +216,7 @@ def get_user_message_event(
"""Build a conversation event representing a user message from text input."""
message_id = str(uuid4())
content_part_id = str(uuid4())
timestamp = datetime.now().isoformat()
timestamp = datetime.now(timezone.utc).isoformat()

msg_start = UiPathConversationMessageStartEvent(
role=role,
Expand Down
6 changes: 3 additions & 3 deletions src/uipath/dev/models/data.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Plain data classes for inter-component communication (no Textual dependency)."""

from dataclasses import dataclass, field
from datetime import datetime
from datetime import datetime, timezone
from typing import Any

from uipath.core.chat import UiPathConversationMessage, UiPathConversationMessageEvent
Expand All @@ -28,7 +28,7 @@ class TraceData:
trace_id: str | None = None
status: str = "running"
duration_ms: float | None = None
timestamp: datetime = field(default_factory=datetime.now)
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
attributes: dict[str, Any] = field(default_factory=dict)


Expand All @@ -41,7 +41,7 @@ class StateData:
qualified_node_name: str | None = None
phase: str | None = None
payload: dict[str, Any] | None = None
timestamp: datetime = field(default_factory=datetime.now)
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))


@dataclass
Expand Down
6 changes: 3 additions & 3 deletions src/uipath/dev/models/execution.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Models for representing execution runs and their data."""

import os
from datetime import datetime
from datetime import datetime, timezone
from enum import Enum
from typing import Any, cast
from uuid import uuid4
Expand Down Expand Up @@ -38,7 +38,7 @@ def __init__(
self.mode = mode
self.resume_data: Any | None = None
self.output_data: dict[str, Any] | str | None = None
self.start_time = datetime.now()
self.start_time = datetime.now(timezone.utc)
self.end_time: datetime | None = None
self.status = "pending" # pending, running, completed, failed, suspended
self.traces: list[TraceData] = []
Expand All @@ -58,7 +58,7 @@ def duration(self) -> str:
delta = self.end_time - self.start_time
return f"{delta.total_seconds():.1f}s"
elif self.start_time:
delta = datetime.now() - self.start_time
delta = datetime.now(timezone.utc) - self.start_time
return f"{delta.total_seconds():.1f}s"
return "0.0s"

Expand Down
6 changes: 3 additions & 3 deletions src/uipath/dev/models/messages.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Messages used for inter-component communication in the UiPath Developer Console."""

from datetime import datetime
from datetime import datetime, timezone
from typing import Any

from rich.console import RenderableType
Expand All @@ -24,7 +24,7 @@ def __init__(
self.run_id = run_id
self.level = level
self.message = message
self.timestamp = timestamp or datetime.now()
self.timestamp = timestamp or datetime.now(timezone.utc)
super().__init__()

@classmethod
Expand Down Expand Up @@ -61,7 +61,7 @@ def __init__(
self.trace_id = trace_id
self.status = status
self.duration_ms = duration_ms
self.timestamp = timestamp or datetime.now()
self.timestamp = timestamp or datetime.now(timezone.utc)
self.attributes = attributes or {}
super().__init__()

Expand Down
1 change: 1 addition & 0 deletions src/uipath/dev/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def __init__(
on_state=self._on_state,
on_interrupt=self._on_interrupt,
debug_bridge_factory=lambda mode: WebDebugBridge(mode=mode),
on_run_removed=self.connection_manager.remove_run_subscriptions,
)

def create_app(self) -> Any:
Expand Down
14 changes: 9 additions & 5 deletions src/uipath/dev/server/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default function App() {
setStateEvents,
setGraphCache,
setActiveNode,
removeActiveNode,
} = useRunStore();
const { view, runId: routeRunId, setupEntrypoint, setupMode, navigate } = useHashRoute();

Expand Down Expand Up @@ -97,15 +98,18 @@ export default function App() {
payload: s.payload,
})),
);
// Seed activeNodes from historical events so the next WS event has proper prev context
// Seed activeNodes from historical events (replay all to get correct prev + executing)
if (detail.status !== "completed" && detail.status !== "failed") {
const lastStarted = [...detail.states].reverse().find((s) => s.phase === "started");
if (lastStarted) {
setActiveNode(runId, lastStarted.node_name, lastStarted.qualified_node_name);
for (const s of detail.states) {
if (s.phase === "started") {
setActiveNode(runId, s.node_name, s.qualified_node_name);
} else if (s.phase === "completed") {
removeActiveNode(runId, s.node_name);
}
}
}
}
}, [upsertRun, setTraces, setLogs, setChatMessages, setStateEvents, setGraphCache, setActiveNode]);
}, [upsertRun, setTraces, setLogs, setChatMessages, setStateEvents, setGraphCache, setActiveNode, removeActiveNode]);

// Subscribe to selected run
useEffect(() => {
Expand Down
73 changes: 45 additions & 28 deletions src/uipath/dev/server/frontend/src/components/graph/GraphPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -448,18 +448,32 @@ export default function GraphPanel({ entrypoint, runId, breakpointNode, breakpoi
);
}, [breakpointNode, layoutSeq, setNodes]);

const stateEvents = useRunStore((s) => s.stateEvents[runId]);

// Highlight edges + nodes during execution
// - Paused at breakpoint: edges INTO breakpoint node + edges to next_nodes
// - Running: edges OUT of completed node, target nodes of those edges
// - Running: edges OUT of executing nodes, target nodes of those edges
// - __start__: highlighted on first state event; __end__: highlighted when run completes
useEffect(() => {
const isPaused = !!breakpointNode;
let matchIds = new Set<string>(); // Full React Flow node IDs of the "current" node
let matchIds = new Set<string>(); // Full React Flow node IDs of the "current" node(s)
const prevNodeIds = new Set<string>(); // Full RF IDs of the previous node (for edge filtering when paused)
const nextNodeIds = new Set<string>(); // Full RF IDs of breakpoint next_nodes
const activeTargetIds = new Set<string>(); // Full RF IDs for isActiveNode
const nodeTypeById = new Map<string, string>();

// Derive currently-executing nodes from the full event log (always consistent)
const executingNodes = new Map<string, string | null>(); // nodeName → qualifiedNodeName
if (stateEvents) {
for (const evt of stateEvents) {
if (evt.phase === "started") {
executingNodes.set(evt.node_name, evt.qualified_node_name ?? null);
} else if (evt.phase === "completed") {
executingNodes.delete(evt.node_name);
}
}
}

// 1) Build matchIds, nextNodeIds, node type map
setNodes((nds) => {
for (const n of nds) {
Expand Down Expand Up @@ -494,33 +508,38 @@ export default function GraphPanel({ entrypoint, runId, breakpointNode, breakpoi
if (activeNode?.prev) {
findNodeIds(activeNode.prev).forEach((id) => prevNodeIds.add(id));
}
} else if (activeNode) {
// Try qualified name first (exact match via "subgraph:node" → "subgraph/node")
const qualifiedName = activeNode.qualifiedNodeName;
if (qualifiedName) {
const qualifiedId = qualifiedName.replace(/:/g, "/");
for (const n of nds) {
if (n.id === qualifiedId) {
matchIds.add(n.id);
}
} else if (executingNodes.size > 0) {
// Build label → RF ID lookup once
const labelToIds = new Map<string, Set<string>>();
for (const n of nds) {
const label = n.data?.label as string | undefined;
if (!label) continue;
const plainId = n.id.includes("/") ? n.id.split("/").pop()! : n.id;
for (const key of [plainId, label]) {
let s = labelToIds.get(key);
if (!s) { s = new Set(); labelToIds.set(key, s); }
s.add(n.id);
}
}
// Fallback: label/plainId matching
if (matchIds.size === 0) {
const labelToIds = new Map<string, Set<string>>();
for (const n of nds) {
const label = n.data?.label as string | undefined;
if (!label) continue;
const plainId = n.id.includes("/") ? n.id.split("/").pop()! : n.id;
for (const key of [plainId, label]) {
let s = labelToIds.get(key);
if (!s) { s = new Set(); labelToIds.set(key, s); }
s.add(n.id);

for (const [nodeName, qualifiedNodeName] of executingNodes) {
let found = false;
// Try qualified name first (exact match via "subgraph:node" → "subgraph/node")
if (qualifiedNodeName) {
const qualifiedId = qualifiedNodeName.replace(/:/g, "/");
for (const n of nds) {
if (n.id === qualifiedId) {
matchIds.add(n.id);
found = true;
}
}
}
matchIds = labelToIds.get(activeNode.current) ?? new Set<string>();
// Fallback: label/plainId matching
if (!found) {
const ids = labelToIds.get(nodeName);
if (ids) ids.forEach((id) => matchIds.add(id));
}
}

}

return nds;
Expand All @@ -542,7 +561,7 @@ export default function GraphPanel({ entrypoint, runId, breakpointNode, breakpoi
isActive = intoBreakpoint
|| (matchIds.has(e.source) && nextNodeIds.has(e.target));
} else {
// Running: edges OUT of completed node
// Running: edges OUT of executing nodes
isActive = matchIds.has(e.source);
// For __end__: also highlight edges INTO it
if (!isActive && nodeTypeById.get(e.target) === "endNode" && matchIds.has(e.target)) {
Expand Down Expand Up @@ -596,9 +615,7 @@ export default function GraphPanel({ entrypoint, runId, breakpointNode, breakpoi
: n;
}),
);
}, [activeNode, breakpointNode, breakpointNextNodes, runStatus, layoutSeq, setNodes, setEdges]);

const stateEvents = useRunStore((s) => s.stateEvents[runId]);
}, [stateEvents, activeNode, breakpointNode, breakpointNextNodes, runStatus, layoutSeq, setNodes, setEdges]);

// Subscribe to cached graph reactively (populated async from run detail)
const cachedGraph = useRunStore((s) => s.graphCache[runId]);
Expand Down
32 changes: 32 additions & 0 deletions src/uipath/dev/server/frontend/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,22 @@ export default function Sidebar({ runs, selectedRunId, onSelectRun, onNewRun, is
</p>
)}
</div>

{/* GitHub link */}
<div className="px-3 h-10 border-t border-[var(--border)] flex items-center justify-center">
<a
href="https://github.com/UiPath/uipath-dev-python"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-[11px] uppercase tracking-widest font-semibold transition-opacity hover:opacity-80"
style={{ color: "var(--text-muted)" }}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/>
</svg>
GitHub
</a>
</div>
</aside>
</>
);
Expand Down Expand Up @@ -197,6 +213,22 @@ export default function Sidebar({ runs, selectedRunId, onSelectRun, onNewRun, is
</p>
)}
</div>

{/* GitHub link */}
<div className="px-3 h-10 border-t border-[var(--border)] flex items-center justify-center">
<a
href="https://github.com/UiPath/uipath-dev-python"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-[11px] uppercase tracking-widest font-semibold transition-opacity hover:opacity-80"
style={{ color: "var(--text-muted)" }}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/>
</svg>
GitHub
</a>
</div>
</aside>
);
}
Loading