ummm new tui thing ?

This commit is contained in:
2026-03-28 21:49:33 +02:00
parent 3db6de2d2c
commit de440305c7
16 changed files with 534 additions and 226 deletions

View File

@@ -67,13 +67,11 @@ def load_config_entries_from_values_md() -> list[ConfigEntry]:
if path in seen:
continue
# Filter out non-robot/server dev configs the user doesn't want here.
if path.startswith("/hub-shim/"):
continue
if path.lower().endswith(".md"):
continue
# Keep it focused on JSON files (these are strict JSON configs).
if not path.lower().endswith(".json"):
continue

View File

@@ -50,12 +50,10 @@ class MainWindowController:
self._identity: Optional[dict] = None
self._connecting = False
# Tabs + connection pill
self.tab_widget = require_child(self.window, "tabWidget", QTabWidget)
self.connection_pill, self.conn_dot, self.conn_text = self._create_connection_pill()
self.tab_widget.setCornerWidget(self.connection_pill, Qt.TopRightCorner)
# Jibo/config
self.jibo_ip = require_child(self.window, "JiboIpField", QLineEdit)
self.connect_button = require_child(self.window, "TryToConnect", QPushButton)
self.jibo_title = require_child(self.window, "jiboTitle", QLabel)
@@ -65,7 +63,6 @@ class MainWindowController:
self.ha_enable = require_child(self.window, "haEnableCheck", QCheckBox)
self.ha_server_ip = require_child(self.window, "haServerIpField", QLineEdit)
# AI Bridge (formerly "AI Provider")
self.ai_enable = require_child(self.window, "aiEnableCheck", QCheckBox)
self.ai_mode = require_child(self.window, "aiProviderCombo", QComboBox)
self.ai_server_base_url = require_child(self.window, "aiEndpointField", QLineEdit)
@@ -82,10 +79,8 @@ class MainWindowController:
self._ai_bridge_obj: Optional[dict[str, Any]] = None
# Tool settings
self.enable_logging_check = require_child(self.window, "enableLoggingCheck", QCheckBox)
# Config editor (main panel "Config" section)
self.config_file_combo = require_child(self.window, "configFileCombo", QComboBox)
self.config_read_button = require_child(self.window, "configReadButton", QPushButton)
self.config_write_button = require_child(self.window, "configWriteButton", QPushButton)
@@ -96,18 +91,15 @@ class MainWindowController:
self._config_last_read_text: Optional[str] = None
self._config_paths: list[str] = []
# Jibo card controls
self.robot_settings_button = require_child(self.window, "RobotSettings", QPushButton)
self.robot_action_combo = require_child(self.window, "comboBox", QComboBox)
self.jibo_image = require_child(self.window, "jiboImage", QLabel)
self._robot_settings_window: Optional[object] = None
# Update page
self.install_button = require_child(self.window, "installButton", QPushButton)
self.check_updates_button = require_child(self.window, "checkUpdatesButton", QPushButton)
# Status page
self.status_dot = require_child(self.window, "statusDot", QLabel)
self.status_text = require_child(self.window, "statusText", QLabel)
@@ -137,7 +129,6 @@ class MainWindowController:
return self.session_connected
def _configure_ui(self) -> None:
# Simple styling, roughly matching the previous QML look.
self.connection_pill.setStyleSheet(
"QFrame#connectionPill {"
"background-color: #f6f6f6;"
@@ -146,22 +137,18 @@ class MainWindowController:
"}"
)
# AI Bridge mode choices
self.ai_mode.clear()
self.ai_mode.addItems(["TEXT", "AUDIO"])
# Robot controls start disabled until connected.
self.robot_settings_button.setEnabled(False)
self.robot_action_combo.setEnabled(False)
# Config editor defaults
self.config_editor.setPlaceholderText("Select a config file, then Read")
self.config_activity_log.setReadOnly(True)
self.config_activity_log.setPlaceholderText("Logging is disabled")
self.config_read_button.setEnabled(False)
self.config_write_button.setEnabled(False)
# Defaults
self.connect_button.setText("Connect")
self.jibo_title.setText("Connect Your Jibo")
@@ -179,7 +166,6 @@ class MainWindowController:
self.edit_ai_bridge_button.clicked.connect(self._jump_to_ai_bridge_config)
# Keep AI Bridge in-memory config in sync with UI edits.
self.ai_enable.toggled.connect(self._sync_ai_bridge_obj_from_ui)
self.ai_mode.currentIndexChanged.connect(self._sync_ai_bridge_obj_from_ui)
self.ai_server_base_url.textChanged.connect(self._sync_ai_bridge_obj_from_ui)
@@ -218,7 +204,6 @@ class MainWindowController:
ssh_client=self._ssh_client,
logging_enabled_check=self.enable_logging_check,
)
# Refresh the SSH client reference in case we reconnected.
try:
self._robot_settings_window.set_ssh_client(self._ssh_client) # type: ignore[attr-defined]
except Exception:
@@ -245,12 +230,10 @@ class MainWindowController:
self.ai_followup_enabled.setEnabled(ai_enabled)
self.ai_followup_delay_ms.setEnabled(ai_enabled)
# Connection button enabled unless a connect attempt is in progress.
self.connect_button.setEnabled(not self._connecting)
connected = self.session_connected
self.config_read_button.setEnabled(connected and self.config_file_combo.count() > 0)
# write button is controlled by editor dirty state
def _sync_all(self) -> None:
host = self.host
@@ -285,7 +268,6 @@ class MainWindowController:
else:
self.status_text.setText("No Jibo IP configured")
# Image swap
assets = Path(__file__).resolve().parent / "Assets" / "Jibo"
img_path = assets / ("JiboFaceForward.png" if visual_connected else "NoJiboConnected.png")
pm = QPixmap(str(img_path))
@@ -340,7 +322,6 @@ class MainWindowController:
self.config_activity_log.appendPlainText(message)
def _populate_config_file_combo(self) -> None:
# Populate from inventory, excluding /usr/local/etc (those belong under Robot Settings)
entries = load_config_entries_from_values_md()
paths = [e.remote_path for e in entries if not e.is_usr_local_etc]
paths = sorted(paths)
@@ -369,8 +350,6 @@ class MainWindowController:
self.config_file_combo.setCurrentIndex(idx)
self._read_selected_config()
# Seed editor with the current AI Bridge UI state so the user can
# immediately press Write.
try:
merged = self._merged_ai_bridge_obj_from_ui()
desired_text = json.dumps(merged, indent=2, ensure_ascii=False) + "\n"
@@ -539,7 +518,6 @@ class MainWindowController:
return base
def _sync_ai_bridge_obj_from_ui(self, *_args: Any) -> None:
# Keep unknown keys (if any) from the on-robot JSON.
try:
self._ai_bridge_obj = self._merged_ai_bridge_obj_from_ui()
except Exception:
@@ -577,7 +555,6 @@ class MainWindowController:
except Exception:
old_obj = MISSING
# Safety: if a /usr/local path ever ends up here, handle remount.
if p.startswith("/usr/local/"):
cmd = "mount -o remount,rw /usr/local"
self._log(f"EXEC {cmd}")
@@ -666,12 +643,10 @@ class MainWindowController:
identity = json.loads(raw_text)
# Success: store session.
self._ssh_client = client
self._identity = identity if isinstance(identity, dict) else None
self.status_text.setText(f"Connected via SSH to {host}")
# Auto-populate AI Bridge section when connected.
try:
self._load_ai_bridge_from_robot()
except Exception:
@@ -705,7 +680,6 @@ class MainWindowController:
layout.addWidget(dot)
layout.addWidget(text)
# Keep it tight on the tab bar.
pill.setSizePolicy(pill.sizePolicy().horizontalPolicy(), pill.sizePolicy().verticalPolicy())
pill.setMinimumHeight(28)
return pill, dot, text

View File

@@ -45,7 +45,6 @@ def resolve_python_invocation() -> tuple[str, list[str]]:
if venv_py.exists():
return (str(venv_py), [])
# Prefer the current interpreter when running inside a venv (e.g. Qt Creator).
try:
if sys.executable and Path(sys.executable).exists():
return (sys.executable, [])
@@ -64,7 +63,6 @@ def resolve_python_invocation() -> tuple[str, list[str]]:
def resolve_python() -> str:
program, prefix = resolve_python_invocation()
if prefix:
# Best-effort string representation (mostly for display)
return " ".join([program] + prefix)
return program
@@ -82,7 +80,6 @@ def _pick_terminal_command() -> Optional[list[str]]:
return None
candidates: list[list[str]] = []
# Debian/Ubuntu alternative system
candidates.append(["x-terminal-emulator", "-e"])
candidates.append(["gnome-terminal", "--"])
candidates.append(["konsole", "-e"])
@@ -102,8 +99,6 @@ def spawn_in_terminal(argv: list[str]) -> bool:
"""
if os.name == "nt":
# Use cmd.exe window, keep it open (/k)
# Build a single string command for cmd.
cmdline = " ".join(shlex.quote(a) for a in argv)
subprocess.Popen(["cmd", "/c", "start", "cmd", "/k", cmdline], shell=False)
return True

View File

@@ -44,13 +44,11 @@ class RobotSettingsWindow:
splitter = QSplitter(Qt.Horizontal)
outer.addWidget(splitter, 1)
# Left: tree
self.tree = QTreeWidget()
self.tree.setHeaderHidden(True)
self.tree.setSelectionMode(QAbstractItemView.SingleSelection)
splitter.addWidget(self.tree)
# Right: editor + buttons + log
right = QWidget()
right_layout = QVBoxLayout(right)
right_layout.setContentsMargins(0, 0, 0, 0)
@@ -168,7 +166,6 @@ class RobotSettingsWindow:
@Slot()
def _on_editor_changed(self) -> None:
# Enable write only if we have a loaded file and text changed.
if not self._current_remote_path or self._last_read_text is None:
self.write_button.setEnabled(False)
return
@@ -243,14 +240,12 @@ class RobotSettingsWindow:
new_text_raw = self.editor.toPlainText()
# Validate JSON if possible; this tool is focused on strict JSON configs.
try:
new_obj = json.loads(new_text_raw)
except Exception as e:
QMessageBox.warning(self.window, "Invalid JSON", f"JSON parse failed: {e}")
return
# Canonicalize to keep robot-side JSON strict/clean.
new_text = json.dumps(new_obj, indent=2, ensure_ascii=False) + "\n"
try:
@@ -264,7 +259,6 @@ class RobotSettingsWindow:
except Exception:
old_obj = MISSING
# Mounted dir special case: /usr/local/* is often read-only until remount.
if remote_path.startswith("/usr/local/"):
cmd = "mount -o remount,rw /usr/local"
self._log(f"EXEC {cmd}")
@@ -280,7 +274,6 @@ class RobotSettingsWindow:
if out.strip():
self._log(out.strip())
# Compute diffs (best-effort).
if old_obj is not MISSING:
diffs = diff_json(old_obj, new_obj)
if diffs:
@@ -302,7 +295,6 @@ class RobotSettingsWindow:
try:
self._sftp_write_text(remote_path, new_text)
self._log(f"WROTE {remote_path} ({len(new_text)} bytes)")
# Refresh read baseline.
self.editor.setPlainText(new_text)
self._last_read_text = new_text
self.write_button.setEnabled(False)

View File

@@ -63,7 +63,6 @@ class ToolRunnerWindow(QObject):
self._host_field.setVisible(self._is_updater)
# Installer-specific UX
self._use_existing_dump.setVisible(self._is_installer)
self._dump_path.setVisible(self._is_installer)
self._browse_dump.setVisible(self._is_installer)
@@ -108,7 +107,6 @@ class ToolRunnerWindow(QObject):
self._sync_buttons()
self._sync_status()
# Ensure the process is stopped when the window closes.
self.window.closeEvent = self._on_close # type: ignore[assignment]
def show(self) -> None:
@@ -125,7 +123,6 @@ class ToolRunnerWindow(QObject):
extra = self._extra_args.text().strip()
extra_args: list[str] = shlex.split(extra) if extra else []
# Installer convenience: if the user has an existing dump, pass it via --dump-path
if self._is_installer and self._use_existing_dump.isChecked():
dump_path = self._dump_path.text().strip()
if dump_path and "--dump-path" not in extra_args:
@@ -150,7 +147,6 @@ class ToolRunnerWindow(QObject):
self._status.setText("Dump file not found")
return
# Reset progress state for a new run.
self._output_buffer = ""
self._last_step_total = None
if self._is_installer:
@@ -168,7 +164,6 @@ class ToolRunnerWindow(QObject):
@Slot(str)
def _append_output(self, chunk: str) -> None:
# Keep it simple: append and scroll to end.
self._log.moveCursor(QTextCursor.End)
self._log.insertPlainText(chunk)
self._log.moveCursor(QTextCursor.End)
@@ -181,7 +176,6 @@ class ToolRunnerWindow(QObject):
self._start_stop.setText("Stop" if running else "Start")
self._open_terminal.setEnabled(not running)
if not running and self._is_installer:
# Leave progress/status in a meaningful final state.
if self.runner.exitCode == 0 and self._last_step_total:
self._progress.setRange(0, self._last_step_total)
self._progress.setValue(self._last_step_total)
@@ -194,7 +188,6 @@ class ToolRunnerWindow(QObject):
def _sync_status(self) -> None:
if self.runner.running:
self._status.setText("Running...")
# Indeterminate until we see a structured step marker.
if self._is_installer and self._last_step_total is None:
self._progress.setRange(0, 0)
return
@@ -233,7 +226,6 @@ class ToolRunnerWindow(QObject):
self._output_buffer += chunk
lines = self._output_buffer.splitlines(keepends=True)
# Keep any partial line for the next chunk.
if lines and not (lines[-1].endswith("\n") or lines[-1].endswith("\r")):
self._output_buffer = lines[-1]
lines = lines[:-1]
@@ -245,7 +237,6 @@ class ToolRunnerWindow(QObject):
if not clean:
continue
# Also surface meaningful non-step status lines (RCM detection, warnings, etc.)
if clean.startswith(("", "", "", "")) or "RCM" in clean:
msg = _clean_status_line(clean)
if msg and not msg.startswith("["):
@@ -258,7 +249,6 @@ class ToolRunnerWindow(QObject):
total = int(m.group(2))
msg = m.group(3).strip()
# Some flows use [0/6] for dependency checks.
if total > 0:
self._last_step_total = total
self._progress.setRange(0, total)
@@ -284,9 +274,7 @@ def _strip_ansi(s: str) -> str:
def _clean_status_line(s: str) -> str:
# Drop leading glyphs used by the CLI (info/warn/success/error)
s = re.sub(r"^[✓⚠✗ℹ]\s+", "", s).strip()
# Collapse extra whitespace
s = re.sub(r"\s+", " ", s).strip()
return s

View File

@@ -27,7 +27,6 @@ def load_ui(ui_path: Path) -> object:
def require_child(parent: object, name: str, typ: type[T]) -> T:
# Qt objects implement findChild; keep typing light.
child = parent.findChild(typ, name) # type: ignore[attr-defined]
if child is None:
raise RuntimeError(f"UI is missing required widget '{name}' ({typ.__name__})")

View File

@@ -1,12 +1,5 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'tool_runner.ui'
##
## Created by: Qt User Interface Compiler version 6.10.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
@@ -149,7 +142,6 @@ class Ui_ToolRunnerWindow(object):
self.retranslateUi(ToolRunnerWindow)
QMetaObject.connectSlotsByName(ToolRunnerWindow)
# setupUi
def retranslateUi(self, ToolRunnerWindow):
ToolRunnerWindow.setWindowTitle(QCoreApplication.translate("ToolRunnerWindow", u"Tool", None))
@@ -165,5 +157,4 @@ class Ui_ToolRunnerWindow(object):
self.currentStepLabel.setText(QCoreApplication.translate("ToolRunnerWindow", u"Idle", None))
self.statusLabel.setText(QCoreApplication.translate("ToolRunnerWindow", u"Idle", None))
self.clearLogButton.setText(QCoreApplication.translate("ToolRunnerWindow", u"Clear log", None))
# retranslateUi

View File

@@ -1,12 +1,5 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'form.ui'
##
## Created by: Qt User Interface Compiler version 6.10.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
@@ -557,7 +550,6 @@ class Ui_MainWindow(object):
QMetaObject.connectSlotsByName(MainWindow)
# setupUi
def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Jibo Tools", None))
@@ -627,5 +619,4 @@ class Ui_MainWindow(object):
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabStatus), QCoreApplication.translate("MainWindow", u"Status", None))
self.robotOsComingSoon.setText(QCoreApplication.translate("MainWindow", u"Coming soon.", None))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabRobotOs), QCoreApplication.translate("MainWindow", u"Robot OS", None))
# retranslateUi