Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,595 @@
|
||||
//! This module contains functions for loading, fetching, and merging conversation data
|
||||
//! from local database and server sources.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::FutureExt;
|
||||
use itertools::Itertools as _;
|
||||
use persistence::model::AgentConversationRecord;
|
||||
use warp_core::features::FeatureFlag;
|
||||
use warpui::{AppContext, SingletonEntity};
|
||||
|
||||
use crate::ai::agent::api::convert_conversation::{
|
||||
convert_conversation_data_to_ai_conversation, RestorationMode,
|
||||
};
|
||||
use crate::ai::agent::api::ServerConversationToken;
|
||||
use crate::ai::agent::conversation::{
|
||||
AIAgentHarness, AIConversation, AIConversationId, ServerAIConversationMetadata,
|
||||
};
|
||||
use crate::ai::agent::task::Task;
|
||||
use crate::persistence::model::{AgentConversation, AgentConversationData};
|
||||
use crate::server::server_api::ai::AIClient;
|
||||
use crate::server::server_api::ServerApiProvider;
|
||||
use crate::terminal::model::block::SerializedBlock;
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::persistence::agent::read_agent_conversation_by_id;
|
||||
|
||||
use super::{AIConversationMetadata, BlocklistAIHistoryModel, MAX_HISTORICAL_CONVERSATIONS};
|
||||
|
||||
/// A conversation transcript from a CLI agent harness (e.g. Claude Code).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CLIAgentConversation {
|
||||
/// Server metadata about this conversation.
|
||||
pub metadata: ServerAIConversationMetadata,
|
||||
/// A snapshot of the final agent TUI state.
|
||||
pub block: SerializedBlock,
|
||||
}
|
||||
|
||||
/// Representation of the conversation data that can be fetched from cloud storage.
|
||||
///
|
||||
/// The exact format depends on the agent harness that produced the conversation.
|
||||
pub enum CloudConversationData {
|
||||
/// A conversation produced by the Oz harness, which we can materialize into the
|
||||
/// [`AIConversation`] data model.
|
||||
Oz(Box<AIConversation>),
|
||||
/// A conversation produced by an external CLI agent harness.
|
||||
CLIAgent(Box<CLIAgentConversation>),
|
||||
}
|
||||
|
||||
/// Converts an `AgentConversation` from the database to an `AIConversation`.
|
||||
/// This utility function extracts the conversion logic that was originally embedded
|
||||
/// in the terminal view restoration process.
|
||||
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
|
||||
pub fn convert_persisted_conversation_to_ai_conversation(
|
||||
persisted_conversation: AgentConversation,
|
||||
) -> Option<AIConversation> {
|
||||
convert_persisted_conversation_to_ai_conversation_with_metadata(persisted_conversation)
|
||||
}
|
||||
|
||||
/// Enhanced version of the conversion function with additional metadata.
|
||||
/// This version supports the full feature set needed by terminal view restoration.
|
||||
pub fn convert_persisted_conversation_to_ai_conversation_with_metadata(
|
||||
persisted_conversation: AgentConversation,
|
||||
) -> Option<AIConversation> {
|
||||
let AgentConversation {
|
||||
tasks,
|
||||
conversation:
|
||||
AgentConversationRecord {
|
||||
conversation_id,
|
||||
conversation_data,
|
||||
..
|
||||
},
|
||||
} = persisted_conversation;
|
||||
|
||||
let conversation_id = match AIConversationId::try_from(conversation_id) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to convert conversation ID: {e:?}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let conversation_data = serde_json::from_str::<AgentConversationData>(&conversation_data).ok();
|
||||
|
||||
match AIConversation::new_restored(conversation_id, tasks, conversation_data) {
|
||||
Ok(conversation) => Some(conversation),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to convert persisted conversation to AIConversation: {e:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a conversation from the server asynchronously.
|
||||
/// This is a free-floating function that can be called without a model reference.
|
||||
pub async fn load_conversation_from_server(
|
||||
conversation_id: AIConversationId,
|
||||
server_conversation_token: ServerConversationToken,
|
||||
server_api: Arc<dyn AIClient>,
|
||||
) -> Option<CloudConversationData> {
|
||||
if !FeatureFlag::CloudConversations.is_enabled() {
|
||||
return None;
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Loading full conversation data for {conversation_id} from server using token {}",
|
||||
server_conversation_token.as_str()
|
||||
);
|
||||
|
||||
match server_api
|
||||
.get_ai_conversation(server_conversation_token.clone())
|
||||
.await
|
||||
{
|
||||
Ok((conversation_data, server_metadata)) => {
|
||||
match server_metadata.harness {
|
||||
AIAgentHarness::Oz => {
|
||||
// Convert Oz conversations to an AIConversation.
|
||||
match convert_conversation_data_to_ai_conversation(
|
||||
conversation_id,
|
||||
&conversation_data,
|
||||
server_metadata,
|
||||
RestorationMode::Continue,
|
||||
) {
|
||||
Some(conversation) => {
|
||||
log::info!("Loaded Oz conversation {conversation_id} from server");
|
||||
Some(CloudConversationData::Oz(Box::new(conversation)))
|
||||
}
|
||||
None => {
|
||||
log::warn!(
|
||||
"Failed to convert Oz server conversation data for {conversation_id}"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
AIAgentHarness::ClaudeCode | AIAgentHarness::Gemini => {
|
||||
if !FeatureFlag::AgentHarness.is_enabled() {
|
||||
log::warn!("Ignoring non-Oz conversation {conversation_id}: AgentHarness flag is disabled");
|
||||
return None;
|
||||
}
|
||||
// Fetch snapshot data for third-party harness conversations.
|
||||
match server_api
|
||||
.get_block_snapshot(server_conversation_token)
|
||||
.await
|
||||
{
|
||||
Ok(block) => {
|
||||
log::info!("Loaded CLI agent block snapshot for {conversation_id}");
|
||||
Some(CloudConversationData::CLIAgent(Box::new(
|
||||
CLIAgentConversation {
|
||||
metadata: server_metadata,
|
||||
block,
|
||||
},
|
||||
)))
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to fetch block snapshot for {conversation_id}: {e:#}"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
AIAgentHarness::Unknown => {
|
||||
log::warn!(
|
||||
"Ignoring conversation {conversation_id}: server reported an unknown harness; this client may be out of date"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to load conversation {conversation_id} from server: {e:#}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Boxes a future with the right type for the platform.
|
||||
/// On WASM, futures must not implement Send.
|
||||
fn box_future<F>(f: F) -> warpui::r#async::BoxFuture<'static, Option<CloudConversationData>>
|
||||
where
|
||||
F: Future<Output = Option<CloudConversationData>> + warpui::r#async::Spawnable,
|
||||
{
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_family = "wasm")] {
|
||||
f.boxed_local()
|
||||
} else {
|
||||
f.boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BlocklistAIHistoryModel {
|
||||
/// Loads conversation data from the appropriate source (DB or server).
|
||||
///
|
||||
/// This method automatically determines whether to load from the local database or
|
||||
/// the server based on the conversation's metadata:
|
||||
/// - If the conversation is already in memory, returns it immediately
|
||||
/// - If has_local_data is true, loads from the local database synchronously
|
||||
/// - Otherwise, loads from the server asynchronously
|
||||
///
|
||||
/// Note: This does NOT insert the conversation into memory. Callers are responsible
|
||||
/// for inserting the loaded conversation if needed.
|
||||
pub fn load_conversation_data(
|
||||
&self,
|
||||
conversation_id: AIConversationId,
|
||||
ctx: &AppContext,
|
||||
) -> warpui::r#async::BoxFuture<'static, Option<CloudConversationData>> {
|
||||
// First check if the conversation is already in memory
|
||||
if let Some(conversation) = self.conversations_by_id.get(&conversation_id) {
|
||||
return box_future(futures::future::ready(Some(CloudConversationData::Oz(
|
||||
Box::new(conversation.clone()),
|
||||
))));
|
||||
}
|
||||
|
||||
// Check metadata to determine the source
|
||||
let Some(metadata) = self
|
||||
.all_conversations_metadata
|
||||
.get(&conversation_id)
|
||||
.cloned()
|
||||
else {
|
||||
log::warn!("No metadata found for conversation {conversation_id}");
|
||||
return box_future(futures::future::ready(None));
|
||||
};
|
||||
|
||||
if metadata.has_local_data {
|
||||
// Load from local database synchronously
|
||||
let result = self
|
||||
.load_conversation_from_db(&conversation_id)
|
||||
.map(|c| CloudConversationData::Oz(Box::new(c)));
|
||||
box_future(futures::future::ready(result))
|
||||
} else {
|
||||
// Load from server asynchronously
|
||||
if let Some(server_token) = metadata.server_conversation_token {
|
||||
// Extract the server API before creating the async future
|
||||
let server_api = ServerApiProvider::as_ref(ctx).get_ai_client();
|
||||
box_future(load_conversation_from_server(
|
||||
conversation_id,
|
||||
server_token,
|
||||
server_api,
|
||||
))
|
||||
} else {
|
||||
log::warn!(
|
||||
"Cannot load conversation {conversation_id}: no local data and no server token"
|
||||
);
|
||||
box_future(futures::future::ready(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a conversation by its server token, with a server fallback.
|
||||
///
|
||||
/// First attempts to find the conversation in local metadata and load it
|
||||
/// via `load_conversation_data`. If the token is not present locally
|
||||
/// (e.g. cloud metadata hasn't been merged yet), falls back to loading
|
||||
/// the conversation directly from the server.
|
||||
///
|
||||
/// Note: This does NOT insert the conversation into memory. Callers are responsible
|
||||
/// for inserting the loaded conversation if needed.
|
||||
pub fn load_conversation_by_server_token(
|
||||
&self,
|
||||
server_token: &ServerConversationToken,
|
||||
ctx: &AppContext,
|
||||
) -> warpui::r#async::BoxFuture<'static, Option<CloudConversationData>> {
|
||||
// Fast path: token is known locally.
|
||||
if let Some(conversation_id) = self.find_conversation_id_by_server_token(server_token) {
|
||||
return self.load_conversation_data(conversation_id, ctx);
|
||||
}
|
||||
|
||||
// Fallback: load directly from the server. This handles cases where
|
||||
// cloud metadata hasn't been merged into the local history model yet
|
||||
// (e.g. timing on startup, or conversations only surfaced via
|
||||
// AgentConversationsModel).
|
||||
log::warn!(
|
||||
"No local metadata for server token {}, falling back to server fetch",
|
||||
server_token.as_str()
|
||||
);
|
||||
let server_api = ServerApiProvider::as_ref(ctx).get_ai_client();
|
||||
// Ephemeral ID — this conversation is not inserted into the history model.
|
||||
let fallback_id = AIConversationId::new();
|
||||
box_future(load_conversation_from_server(
|
||||
fallback_id,
|
||||
server_token.clone(),
|
||||
server_api,
|
||||
))
|
||||
}
|
||||
|
||||
/// Loads a conversation from local DB and returns it.
|
||||
/// This is a private helper method. Use `get_load_conversation_data_future` instead.
|
||||
///
|
||||
/// Note: This does NOT insert the conversation into memory. Callers are responsible
|
||||
/// for inserting the loaded conversation if needed.
|
||||
pub(super) fn load_conversation_from_db(
|
||||
&self,
|
||||
conversation_id: &AIConversationId,
|
||||
) -> Option<AIConversation> {
|
||||
// First check if the conversation is in memory
|
||||
if let Some(conversation) = self.conversations_by_id.get(conversation_id) {
|
||||
return Some(conversation.clone());
|
||||
}
|
||||
|
||||
// If not in memory, try to load from the database
|
||||
#[cfg(feature = "local_fs")]
|
||||
{
|
||||
let persisted_ai_conversation = self.db_connection.clone().and_then(|conn| {
|
||||
let mut conn = conn.lock().ok()?;
|
||||
|
||||
let id_str = conversation_id.to_string();
|
||||
log::info!("Loading conversation {id_str} from db");
|
||||
match read_agent_conversation_by_id(&mut conn, &id_str) {
|
||||
Ok(Some(conv)) => Some(conv),
|
||||
Ok(None) => {
|
||||
log::warn!("No AgentConversation found with id {id_str}");
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to read AgentConversation {id_str}: {e:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert the persisted conversation to an AIConversation
|
||||
if let Some(persisted_conversation) = persisted_ai_conversation {
|
||||
if let Some(conversation) =
|
||||
convert_persisted_conversation_to_ai_conversation(persisted_conversation)
|
||||
{
|
||||
return Some(conversation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Merges cloud conversation metadata with existing local metadata.
|
||||
/// Deduplicates by conversation_id and server_conversation_token.
|
||||
/// Also updates server_metadata on any already-restored conversations that match by token.
|
||||
pub fn merge_cloud_conversation_metadata(
|
||||
&mut self,
|
||||
cloud_metadata_list: Vec<ServerAIConversationMetadata>,
|
||||
) {
|
||||
let local_count = self.all_conversations_metadata.len();
|
||||
let mut local_matched_with_server_count = 0;
|
||||
let mut new_cloud_count = 0;
|
||||
let mut restored_conversations_updated = 0;
|
||||
|
||||
// Build a map from server_conversation_token to conversation_id
|
||||
let mut token_to_conv_id: HashMap<String, AIConversationId> = HashMap::new();
|
||||
for (conv_id, meta) in self.all_conversations_metadata.iter() {
|
||||
if let Some(token) = &meta.server_conversation_token {
|
||||
token_to_conv_id.insert(token.as_str().to_string(), *conv_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Build a map from server_conversation_token to conversation_id for restored conversations,
|
||||
// and collect tokens belonging to child agent conversations so we can skip them.
|
||||
let mut token_to_restored_conv_id: HashMap<String, AIConversationId> = HashMap::new();
|
||||
let mut child_conversation_tokens: HashSet<String> = HashSet::new();
|
||||
for (conv_id, conv) in self.conversations_by_id.iter() {
|
||||
if let Some(token) = conv.server_conversation_token() {
|
||||
token_to_restored_conv_id.insert(token.as_str().to_string(), *conv_id);
|
||||
if conv.is_child_agent_conversation() {
|
||||
child_conversation_tokens.insert(token.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now iterate through cloud metadata once, using the map for O(1) lookups
|
||||
for server_meta in cloud_metadata_list {
|
||||
let server_token = server_meta.server_conversation_token.clone();
|
||||
let server_token_str = server_token.as_str();
|
||||
|
||||
// Child agent conversations are managed by their parent's status card
|
||||
// and should not appear in navigation/history.
|
||||
if child_conversation_tokens.contains(server_token_str) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update any already-restored conversations that match by server token
|
||||
if let Some(conv_id) = token_to_restored_conv_id.get(server_token_str) {
|
||||
if let Some(conversation) = self.conversations_by_id.get_mut(conv_id) {
|
||||
if conversation.server_metadata().is_none() {
|
||||
conversation.set_server_metadata(server_meta.clone());
|
||||
restored_conversations_updated += 1;
|
||||
log::debug!(
|
||||
"Updated server metadata for restored conversation {conv_id} with token {server_token_str}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(conv_id) = token_to_conv_id.get(server_token_str) {
|
||||
// Found a match by token - update this entry with server metadata
|
||||
let conversation_id = *conv_id;
|
||||
let metadata =
|
||||
AIConversationMetadata::from_server_metadata(conversation_id, server_meta);
|
||||
self.server_token_to_conversation_id
|
||||
.insert(server_token.clone(), conversation_id);
|
||||
self.all_conversations_metadata
|
||||
.insert(conversation_id, metadata);
|
||||
local_matched_with_server_count += 1;
|
||||
log::debug!(
|
||||
"Matched local conversation {conversation_id} with server token {server_token_str}"
|
||||
);
|
||||
} else {
|
||||
// This is a new cloud-only conversation
|
||||
// We need to create a local AIConversationId for it
|
||||
let conversation_id = AIConversationId::new();
|
||||
let metadata =
|
||||
AIConversationMetadata::from_server_metadata(conversation_id, server_meta);
|
||||
self.server_token_to_conversation_id
|
||||
.insert(server_token.clone(), conversation_id);
|
||||
self.all_conversations_metadata
|
||||
.insert(conversation_id, metadata);
|
||||
new_cloud_count += 1;
|
||||
log::debug!(
|
||||
"Added new cloud-only conversation with local ID {conversation_id} and server token {server_token_str}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Merged cloud conversations: {} local, {} found matched cloud metadata, {} new cloud-only added, {} restored conversations updated. Total: {}",
|
||||
local_count,
|
||||
local_matched_with_server_count,
|
||||
new_cloud_count,
|
||||
restored_conversations_updated,
|
||||
self.all_conversations_metadata.len()
|
||||
);
|
||||
}
|
||||
|
||||
/// Initializes historical conversations from restored agent conversations.
|
||||
pub(super) fn initialize_historical_conversations(
|
||||
&mut self,
|
||||
conversations: &[AgentConversation],
|
||||
) {
|
||||
let conversations = conversations
|
||||
.iter()
|
||||
.sorted_by_key(|c| c.conversation.last_modified_at)
|
||||
.rev();
|
||||
|
||||
let collected: HashMap<AIConversationId, AIConversationMetadata> = conversations
|
||||
.take(MAX_HISTORICAL_CONVERSATIONS)
|
||||
.filter_map(|agent_conv| {
|
||||
// Try to convert the conversation ID
|
||||
let conversation_id = match AIConversationId::try_from(
|
||||
agent_conv.conversation.conversation_id.clone(),
|
||||
) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to convert conversation ID: {e:?}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
if !agent_conv.is_restorable() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Child agent conversations are managed by their parent's
|
||||
// status card and should not appear in navigation/history.
|
||||
// Record the parent→child mapping before filtering so that
|
||||
// create_missing_child_agent_panes can discover children
|
||||
// before they are loaded into conversations_by_id.
|
||||
let conversation_data = serde_json::from_str::<AgentConversationData>(
|
||||
&agent_conv.conversation.conversation_data,
|
||||
)
|
||||
.ok();
|
||||
if let Some(parent_id_str) = conversation_data
|
||||
.as_ref()
|
||||
.and_then(|data| data.parent_conversation_id.as_deref())
|
||||
{
|
||||
if let Ok(parent_id) = AIConversationId::try_from(parent_id_str.to_string()) {
|
||||
self.children_by_parent
|
||||
.entry(parent_id)
|
||||
.or_default()
|
||||
.push(conversation_id);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// Skip conversations that contain AutoCodeDiff system queries but do not contain any UserQuery messages.
|
||||
// These are passive, auto-initiated diffs that were never interacted with (past accepting or rejecting the diff),
|
||||
// so we don't want to list them as historical conversations.
|
||||
let mut has_user_query = false;
|
||||
let mut has_autocodediff = false;
|
||||
for task in &agent_conv.tasks {
|
||||
for message in &task.messages {
|
||||
match &message.message {
|
||||
Some(warp_multi_agent_api::message::Message::UserQuery(_)) => {
|
||||
has_user_query = true;
|
||||
}
|
||||
Some(warp_multi_agent_api::message::Message::SystemQuery(sys)) => {
|
||||
if let Some(warp_multi_agent_api::message::system_query::Type::AutoCodeDiff(_)) = &sys.r#type {
|
||||
has_autocodediff = true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if has_autocodediff && !has_user_query {
|
||||
return None;
|
||||
}
|
||||
|
||||
let root_task = agent_conv.tasks.iter().find(|task| {
|
||||
task.dependencies.is_none()
|
||||
});
|
||||
|
||||
let initial_query = root_task.map(|task| {
|
||||
// find the first task with a user query
|
||||
// (or in the case of a passive code diff, the summary of the diff)
|
||||
task.messages.iter().find_map(|msg| {
|
||||
match &msg.message {
|
||||
Some(warp_multi_agent_api::message::Message::UserQuery(user_query)) => {
|
||||
Some(user_query.query.clone())
|
||||
}
|
||||
Some(warp_multi_agent_api::message::Message::ToolCall(tool_call)) => {
|
||||
let Some(tool) = &tool_call.tool else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let warp_multi_agent_api::message::tool_call::Tool::ApplyFileDiffs(diff_suggestion) = tool {
|
||||
Some(diff_suggestion.summary.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}).unwrap_or_default()
|
||||
}).unwrap_or_default();
|
||||
|
||||
if initial_query.is_empty() {
|
||||
log::warn!(
|
||||
"Failed to record conversation with ID {conversation_id} because it was missing an initial query"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
// We derive the title from the description of the root task,
|
||||
// falling back to initial_query if the description is empty
|
||||
let title = root_task
|
||||
.map(|task| task.description.clone())
|
||||
.filter(|desc| !desc.is_empty())
|
||||
.unwrap_or_else(|| initial_query.clone());
|
||||
|
||||
// Extract working directory from the first UserQuery message in the tasks
|
||||
// TODO: search tasks in correct order once we've implemented task ordering.
|
||||
let initial_working_directory = agent_conv.tasks.iter().find_map(Task::api_task_initial_working_directory);
|
||||
let credits_spent = conversation_data
|
||||
.as_ref()
|
||||
.and_then(|data| data.conversation_usage_metadata.as_ref())
|
||||
.map(|m| m.credits_spent);
|
||||
let artifacts = conversation_data
|
||||
.as_ref()
|
||||
.and_then(|data| data.artifacts_json.as_ref())
|
||||
.and_then(|json| serde_json::from_str(json).ok())
|
||||
.unwrap_or_default();
|
||||
let server_conversation_token = conversation_data
|
||||
.and_then(|data| data.server_conversation_token)
|
||||
.map(ServerConversationToken::new);
|
||||
|
||||
Some((conversation_id, AIConversationMetadata {
|
||||
id: conversation_id,
|
||||
title,
|
||||
initial_query,
|
||||
last_modified_at: agent_conv.conversation.last_modified_at,
|
||||
initial_working_directory,
|
||||
credits_spent,
|
||||
// If we have a server token, the conversation was synced to cloud
|
||||
has_cloud_data: server_conversation_token.is_some(),
|
||||
server_conversation_token,
|
||||
has_local_data: true,
|
||||
artifacts,
|
||||
// Only populated when loading from server, not from local DB
|
||||
server_conversation_metadata: None,
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Populate the token → conversation reverse index alongside the
|
||||
// forward metadata map.
|
||||
for (conversation_id, metadata) in &collected {
|
||||
if let Some(token) = &metadata.server_conversation_token {
|
||||
self.server_token_to_conversation_id
|
||||
.insert(token.clone(), *conversation_id);
|
||||
}
|
||||
}
|
||||
self.all_conversations_metadata = collected;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user