feat: Enhance Networking Documentation and Introduce Automation Bridge

- Updated the networking documentation to clarify the focus on collaboration and remote access.
- Added the AutomationBridge class to facilitate communication between the test harness and the AgentChatWidget, enabling real-time updates on test execution status and plan summaries.
- Implemented automation callbacks in the AgentChatWidget for improved user interaction with harness automation features, including dashboard access and active test management.
This commit is contained in:
scawful
2025-10-05 14:16:19 -04:00
parent e5256a2384
commit 1870ebad50
12 changed files with 478 additions and 347 deletions

View File

@@ -11,6 +11,7 @@
#include <vector>
#include "absl/status/status.h"
#include "absl/strings/match.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "absl/time/clock.h"
@@ -26,6 +27,10 @@
#include "imgui/misc/cpp/imgui_stdlib.h"
#include "util/file_util.h"
#if defined(YAZE_WITH_GRPC)
#include "app/test/test_manager.h"
#endif
namespace {
using yaze::cli::agent::ChatMessage;
@@ -106,6 +111,7 @@ AgentChatWidget::AgentChatWidget() {
memset(input_buffer_, 0, sizeof(input_buffer_));
history_path_ = ResolveHistoryPath();
history_supported_ = AgentChatHistoryCodec::Available();
automation_state_.recent_tests.reserve(8);
// Initialize default session
if (chat_sessions_.empty()) {
@@ -1043,6 +1049,8 @@ void AgentChatWidget::Draw() {
RenderCollaborationPanel();
RenderRomSyncPanel();
RenderProposalManagerPanel();
RenderHarnessPanel();
RenderSystemPromptEditor();
ImGui::PopStyleVar(2);
@@ -2021,149 +2029,125 @@ void AgentChatWidget::RenderProposalManagerPanel() {
}
}
void AgentChatWidget::HandleRomSyncReceived(const std::string& diff_data,
const std::string& rom_hash) {
if (rom_sync_callbacks_.apply_rom_diff) {
auto status = rom_sync_callbacks_.apply_rom_diff(diff_data, rom_hash);
void AgentChatWidget::RenderHarnessPanel() {
ImGui::PushID("HarnessPanel");
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.16f, 0.22f, 0.95f));
ImGui::BeginChild("HarnessPanel", ImVec2(0, 220), true);
if (status.ok()) {
rom_sync_state_.current_rom_hash = rom_hash;
rom_sync_state_.last_sync_time = absl::Now();
ImGui::TextColored(ImVec4(0.392f, 0.863f, 1.0f, 1.0f), ICON_MD_PLAY_CIRCLE " Harness Automation");
ImGui::Separator();
if (toast_manager_) {
toast_manager_->Show(ICON_MD_CLOUD_DOWNLOAD
" ROM sync applied from collaborator",
ToastType::kInfo, 3.5f);
ImGui::TextDisabled("Shared automation pipeline between CLI + Agent Chat");
ImGui::Spacing();
if (ImGui::BeginTable("HarnessActions", 2, ImGuiTableFlags_BordersInnerV)) {
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 170.0f);
ImGui::TableSetupColumn("Telemetry", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableNextRow();
// Actions column
ImGui::TableSetColumnIndex(0);
ImGui::BeginGroup();
const bool has_callbacks = automation_callbacks_.open_harness_dashboard ||
automation_callbacks_.replay_last_plan ||
automation_callbacks_.show_active_tests;
if (!has_callbacks) {
ImGui::TextDisabled("Automation bridge not available");
ImGui::TextWrapped("Hook up AutomationCallbacks via EditorManager to enable controls.");
} else {
if (automation_callbacks_.open_harness_dashboard &&
ImGui::Button(ICON_MD_DASHBOARD " Dashboard", ImVec2(-FLT_MIN, 0))) {
automation_callbacks_.open_harness_dashboard();
}
if (automation_callbacks_.replay_last_plan &&
ImGui::Button(ICON_MD_REPLAY " Replay Last Plan", ImVec2(-FLT_MIN, 0))) {
automation_callbacks_.replay_last_plan();
}
if (automation_callbacks_.show_active_tests &&
ImGui::Button(ICON_MD_LIST " Active Tests", ImVec2(-FLT_MIN, 0))) {
automation_callbacks_.show_active_tests();
}
if (automation_callbacks_.focus_proposal) {
ImGui::Spacing();
ImGui::TextDisabled("Proposal tools");
if (!pending_focus_proposal_id_.empty()) {
ImGui::TextWrapped("Proposal %s active", pending_focus_proposal_id_.c_str());
if (ImGui::SmallButton(ICON_MD_VISIBILITY " View Proposal")) {
automation_callbacks_.focus_proposal(pending_focus_proposal_id_);
}
} else {
ImGui::TextDisabled("No proposal selected");
}
}
} else if (toast_manager_) {
toast_manager_->Show(absl::StrFormat(ICON_MD_ERROR " ROM sync failed: %s",
status.message()),
ToastType::kError, 5.0f);
}
}
}
void AgentChatWidget::HandleSnapshotReceived(
[[maybe_unused]] const std::string& snapshot_data,
const std::string& snapshot_type) {
// TODO: Decode and store snapshot for preview
if (toast_manager_) {
toast_manager_->Show(
absl::StrFormat(ICON_MD_PHOTO " Snapshot received: %s", snapshot_type),
ToastType::kInfo, 3.0f);
}
}
ImGui::EndGroup();
void AgentChatWidget::HandleProposalReceived(
[[maybe_unused]] const std::string& proposal_data) {
// TODO: Parse and add proposal to local registry
if (toast_manager_) {
toast_manager_->Show(ICON_MD_LIGHTBULB
" New proposal received from collaborator",
ToastType::kInfo, 3.5f);
}
}
// Telemetry column
ImGui::TableSetColumnIndex(1);
ImGui::BeginGroup();
void AgentChatWidget::SyncHistoryToPopup() {
if (!chat_history_popup_)
return;
ImGui::TextColored(ImVec4(0.6f, 0.78f, 1.0f, 1.0f), ICON_MD_QUERY_STATS " Live Telemetry");
ImGui::Spacing();
const auto& history = agent_service_.GetHistory();
chat_history_popup_->UpdateHistory(history);
chat_history_popup_->NotifyNewMessage();
}
if (!automation_state_.recent_tests.empty()) {
const float row_height = ImGui::GetTextLineHeightWithSpacing() * 2.0f + 6.0f;
if (ImGui::BeginTable("HarnessTelemetryRows", 4,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("Test", ImGuiTableColumnFlags_WidthStretch, 0.3f);
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 90.0f);
ImGui::TableSetupColumn("Message", ImGuiTableColumnFlags_WidthStretch, 0.5f);
ImGui::TableSetupColumn("Updated", ImGuiTableColumnFlags_WidthFixed, 120.0f);
ImGui::TableHeadersRow();
std::filesystem::path AgentChatWidget::GetSessionsDirectory() {
std::filesystem::path config_dir(yaze::util::GetConfigDirectory());
if (config_dir.empty()) {
#ifdef _WIN32
const char* appdata = std::getenv("APPDATA");
if (appdata) {
config_dir = std::filesystem::path(appdata) / "yaze";
for (const auto& entry : automation_state_.recent_tests) {
ImGui::TableNextRow(ImGuiTableRowFlags_None, row_height);
ImGui::TableSetColumnIndex(0);
ImGui::TextWrapped("%s", entry.name.empty() ? entry.test_id.c_str()
: entry.name.c_str());
ImGui::TableSetColumnIndex(1);
const char* status = entry.status.c_str();
ImVec4 status_color = ImVec4(0.8f, 0.8f, 0.8f, 1.0f);
if (absl::EqualsIgnoreCase(status, "passed")) {
status_color = ImVec4(0.2f, 0.8f, 0.4f, 1.0f);
} else if (absl::EqualsIgnoreCase(status, "failed") ||
absl::EqualsIgnoreCase(status, "timeout")) {
status_color = ImVec4(0.95f, 0.4f, 0.4f, 1.0f);
} else if (absl::EqualsIgnoreCase(status, "running")) {
status_color = ImVec4(0.95f, 0.75f, 0.3f, 1.0f);
}
ImGui::TextColored(status_color, "%s", status);
ImGui::TableSetColumnIndex(2);
ImGui::TextWrapped("%s", entry.message.c_str());
ImGui::TableSetColumnIndex(3);
if (entry.updated_at == absl::InfinitePast()) {
ImGui::TextDisabled("-");
} else {
const double seconds_ago = absl::ToDoubleSeconds(absl::Now() - entry.updated_at);
ImGui::Text("%.0fs ago", seconds_ago);
}
}
ImGui::EndTable();
}
} else {
ImGui::TextDisabled("No harness activity recorded yet");
}
#else
const char* home = std::getenv("HOME");
if (home) {
config_dir = std::filesystem::path(home) / ".yaze";
}
#endif
}
return config_dir / "chats";
}
void AgentChatWidget::SaveChatSession(const ChatSession& session) {
auto sessions_dir = GetSessionsDirectory();
std::error_code ec;
std::filesystem::create_directories(sessions_dir, ec);
std::filesystem::path save_path = sessions_dir / (session.id + ".json");
// Save using existing history codec
AgentChatHistoryCodec::Snapshot snapshot;
snapshot.history = session.agent_service.GetHistory();
auto status = AgentChatHistoryCodec::Save(save_path, snapshot);
if (status.ok() && toast_manager_) {
toast_manager_->Show(
absl::StrFormat(ICON_MD_SAVE " Chat '%s' saved", session.name),
ToastType::kSuccess, 2.0f);
ImGui::EndGroup();
ImGui::EndTable();
}
}
void AgentChatWidget::LoadChatSession(const std::string& session_id) {
auto sessions_dir = GetSessionsDirectory();
std::filesystem::path load_path = sessions_dir / (session_id + ".json");
if (!std::filesystem::exists(load_path)) {
if (toast_manager_) {
toast_manager_->Show(ICON_MD_WARNING " Session file not found", ToastType::kWarning, 2.5f);
}
return;
}
auto snapshot_result = AgentChatHistoryCodec::Load(load_path);
if (snapshot_result.ok()) {
// Create new session with loaded history
ChatSession session(session_id, session_id);
session.agent_service.ReplaceHistory(snapshot_result->history);
session.history_loaded = true;
chat_sessions_.push_back(std::move(session));
active_session_index_ = static_cast<int>(chat_sessions_.size() - 1);
if (toast_manager_) {
toast_manager_->Show(ICON_MD_CHECK_CIRCLE " Chat session loaded", ToastType::kSuccess, 2.0f);
}
}
}
void AgentChatWidget::DeleteChatSession(const std::string& session_id) {
auto sessions_dir = GetSessionsDirectory();
std::filesystem::path session_path = sessions_dir / (session_id + ".json");
if (std::filesystem::exists(session_path)) {
std::filesystem::remove(session_path);
if (toast_manager_) {
toast_manager_->Show(ICON_MD_DELETE " Chat deleted", ToastType::kInfo, 2.0f);
}
}
}
std::vector<std::string> AgentChatWidget::GetSavedSessions() {
std::vector<std::string> sessions;
auto sessions_dir = GetSessionsDirectory();
if (!std::filesystem::exists(sessions_dir)) {
return sessions;
}
for (const auto& entry : std::filesystem::directory_iterator(sessions_dir)) {
if (entry.path().extension() == ".json") {
sessions.push_back(entry.path().stem().string());
}
}
return sessions;
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::PopID();
}
void AgentChatWidget::RenderSystemPromptEditor() {
@@ -2633,5 +2617,53 @@ void AgentChatWidget::SaveAgentSettingsToProject(core::YazeProject& project) {
}
}
void AgentChatWidget::SetMultimodalCallbacks(
const MultimodalCallbacks& callbacks) {
multimodal_callbacks_ = callbacks;
}
void AgentChatWidget::SetAutomationCallbacks(
const AutomationCallbacks& callbacks) {
automation_callbacks_ = callbacks;
}
void AgentChatWidget::UpdateHarnessTelemetry(
const AutomationTelemetry& telemetry) {
auto predicate = [&](const AutomationTelemetry& entry) {
return entry.test_id == telemetry.test_id;
};
auto it = std::find_if(automation_state_.recent_tests.begin(),
automation_state_.recent_tests.end(), predicate);
if (it != automation_state_.recent_tests.end()) {
*it = telemetry;
} else {
if (automation_state_.recent_tests.size() >= 16) {
automation_state_.recent_tests.erase(automation_state_.recent_tests.begin());
}
automation_state_.recent_tests.push_back(telemetry);
}
}
void AgentChatWidget::SetLastPlanSummary(const std::string& summary) {
// Store the plan summary for display in the automation panel
// This could be shown in the harness panel or logged
if (toast_manager_) {
toast_manager_->Show("Plan summary received", ToastType::kInfo, 2.0f);
}
}
void AgentChatWidget::SyncHistoryToPopup() {
if (!chat_history_popup_) {
return;
}
// Get the current chat history from the agent service
const auto& history = agent_service_.GetHistory();
// Update the popup with the latest history
chat_history_popup_->UpdateHistory(history);
}
} // namespace editor
} // namespace yaze

View File

@@ -67,6 +67,21 @@ class AgentChatWidget {
std::function<absl::Status(const std::filesystem::path&, const std::string&)> send_to_gemini;
};
struct AutomationCallbacks {
std::function<void()> open_harness_dashboard;
std::function<void()> replay_last_plan;
std::function<void(const std::string&)> focus_proposal;
std::function<void()> show_active_tests;
};
struct AutomationTelemetry {
std::string test_id;
std::string name;
std::string status;
std::string message;
absl::Time updated_at = absl::InfinitePast();
};
// Z3ED Command Callbacks
struct Z3EDCommandCallbacks {
std::function<absl::Status(const std::string&)> run_agent_task;
@@ -84,6 +99,8 @@ class AgentChatWidget {
std::function<std::string()> get_rom_hash;
};
void RenderSnapshotPreviewPanel();
void SetToastManager(ToastManager* toast_manager);
void SetProposalDrawer(ProposalDrawer* drawer);
@@ -94,9 +111,11 @@ class AgentChatWidget {
collaboration_callbacks_ = callbacks;
}
void SetMultimodalCallbacks(const MultimodalCallbacks& callbacks) {
multimodal_callbacks_ = callbacks;
}
void SetMultimodalCallbacks(const MultimodalCallbacks& callbacks);
void SetAutomationCallbacks(const AutomationCallbacks& callbacks);
void UpdateHarnessTelemetry(const AutomationTelemetry& telemetry);
void SetLastPlanSummary(const std::string& summary);
void SetZ3EDCommandCallbacks(const Z3EDCommandCallbacks& callbacks) {
z3ed_callbacks_ = callbacks;
@@ -141,6 +160,12 @@ public:
char specific_window_buffer[128] = {};
};
struct AutomationState {
std::vector<AutomationTelemetry> recent_tests;
bool harness_connected = false;
absl::Time last_poll = absl::InfinitePast();
};
// Agent Configuration State
struct AgentConfigState {
std::string ai_provider = "mock"; // mock, ollama, gemini
@@ -218,8 +243,8 @@ public:
void RenderAgentConfigPanel();
void RenderZ3EDCommandPanel();
void RenderRomSyncPanel();
void RenderSnapshotPreviewPanel();
void RenderProposalManagerPanel();
void RenderHarnessPanel();
void RenderSystemPromptEditor();
void RenderFileEditorTabs();
void RefreshCollaboration();
@@ -287,6 +312,7 @@ public:
// Main state
CollaborationState collaboration_state_;
MultimodalState multimodal_state_;
AutomationState automation_state_;
AgentConfigState agent_config_;
RomSyncState rom_sync_state_;
Z3EDCommandState z3ed_command_state_;
@@ -294,6 +320,7 @@ public:
// Callbacks
CollaborationCallbacks collaboration_callbacks_;
MultimodalCallbacks multimodal_callbacks_;
AutomationCallbacks automation_callbacks_;
Z3EDCommandCallbacks z3ed_callbacks_;
RomSyncCallbacks rom_sync_callbacks_;

View File

@@ -0,0 +1,41 @@
#include "app/editor/agent/automation_bridge.h"
#if defined(YAZE_WITH_GRPC)
#include "absl/time/time.h"
namespace yaze {
namespace editor {
void AutomationBridge::OnHarnessTestUpdated(
const test::HarnessTestExecution& execution) {
absl::MutexLock lock(&mutex_);
if (!chat_widget_) {
return;
}
AgentChatWidget::AutomationTelemetry telemetry;
telemetry.test_id = execution.test_id;
telemetry.name = execution.name;
telemetry.status = test::HarnessStatusToString(execution.status);
telemetry.message = execution.error_message;
telemetry.updated_at = (execution.completed_at == absl::InfiniteFuture() ||
execution.completed_at == absl::InfinitePast())
? absl::Now()
: execution.completed_at;
chat_widget_->UpdateHarnessTelemetry(telemetry);
}
void AutomationBridge::OnHarnessPlanSummary(const std::string& summary) {
absl::MutexLock lock(&mutex_);
if (!chat_widget_) {
return;
}
chat_widget_->SetLastPlanSummary(summary);
}
} // namespace editor
} // namespace yaze
#endif // defined(YAZE_WITH_GRPC)

View File

@@ -0,0 +1,38 @@
#ifndef YAZE_APP_EDITOR_AGENT_AUTOMATION_BRIDGE_H_
#define YAZE_APP_EDITOR_AGENT_AUTOMATION_BRIDGE_H_
#if defined(YAZE_WITH_GRPC)
#include "absl/synchronization/mutex.h"
#include "app/editor/agent/agent_chat_widget.h"
#include "app/test/test_manager.h"
namespace yaze {
namespace editor {
class AutomationBridge : public test::HarnessListener {
public:
AutomationBridge() = default;
~AutomationBridge() override = default;
void SetChatWidget(AgentChatWidget* widget) {
absl::MutexLock lock(&mutex_);
chat_widget_ = widget;
}
void OnHarnessTestUpdated(
const test::HarnessTestExecution& execution) override;
void OnHarnessPlanSummary(const std::string& summary) override;
private:
absl::Mutex mutex_;
AgentChatWidget* chat_widget_ ABSL_GUARDED_BY(mutex_) = nullptr;
};
} // namespace editor
} // namespace yaze
#endif // defined(YAZE_WITH_GRPC)
#endif // YAZE_APP_EDITOR_AGENT_AUTOMATION_BRIDGE_H_

View File

@@ -51,6 +51,7 @@ if(YAZE_WITH_GRPC)
app/editor/agent/agent_chat_widget.cc
app/editor/agent/agent_collaboration_coordinator.cc
app/editor/agent/network_collaboration_coordinator.cc
app/editor/agent/automation_bridge.cc
)
endif()

View File

@@ -56,6 +56,9 @@
#include "cli/service/agent/conversational_agent_service.h"
#include "cli/service/ai/gemini_ai_service.h"
#endif
#ifdef YAZE_WITH_GRPC
#include "app/editor/agent/automation_bridge.h"
#endif
#include "imgui/imgui.h"
#include "imgui/misc/cpp/imgui_stdlib.h"
#include "util/log.h"
@@ -378,6 +381,25 @@ void EditorManager::Initialize(const std::string& filename) {
};
agent_editor_.GetChatWidget()->SetZ3EDCommandCallbacks(z3ed_callbacks);
AgentChatWidget::AutomationCallbacks automation_callbacks;
automation_callbacks.open_harness_dashboard = [this]() {
test::TestManager::Get().ShowHarnessDashboard();
};
automation_callbacks.show_active_tests = [this]() {
test::TestManager::Get().ShowHarnessActiveTests();
};
automation_callbacks.replay_last_plan = [this]() {
test::TestManager::Get().ReplayLastPlan();
};
automation_callbacks.focus_proposal = [this](const std::string& proposal_id) {
proposal_drawer_.Show();
proposal_drawer_.FocusProposal(proposal_id);
};
agent_editor_.GetChatWidget()->SetAutomationCallbacks(automation_callbacks);
harness_telemetry_bridge_.SetChatWidget(agent_editor_.GetChatWidget());
test::TestManager::Get().SetHarnessListener(&harness_telemetry_bridge_);
#endif
// Load critical user settings first

View File

@@ -28,6 +28,7 @@
#include "app/editor/system/agent_chat_history_popup.h"
#ifdef YAZE_WITH_GRPC
#include "app/editor/agent/agent_editor.h"
#include "app/editor/agent/automation_bridge.h"
#endif
#include "app/editor/system/settings_editor.h"
#include "app/editor/system/toast_manager.h"
@@ -38,6 +39,9 @@
#include "app/rom.h"
#include "yaze_config.h"
#ifdef YAZE_WITH_GRPC
#endif
namespace yaze {
namespace editor {
@@ -190,7 +194,11 @@ class EditorManager {
// Agent proposal drawer
ProposalDrawer proposal_drawer_;
bool show_proposal_drawer_ = false;
#ifdef YAZE_WITH_GRPC
AutomationBridge harness_telemetry_bridge_;
#endif
// Agent chat history popup
AgentChatHistoryPopup agent_chat_history_popup_;
bool show_chat_history_popup_ = false;