Skip to content

Commit a121c71

Browse files
committed
feat: dev terminal
1 parent 649d6a9 commit a121c71

File tree

15 files changed

+2030
-468
lines changed

15 files changed

+2030
-468
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.1.27"
3+
version = "2.1.28"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.10"
@@ -17,6 +17,7 @@ dependencies = [
1717
"rich>=13.0.0",
1818
"azure-monitor-opentelemetry>=1.6.8",
1919
"truststore>=0.10.1",
20+
"textual>=5.3.0",
2021
]
2122
classifiers = [
2223
"Development Status :: 3 - Alpha",
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import asyncio
2+
import json
3+
from datetime import datetime
4+
from os import environ as env
5+
from pathlib import Path
6+
from typing import Any, Dict
7+
from uuid import uuid4
8+
9+
from textual.app import App, ComposeResult
10+
from textual.binding import Binding
11+
from textual.containers import Container, Horizontal
12+
from textual.widgets import Button, ListView
13+
14+
from ..._runtime._contracts import (
15+
UiPathRuntimeContext,
16+
UiPathRuntimeFactory,
17+
)
18+
from ._components._details import RunDetailsPanel
19+
from ._components._history import RunHistoryPanel
20+
from ._components._new import NewRunPanel
21+
from ._models._execution import ExecutionRun
22+
from ._models._messages import LogMessage, TraceMessage
23+
from ._traces._exporter import RunContextExporter
24+
from ._traces._logger import RunContextLogHandler
25+
26+
27+
class UiPathDevTerminal(App[Any]):
28+
"""UiPath development terminal interface."""
29+
30+
CSS_PATH = Path(__file__).parent / "_styles" / "terminal.tcss"
31+
32+
BINDINGS = [
33+
Binding("q", "quit", "Quit"),
34+
Binding("n", "new_run", "New Run"),
35+
Binding("r", "execute_run", "Execute"),
36+
Binding("c", "clear_history", "Clear History"),
37+
Binding("escape", "cancel", "Cancel"),
38+
]
39+
40+
def __init__(
41+
self,
42+
runtime_factory: UiPathRuntimeFactory[Any, Any],
43+
**kwargs,
44+
):
45+
super().__init__(**kwargs)
46+
47+
self.initial_entrypoint: str = "main.py"
48+
self.initial_input: str = '{\n "message": "Hello World"\n}'
49+
self.runs: Dict[str, ExecutionRun] = {}
50+
self.runtime_factory = runtime_factory
51+
self.runtime_factory.add_span_exporter(
52+
RunContextExporter(
53+
on_trace=self._handle_trace_message,
54+
on_log=self._handle_log_message,
55+
),
56+
batch=False,
57+
)
58+
59+
def compose(self) -> ComposeResult:
60+
with Horizontal():
61+
# Left sidebar - run history
62+
with Container(classes="run-history"):
63+
yield RunHistoryPanel(id="history-panel")
64+
65+
# Main content area
66+
with Container(classes="main-content"):
67+
# New run panel (initially visible)
68+
yield NewRunPanel(
69+
id="new-run-panel",
70+
classes="new-run-panel",
71+
)
72+
73+
# Run details panel (initially hidden)
74+
yield RunDetailsPanel(id="details-panel", classes="hidden")
75+
76+
async def on_button_pressed(self, event: Button.Pressed) -> None:
77+
"""Handle button press events."""
78+
if event.button.id == "new-run-btn":
79+
await self.action_new_run()
80+
elif event.button.id == "execute-btn":
81+
await self.action_execute_run()
82+
elif event.button.id == "cancel-btn":
83+
await self.action_cancel()
84+
85+
async def on_list_view_selected(self, event: ListView.Selected) -> None:
86+
"""Handle run selection from history."""
87+
if event.list_view.id == "run-list" and event.item:
88+
run_id = getattr(event.item, "run_id", None)
89+
if run_id:
90+
history_panel = self.query_one("#history-panel", RunHistoryPanel)
91+
run = history_panel.get_run_by_id(run_id)
92+
if run:
93+
self._show_run_details(run)
94+
95+
async def action_new_run(self) -> None:
96+
"""Show new run panel."""
97+
new_panel = self.query_one("#new-run-panel")
98+
details_panel = self.query_one("#details-panel")
99+
100+
new_panel.remove_class("hidden")
101+
details_panel.add_class("hidden")
102+
103+
async def action_cancel(self) -> None:
104+
"""Cancel and return to new run view."""
105+
await self.action_new_run()
106+
107+
async def action_execute_run(self) -> None:
108+
"""Execute a new run with UiPath runtime."""
109+
new_run_panel = self.query_one("#new-run-panel", NewRunPanel)
110+
entrypoint, input_data = new_run_panel.get_input_values()
111+
112+
if not entrypoint:
113+
return
114+
115+
try:
116+
json.loads(input_data)
117+
except json.JSONDecodeError:
118+
return
119+
120+
run = ExecutionRun(entrypoint, input_data)
121+
self.runs[run.id] = run
122+
123+
self._add_run_in_history(run)
124+
125+
self._show_run_details(run)
126+
127+
asyncio.create_task(self._execute_runtime(run))
128+
129+
async def action_clear_history(self) -> None:
130+
"""Clear run history."""
131+
history_panel = self.query_one("#history-panel", RunHistoryPanel)
132+
history_panel.clear_runs()
133+
await self.action_new_run()
134+
135+
async def _execute_runtime(self, run: ExecutionRun):
136+
"""Execute the script using UiPath runtime."""
137+
try:
138+
context: UiPathRuntimeContext = self.runtime_factory.new_context(
139+
entrypoint=run.entrypoint,
140+
input=run.input_data,
141+
trace_id=str(uuid4()),
142+
execution_id=run.id,
143+
logs_min_level=env.get("LOG_LEVEL", "INFO"),
144+
log_handler=RunContextLogHandler(
145+
run_id=run.id, on_log=self._handle_log_message
146+
),
147+
)
148+
149+
self._add_info_log(run, f"Starting execution: {run.entrypoint}")
150+
151+
result = await self.runtime_factory.execute_in_root_span(context)
152+
153+
if result is not None:
154+
run.output_data = json.dumps(result.output)
155+
if run.output_data:
156+
self._add_info_log(run, f"Execution result: {run.output_data}")
157+
158+
self._add_info_log(run, "✅ Execution completed successfully")
159+
run.status = "completed"
160+
run.end_time = datetime.now()
161+
162+
except Exception as e:
163+
error_msg = f"Execution failed: {str(e)}"
164+
self._add_error_log(run, error_msg)
165+
run.status = "failed"
166+
run.end_time = datetime.now()
167+
168+
self._update_run_in_history(run)
169+
self._update_run_details(run)
170+
171+
def _show_run_details(self, run: ExecutionRun):
172+
"""Show details panel for a specific run."""
173+
# Hide new run panel, show details panel
174+
new_panel = self.query_one("#new-run-panel")
175+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
176+
177+
new_panel.add_class("hidden")
178+
details_panel.remove_class("hidden")
179+
180+
# Populate the details panel with run data
181+
details_panel.update_run(run)
182+
183+
def _add_run_in_history(self, run: ExecutionRun):
184+
"""Add run to history panel."""
185+
history_panel = self.query_one("#history-panel", RunHistoryPanel)
186+
history_panel.add_run(run)
187+
188+
def _update_run_in_history(self, run: ExecutionRun):
189+
"""Update run display in history panel."""
190+
history_panel = self.query_one("#history-panel", RunHistoryPanel)
191+
history_panel.update_run(run)
192+
193+
def _update_run_details(self, run: ExecutionRun):
194+
"""Update the displayed run information."""
195+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
196+
details_panel.update_run_details(run)
197+
198+
def _handle_trace_message(self, trace_msg: TraceMessage):
199+
"""Handle trace message from exporter."""
200+
run = self.runs[trace_msg.run_id]
201+
for i, existing_trace in enumerate(run.traces):
202+
if existing_trace.span_id == trace_msg.span_id:
203+
run.traces[i] = trace_msg
204+
break
205+
else:
206+
run.traces.append(trace_msg)
207+
208+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
209+
details_panel.add_trace(trace_msg)
210+
211+
def _handle_log_message(self, log_msg: LogMessage):
212+
"""Handle log message from exporter."""
213+
self.runs[log_msg.run_id].logs.append(log_msg)
214+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
215+
details_panel.add_log(log_msg)
216+
217+
def _add_info_log(self, run: ExecutionRun, message: str):
218+
"""Add info log to run."""
219+
timestamp = datetime.now()
220+
log_msg = LogMessage(run.id, "INFO", message, timestamp)
221+
self._handle_log_message(log_msg)
222+
223+
def _add_error_log(self, run: ExecutionRun, message: str):
224+
"""Add error log to run."""
225+
timestamp = datetime.now()
226+
log_msg = LogMessage(run.id, "ERROR", message, timestamp)
227+
self._handle_log_message(log_msg)

0 commit comments

Comments
 (0)