diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index b60bddd0fe4b..c677d03796fc 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -14,8 +14,9 @@ use rmcp::model::{ use utoipa::{OpenApi, ToSchema}; use goose::conversation::message::{ - ContextLengthExceeded, FrontendToolRequest, Message, MessageContent, RedactedThinkingContent, - SummarizationRequested, ThinkingContent, ToolConfirmationRequest, ToolRequest, ToolResponse, + ContextLengthExceeded, FrontendToolRequest, Message, MessageContent, MessageMetadata, + RedactedThinkingContent, SummarizationRequested, ThinkingContent, ToolConfirmationRequest, + ToolRequest, ToolResponse, }; use utoipa::openapi::schema::{ AdditionalProperties, AnyOfBuilder, ArrayBuilder, ObjectBuilder, OneOfBuilder, Schema, @@ -421,6 +422,7 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema { super::routes::session::SessionHistoryResponse, Message, MessageContent, + MessageMetadata, ContentSchema, EmbeddedResourceSchema, ImageContentSchema, diff --git a/crates/goose-server/src/routes/context.rs b/crates/goose-server/src/routes/context.rs index 0a1899252124..205d2837c6b4 100644 --- a/crates/goose-server/src/routes/context.rs +++ b/crates/goose-server/src/routes/context.rs @@ -62,7 +62,12 @@ async fn manage_context( } Ok(Json(ContextManageResponse { - messages: processed_messages.messages().clone(), + messages: processed_messages + .messages() + .iter() + .filter(|m| m.is_user_visible()) + .cloned() + .collect(), token_counts, })) } diff --git a/crates/goose-server/src/routes/reply.rs b/crates/goose-server/src/routes/reply.rs index 34c4a8f60853..880f6746796d 100644 --- a/crates/goose-server/src/routes/reply.rs +++ b/crates/goose-server/src/routes/reply.rs @@ -309,7 +309,11 @@ async fn reply_handler( } all_messages.push(message.clone()); - stream_event(MessageEvent::Message { message }, &tx, &cancel_token).await; + + // Only send message to client if it's user_visible + if message.is_user_visible() { + stream_event(MessageEvent::Message { message }, &tx, &cancel_token).await; + } } Ok(Some(Ok(AgentEvent::HistoryReplaced(new_messages)))) => { // Replace the message history with the compacted messages diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index c3cd61c8778b..c8d987c7431c 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -124,10 +124,18 @@ async fn get_session_history( } }; + // Filter messages to only include user_visible ones + let user_visible_messages: Vec = messages + .messages() + .iter() + .filter(|m| m.is_user_visible()) + .cloned() + .collect(); + Ok(Json(SessionHistoryResponse { session_id, metadata, - messages: messages.messages().clone(), + messages: user_visible_messages, })) } diff --git a/crates/goose/src/agents/context.rs b/crates/goose/src/agents/context.rs index 1a6cd5b64a5c..8b594bbc0ab5 100644 --- a/crates/goose/src/agents/context.rs +++ b/crates/goose/src/agents/context.rs @@ -1,6 +1,6 @@ use anyhow::Ok; -use crate::conversation::message::Message; +use crate::conversation::message::{Message, MessageMetadata}; use crate::conversation::Conversation; use crate::token_counter::create_async_token_counter; @@ -64,17 +64,8 @@ impl Agent { let provider = self.provider().await?; let summary_result = summarize_messages(provider.clone(), messages).await?; - let (mut new_messages, mut new_token_counts, summarization_usage) = match summary_result { - Some((summary_message, provider_usage)) => { - // For token counting purposes, we use the output tokens (the actual summary content) - // since that's what will be in the context going forward - let total_tokens = provider_usage.usage.output_tokens.unwrap_or(0) as usize; - ( - vec![summary_message], - vec![total_tokens], - Some(provider_usage), - ) - } + let (summary_message, summarization_usage) = match summary_result { + Some((summary_message, provider_usage)) => (summary_message, Some(provider_usage)), None => { // No summary was generated (empty input) tracing::warn!("Summarization failed. Returning empty messages."); @@ -82,30 +73,55 @@ impl Agent { } }; - // Add an assistant message to the summarized messages to ensure the assistant's response is included in the context. - if new_messages.len() == 1 { - let compaction_marker = Message::assistant() - .with_summarization_requested("Conversation compacted and summarized"); - let compaction_marker_tokens: usize = 8; - - // Insert the marker before the summary message - new_messages.insert(0, compaction_marker); - new_token_counts.insert(0, compaction_marker_tokens); - - // Add an assistant message to continue the conversation - let assistant_message = Message::assistant().with_text(" - The previous message contains a summary that was prepared because a context limit was reached. - Do not mention that you read a summary or that conversation summarization occurred - Just continue the conversation naturally based on the summarized context - "); - let assistant_message_tokens: usize = 41; - new_messages.push(assistant_message); - new_token_counts.push(assistant_message_tokens); + // Create the final message list with updated visibility metadata: + // 1. Original messages become user_visible but not agent_visible + // 2. Summary message becomes agent_visible but not user_visible + // 3. Assistant messages to continue the conversation remain both user_visible and agent_visible + + let mut final_messages = Vec::new(); + let mut final_token_counts = Vec::new(); + + // Add all original messages with updated visibility (preserve user_visible, set agent_visible=false) + for msg in messages.iter().cloned() { + let updated_metadata = msg.metadata.with_agent_invisible(); + let updated_msg = msg.with_metadata(updated_metadata); + final_messages.push(updated_msg); + // Token count doesn't matter for agent_visible=false messages, but we'll use 0 + final_token_counts.push(0); } + // Add the compaction marker (user_visible=true, agent_visible=false) + let compaction_marker = Message::assistant() + .with_summarization_requested("Conversation compacted and summarized") + .with_metadata(MessageMetadata::user_only()); + let compaction_marker_tokens: usize = 0; // Not counted since agent_visible=false + final_messages.push(compaction_marker); + final_token_counts.push(compaction_marker_tokens); + + // Add the summary message (agent_visible=true, user_visible=false) + let summary_msg = summary_message.with_metadata(MessageMetadata::agent_only()); + // For token counting purposes, we use the output tokens (the actual summary content) + // since that's what will be in the context going forward + let summary_tokens = summarization_usage + .as_ref() + .and_then(|usage| usage.usage.output_tokens) + .unwrap_or(0) as usize; + final_messages.push(summary_msg); + final_token_counts.push(summary_tokens); + + // Add an assistant message to continue the conversation (agent_visible=true, user_visible=false) + let assistant_message = Message::assistant().with_text(" + The previous message contains a summary that was prepared because a context limit was reached. + Do not mention that you read a summary or that conversation summarization occurred + Just continue the conversation naturally based on the summarized context + ").with_metadata(MessageMetadata::agent_only()); + let assistant_message_tokens: usize = 0; // Not counted since it's for agent context only + final_messages.push(assistant_message); + final_token_counts.push(assistant_message_tokens); + Ok(( - Conversation::new_unvalidated(new_messages), - new_token_counts, + Conversation::new_unvalidated(final_messages), + final_token_counts, summarization_usage, )) } diff --git a/crates/goose/src/agents/reply_parts.rs b/crates/goose/src/agents/reply_parts.rs index 121631aedade..ba6aa8d09284 100644 --- a/crates/goose/src/agents/reply_parts.rs +++ b/crates/goose/src/agents/reply_parts.rs @@ -245,12 +245,13 @@ impl Agent { } } - let filtered_message = Message { - id: response.id.clone(), - role: response.role.clone(), - created: response.created, - content: filtered_content, - }; + let mut filtered_message = + Message::new(response.role.clone(), response.created, filtered_content); + + // Preserve the ID if it exists + if let Some(id) = response.id.clone() { + filtered_message = filtered_message.with_id(id); + } // Categorize tool requests let mut frontend_requests = Vec::new(); diff --git a/crates/goose/src/context_mgmt/auto_compact.rs b/crates/goose/src/context_mgmt/auto_compact.rs index 444990b4ebb8..a86f7d5a07ca 100644 --- a/crates/goose/src/context_mgmt/auto_compact.rs +++ b/crates/goose/src/context_mgmt/auto_compact.rs @@ -207,8 +207,35 @@ pub async fn check_and_compact_messages( check_result.usage_ratio * 100.0 ); - // Use perform_compaction to do the actual work - perform_compaction(agent, messages).await + // Check if the most recent message is a user message + let (messages_to_compact, preserved_user_message) = if let Some(last_message) = messages.last() + { + if matches!(last_message.role, rmcp::model::Role::User) { + // Remove the last user message before auto-compaction + (&messages[..messages.len() - 1], Some(last_message.clone())) + } else { + (messages, None) + } + } else { + (messages, None) + }; + + // Perform the compaction on messages excluding the preserved user message + // The summarize_context method already handles the visibility properly + let (mut summary_messages, _, summarization_usage) = + agent.summarize_context(messages_to_compact).await?; + + // Add back the preserved user message if it exists + // (keeps default visibility: both true) + if let Some(user_message) = preserved_user_message { + summary_messages.push(user_message); + } + + Ok(AutoCompactResult { + compacted: true, + messages: summary_messages, + summarization_usage, + }) } #[cfg(test)] @@ -455,8 +482,9 @@ mod tests { ); } - // Should have fewer messages (summarized) - assert!(result.messages.len() <= messages.len()); + // After visibility implementation, we keep all messages plus summary + // Original messages become user_visible only, summary becomes agent_visible only + assert!(result.messages.len() > messages.len()); } #[tokio::test] @@ -641,8 +669,9 @@ mod tests { // Verify the compacted messages are returned assert!(!result.messages.is_empty()); - // Should have fewer messages after compaction - assert!(result.messages.len() <= messages.len()); + // After visibility implementation, we keep all messages plus summary + // Original messages become user_visible only, summary becomes agent_visible only + assert!(result.messages.len() > messages.len()); } #[tokio::test] diff --git a/crates/goose/src/conversation/message.rs b/crates/goose/src/conversation/message.rs index 3445f5f41a13..db445b2e9b60 100644 --- a/crates/goose/src/conversation/message.rs +++ b/crates/goose/src/conversation/message.rs @@ -373,6 +373,89 @@ impl From for Message { } } +#[derive(ToSchema, Clone, Copy, PartialEq, Serialize, Deserialize)] +/// Metadata for message visibility +#[serde(rename_all = "camelCase")] +pub struct MessageMetadata { + /// Whether the message should be visible to the user in the UI + #[serde(default = "default_true")] + pub user_visible: bool, + /// Whether the message should be included in the agent's context window + #[serde(default = "default_true")] + pub agent_visible: bool, +} + +impl Default for MessageMetadata { + fn default() -> Self { + MessageMetadata { + user_visible: true, + agent_visible: true, + } + } +} + +impl MessageMetadata { + /// Create metadata for messages visible only to the agent + pub fn agent_only() -> Self { + MessageMetadata { + user_visible: false, + agent_visible: true, + } + } + + /// Create metadata for messages visible only to the user + pub fn user_only() -> Self { + MessageMetadata { + user_visible: true, + agent_visible: false, + } + } + + /// Create metadata for messages visible to neither user nor agent (archived) + pub fn invisible() -> Self { + MessageMetadata { + user_visible: false, + agent_visible: false, + } + } + + /// Return a copy with agent_visible set to false + pub fn with_agent_invisible(self) -> Self { + Self { + agent_visible: false, + ..self + } + } + + /// Return a copy with user_visible set to false + pub fn with_user_invisible(self) -> Self { + Self { + user_visible: false, + ..self + } + } + + /// Return a copy with agent_visible set to true + pub fn with_agent_visible(self) -> Self { + Self { + agent_visible: true, + ..self + } + } + + /// Return a copy with user_visible set to true + pub fn with_user_visible(self) -> Self { + Self { + user_visible: true, + ..self + } + } +} + +fn default_true() -> bool { + true +} + #[derive(ToSchema, Clone, PartialEq, Serialize, Deserialize)] /// A message to or from an LLM #[serde(rename_all = "camelCase")] @@ -383,6 +466,8 @@ pub struct Message { pub created: i64, #[serde(deserialize_with = "deserialize_sanitized_content")] pub content: Vec, + #[serde(default)] + pub metadata: MessageMetadata, } impl fmt::Debug for Message { @@ -409,6 +494,7 @@ impl Message { role, created, content, + metadata: MessageMetadata::default(), } } pub fn debug(&self) -> String { @@ -422,6 +508,7 @@ impl Message { role: Role::User, created: Utc::now().timestamp(), content: Vec::new(), + metadata: MessageMetadata::default(), } } @@ -432,6 +519,7 @@ impl Message { role: Role::Assistant, created: Utc::now().timestamp(), content: Vec::new(), + metadata: MessageMetadata::default(), } } @@ -597,11 +685,48 @@ impl Message { pub fn with_summarization_requested>(self, msg: S) -> Self { self.with_content(MessageContent::summarization_requested(msg)) } + + /// Set the visibility metadata for the message + pub fn with_visibility(mut self, user_visible: bool, agent_visible: bool) -> Self { + self.metadata.user_visible = user_visible; + self.metadata.agent_visible = agent_visible; + self + } + + /// Set the entire metadata for the message + pub fn with_metadata(mut self, metadata: MessageMetadata) -> Self { + self.metadata = metadata; + self + } + + /// Mark the message as only visible to the user (not the agent) + pub fn user_only(mut self) -> Self { + self.metadata.user_visible = true; + self.metadata.agent_visible = false; + self + } + + /// Mark the message as only visible to the agent (not the user) + pub fn agent_only(mut self) -> Self { + self.metadata.user_visible = false; + self.metadata.agent_visible = true; + self + } + + /// Check if the message is visible to the user + pub fn is_user_visible(&self) -> bool { + self.metadata.user_visible + } + + /// Check if the message is visible to the agent + pub fn is_agent_visible(&self) -> bool { + self.metadata.agent_visible + } } #[cfg(test)] mod tests { - use crate::conversation::message::{Message, MessageContent}; + use crate::conversation::message::{Message, MessageContent, MessageMetadata}; use crate::conversation::*; use mcp_core::ToolCall; use rmcp::model::{ @@ -953,4 +1078,148 @@ mod tests { assert_eq!(message.as_concat_text(), "Hello world δΈ–η•Œ 🌍"); } + + #[test] + fn test_message_metadata_defaults() { + let message = Message::user().with_text("Test"); + + // By default, messages should be both user and agent visible + assert!(message.is_user_visible()); + assert!(message.is_agent_visible()); + } + + #[test] + fn test_message_visibility_methods() { + // Test user_only + let user_only_msg = Message::user().with_text("User only").user_only(); + assert!(user_only_msg.is_user_visible()); + assert!(!user_only_msg.is_agent_visible()); + + // Test agent_only + let agent_only_msg = Message::assistant().with_text("Agent only").agent_only(); + assert!(!agent_only_msg.is_user_visible()); + assert!(agent_only_msg.is_agent_visible()); + + // Test with_visibility + let custom_msg = Message::user() + .with_text("Custom visibility") + .with_visibility(false, true); + assert!(!custom_msg.is_user_visible()); + assert!(custom_msg.is_agent_visible()); + } + + #[test] + fn test_message_metadata_serialization() { + let message = Message::user() + .with_text("Test message") + .with_visibility(false, true); + + let json_str = serde_json::to_string(&message).unwrap(); + let value: Value = serde_json::from_str(&json_str).unwrap(); + + assert_eq!(value["metadata"]["userVisible"], false); + assert_eq!(value["metadata"]["agentVisible"], true); + } + + #[test] + fn test_message_metadata_deserialization() { + // Test with explicit metadata + let json_with_metadata = r#"{ + "role": "user", + "created": 1640995200, + "content": [{ + "type": "text", + "text": "Test" + }], + "metadata": { + "userVisible": false, + "agentVisible": true + } + }"#; + + let message: Message = serde_json::from_str(json_with_metadata).unwrap(); + assert!(!message.is_user_visible()); + assert!(message.is_agent_visible()); + + // Test without metadata (should use defaults) + let json_without_metadata = r#"{ + "role": "user", + "created": 1640995200, + "content": [{ + "type": "text", + "text": "Test" + }] + }"#; + + let message: Message = serde_json::from_str(json_without_metadata).unwrap(); + assert!(message.is_user_visible()); + assert!(message.is_agent_visible()); + } + + #[test] + fn test_message_metadata_static_methods() { + // Test MessageMetadata::agent_only() + let agent_only_metadata = MessageMetadata::agent_only(); + assert!(!agent_only_metadata.user_visible); + assert!(agent_only_metadata.agent_visible); + + // Test MessageMetadata::user_only() + let user_only_metadata = MessageMetadata::user_only(); + assert!(user_only_metadata.user_visible); + assert!(!user_only_metadata.agent_visible); + + // Test MessageMetadata::invisible() + let invisible_metadata = MessageMetadata::invisible(); + assert!(!invisible_metadata.user_visible); + assert!(!invisible_metadata.agent_visible); + + // Test using them with messages + let agent_msg = Message::assistant() + .with_text("Agent only message") + .with_metadata(MessageMetadata::agent_only()); + assert!(!agent_msg.is_user_visible()); + assert!(agent_msg.is_agent_visible()); + + let user_msg = Message::user() + .with_text("User only message") + .with_metadata(MessageMetadata::user_only()); + assert!(user_msg.is_user_visible()); + assert!(!user_msg.is_agent_visible()); + + let invisible_msg = Message::user() + .with_text("Invisible message") + .with_metadata(MessageMetadata::invisible()); + assert!(!invisible_msg.is_user_visible()); + assert!(!invisible_msg.is_agent_visible()); + } + + #[test] + fn test_message_metadata_builder_methods() { + // Test with_agent_invisible + let metadata = MessageMetadata::default().with_agent_invisible(); + assert!(metadata.user_visible); + assert!(!metadata.agent_visible); + + // Test with_user_invisible + let metadata = MessageMetadata::default().with_user_invisible(); + assert!(!metadata.user_visible); + assert!(metadata.agent_visible); + + // Test with_agent_visible + let metadata = MessageMetadata::invisible().with_agent_visible(); + assert!(!metadata.user_visible); + assert!(metadata.agent_visible); + + // Test with_user_visible + let metadata = MessageMetadata::invisible().with_user_visible(); + assert!(metadata.user_visible); + assert!(!metadata.agent_visible); + + // Test chaining + let metadata = MessageMetadata::invisible() + .with_user_visible() + .with_agent_visible(); + assert!(metadata.user_visible); + assert!(metadata.agent_visible); + } } diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index a71bc2113a29..2c17d452f817 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -328,6 +328,7 @@ pub trait Provider: Send + Sync { ) -> Result<(Message, ProviderUsage), ProviderError>; // Default implementation: use the provider's configured model + // This method filters messages to only include agent_visible ones async fn complete( &self, system: &str, @@ -335,11 +336,20 @@ pub trait Provider: Send + Sync { tools: &[Tool], ) -> Result<(Message, ProviderUsage), ProviderError> { let model_config = self.get_model_config(); - self.complete_with_model(&model_config, system, messages, tools) + + // Filter messages to only include agent_visible ones + let agent_visible_messages: Vec = messages + .iter() + .filter(|m| m.is_agent_visible()) + .cloned() + .collect(); + + self.complete_with_model(&model_config, system, &agent_visible_messages, tools) .await } // Check if a fast model is configured, otherwise fall back to regular model + // This method filters messages to only include agent_visible ones async fn complete_fast( &self, system: &str, @@ -349,8 +359,15 @@ pub trait Provider: Send + Sync { let model_config = self.get_model_config(); let fast_config = model_config.use_fast_model(); + // Filter messages to only include agent_visible ones + let agent_visible_messages: Vec = messages + .iter() + .filter(|m| m.is_agent_visible()) + .cloned() + .collect(); + match self - .complete_with_model(&fast_config, system, messages, tools) + .complete_with_model(&fast_config, system, &agent_visible_messages, tools) .await { Ok(result) => Ok(result), @@ -362,7 +379,7 @@ pub trait Provider: Send + Sync { e, model_config.model_name ); - self.complete_with_model(&model_config, system, messages, tools) + self.complete_with_model(&model_config, system, &agent_visible_messages, tools) .await } else { Err(e) diff --git a/crates/goose/src/providers/claude_code.rs b/crates/goose/src/providers/claude_code.rs index 9ef2950c272c..b474432b3607 100644 --- a/crates/goose/src/providers/claude_code.rs +++ b/crates/goose/src/providers/claude_code.rs @@ -282,12 +282,11 @@ impl ClaudeCodeProvider { let message_content = vec![MessageContent::text(combined_text)]; - let response_message = Message { - id: None, - role: Role::Assistant, - created: chrono::Utc::now().timestamp(), - content: message_content, - }; + let response_message = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + message_content, + ); Ok((response_message, usage)) } @@ -433,12 +432,11 @@ impl ClaudeCodeProvider { println!("================================"); } - let message = Message { - id: None, - role: Role::Assistant, - created: chrono::Utc::now().timestamp(), - content: vec![MessageContent::text(description.clone())], - }; + let message = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + vec![MessageContent::text(description.clone())], + ); let usage = Usage::default(); diff --git a/crates/goose/src/providers/cursor_agent.rs b/crates/goose/src/providers/cursor_agent.rs index bbce315f648b..a3f0d12858db 100644 --- a/crates/goose/src/providers/cursor_agent.rs +++ b/crates/goose/src/providers/cursor_agent.rs @@ -214,12 +214,11 @@ impl CursorAgentProvider { }; let message_content = vec![MessageContent::text(text_content)]; - let response_message = Message { - id: None, - role: Role::Assistant, - created: chrono::Utc::now().timestamp(), - content: message_content, - }; + let response_message = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + message_content, + ); let usage = Usage::default(); @@ -233,12 +232,11 @@ impl CursorAgentProvider { let response_text = lines.join("\n"); let message_content = vec![MessageContent::text(response_text)]; - let response_message = Message { - id: None, - role: Role::Assistant, - created: chrono::Utc::now().timestamp(), - content: message_content, - }; + let response_message = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + message_content, + ); let usage = Usage::default(); Ok((response_message, usage)) @@ -366,12 +364,11 @@ impl CursorAgentProvider { println!("================================"); } - let message = Message { - id: None, - role: Role::Assistant, - created: chrono::Utc::now().timestamp(), - content: vec![MessageContent::text(description.clone())], - }; + let message = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + vec![MessageContent::text(description.clone())], + ); let usage = Usage::default(); diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index 3ff4b712a867..897293109e73 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -534,23 +534,35 @@ where } } + let mut msg = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + contents, + ); + + // Add ID if present + if let Some(id) = chunk.id { + msg = msg.with_id(id); + } + yield ( - Some(Message { - id: chunk.id, - role: Role::Assistant, - created: chrono::Utc::now().timestamp(), - content: contents, - }), + Some(msg), usage, ) } else if let Some(text) = &chunk.choices[0].delta.content { + let mut msg = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + vec![MessageContent::text(text)], + ); + + // Add ID if present + if let Some(id) = chunk.id { + msg = msg.with_id(id); + } + yield ( - Some(Message { - id: chunk.id, - role: Role::Assistant, - created: chrono::Utc::now().timestamp(), - content: vec![MessageContent::text(text)], - }), + Some(msg), if chunk.choices[0].finish_reason.is_some() { usage } else { diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 46e18a0fd2a4..d52509a9b285 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -2562,6 +2562,9 @@ "type": "string", "nullable": true }, + "metadata": { + "$ref": "#/components/schemas/MessageMetadata" + }, "role": { "$ref": "#/components/schemas/Role" } @@ -2785,6 +2788,20 @@ "propertyName": "type" } }, + "MessageMetadata": { + "type": "object", + "description": "Metadata for message visibility", + "properties": { + "agentVisible": { + "type": "boolean", + "description": "Whether the message should be included in the agent's context window" + }, + "userVisible": { + "type": "boolean", + "description": "Whether the message should be visible to the user in the UI" + } + } + }, "ModelInfo": { "type": "object", "description": "Information about a model's capabilities", diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 6dba19df178a..bf6a30e182c6 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -360,6 +360,7 @@ export type Message = { content: Array; created?: number; id?: string | null; + metadata?: MessageMetadata; role: Role; }; @@ -388,6 +389,20 @@ export type MessageContent = (TextContent & { type: 'summarizationRequested'; }); +/** + * Metadata for message visibility + */ +export type MessageMetadata = { + /** + * Whether the message should be included in the agent's context window + */ + agentVisible?: boolean; + /** + * Whether the message should be visible to the user in the UI + */ + userVisible?: boolean; +}; + /** * Information about a model's capabilities */ diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx index f074969dce83..aace7068a8f0 100644 --- a/ui/desktop/src/components/BaseChat.tsx +++ b/ui/desktop/src/components/BaseChat.tsx @@ -165,7 +165,6 @@ function BaseChatContent({ const { messages, filteredMessages, - setAncestorMessages, append, chatState, error, @@ -238,14 +237,13 @@ function BaseChatContent({ console.log('Switching from recipe:', previousTitle, 'to:', newTitle); setHasStartedUsingRecipe(false); setMessages([]); - setAncestorMessages([]); } else if (isInitialRecipeLoad) { setHasStartedUsingRecipe(false); } else if (hasExistingConversation) { setHasStartedUsingRecipe(true); } } - }, [recipeConfig?.title, currentRecipeTitle, messages.length, setMessages, setAncestorMessages]); + }, [recipeConfig?.title, currentRecipeTitle, messages.length, setMessages]); // Handle recipe auto-execution useEffect(() => { @@ -448,12 +446,7 @@ function BaseChatContent({ onClick={async () => { clearError(); - await handleManualCompaction( - messages, - setMessages, - append, - setAncestorMessages - ); + await handleManualCompaction(messages, setMessages, append); }} > Summarize Conversation @@ -533,7 +526,6 @@ function BaseChatContent({ initialPrompt={initialPrompt} toolCount={toolCount || 0} autoSubmit={autoSubmit} - setAncestorMessages={setAncestorMessages} append={append} {...customChatInputProps} /> diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index 514aab69375c..60d8982c8f9e 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -86,7 +86,6 @@ interface ChatInputProps { initialPrompt?: string; toolCount: number; autoSubmit: boolean; - setAncestorMessages?: (messages: Message[]) => void; append?: (message: Message) => void; isExtensionsLoading?: boolean; } @@ -115,7 +114,6 @@ export default function ChatInput({ toolCount, autoSubmit = false, append, - setAncestorMessages, isExtensionsLoading = false, }: ChatInputProps) { const [_value, setValue] = useState(initialValue); @@ -570,7 +568,7 @@ export default function ChatInput({ // Hide the alert popup by dispatching a custom event that the popover can listen to // Importantly, this leaves the alert so the dot still shows up, but hides the popover window.dispatchEvent(new CustomEvent('hide-alert-popover')); - handleManualCompaction(messages, setMessages, append, setAncestorMessages); + handleManualCompaction(messages, setMessages, append); }, compactIcon: , autoCompactThreshold: autoCompactThreshold, diff --git a/ui/desktop/src/components/context_management/ContextManager.tsx b/ui/desktop/src/components/context_management/ContextManager.tsx index d2f660eeb558..f95e396cb395 100644 --- a/ui/desktop/src/components/context_management/ContextManager.tsx +++ b/ui/desktop/src/components/context_management/ContextManager.tsx @@ -12,14 +12,12 @@ interface ContextManagerActions { handleAutoCompaction: ( messages: Message[], setMessages: (messages: Message[]) => void, - append: (message: Message) => void, - setAncestorMessages?: (messages: Message[]) => void + append: (message: Message) => void ) => Promise; handleManualCompaction: ( messages: Message[], setMessages: (messages: Message[]) => void, - append?: (message: Message) => void, - setAncestorMessages?: (messages: Message[]) => void + append?: (message: Message) => void ) => Promise; hasCompactionMarker: (message: Message) => boolean; } @@ -39,8 +37,7 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = ( messages: Message[], setMessages: (messages: Message[]) => void, append: (message: Message) => void, - isManual: boolean = false, - setAncestorMessages?: (messages: Message[]) => void + isManual: boolean = false ) => { setIsCompacting(true); setCompactionError(null); @@ -53,29 +50,10 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = ( }); // Convert API messages to frontend messages - const convertedMessages = summaryResponse.messages.map((apiMessage) => { - const isCompactionMarker = apiMessage.content.some( - (content) => content.type === 'summarizationRequested' - ); - - if (isCompactionMarker) { - // show to user but not model - return convertApiMessageToFrontendMessage(apiMessage, true, false); - } - - // show to model but not user - return convertApiMessageToFrontendMessage(apiMessage, false, true); - }); - - // Store the original messages as ancestor messages so they can still be scrolled to - if (setAncestorMessages) { - const ancestorMessages = messages.map((msg) => ({ - ...msg, - display: msg.display === false ? false : true, - sendToLLM: false, - })); - setAncestorMessages(ancestorMessages); - } + // The server now handles all visibility - we just display what we receive + const convertedMessages = summaryResponse.messages.map((apiMessage) => + convertApiMessageToFrontendMessage(apiMessage) + ); // Replace messages with the server-provided messages setMessages(convertedMessages); @@ -109,8 +87,6 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = ( msg: 'Compaction failed. Please try again or start a new session.', }, ], - display: true, - sendToLLM: false, }; setMessages([...messages, errorMarker]); @@ -124,10 +100,9 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = ( async ( messages: Message[], setMessages: (messages: Message[]) => void, - append: (message: Message) => void, - setAncestorMessages?: (messages: Message[]) => void + append: (message: Message) => void ) => { - await performCompaction(messages, setMessages, append, false, setAncestorMessages); + await performCompaction(messages, setMessages, append, false); }, [performCompaction] ); @@ -136,16 +111,9 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = ( async ( messages: Message[], setMessages: (messages: Message[]) => void, - append?: (message: Message) => void, - setAncestorMessages?: (messages: Message[]) => void + append?: (message: Message) => void ) => { - await performCompaction( - messages, - setMessages, - append || (() => {}), - true, - setAncestorMessages - ); + await performCompaction(messages, setMessages, append || (() => {}), true); }, [performCompaction] ); diff --git a/ui/desktop/src/components/context_management/__tests__/CompactionMarker.test.tsx b/ui/desktop/src/components/context_management/__tests__/CompactionMarker.test.tsx index 5c1cce1a8646..8e1691ba24b9 100644 --- a/ui/desktop/src/components/context_management/__tests__/CompactionMarker.test.tsx +++ b/ui/desktop/src/components/context_management/__tests__/CompactionMarker.test.tsx @@ -10,8 +10,6 @@ describe('CompactionMarker', () => { role: 'assistant', created: 1000, content: [{ type: 'text', text: 'Regular message' }], - display: true, - sendToLLM: false, }; render(); @@ -28,8 +26,6 @@ describe('CompactionMarker', () => { { type: 'text', text: 'Some other content' }, { type: 'summarizationRequested', msg: 'Custom compaction message' }, ], - display: true, - sendToLLM: false, }; render(); @@ -43,8 +39,6 @@ describe('CompactionMarker', () => { role: 'assistant', created: 1000, content: [], - display: true, - sendToLLM: false, }; render(); @@ -58,8 +52,6 @@ describe('CompactionMarker', () => { role: 'assistant', created: 1000, content: [{ type: 'summarizationRequested', msg: '' }], - display: true, - sendToLLM: false, }; render(); @@ -75,8 +67,6 @@ describe('CompactionMarker', () => { created: 1000, // eslint-disable-next-line @typescript-eslint/no-explicit-any content: [{ type: 'summarizationRequested' } as any], - display: true, - sendToLLM: false, }; render(); diff --git a/ui/desktop/src/components/context_management/__tests__/ContextManager.test.tsx b/ui/desktop/src/components/context_management/__tests__/ContextManager.test.tsx index 20f5152c1a6b..453d8cfa5a36 100644 --- a/ui/desktop/src/components/context_management/__tests__/ContextManager.test.tsx +++ b/ui/desktop/src/components/context_management/__tests__/ContextManager.test.tsx @@ -23,16 +23,12 @@ describe('ContextManager', () => { role: 'user', created: 1000, content: [{ type: 'text', text: 'Hello' }], - display: true, - sendToLLM: true, }, { id: '2', role: 'assistant', created: 2000, content: [{ type: 'text', text: 'Hi there!' }], - display: true, - sendToLLM: true, }, ]; @@ -41,13 +37,10 @@ describe('ContextManager', () => { role: 'assistant', created: 3000, content: [{ type: 'text', text: 'This is a summary of the conversation.' }], - display: false, - sendToLLM: true, }; const mockSetMessages = vi.fn(); const mockAppend = vi.fn(); - const mockSetAncestorMessages = vi.fn(); beforeEach(() => { vi.clearAllMocks(); @@ -84,8 +77,6 @@ describe('ContextManager', () => { role: 'assistant', created: 1000, content: [{ type: 'summarizationRequested', msg: 'Compaction marker' }], - display: true, - sendToLLM: false, }; expect(result.current.hasCompactionMarker(messageWithMarker)).toBe(true); @@ -98,8 +89,6 @@ describe('ContextManager', () => { role: 'user', created: 1000, content: [{ type: 'text', text: 'Hello' }], - display: true, - sendToLLM: true, }; expect(result.current.hasCompactionMarker(regularMessage)).toBe(false); @@ -115,8 +104,6 @@ describe('ContextManager', () => { { type: 'text', text: 'Some text' }, { type: 'summarizationRequested', msg: 'Compaction marker' }, ], - display: true, - sendToLLM: false, }; expect(result.current.hasCompactionMarker(mixedMessage)).toBe(true); @@ -156,8 +143,6 @@ describe('ContextManager', () => { role: 'assistant', created: 3000, content: [{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' }], - display: true, - sendToLLM: false, }; const mockContinuationMessage: Message = { @@ -170,25 +155,18 @@ describe('ContextManager', () => { text: 'The previous message contains a summary that was prepared because a context limit was reached. Do not mention that you read a summary or that conversation summarization occurred Just continue the conversation naturally based on the summarized context', }, ], - display: false, - sendToLLM: true, }; // Mock the conversion function to return different messages based on call order mockConvertApiMessageToFrontendMessage - .mockReturnValueOnce(mockCompactionMarker) // First call - compaction marker (display: true, sendToLLM: false) - .mockReturnValueOnce(mockSummaryMessage) // Second call - summary (display: false, sendToLLM: true) - .mockReturnValueOnce(mockContinuationMessage); // Third call - continuation (display: false, sendToLLM: true) + .mockReturnValueOnce(mockCompactionMarker) // First call - compaction marker + .mockReturnValueOnce(mockSummaryMessage) // Second call - summary + .mockReturnValueOnce(mockContinuationMessage); // Third call - continuation const { result } = renderContextManager(); await act(async () => { - await result.current.handleAutoCompaction( - mockMessages, - mockSetMessages, - mockAppend, - mockSetAncestorMessages - ); + await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend); }); expect(mockManageContextFromBackend).toHaveBeenCalledWith({ @@ -203,17 +181,13 @@ describe('ContextManager', () => { content: [ { type: 'summarizationRequested', msg: 'Conversation compacted and summarized' }, ], - }), - true, // display: true - false // sendToLLM: false + }) ); expect(mockConvertApiMessageToFrontendMessage).toHaveBeenNthCalledWith( 2, expect.objectContaining({ content: [{ type: 'text', text: 'Summary content' }], - }), - false, // display: false - true // sendToLLM: true + }) ); expect(mockConvertApiMessageToFrontendMessage).toHaveBeenNthCalledWith( 3, @@ -224,24 +198,7 @@ describe('ContextManager', () => { text: expect.stringContaining('The previous message contains a summary'), }, ], - }), - false, // display: false - true // sendToLLM: true - ); - - expect(mockSetAncestorMessages).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - id: '1', - display: true, - sendToLLM: false, - }), - expect.objectContaining({ - id: '2', - display: true, - sendToLLM: false, - }), - ]) + }) ); // Expect setMessages to be called with all 3 converted messages @@ -268,12 +225,7 @@ describe('ContextManager', () => { const { result } = renderContextManager(); await act(async () => { - await result.current.handleAutoCompaction( - mockMessages, - mockSetMessages, - mockAppend, - mockSetAncestorMessages - ); + await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend); }); expect(result.current.compactionError).toBe('Backend error'); @@ -304,12 +256,7 @@ describe('ContextManager', () => { // Start compaction act(() => { - result.current.handleAutoCompaction( - mockMessages, - mockSetMessages, - mockAppend, - mockSetAncestorMessages - ); + result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend); }); // Should be compacting @@ -336,8 +283,8 @@ describe('ContextManager', () => { // Should no longer be compacting expect(result.current.isCompacting).toBe(false); }); + it('preserves display: false for ancestor messages', async () => { - // Backend returns no new messages; we're validating ancestor behavior only mockManageContextFromBackend.mockResolvedValue({ messages: [], tokenCounts: [] }); const hiddenMessage: Message = { @@ -345,8 +292,6 @@ describe('ContextManager', () => { role: 'user', created: 1500, content: [{ type: 'text', text: 'Secret' }], - display: false, - sendToLLM: true, }; const visibleMessage: Message = { @@ -354,8 +299,6 @@ describe('ContextManager', () => { role: 'assistant', created: 1600, content: [{ type: 'text', text: 'Public' }], - display: true, - sendToLLM: true, }; const messages: Message[] = [hiddenMessage, visibleMessage]; @@ -363,21 +306,9 @@ describe('ContextManager', () => { const { result } = renderContextManager(); await act(async () => { - await result.current.handleAutoCompaction( - messages, - mockSetMessages, - mockAppend, - mockSetAncestorMessages - ); + await result.current.handleAutoCompaction(messages, mockSetMessages, mockAppend); }); - expect(mockSetAncestorMessages).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ id: 'hidden-1', display: false, sendToLLM: false }), - expect.objectContaining({ id: 'visible-1', display: true, sendToLLM: false }), - ]) - ); - // No server messages -> setMessages called with empty list expect(mockSetMessages).toHaveBeenCalledWith([]); expect(mockAppend).not.toHaveBeenCalled(); @@ -416,8 +347,6 @@ describe('ContextManager', () => { role: 'assistant', created: 3000, content: [{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' }], - display: true, - sendToLLM: false, }; const mockContinuationMessage: Message = { @@ -430,8 +359,6 @@ describe('ContextManager', () => { text: 'The previous message contains a summary that was prepared because a context limit was reached. Do not mention that you read a summary or that conversation summarization occurred Just continue the conversation naturally based on the summarized context', }, ], - display: false, - sendToLLM: true, }; mockConvertApiMessageToFrontendMessage @@ -442,12 +369,7 @@ describe('ContextManager', () => { const { result } = renderContextManager(); await act(async () => { - await result.current.handleManualCompaction( - mockMessages, - mockSetMessages, - mockAppend, - mockSetAncestorMessages - ); + await result.current.handleManualCompaction(mockMessages, mockSetMessages, mockAppend); }); expect(mockManageContextFromBackend).toHaveBeenCalledWith({ @@ -490,8 +412,7 @@ describe('ContextManager', () => { await result.current.handleManualCompaction( mockMessages, mockSetMessages, - undefined, // No append function - mockSetAncestorMessages + undefined // No append function ); }); @@ -538,8 +459,6 @@ describe('ContextManager', () => { role: 'assistant', created: 3000, content: [{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' }], - display: true, - sendToLLM: false, }; const mockContinuationMessage: Message = { @@ -552,8 +471,6 @@ describe('ContextManager', () => { text: 'The previous message contains a summary that was prepared because a context limit was reached. Do not mention that you read a summary or that conversation summarization occurred Just continue the conversation naturally based on the summarized context', }, ], - display: false, - sendToLLM: true, }; mockConvertApiMessageToFrontendMessage @@ -564,12 +481,7 @@ describe('ContextManager', () => { const { result } = renderContextManager(); await act(async () => { - await result.current.handleManualCompaction( - mockMessages, - mockSetMessages, - mockAppend, // Provide append function - mockSetAncestorMessages - ); + await result.current.handleManualCompaction(mockMessages, mockSetMessages, mockAppend); }); // Verify all three messages are set @@ -596,12 +508,7 @@ describe('ContextManager', () => { const { result } = renderContextManager(); await act(async () => { - await result.current.handleAutoCompaction( - mockMessages, - mockSetMessages, - mockAppend, - mockSetAncestorMessages - ); + await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend); }); expect(result.current.compactionError).toBe('Unknown error during compaction'); @@ -625,8 +532,6 @@ describe('ContextManager', () => { role: 'assistant', created: 3000, content: [{ type: 'toolResponse', id: 'test', toolResult: { status: 'success' } }], - display: false, - sendToLLM: true, }; mockConvertApiMessageToFrontendMessage.mockReturnValue(mockMessageWithoutText); @@ -634,12 +539,7 @@ describe('ContextManager', () => { const { result } = renderContextManager(); await act(async () => { - await result.current.handleAutoCompaction( - mockMessages, - mockSetMessages, - mockAppend, - mockSetAncestorMessages - ); + await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend); }); // Should complete without error even if content is not text diff --git a/ui/desktop/src/components/context_management/index.ts b/ui/desktop/src/components/context_management/index.ts index 195469ba64ee..68ddbb8cf013 100644 --- a/ui/desktop/src/components/context_management/index.ts +++ b/ui/desktop/src/components/context_management/index.ts @@ -50,15 +50,8 @@ export async function manageContextFromBackend({ } // Function to convert API Message to frontend Message -// TODO(Douwe): get rid of this and use the API Message format everywhere -export function convertApiMessageToFrontendMessage( - apiMessage: ApiMessage, - display?: boolean, - sendToLLM?: boolean -): FrontendMessage { +export function convertApiMessageToFrontendMessage(apiMessage: ApiMessage): FrontendMessage { return { - display: display ?? true, - sendToLLM: sendToLLM ?? true, id: generateId(), role: apiMessage.role as Role, created: apiMessage.created ?? Math.floor(Date.now() / 1000), @@ -150,7 +143,5 @@ export function createSummarizationRequestMessage( msg: requestMessage, }, ], - sendToLLM: false, - display: true, }; } diff --git a/ui/desktop/src/components/sessions/SessionHistoryView.tsx b/ui/desktop/src/components/sessions/SessionHistoryView.tsx index 82cd8fe25d7e..c0b08df21d20 100644 --- a/ui/desktop/src/components/sessions/SessionHistoryView.tsx +++ b/ui/desktop/src/components/sessions/SessionHistoryView.tsx @@ -42,7 +42,7 @@ const isUserMessage = (message: Message): boolean => { }; const filterMessagesForDisplay = (messages: Message[]): Message[] => { - return messages.filter((message) => message.display ?? true); + return messages; }; interface SessionHistoryViewProps { diff --git a/ui/desktop/src/hooks/useAgent.ts b/ui/desktop/src/hooks/useAgent.ts index 93ab150a7dde..d70b1c48cc4d 100644 --- a/ui/desktop/src/hooks/useAgent.ts +++ b/ui/desktop/src/hooks/useAgent.ts @@ -79,7 +79,7 @@ export function useAgent(): UseAgentReturn { title: sessionMetadata.recipe?.title || sessionMetadata.description, messageHistoryIndex: 0, messages: agentSessionInfo.messages.map((message: ApiMessage) => - convertApiMessageToFrontendMessage(message, true, true) + convertApiMessageToFrontendMessage(message) ), recipeConfig: sessionMetadata.recipe, }; @@ -159,7 +159,7 @@ export function useAgent(): UseAgentReturn { title: sessionMetadata.recipe?.title || sessionMetadata.description, messageHistoryIndex: 0, messages: agentSessionInfo.messages.map((message: ApiMessage) => - convertApiMessageToFrontendMessage(message, true, true) + convertApiMessageToFrontendMessage(message) ), recipeConfig: sessionMetadata.recipe, }; diff --git a/ui/desktop/src/hooks/useChatEngine.ts b/ui/desktop/src/hooks/useChatEngine.ts index eb01d0b6cd2c..f9ab4b3e687b 100644 --- a/ui/desktop/src/hooks/useChatEngine.ts +++ b/ui/desktop/src/hooks/useChatEngine.ts @@ -40,7 +40,6 @@ export const useChatEngine = ({ }: UseChatEngineProps) => { const [lastInteractionTime, setLastInteractionTime] = useState(Date.now()); const [sessionTokenCount, setSessionTokenCount] = useState(0); - const [ancestorMessages, setAncestorMessages] = useState([]); const [sessionInputTokens, setSessionInputTokens] = useState(0); const [sessionOutputTokens, setSessionOutputTokens] = useState(0); const [localInputTokens, setLocalInputTokens] = useState(0); @@ -353,8 +352,6 @@ export const useChatEngine = ({ // Create tool responses for all interrupted tool requests let responseMessage: Message = { - display: true, - sendToLLM: true, role: 'user', created: Date.now(), content: [], @@ -381,11 +378,12 @@ export const useChatEngine = ({ } }, [stop, messages, _setInput, setMessages, stopPowerSaveBlocker]); + // Since server now handles all filtering, we just use messages directly const filteredMessages = useMemo(() => { - return [...ancestorMessages, ...messages].filter((message) => message.display ?? true); - }, [ancestorMessages, messages]); + return messages; + }, [messages]); - // Generate command history from filtered messages + // Generate command history from messages const commandHistory = useMemo(() => { return filteredMessages .reduce((history, message) => { @@ -445,8 +443,6 @@ export const useChatEngine = ({ // Core message data messages, filteredMessages, - ancestorMessages, - setAncestorMessages, // Message stream controls append, diff --git a/ui/desktop/src/hooks/useMessageStream.ts b/ui/desktop/src/hooks/useMessageStream.ts index 4fdb0eaa1755..e20244a41bef 100644 --- a/ui/desktop/src/hooks/useMessageStream.ts +++ b/ui/desktop/src/hooks/useMessageStream.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useId, useReducer, useRef, useState } from 'react'; import useSWR from 'swr'; -import { createUserMessage, hasCompletedToolCalls, Message } from '../types/message'; +import { createUserMessage, hasCompletedToolCalls, Message, Role } from '../types/message'; import { getSessionHistory, SessionMetadata } from '../api'; import { ChatState } from '../types/chatState'; @@ -273,19 +273,12 @@ export function useMessageStream({ mutateChatState(ChatState.Streaming); // Create a new message object with the properties preserved or defaulted - const newMessage = { + const newMessage: Message = { ...parsedEvent.message, - // Ensure the message has an ID - if not provided, generate one - id: parsedEvent.message.id || generateMessageId(), - // Only set to true if it's undefined (preserve false values) - display: - parsedEvent.message.display === undefined - ? true - : parsedEvent.message.display, - sendToLLM: - parsedEvent.message.sendToLLM === undefined - ? true - : parsedEvent.message.sendToLLM, + id: parsedEvent.message.id || undefined, + role: parsedEvent.message.role as Role, + created: parsedEvent.message.created || Date.now(), + content: parsedEvent.message.content || [], }; // Update messages with the new message @@ -408,9 +401,6 @@ export function useMessageStream({ const abortController = new AbortController(); abortControllerRef.current = abortController; - // Filter out messages where sendToLLM is explicitly false - const filteredMessages = requestMessages.filter((message) => message.sendToLLM !== false); - // Send request to the server const response = await fetch(api, { method: 'POST', @@ -420,7 +410,7 @@ export function useMessageStream({ ...extraMetadataRef.current.headers, }, body: JSON.stringify({ - messages: filteredMessages, + messages: requestMessages, ...extraMetadataRef.current.body, }), signal: abortController.signal, diff --git a/ui/desktop/src/sessions.ts b/ui/desktop/src/sessions.ts index bb488b4ee8ab..74bfc9946455 100644 --- a/ui/desktop/src/sessions.ts +++ b/ui/desktop/src/sessions.ts @@ -98,14 +98,19 @@ export async function fetchSessionDetails(sessionId: string): Promise - convertApiMessageToFrontendMessage(message, true, true) - ), // slight diffs between backend and frontend Message obj - }; + try { + // Convert the SessionHistoryResponse to a SessionDetails object + return { + sessionId: response.data.sessionId, + metadata: ensureWorkingDir(response.data.metadata), + messages: response.data.messages.map((message: ApiMessage) => + convertApiMessageToFrontendMessage(message) + ), // slight diffs between backend and frontend Message obj + }; + } catch (error) { + console.error(`Error fetching session details for ${sessionId}:`, error); + throw error; + } } /** diff --git a/ui/desktop/src/types/message.ts b/ui/desktop/src/types/message.ts index 3c45bc75d7ad..578636d9c152 100644 --- a/ui/desktop/src/types/message.ts +++ b/ui/desktop/src/types/message.ts @@ -108,8 +108,6 @@ export interface Message { role: Role; created: number; content: MessageContent[]; - display?: boolean; - sendToLLM?: boolean; } // Helper functions to create messages