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:
scawful
2025-10-04 17:47:23 -04:00
parent fbbe911ae0
commit b54f4b99dd
13 changed files with 831 additions and 326 deletions

View 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

View 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_

File diff suppressed because it is too large Load Diff

View 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_

View 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

View 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_

View 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

View 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_

View 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

View 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_