Skip to content

Commit ded33e4

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

File tree

14 files changed

+2002
-461
lines changed

14 files changed

+2002
-461
lines changed

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: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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+
)
57+
58+
def compose(self) -> ComposeResult:
59+
with Horizontal():
60+
# Left sidebar - run history
61+
with Container(classes="run-history"):
62+
yield RunHistoryPanel(id="history-panel")
63+
64+
# Main content area
65+
with Container(classes="main-content"):
66+
# New run panel (initially visible)
67+
yield NewRunPanel(
68+
id="new-run-panel",
69+
classes="new-run-panel",
70+
)
71+
72+
# Run details panel (initially hidden)
73+
yield RunDetailsPanel(id="details-panel", classes="hidden")
74+
75+
async def on_button_pressed(self, event: Button.Pressed) -> None:
76+
"""Handle button press events."""
77+
if event.button.id == "new-run-btn":
78+
await self.action_new_run()
79+
elif event.button.id == "execute-btn":
80+
await self.action_execute_run()
81+
elif event.button.id == "cancel-btn":
82+
await self.action_cancel()
83+
84+
async def on_list_view_selected(self, event: ListView.Selected) -> None:
85+
"""Handle run selection from history."""
86+
if event.list_view.id == "run-list" and event.item:
87+
run_id = getattr(event.item, "run_id", None)
88+
if run_id:
89+
history_panel = self.query_one("#history-panel", RunHistoryPanel)
90+
run = history_panel.get_run_by_id(run_id)
91+
if run:
92+
self._show_run_details(run)
93+
94+
async def action_new_run(self) -> None:
95+
"""Show new run panel."""
96+
new_panel = self.query_one("#new-run-panel")
97+
details_panel = self.query_one("#details-panel")
98+
99+
new_panel.remove_class("hidden")
100+
details_panel.add_class("hidden")
101+
102+
async def action_cancel(self) -> None:
103+
"""Cancel and return to new run view."""
104+
await self.action_new_run()
105+
106+
async def action_execute_run(self) -> None:
107+
"""Execute a new run with UiPath runtime."""
108+
new_run_panel = self.query_one("#new-run-panel", NewRunPanel)
109+
entrypoint, input_data = new_run_panel.get_input_values()
110+
111+
if not entrypoint:
112+
return
113+
114+
try:
115+
json.loads(input_data)
116+
except json.JSONDecodeError:
117+
return
118+
119+
run = ExecutionRun(entrypoint, input_data)
120+
self.runs[run.id] = run
121+
122+
self._add_run_in_history(run)
123+
124+
self._show_run_details(run)
125+
126+
asyncio.create_task(self._execute_runtime(run))
127+
128+
async def action_clear_history(self) -> None:
129+
"""Clear run history."""
130+
history_panel = self.query_one("#history-panel", RunHistoryPanel)
131+
history_panel.clear_runs()
132+
await self.action_new_run()
133+
134+
async def _execute_runtime(self, run: ExecutionRun):
135+
"""Execute the script using UiPath runtime."""
136+
try:
137+
context: UiPathRuntimeContext = self.runtime_factory.new_context(
138+
entrypoint=run.entrypoint,
139+
input=run.input_data,
140+
trace_id=str(uuid4()),
141+
execution_id=run.id,
142+
logs_min_level=env.get("LOG_LEVEL", "INFO"),
143+
log_handler=RunContextLogHandler(
144+
run_id=run.id, on_log=self._handle_log_message
145+
),
146+
)
147+
148+
self._add_info_log(run, f"Starting execution: {run.entrypoint}")
149+
150+
result = await self.runtime_factory.execute_in_root_span(context)
151+
152+
if result is not None:
153+
run.output_data = json.dumps(result.output)
154+
if run.output_data:
155+
self._add_info_log(run, f"Execution result: {run.output_data}")
156+
157+
self._add_info_log(run, "✅ Execution completed successfully")
158+
run.status = "completed"
159+
run.end_time = datetime.now()
160+
161+
except Exception as e:
162+
error_msg = f"Execution failed: {str(e)}"
163+
self._add_error_log(run, error_msg)
164+
run.status = "failed"
165+
run.end_time = datetime.now()
166+
167+
self._update_run_in_history(run)
168+
self._update_run_details(run)
169+
170+
def _show_run_details(self, run: ExecutionRun):
171+
"""Show details panel for a specific run."""
172+
# Hide new run panel, show details panel
173+
new_panel = self.query_one("#new-run-panel")
174+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
175+
176+
new_panel.add_class("hidden")
177+
details_panel.remove_class("hidden")
178+
179+
# Populate the details panel with run data
180+
details_panel.update_run(run)
181+
182+
def _add_run_in_history(self, run: ExecutionRun):
183+
"""Add run to history panel."""
184+
history_panel = self.query_one("#history-panel", RunHistoryPanel)
185+
history_panel.add_run(run)
186+
187+
def _update_run_in_history(self, run: ExecutionRun):
188+
"""Update run display in history panel."""
189+
history_panel = self.query_one("#history-panel", RunHistoryPanel)
190+
history_panel.update_run(run)
191+
192+
def _update_run_details(self, run: ExecutionRun):
193+
"""Update the displayed run information."""
194+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
195+
details_panel.update_run_details(run)
196+
197+
def _handle_trace_message(self, trace_msg: TraceMessage):
198+
"""Handle trace message from exporter."""
199+
run = self.runs[trace_msg.run_id]
200+
for i, existing_trace in enumerate(run.traces):
201+
if existing_trace.span_id == trace_msg.span_id:
202+
run.traces[i] = trace_msg
203+
break
204+
else:
205+
run.traces.append(trace_msg)
206+
207+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
208+
details_panel.add_trace(trace_msg)
209+
210+
def _handle_log_message(self, log_msg: LogMessage):
211+
"""Handle log message from exporter."""
212+
self.runs[log_msg.run_id].logs.append(log_msg)
213+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
214+
details_panel.add_log(log_msg)
215+
216+
def _add_info_log(self, run: ExecutionRun, message: str):
217+
"""Add info log to run."""
218+
timestamp = datetime.now()
219+
log_msg = LogMessage(run.id, "INFO", message, timestamp)
220+
self._handle_log_message(log_msg)
221+
222+
def _add_error_log(self, run: ExecutionRun, message: str):
223+
"""Add error log to run."""
224+
timestamp = datetime.now()
225+
log_msg = LogMessage(run.id, "ERROR", message, timestamp)
226+
self._handle_log_message(log_msg)

0 commit comments

Comments
 (0)