feat: Implement AgentChatHistoryCodec for chat history persistence with JSON support

This commit is contained in:
scawful
2025-10-04 14:39:53 -04:00
parent 37e8e77376
commit ea40e676af
4 changed files with 480 additions and 284 deletions

View File

@@ -0,0 +1,336 @@
#include "app/editor/system/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"
#ifdef YAZE_WITH_GRPC
#include "nlohmann/json.hpp"
#endif
namespace yaze {
namespace editor {
namespace {
#ifdef YAZE_WITH_GRPC
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>(),
absl::UTCTimeZone(), &parsed)) {
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() {
#ifdef YAZE_WITH_GRPC
return true;
#else
return false;
#endif
}
absl::StatusOr<AgentChatHistoryCodec::Snapshot> AgentChatHistoryCodec::Load(
const std::filesystem::path& path) {
#ifdef YAZE_WITH_GRPC
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.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 (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) {
#ifdef YAZE_WITH_GRPC
Json json;
json["version"] = 2;
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["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,53 @@
#ifndef YAZE_APP_EDITOR_SYSTEM_AGENT_CHAT_HISTORY_CODEC_H_
#define YAZE_APP_EDITOR_SYSTEM_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::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_SYSTEM_AGENT_CHAT_HISTORY_CODEC_H_

View File

@@ -3,21 +3,21 @@
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "absl/strings/str_format.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "app/core/platform/file_dialog.h"
#include "app/editor/system/agent_chat_history_codec.h"
#include "app/editor/system/proposal_drawer.h"
#include "app/editor/system/toast_manager.h"
#include "app/gui/icons.h"
#include "imgui/imgui.h"
#include "imgui/misc/cpp/imgui_stdlib.h"
#include "nlohmann/json.hpp"
namespace {
@@ -47,95 +47,6 @@ std::filesystem::path ResolveHistoryPath() {
return directory / "chat_history.json";
}
absl::Time ParseTimestamp(const nlohmann::json& value) {
if (!value.is_string()) {
return absl::Now();
}
absl::Time parsed;
if (absl::ParseTime(absl::RFC3339_full, value.get<std::string>(),
absl::UTCTimeZone(), &parsed)) {
return parsed;
}
return absl::Now();
}
nlohmann::json SerializeTableData(const ChatMessage::TableData& table) {
nlohmann::json json;
json["headers"] = table.headers;
json["rows"] = table.rows;
return json;
}
std::optional<ChatMessage::TableData> ParseTableData(const nlohmann::json& json) {
if (!json.is_object()) {
return std::nullopt;
}
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;
}
nlohmann::json SerializeProposal(const ChatMessage::ProposalSummary& proposal) {
nlohmann::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<ChatMessage::ProposalSummary> ParseProposal(
const nlohmann::json& json) {
if (!json.is_object()) {
return std::nullopt;
}
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;
}
void RenderTable(const ChatMessage::TableData& table_data) {
const int column_count = static_cast<int>(table_data.headers.size());
if (column_count <= 0) {
@@ -175,6 +86,7 @@ AgentChatWidget::AgentChatWidget() {
title_ = "Agent Chat";
memset(input_buffer_, 0, sizeof(input_buffer_));
history_path_ = ResolveHistoryPath();
history_supported_ = AgentChatHistoryCodec::Available();
}
void AgentChatWidget::SetRomContext(Rom* rom) {
@@ -212,122 +124,62 @@ void AgentChatWidget::EnsureHistoryLoaded() {
return;
}
}
std::ifstream file(history_path_);
if (!file.good()) {
if (!history_supported_) {
if (!history_warning_displayed_ && toast_manager_) {
toast_manager_->Show(
"Chat history requires gRPC/JSON support and is disabled",
ToastType::kWarning, 5.0f);
history_warning_displayed_ = true;
}
return;
}
try {
nlohmann::json json;
file >> json;
if (!json.contains("messages") || !json["messages"].is_array()) {
absl::StatusOr<AgentChatHistoryCodec::Snapshot> result =
AgentChatHistoryCodec::Load(history_path_);
if (!result.ok()) {
if (result.status().code() == absl::StatusCode::kUnimplemented) {
history_supported_ = false;
if (!history_warning_displayed_ && toast_manager_) {
toast_manager_->Show(
"Chat history requires gRPC/JSON support and is disabled",
ToastType::kWarning, 5.0f);
history_warning_displayed_ = true;
}
return;
}
std::vector<ChatMessage> history;
for (const auto& item : json["messages"]) {
if (!item.is_object()) {
continue;
}
ChatMessage message;
std::string sender = item.value("sender", "agent");
message.sender =
sender == "user" ? ChatMessage::Sender::kUser
: 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()) {
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"]);
}
history.push_back(std::move(message));
}
if (!history.empty()) {
agent_service_.ReplaceHistory(std::move(history));
last_history_size_ = agent_service_.GetHistory().size();
last_proposal_count_ = CountKnownProposals();
history_dirty_ = false;
last_persist_time_ = absl::Now();
if (toast_manager_) {
toast_manager_->Show("Restored chat history",
ToastType::kInfo, 3.5f);
}
}
if (json.contains("collaboration") && json["collaboration"].is_object()) {
const auto& collab_json = json["collaboration"];
collaboration_state_.active = collab_json.value("active", false);
collaboration_state_.session_id = collab_json.value("session_id", "");
collaboration_state_.participants.clear();
if (collab_json.contains("participants") &&
collab_json["participants"].is_array()) {
for (const auto& participant : collab_json["participants"]) {
if (participant.is_string()) {
collaboration_state_.participants.push_back(
participant.get<std::string>());
}
}
}
if (collab_json.contains("last_synced")) {
collaboration_state_.last_synced =
ParseTimestamp(collab_json["last_synced"]);
}
}
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 = multimodal_json["last_capture_path"].get<std::string>();
if (!path.empty()) {
multimodal_state_.last_capture_path = std::filesystem::path(path);
}
}
multimodal_state_.status_message =
multimodal_json.value("status_message", "");
if (multimodal_json.contains("last_updated")) {
multimodal_state_.last_updated =
ParseTimestamp(multimodal_json["last_updated"]);
}
}
} catch (const std::exception& e) {
if (toast_manager_) {
toast_manager_->Show(
absl::StrFormat("Failed to load chat history: %s", e.what()),
absl::StrFormat("Failed to load chat history: %s",
result.status().ToString()),
ToastType::kError, 6.0f);
}
return;
}
AgentChatHistoryCodec::Snapshot snapshot = std::move(result.value());
if (!snapshot.history.empty()) {
agent_service_.ReplaceHistory(std::move(snapshot.history));
last_history_size_ = agent_service_.GetHistory().size();
last_proposal_count_ = CountKnownProposals();
history_dirty_ = false;
last_persist_time_ = absl::Now();
if (toast_manager_) {
toast_manager_->Show("Restored chat history",
ToastType::kInfo, 3.5f);
}
}
collaboration_state_.active = snapshot.collaboration.active;
collaboration_state_.session_id = snapshot.collaboration.session_id;
collaboration_state_.participants = snapshot.collaboration.participants;
collaboration_state_.last_synced = snapshot.collaboration.last_synced;
multimodal_state_.last_capture_path =
snapshot.multimodal.last_capture_path;
multimodal_state_.status_message = snapshot.multimodal.status_message;
multimodal_state_.last_updated = snapshot.multimodal.last_updated;
}
void AgentChatWidget::PersistHistory() {
@@ -335,98 +187,51 @@ void AgentChatWidget::PersistHistory() {
return;
}
const auto& history = agent_service_.GetHistory();
nlohmann::json json;
json["version"] = 2;
json["messages"] = nlohmann::json::array();
for (const auto& message : history) {
nlohmann::json entry;
entry["sender"] =
message.sender == 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;
nlohmann::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));
}
nlohmann::json collab_json;
collab_json["active"] = collaboration_state_.active;
collab_json["session_id"] = collaboration_state_.session_id;
collab_json["participants"] = collaboration_state_.participants;
if (collaboration_state_.last_synced != absl::InfinitePast()) {
collab_json["last_synced"] = absl::FormatTime(
absl::RFC3339_full, collaboration_state_.last_synced,
absl::UTCTimeZone());
}
json["collaboration"] = std::move(collab_json);
nlohmann::json multimodal_json;
if (multimodal_state_.last_capture_path.has_value()) {
multimodal_json["last_capture_path"] =
multimodal_state_.last_capture_path->string();
} else {
multimodal_json["last_capture_path"] = "";
}
multimodal_json["status_message"] = multimodal_state_.status_message;
if (multimodal_state_.last_updated != absl::InfinitePast()) {
multimodal_json["last_updated"] = absl::FormatTime(
absl::RFC3339_full, multimodal_state_.last_updated,
absl::UTCTimeZone());
}
json["multimodal"] = std::move(multimodal_json);
std::error_code ec;
auto directory = history_path_.parent_path();
if (!directory.empty()) {
std::filesystem::create_directories(directory, ec);
if (ec) {
if (toast_manager_) {
toast_manager_->Show(
"Unable to create chat history directory",
ToastType::kError, 5.0f);
}
return;
}
}
std::ofstream file(history_path_);
if (!file.is_open()) {
if (toast_manager_) {
toast_manager_->Show("Cannot write chat history",
ToastType::kError, 5.0f);
if (!history_supported_) {
history_dirty_ = false;
if (!history_warning_displayed_ && toast_manager_) {
toast_manager_->Show(
"Chat history requires gRPC/JSON support and is disabled",
ToastType::kWarning, 5.0f);
history_warning_displayed_ = true;
}
return;
}
AgentChatHistoryCodec::Snapshot snapshot;
snapshot.history = agent_service_.GetHistory();
snapshot.collaboration.active = collaboration_state_.active;
snapshot.collaboration.session_id = collaboration_state_.session_id;
snapshot.collaboration.participants = collaboration_state_.participants;
snapshot.collaboration.last_synced = collaboration_state_.last_synced;
snapshot.multimodal.last_capture_path =
multimodal_state_.last_capture_path;
snapshot.multimodal.status_message = multimodal_state_.status_message;
snapshot.multimodal.last_updated = multimodal_state_.last_updated;
absl::Status status = AgentChatHistoryCodec::Save(history_path_, snapshot);
if (!status.ok()) {
if (status.code() == absl::StatusCode::kUnimplemented) {
history_supported_ = false;
if (!history_warning_displayed_ && toast_manager_) {
toast_manager_->Show(
"Chat history requires gRPC/JSON support and is disabled",
ToastType::kWarning, 5.0f);
history_warning_displayed_ = true;
}
history_dirty_ = false;
return;
}
if (toast_manager_) {
toast_manager_->Show(
absl::StrFormat("Failed to persist chat history: %s",
status.ToString()),
ToastType::kError, 6.0f);
}
return;
}
file << json.dump(2);
history_dirty_ = false;
last_persist_time_ = absl::Now();
}

View File

@@ -96,6 +96,8 @@ class AgentChatWidget {
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;