uipath 2.1.26__py3-none-any.whl → 2.1.28__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
uipath/_cli/__init__.py CHANGED
@@ -5,6 +5,7 @@ import click
5
5
 
6
6
  from .cli_auth import auth as auth
7
7
  from .cli_deploy import deploy as deploy # type: ignore
8
+ from .cli_dev import dev as dev
8
9
  from .cli_eval import eval as eval # type: ignore
9
10
  from .cli_init import init as init # type: ignore
10
11
  from .cli_invoke import invoke as invoke # type: ignore
@@ -69,3 +70,4 @@ cli.add_command(invoke)
69
70
  cli.add_command(push)
70
71
  cli.add_command(pull)
71
72
  cli.add_command(eval)
73
+ cli.add_command(dev)
@@ -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,0 +1,421 @@
1
+ from typing import Dict, List, Optional
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.containers import Container, Horizontal, Vertical
5
+ from textual.reactive import reactive
6
+ from textual.widgets import RichLog, TabbedContent, TabPane, Tree
7
+ from textual.widgets.tree import TreeNode
8
+
9
+ from .._models._execution import ExecutionRun
10
+ from .._models._messages import LogMessage, TraceMessage
11
+
12
+
13
+ class SpanDetailsDisplay(Container):
14
+ """Widget to display details of a selected span."""
15
+
16
+ def compose(self) -> ComposeResult:
17
+ yield RichLog(
18
+ id="span-details",
19
+ max_lines=1000,
20
+ highlight=True,
21
+ markup=True,
22
+ classes="detail-log",
23
+ )
24
+
25
+ def show_span_details(self, trace_msg: TraceMessage):
26
+ """Display detailed information about a trace span."""
27
+ details_log = self.query_one("#span-details", RichLog)
28
+ details_log.clear()
29
+
30
+ # Format span details
31
+ details_log.write(f"[bold cyan]Span: {trace_msg.span_name}[/bold cyan]")
32
+ details_log.write(f"[dim]Trace ID: {trace_msg.trace_id}[/dim]")
33
+ details_log.write(f"[dim]Span ID: {trace_msg.span_id}[/dim]")
34
+ details_log.write(f"[dim]Run ID: {trace_msg.run_id}[/dim]")
35
+
36
+ if trace_msg.parent_span_id:
37
+ details_log.write(f"[dim]Parent Span: {trace_msg.parent_span_id}[/dim]")
38
+
39
+ details_log.write("") # Empty line
40
+
41
+ # Status with color
42
+ color_map = {
43
+ "started": "blue",
44
+ "running": "yellow",
45
+ "completed": "green",
46
+ "failed": "red",
47
+ "error": "red",
48
+ }
49
+ color = color_map.get(trace_msg.status.lower(), "white")
50
+ details_log.write(f"Status: [{color}]{trace_msg.status.upper()}[/{color}]")
51
+
52
+ # Timestamps
53
+ details_log.write(
54
+ f"Started: [dim]{trace_msg.timestamp.strftime('%H:%M:%S.%f')[:-3]}[/dim]"
55
+ )
56
+
57
+ if trace_msg.duration_ms is not None:
58
+ details_log.write(
59
+ f"Duration: [yellow]{trace_msg.duration_ms:.2f}ms[/yellow]"
60
+ )
61
+
62
+ # Additional attributes if available
63
+ if trace_msg.attributes:
64
+ details_log.write("")
65
+ details_log.write("[bold]Attributes:[/bold]")
66
+ for key, value in trace_msg.attributes.items():
67
+ details_log.write(f" {key}: {value}")
68
+
69
+
70
+ class RunDetailsPanel(Container):
71
+ """Panel showing traces and logs for selected run with tabbed interface."""
72
+
73
+ current_run: reactive[Optional[ExecutionRun]] = reactive(None)
74
+
75
+ def __init__(self, **kwargs):
76
+ super().__init__(**kwargs)
77
+ self.span_tree_nodes = {} # Map span_id to tree nodes
78
+ self.current_run = None # Store reference to current run
79
+
80
+ def compose(self) -> ComposeResult:
81
+ with TabbedContent():
82
+ # Run details tab
83
+ with TabPane("Details", id="run-tab"):
84
+ yield RichLog(
85
+ id="run-details-log",
86
+ max_lines=1000,
87
+ highlight=True,
88
+ markup=True,
89
+ classes="detail-log",
90
+ )
91
+
92
+ # Traces tab
93
+ with TabPane("Traces", id="traces-tab"):
94
+ with Horizontal(classes="traces-content"):
95
+ # Left side - Span tree
96
+ with Vertical(
97
+ classes="spans-tree-section", id="spans-tree-container"
98
+ ):
99
+ yield Tree("Trace", id="spans-tree", classes="spans-tree")
100
+
101
+ # Right side - Span details
102
+ with Vertical(classes="span-details-section"):
103
+ yield SpanDetailsDisplay(id="span-details-display")
104
+
105
+ # Logs tab
106
+ with TabPane("Logs", id="logs-tab"):
107
+ yield RichLog(
108
+ id="logs-log",
109
+ max_lines=1000,
110
+ highlight=True,
111
+ markup=True,
112
+ classes="detail-log",
113
+ )
114
+
115
+ def watch_current_run(
116
+ self, old_value: Optional[ExecutionRun], new_value: Optional[ExecutionRun]
117
+ ):
118
+ """Watch for changes to the current run."""
119
+ if new_value is not None:
120
+ if old_value != new_value:
121
+ self.current_run = new_value
122
+ self.show_run(new_value)
123
+
124
+ def update_run(self, run: ExecutionRun):
125
+ """Update the displayed run information."""
126
+ self.current_run = run
127
+
128
+ def show_run(self, run: ExecutionRun):
129
+ """Display traces and logs for a specific run."""
130
+ # Populate run details tab
131
+ self._show_run_details(run)
132
+
133
+ # Populate logs - convert string logs to display format
134
+ logs_log = self.query_one("#logs-log", RichLog)
135
+ logs_log.clear()
136
+ for log in run.logs:
137
+ self.add_log(log)
138
+
139
+ # Clear and rebuild traces tree using TraceMessage objects
140
+ self._rebuild_spans_tree()
141
+
142
+ def _show_run_details(self, run: ExecutionRun):
143
+ """Display detailed information about the run in the Details tab."""
144
+ run_details_log = self.query_one("#run-details-log", RichLog)
145
+ run_details_log.clear()
146
+
147
+ # Run header
148
+ run_details_log.write(f"[bold cyan]Run ID: {run.id}[/bold cyan]")
149
+ run_details_log.write("")
150
+
151
+ # Run status with color
152
+ status_color_map = {
153
+ "started": "blue",
154
+ "running": "yellow",
155
+ "completed": "green",
156
+ "failed": "red",
157
+ "error": "red",
158
+ }
159
+ status = getattr(run, "status", "unknown")
160
+ color = status_color_map.get(status.lower(), "white")
161
+ run_details_log.write(
162
+ f"[bold]Status:[/bold] [{color}]{status.upper()}[/{color}]"
163
+ )
164
+
165
+ # Timestamps
166
+ if hasattr(run, "start_time") and run.start_time:
167
+ run_details_log.write(
168
+ f"[bold]Started:[/bold] [dim]{run.start_time.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}[/dim]"
169
+ )
170
+
171
+ if hasattr(run, "end_time") and run.end_time:
172
+ run_details_log.write(
173
+ f"[bold]Ended:[/bold] [dim]{run.end_time.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}[/dim]"
174
+ )
175
+
176
+ # Duration
177
+ if hasattr(run, "duration_ms") and run.duration_ms is not None:
178
+ run_details_log.write(
179
+ f"[bold]Duration:[/bold] [yellow]{run.duration_ms:.2f}ms[/yellow]"
180
+ )
181
+ elif (
182
+ hasattr(run, "start_time")
183
+ and hasattr(run, "end_time")
184
+ and run.start_time
185
+ and run.end_time
186
+ ):
187
+ duration = (run.end_time - run.start_time).total_seconds() * 1000
188
+ run_details_log.write(
189
+ f"[bold]Duration:[/bold] [yellow]{duration:.2f}ms[/yellow]"
190
+ )
191
+
192
+ run_details_log.write("")
193
+
194
+ # Input section
195
+ if hasattr(run, "input_data") and run.input_data is not None:
196
+ run_details_log.write("[bold green]INPUT:[/bold green]")
197
+ run_details_log.write("[dim]" + "=" * 50 + "[/dim]")
198
+
199
+ # Handle different input types
200
+ if isinstance(run.input_data, str):
201
+ run_details_log.write(run.input_data)
202
+ elif isinstance(run.input_data, dict):
203
+ import json
204
+
205
+ run_details_log.write(json.dumps(run.input_data, indent=2))
206
+ else:
207
+ run_details_log.write(str(run.input_data))
208
+
209
+ run_details_log.write("")
210
+
211
+ # Output section
212
+ if hasattr(run, "output_data") and run.output_data is not None:
213
+ run_details_log.write("[bold magenta]OUTPUT:[/bold magenta]")
214
+ run_details_log.write("[dim]" + "=" * 50 + "[/dim]")
215
+
216
+ # Handle different output types
217
+ if isinstance(run.output_data, str):
218
+ run_details_log.write(run.output_data)
219
+ elif isinstance(run.output_data, dict):
220
+ import json
221
+
222
+ run_details_log.write(json.dumps(run.output_data, indent=2))
223
+ else:
224
+ run_details_log.write(str(run.output_data))
225
+
226
+ run_details_log.write("")
227
+
228
+ # Error section (if applicable)
229
+ if hasattr(run, "error") and run.error:
230
+ run_details_log.write("[bold red]ERROR:[/bold red]")
231
+ run_details_log.write("[dim]" + "=" * 50 + "[/dim]")
232
+ run_details_log.write(f"[red]{run.error}[/red]")
233
+ run_details_log.write("")
234
+
235
+ # Additional metadata
236
+ run_details_log.write("[bold]METADATA:[/bold]")
237
+ run_details_log.write("[dim]" + "=" * 50 + "[/dim]")
238
+
239
+ # Show available attributes
240
+ for attr in ["id", "status", "start_time", "end_time", "duration_ms"]:
241
+ if hasattr(run, attr):
242
+ value = getattr(run, attr)
243
+ if value is not None:
244
+ run_details_log.write(f" {attr}: {value}")
245
+
246
+ # Show traces count
247
+ traces_count = len(run.traces) if run.traces else 0
248
+ run_details_log.write(f" traces_count: {traces_count}")
249
+
250
+ # Show logs count
251
+ logs_count = len(run.logs) if run.logs else 0
252
+ run_details_log.write(f" logs_count: {logs_count}")
253
+
254
+ def _rebuild_spans_tree(self):
255
+ """Rebuild the spans tree from current run's traces."""
256
+ spans_tree = self.query_one("#spans-tree", Tree)
257
+ spans_tree.clear()
258
+ # Only clear the node mapping since we're rebuilding the tree structure
259
+ self.span_tree_nodes.clear()
260
+
261
+ if not self.current_run or not self.current_run.traces:
262
+ return
263
+
264
+ # Build spans tree from TraceMessage objects
265
+ self._build_spans_tree(self.current_run.traces)
266
+
267
+ # Expand the root "Trace" node
268
+ spans_tree.root.expand()
269
+
270
+ def _build_spans_tree(self, trace_messages: list[TraceMessage]):
271
+ """Build the spans tree from trace messages."""
272
+ spans_tree = self.query_one("#spans-tree", Tree)
273
+ root = spans_tree.root
274
+
275
+ # Group spans by parent relationship
276
+ root_spans = []
277
+ child_spans: Dict[str, List[TraceMessage]] = {}
278
+
279
+ for trace_msg in trace_messages:
280
+ if not trace_msg.parent_span_id:
281
+ root_spans.append(trace_msg)
282
+ else:
283
+ if trace_msg.parent_span_id not in child_spans:
284
+ child_spans[trace_msg.parent_span_id] = []
285
+ child_spans[trace_msg.parent_span_id].append(trace_msg)
286
+
287
+ # Build tree recursively
288
+ for root_span in sorted(root_spans, key=lambda x: x.timestamp):
289
+ if root_span.span_id in child_spans:
290
+ for child in sorted(
291
+ child_spans[root_span.span_id], key=lambda x: x.timestamp
292
+ ):
293
+ self._add_span_node(root, child, child_spans)
294
+
295
+ def _add_span_node(
296
+ self,
297
+ parent_node: TreeNode[str],
298
+ trace_msg: TraceMessage,
299
+ child_spans: Dict[str, List[TraceMessage]],
300
+ ):
301
+ """Recursively add span nodes to the tree."""
302
+ # Create display label for the span
303
+ color_map = {
304
+ "started": "🔵",
305
+ "running": "🟡",
306
+ "completed": "🟢",
307
+ "failed": "🔴",
308
+ "error": "🔴",
309
+ }
310
+ status_icon = color_map.get(trace_msg.status.lower(), "⚪")
311
+
312
+ duration_str = (
313
+ f" ({trace_msg.duration_ms:.1f}ms)" if trace_msg.duration_ms else ""
314
+ )
315
+ label = f"{status_icon} {trace_msg.span_name}{duration_str}"
316
+
317
+ # Add node to tree
318
+ node = parent_node.add(label)
319
+ node.data = trace_msg.span_id # Store span_id for reference
320
+ self.span_tree_nodes[trace_msg.span_id] = node
321
+
322
+ node.expand()
323
+
324
+ # Add child spans (sorted by timestamp)
325
+ if trace_msg.span_id in child_spans:
326
+ sorted_children = sorted(
327
+ child_spans[trace_msg.span_id], key=lambda x: x.timestamp
328
+ )
329
+ for child_span in sorted_children:
330
+ self._add_span_node(node, child_span, child_spans)
331
+
332
+ def on_tree_node_selected(self, event: Tree.NodeSelected[str]) -> None:
333
+ """Handle span selection in the tree."""
334
+ # Check if this is our spans tree
335
+ spans_tree = self.query_one("#spans-tree", Tree)
336
+ if event.control != spans_tree:
337
+ return
338
+
339
+ # Get the selected span data
340
+ if hasattr(event.node, "data") and event.node.data:
341
+ span_id = event.node.data
342
+ # Find the trace in current_run.traces
343
+ trace_msg = None
344
+ if self.current_run:
345
+ for trace in self.current_run.traces:
346
+ if trace.span_id == span_id:
347
+ trace_msg = trace
348
+ break
349
+
350
+ if trace_msg:
351
+ # Show span details
352
+ span_details_display = self.query_one(
353
+ "#span-details-display", SpanDetailsDisplay
354
+ )
355
+ span_details_display.show_span_details(trace_msg)
356
+
357
+ def update_run_details(self, run: ExecutionRun):
358
+ if not self.current_run or run.id != self.current_run.id:
359
+ return
360
+
361
+ self._show_run_details(run)
362
+
363
+ def add_trace(self, trace_msg: TraceMessage):
364
+ """Add trace to current run if it matches."""
365
+ if not self.current_run or trace_msg.run_id != self.current_run.id:
366
+ return
367
+
368
+ # Rebuild the tree to include new trace
369
+ self._rebuild_spans_tree()
370
+
371
+ def add_log(self, log_msg: LogMessage):
372
+ """Add log to current run if it matches."""
373
+ if not self.current_run or log_msg.run_id != self.current_run.id:
374
+ return
375
+
376
+ color_map = {
377
+ "DEBUG": "dim cyan",
378
+ "INFO": "blue",
379
+ "WARN": "yellow",
380
+ "WARNING": "yellow",
381
+ "ERROR": "red",
382
+ "CRITICAL": "bold red",
383
+ }
384
+
385
+ color = color_map.get(log_msg.level.upper(), "white")
386
+ timestamp_str = log_msg.timestamp.strftime("%H:%M:%S")
387
+ level_short = log_msg.level[:4].upper()
388
+
389
+ log_text = (
390
+ f"[dim]{timestamp_str}[/dim] "
391
+ f"[{color}]{level_short}[/{color}] "
392
+ f"{log_msg.message}"
393
+ )
394
+
395
+ logs_log = self.query_one("#logs-log", RichLog)
396
+ logs_log.write(log_text)
397
+
398
+ def clear_display(self):
399
+ """Clear both traces and logs display."""
400
+ run_details_log = self.query_one("#run-details-log", RichLog)
401
+ logs_log = self.query_one("#logs-log", RichLog)
402
+ spans_tree = self.query_one("#spans-tree", Tree)
403
+
404
+ run_details_log.clear()
405
+ logs_log.clear()
406
+ spans_tree.clear()
407
+
408
+ self.current_run = None
409
+ self.span_tree_nodes.clear()
410
+
411
+ # Clear span details
412
+ span_details_display = self.query_one(
413
+ "#span-details-display", SpanDetailsDisplay
414
+ )
415
+ span_details_log = span_details_display.query_one("#span-details", RichLog)
416
+ span_details_log.clear()
417
+
418
+ def refresh_display(self):
419
+ """Refresh the display with current run data."""
420
+ if self.current_run:
421
+ self.show_run(self.current_run)