aether-robotics 4.2.0a2__tar.gz → 4.2.0a4__tar.gz
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.
- {aether_robotics-4.2.0a2/aether_robotics.egg-info → aether_robotics-4.2.0a4}/PKG-INFO +1 -1
- aether_robotics-4.2.0a4/VERSION +1 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/__init__.py +1 -1
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/app.py +21 -1
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/tui/app.py +49 -11
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/tui/widgets/__init__.py +4 -3
- aether_robotics-4.2.0a4/aether/tui/widgets/agent.py +357 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/tui/widgets/common.py +24 -1
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4/aether_robotics.egg-info}/PKG-INFO +1 -1
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/pyproject.toml +1 -1
- aether_robotics-4.2.0a4/tests/test_tui_widgets.py +320 -0
- aether_robotics-4.2.0a2/VERSION +0 -1
- aether_robotics-4.2.0a2/aether/tui/widgets/agent.py +0 -132
- aether_robotics-4.2.0a2/tests/test_tui_widgets.py +0 -131
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/LICENSE +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/MANIFEST.in +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/README.md +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/__main__.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/actions/__init__.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/actions/abstract_actions.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/__init__.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/_generator.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/adeept_hat_v3.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/base_adapter.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/drone_adapter.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/freenove_4wd.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/freenove_mecanum.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/gopigo3.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/l298n_direct_gpio.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/pca9685_generic.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/rover_adapter.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/sunfounder_picarx.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/sunfounder_pidog.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/universal_adapter.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/waveshare_motor_driver.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/adapters/yahboom_robot_hat.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/agents/__init__.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/agents/adaptation_agent.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/agents/camera_agent.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/agents/correction_agent.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/agents/execution_agent.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/agents/fault_agent.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/agents/memory_agent.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/agents/movement_agent.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/agents/navigation_agent.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/agents/perception_agent.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/agents/planner_agent.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/agents/power_agent.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/agents/task_manager.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/agents/thermal_agent.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/capabilities/__init__.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/capabilities/capability_loader.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/cli/__init__.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/cli/analytics.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/cli/autonomous.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/cli/calibrate.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/cli/doctor.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/cli/fix.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/cli/kits.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/cli/kits_install_recipes/freenove_4wd.yaml +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/cli/kits_install_recipes/gopigo3.yaml +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/cli/kits_install_recipes/sunfounder_picarx.yaml +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/cli/kits_install_recipes/sunfounder_pidog.yaml +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/cli/kits_install_recipes/yahboom_robot_hat.yaml +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/__init__.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/analytics.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/auto_installer.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/auto_updater.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/autonomous.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/banner.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/calibration.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/decomposer.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/executor.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/feedback.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/genome.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/goal_parser.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/goals.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/llm_planner.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/mapper.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/mavlink_integration.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/memory.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/message_bus.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/metrics.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/navigation_engine.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/overrides.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/patches.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/planner.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/security.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/task_scheduler.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/telemetry_config.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/tool_builder.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/tool_discovery.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/tool_registry.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/ui.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/core/visualizer.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/faults/__init__.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/faults/fault_detector.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/faults/fault_injector.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/parallel/__init__.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/parallel/executor.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/simulation/__init__.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/simulation/environment.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/simulation/real_perception.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/simulation/scenarios.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/skills/__init__.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/skills/drive_until_obstacle.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/tui/__init__.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether/tui/widgets/doctor.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether_robotics.egg-info/SOURCES.txt +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether_robotics.egg-info/dependency_links.txt +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether_robotics.egg-info/entry_points.txt +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether_robotics.egg-info/requires.txt +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/aether_robotics.egg-info/top_level.txt +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/configs/calibrated_camera_only_20260331_121709.json +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/configs/calibrated_camera_only_20260331_122446.json +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/configs/calibrated_camera_only_20260401_094544.json +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/configs/calibrated_camera_only_20260401_094554.json +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/configs/drone_v1.json +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/configs/rover_v1.json +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/context/aether_definitions.txt +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/install.sh +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/main.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/requirements.txt +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/setup.cfg +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_adapter_gpio_manifests.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_adapter_override_wiring.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_adapter_preflight.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_adapter_resolver_v397.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_analytics_cli.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_analytics_module.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_analytics_wiring.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_atexit_adapter_shutdown.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_atexit_reaches_stop_all.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_auto_adapter.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_auto_updater.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_autonomous_cli.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_autonomous_loop.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_autonomous_no_between_step_emergency_stop.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_boot_resolver_flow.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_calibrate_wizard.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_calibration.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_calibration_unit.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_chaos.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_cli_doctor.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_cli_fix.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_cli_fix_patches.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_cli_fix_v3_10_6.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_cli_fix_v3_10_8.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_cli_fix_visibility.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_cli_kits.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_cli_routing.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_consent_flow.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_correction_agent.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_decomposer.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_decomposer_avoids_legacy.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_decomposer_routing.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_display_bug_fix.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_doctor_gpio_allocation.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_doctor_multi_adapter.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_drive_until_obstacle.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_entrypoint.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_fix_scope_validation.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_fix_sees_named_servos.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_genome.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_goal_persistence.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_gopigo3_adapter.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_gpio_conflict_detection.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_high_level_task_examples.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_map_room_high_level_task.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_mapper.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_mavlink_integration.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_multi_adapter_boot_flow.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_multi_adapter_registration.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_multi_discovery.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_named_servo_registration.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_navigation_engine.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_overrides.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_overrides_multi_adapter.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_overrides_schema_v1_2.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_parallel_executor.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_pca9685_emergency_stop.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_pca9685_emergency_stop_hardware.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_pca9685_hard_off.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_pca9685_pwm_primitives.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_pca9685_wake_sequence.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_persistent_memory.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_planner_routing.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_planner_stop_routing.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_posthog_integration.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_primary_adapter_designation.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_robot_json_v1_1.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_security.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_set_servo_angle_duration.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_stop_full_off_behavior.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_stop_servo_sleep_fallback.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_sunfounder_picarx_adapter.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_sunfounder_pidog_adapter.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_synthesizer_fidelity_check.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_task_scheduler.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_tier1_motion_cleanup.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_tool_builder.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_tool_discovery.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_tool_discovery_gpio_cleanup.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_tool_namespacing.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_tui_app.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_tui_backward_compat.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_ui_autonomous.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_ui_dashboard.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_ui_module.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_ui_wizard.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_validator_rejects_legacy.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_yahboom_robot_hat_adapter.py +0 -0
- {aether_robotics-4.2.0a2 → aether_robotics-4.2.0a4}/tests/test_yolo_integration.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.2.0a4
|
|
@@ -1719,7 +1719,8 @@ def _summarize_output(output) -> str:
|
|
|
1719
1719
|
|
|
1720
1720
|
def _execute_objective(objective: str, registry, llm_planner, corrector,
|
|
1721
1721
|
parser_obj, manifest, recent_memory,
|
|
1722
|
-
available, fix_mode: bool = False
|
|
1722
|
+
available, fix_mode: bool = False,
|
|
1723
|
+
on_step=None) -> Dict:
|
|
1723
1724
|
"""Execute a single objective and return structured result.
|
|
1724
1725
|
|
|
1725
1726
|
Used by agent mode (interactive), server mode (HTTP), and `aether --fix`
|
|
@@ -1755,6 +1756,20 @@ def _execute_objective(objective: str, registry, llm_planner, corrector,
|
|
|
1755
1756
|
if fix_mode:
|
|
1756
1757
|
print(f" [agent] planning: {objective}")
|
|
1757
1758
|
|
|
1759
|
+
# v4.2.0a3: optional live-progress hook (TUI). Phases: "plan" (full plan
|
|
1760
|
+
# list, once), "start" (before each step), "done" (after each step).
|
|
1761
|
+
# Presentation-only — a hook failure must never affect execution.
|
|
1762
|
+
def _report(phase: str, data: Dict) -> None:
|
|
1763
|
+
if on_step is None:
|
|
1764
|
+
return
|
|
1765
|
+
try:
|
|
1766
|
+
on_step(phase, data)
|
|
1767
|
+
except Exception:
|
|
1768
|
+
pass
|
|
1769
|
+
|
|
1770
|
+
_report("plan", {"plan": plan, "source": plan_source,
|
|
1771
|
+
"objective": objective})
|
|
1772
|
+
|
|
1758
1773
|
# Execute plan steps
|
|
1759
1774
|
step_faults = 0
|
|
1760
1775
|
prev_output = None
|
|
@@ -1767,6 +1782,8 @@ def _execute_objective(objective: str, registry, llm_planner, corrector,
|
|
|
1767
1782
|
for i, action in enumerate(plan, 1):
|
|
1768
1783
|
tool_name = action["tool"]
|
|
1769
1784
|
params = dict(action.get("params", {}))
|
|
1785
|
+
_report("start", {"index": i, "total": len(plan),
|
|
1786
|
+
"tool": tool_name, "params": dict(params)})
|
|
1770
1787
|
|
|
1771
1788
|
# Pipe previous output
|
|
1772
1789
|
pipe_key = action.get("pipe_input")
|
|
@@ -1854,6 +1871,7 @@ def _execute_objective(objective: str, registry, llm_planner, corrector,
|
|
|
1854
1871
|
|
|
1855
1872
|
action_log.append(entry)
|
|
1856
1873
|
step_outputs.append(str(prev_output) if prev_output else None)
|
|
1874
|
+
_report("done", {"index": i, "total": len(plan), "entry": dict(entry)})
|
|
1857
1875
|
|
|
1858
1876
|
if fix_mode:
|
|
1859
1877
|
from aether.core import ui
|
|
@@ -1976,6 +1994,8 @@ def bootstrap_agent_state_for_fix() -> dict:
|
|
|
1976
1994
|
"parser": parser,
|
|
1977
1995
|
"manifest": manifest,
|
|
1978
1996
|
"available": registry.available_tools(),
|
|
1997
|
+
# v4.2.0a3: adapter identity for the TUI status bar / boot detail.
|
|
1998
|
+
"adapter_id": _adapter_id,
|
|
1979
1999
|
}
|
|
1980
2000
|
|
|
1981
2001
|
|
|
@@ -60,8 +60,8 @@ AETHER_THEME = Theme(
|
|
|
60
60
|
)
|
|
61
61
|
|
|
62
62
|
from aether import __version__
|
|
63
|
-
from aether.tui.widgets import (AgentWidget, DoctorWidget,
|
|
64
|
-
WelcomeWidget)
|
|
63
|
+
from aether.tui.widgets import (AgentWidget, DoctorWidget, MascotWidget,
|
|
64
|
+
PlaceholderWidget, WelcomeWidget)
|
|
65
65
|
|
|
66
66
|
# (mode id, sidebar label, phase-1 availability)
|
|
67
67
|
MODES = [
|
|
@@ -140,9 +140,11 @@ class AetherApp(App):
|
|
|
140
140
|
#header-bar { dock: top; height: 1; background: ansi_default;
|
|
141
141
|
padding: 0 1; }
|
|
142
142
|
#body { height: 1fr; background: ansi_default; }
|
|
143
|
-
#sidebar { width:
|
|
144
|
-
|
|
143
|
+
#sidebar-col { width: 17; background: ansi_default; }
|
|
144
|
+
#sidebar { width: 100%; height: 1fr; background: ansi_default;
|
|
145
|
+
border: none; padding: 0 1; }
|
|
145
146
|
#sidebar ListItem { background: ansi_default; padding: 0 1; }
|
|
147
|
+
#mascot { dock: bottom; }
|
|
146
148
|
#main-switcher { width: 1fr; border: none; background: ansi_default;
|
|
147
149
|
padding: 0 1; }
|
|
148
150
|
#status-bar { height: 1; background: ansi_default;
|
|
@@ -165,15 +167,27 @@ class AetherApp(App):
|
|
|
165
167
|
super().__init__(**kwargs)
|
|
166
168
|
self._session_start = time.monotonic()
|
|
167
169
|
self._telemetry_enabled: Optional[bool] = None
|
|
170
|
+
self._captured_prints: list = [] # tail of swallowed print() output
|
|
171
|
+
self._busy_spin = 0
|
|
172
|
+
|
|
173
|
+
def on_print(self, event) -> None:
|
|
174
|
+
"""Swallow print() output from the classic pipeline (see on_mount);
|
|
175
|
+
keep the last 200 lines for debugging."""
|
|
176
|
+
text = event.text.rstrip()
|
|
177
|
+
if text:
|
|
178
|
+
self._captured_prints.append(text)
|
|
179
|
+
del self._captured_prints[:-200]
|
|
168
180
|
|
|
169
181
|
# ── layout ─────────────────────────────────────────────────────────
|
|
170
182
|
def compose(self) -> ComposeResult:
|
|
171
183
|
yield Static(id="header-bar")
|
|
172
184
|
with Horizontal(id="body"):
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
185
|
+
with Vertical(id="sidebar-col"):
|
|
186
|
+
yield ListView(
|
|
187
|
+
*[ListItem(Label(label), id=f"nav-{mode_id}")
|
|
188
|
+
for mode_id, label, _ in MODES],
|
|
189
|
+
id="sidebar")
|
|
190
|
+
yield MascotWidget(id="mascot")
|
|
177
191
|
with ContentSwitcher(initial="welcome", id="main-switcher"):
|
|
178
192
|
yield WelcomeWidget(id="welcome")
|
|
179
193
|
yield AgentWidget(id="agent")
|
|
@@ -187,6 +201,11 @@ class AetherApp(App):
|
|
|
187
201
|
def on_mount(self) -> None:
|
|
188
202
|
self.register_theme(AETHER_THEME)
|
|
189
203
|
self.theme = "aether"
|
|
204
|
+
# v4.2.0a4: the classic pipeline (discovery, adapters, planner) does
|
|
205
|
+
# raw print()s; uncaptured they hit the terminal under the TUI and
|
|
206
|
+
# flash/corrupt the screen until the next repaint. Capture them
|
|
207
|
+
# app-wide; keep the tail for debugging.
|
|
208
|
+
self.begin_capture_print(self)
|
|
190
209
|
try:
|
|
191
210
|
from aether.core.analytics import load_telemetry_config
|
|
192
211
|
self._telemetry_enabled = bool(
|
|
@@ -196,6 +215,7 @@ class AetherApp(App):
|
|
|
196
215
|
self._refresh_header()
|
|
197
216
|
self._refresh_status()
|
|
198
217
|
self.set_interval(1.0, self._tick_session)
|
|
218
|
+
self.set_interval(0.12, self._tick_busy)
|
|
199
219
|
self.query_one("#sidebar", ListView).focus()
|
|
200
220
|
|
|
201
221
|
# ── reactive watchers ──────────────────────────────────────────────
|
|
@@ -220,6 +240,13 @@ class AetherApp(App):
|
|
|
220
240
|
def _tick_session(self) -> None:
|
|
221
241
|
self.session_seconds = int(time.monotonic() - self._session_start)
|
|
222
242
|
|
|
243
|
+
def _tick_busy(self) -> None:
|
|
244
|
+
"""Animate the status-bar spinner while something is in flight."""
|
|
245
|
+
if self.busy_text:
|
|
246
|
+
from aether.tui.widgets.agent import SPINNER
|
|
247
|
+
self._busy_spin = (self._busy_spin + 1) % len(SPINNER)
|
|
248
|
+
self._refresh_status()
|
|
249
|
+
|
|
223
250
|
def _refresh_header(self) -> None:
|
|
224
251
|
adapters = (f"{self.adapter_count} adapter"
|
|
225
252
|
f"{'s' if self.adapter_count != 1 else ''}")
|
|
@@ -234,8 +261,13 @@ class AetherApp(App):
|
|
|
234
261
|
m, s = divmod(self.session_seconds, 60)
|
|
235
262
|
telemetry = {True: "enabled", False: "disabled",
|
|
236
263
|
None: "unknown"}[self._telemetry_enabled]
|
|
237
|
-
|
|
238
|
-
|
|
264
|
+
if self.busy_text:
|
|
265
|
+
from aether.tui.widgets.agent import SPINNER
|
|
266
|
+
frame = SPINNER[self._busy_spin]
|
|
267
|
+
busy = (f" • [{AETHER_ORANGE}]{frame} "
|
|
268
|
+
f"{self.busy_text}[/{AETHER_ORANGE}]")
|
|
269
|
+
else:
|
|
270
|
+
busy = ""
|
|
239
271
|
try:
|
|
240
272
|
self.query_one("#status-bar", Static).update(
|
|
241
273
|
f"● {self.adapters_line} • Telemetry: {telemetry} • "
|
|
@@ -266,8 +298,14 @@ class AetherApp(App):
|
|
|
266
298
|
return
|
|
267
299
|
except Exception:
|
|
268
300
|
pass
|
|
301
|
+
# Single-adapter path (load_tier1 doesn't fill the active list).
|
|
302
|
+
adapter_id = state.get("adapter_id")
|
|
269
303
|
n = len(state.get("available", []))
|
|
270
|
-
|
|
304
|
+
if adapter_id:
|
|
305
|
+
self.adapters_line = f"{adapter_id} (primary) · {n} tools"
|
|
306
|
+
self.adapter_count = 1
|
|
307
|
+
else:
|
|
308
|
+
self.adapters_line = f"registry ready ({n} tools)"
|
|
271
309
|
|
|
272
310
|
# ── mode switching ─────────────────────────────────────────────────
|
|
273
311
|
def activate_mode(self, mode_id: str) -> None:
|
|
@@ -5,7 +5,8 @@ render a PlaceholderWidget until phase 2/3.
|
|
|
5
5
|
"""
|
|
6
6
|
from aether.tui.widgets.agent import AgentWidget
|
|
7
7
|
from aether.tui.widgets.doctor import DoctorWidget
|
|
8
|
-
from aether.tui.widgets.common import PlaceholderWidget,
|
|
8
|
+
from aether.tui.widgets.common import (MascotWidget, PlaceholderWidget,
|
|
9
|
+
WelcomeWidget)
|
|
9
10
|
|
|
10
|
-
__all__ = ["AgentWidget", "DoctorWidget", "
|
|
11
|
-
"WelcomeWidget"]
|
|
11
|
+
__all__ = ["AgentWidget", "DoctorWidget", "MascotWidget",
|
|
12
|
+
"PlaceholderWidget", "WelcomeWidget"]
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Agent mode widget: objective input + in-flow live plan blocks (v4.2.0a4).
|
|
2
|
+
|
|
3
|
+
Dispatches through the exact same pipeline as the classic REPL:
|
|
4
|
+
bootstrap_agent_state_for_fix() builds the registry/planner once (in a
|
|
5
|
+
thread worker, since it touches hardware), then each objective runs through
|
|
6
|
+
aether.app._execute_objective and its on_step hook.
|
|
7
|
+
|
|
8
|
+
Layout model: the log is a scroll of blocks. Each objective mounts ONE
|
|
9
|
+
RunBlock directly under it that live-updates in place — thinking spinner →
|
|
10
|
+
concise plan ("thought") line → step list with ○/spinner/●/seconds — and
|
|
11
|
+
simply stays there when the run finishes. Planning is always right below
|
|
12
|
+
the objective, never docked at the bottom. Only presentation; execution is
|
|
13
|
+
untouched (DO NOT CHANGE contract).
|
|
14
|
+
"""
|
|
15
|
+
from typing import Callable, List, Optional
|
|
16
|
+
|
|
17
|
+
from aether.tui import AETHER_ORANGE
|
|
18
|
+
|
|
19
|
+
from textual import work
|
|
20
|
+
from textual.app import ComposeResult
|
|
21
|
+
from textual.containers import Vertical, VerticalScroll
|
|
22
|
+
from textual.widgets import Input, Static
|
|
23
|
+
|
|
24
|
+
_SLASH_PREFIX = "/"
|
|
25
|
+
SPINNER = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _params_str(params: dict) -> str:
|
|
29
|
+
return ", ".join(f"{k}={v!r}" for k, v in (params or {}).items())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RunBlock(Static):
|
|
33
|
+
"""One objective's live block: thinking → plan → steps → verdict.
|
|
34
|
+
|
|
35
|
+
All mutation happens on the UI thread; `tick()` advances the spinner
|
|
36
|
+
while anything is in flight.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, objective: str, **kwargs) -> None:
|
|
40
|
+
super().__init__(**kwargs)
|
|
41
|
+
self.objective = objective
|
|
42
|
+
self.phase = "thinking" # thinking | running | finished
|
|
43
|
+
self.plan_source = ""
|
|
44
|
+
self.steps: List[dict] = []
|
|
45
|
+
self.verdict = ""
|
|
46
|
+
self._spin = 0
|
|
47
|
+
self._render_now()
|
|
48
|
+
|
|
49
|
+
# -- events from the worker (already marshalled to the UI thread) --
|
|
50
|
+
def on_step(self, phase: str, data: dict) -> None:
|
|
51
|
+
if phase == "plan":
|
|
52
|
+
self.phase = "running"
|
|
53
|
+
self.plan_source = data.get("source", "?")
|
|
54
|
+
self.steps = [
|
|
55
|
+
{"tool": a.get("tool", "?"),
|
|
56
|
+
"params": dict(a.get("params", {})),
|
|
57
|
+
"status": "pending", "secs": None, "error": ""}
|
|
58
|
+
for a in data.get("plan", [])]
|
|
59
|
+
elif phase == "start" and self.steps:
|
|
60
|
+
i = data.get("index", 1) - 1
|
|
61
|
+
if 0 <= i < len(self.steps):
|
|
62
|
+
self.steps[i]["status"] = "running"
|
|
63
|
+
elif phase == "done" and self.steps:
|
|
64
|
+
i = data.get("index", 1) - 1
|
|
65
|
+
entry = data.get("entry", {})
|
|
66
|
+
if 0 <= i < len(self.steps):
|
|
67
|
+
step = self.steps[i]
|
|
68
|
+
step["status"] = "done" if entry.get("success") else "failed"
|
|
69
|
+
step["secs"] = entry.get("duration_ms", 0) / 1000.0
|
|
70
|
+
step["error"] = entry.get("error", "")
|
|
71
|
+
self._render_now()
|
|
72
|
+
|
|
73
|
+
def finish(self, result: dict) -> None:
|
|
74
|
+
self.phase = "finished"
|
|
75
|
+
status = result.get("status", "?")
|
|
76
|
+
color = "green" if status in ("ok", "success", "SUCCESS",
|
|
77
|
+
"completed") else "yellow"
|
|
78
|
+
if self.steps:
|
|
79
|
+
done = sum(1 for s in self.steps if s["status"] == "done")
|
|
80
|
+
total_s = sum(s["secs"] or 0.0 for s in self.steps)
|
|
81
|
+
detail = f"{done}/{len(self.steps)} steps · {total_s:.1f}s total"
|
|
82
|
+
else:
|
|
83
|
+
# Legacy path (execute_fn without on_step): backfill from result.
|
|
84
|
+
self.plan_source = result.get("plan_source", "?")
|
|
85
|
+
self.steps = [
|
|
86
|
+
{"tool": e.get("tool", "?"), "params": {},
|
|
87
|
+
"status": "done" if e.get("success") else "failed",
|
|
88
|
+
"secs": e.get("duration_ms", 0) / 1000.0,
|
|
89
|
+
"error": e.get("error", "")}
|
|
90
|
+
for e in result.get("actions", [])]
|
|
91
|
+
detail = f"{len(self.steps)} step(s)"
|
|
92
|
+
self.verdict = (f"[{color}]status: {status}[/{color}] "
|
|
93
|
+
f"[dim]· {detail}[/dim]")
|
|
94
|
+
self._render_now()
|
|
95
|
+
|
|
96
|
+
def fail(self, message: str) -> None:
|
|
97
|
+
self.phase = "finished"
|
|
98
|
+
self.verdict = f"[red]\\[FAIL][/red] {message}"
|
|
99
|
+
self._render_now()
|
|
100
|
+
|
|
101
|
+
def tick(self) -> bool:
|
|
102
|
+
"""Advance the spinner. Returns True while still animating."""
|
|
103
|
+
if self.phase == "finished":
|
|
104
|
+
return False
|
|
105
|
+
self._spin = (self._spin + 1) % len(SPINNER)
|
|
106
|
+
self._render_now()
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
# -- rendering --
|
|
110
|
+
def _thought_line(self) -> str:
|
|
111
|
+
chain = " → ".join(s["tool"] for s in self.steps)
|
|
112
|
+
return (f" [{AETHER_ORANGE}]\\[PLAN][/{AETHER_ORANGE}] "
|
|
113
|
+
f"{self.plan_source} · {len(self.steps)} step(s)"
|
|
114
|
+
f" [dim]{chain}[/dim]")
|
|
115
|
+
|
|
116
|
+
def _step_line(self, step: dict) -> str:
|
|
117
|
+
call = f"{step['tool']}({_params_str(step['params'])})"
|
|
118
|
+
status = step["status"]
|
|
119
|
+
if status == "pending":
|
|
120
|
+
return f" [dim]○ {call}[/dim]"
|
|
121
|
+
if status == "running":
|
|
122
|
+
frame = SPINNER[self._spin]
|
|
123
|
+
return (f" [{AETHER_ORANGE}]{frame}[/{AETHER_ORANGE}] {call} "
|
|
124
|
+
f"[dim]running...[/dim]")
|
|
125
|
+
secs = (f" [dim]({step['secs']:.1f}s)[/dim]"
|
|
126
|
+
if step["secs"] is not None else "")
|
|
127
|
+
if status == "done":
|
|
128
|
+
return f" [green]●[/green] {call}{secs}"
|
|
129
|
+
return (f" [red]●[/red] {call}{secs} "
|
|
130
|
+
f"[red]{step.get('error', '')}[/red]")
|
|
131
|
+
|
|
132
|
+
def _render_now(self) -> None:
|
|
133
|
+
lines = [f"[bold {AETHER_ORANGE}]objective>[/bold {AETHER_ORANGE}] "
|
|
134
|
+
f"{self.objective}", ""]
|
|
135
|
+
if self.phase == "thinking":
|
|
136
|
+
frame = SPINNER[self._spin]
|
|
137
|
+
lines.append(f" [{AETHER_ORANGE}]{frame}[/{AETHER_ORANGE}] "
|
|
138
|
+
f"[dim]thinking...[/dim]")
|
|
139
|
+
else:
|
|
140
|
+
lines.append(self._thought_line())
|
|
141
|
+
if self.steps:
|
|
142
|
+
lines.append("")
|
|
143
|
+
lines.extend(self._step_line(s) for s in self.steps)
|
|
144
|
+
if self.verdict:
|
|
145
|
+
lines.extend(["", f" {self.verdict}"])
|
|
146
|
+
self.update("\n".join(lines))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class BootBlock(Static):
|
|
150
|
+
"""Animated one-shot block for the hardware bootstrap."""
|
|
151
|
+
|
|
152
|
+
def __init__(self, **kwargs) -> None:
|
|
153
|
+
super().__init__(**kwargs)
|
|
154
|
+
self.done = False
|
|
155
|
+
self._spin = 0
|
|
156
|
+
self._render_now()
|
|
157
|
+
|
|
158
|
+
def tick(self) -> bool:
|
|
159
|
+
if self.done:
|
|
160
|
+
return False
|
|
161
|
+
self._spin = (self._spin + 1) % len(SPINNER)
|
|
162
|
+
self._render_now()
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
def _render_now(self) -> None:
|
|
166
|
+
frame = SPINNER[self._spin]
|
|
167
|
+
self.update(f" [{AETHER_ORANGE}]{frame}[/{AETHER_ORANGE}] "
|
|
168
|
+
f"[dim]discovering hardware + adapters...[/dim]")
|
|
169
|
+
|
|
170
|
+
def finish(self, lines: List[str]) -> None:
|
|
171
|
+
self.done = True
|
|
172
|
+
self.update("\n".join(lines))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class AgentWidget(Vertical):
|
|
176
|
+
"""Objective REPL rendered as a TUI pane."""
|
|
177
|
+
|
|
178
|
+
DEFAULT_CSS = """
|
|
179
|
+
AgentWidget { background: ansi_default; }
|
|
180
|
+
AgentWidget #agent-log { height: 1fr; border: none;
|
|
181
|
+
background: ansi_default; padding: 0 1; }
|
|
182
|
+
AgentWidget #agent-log > Static { margin-bottom: 1;
|
|
183
|
+
background: ansi_default; }
|
|
184
|
+
AgentWidget #agent-input { border: round $primary 50%;
|
|
185
|
+
background: ansi_default; padding: 0 1;
|
|
186
|
+
margin-top: 1; }
|
|
187
|
+
AgentWidget #agent-input:focus { border: round $primary; }
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def __init__(self, bootstrap_fn: Optional[Callable] = None,
|
|
191
|
+
execute_fn: Optional[Callable] = None, **kwargs) -> None:
|
|
192
|
+
"""bootstrap_fn/execute_fn are injectable for tests; the defaults are
|
|
193
|
+
the production agent pipeline. An injected execute_fn may accept
|
|
194
|
+
(objective, state) or (objective, state, on_step)."""
|
|
195
|
+
super().__init__(**kwargs)
|
|
196
|
+
self._bootstrap_fn = bootstrap_fn
|
|
197
|
+
self._execute_fn = execute_fn
|
|
198
|
+
self._agent_state: Optional[dict] = None
|
|
199
|
+
self._active_blocks: List = [] # blocks still animating
|
|
200
|
+
|
|
201
|
+
# -- layout --
|
|
202
|
+
def compose(self) -> ComposeResult:
|
|
203
|
+
yield VerticalScroll(id="agent-log")
|
|
204
|
+
yield Input(placeholder="objective> (or / for commands)",
|
|
205
|
+
id="agent-input")
|
|
206
|
+
|
|
207
|
+
def on_mount(self) -> None:
|
|
208
|
+
self._log("[bold]AETHER Agent[/bold]\n"
|
|
209
|
+
"[dim]Type an objective in plain English. "
|
|
210
|
+
"First objective boots the hardware registry.[/dim]")
|
|
211
|
+
self.set_interval(0.12, self._animate)
|
|
212
|
+
|
|
213
|
+
def focus_input(self) -> None:
|
|
214
|
+
self.query_one("#agent-input", Input).focus()
|
|
215
|
+
|
|
216
|
+
# -- log helpers --
|
|
217
|
+
def _scroll(self) -> VerticalScroll:
|
|
218
|
+
return self.query_one("#agent-log", VerticalScroll)
|
|
219
|
+
|
|
220
|
+
def _log(self, markup: str) -> None:
|
|
221
|
+
log = self._scroll()
|
|
222
|
+
log.mount(Static(markup))
|
|
223
|
+
log.scroll_end(animate=False)
|
|
224
|
+
|
|
225
|
+
def _mount_block(self, block) -> None:
|
|
226
|
+
log = self._scroll()
|
|
227
|
+
log.mount(block)
|
|
228
|
+
self._active_blocks.append(block)
|
|
229
|
+
log.scroll_end(animate=False)
|
|
230
|
+
|
|
231
|
+
def _animate(self) -> None:
|
|
232
|
+
if not self._active_blocks:
|
|
233
|
+
return
|
|
234
|
+
self._active_blocks = [b for b in self._active_blocks if b.tick()]
|
|
235
|
+
self._scroll().scroll_end(animate=False)
|
|
236
|
+
|
|
237
|
+
def clear_log(self) -> None:
|
|
238
|
+
self._scroll().remove_children()
|
|
239
|
+
self._active_blocks = []
|
|
240
|
+
|
|
241
|
+
def log_text(self) -> str:
|
|
242
|
+
"""Plain text of every block (used by /help and tests)."""
|
|
243
|
+
return "\n".join(str(w.render()) for w in self._scroll().children
|
|
244
|
+
if isinstance(w, Static))
|
|
245
|
+
|
|
246
|
+
# -- input handling --
|
|
247
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
248
|
+
text = event.value.strip()
|
|
249
|
+
event.input.value = ""
|
|
250
|
+
if not text:
|
|
251
|
+
return
|
|
252
|
+
if text.startswith(_SLASH_PREFIX):
|
|
253
|
+
self.app.run_slash_command(text)
|
|
254
|
+
return
|
|
255
|
+
block = RunBlock(text)
|
|
256
|
+
self._mount_block(block)
|
|
257
|
+
self.run_objective(text, block)
|
|
258
|
+
|
|
259
|
+
# -- objective pipeline (thread worker: hardware + LLM latency) --
|
|
260
|
+
@work(thread=True, exclusive=True, group="agent-objective")
|
|
261
|
+
def run_objective(self, objective: str, block: RunBlock) -> None:
|
|
262
|
+
app = self.app
|
|
263
|
+
try:
|
|
264
|
+
state = self._ensure_agent_state()
|
|
265
|
+
app.call_from_thread(app.set_busy,
|
|
266
|
+
"planning objective with Claude...")
|
|
267
|
+
|
|
268
|
+
def on_step(phase: str, data: dict) -> None:
|
|
269
|
+
app.call_from_thread(block.on_step, phase, data)
|
|
270
|
+
|
|
271
|
+
execute = self._execute_fn or self._default_execute
|
|
272
|
+
try:
|
|
273
|
+
result = execute(objective, state, on_step)
|
|
274
|
+
except TypeError:
|
|
275
|
+
result = execute(objective, state) # legacy 2-arg fake
|
|
276
|
+
app.call_from_thread(block.finish, result)
|
|
277
|
+
app.call_from_thread(app.count_action)
|
|
278
|
+
except Exception as e: # noqa: BLE001 — surface, never crash the TUI
|
|
279
|
+
app.call_from_thread(block.fail, f"{type(e).__name__}: {e}")
|
|
280
|
+
finally:
|
|
281
|
+
app.call_from_thread(app.set_busy, "")
|
|
282
|
+
|
|
283
|
+
def _ensure_agent_state(self) -> dict:
|
|
284
|
+
if self._agent_state is None:
|
|
285
|
+
app = self.app
|
|
286
|
+
app.call_from_thread(app.set_busy, "booting hardware registry...")
|
|
287
|
+
boot = BootBlock()
|
|
288
|
+
app.call_from_thread(self._mount_block, boot)
|
|
289
|
+
try:
|
|
290
|
+
bootstrap = self._bootstrap_fn or self._default_bootstrap
|
|
291
|
+
self._agent_state = bootstrap()
|
|
292
|
+
except Exception:
|
|
293
|
+
app.call_from_thread(
|
|
294
|
+
boot.finish, [" [red]hardware bootstrap failed[/red]"])
|
|
295
|
+
raise
|
|
296
|
+
app.call_from_thread(app.on_agent_state_ready, self._agent_state)
|
|
297
|
+
app.call_from_thread(
|
|
298
|
+
boot.finish, _adapter_detail_lines(self._agent_state))
|
|
299
|
+
return self._agent_state
|
|
300
|
+
|
|
301
|
+
@staticmethod
|
|
302
|
+
def _default_bootstrap() -> dict:
|
|
303
|
+
from aether.app import bootstrap_agent_state_for_fix
|
|
304
|
+
return bootstrap_agent_state_for_fix()
|
|
305
|
+
|
|
306
|
+
@staticmethod
|
|
307
|
+
def _default_execute(objective: str, state: dict, on_step=None) -> dict:
|
|
308
|
+
from aether.app import _execute_objective
|
|
309
|
+
return _execute_objective(
|
|
310
|
+
objective, state["registry"], state["planner"],
|
|
311
|
+
state["corrector"], state["parser"], state["manifest"],
|
|
312
|
+
recent_memory=[], available=state["available"], on_step=on_step)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _adapter_detail_lines(state: dict) -> List[str]:
|
|
316
|
+
"""Boot-time adapter detail: name, role, tool count, address, GPIO.
|
|
317
|
+
Best effort — never raises."""
|
|
318
|
+
lines: List[str] = []
|
|
319
|
+
try:
|
|
320
|
+
from aether.adapters import (get_active_adapters, get_adapter_gpio_pins,
|
|
321
|
+
get_last_detected, get_primary_adapter,
|
|
322
|
+
get_tool_origin)
|
|
323
|
+
active = get_active_adapters()
|
|
324
|
+
addr_of = {d["adapter"]: d.get("address") for d in get_last_detected()}
|
|
325
|
+
if active:
|
|
326
|
+
primary = get_primary_adapter()
|
|
327
|
+
counts: dict = {}
|
|
328
|
+
for _tool, adp in get_tool_origin().items():
|
|
329
|
+
counts[adp] = counts.get(adp, 0) + 1
|
|
330
|
+
for name in active:
|
|
331
|
+
lines.append(_adapter_line(
|
|
332
|
+
name, name == primary, counts.get(name, 0),
|
|
333
|
+
addr_of.get(name), get_adapter_gpio_pins(name)))
|
|
334
|
+
return lines
|
|
335
|
+
adapter_id = state.get("adapter_id")
|
|
336
|
+
if adapter_id:
|
|
337
|
+
tools = len(state.get("manifest", {}).get("adapter_tools", []))
|
|
338
|
+
lines.append(_adapter_line(
|
|
339
|
+
adapter_id, True, tools, addr_of.get(adapter_id),
|
|
340
|
+
get_adapter_gpio_pins(adapter_id)))
|
|
341
|
+
return lines
|
|
342
|
+
except Exception:
|
|
343
|
+
pass
|
|
344
|
+
if not lines:
|
|
345
|
+
lines.append(f" [dim]● registry ready "
|
|
346
|
+
f"({len(state.get('available', []))} tools)[/dim]")
|
|
347
|
+
return lines
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _adapter_line(name: str, primary: bool, tools: int, address,
|
|
351
|
+
gpio_pins) -> str:
|
|
352
|
+
role = ("[green]primary[/green]" if primary
|
|
353
|
+
else f"[{AETHER_ORANGE}]namespaced[/{AETHER_ORANGE}]")
|
|
354
|
+
gpio = ", ".join(str(p) for p in (gpio_pins or [])) or "none (I2C only)"
|
|
355
|
+
addr = address or "vendor lib"
|
|
356
|
+
return (f" [green]●[/green] [bold]{name}[/bold] — {role} · "
|
|
357
|
+
f"{tools} tools · addr {addr} · [dim]GPIO {gpio}[/dim]")
|
|
@@ -1,9 +1,32 @@
|
|
|
1
|
-
"""Shared TUI widgets: first-run welcome
|
|
1
|
+
"""Shared TUI widgets: mascot mark, first-run welcome, phase-2 placeholders."""
|
|
2
2
|
from textual.app import ComposeResult
|
|
3
3
|
from textual.containers import Horizontal, Vertical
|
|
4
4
|
from textual.widgets import Button, Static
|
|
5
5
|
|
|
6
6
|
from aether import __version__
|
|
7
|
+
from aether.tui import AETHER_ORANGE
|
|
8
|
+
|
|
9
|
+
# Compact rendition of the AETHER banner mascot (the robot face) for the
|
|
10
|
+
# bottom-left corner of the TUI.
|
|
11
|
+
_MASCOT_ART = (
|
|
12
|
+
f"[{AETHER_ORANGE}]▄████▄ ▄████▄[/{AETHER_ORANGE}]\n"
|
|
13
|
+
f"[{AETHER_ORANGE}]█████████████[/{AETHER_ORANGE}]\n"
|
|
14
|
+
f"[dim]█▀▀[/dim][{AETHER_ORANGE}]███████[/{AETHER_ORANGE}][dim]▀▀█[/dim]\n"
|
|
15
|
+
f"[{AETHER_ORANGE}]▀████▀ ▀████▀[/{AETHER_ORANGE}]\n"
|
|
16
|
+
f"[dim] ━━ AETHER ━━[/dim]"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MascotWidget(Static):
|
|
21
|
+
"""The AETHER robot face, parked bottom-left under the sidebar."""
|
|
22
|
+
|
|
23
|
+
DEFAULT_CSS = """
|
|
24
|
+
MascotWidget { height: 5; background: ansi_default; padding: 0 1;
|
|
25
|
+
color: $primary; }
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, **kwargs) -> None:
|
|
29
|
+
super().__init__(_MASCOT_ART, **kwargs)
|
|
7
30
|
|
|
8
31
|
|
|
9
32
|
class WelcomeWidget(Vertical):
|