Skip to content

Commit 1607b74

Browse files
authored
Merge pull request #7 from UiPath/fix/reactive_ui
fix: reactive ui
2 parents 39b1656 + 24b9ce2 commit 1607b74

File tree

4 files changed

+347
-196
lines changed

4 files changed

+347
-196
lines changed

src/uipath/dev/__init__.py

Lines changed: 57 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,35 @@
22

33
import asyncio
44
import json
5-
import traceback
65
from datetime import datetime
76
from pathlib import Path
8-
from typing import Any, Optional
7+
from typing import Any
98

109
import pyperclip # type: ignore[import-untyped]
11-
from pydantic import BaseModel
12-
from rich.traceback import Traceback
1310
from textual import on
1411
from textual.app import App, ComposeResult
1512
from textual.binding import Binding
1613
from textual.containers import Container, Horizontal
1714
from textual.widgets import Button, Footer, Input, ListView, RichLog
1815
from uipath.core.tracing import UiPathTraceManager
19-
from uipath.runtime import (
20-
UiPathExecuteOptions,
21-
UiPathExecutionRuntime,
22-
UiPathRuntimeFactoryProtocol,
23-
UiPathRuntimeStatus,
24-
)
25-
from uipath.runtime.errors import UiPathErrorContract, UiPathRuntimeError
16+
from uipath.runtime import UiPathRuntimeFactoryProtocol
2617

2718
from uipath.dev.infrastructure import (
28-
RunContextExporter,
29-
RunContextLogHandler,
3019
patch_textual_stderr,
3120
)
3221
from uipath.dev.models import ExecutionRun, LogMessage, TraceMessage
22+
from uipath.dev.services import RunService
3323
from uipath.dev.ui.panels import NewRunPanel, RunDetailsPanel, RunHistoryPanel
3424

3525

3626
class UiPathDeveloperConsole(App[Any]):
3727
"""UiPath developer console interface."""
3828

3929
TITLE = "UiPath Developer Console"
40-
SUB_TITLE = "Interactive terminal application for building, testing, and debugging UiPath Python runtimes, agents, and automation scripts."
30+
SUB_TITLE = (
31+
"Interactive terminal application for building, testing, and debugging "
32+
"UiPath Python runtimes, agents, and automation scripts."
33+
)
4134
CSS_PATH = Path(__file__).parent / "ui" / "styles" / "terminal.tcss"
4235

4336
BINDINGS = [
@@ -56,23 +49,27 @@ def __init__(
5649
**kwargs,
5750
):
5851
"""Initialize the UiPath Dev Terminal App."""
52+
# Capture subprocess stderr lines and route to our log handler
5953
self._stderr_write_fd: int = patch_textual_stderr(self._add_subprocess_log)
6054

6155
super().__init__(**kwargs)
6256

63-
self.initial_entrypoint: str = "main.py"
64-
self.initial_input: str = '{\n "message": "Hello World"\n}'
65-
self.runs: dict[str, ExecutionRun] = {}
6657
self.runtime_factory = runtime_factory
6758
self.trace_manager = trace_manager
68-
self.trace_manager.add_span_exporter(
69-
RunContextExporter(
70-
on_trace=self._handle_trace_message,
71-
on_log=self._handle_log_message,
72-
),
73-
batch=False,
59+
60+
# Core service: owns run state, logs, traces
61+
self.run_service = RunService(
62+
runtime_factory=self.runtime_factory,
63+
trace_manager=self.trace_manager,
64+
on_run_updated=self._on_run_updated,
65+
on_log=self._on_log_for_ui,
66+
on_trace=self._on_trace_for_ui,
7467
)
7568

69+
# Just defaults for convenience
70+
self.initial_entrypoint: str = "main.py"
71+
self.initial_input: str = '{\n "message": "Hello World"\n}'
72+
7673
def compose(self) -> ComposeResult:
7774
"""Compose the UI layout."""
7875
with Horizontal():
@@ -127,8 +124,10 @@ async def handle_chat_input(self, event: Input.Submitted) -> None:
127124
"Wait for agent response...", timeout=1.5, severity="warning"
128125
)
129126
return
127+
130128
if details_panel.current_run.status == "suspended":
131129
details_panel.current_run.resume_data = {"message": user_text}
130+
132131
asyncio.create_task(self._execute_runtime(details_panel.current_run))
133132
event.input.clear()
134133

@@ -145,24 +144,24 @@ async def action_cancel(self) -> None:
145144
await self.action_new_run()
146145

147146
async def action_execute_run(self) -> None:
148-
"""Execute a new run with UiPath runtime."""
147+
"""Execute a new run based on NewRunPanel inputs."""
149148
new_run_panel = self.query_one("#new-run-panel", NewRunPanel)
150149
entrypoint, input_data, conversational = new_run_panel.get_input_values()
151150

152151
if not entrypoint:
153152
return
154153

155-
input: dict[str, Any] = {}
156154
try:
157-
input = json.loads(input_data)
155+
input_payload: dict[str, Any] = json.loads(input_data)
158156
except json.JSONDecodeError:
159157
return
160158

161-
run = ExecutionRun(entrypoint, input, conversational)
159+
run = ExecutionRun(entrypoint, input_payload, conversational)
162160

163-
self.runs[run.id] = run
161+
history_panel = self.query_one("#history-panel", RunHistoryPanel)
162+
history_panel.add_run(run)
164163

165-
self._add_run_in_history(run)
164+
self.run_service.register_run(run)
166165

167166
self._show_run_details(run)
168167

@@ -187,140 +186,48 @@ def action_copy(self) -> None:
187186
else:
188187
self.app.notify("Nothing to copy here.", timeout=1.5, severity="warning")
189188

190-
async def _execute_runtime(self, run: ExecutionRun):
191-
"""Execute the script using UiPath runtime."""
192-
try:
193-
execution_input: Optional[dict[str, Any]] = {}
194-
execution_options: UiPathExecuteOptions = UiPathExecuteOptions()
195-
if run.status == "suspended":
196-
execution_input = run.resume_data
197-
execution_options.resume = True
198-
self._add_info_log(run, f"Resuming execution: {run.entrypoint}")
199-
else:
200-
execution_input = run.input_data
201-
self._add_info_log(run, f"Starting execution: {run.entrypoint}")
202-
203-
run.status = "running"
204-
run.start_time = datetime.now()
205-
log_handler = RunContextLogHandler(
206-
run_id=run.id,
207-
callback=self._handle_log_message,
208-
)
209-
runtime = await self.runtime_factory.new_runtime(entrypoint=run.entrypoint)
210-
execution_runtime = UiPathExecutionRuntime(
211-
delegate=runtime,
212-
trace_manager=self.trace_manager,
213-
log_handler=log_handler,
214-
execution_id=run.id,
215-
)
216-
result = await execution_runtime.execute(execution_input, execution_options)
217-
218-
if result is not None:
219-
if (
220-
result.status == UiPathRuntimeStatus.SUSPENDED.value
221-
and result.resume
222-
):
223-
run.status = "suspended"
224-
else:
225-
if result.output is None:
226-
run.output_data = {}
227-
elif isinstance(result.output, BaseModel):
228-
run.output_data = result.output.model_dump()
229-
else:
230-
run.output_data = result.output
231-
run.status = "completed"
232-
if run.output_data:
233-
self._add_info_log(run, f"Execution result: {run.output_data}")
234-
235-
self._add_info_log(run, "✅ Execution completed successfully")
236-
run.end_time = datetime.now()
237-
238-
except UiPathRuntimeError as e:
239-
self._add_error_log(run)
240-
run.status = "failed"
241-
run.end_time = datetime.now()
242-
run.error = e.error_info
243-
244-
except Exception as e:
245-
self._add_error_log(run)
246-
run.status = "failed"
247-
run.end_time = datetime.now()
248-
run.error = UiPathErrorContract(
249-
code="Unknown", title=str(e), detail=traceback.format_exc()
250-
)
251-
252-
self._update_run_in_history(run)
253-
self._update_run_details(run)
254-
255-
def _show_run_details(self, run: ExecutionRun):
189+
async def _execute_runtime(self, run: ExecutionRun) -> None:
190+
"""Wrapper that delegates execution to RunService."""
191+
await self.run_service.execute(run)
192+
193+
def _on_run_updated(self, run: ExecutionRun) -> None:
194+
"""Called whenever a run changes (status, times, logs, traces)."""
195+
# Update the run in history
196+
history_panel = self.query_one("#history-panel", RunHistoryPanel)
197+
history_panel.update_run(run)
198+
199+
# If this run is currently shown, refresh details
200+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
201+
if details_panel.current_run and details_panel.current_run.id == run.id:
202+
details_panel.update_run_details(run)
203+
204+
def _on_log_for_ui(self, log_msg: LogMessage) -> None:
205+
"""Append a log message to the logs UI."""
206+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
207+
details_panel.add_log(log_msg)
208+
209+
def _on_trace_for_ui(self, trace_msg: TraceMessage) -> None:
210+
"""Append/refresh traces in the UI."""
211+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
212+
details_panel.add_trace(trace_msg)
213+
214+
def _show_run_details(self, run: ExecutionRun) -> None:
256215
"""Show details panel for a specific run."""
257-
# Hide new run panel, show details panel
258216
new_panel = self.query_one("#new-run-panel")
259217
details_panel = self.query_one("#details-panel", RunDetailsPanel)
260218

261219
new_panel.add_class("hidden")
262220
details_panel.remove_class("hidden")
263221

264-
# Populate the details panel with run data
265222
details_panel.update_run(run)
266223

267-
def _focus_chat_input(self):
224+
def _focus_chat_input(self) -> None:
268225
"""Focus the chat input box."""
269226
details_panel = self.query_one("#details-panel", RunDetailsPanel)
270227
details_panel.switch_tab("chat-tab")
271228
chat_input = details_panel.query_one("#chat-input", Input)
272229
chat_input.focus()
273230

274-
def _add_run_in_history(self, run: ExecutionRun):
275-
"""Add run to history panel."""
276-
history_panel = self.query_one("#history-panel", RunHistoryPanel)
277-
history_panel.add_run(run)
278-
279-
def _update_run_in_history(self, run: ExecutionRun):
280-
"""Update run display in history panel."""
281-
history_panel = self.query_one("#history-panel", RunHistoryPanel)
282-
history_panel.update_run(run)
283-
284-
def _update_run_details(self, run: ExecutionRun):
285-
"""Update the displayed run information."""
286-
details_panel = self.query_one("#details-panel", RunDetailsPanel)
287-
details_panel.update_run_details(run)
288-
289-
def _handle_trace_message(self, trace_msg: TraceMessage):
290-
"""Handle trace message from exporter."""
291-
run = self.runs[trace_msg.run_id]
292-
for i, existing_trace in enumerate(run.traces):
293-
if existing_trace.span_id == trace_msg.span_id:
294-
run.traces[i] = trace_msg
295-
break
296-
else:
297-
run.traces.append(trace_msg)
298-
299-
details_panel = self.query_one("#details-panel", RunDetailsPanel)
300-
details_panel.add_trace(trace_msg)
301-
302-
def _handle_log_message(self, log_msg: LogMessage):
303-
"""Handle log message from exporter."""
304-
self.runs[log_msg.run_id].logs.append(log_msg)
305-
details_panel = self.query_one("#details-panel", RunDetailsPanel)
306-
details_panel.add_log(log_msg)
307-
308-
def _add_info_log(self, run: ExecutionRun, message: str):
309-
"""Add info log to run."""
310-
timestamp = datetime.now()
311-
log_msg = LogMessage(run.id, "INFO", message, timestamp)
312-
self._handle_log_message(log_msg)
313-
314-
def _add_error_log(self, run: ExecutionRun):
315-
"""Add error log to run."""
316-
timestamp = datetime.now()
317-
tb = Traceback(
318-
show_locals=False,
319-
max_frames=4,
320-
)
321-
log_msg = LogMessage(run.id, "ERROR", tb, timestamp)
322-
self._handle_log_message(log_msg)
323-
324231
def _add_subprocess_log(self, level: str, message: str) -> None:
325232
"""Handle a stderr line coming from subprocesses."""
326233

@@ -329,6 +236,7 @@ def add_log() -> None:
329236
run = getattr(details_panel, "current_run", None)
330237
if run:
331238
log_msg = LogMessage(run.id, level, message, datetime.now())
332-
self._handle_log_message(log_msg)
239+
# Route through RunService so state + UI stay in sync
240+
self.run_service.handle_log(log_msg)
333241

334242
self.call_from_thread(add_log)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""UiPath Developer Console services module."""
2+
3+
from uipath.dev.services.run_service import RunService
4+
5+
__all__ = [
6+
"RunService",
7+
]

0 commit comments

Comments
 (0)