feat: Add collaboration features to AgentChatWidget and AgentCollaborationCoordinator
This commit is contained in:
@@ -33,13 +33,12 @@ set(
|
||||
app/editor/system/shortcut_manager.cc
|
||||
app/editor/system/popup_manager.cc
|
||||
app/editor/system/agent_chat_history_codec.cc
|
||||
app/test/test_manager.cc
|
||||
app/test/integrated_test_suite.h
|
||||
app/test/rom_dependent_test_suite.h
|
||||
app/test/unit_test_suite.h
|
||||
app/editor/system/proposal_drawer.cc
|
||||
)
|
||||
|
||||
if(YAZE_WITH_GRPC)
|
||||
list(APPEND YAZE_APP_EDITOR_SRC app/editor/system/agent_chat_widget.cc)
|
||||
list(APPEND YAZE_APP_EDITOR_SRC
|
||||
app/editor/system/agent_chat_widget.cc
|
||||
app/editor/system/agent_collaboration_coordinator.cc
|
||||
)
|
||||
endif()
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cstring>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "app/core/features.h"
|
||||
@@ -226,6 +227,41 @@ void EditorManager::Initialize(const std::string& filename) {
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
agent_chat_widget_.SetToastManager(&toast_manager_);
|
||||
agent_chat_widget_.SetProposalDrawer(&proposal_drawer_);
|
||||
AgentChatWidget::CollaborationCallbacks collab_callbacks;
|
||||
collab_callbacks.host_session =
|
||||
[this](const std::string& session_name)
|
||||
-> absl::StatusOr<AgentChatWidget::CollaborationCallbacks::SessionContext> {
|
||||
ASSIGN_OR_RETURN(auto session,
|
||||
collaboration_coordinator_.HostSession(session_name));
|
||||
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,
|
||||
collaboration_coordinator_.JoinSession(session_code));
|
||||
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 collaboration_coordinator_.LeaveSession(); };
|
||||
collab_callbacks.refresh_session =
|
||||
[this]() -> absl::StatusOr<AgentChatWidget::CollaborationCallbacks::SessionContext> {
|
||||
ASSIGN_OR_RETURN(auto session, collaboration_coordinator_.RefreshSession());
|
||||
AgentChatWidget::CollaborationCallbacks::SessionContext context;
|
||||
context.session_id = session.session_id;
|
||||
context.session_name = session.session_name;
|
||||
context.participants = session.participants;
|
||||
return context;
|
||||
};
|
||||
agent_chat_widget_.SetCollaborationCallbacks(collab_callbacks);
|
||||
#endif
|
||||
|
||||
// Load critical user settings first
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "app/editor/system/popup_manager.h"
|
||||
#include "app/editor/system/proposal_drawer.h"
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
#include "app/editor/system/agent_collaboration_coordinator.h"
|
||||
#include "app/editor/system/agent_chat_widget.h"
|
||||
#endif
|
||||
#include "app/editor/system/settings_editor.h"
|
||||
@@ -184,6 +185,7 @@ class EditorManager {
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
// Agent chat widget
|
||||
AgentCollaborationCoordinator collaboration_coordinator_;
|
||||
AgentChatWidget agent_chat_widget_;
|
||||
#endif
|
||||
|
||||
|
||||
@@ -194,6 +194,8 @@ absl::StatusOr<AgentChatHistoryCodec::Snapshot> AgentChatHistoryCodec::Load(
|
||||
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()) {
|
||||
@@ -208,6 +210,11 @@ absl::StatusOr<AgentChatHistoryCodec::Snapshot> AgentChatHistoryCodec::Load(
|
||||
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()) {
|
||||
@@ -241,7 +248,7 @@ absl::Status AgentChatHistoryCodec::Save(
|
||||
const std::filesystem::path& path, const Snapshot& snapshot) {
|
||||
#if defined(YAZE_WITH_JSON)
|
||||
Json json;
|
||||
json["version"] = 2;
|
||||
json["version"] = 3;
|
||||
json["messages"] = Json::array();
|
||||
|
||||
for (const auto& message : snapshot.history) {
|
||||
@@ -284,6 +291,7 @@ absl::Status AgentChatHistoryCodec::Save(
|
||||
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(
|
||||
|
||||
@@ -23,6 +23,7 @@ class AgentChatHistoryCodec {
|
||||
struct CollaborationState {
|
||||
bool active = false;
|
||||
std::string session_id;
|
||||
std::string session_name;
|
||||
std::vector<std::string> participants;
|
||||
absl::Time last_synced = absl::InfinitePast();
|
||||
};
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/time/clock.h"
|
||||
#include "absl/time/time.h"
|
||||
@@ -173,8 +175,13 @@ void AgentChatWidget::EnsureHistoryLoaded() {
|
||||
|
||||
collaboration_state_.active = snapshot.collaboration.active;
|
||||
collaboration_state_.session_id = snapshot.collaboration.session_id;
|
||||
collaboration_state_.session_name = snapshot.collaboration.session_name;
|
||||
collaboration_state_.participants = snapshot.collaboration.participants;
|
||||
collaboration_state_.last_synced = snapshot.collaboration.last_synced;
|
||||
if (collaboration_state_.session_name.empty() &&
|
||||
!collaboration_state_.session_id.empty()) {
|
||||
collaboration_state_.session_name = collaboration_state_.session_id;
|
||||
}
|
||||
|
||||
multimodal_state_.last_capture_path =
|
||||
snapshot.multimodal.last_capture_path;
|
||||
@@ -202,6 +209,7 @@ void AgentChatWidget::PersistHistory() {
|
||||
snapshot.history = agent_service_.GetHistory();
|
||||
snapshot.collaboration.active = collaboration_state_.active;
|
||||
snapshot.collaboration.session_id = collaboration_state_.session_id;
|
||||
snapshot.collaboration.session_name = collaboration_state_.session_name;
|
||||
snapshot.collaboration.participants = collaboration_state_.participants;
|
||||
snapshot.collaboration.last_synced = collaboration_state_.last_synced;
|
||||
snapshot.multimodal.last_capture_path =
|
||||
@@ -486,8 +494,21 @@ void AgentChatWidget::RenderCollaborationPanel() {
|
||||
|
||||
const bool connected = collaboration_state_.active;
|
||||
ImGui::Text("Status: %s", connected ? "Connected" : "Not connected");
|
||||
if (!collaboration_state_.session_name.empty()) {
|
||||
ImGui::Text("Session Name: %s",
|
||||
collaboration_state_.session_name.c_str());
|
||||
}
|
||||
if (!collaboration_state_.session_id.empty()) {
|
||||
ImGui::Text("Session ID: %s", collaboration_state_.session_id.c_str());
|
||||
ImGui::Text("Session Code: %s",
|
||||
collaboration_state_.session_id.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Copy Code")) {
|
||||
ImGui::SetClipboardText(collaboration_state_.session_id.c_str());
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show("Session code copied",
|
||||
ToastType::kInfo, 2.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (collaboration_state_.last_synced != absl::InfinitePast()) {
|
||||
ImGui::TextDisabled(
|
||||
@@ -498,9 +519,10 @@ void AgentChatWidget::RenderCollaborationPanel() {
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
bool can_host = static_cast<bool>(collaboration_callbacks_.host_session);
|
||||
bool can_join = static_cast<bool>(collaboration_callbacks_.join_session);
|
||||
bool can_leave = static_cast<bool>(collaboration_callbacks_.leave_session);
|
||||
const bool can_host = static_cast<bool>(collaboration_callbacks_.host_session);
|
||||
const bool can_join = static_cast<bool>(collaboration_callbacks_.join_session);
|
||||
const bool can_leave = static_cast<bool>(collaboration_callbacks_.leave_session);
|
||||
const bool can_refresh = static_cast<bool>(collaboration_callbacks_.refresh_session);
|
||||
|
||||
ImGui::InputTextWithHint("##session_name", "Session name",
|
||||
session_name_buffer_,
|
||||
@@ -515,23 +537,23 @@ void AgentChatWidget::RenderCollaborationPanel() {
|
||||
ToastType::kWarning, 3.0f);
|
||||
}
|
||||
} else {
|
||||
absl::Status status =
|
||||
collaboration_callbacks_.host_session(name);
|
||||
if (status.ok()) {
|
||||
collaboration_state_.active = true;
|
||||
collaboration_state_.session_id = name;
|
||||
collaboration_state_.participants.clear();
|
||||
collaboration_state_.last_synced = absl::Now();
|
||||
last_collaboration_action_ = absl::Now();
|
||||
RefreshParticipants();
|
||||
auto session_or = collaboration_callbacks_.host_session(name);
|
||||
if (session_or.ok()) {
|
||||
ApplyCollaborationSession(session_or.value(), /*update_action_timestamp=*/true);
|
||||
std::snprintf(join_code_buffer_, sizeof(join_code_buffer_), "%s",
|
||||
collaboration_state_.session_id.c_str());
|
||||
session_name_buffer_[0] = '\0';
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show("Hosting collaborative session",
|
||||
ToastType::kSuccess, 3.5f);
|
||||
toast_manager_->Show(
|
||||
absl::StrFormat("Hosting session %s",
|
||||
collaboration_state_.session_id.c_str()),
|
||||
ToastType::kSuccess, 3.5f);
|
||||
}
|
||||
MarkHistoryDirty();
|
||||
} else if (toast_manager_) {
|
||||
toast_manager_->Show(
|
||||
absl::StrFormat("Failed to host: %s", status.message()),
|
||||
absl::StrFormat("Failed to host: %s",
|
||||
session_or.status().message()),
|
||||
ToastType::kError, 5.0f);
|
||||
}
|
||||
}
|
||||
@@ -556,23 +578,22 @@ void AgentChatWidget::RenderCollaborationPanel() {
|
||||
ToastType::kWarning, 3.0f);
|
||||
}
|
||||
} else {
|
||||
absl::Status status =
|
||||
collaboration_callbacks_.join_session(code);
|
||||
if (status.ok()) {
|
||||
collaboration_state_.active = true;
|
||||
collaboration_state_.session_id = code;
|
||||
collaboration_state_.last_synced = absl::Now();
|
||||
last_collaboration_action_ = absl::Now();
|
||||
RefreshParticipants();
|
||||
auto session_or = collaboration_callbacks_.join_session(code);
|
||||
if (session_or.ok()) {
|
||||
ApplyCollaborationSession(session_or.value(), /*update_action_timestamp=*/true);
|
||||
std::snprintf(join_code_buffer_, sizeof(join_code_buffer_), "%s",
|
||||
collaboration_state_.session_id.c_str());
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show(
|
||||
absl::StrFormat("Joined session %s", code.c_str()),
|
||||
absl::StrFormat("Joined session %s",
|
||||
collaboration_state_.session_id.c_str()),
|
||||
ToastType::kSuccess, 3.5f);
|
||||
}
|
||||
MarkHistoryDirty();
|
||||
} else if (toast_manager_) {
|
||||
toast_manager_->Show(
|
||||
absl::StrFormat("Failed to join: %s", status.message()),
|
||||
absl::StrFormat("Failed to join: %s",
|
||||
session_or.status().message()),
|
||||
ToastType::kError, 5.0f);
|
||||
}
|
||||
}
|
||||
@@ -592,6 +613,7 @@ void AgentChatWidget::RenderCollaborationPanel() {
|
||||
: absl::OkStatus();
|
||||
if (status.ok()) {
|
||||
collaboration_state_ = CollaborationState{};
|
||||
join_code_buffer_[0] = '\0';
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show("Left collaborative session",
|
||||
ToastType::kInfo, 3.0f);
|
||||
@@ -608,9 +630,14 @@ void AgentChatWidget::RenderCollaborationPanel() {
|
||||
|
||||
if (connected) {
|
||||
ImGui::Separator();
|
||||
if (!can_refresh) ImGui::BeginDisabled();
|
||||
if (ImGui::Button("Refresh Participants")) {
|
||||
RefreshParticipants();
|
||||
RefreshCollaboration();
|
||||
}
|
||||
if (!can_refresh && ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Provide refresh_session callback to enable");
|
||||
}
|
||||
if (!can_refresh) ImGui::EndDisabled();
|
||||
if (collaboration_state_.participants.empty()) {
|
||||
ImGui::TextDisabled("Awaiting participant list...");
|
||||
} else {
|
||||
@@ -727,26 +754,45 @@ void AgentChatWidget::RenderMultimodalPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
void AgentChatWidget::RefreshParticipants() {
|
||||
if (!collaboration_callbacks_.refresh_participants) {
|
||||
void AgentChatWidget::RefreshCollaboration() {
|
||||
if (!collaboration_callbacks_.refresh_session) {
|
||||
return;
|
||||
}
|
||||
auto participants_or = collaboration_callbacks_.refresh_participants();
|
||||
if (!participants_or.ok()) {
|
||||
auto session_or = collaboration_callbacks_.refresh_session();
|
||||
if (!session_or.ok()) {
|
||||
if (session_or.status().code() == absl::StatusCode::kNotFound) {
|
||||
collaboration_state_ = CollaborationState{};
|
||||
join_code_buffer_[0] = '\0';
|
||||
MarkHistoryDirty();
|
||||
}
|
||||
if (toast_manager_) {
|
||||
toast_manager_->Show(
|
||||
absl::StrFormat("Failed to refresh participants: %s",
|
||||
participants_or.status().message()),
|
||||
session_or.status().message()),
|
||||
ToastType::kError, 5.0f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
collaboration_state_.participants = participants_or.value();
|
||||
collaboration_state_.last_synced = absl::Now();
|
||||
ApplyCollaborationSession(session_or.value(), /*update_action_timestamp=*/false);
|
||||
MarkHistoryDirty();
|
||||
}
|
||||
|
||||
void AgentChatWidget::ApplyCollaborationSession(
|
||||
const CollaborationCallbacks::SessionContext& context,
|
||||
bool update_action_timestamp) {
|
||||
collaboration_state_.active = true;
|
||||
collaboration_state_.session_id = context.session_id;
|
||||
collaboration_state_.session_name = context.session_name.empty()
|
||||
? context.session_id
|
||||
: context.session_name;
|
||||
collaboration_state_.participants = context.participants;
|
||||
collaboration_state_.last_synced = absl::Now();
|
||||
if (update_action_timestamp) {
|
||||
last_collaboration_action_ = absl::Now();
|
||||
}
|
||||
}
|
||||
|
||||
void AgentChatWidget::MarkHistoryDirty() {
|
||||
history_dirty_ = true;
|
||||
const absl::Time now = absl::Now();
|
||||
|
||||
@@ -30,10 +30,16 @@ class AgentChatWidget {
|
||||
void SetRomContext(Rom* rom);
|
||||
|
||||
struct CollaborationCallbacks {
|
||||
std::function<absl::Status(const std::string&)> host_session;
|
||||
std::function<absl::Status(const std::string&)> join_session;
|
||||
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<std::vector<std::string>>()> refresh_participants;
|
||||
std::function<absl::StatusOr<SessionContext>()> refresh_session;
|
||||
};
|
||||
|
||||
struct MultimodalCallbacks {
|
||||
@@ -61,6 +67,7 @@ class AgentChatWidget {
|
||||
struct CollaborationState {
|
||||
bool active = false;
|
||||
std::string session_id;
|
||||
std::string session_name;
|
||||
std::vector<std::string> participants;
|
||||
absl::Time last_synced = absl::InfinitePast();
|
||||
};
|
||||
@@ -86,7 +93,10 @@ class AgentChatWidget {
|
||||
int new_total_proposals);
|
||||
void RenderCollaborationPanel();
|
||||
void RenderMultimodalPanel();
|
||||
void RefreshParticipants();
|
||||
void RefreshCollaboration();
|
||||
void ApplyCollaborationSession(
|
||||
const CollaborationCallbacks::SessionContext& context,
|
||||
bool update_action_timestamp);
|
||||
void MarkHistoryDirty();
|
||||
|
||||
cli::agent::ConversationalAgentService agent_service_;
|
||||
|
||||
350
src/app/editor/system/agent_collaboration_coordinator.cc
Normal file
350
src/app/editor/system/agent_collaboration_coordinator.cc
Normal file
@@ -0,0 +1,350 @@
|
||||
#include "app/editor/system/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 = std::getenv("HOME");
|
||||
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/system/agent_collaboration_coordinator.h
Normal file
66
src/app/editor/system/agent_collaboration_coordinator.h
Normal file
@@ -0,0 +1,66 @@
|
||||
#ifndef YAZE_APP_EDITOR_SYSTEM_AGENT_COLLABORATION_COORDINATOR_H_
|
||||
#define YAZE_APP_EDITOR_SYSTEM_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_SYSTEM_AGENT_COLLABORATION_COORDINATOR_H_
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "app/editor/system/settings_editor.h"
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/core/features.h"
|
||||
#include "app/gui/feature_flags_menu.h"
|
||||
#include "app/gfx/performance_profiler.h"
|
||||
#include "app/gui/style.h"
|
||||
#include "imgui/imgui.h"
|
||||
@@ -10,11 +10,9 @@
|
||||
namespace yaze {
|
||||
namespace editor {
|
||||
|
||||
using ImGui::BeginChild;
|
||||
using ImGui::BeginTabBar;
|
||||
using ImGui::BeginTabItem;
|
||||
using ImGui::BeginTable;
|
||||
using ImGui::EndChild;
|
||||
using ImGui::EndTabBar;
|
||||
using ImGui::EndTabItem;
|
||||
using ImGui::EndTable;
|
||||
@@ -50,7 +48,7 @@ absl::Status SettingsEditor::Update() {
|
||||
}
|
||||
|
||||
void SettingsEditor::DrawGeneralSettings() {
|
||||
static core::FlagsMenu flags;
|
||||
static gui::FlagsMenu flags;
|
||||
|
||||
if (BeginTable("##SettingsTable", 4,
|
||||
ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable |
|
||||
|
||||
Reference in New Issue
Block a user