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.
- lang2sim/__init__.py +47 -0
- lang2sim/agent/__init__.py +5 -0
- lang2sim/agent/nemotron_client.py +109 -0
- lang2sim/agent/react_loop.py +848 -0
- lang2sim/agent/tools.py +998 -0
- lang2sim/cli/app.py +183 -0
- lang2sim/cli/screens/__init__.py +8 -0
- lang2sim/cli/screens/command_palette_modal.py +146 -0
- lang2sim/cli/screens/landing_screen.py +96 -0
- lang2sim/cli/screens/sessions_modal.py +236 -0
- lang2sim/cli/screens/workspace_screen.py +709 -0
- lang2sim/cli/widgets/__init__.py +16 -0
- lang2sim/cli/widgets/action_pill.py +67 -0
- lang2sim/cli/widgets/command_card.py +265 -0
- lang2sim/cli/widgets/plan_card.py +106 -0
- lang2sim/cli/widgets/prompt_card.py +96 -0
- lang2sim/cli/widgets/telemetry_sidebar.py +117 -0
- lang2sim/cli/widgets/thought_box.py +64 -0
- lang2sim/compiler/__init__.py +15 -0
- lang2sim/compiler/sdf_compiler.py +65 -0
- lang2sim/compiler/sir_generator.py +46 -0
- lang2sim/compiler/sir_validator.py +87 -0
- lang2sim/core/__init__.py +50 -0
- lang2sim/core/config.py +116 -0
- lang2sim/core/models.py +279 -0
- lang2sim/core/session_manager.py +189 -0
- lang2sim/perturbation/__init__.py +50 -0
- lang2sim/perturbation/_compat.py +19 -0
- lang2sim/perturbation/gazebo_bridge.py +520 -0
- lang2sim/perturbation/mcp_server.py +129 -0
- lang2sim/perturbation/routers/__init__.py +19 -0
- lang2sim/perturbation/routers/world.py +110 -0
- lang2sim/perturbation/sdf_snippets.py +112 -0
- lang2sim/runtime/__init__.py +7 -0
- lang2sim/runtime/control_api/__init__.py +7 -0
- lang2sim/runtime/control_api/client.py +187 -0
- lang2sim/runtime/control_api/server.py +504 -0
- lang2sim/runtime/display.py +141 -0
- lang2sim/runtime/docker_runner.py +286 -0
- lang2sim/runtime/doctor.py +220 -0
- lang2sim/runtime/hardware_probe.py +144 -0
- lang2sim/runtime/health_monitor.py +66 -0
- lang2sim/runtime/launch_builder.py +137 -0
- lang2sim/runtime/nav2_dispatcher.py +215 -0
- lang2sim/runtime/platform.py +137 -0
- lang2sim/runtime/ros_bridge.py +183 -0
- lang2sim/runtime/run_folder.py +86 -0
- lang2sim/runtime/self_healing.py +387 -0
- lang2sim/runtime/session.py +136 -0
- lang2sim/runtime/world_check.py +197 -0
- lang2sim/telemetry/__init__.py +85 -0
- lang2sim/telemetry/nqi_scorer.py +645 -0
- lang2sim/telemetry/ros_subscribers.py +489 -0
- lang2sim/telemetry/trajectory.py +258 -0
- lang2sim/telemetry/vision.py +477 -0
- lang2sim/world_gen/__init__.py +16 -0
- lang2sim/world_gen/catalog_loader.py +83 -0
- lang2sim/world_gen/map_slicer.py +131 -0
- lang2sim/world_gen/shapely_solver.py +256 -0
- lang2sim-2.0.0.dist-info/METADATA +53 -0
- lang2sim-2.0.0.dist-info/RECORD +65 -0
- lang2sim-2.0.0.dist-info/WHEEL +5 -0
- lang2sim-2.0.0.dist-info/entry_points.txt +2 -0
- lang2sim-2.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|