feat: Add collaboration features to AgentChatWidget and AgentCollaborationCoordinator

This commit is contained in:
scawful
2025-10-04 15:14:53 -04:00
parent 28dc394a7c
commit d699d1133d
12 changed files with 642 additions and 66 deletions

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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();
};

View File

@@ -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();

View File

@@ -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_;

View 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

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

View File

@@ -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 |