lang2sim 2.0.0__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.
Files changed (65) hide show
  1. lang2sim/__init__.py +47 -0
  2. lang2sim/agent/__init__.py +5 -0
  3. lang2sim/agent/nemotron_client.py +109 -0
  4. lang2sim/agent/react_loop.py +848 -0
  5. lang2sim/agent/tools.py +998 -0
  6. lang2sim/cli/app.py +183 -0
  7. lang2sim/cli/screens/__init__.py +8 -0
  8. lang2sim/cli/screens/command_palette_modal.py +146 -0
  9. lang2sim/cli/screens/landing_screen.py +96 -0
  10. lang2sim/cli/screens/sessions_modal.py +236 -0
  11. lang2sim/cli/screens/workspace_screen.py +709 -0
  12. lang2sim/cli/widgets/__init__.py +16 -0
  13. lang2sim/cli/widgets/action_pill.py +67 -0
  14. lang2sim/cli/widgets/command_card.py +265 -0
  15. lang2sim/cli/widgets/plan_card.py +106 -0
  16. lang2sim/cli/widgets/prompt_card.py +96 -0
  17. lang2sim/cli/widgets/telemetry_sidebar.py +117 -0
  18. lang2sim/cli/widgets/thought_box.py +64 -0
  19. lang2sim/compiler/__init__.py +15 -0
  20. lang2sim/compiler/sdf_compiler.py +65 -0
  21. lang2sim/compiler/sir_generator.py +46 -0
  22. lang2sim/compiler/sir_validator.py +87 -0
  23. lang2sim/core/__init__.py +50 -0
  24. lang2sim/core/config.py +116 -0
  25. lang2sim/core/models.py +279 -0
  26. lang2sim/core/session_manager.py +189 -0
  27. lang2sim/perturbation/__init__.py +50 -0
  28. lang2sim/perturbation/_compat.py +19 -0
  29. lang2sim/perturbation/gazebo_bridge.py +520 -0
  30. lang2sim/perturbation/mcp_server.py +129 -0
  31. lang2sim/perturbation/routers/__init__.py +19 -0
  32. lang2sim/perturbation/routers/world.py +110 -0
  33. lang2sim/perturbation/sdf_snippets.py +112 -0
  34. lang2sim/runtime/__init__.py +7 -0
  35. lang2sim/runtime/control_api/__init__.py +7 -0
  36. lang2sim/runtime/control_api/client.py +187 -0
  37. lang2sim/runtime/control_api/server.py +504 -0
  38. lang2sim/runtime/display.py +141 -0
  39. lang2sim/runtime/docker_runner.py +286 -0
  40. lang2sim/runtime/doctor.py +220 -0
  41. lang2sim/runtime/hardware_probe.py +144 -0
  42. lang2sim/runtime/health_monitor.py +66 -0
  43. lang2sim/runtime/launch_builder.py +137 -0
  44. lang2sim/runtime/nav2_dispatcher.py +215 -0
  45. lang2sim/runtime/platform.py +137 -0
  46. lang2sim/runtime/ros_bridge.py +183 -0
  47. lang2sim/runtime/run_folder.py +86 -0
  48. lang2sim/runtime/self_healing.py +387 -0
  49. lang2sim/runtime/session.py +136 -0
  50. lang2sim/runtime/world_check.py +197 -0
  51. lang2sim/telemetry/__init__.py +85 -0
  52. lang2sim/telemetry/nqi_scorer.py +645 -0
  53. lang2sim/telemetry/ros_subscribers.py +489 -0
  54. lang2sim/telemetry/trajectory.py +258 -0
  55. lang2sim/telemetry/vision.py +477 -0
  56. lang2sim/world_gen/__init__.py +16 -0
  57. lang2sim/world_gen/catalog_loader.py +83 -0
  58. lang2sim/world_gen/map_slicer.py +131 -0
  59. lang2sim/world_gen/shapely_solver.py +256 -0
  60. lang2sim-2.0.0.dist-info/METADATA +53 -0
  61. lang2sim-2.0.0.dist-info/RECORD +65 -0
  62. lang2sim-2.0.0.dist-info/WHEEL +5 -0
  63. lang2sim-2.0.0.dist-info/entry_points.txt +2 -0
  64. lang2sim-2.0.0.dist-info/licenses/LICENSE +21 -0
  65. lang2sim-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,16 @@
1
+ from .thought_box import ThoughtBox
2
+ from .action_pill import ActionPill
3
+ from .prompt_card import PromptCard
4
+ from .telemetry_sidebar import TelemetrySidebar
5
+ from .command_card import CommandCard
6
+ from .plan_card import PlanCard
7
+
8
+ __all__ = [
9
+ "ThoughtBox",
10
+ "ActionPill",
11
+ "PromptCard",
12
+ "TelemetrySidebar",
13
+ "CommandCard",
14
+ "PlanCard",
15
+ ]
16
+
@@ -0,0 +1,67 @@
1
+ """
2
+ Tool Action Status Pill Widget
3
+ File: lang2sim/cli/widgets/action_pill.py
4
+ """
5
+
6
+ from textual.widget import Widget
7
+ from textual.widgets import Static
8
+ from textual.reactive import reactive
9
+
10
+ class ActionPill(Widget):
11
+ """Tool invocation status badge pill (e.g. ■ solve_spatial_layout · 0.4s · completed)."""
12
+
13
+ DEFAULT_CSS = """
14
+ ActionPill {
15
+ height: auto;
16
+ background: #161616;
17
+ border: solid #222222;
18
+ padding: 0 1;
19
+ margin-bottom: 1;
20
+ }
21
+ """
22
+
23
+ tool_name = reactive("")
24
+ duration_str = reactive("0.0s")
25
+ status = reactive("running") # running, completed, error
26
+
27
+ def __init__(self, tool_name: str, duration_str: str = "0.4s", status: str = "completed", **kwargs):
28
+ super().__init__(classes="action-pill", **kwargs)
29
+ self.tool_name = tool_name
30
+ self.duration_str = duration_str
31
+ self.status = status
32
+
33
+ def compose(self):
34
+ yield Static(self._format_content(), id="pill-content")
35
+
36
+ def _format_content(self) -> str:
37
+ icon = "[#38bdf8]■[/]"
38
+ if self.status == "completed":
39
+ status_text = "[#2ed573]completed[/]"
40
+ elif self.status == "running":
41
+ status_text = "[#ffa502]running...[/]"
42
+ else:
43
+ status_text = "[#ff4757]failed[/]"
44
+ return f"{icon} [bold #f1f2f6]{self.tool_name}[/] · [#a4b0be]{self.duration_str}[/] · {status_text}"
45
+
46
+ def set_running(self) -> None:
47
+ self.status = "running"
48
+ try:
49
+ self.query_one("#pill-content", Static).update(self._format_content())
50
+ except Exception:
51
+ pass
52
+
53
+ def set_completed(self, duration: str = "0.5s") -> None:
54
+ self.status = "completed"
55
+ self.duration_str = duration
56
+ try:
57
+ self.query_one("#pill-content", Static).update(self._format_content())
58
+ except Exception:
59
+ pass
60
+
61
+ def set_failed(self, duration: str = "0.5s") -> None:
62
+ self.status = "error"
63
+ self.duration_str = duration
64
+ try:
65
+ self.query_one("#pill-content", Static).update(self._format_content())
66
+ except Exception:
67
+ pass
@@ -0,0 +1,265 @@
1
+ """
2
+ Interactive Terminal Command & Permission Card Widget
3
+ File: lang2sim/cli/widgets/command_card.py
4
+ """
5
+
6
+ from typing import Optional, Dict, Any
7
+ from textual.widget import Widget
8
+ from textual.widgets import Static, Button
9
+ from textual.containers import Horizontal, Vertical
10
+ from textual.message import Message
11
+ from textual.binding import Binding
12
+
13
+
14
+ class CommandCard(Widget):
15
+ """
16
+ OpenCode/Antigravity-style Interactive Terminal Command Card.
17
+ Presents proposed commands to the user, asks for approval, runs execution,
18
+ and displays exit codes, execution duration, and terminal stdout/stderr.
19
+ """
20
+
21
+ DEFAULT_CSS = """
22
+ CommandCard {
23
+ height: auto;
24
+ background: #111114;
25
+ border: solid #27272a;
26
+ border-left: thick #ffa502;
27
+ padding: 1 1;
28
+ margin-bottom: 1;
29
+ }
30
+
31
+ CommandCard.status-running {
32
+ border-left: thick #38bdf8;
33
+ }
34
+
35
+ CommandCard.status-completed {
36
+ border-left: thick #2ed573;
37
+ }
38
+
39
+ CommandCard.status-failed {
40
+ border-left: thick #ff4757;
41
+ }
42
+
43
+ CommandCard.status-rejected {
44
+ border-left: thick #747d8c;
45
+ }
46
+
47
+ .cmd-header {
48
+ height: 1;
49
+ margin-bottom: 1;
50
+ }
51
+
52
+ .cmd-title {
53
+ color: #f1f2f6;
54
+ text-style: bold;
55
+ }
56
+
57
+ .cmd-badge {
58
+ text-align: right;
59
+ }
60
+
61
+ .cmd-snippet {
62
+ background: #09090b;
63
+ border: solid #1e1e24;
64
+ color: #38bdf8;
65
+ padding: 0 1;
66
+ margin-bottom: 1;
67
+ height: auto;
68
+ }
69
+
70
+ .cmd-actions {
71
+ height: 3;
72
+ margin-top: 1;
73
+ align-horizontal: left;
74
+ }
75
+
76
+ .cmd-actions Button {
77
+ margin-right: 2;
78
+ min-width: 16;
79
+ height: 3;
80
+ }
81
+
82
+ .cmd-output {
83
+ background: #08080a;
84
+ border: solid #1c1c22;
85
+ color: #ced6e0;
86
+ padding: 0 1;
87
+ margin-top: 1;
88
+ height: auto;
89
+ max-height: 12;
90
+ overflow-y: auto;
91
+ }
92
+ """
93
+
94
+ BINDINGS = [
95
+ Binding("y", "approve", "Approve", show=False),
96
+ Binding("enter", "approve", "Approve", show=False),
97
+ Binding("a", "always_allow", "Always Allow", show=False),
98
+ Binding("n", "reject", "Reject", show=False),
99
+ Binding("escape", "reject", "Reject", show=False),
100
+ ]
101
+
102
+ class Approved(Message):
103
+ """User approved command execution."""
104
+ def __init__(self, event_id: str, always_allow: bool = False):
105
+ super().__init__()
106
+ self.event_id = event_id
107
+ self.always_allow = always_allow
108
+
109
+ class Rejected(Message):
110
+ """User rejected command execution."""
111
+ def __init__(self, event_id: str):
112
+ super().__init__()
113
+ self.event_id = event_id
114
+
115
+ def __init__(
116
+ self,
117
+ event_id: str,
118
+ command: str,
119
+ tool_name: str = "run_command",
120
+ arguments: Optional[Dict[str, Any]] = None,
121
+ permission_required: bool = True,
122
+ **kwargs,
123
+ ):
124
+ super().__init__(**kwargs)
125
+ self.event_id = event_id
126
+ self.command = command
127
+ self.tool_name = tool_name
128
+ self.arguments = arguments or {}
129
+ self.permission_required = permission_required
130
+ self.status = "awaiting_permission" if permission_required else "running"
131
+ self.exit_code: Optional[int] = None
132
+ self.duration_str: str = "0.0s"
133
+ self.stdout: str = ""
134
+ self.stderr: str = ""
135
+
136
+ def compose(self):
137
+ # Header: Title + Status Badge
138
+ with Horizontal(classes="cmd-header"):
139
+ yield Static(f"⚡ [bold #f1f2f6]Terminal Command[/] [#747d8c]({self.tool_name})[/]", classes="cmd-title")
140
+ yield Static(self._format_badge(), id="cmd-badge", classes="cmd-badge")
141
+
142
+ # Command Code Snippet
143
+ yield Static(f"[bold #38bdf8]$[/] [bold #ffffff]{self.command}[/]", classes="cmd-snippet")
144
+
145
+ # Interactive Approval Buttons
146
+ if self.status == "awaiting_permission":
147
+ with Horizontal(id="cmd-actions", classes="cmd-actions"):
148
+ yield Button("Approve & Run (y)", id=f"btn-approve-{self.event_id}", variant="success")
149
+ yield Button("Always Allow (a)", id=f"btn-always-{self.event_id}", variant="default")
150
+ yield Button("Reject (n)", id=f"btn-reject-{self.event_id}", variant="error")
151
+
152
+ # Output Terminal Container (hidden until execution completes)
153
+ yield Static("", id="cmd-output", classes="cmd-output", expand=True)
154
+
155
+ def on_mount(self) -> None:
156
+ self._update_classes()
157
+ out_widget = self.query_one("#cmd-output", Static)
158
+ out_widget.display = False
159
+ if self.status == "awaiting_permission":
160
+ try:
161
+ self.query_one(f"#btn-approve-{self.event_id}", Button).focus()
162
+ except Exception:
163
+ pass
164
+
165
+ def _format_badge(self) -> str:
166
+ if self.status == "awaiting_permission":
167
+ return "[bold #ffa502]⏳ AWAITING PERMISSION[/]"
168
+ elif self.status == "running":
169
+ return "[bold #38bdf8]● RUNNING...[/]"
170
+ elif self.status == "completed":
171
+ code = self.exit_code if self.exit_code is not None else 0
172
+ if code == 0:
173
+ return f"[bold #2ed573]✔ EXIT 0[/] [#747d8c]· {self.duration_str}[/]"
174
+ else:
175
+ return f"[bold #ff4757]✖ EXIT {code}[/] [#747d8c]· {self.duration_str}[/]"
176
+ elif self.status == "rejected":
177
+ return "[bold #747d8c]⛔ REJECTED[/]"
178
+ return f"[#747d8c]{self.status}[/]"
179
+
180
+ def _update_classes(self) -> None:
181
+ self.remove_class("status-running")
182
+ self.remove_class("status-completed")
183
+ self.remove_class("status-failed")
184
+ self.remove_class("status-rejected")
185
+
186
+ if self.status == "running":
187
+ self.add_class("status-running")
188
+ elif self.status == "completed":
189
+ if (self.exit_code or 0) == 0:
190
+ self.add_class("status-completed")
191
+ else:
192
+ self.add_class("status-failed")
193
+ elif self.status == "rejected":
194
+ self.add_class("status-rejected")
195
+
196
+ def on_button_pressed(self, event: Button.Pressed) -> None:
197
+ btn_id = event.button.id or ""
198
+ if "btn-approve" in btn_id:
199
+ self.action_approve()
200
+ elif "btn-always" in btn_id:
201
+ self.action_always_allow()
202
+ elif "btn-reject" in btn_id:
203
+ self.action_reject()
204
+
205
+ def action_approve(self) -> None:
206
+ if self.status != "awaiting_permission":
207
+ return
208
+ self.set_running()
209
+ self.post_message(self.Approved(self.event_id, always_allow=False))
210
+
211
+ def action_always_allow(self) -> None:
212
+ if self.status != "awaiting_permission":
213
+ return
214
+ self.set_running()
215
+ self.post_message(self.Approved(self.event_id, always_allow=True))
216
+
217
+ def action_reject(self) -> None:
218
+ if self.status != "awaiting_permission":
219
+ return
220
+ self.status = "rejected"
221
+ self._update_classes()
222
+ self._remove_actions_row()
223
+ self.query_one("#cmd-badge", Static).update(self._format_badge())
224
+ self.post_message(self.Rejected(self.event_id))
225
+
226
+ def _remove_actions_row(self) -> None:
227
+ try:
228
+ actions_row = self.query_one("#cmd-actions")
229
+ actions_row.remove()
230
+ except Exception:
231
+ pass
232
+
233
+ def set_running(self) -> None:
234
+ self.status = "running"
235
+ self._update_classes()
236
+ self._remove_actions_row()
237
+ try:
238
+ self.query_one("#cmd-badge", Static).update(self._format_badge())
239
+ except Exception:
240
+ pass
241
+
242
+ def set_result(self, exit_code: int, duration_str: str, stdout: str = "", stderr: str = "") -> None:
243
+ self.status = "completed"
244
+ self.exit_code = exit_code
245
+ self.duration_str = duration_str
246
+ self.stdout = stdout
247
+ self.stderr = stderr
248
+ self._update_classes()
249
+ self._remove_actions_row()
250
+
251
+ try:
252
+ self.query_one("#cmd-badge", Static).update(self._format_badge())
253
+ out_widget = self.query_one("#cmd-output", Static)
254
+ lines = []
255
+ if stdout.strip():
256
+ lines.append(f"[#a4b0be]{stdout.strip()}[/]")
257
+ if stderr.strip():
258
+ lines.append(f"[#ff6b81]{stderr.strip()}[/]")
259
+ if not lines:
260
+ lines.append("[italic #57606f](No output produced)[/]")
261
+
262
+ out_widget.update("\n".join(lines))
263
+ out_widget.display = True
264
+ except Exception:
265
+ pass
@@ -0,0 +1,106 @@
1
+ """
2
+ Stage-wise Execution Plan Card Widget
3
+ File: lang2sim/cli/widgets/plan_card.py
4
+ """
5
+
6
+ from typing import List, Dict, Any, Optional
7
+ from textual.widget import Widget
8
+ from textual.widgets import Static
9
+ from textual.containers import Horizontal, Vertical
10
+
11
+
12
+ class PlanCard(Widget):
13
+ """
14
+ Antigravity/OpenCode-style Stage-wise Execution Plan Card.
15
+ Visualizes multi-step autonomous workflows ([ ] -> [▶] -> [✔]).
16
+ """
17
+
18
+ DEFAULT_CSS = """
19
+ PlanCard {
20
+ height: auto;
21
+ background: #111114;
22
+ border: solid #27272a;
23
+ border-left: thick #a78bfa;
24
+ padding: 0 1;
25
+ margin-top: 1;
26
+ margin-bottom: 1;
27
+ }
28
+
29
+ .plan-header {
30
+ height: 1;
31
+ margin-bottom: 1;
32
+ }
33
+
34
+ .plan-title {
35
+ color: #f1f2f6;
36
+ text-style: bold;
37
+ }
38
+
39
+ .plan-counter {
40
+ text-align: right;
41
+ color: #a78bfa;
42
+ }
43
+
44
+ .plan-step {
45
+ height: auto;
46
+ padding: 0 1;
47
+ margin-bottom: 0;
48
+ }
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ steps: List[str],
54
+ title: str = "Autonomous Execution Plan",
55
+ **kwargs,
56
+ ):
57
+ super().__init__(**kwargs)
58
+ self.plan_title = title
59
+ # Each step: {"title": str, "status": "pending"|"running"|"completed"|"failed", "detail": ""}
60
+ self.steps: List[Dict[str, Any]] = [
61
+ {"title": s, "status": "pending", "detail": ""} for s in steps
62
+ ]
63
+ if self.steps:
64
+ self.steps[0]["status"] = "running"
65
+
66
+ def compose(self):
67
+ with Horizontal(classes="plan-header"):
68
+ yield Static(f"📋 [bold #f1f2f6]{self.plan_title}[/]", classes="plan-title")
69
+ yield Static(self._format_counter(), id="plan-counter", classes="plan-counter")
70
+
71
+ for i, step in enumerate(self.steps):
72
+ yield Static(self._format_step(i), id=f"step-{i}", classes="plan-step")
73
+
74
+ def _format_counter(self) -> str:
75
+ completed = sum(1 for s in self.steps if s["status"] == "completed")
76
+ total = len(self.steps)
77
+ if completed == total and total > 0:
78
+ return f"[bold #2ed573]All {total} Steps Completed ✔[/]"
79
+ return f"[#ced6e0]{completed}/{total} Completed[/]"
80
+
81
+ def _format_step(self, index: int) -> str:
82
+ step = self.steps[index]
83
+ status = step["status"]
84
+ title = step["title"]
85
+ detail = f" [#747d8c]({step['detail']})[/]" if step.get("detail") else ""
86
+
87
+ if status == "completed":
88
+ return f" [bold #2ed573]✔[/] [bold #ffffff]{title}[/]{detail}"
89
+ elif status == "running":
90
+ return f" [bold #38bdf8]▶[/] [bold #38bdf8]{title}[/]{detail}"
91
+ elif status == "failed":
92
+ return f" [bold #ff4757]✖[/] [bold #ff4757]{title}[/]{detail}"
93
+ else:
94
+ return f" [#57606f][ ][/] [#a4b0be]{title}[/]"
95
+
96
+ def update_step(self, index: int, status: str, detail: str = "") -> None:
97
+ """Updates the status and detail of a specific step."""
98
+ if 0 <= index < len(self.steps):
99
+ self.steps[index]["status"] = status
100
+ if detail:
101
+ self.steps[index]["detail"] = detail
102
+ try:
103
+ self.query_one(f"#step-{index}", Static).update(self._format_step(index))
104
+ self.query_one("#plan-counter", Static).update(self._format_counter())
105
+ except Exception:
106
+ pass
@@ -0,0 +1,96 @@
1
+ """
2
+ Input Card Widget (Floating & Sticky Docked)
3
+ File: lang2sim/cli/widgets/prompt_card.py
4
+ """
5
+
6
+ from textual.widget import Widget
7
+ from textual.widgets import Input, Static
8
+ from textual.containers import Vertical
9
+ from textual.message import Message
10
+
11
+
12
+ class PromptCard(Widget):
13
+ """Input card with cyan vertical bar and model status badge row."""
14
+
15
+ DEFAULT_CSS = """
16
+ PromptCard {
17
+ height: auto;
18
+ max-height: 5;
19
+ background: #18181b;
20
+ border-left: thick #00d2ff;
21
+ padding: 1 2;
22
+ }
23
+
24
+ PromptCard > Vertical {
25
+ height: auto;
26
+ }
27
+
28
+ PromptCard Input {
29
+ background: transparent;
30
+ border: none !important;
31
+ height: 1;
32
+ padding: 0;
33
+ margin: 0;
34
+ color: #ffffff;
35
+ }
36
+
37
+ PromptCard Input:focus {
38
+ border: none !important;
39
+ background: transparent;
40
+ }
41
+
42
+ PromptCard Input > .input--placeholder {
43
+ color: #6b7280;
44
+ }
45
+
46
+ PromptCard Input > .input--cursor {
47
+ background: #00d2ff;
48
+ color: #000000;
49
+ }
50
+
51
+ PromptCard #badge-row {
52
+ height: 1;
53
+ margin-top: 1;
54
+ }
55
+ """
56
+
57
+ class Submitted(Message):
58
+ """Dispatched when the user presses Enter in the prompt card."""
59
+
60
+ def __init__(self, value: str) -> None:
61
+ self.value = value
62
+ super().__init__()
63
+
64
+ def __init__(
65
+ self,
66
+ placeholder: str = "Ask anything.. 'Spawn 14x10m warehouse with 8 racks, send AMR to rack 5'",
67
+ card_id: str = "prompt-input",
68
+ **kwargs,
69
+ ):
70
+ super().__init__(**kwargs)
71
+ self.placeholder_text = placeholder
72
+ self.card_id = card_id
73
+
74
+ def compose(self):
75
+ with Vertical():
76
+ yield Input(placeholder=self.placeholder_text, id=self.card_id)
77
+ yield Static(
78
+ "[bold white on #2563eb] Build [/] [#4b5563]·[/] "
79
+ "[#9ca3af]Nemotron 3 Ultra 550B A55B Nvidia[/] [#4b5563]·[/] "
80
+ "[bold white on #d97706] medium [/]",
81
+ id="badge-row",
82
+ )
83
+
84
+ def on_input_submitted(self, event: Input.Submitted) -> None:
85
+ value = event.value.strip()
86
+ if value:
87
+ event.input.value = ""
88
+ self.post_message(self.Submitted(value))
89
+
90
+ def focus_input(self) -> None:
91
+ try:
92
+ inp = self.query_one(Input)
93
+ inp.focus()
94
+ except Exception:
95
+ pass
96
+
@@ -0,0 +1,117 @@
1
+ """
2
+ Telemetry Sidebar Widget (20% Workspace Right Pane)
3
+ File: lang2sim/cli/widgets/telemetry_sidebar.py
4
+ """
5
+
6
+ from textual.widget import Widget
7
+ from textual.widgets import Static
8
+ from textual.containers import Vertical
9
+ from textual import work
10
+ from dataclasses import dataclass
11
+
12
+ @dataclass
13
+ class TelemetryData:
14
+ sim_state: str = "Idle (Offline)"
15
+ sim_time: str = "00:00.0"
16
+ rtf: str = "0.0x"
17
+ robot_x: float = 0.0
18
+ robot_y: float = 0.0
19
+ speed: float = 0.0
20
+ clearance: float = 0.0
21
+ bumper_force: float = 0.0
22
+ collision_count: int = 0
23
+ ate_drift_m: float = 0.0
24
+ tokens_used: int = 0
25
+ context_capacity_pct: int = 0
26
+
27
+ class TelemetrySidebar(Widget):
28
+ """Right-hand sidebar displaying live simulation, robot kinematics, bumper, and context stats."""
29
+
30
+ DEFAULT_CSS = """
31
+ TelemetrySidebar {
32
+ width: 100%;
33
+ height: 100%;
34
+ background: #0e0e0e;
35
+ padding: 1;
36
+ }
37
+ """
38
+
39
+ def __init__(self, **kwargs):
40
+ super().__init__(id="telemetry-sidebar", **kwargs)
41
+ self.data = TelemetryData()
42
+
43
+ def compose(self):
44
+ with Vertical():
45
+ yield Static("[bold #f1f2f6]SIMULATION[/]", classes="sidebar-section-title")
46
+ yield Static(self._format_sim_state(), id="sim-state", classes="sidebar-metric")
47
+ yield Static(self._format_sim_time(), id="sim-time", classes="sidebar-metric")
48
+ yield Static(self._format_rtf(), id="sim-rtf", classes="sidebar-metric")
49
+
50
+ yield Static("[bold #f1f2f6]ROBOT TELEMETRY[/]", classes="sidebar-section-title")
51
+ yield Static(self._format_pose(), id="robot-pose", classes="sidebar-metric")
52
+ yield Static(self._format_speed(), id="robot-speed", classes="sidebar-metric")
53
+ yield Static(self._format_clearance(), id="robot-clearance", classes="sidebar-metric")
54
+ yield Static(self._format_ate_drift(), id="robot-drift", classes="sidebar-metric")
55
+
56
+ yield Static("[bold #f1f2f6]BUMPER[/]", classes="sidebar-section-title")
57
+ yield Static(self._format_bumper_force(), id="bumper-force", classes="sidebar-metric")
58
+ yield Static(self._format_collision_count(), id="collision-count", classes="sidebar-metric")
59
+
60
+ yield Static("[bold #f1f2f6]CONTEXT[/]", classes="sidebar-section-title")
61
+ yield Static(self._format_context(), id="token-context", classes="sidebar-metric")
62
+
63
+ def _format_sim_state(self) -> str:
64
+ color = "#2ed573" if "Active" in self.data.sim_state else "#ffa502"
65
+ return f"State: [{color}]{self.data.sim_state}[/]"
66
+
67
+ def _format_sim_time(self) -> str:
68
+ return f"Sim Time: [#ced6e0]{self.data.sim_time}[/]"
69
+
70
+ def _format_rtf(self) -> str:
71
+ return f"RTF: [#ced6e0]{self.data.rtf}[/]"
72
+
73
+ def _format_pose(self) -> str:
74
+ return f"Pose: [#ced6e0]({self.data.robot_x:.2f}m, {self.data.robot_y:.2f}m)[/]"
75
+
76
+ def _format_speed(self) -> str:
77
+ return f"Speed: [#ced6e0]{self.data.speed:.2f}m/s[/]"
78
+
79
+ def _format_clearance(self) -> str:
80
+ color = "#ff4757" if self.data.clearance < 0.25 else "#2ed573"
81
+ return f"Clearance: [{color}]{self.data.clearance:.2f}m[/]"
82
+
83
+ def _format_ate_drift(self) -> str:
84
+ color = "#ff4757" if self.data.ate_drift_m > 0.35 else "#2ed573"
85
+ alert = " [#ff4757]⚠ divergence[/]" if self.data.ate_drift_m > 0.35 else ""
86
+ return f"Drift (ATE): [{color}]{self.data.ate_drift_m:.2f}m[/]{alert}"
87
+
88
+ def _format_bumper_force(self) -> str:
89
+ return f"Forces: [#ced6e0]{self.data.bumper_force:.1f} N[/]"
90
+
91
+ def _format_collision_count(self) -> str:
92
+ color = "#ff4757" if self.data.collision_count > 0 else "#ced6e0"
93
+ return f"Hits: [{color}]{self.data.collision_count}[/]"
94
+
95
+ def _format_context(self) -> str:
96
+ spend = (self.data.tokens_used / 1_000_000.0) * 0.20
97
+ return f"[#ced6e0]{self.data.tokens_used:,} tokens\n{self.data.context_capacity_pct}% used\n${spend:.4f} spent[/]"
98
+
99
+ def update_telemetry(self, **kwargs) -> None:
100
+ """Update any metric dynamically and refresh corresponding label."""
101
+ for key, value in kwargs.items():
102
+ if hasattr(self.data, key):
103
+ setattr(self.data, key, value)
104
+
105
+ try:
106
+ self.query_one("#sim-state", Static).update(self._format_sim_state())
107
+ self.query_one("#sim-time", Static).update(self._format_sim_time())
108
+ self.query_one("#sim-rtf", Static).update(self._format_rtf())
109
+ self.query_one("#robot-pose", Static).update(self._format_pose())
110
+ self.query_one("#robot-speed", Static).update(self._format_speed())
111
+ self.query_one("#robot-clearance", Static).update(self._format_clearance())
112
+ self.query_one("#robot-drift", Static).update(self._format_ate_drift())
113
+ self.query_one("#bumper-force", Static).update(self._format_bumper_force())
114
+ self.query_one("#collision-count", Static).update(self._format_collision_count())
115
+ self.query_one("#token-context", Static).update(self._format_context())
116
+ except Exception:
117
+ pass