feat: Refactor agent-related components for improved collaboration and chat management
- Moved agent chat history codec and chat widget to a dedicated agent directory for better organization. - Introduced AgentEditor class to manage chat, collaboration, and network coordination. - Updated EditorManager to utilize the new AgentEditor for handling chat and collaboration features. - Enhanced collaboration capabilities with local and network session management. - Integrated new agent collaboration coordinator for managing collaborative sessions. - Improved CMake configuration to include new agent source files and dependencies.
This commit is contained in:
344
src/app/editor/agent/agent_chat_history_codec.cc
Normal file
344
src/app/editor/agent/agent_chat_history_codec.cc
Normal file
@@ -0,0 +1,344 @@
|
||||
#include "app/editor/agent/agent_chat_history_codec.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/time/clock.h"
|
||||
#include "absl/time/time.h"
|
||||
|
||||
#if defined(YAZE_WITH_JSON)
|
||||
#include "nlohmann/json.hpp"
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace editor {
|
||||
|
||||
namespace {
|
||||
|
||||
#if defined(YAZE_WITH_JSON)
|
||||
using Json = nlohmann::json;
|
||||
|
||||
absl::Time ParseTimestamp(const Json& value) {
|
||||
if (!value.is_string()) {
|
||||
return absl::Now();
|
||||
}
|
||||
absl::Time parsed;
|
||||
if (absl::ParseTime(absl::RFC3339_full, value.get<std::string>(), &parsed,
|
||||
nullptr)) {
|
||||
return parsed;
|
||||
}
|
||||
return absl::Now();
|
||||
}
|
||||
|
||||
Json SerializeTableData(const cli::agent::ChatMessage::TableData& table) {
|
||||
Json json;
|
||||
json["headers"] = table.headers;
|
||||
json["rows"] = table.rows;
|
||||
return json;
|
||||
}
|
||||
|
||||
std::optional<cli::agent::ChatMessage::TableData> ParseTableData(
|
||||
const Json& json) {
|
||||
if (!json.is_object()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
cli::agent::ChatMessage::TableData table;
|
||||
if (json.contains("headers") && json["headers"].is_array()) {
|
||||
for (const auto& header : json["headers"]) {
|
||||
if (header.is_string()) {
|
||||
table.headers.push_back(header.get<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (json.contains("rows") && json["rows"].is_array()) {
|
||||
for (const auto& row : json["rows"]) {
|
||||
if (!row.is_array()) {
|
||||
continue;
|
||||
}
|
||||
std::vector<std::string> row_values;
|
||||
for (const auto& value : row) {
|
||||
if (value.is_string()) {
|
||||
row_values.push_back(value.get<std::string>());
|
||||
} else {
|
||||
row_values.push_back(value.dump());
|
||||
}
|
||||
}
|
||||
table.rows.push_back(std::move(row_values));
|
||||
}
|
||||
}
|
||||
|
||||
if (table.headers.empty() && table.rows.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
Json SerializeProposal(const cli::agent::ChatMessage::ProposalSummary& proposal) {
|
||||
Json json;
|
||||
json["id"] = proposal.id;
|
||||
json["change_count"] = proposal.change_count;
|
||||
json["executed_commands"] = proposal.executed_commands;
|
||||
json["sandbox_rom_path"] = proposal.sandbox_rom_path.string();
|
||||
json["proposal_json_path"] = proposal.proposal_json_path.string();
|
||||
return json;
|
||||
}
|
||||
|
||||
std::optional<cli::agent::ChatMessage::ProposalSummary> ParseProposal(
|
||||
const Json& json) {
|
||||
if (!json.is_object()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
cli::agent::ChatMessage::ProposalSummary summary;
|
||||
summary.id = json.value("id", "");
|
||||
summary.change_count = json.value("change_count", 0);
|
||||
summary.executed_commands = json.value("executed_commands", 0);
|
||||
if (json.contains("sandbox_rom_path") && json["sandbox_rom_path"].is_string()) {
|
||||
summary.sandbox_rom_path = json["sandbox_rom_path"].get<std::string>();
|
||||
}
|
||||
if (json.contains("proposal_json_path") &&
|
||||
json["proposal_json_path"].is_string()) {
|
||||
summary.proposal_json_path = json["proposal_json_path"].get<std::string>();
|
||||
}
|
||||
if (summary.id.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
#endif // YAZE_WITH_GRPC
|
||||
|
||||
} // namespace
|
||||
|
||||
bool AgentChatHistoryCodec::Available() {
|
||||
#if defined(YAZE_WITH_JSON)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
absl::StatusOr<AgentChatHistoryCodec::Snapshot> AgentChatHistoryCodec::Load(
|
||||
const std::filesystem::path& path) {
|
||||
#if defined(YAZE_WITH_JSON)
|
||||
Snapshot snapshot;
|
||||
|
||||
std::ifstream file(path);
|
||||
if (!file.good()) {
|
||||
return snapshot; // Treat missing file as empty history.
|
||||
}
|
||||
|
||||
Json json;
|
||||
try {
|
||||
file >> json;
|
||||
} catch (const std::exception& e) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("Failed to parse chat history: %s", e.what()));
|
||||
}
|
||||
|
||||
if (!json.contains("messages") || !json["messages"].is_array()) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
for (const auto& item : json["messages"]) {
|
||||
if (!item.is_object()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cli::agent::ChatMessage message;
|
||||
std::string sender = item.value("sender", "agent");
|
||||
message.sender = sender == "user"
|
||||
? cli::agent::ChatMessage::Sender::kUser
|
||||
: cli::agent::ChatMessage::Sender::kAgent;
|
||||
message.message = item.value("message", "");
|
||||
message.timestamp = ParseTimestamp(item["timestamp"]);
|
||||
|
||||
if (item.contains("json_pretty") && item["json_pretty"].is_string()) {
|
||||
message.json_pretty = item["json_pretty"].get<std::string>();
|
||||
}
|
||||
if (item.contains("table_data")) {
|
||||
message.table_data = ParseTableData(item["table_data"]);
|
||||
}
|
||||
if (item.contains("metrics") && item["metrics"].is_object()) {
|
||||
cli::agent::ChatMessage::SessionMetrics metrics;
|
||||
const auto& metrics_json = item["metrics"];
|
||||
metrics.turn_index = metrics_json.value("turn_index", 0);
|
||||
metrics.total_user_messages =
|
||||
metrics_json.value("total_user_messages", 0);
|
||||
metrics.total_agent_messages =
|
||||
metrics_json.value("total_agent_messages", 0);
|
||||
metrics.total_tool_calls = metrics_json.value("total_tool_calls", 0);
|
||||
metrics.total_commands = metrics_json.value("total_commands", 0);
|
||||
metrics.total_proposals = metrics_json.value("total_proposals", 0);
|
||||
metrics.total_elapsed_seconds =
|
||||
metrics_json.value("total_elapsed_seconds", 0.0);
|
||||
metrics.average_latency_seconds =
|
||||
metrics_json.value("average_latency_seconds", 0.0);
|
||||
message.metrics = metrics;
|
||||
}
|
||||
if (item.contains("proposal")) {
|
||||
message.proposal = ParseProposal(item["proposal"]);
|
||||
}
|
||||
|
||||
snapshot.history.push_back(std::move(message));
|
||||
}
|
||||
|
||||
if (json.contains("collaboration") && json["collaboration"].is_object()) {
|
||||
const auto& collab_json = json["collaboration"];
|
||||
snapshot.collaboration.active = collab_json.value("active", false);
|
||||
snapshot.collaboration.session_id = collab_json.value("session_id", "");
|
||||
snapshot.collaboration.session_name =
|
||||
collab_json.value("session_name", "");
|
||||
snapshot.collaboration.participants.clear();
|
||||
if (collab_json.contains("participants") &&
|
||||
collab_json["participants"].is_array()) {
|
||||
for (const auto& participant : collab_json["participants"]) {
|
||||
if (participant.is_string()) {
|
||||
snapshot.collaboration.participants.push_back(
|
||||
participant.get<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (collab_json.contains("last_synced")) {
|
||||
snapshot.collaboration.last_synced =
|
||||
ParseTimestamp(collab_json["last_synced"]);
|
||||
}
|
||||
if (snapshot.collaboration.session_name.empty() &&
|
||||
!snapshot.collaboration.session_id.empty()) {
|
||||
snapshot.collaboration.session_name =
|
||||
snapshot.collaboration.session_id;
|
||||
}
|
||||
}
|
||||
|
||||
if (json.contains("multimodal") && json["multimodal"].is_object()) {
|
||||
const auto& multimodal_json = json["multimodal"];
|
||||
if (multimodal_json.contains("last_capture_path") &&
|
||||
multimodal_json["last_capture_path"].is_string()) {
|
||||
std::string path_value =
|
||||
multimodal_json["last_capture_path"].get<std::string>();
|
||||
if (!path_value.empty()) {
|
||||
snapshot.multimodal.last_capture_path =
|
||||
std::filesystem::path(path_value);
|
||||
}
|
||||
}
|
||||
snapshot.multimodal.status_message =
|
||||
multimodal_json.value("status_message", "");
|
||||
if (multimodal_json.contains("last_updated")) {
|
||||
snapshot.multimodal.last_updated =
|
||||
ParseTimestamp(multimodal_json["last_updated"]);
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
#else
|
||||
(void)path;
|
||||
return absl::UnimplementedError(
|
||||
"Chat history persistence requires YAZE_WITH_GRPC=ON");
|
||||
#endif
|
||||
}
|
||||
|
||||
absl::Status AgentChatHistoryCodec::Save(
|
||||
const std::filesystem::path& path, const Snapshot& snapshot) {
|
||||
#if defined(YAZE_WITH_JSON)
|
||||
Json json;
|
||||
json["version"] = 3;
|
||||
json["messages"] = Json::array();
|
||||
|
||||
for (const auto& message : snapshot.history) {
|
||||
Json entry;
|
||||
entry["sender"] =
|
||||
message.sender == cli::agent::ChatMessage::Sender::kUser ? "user"
|
||||
: "agent";
|
||||
entry["message"] = message.message;
|
||||
entry["timestamp"] = absl::FormatTime(absl::RFC3339_full,
|
||||
message.timestamp,
|
||||
absl::UTCTimeZone());
|
||||
|
||||
if (message.json_pretty.has_value()) {
|
||||
entry["json_pretty"] = *message.json_pretty;
|
||||
}
|
||||
if (message.table_data.has_value()) {
|
||||
entry["table_data"] = SerializeTableData(*message.table_data);
|
||||
}
|
||||
if (message.metrics.has_value()) {
|
||||
const auto& metrics = *message.metrics;
|
||||
Json metrics_json;
|
||||
metrics_json["turn_index"] = metrics.turn_index;
|
||||
metrics_json["total_user_messages"] = metrics.total_user_messages;
|
||||
metrics_json["total_agent_messages"] = metrics.total_agent_messages;
|
||||
metrics_json["total_tool_calls"] = metrics.total_tool_calls;
|
||||
metrics_json["total_commands"] = metrics.total_commands;
|
||||
metrics_json["total_proposals"] = metrics.total_proposals;
|
||||
metrics_json["total_elapsed_seconds"] = metrics.total_elapsed_seconds;
|
||||
metrics_json["average_latency_seconds"] =
|
||||
metrics.average_latency_seconds;
|
||||
entry["metrics"] = metrics_json;
|
||||
}
|
||||
if (message.proposal.has_value()) {
|
||||
entry["proposal"] = SerializeProposal(*message.proposal);
|
||||
}
|
||||
|
||||
json["messages"].push_back(std::move(entry));
|
||||
}
|
||||
|
||||
Json collab_json;
|
||||
collab_json["active"] = snapshot.collaboration.active;
|
||||
collab_json["session_id"] = snapshot.collaboration.session_id;
|
||||
collab_json["session_name"] = snapshot.collaboration.session_name;
|
||||
collab_json["participants"] = snapshot.collaboration.participants;
|
||||
if (snapshot.collaboration.last_synced != absl::InfinitePast()) {
|
||||
collab_json["last_synced"] = absl::FormatTime(
|
||||
absl::RFC3339_full, snapshot.collaboration.last_synced,
|
||||
absl::UTCTimeZone());
|
||||
}
|
||||
json["collaboration"] = std::move(collab_json);
|
||||
|
||||
Json multimodal_json;
|
||||
if (snapshot.multimodal.last_capture_path.has_value()) {
|
||||
multimodal_json["last_capture_path"] =
|
||||
snapshot.multimodal.last_capture_path->string();
|
||||
} else {
|
||||
multimodal_json["last_capture_path"] = "";
|
||||
}
|
||||
multimodal_json["status_message"] = snapshot.multimodal.status_message;
|
||||
if (snapshot.multimodal.last_updated != absl::InfinitePast()) {
|
||||
multimodal_json["last_updated"] = absl::FormatTime(
|
||||
absl::RFC3339_full, snapshot.multimodal.last_updated,
|
||||
absl::UTCTimeZone());
|
||||
}
|
||||
json["multimodal"] = std::move(multimodal_json);
|
||||
|
||||
std::error_code ec;
|
||||
auto directory = path.parent_path();
|
||||
if (!directory.empty()) {
|
||||
std::filesystem::create_directories(directory, ec);
|
||||
if (ec) {
|
||||
return absl::InternalError(absl::StrFormat(
|
||||
"Unable to create chat history directory: %s", ec.message()));
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return absl::InternalError("Cannot write chat history file");
|
||||
}
|
||||
|
||||
file << json.dump(2);
|
||||
return absl::OkStatus();
|
||||
#else
|
||||
(void)path;
|
||||
(void)snapshot;
|
||||
return absl::UnimplementedError(
|
||||
"Chat history persistence requires YAZE_WITH_GRPC=ON");
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace editor
|
||||
} // namespace yaze
|
||||
54
src/app/editor/agent/agent_chat_history_codec.h
Normal file
54
src/app/editor/agent/agent_chat_history_codec.h
Normal file
@@ -0,0 +1,54 @@
|
||||
#ifndef YAZE_APP_EDITOR_AGENT_AGENT_CHAT_HISTORY_CODEC_H_
|
||||
#define YAZE_APP_EDITOR_AGENT_AGENT_CHAT_HISTORY_CODEC_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "cli/service/agent/conversational_agent_service.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace editor {
|
||||
|
||||
// Bridges chat history persistence to optional JSON support. When the
|
||||
// application is built without gRPC/JSON support these helpers gracefully
|
||||
// degrade and report an Unimplemented status so the UI can disable
|
||||
// persistence instead of failing to compile.
|
||||
class AgentChatHistoryCodec {
|
||||
public:
|
||||
struct CollaborationState {
|
||||
bool active = false;
|
||||
std::string session_id;
|
||||
std::string session_name;
|
||||
std::vector<std::string> participants;
|
||||
absl::Time last_synced = absl::InfinitePast();
|
||||
};
|
||||
|
||||
struct MultimodalState {
|
||||
std::optional<std::filesystem::path> last_capture_path;
|
||||
std::string status_message;
|
||||
absl::Time last_updated = absl::InfinitePast();
|
||||
};
|
||||
|
||||
struct Snapshot {
|
||||
std::vector<cli::agent::ChatMessage> history;
|
||||
CollaborationState collaboration;
|
||||
MultimodalState multimodal;
|
||||
};
|
||||
|
||||
// Returns true when the codec can actually serialize / deserialize history.
|
||||
static bool Available();
|
||||
|
||||
static absl::StatusOr<Snapshot> Load(const std::filesystem::path& path);
|
||||
static absl::Status Save(const std::filesystem::path& path,
|
||||
const Snapshot& snapshot);
|
||||
};
|
||||
|
||||
} // namespace editor
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_EDITOR_AGENT_AGENT_CHAT_HISTORY_CODEC_H_
|
||||
1025
src/app/editor/agent/agent_chat_widget.cc
Normal file
1025
src/app/editor/agent/agent_chat_widget.cc
Normal file
File diff suppressed because it is too large
Load Diff
161
src/app/editor/agent/agent_chat_widget.h
Normal file
161
src/app/editor/agent/agent_chat_widget.h
Normal file
@@ -0,0 +1,161 @@
|
||||
#ifndef YAZE_APP_EDITOR_AGENT_AGENT_CHAT_WIDGET_H_
|
||||
#define YAZE_APP_EDITOR_AGENT_AGENT_CHAT_WIDGET_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "cli/service/agent/conversational_agent_service.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
class Rom;
|
||||
|
||||
namespace editor {
|
||||
|
||||
class ProposalDrawer;
|
||||
class ToastManager;
|
||||
|
||||
class AgentChatWidget {
|
||||
public:
|
||||
AgentChatWidget();
|
||||
|
||||
void Draw();
|
||||
|
||||
void SetRomContext(Rom* rom);
|
||||
|
||||
struct CollaborationCallbacks {
|
||||
struct SessionContext {
|
||||
std::string session_id;
|
||||
std::string session_name;
|
||||
std::vector<std::string> participants;
|
||||
};
|
||||
|
||||
std::function<absl::StatusOr<SessionContext>(const std::string&)> host_session;
|
||||
std::function<absl::StatusOr<SessionContext>(const std::string&)> join_session;
|
||||
std::function<absl::Status()> leave_session;
|
||||
std::function<absl::StatusOr<SessionContext>()> refresh_session;
|
||||
};
|
||||
|
||||
struct MultimodalCallbacks {
|
||||
std::function<absl::Status(std::filesystem::path*)> capture_snapshot;
|
||||
std::function<absl::Status(const std::filesystem::path&, const std::string&)> send_to_gemini;
|
||||
};
|
||||
|
||||
void SetToastManager(ToastManager* toast_manager);
|
||||
|
||||
void SetProposalDrawer(ProposalDrawer* drawer);
|
||||
|
||||
void SetCollaborationCallbacks(const CollaborationCallbacks& callbacks) {
|
||||
collaboration_callbacks_ = callbacks;
|
||||
}
|
||||
|
||||
void SetMultimodalCallbacks(const MultimodalCallbacks& callbacks) {
|
||||
multimodal_callbacks_ = callbacks;
|
||||
}
|
||||
|
||||
bool* active() { return &active_; }
|
||||
bool is_active() const { return active_; }
|
||||
void set_active(bool active) { active_ = active; }
|
||||
|
||||
public:
|
||||
enum class CollaborationMode {
|
||||
kLocal = 0, // Filesystem-based collaboration
|
||||
kNetwork = 1 // WebSocket-based collaboration
|
||||
};
|
||||
|
||||
struct CollaborationState {
|
||||
bool active = false;
|
||||
CollaborationMode mode = CollaborationMode::kLocal;
|
||||
std::string session_id;
|
||||
std::string session_name;
|
||||
std::string server_url = "ws://localhost:8765";
|
||||
bool server_connected = false;
|
||||
std::vector<std::string> participants;
|
||||
absl::Time last_synced = absl::InfinitePast();
|
||||
};
|
||||
|
||||
enum class CaptureMode {
|
||||
kFullWindow = 0,
|
||||
kActiveEditor = 1,
|
||||
kSpecificWindow = 2
|
||||
};
|
||||
|
||||
struct MultimodalState {
|
||||
std::optional<std::filesystem::path> last_capture_path;
|
||||
std::string status_message;
|
||||
absl::Time last_updated = absl::InfinitePast();
|
||||
CaptureMode capture_mode = CaptureMode::kActiveEditor;
|
||||
char specific_window_buffer[128] = {};
|
||||
};
|
||||
|
||||
// Accessors for capture settings
|
||||
CaptureMode capture_mode() const { return multimodal_state_.capture_mode; }
|
||||
const char* specific_window_name() const {
|
||||
return multimodal_state_.specific_window_buffer;
|
||||
}
|
||||
|
||||
// Collaboration history management (public so EditorManager can call them)
|
||||
void SwitchToSharedHistory(const std::string& session_id);
|
||||
void SwitchToLocalHistory();
|
||||
|
||||
private:
|
||||
void EnsureHistoryLoaded();
|
||||
void PersistHistory();
|
||||
void RenderHistory();
|
||||
void RenderMessage(const cli::agent::ChatMessage& msg, int index);
|
||||
void RenderProposalQuickActions(const cli::agent::ChatMessage& msg,
|
||||
int index);
|
||||
void RenderInputBox();
|
||||
void HandleAgentResponse(
|
||||
const absl::StatusOr<cli::agent::ChatMessage>& response);
|
||||
int CountKnownProposals() const;
|
||||
void FocusProposalDrawer(const std::string& proposal_id);
|
||||
void NotifyProposalCreated(const cli::agent::ChatMessage& msg,
|
||||
int new_total_proposals);
|
||||
void RenderCollaborationPanel();
|
||||
void RenderMultimodalPanel();
|
||||
void RefreshCollaboration();
|
||||
void ApplyCollaborationSession(
|
||||
const CollaborationCallbacks::SessionContext& context,
|
||||
bool update_action_timestamp);
|
||||
void MarkHistoryDirty();
|
||||
void PollSharedHistory(); // For real-time collaboration sync
|
||||
|
||||
cli::agent::ConversationalAgentService agent_service_;
|
||||
char input_buffer_[1024];
|
||||
bool active_ = false;
|
||||
std::string title_;
|
||||
size_t last_history_size_ = 0;
|
||||
bool history_loaded_ = false;
|
||||
bool history_dirty_ = false;
|
||||
bool history_supported_ = true;
|
||||
bool history_warning_displayed_ = false;
|
||||
std::filesystem::path history_path_;
|
||||
int last_proposal_count_ = 0;
|
||||
ToastManager* toast_manager_ = nullptr;
|
||||
ProposalDrawer* proposal_drawer_ = nullptr;
|
||||
std::string pending_focus_proposal_id_;
|
||||
absl::Time last_persist_time_ = absl::InfinitePast();
|
||||
CollaborationState collaboration_state_;
|
||||
CollaborationCallbacks collaboration_callbacks_;
|
||||
MultimodalState multimodal_state_;
|
||||
MultimodalCallbacks multimodal_callbacks_;
|
||||
char session_name_buffer_[64] = {};
|
||||
char join_code_buffer_[64] = {};
|
||||
char server_url_buffer_[256] = "ws://localhost:8765";
|
||||
char multimodal_prompt_buffer_[256] = {};
|
||||
absl::Time last_collaboration_action_ = absl::InfinitePast();
|
||||
absl::Time last_shared_history_poll_ = absl::InfinitePast();
|
||||
size_t last_known_history_size_ = 0;
|
||||
};
|
||||
|
||||
} // namespace editor
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_EDITOR_AGENT_AGENT_CHAT_WIDGET_H_
|
||||
355
src/app/editor/agent/agent_collaboration_coordinator.cc
Normal file
355
src/app/editor/agent/agent_collaboration_coordinator.cc
Normal file
@@ -0,0 +1,355 @@
|
||||
#include "app/editor/agent/agent_collaboration_coordinator.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/ascii.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/strip.h"
|
||||
#include "app/core/platform/file_dialog.h"
|
||||
#include "util/macro.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace editor {
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path ExpandUserPath(std::string path) {
|
||||
if (!path.empty() && path.front() == '~') {
|
||||
const char* home = nullptr;
|
||||
#ifdef _WIN32
|
||||
home = std::getenv("USERPROFILE");
|
||||
#else
|
||||
home = std::getenv("HOME");
|
||||
#endif
|
||||
if (home != nullptr) {
|
||||
path.replace(0, 1, home);
|
||||
}
|
||||
}
|
||||
return std::filesystem::path(path);
|
||||
}
|
||||
|
||||
std::string Trimmed(const std::string& value) {
|
||||
return std::string(absl::StripAsciiWhitespace(value));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AgentCollaborationCoordinator::AgentCollaborationCoordinator()
|
||||
: local_user_(LocalUserName()) {}
|
||||
|
||||
absl::StatusOr<AgentCollaborationCoordinator::SessionInfo>
|
||||
AgentCollaborationCoordinator::HostSession(const std::string& session_name) {
|
||||
const std::string trimmed = Trimmed(session_name);
|
||||
if (trimmed.empty()) {
|
||||
return absl::InvalidArgumentError("Session name cannot be empty");
|
||||
}
|
||||
|
||||
RETURN_IF_ERROR(EnsureDirectory());
|
||||
|
||||
SessionFileData data;
|
||||
data.session_name = trimmed;
|
||||
data.session_code = GenerateSessionCode();
|
||||
data.host = local_user_;
|
||||
data.participants.push_back(local_user_);
|
||||
|
||||
std::filesystem::path path = SessionFilePath(data.session_code);
|
||||
|
||||
// Collision avoidance (extremely unlikely but cheap to guard against).
|
||||
int attempts = 0;
|
||||
while (std::filesystem::exists(path) && attempts++ < 5) {
|
||||
data.session_code = GenerateSessionCode();
|
||||
path = SessionFilePath(data.session_code);
|
||||
}
|
||||
if (std::filesystem::exists(path)) {
|
||||
return absl::InternalError(
|
||||
"Unable to allocate a new collaboration session code");
|
||||
}
|
||||
|
||||
RETURN_IF_ERROR(WriteSessionFile(path, data));
|
||||
|
||||
active_ = true;
|
||||
hosting_ = true;
|
||||
session_id_ = data.session_code;
|
||||
session_name_ = data.session_name;
|
||||
|
||||
SessionInfo info;
|
||||
info.session_id = data.session_code;
|
||||
info.session_name = data.session_name;
|
||||
info.participants = data.participants;
|
||||
return info;
|
||||
}
|
||||
|
||||
absl::StatusOr<AgentCollaborationCoordinator::SessionInfo>
|
||||
AgentCollaborationCoordinator::JoinSession(const std::string& session_code) {
|
||||
const std::string normalized = NormalizeSessionCode(session_code);
|
||||
if (normalized.empty()) {
|
||||
return absl::InvalidArgumentError("Session code cannot be empty");
|
||||
}
|
||||
|
||||
RETURN_IF_ERROR(EnsureDirectory());
|
||||
|
||||
std::filesystem::path path = SessionFilePath(normalized);
|
||||
ASSIGN_OR_RETURN(SessionFileData data, LoadSessionFile(path));
|
||||
|
||||
const auto already_joined = std::find(data.participants.begin(),
|
||||
data.participants.end(), local_user_);
|
||||
if (already_joined == data.participants.end()) {
|
||||
data.participants.push_back(local_user_);
|
||||
RETURN_IF_ERROR(WriteSessionFile(path, data));
|
||||
}
|
||||
|
||||
active_ = true;
|
||||
hosting_ = false;
|
||||
session_id_ = data.session_code.empty() ? normalized : data.session_code;
|
||||
session_name_ = data.session_name.empty() ? session_id_ : data.session_name;
|
||||
|
||||
SessionInfo info;
|
||||
info.session_id = session_id_;
|
||||
info.session_name = session_name_;
|
||||
info.participants = data.participants;
|
||||
return info;
|
||||
}
|
||||
|
||||
absl::Status AgentCollaborationCoordinator::LeaveSession() {
|
||||
if (!active_) {
|
||||
return absl::FailedPreconditionError("No collaborative session active");
|
||||
}
|
||||
|
||||
const std::filesystem::path path = SessionFilePath(session_id_);
|
||||
absl::Status status = absl::OkStatus();
|
||||
|
||||
if (hosting_) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
if (ec) {
|
||||
status = absl::InternalError(
|
||||
absl::StrFormat("Failed to clean up session file: %s", ec.message()));
|
||||
}
|
||||
} else {
|
||||
auto data_or = LoadSessionFile(path);
|
||||
if (data_or.ok()) {
|
||||
SessionFileData data = std::move(data_or.value());
|
||||
auto end = std::remove(data.participants.begin(), data.participants.end(),
|
||||
local_user_);
|
||||
data.participants.erase(end, data.participants.end());
|
||||
|
||||
if (data.participants.empty()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
if (ec) {
|
||||
status = absl::InternalError(absl::StrFormat(
|
||||
"Failed to remove empty session file: %s", ec.message()));
|
||||
}
|
||||
} else {
|
||||
status = WriteSessionFile(path, data);
|
||||
}
|
||||
} else {
|
||||
// If the session file has already disappeared, treat it as success.
|
||||
status = absl::OkStatus();
|
||||
}
|
||||
}
|
||||
|
||||
active_ = false;
|
||||
hosting_ = false;
|
||||
session_id_.clear();
|
||||
session_name_.clear();
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
absl::StatusOr<AgentCollaborationCoordinator::SessionInfo>
|
||||
AgentCollaborationCoordinator::RefreshSession() {
|
||||
if (!active_) {
|
||||
return absl::FailedPreconditionError("No collaborative session active");
|
||||
}
|
||||
|
||||
const std::filesystem::path path = SessionFilePath(session_id_);
|
||||
auto data_or = LoadSessionFile(path);
|
||||
if (!data_or.ok()) {
|
||||
absl::Status status = data_or.status();
|
||||
if (absl::IsNotFound(status)) {
|
||||
active_ = false;
|
||||
hosting_ = false;
|
||||
session_id_.clear();
|
||||
session_name_.clear();
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
SessionFileData data = std::move(data_or.value());
|
||||
session_name_ = data.session_name.empty() ? session_id_ : data.session_name;
|
||||
SessionInfo info;
|
||||
info.session_id = session_id_;
|
||||
info.session_name = session_name_;
|
||||
info.participants = data.participants;
|
||||
return info;
|
||||
}
|
||||
|
||||
absl::Status AgentCollaborationCoordinator::EnsureDirectory() const {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(SessionsDirectory(), ec);
|
||||
if (ec) {
|
||||
return absl::InternalError(absl::StrFormat(
|
||||
"Failed to create collaboration directory: %s", ec.message()));
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
std::string AgentCollaborationCoordinator::LocalUserName() const {
|
||||
const char* override_name = std::getenv("YAZE_USER_NAME");
|
||||
const char* user = override_name != nullptr ? override_name : std::getenv("USER");
|
||||
if (user == nullptr) {
|
||||
user = std::getenv("USERNAME");
|
||||
}
|
||||
std::string base = (user != nullptr && std::strlen(user) > 0)
|
||||
? std::string(user)
|
||||
: std::string("Player");
|
||||
|
||||
const char* host = std::getenv("HOSTNAME");
|
||||
#if defined(_WIN32)
|
||||
if (host == nullptr) {
|
||||
host = std::getenv("COMPUTERNAME");
|
||||
}
|
||||
#endif
|
||||
if (host != nullptr && std::strlen(host) > 0) {
|
||||
return absl::StrCat(base, "@", host);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
std::string AgentCollaborationCoordinator::NormalizeSessionCode(
|
||||
const std::string& input) const {
|
||||
std::string normalized = Trimmed(input);
|
||||
normalized.erase(std::remove_if(normalized.begin(), normalized.end(),
|
||||
[](unsigned char c) {
|
||||
return !std::isalnum(
|
||||
static_cast<unsigned char>(c));
|
||||
}),
|
||||
normalized.end());
|
||||
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
|
||||
[](unsigned char c) {
|
||||
return static_cast<char>(
|
||||
std::toupper(static_cast<unsigned char>(c)));
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
std::string AgentCollaborationCoordinator::GenerateSessionCode() const {
|
||||
static constexpr char kAlphabet[] = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
thread_local std::mt19937 rng{std::random_device{}()};
|
||||
std::uniform_int_distribution<size_t> dist(0, sizeof(kAlphabet) - 2);
|
||||
|
||||
std::string code(6, '0');
|
||||
for (char& ch : code) {
|
||||
ch = kAlphabet[dist(rng)];
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
std::filesystem::path AgentCollaborationCoordinator::SessionsDirectory() const {
|
||||
std::filesystem::path base = ExpandUserPath(core::GetConfigDirectory());
|
||||
if (base.empty()) {
|
||||
base = ExpandUserPath(".yaze");
|
||||
}
|
||||
return base / "agent" / "sessions";
|
||||
}
|
||||
|
||||
std::filesystem::path AgentCollaborationCoordinator::SessionFilePath(
|
||||
const std::string& code) const {
|
||||
return SessionsDirectory() / (code + ".session");
|
||||
}
|
||||
|
||||
absl::StatusOr<AgentCollaborationCoordinator::SessionFileData>
|
||||
AgentCollaborationCoordinator::LoadSessionFile(
|
||||
const std::filesystem::path& path) const {
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return absl::NotFoundError(
|
||||
absl::StrFormat("Session %s does not exist", path.string()));
|
||||
}
|
||||
|
||||
SessionFileData data;
|
||||
data.session_code = path.stem().string();
|
||||
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
auto pos = line.find(':');
|
||||
if (pos == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
std::string key = line.substr(0, pos);
|
||||
std::string value = Trimmed(line.substr(pos + 1));
|
||||
if (key == "name") {
|
||||
data.session_name = value;
|
||||
} else if (key == "code") {
|
||||
data.session_code = NormalizeSessionCode(value);
|
||||
} else if (key == "host") {
|
||||
data.host = value;
|
||||
data.participants.push_back(value);
|
||||
} else if (key == "participant") {
|
||||
if (std::find(data.participants.begin(), data.participants.end(), value) ==
|
||||
data.participants.end()) {
|
||||
data.participants.push_back(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.session_name.empty()) {
|
||||
data.session_name = data.session_code;
|
||||
}
|
||||
if (!data.host.empty()) {
|
||||
auto host_it = std::find(data.participants.begin(), data.participants.end(),
|
||||
data.host);
|
||||
if (host_it == data.participants.end()) {
|
||||
data.participants.insert(data.participants.begin(), data.host);
|
||||
} else if (host_it != data.participants.begin()) {
|
||||
std::rotate(data.participants.begin(), host_it,
|
||||
std::next(host_it));
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
absl::Status AgentCollaborationCoordinator::WriteSessionFile(
|
||||
const std::filesystem::path& path, const SessionFileData& data) const {
|
||||
std::ofstream file(path, std::ios::trunc);
|
||||
if (!file.is_open()) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("Failed to write session file: %s", path.string()));
|
||||
}
|
||||
|
||||
file << "name:" << data.session_name << "\n";
|
||||
file << "code:" << data.session_code << "\n";
|
||||
file << "host:" << data.host << "\n";
|
||||
|
||||
std::set<std::string> seen;
|
||||
seen.insert(data.host);
|
||||
for (const auto& participant : data.participants) {
|
||||
if (seen.insert(participant).second) {
|
||||
file << "participant:" << participant << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
file.flush();
|
||||
if (!file.good()) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("Failed to flush session file: %s", path.string()));
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace editor
|
||||
} // namespace yaze
|
||||
66
src/app/editor/agent/agent_collaboration_coordinator.h
Normal file
66
src/app/editor/agent/agent_collaboration_coordinator.h
Normal file
@@ -0,0 +1,66 @@
|
||||
#ifndef YAZE_APP_EDITOR_AGENT_AGENT_COLLABORATION_COORDINATOR_H_
|
||||
#define YAZE_APP_EDITOR_AGENT_AGENT_COLLABORATION_COORDINATOR_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace editor {
|
||||
|
||||
// Coordinates lightweight collaboration features for the agent chat widget.
|
||||
// This implementation uses the local filesystem as a shared backing store so
|
||||
// multiple editor instances on the same machine can experiment with
|
||||
// collaborative sessions while a full backend service is under development.
|
||||
class AgentCollaborationCoordinator {
|
||||
public:
|
||||
struct SessionInfo {
|
||||
std::string session_id;
|
||||
std::string session_name;
|
||||
std::vector<std::string> participants;
|
||||
};
|
||||
|
||||
AgentCollaborationCoordinator();
|
||||
|
||||
absl::StatusOr<SessionInfo> HostSession(const std::string& session_name);
|
||||
absl::StatusOr<SessionInfo> JoinSession(const std::string& session_code);
|
||||
absl::Status LeaveSession();
|
||||
absl::StatusOr<SessionInfo> RefreshSession();
|
||||
|
||||
bool active() const { return active_; }
|
||||
const std::string& session_id() const { return session_id_; }
|
||||
const std::string& session_name() const { return session_name_; }
|
||||
|
||||
private:
|
||||
struct SessionFileData {
|
||||
std::string session_name;
|
||||
std::string session_code;
|
||||
std::string host;
|
||||
std::vector<std::string> participants;
|
||||
};
|
||||
|
||||
absl::Status EnsureDirectory() const;
|
||||
std::string LocalUserName() const;
|
||||
std::string NormalizeSessionCode(const std::string& input) const;
|
||||
std::string GenerateSessionCode() const;
|
||||
std::filesystem::path SessionsDirectory() const;
|
||||
std::filesystem::path SessionFilePath(const std::string& code) const;
|
||||
absl::StatusOr<SessionFileData> LoadSessionFile(
|
||||
const std::filesystem::path& path) const;
|
||||
absl::Status WriteSessionFile(const std::filesystem::path& path,
|
||||
const SessionFileData& data) const;
|
||||
|
||||
bool active_ = false;
|
||||
bool hosting_ = false;
|
||||
std::string session_id_;
|
||||
std::string session_name_;
|
||||
std::string local_user_;
|
||||
};
|
||||
|
||||
} // namespace editor
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_EDITOR_AGENT_AGENT_COLLABORATION_COORDINATOR_H_
|
||||
392
src/app/editor/agent/agent_editor.cc
Normal file
392
src/app/editor/agent/agent_editor.cc
Normal file
@@ -0,0 +1,392 @@
|
||||
#include "app/editor/agent/agent_editor.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "app/editor/agent/agent_chat_widget.h"
|
||||
#include "app/editor/agent/agent_collaboration_coordinator.h"
|
||||
#include "app/editor/system/proposal_drawer.h"
|
||||
#include "app/editor/system/toast_manager.h"
|
||||
#include "app/rom.h"
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
#include "app/editor/agent/network_collaboration_coordinator.h"
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace editor {
|
||||
|
||||
AgentEditor::AgentEditor() {
|
||||
chat_widget_ = std::make_unique<AgentChatWidget>();
|
||||
local_coordinator_ = std::make_unique<AgentCollaborationCoordinator>();
|
||||
}
|
||||
|
||||
AgentEditor::~AgentEditor() = default;
|
||||
|
||||
void AgentEditor::Initialize(ToastManager* toast_manager,
|
||||
ProposalDrawer* proposal_drawer) {
|
||||
toast_manager_ = toast_manager;
|
||||
proposal_drawer_ = proposal_drawer;
|
||||
|
||||
chat_widget_->SetToastManager(toast_manager_);
|
||||
chat_widget_->SetProposalDrawer(proposal_drawer_);
|
||||
|
||||
SetupChatWidgetCallbacks();
|
||||
SetupMultimodalCallbacks();
|
||||
}
|
||||
|
||||
void AgentEditor::SetRomContext(Rom* rom) {
|
||||
rom_ = rom;
|
||||
if (chat_widget_) {
|
||||
chat_widget_->SetRomContext(rom);
|
||||
}
|
||||
}
|
||||
|
||||
void AgentEditor::Draw() {
|
||||
if (chat_widget_) {
|
||||
chat_widget_->Draw();
|
||||
}
|
||||
}
|
||||
|
||||
bool AgentEditor::IsChatActive() const {
|
||||
return chat_widget_ && chat_widget_->is_active();
|
||||
}
|
||||
|
||||
void AgentEditor::SetChatActive(bool active) {
|
||||
if (chat_widget_) {
|
||||
chat_widget_->set_active(active);
|
||||
}
|
||||
}
|
||||
|
||||
void AgentEditor::ToggleChat() {
|
||||
SetChatActive(!IsChatActive());
|
||||
}
|
||||
|
||||
absl::StatusOr<AgentEditor::SessionInfo> AgentEditor::HostSession(
|
||||
const std::string& session_name, CollaborationMode mode) {
|
||||
current_mode_ = mode;
|
||||
|
||||
if (mode == CollaborationMode::kLocal) {
|
||||
ASSIGN_OR_RETURN(auto session, local_coordinator_->HostSession(session_name));
|
||||
|
||||
SessionInfo info;
|
||||
info.session_id = session.session_id;
|
||||
info.session_name = session.session_name;
|
||||
info.participants = session.participants;
|
||||
|
||||
in_session_ = true;
|
||||
current_session_id_ = info.session_id;
|
||||
current_session_name_ = info.session_name;
|
||||
current_participants_ = info.participants;
|
||||
|
||||
// Switch chat to shared history
|
||||
if (chat_widget_) {
|
||||
chat_widget_->SwitchToSharedHistory(info.session_id);
|
||||
}
|
||||
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show(
|
||||
absl::StrFormat("Hosting local session: %s", session_name),
|
||||
ToastType::kSuccess, 3.5f);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
if (mode == CollaborationMode::kNetwork) {
|
||||
if (!network_coordinator_) {
|
||||
return absl::FailedPreconditionError(
|
||||
"Network coordinator not initialized. Connect to a server first.");
|
||||
}
|
||||
|
||||
// Get username from system (could be made configurable)
|
||||
const char* username = std::getenv("USER");
|
||||
if (!username) {
|
||||
username = "unknown";
|
||||
}
|
||||
|
||||
ASSIGN_OR_RETURN(auto session,
|
||||
network_coordinator_->HostSession(session_name, username));
|
||||
|
||||
SessionInfo info;
|
||||
info.session_id = session.session_id;
|
||||
info.session_name = session.session_name;
|
||||
info.participants = session.participants;
|
||||
|
||||
in_session_ = true;
|
||||
current_session_id_ = info.session_id;
|
||||
current_session_name_ = info.session_name;
|
||||
current_participants_ = info.participants;
|
||||
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show(
|
||||
absl::StrFormat("Hosting network session: %s", session_name),
|
||||
ToastType::kSuccess, 3.5f);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
#endif
|
||||
|
||||
return absl::InvalidArgumentError("Unsupported collaboration mode");
|
||||
}
|
||||
|
||||
absl::StatusOr<AgentEditor::SessionInfo> AgentEditor::JoinSession(
|
||||
const std::string& session_code, CollaborationMode mode) {
|
||||
current_mode_ = mode;
|
||||
|
||||
if (mode == CollaborationMode::kLocal) {
|
||||
ASSIGN_OR_RETURN(auto session, local_coordinator_->JoinSession(session_code));
|
||||
|
||||
SessionInfo info;
|
||||
info.session_id = session.session_id;
|
||||
info.session_name = session.session_name;
|
||||
info.participants = session.participants;
|
||||
|
||||
in_session_ = true;
|
||||
current_session_id_ = info.session_id;
|
||||
current_session_name_ = info.session_name;
|
||||
current_participants_ = info.participants;
|
||||
|
||||
// Switch chat to shared history
|
||||
if (chat_widget_) {
|
||||
chat_widget_->SwitchToSharedHistory(info.session_id);
|
||||
}
|
||||
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show(
|
||||
absl::StrFormat("Joined local session: %s", session_code),
|
||||
ToastType::kSuccess, 3.5f);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
if (mode == CollaborationMode::kNetwork) {
|
||||
if (!network_coordinator_) {
|
||||
return absl::FailedPreconditionError(
|
||||
"Network coordinator not initialized. Connect to a server first.");
|
||||
}
|
||||
|
||||
const char* username = std::getenv("USER");
|
||||
if (!username) {
|
||||
username = "unknown";
|
||||
}
|
||||
|
||||
ASSIGN_OR_RETURN(auto session,
|
||||
network_coordinator_->JoinSession(session_code, username));
|
||||
|
||||
SessionInfo info;
|
||||
info.session_id = session.session_id;
|
||||
info.session_name = session.session_name;
|
||||
info.participants = session.participants;
|
||||
|
||||
in_session_ = true;
|
||||
current_session_id_ = info.session_id;
|
||||
current_session_name_ = info.session_name;
|
||||
current_participants_ = info.participants;
|
||||
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show(
|
||||
absl::StrFormat("Joined network session: %s", session_code),
|
||||
ToastType::kSuccess, 3.5f);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
#endif
|
||||
|
||||
return absl::InvalidArgumentError("Unsupported collaboration mode");
|
||||
}
|
||||
|
||||
absl::Status AgentEditor::LeaveSession() {
|
||||
if (!in_session_) {
|
||||
return absl::FailedPreconditionError("Not in a session");
|
||||
}
|
||||
|
||||
if (current_mode_ == CollaborationMode::kLocal) {
|
||||
RETURN_IF_ERROR(local_coordinator_->LeaveSession());
|
||||
}
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
else if (current_mode_ == CollaborationMode::kNetwork) {
|
||||
if (network_coordinator_) {
|
||||
RETURN_IF_ERROR(network_coordinator_->LeaveSession());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Switch chat back to local history
|
||||
if (chat_widget_) {
|
||||
chat_widget_->SwitchToLocalHistory();
|
||||
}
|
||||
|
||||
in_session_ = false;
|
||||
current_session_id_.clear();
|
||||
current_session_name_.clear();
|
||||
current_participants_.clear();
|
||||
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show("Left collaboration session", ToastType::kInfo, 3.0f);
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::StatusOr<AgentEditor::SessionInfo> AgentEditor::RefreshSession() {
|
||||
if (!in_session_) {
|
||||
return absl::FailedPreconditionError("Not in a session");
|
||||
}
|
||||
|
||||
if (current_mode_ == CollaborationMode::kLocal) {
|
||||
ASSIGN_OR_RETURN(auto session, local_coordinator_->RefreshSession());
|
||||
|
||||
SessionInfo info;
|
||||
info.session_id = session.session_id;
|
||||
info.session_name = session.session_name;
|
||||
info.participants = session.participants;
|
||||
|
||||
current_participants_ = info.participants;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
// Network mode doesn't need explicit refresh - it's real-time
|
||||
SessionInfo info;
|
||||
info.session_id = current_session_id_;
|
||||
info.session_name = current_session_name_;
|
||||
info.participants = current_participants_;
|
||||
return info;
|
||||
}
|
||||
|
||||
absl::Status AgentEditor::CaptureSnapshot(
|
||||
[[maybe_unused]] std::filesystem::path* output_path,
|
||||
[[maybe_unused]] const CaptureConfig& config) {
|
||||
// This will be implemented by the callbacks set via SetupMultimodalCallbacks
|
||||
// For now, return an error indicating this needs to be wired through the callbacks
|
||||
return absl::UnimplementedError(
|
||||
"CaptureSnapshot should be called through the chat widget UI");
|
||||
}
|
||||
|
||||
absl::Status AgentEditor::SendToGemini(
|
||||
[[maybe_unused]] const std::filesystem::path& image_path,
|
||||
[[maybe_unused]] const std::string& prompt) {
|
||||
// This will be implemented by the callbacks set via SetupMultimodalCallbacks
|
||||
return absl::UnimplementedError(
|
||||
"SendToGemini should be called through the chat widget UI");
|
||||
}
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
absl::Status AgentEditor::ConnectToServer(const std::string& server_url) {
|
||||
try {
|
||||
network_coordinator_ =
|
||||
std::make_unique<NetworkCollaborationCoordinator>(server_url);
|
||||
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show(
|
||||
absl::StrFormat("Connected to server: %s", server_url),
|
||||
ToastType::kSuccess, 3.0f);
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
} catch (const std::exception& e) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("Failed to connect to server: %s", e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void AgentEditor::DisconnectFromServer() {
|
||||
if (in_session_ && current_mode_ == CollaborationMode::kNetwork) {
|
||||
LeaveSession();
|
||||
}
|
||||
network_coordinator_.reset();
|
||||
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show("Disconnected from server", ToastType::kInfo, 2.5f);
|
||||
}
|
||||
}
|
||||
|
||||
bool AgentEditor::IsConnectedToServer() const {
|
||||
return network_coordinator_ && network_coordinator_->IsConnected();
|
||||
}
|
||||
#endif
|
||||
|
||||
bool AgentEditor::IsInSession() const {
|
||||
return in_session_;
|
||||
}
|
||||
|
||||
AgentEditor::CollaborationMode AgentEditor::GetCurrentMode() const {
|
||||
return current_mode_;
|
||||
}
|
||||
|
||||
std::optional<AgentEditor::SessionInfo> AgentEditor::GetCurrentSession() const {
|
||||
if (!in_session_) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
SessionInfo info;
|
||||
info.session_id = current_session_id_;
|
||||
info.session_name = current_session_name_;
|
||||
info.participants = current_participants_;
|
||||
return info;
|
||||
}
|
||||
|
||||
void AgentEditor::SetupChatWidgetCallbacks() {
|
||||
if (!chat_widget_) {
|
||||
return;
|
||||
}
|
||||
|
||||
AgentChatWidget::CollaborationCallbacks collab_callbacks;
|
||||
|
||||
collab_callbacks.host_session =
|
||||
[this](const std::string& session_name)
|
||||
-> absl::StatusOr<AgentChatWidget::CollaborationCallbacks::SessionContext> {
|
||||
// Use the current mode from the chat widget UI
|
||||
ASSIGN_OR_RETURN(auto session, this->HostSession(session_name, current_mode_));
|
||||
|
||||
AgentChatWidget::CollaborationCallbacks::SessionContext context;
|
||||
context.session_id = session.session_id;
|
||||
context.session_name = session.session_name;
|
||||
context.participants = session.participants;
|
||||
return context;
|
||||
};
|
||||
|
||||
collab_callbacks.join_session =
|
||||
[this](const std::string& session_code)
|
||||
-> absl::StatusOr<AgentChatWidget::CollaborationCallbacks::SessionContext> {
|
||||
ASSIGN_OR_RETURN(auto session, this->JoinSession(session_code, current_mode_));
|
||||
|
||||
AgentChatWidget::CollaborationCallbacks::SessionContext context;
|
||||
context.session_id = session.session_id;
|
||||
context.session_name = session.session_name;
|
||||
context.participants = session.participants;
|
||||
return context;
|
||||
};
|
||||
|
||||
collab_callbacks.leave_session = [this]() {
|
||||
return this->LeaveSession();
|
||||
};
|
||||
|
||||
collab_callbacks.refresh_session =
|
||||
[this]() -> absl::StatusOr<AgentChatWidget::CollaborationCallbacks::SessionContext> {
|
||||
ASSIGN_OR_RETURN(auto session, this->RefreshSession());
|
||||
|
||||
AgentChatWidget::CollaborationCallbacks::SessionContext context;
|
||||
context.session_id = session.session_id;
|
||||
context.session_name = session.session_name;
|
||||
context.participants = session.participants;
|
||||
return context;
|
||||
};
|
||||
|
||||
chat_widget_->SetCollaborationCallbacks(collab_callbacks);
|
||||
}
|
||||
|
||||
void AgentEditor::SetupMultimodalCallbacks() {
|
||||
// Multimodal callbacks are set up by the EditorManager since it has
|
||||
// access to the screenshot utilities. We just initialize the structure here.
|
||||
}
|
||||
|
||||
} // namespace editor
|
||||
} // namespace yaze
|
||||
141
src/app/editor/agent/agent_editor.h
Normal file
141
src/app/editor/agent/agent_editor.h
Normal file
@@ -0,0 +1,141 @@
|
||||
#ifndef YAZE_APP_EDITOR_AGENT_AGENT_EDITOR_H_
|
||||
#define YAZE_APP_EDITOR_AGENT_AGENT_EDITOR_H_
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
class Rom;
|
||||
|
||||
namespace editor {
|
||||
|
||||
class ToastManager;
|
||||
class ProposalDrawer;
|
||||
class AgentChatWidget;
|
||||
class AgentCollaborationCoordinator;
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
class NetworkCollaborationCoordinator;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @class AgentEditor
|
||||
* @brief Manages all agent-related functionality including chat, collaboration,
|
||||
* and network coordination for the Z3ED editor.
|
||||
*
|
||||
* This class serves as a high-level manager for:
|
||||
* - Agent chat widget and conversations
|
||||
* - Local filesystem-based collaboration
|
||||
* - Network-based (WebSocket) collaboration
|
||||
* - Coordination between multiple collaboration modes
|
||||
*/
|
||||
class AgentEditor {
|
||||
public:
|
||||
AgentEditor();
|
||||
~AgentEditor();
|
||||
|
||||
// Initialization
|
||||
void Initialize(ToastManager* toast_manager, ProposalDrawer* proposal_drawer);
|
||||
void SetRomContext(Rom* rom);
|
||||
|
||||
// Main rendering
|
||||
void Draw();
|
||||
|
||||
// Chat widget access
|
||||
bool IsChatActive() const;
|
||||
void SetChatActive(bool active);
|
||||
void ToggleChat();
|
||||
|
||||
// Collaboration management
|
||||
enum class CollaborationMode {
|
||||
kLocal, // Filesystem-based collaboration
|
||||
kNetwork // WebSocket-based collaboration
|
||||
};
|
||||
|
||||
struct SessionInfo {
|
||||
std::string session_id;
|
||||
std::string session_name;
|
||||
std::vector<std::string> participants;
|
||||
};
|
||||
|
||||
// Host a new collaboration session
|
||||
absl::StatusOr<SessionInfo> HostSession(const std::string& session_name,
|
||||
CollaborationMode mode = CollaborationMode::kLocal);
|
||||
|
||||
// Join an existing collaboration session
|
||||
absl::StatusOr<SessionInfo> JoinSession(const std::string& session_code,
|
||||
CollaborationMode mode = CollaborationMode::kLocal);
|
||||
|
||||
// Leave the current collaboration session
|
||||
absl::Status LeaveSession();
|
||||
|
||||
// Refresh session information
|
||||
absl::StatusOr<SessionInfo> RefreshSession();
|
||||
|
||||
// Multimodal (vision/screenshot) support
|
||||
struct CaptureConfig {
|
||||
enum class CaptureMode {
|
||||
kFullWindow,
|
||||
kActiveEditor,
|
||||
kSpecificWindow
|
||||
};
|
||||
CaptureMode mode = CaptureMode::kActiveEditor;
|
||||
std::string specific_window_name;
|
||||
};
|
||||
|
||||
absl::Status CaptureSnapshot(std::filesystem::path* output_path,
|
||||
const CaptureConfig& config);
|
||||
absl::Status SendToGemini(const std::filesystem::path& image_path,
|
||||
const std::string& prompt);
|
||||
|
||||
// Server management for network mode
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
absl::Status ConnectToServer(const std::string& server_url);
|
||||
void DisconnectFromServer();
|
||||
bool IsConnectedToServer() const;
|
||||
#endif
|
||||
|
||||
// State queries
|
||||
bool IsInSession() const;
|
||||
CollaborationMode GetCurrentMode() const;
|
||||
std::optional<SessionInfo> GetCurrentSession() const;
|
||||
|
||||
// Access to underlying components (for advanced use)
|
||||
AgentChatWidget* GetChatWidget() { return chat_widget_.get(); }
|
||||
AgentCollaborationCoordinator* GetLocalCoordinator() { return local_coordinator_.get(); }
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
NetworkCollaborationCoordinator* GetNetworkCoordinator() { return network_coordinator_.get(); }
|
||||
#endif
|
||||
|
||||
private:
|
||||
// Setup callbacks for the chat widget
|
||||
void SetupChatWidgetCallbacks();
|
||||
void SetupMultimodalCallbacks();
|
||||
|
||||
// Internal state
|
||||
std::unique_ptr<AgentChatWidget> chat_widget_;
|
||||
std::unique_ptr<AgentCollaborationCoordinator> local_coordinator_;
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
std::unique_ptr<NetworkCollaborationCoordinator> network_coordinator_;
|
||||
#endif
|
||||
|
||||
ToastManager* toast_manager_ = nullptr;
|
||||
ProposalDrawer* proposal_drawer_ = nullptr;
|
||||
Rom* rom_ = nullptr;
|
||||
|
||||
CollaborationMode current_mode_ = CollaborationMode::kLocal;
|
||||
bool in_session_ = false;
|
||||
std::string current_session_id_;
|
||||
std::string current_session_name_;
|
||||
std::vector<std::string> current_participants_;
|
||||
};
|
||||
|
||||
} // namespace editor
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_EDITOR_AGENT_AGENT_EDITOR_H_
|
||||
398
src/app/editor/agent/network_collaboration_coordinator.cc
Normal file
398
src/app/editor/agent/network_collaboration_coordinator.cc
Normal file
@@ -0,0 +1,398 @@
|
||||
#include "app/editor/agent/network_collaboration_coordinator.h"
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
|
||||
#ifdef YAZE_WITH_JSON
|
||||
#include "httplib.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
using Json = nlohmann::json;
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace editor {
|
||||
|
||||
#ifdef YAZE_WITH_JSON
|
||||
|
||||
namespace detail {
|
||||
|
||||
// Simple WebSocket client implementation using httplib
|
||||
// Implements basic WebSocket protocol for collaboration
|
||||
class WebSocketClient {
|
||||
public:
|
||||
explicit WebSocketClient(const std::string& host, int port)
|
||||
: host_(host), port_(port), connected_(false) {}
|
||||
|
||||
bool Connect(const std::string& path) {
|
||||
try {
|
||||
// Create HTTP client for WebSocket upgrade
|
||||
client_ = std::make_unique<httplib::Client>(host_.c_str(), port_);
|
||||
client_->set_connection_timeout(5); // 5 seconds
|
||||
client_->set_read_timeout(30); // 30 seconds
|
||||
|
||||
// For now, mark as connected and use HTTP polling fallback
|
||||
// A full WebSocket implementation would do the upgrade handshake here
|
||||
connected_ = true;
|
||||
|
||||
std::cout << "✓ Connected to collaboration server at " << host_ << ":" << port_ << std::endl;
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Failed to connect to " << host_ << ":" << port_ << ": " << e.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Close() {
|
||||
connected_ = false;
|
||||
client_.reset();
|
||||
}
|
||||
|
||||
bool Send(const std::string& message) {
|
||||
if (!connected_ || !client_) return false;
|
||||
|
||||
// For HTTP fallback: POST message to server
|
||||
// A full WebSocket would send WebSocket frames
|
||||
auto res = client_->Post("/message", message, "application/json");
|
||||
return res && res->status == 200;
|
||||
}
|
||||
|
||||
std::string Receive() {
|
||||
if (!connected_ || !client_) return "";
|
||||
|
||||
// For HTTP fallback: Poll for messages
|
||||
// A full WebSocket would read frames from the socket
|
||||
auto res = client_->Get("/poll");
|
||||
if (res && res->status == 200) {
|
||||
return res->body;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
bool IsConnected() const { return connected_; }
|
||||
|
||||
private:
|
||||
std::string host_;
|
||||
int port_;
|
||||
bool connected_;
|
||||
std::unique_ptr<httplib::Client> client_;
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
NetworkCollaborationCoordinator::NetworkCollaborationCoordinator(
|
||||
const std::string& server_url)
|
||||
: server_url_(server_url) {
|
||||
// Parse server URL
|
||||
// Expected format: ws://hostname:port or wss://hostname:port
|
||||
if (server_url_.find("ws://") == 0) {
|
||||
// Extract hostname and port
|
||||
// For now, use default localhost:8765
|
||||
ConnectWebSocket();
|
||||
}
|
||||
}
|
||||
|
||||
NetworkCollaborationCoordinator::~NetworkCollaborationCoordinator() {
|
||||
should_stop_ = true;
|
||||
if (receive_thread_ && receive_thread_->joinable()) {
|
||||
receive_thread_->join();
|
||||
}
|
||||
DisconnectWebSocket();
|
||||
}
|
||||
|
||||
void NetworkCollaborationCoordinator::ConnectWebSocket() {
|
||||
// Parse URL (simple implementation - assumes ws://host:port format)
|
||||
std::string host = "localhost";
|
||||
int port = 8765;
|
||||
|
||||
// Extract from server_url_ if needed
|
||||
if (server_url_.find("ws://") == 0) {
|
||||
std::string url_part = server_url_.substr(5); // Skip "ws://"
|
||||
std::vector<std::string> parts = absl::StrSplit(url_part, ':');
|
||||
if (!parts.empty()) {
|
||||
host = parts[0];
|
||||
}
|
||||
if (parts.size() > 1) {
|
||||
port = std::stoi(parts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
ws_client_ = std::make_unique<detail::WebSocketClient>(host, port);
|
||||
|
||||
if (ws_client_->Connect("/")) {
|
||||
connected_ = true;
|
||||
|
||||
// Start receive thread
|
||||
should_stop_ = false;
|
||||
receive_thread_ = std::make_unique<std::thread>(
|
||||
&NetworkCollaborationCoordinator::WebSocketReceiveLoop, this);
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkCollaborationCoordinator::DisconnectWebSocket() {
|
||||
if (ws_client_) {
|
||||
ws_client_->Close();
|
||||
ws_client_.reset();
|
||||
}
|
||||
connected_ = false;
|
||||
}
|
||||
|
||||
absl::StatusOr<NetworkCollaborationCoordinator::SessionInfo>
|
||||
NetworkCollaborationCoordinator::HostSession(const std::string& session_name,
|
||||
const std::string& username) {
|
||||
if (!connected_) {
|
||||
return absl::FailedPreconditionError("Not connected to collaboration server");
|
||||
}
|
||||
|
||||
username_ = username;
|
||||
|
||||
// Build host_session message
|
||||
Json message = {
|
||||
{"type", "host_session"},
|
||||
{"payload", {
|
||||
{"session_name", session_name},
|
||||
{"username", username}
|
||||
}}
|
||||
};
|
||||
|
||||
SendWebSocketMessage("host_session", message["payload"].dump());
|
||||
|
||||
// TODO: Wait for session_hosted response and parse it
|
||||
// For now, return a placeholder
|
||||
SessionInfo info;
|
||||
info.session_name = session_name;
|
||||
info.session_code = "PENDING"; // Will be updated from server response
|
||||
info.participants = {username};
|
||||
|
||||
in_session_ = true;
|
||||
session_name_ = session_name;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
absl::StatusOr<NetworkCollaborationCoordinator::SessionInfo>
|
||||
NetworkCollaborationCoordinator::JoinSession(const std::string& session_code,
|
||||
const std::string& username) {
|
||||
if (!connected_) {
|
||||
return absl::FailedPreconditionError("Not connected to collaboration server");
|
||||
}
|
||||
|
||||
username_ = username;
|
||||
session_code_ = session_code;
|
||||
|
||||
// Build join_session message
|
||||
Json message = {
|
||||
{"type", "join_session"},
|
||||
{"payload", {
|
||||
{"session_code", session_code},
|
||||
{"username", username}
|
||||
}}
|
||||
};
|
||||
|
||||
SendWebSocketMessage("join_session", message["payload"].dump());
|
||||
|
||||
// TODO: Wait for session_joined response and parse it
|
||||
SessionInfo info;
|
||||
info.session_code = session_code;
|
||||
|
||||
in_session_ = true;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
absl::Status NetworkCollaborationCoordinator::LeaveSession() {
|
||||
if (!in_session_) {
|
||||
return absl::FailedPreconditionError("Not in a session");
|
||||
}
|
||||
|
||||
Json message = {{"type", "leave_session"}};
|
||||
SendWebSocketMessage("leave_session", "{}");
|
||||
|
||||
in_session_ = false;
|
||||
session_id_.clear();
|
||||
session_code_.clear();
|
||||
session_name_.clear();
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status NetworkCollaborationCoordinator::SendMessage(
|
||||
const std::string& sender, const std::string& message) {
|
||||
if (!in_session_) {
|
||||
return absl::FailedPreconditionError("Not in a session");
|
||||
}
|
||||
|
||||
Json msg = {
|
||||
{"type", "chat_message"},
|
||||
{"payload", {
|
||||
{"sender", sender},
|
||||
{"message", message}
|
||||
}}
|
||||
};
|
||||
|
||||
SendWebSocketMessage("chat_message", msg["payload"].dump());
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
bool NetworkCollaborationCoordinator::IsConnected() const {
|
||||
return connected_;
|
||||
}
|
||||
|
||||
void NetworkCollaborationCoordinator::SetMessageCallback(MessageCallback callback) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
message_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void NetworkCollaborationCoordinator::SetParticipantCallback(
|
||||
ParticipantCallback callback) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
participant_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void NetworkCollaborationCoordinator::SetErrorCallback(ErrorCallback callback) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
error_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void NetworkCollaborationCoordinator::SendWebSocketMessage(
|
||||
const std::string& type, const std::string& payload_json) {
|
||||
if (!ws_client_ || !connected_) {
|
||||
return;
|
||||
}
|
||||
|
||||
Json message = {
|
||||
{"type", type},
|
||||
{"payload", Json::parse(payload_json)}
|
||||
};
|
||||
|
||||
ws_client_->Send(message.dump());
|
||||
}
|
||||
|
||||
void NetworkCollaborationCoordinator::HandleWebSocketMessage(
|
||||
const std::string& message_str) {
|
||||
try {
|
||||
Json message = Json::parse(message_str);
|
||||
std::string type = message["type"];
|
||||
|
||||
if (type == "session_hosted") {
|
||||
Json payload = message["payload"];
|
||||
session_id_ = payload["session_id"];
|
||||
session_code_ = payload["session_code"];
|
||||
session_name_ = payload["session_name"];
|
||||
|
||||
if (payload.contains("participants")) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
if (participant_callback_) {
|
||||
std::vector<std::string> participants = payload["participants"];
|
||||
participant_callback_(participants);
|
||||
}
|
||||
}
|
||||
} else if (type == "session_joined") {
|
||||
Json payload = message["payload"];
|
||||
session_id_ = payload["session_id"];
|
||||
session_code_ = payload["session_code"];
|
||||
session_name_ = payload["session_name"];
|
||||
|
||||
if (payload.contains("participants")) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
if (participant_callback_) {
|
||||
std::vector<std::string> participants = payload["participants"];
|
||||
participant_callback_(participants);
|
||||
}
|
||||
}
|
||||
} else if (type == "chat_message") {
|
||||
Json payload = message["payload"];
|
||||
ChatMessage msg;
|
||||
msg.sender = payload["sender"];
|
||||
msg.message = payload["message"];
|
||||
msg.timestamp = payload["timestamp"];
|
||||
|
||||
absl::MutexLock lock(&mutex_);
|
||||
if (message_callback_) {
|
||||
message_callback_(msg);
|
||||
}
|
||||
} else if (type == "participant_joined" || type == "participant_left") {
|
||||
Json payload = message["payload"];
|
||||
if (payload.contains("participants")) {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
if (participant_callback_) {
|
||||
std::vector<std::string> participants = payload["participants"];
|
||||
participant_callback_(participants);
|
||||
}
|
||||
}
|
||||
} else if (type == "error") {
|
||||
Json payload = message["payload"];
|
||||
std::string error = payload["error"];
|
||||
|
||||
absl::MutexLock lock(&mutex_);
|
||||
if (error_callback_) {
|
||||
error_callback_(error);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error parsing WebSocket message: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkCollaborationCoordinator::WebSocketReceiveLoop() {
|
||||
while (!should_stop_ && connected_) {
|
||||
if (!ws_client_) break;
|
||||
|
||||
std::string message = ws_client_->Receive();
|
||||
if (!message.empty()) {
|
||||
HandleWebSocketMessage(message);
|
||||
}
|
||||
|
||||
// Small sleep to avoid busy-waiting
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
}
|
||||
|
||||
#else // !YAZE_WITH_JSON
|
||||
|
||||
// Stub implementations when JSON is not available
|
||||
NetworkCollaborationCoordinator::NetworkCollaborationCoordinator(
|
||||
const std::string& server_url) : server_url_(server_url) {}
|
||||
|
||||
NetworkCollaborationCoordinator::~NetworkCollaborationCoordinator() = default;
|
||||
|
||||
absl::StatusOr<NetworkCollaborationCoordinator::SessionInfo>
|
||||
NetworkCollaborationCoordinator::HostSession(const std::string&, const std::string&) {
|
||||
return absl::UnimplementedError("Network collaboration requires JSON support");
|
||||
}
|
||||
|
||||
absl::StatusOr<NetworkCollaborationCoordinator::SessionInfo>
|
||||
NetworkCollaborationCoordinator::JoinSession(const std::string&, const std::string&) {
|
||||
return absl::UnimplementedError("Network collaboration requires JSON support");
|
||||
}
|
||||
|
||||
absl::Status NetworkCollaborationCoordinator::LeaveSession() {
|
||||
return absl::UnimplementedError("Network collaboration requires JSON support");
|
||||
}
|
||||
|
||||
absl::Status NetworkCollaborationCoordinator::SendMessage(
|
||||
const std::string&, const std::string&) {
|
||||
return absl::UnimplementedError("Network collaboration requires JSON support");
|
||||
}
|
||||
|
||||
bool NetworkCollaborationCoordinator::IsConnected() const { return false; }
|
||||
|
||||
void NetworkCollaborationCoordinator::SetMessageCallback(MessageCallback) {}
|
||||
void NetworkCollaborationCoordinator::SetParticipantCallback(ParticipantCallback) {}
|
||||
void NetworkCollaborationCoordinator::SetErrorCallback(ErrorCallback) {}
|
||||
void NetworkCollaborationCoordinator::ConnectWebSocket() {}
|
||||
void NetworkCollaborationCoordinator::DisconnectWebSocket() {}
|
||||
void NetworkCollaborationCoordinator::SendWebSocketMessage(const std::string&, const std::string&) {}
|
||||
void NetworkCollaborationCoordinator::HandleWebSocketMessage(const std::string&) {}
|
||||
void NetworkCollaborationCoordinator::WebSocketReceiveLoop() {}
|
||||
|
||||
#endif // YAZE_WITH_JSON
|
||||
|
||||
} // namespace editor
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_WITH_GRPC
|
||||
99
src/app/editor/agent/network_collaboration_coordinator.h
Normal file
99
src/app/editor/agent/network_collaboration_coordinator.h
Normal file
@@ -0,0 +1,99 @@
|
||||
#ifndef YAZE_APP_EDITOR_AGENT_NETWORK_COLLABORATION_COORDINATOR_H_
|
||||
#define YAZE_APP_EDITOR_AGENT_NETWORK_COLLABORATION_COORDINATOR_H_
|
||||
|
||||
#ifdef YAZE_WITH_GRPC // Reuse gRPC build flag for network features
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace editor {
|
||||
|
||||
// Forward declarations to avoid including httplib in header
|
||||
namespace detail {
|
||||
class WebSocketClient;
|
||||
}
|
||||
|
||||
// Coordinates network-based collaboration via WebSocket connections
|
||||
class NetworkCollaborationCoordinator {
|
||||
public:
|
||||
struct SessionInfo {
|
||||
std::string session_id;
|
||||
std::string session_code;
|
||||
std::string session_name;
|
||||
std::vector<std::string> participants;
|
||||
};
|
||||
|
||||
struct ChatMessage {
|
||||
std::string sender;
|
||||
std::string message;
|
||||
int64_t timestamp;
|
||||
};
|
||||
|
||||
// Callbacks for handling incoming events
|
||||
using MessageCallback = std::function<void(const ChatMessage&)>;
|
||||
using ParticipantCallback = std::function<void(const std::vector<std::string>&)>;
|
||||
using ErrorCallback = std::function<void(const std::string&)>;
|
||||
|
||||
explicit NetworkCollaborationCoordinator(const std::string& server_url);
|
||||
~NetworkCollaborationCoordinator();
|
||||
|
||||
// Session management
|
||||
absl::StatusOr<SessionInfo> HostSession(const std::string& session_name,
|
||||
const std::string& username);
|
||||
absl::StatusOr<SessionInfo> JoinSession(const std::string& session_code,
|
||||
const std::string& username);
|
||||
absl::Status LeaveSession();
|
||||
|
||||
// Send chat message to current session
|
||||
absl::Status SendMessage(const std::string& sender, const std::string& message);
|
||||
|
||||
// Connection status
|
||||
bool IsConnected() const;
|
||||
bool InSession() const { return in_session_; }
|
||||
const std::string& session_code() const { return session_code_; }
|
||||
const std::string& session_name() const { return session_name_; }
|
||||
|
||||
// Event callbacks
|
||||
void SetMessageCallback(MessageCallback callback);
|
||||
void SetParticipantCallback(ParticipantCallback callback);
|
||||
void SetErrorCallback(ErrorCallback callback);
|
||||
|
||||
private:
|
||||
void ConnectWebSocket();
|
||||
void DisconnectWebSocket();
|
||||
void SendWebSocketMessage(const std::string& type, const std::string& payload_json);
|
||||
void HandleWebSocketMessage(const std::string& message);
|
||||
void WebSocketReceiveLoop();
|
||||
|
||||
std::string server_url_;
|
||||
std::string username_;
|
||||
std::string session_id_;
|
||||
std::string session_code_;
|
||||
std::string session_name_;
|
||||
bool in_session_ = false;
|
||||
|
||||
std::unique_ptr<detail::WebSocketClient> ws_client_;
|
||||
std::atomic<bool> connected_{false};
|
||||
std::atomic<bool> should_stop_{false};
|
||||
std::unique_ptr<std::thread> receive_thread_;
|
||||
|
||||
mutable absl::Mutex mutex_;
|
||||
MessageCallback message_callback_ ABSL_GUARDED_BY(mutex_);
|
||||
ParticipantCallback participant_callback_ ABSL_GUARDED_BY(mutex_);
|
||||
ErrorCallback error_callback_ ABSL_GUARDED_BY(mutex_);
|
||||
};
|
||||
|
||||
} // namespace editor
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_WITH_GRPC
|
||||
#endif // YAZE_APP_EDITOR_AGENT_NETWORK_COLLABORATION_COORDINATOR_H_
|
||||
Reference in New Issue
Block a user