Skip to content

Commit c2dc793

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

13 files changed

Lines changed: 2051 additions & 458 deletions

File tree

main.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from dataclasses import dataclass
2+
from typing import Optional
3+
4+
from opentelemetry import trace
5+
from opentelemetry.trace import StatusCode
6+
7+
tracer = trace.get_tracer("uipath-dev-terminal")
8+
9+
10+
@dataclass
11+
class EchoIn:
12+
message: str
13+
repeat: Optional[int] = 1
14+
prefix: Optional[str] = None
15+
16+
17+
@dataclass
18+
class EchoOut:
19+
message: str
20+
21+
22+
def main(input: EchoIn) -> EchoOut:
23+
result = []
24+
print("starting echo function")
25+
with tracer.start_as_current_span("my-operation") as span:
26+
span.set_attribute("key", "value")
27+
span.add_event("Something happened")
28+
29+
try:
30+
for _ in range(input.repeat or 1):
31+
with tracer.start_as_current_span("inner-operation") as inner_span:
32+
inner_span.set_attribute("key2", "value2")
33+
line = input.message
34+
if input.prefix:
35+
line = f"{input.prefix}: {line}"
36+
result.append(line)
37+
span.set_status(StatusCode.OK)
38+
except Exception as e:
39+
span.set_status(StatusCode.ERROR, str(e))
40+
span.record_exception(e)
41+
raise
42+
43+
print("after echo function")
44+
return EchoOut(message="\n".join(result))

pyproject.toml

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

0 commit comments

Comments
 (0)