From 3cda5d62fb112fd25f1903ddcb809e92d9a18574 Mon Sep 17 00:00:00 2001 From: Leon Fattakhov Date: Thu, 11 Jun 2026 12:12:56 -0400 Subject: [PATCH 01/13] Initialize fetched_memories in test Message constructors Mechanical addition of fetched_memories: vec![] to existing api::Message literals, split out for easier review. Co-Authored-By: Oz --- .../agent/api/convert_conversation_tests.rs | 62 +++++++++++++++++++ app/src/ai/agent/api/convert_from_tests.rs | 5 ++ app/src/ai/agent/conversation_tests.rs | 2 + app/src/ai/agent/conversation_yaml_tests.rs | 3 + app/src/ai/agent/linearization_tests.rs | 3 + app/src/ai/agent/task_tests.rs | 1 + .../orchestration_pill_bar_tests.rs | 2 + .../block/view_impl/orchestration_tests.rs | 2 + app/src/ai/blocklist/history_model_tests.rs | 1 + .../orchestration_event_streamer_tests.rs | 1 + .../ai/conversation_details_panel_tests.rs | 2 + .../view/shared_session/view_impl_tests.rs | 1 + app/src/test_util/ai_agent_tasks.rs | 2 + crates/integration/src/test/agent_mode.rs | 2 + crates/persistence/src/model_tests.rs | 1 + 15 files changed, 90 insertions(+) diff --git a/app/src/ai/agent/api/convert_conversation_tests.rs b/app/src/ai/agent/api/convert_conversation_tests.rs index 29de963330..ef81ea67ab 100644 --- a/app/src/ai/agent/api/convert_conversation_tests.rs +++ b/app/src/ai/agent/api/convert_conversation_tests.rs @@ -360,6 +360,7 @@ fn test_into_exchanges_basic() { // Create minimal test data let messages = vec![ api::Message { + fetched_memories: vec![], id: "user_msg".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -375,6 +376,7 @@ fn test_into_exchanges_basic() { timestamp: None, }, api::Message { + fetched_memories: vec![], id: "agent_msg".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -388,6 +390,7 @@ fn test_into_exchanges_basic() { timestamp: None, }, api::Message { + fetched_memories: vec![], id: "user_msg2".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -403,6 +406,7 @@ fn test_into_exchanges_basic() { timestamp: None, }, api::Message { + fetched_memories: vec![], id: "agent_msg2".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -416,6 +420,7 @@ fn test_into_exchanges_basic() { timestamp: None, }, api::Message { + fetched_memories: vec![], id: "user_msg3".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -431,6 +436,7 @@ fn test_into_exchanges_basic() { timestamp: None, }, api::Message { + fetched_memories: vec![], id: "agent_msg3".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -475,6 +481,7 @@ fn test_invoke_skill_arguments_round_trip() { let query = "arg1 arg2".to_string(); let messages = vec![ api::Message { + fetched_memories: vec![], id: "invoke_skill_msg".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -495,6 +502,7 @@ fn test_invoke_skill_arguments_round_trip() { timestamp: None, }, api::Message { + fetched_memories: vec![], id: "agent_msg".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -542,6 +550,7 @@ fn test_invoke_skill_arguments_round_trip() { #[test] fn test_invoke_skill_missing_user_query_maps_to_none() { let messages = vec![api::Message { + fetched_memories: vec![], id: "invoke_skill_msg".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -588,6 +597,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() { let messages = vec![ // User query api::Message { + fetched_memories: vec![], id: "user_query".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -604,6 +614,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() { }, // Agent response api::Message { + fetched_memories: vec![], id: "agent_response".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -618,6 +629,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() { }, // Tool call 1 api::Message { + fetched_memories: vec![], id: "tool_call_1".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -641,6 +653,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() { }, // Tool call 2 api::Message { + fetched_memories: vec![], id: "tool_call_2".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -664,6 +677,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() { }, // Tool call 3 api::Message { + fetched_memories: vec![], id: "tool_call_3".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -687,6 +701,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() { }, // Tool call result - cancelled (call_2) api::Message { + fetched_memories: vec![], id: "result_cancelled".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -703,6 +718,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() { }, // Tool call result - success (call_1) api::Message { + fetched_memories: vec![], id: "result_success_1".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -735,6 +751,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() { }, // Tool call result - success (call_3) api::Message { + fetched_memories: vec![], id: "result_success_3".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -767,6 +784,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() { }, // Final agent response api::Message { + fetched_memories: vec![], id: "final_response".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -781,6 +799,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() { }, // Follow-up user query api::Message { + fetched_memories: vec![], id: "followup_query".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -797,6 +816,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() { }, // Final agent response api::Message { + fetched_memories: vec![], id: "final_response2".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -898,6 +918,7 @@ fn test_into_exchanges_with_code_diffs() { let messages = vec![ // User query asking for code changes api::Message { + fetched_memories: vec![], id: "user_query".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -914,6 +935,7 @@ fn test_into_exchanges_with_code_diffs() { }, // Agent response api::Message { + fetched_memories: vec![], id: "agent_response".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -928,6 +950,7 @@ fn test_into_exchanges_with_code_diffs() { }, // File diff tool call api::Message { + fetched_memories: vec![], id: "diff_call".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -949,6 +972,7 @@ fn test_into_exchanges_with_code_diffs() { }, // User cancels the diff api::Message { + fetched_memories: vec![], id: "diff_cancelled".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -965,6 +989,7 @@ fn test_into_exchanges_with_code_diffs() { }, // User provides feedback api::Message { + fetched_memories: vec![], id: "user_feedback".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -981,6 +1006,7 @@ fn test_into_exchanges_with_code_diffs() { }, // Agent response api::Message { + fetched_memories: vec![], id: "agent_response_2".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -995,6 +1021,7 @@ fn test_into_exchanges_with_code_diffs() { }, // Second file diff tool call api::Message { + fetched_memories: vec![], id: "diff_call_2".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1016,6 +1043,7 @@ fn test_into_exchanges_with_code_diffs() { }, // User accepts the diff api::Message { + fetched_memories: vec![], id: "diff_accepted".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1043,6 +1071,7 @@ fn test_into_exchanges_with_code_diffs() { }, // Final agent response api::Message { + fetched_memories: vec![], id: "final_response".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1057,6 +1086,7 @@ fn test_into_exchanges_with_code_diffs() { }, // Follow-up user query api::Message { + fetched_memories: vec![], id: "followup".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1073,6 +1103,7 @@ fn test_into_exchanges_with_code_diffs() { }, // Final agent response api::Message { + fetched_memories: vec![], id: "final_response_2".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1170,6 +1201,7 @@ fn test_into_exchanges_with_code_diffs() { fn test_user_query_mode_conversion() { // Test conversion with Plan mode let messages = vec![api::Message { + fetched_memories: vec![], id: "user_msg".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1216,6 +1248,7 @@ fn test_user_query_mode_conversion() { // Test conversion with Normal mode (no type set) let messages_normal = vec![api::Message { + fetched_memories: vec![], id: "user_msg".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1260,6 +1293,7 @@ fn test_user_query_mode_conversion() { // Test conversion with no mode field (should default to Normal) let messages_default = vec![api::Message { + fetched_memories: vec![], id: "user_msg".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1313,6 +1347,7 @@ fn test_exchanges_grouped_by_request_id() { let messages = vec![ // Message 0: Server message (should be ignored or handled gracefully) api::Message { + fetched_memories: vec![], id: "2512077c-0ede-46b0-8f69-230c8792df07".to_string(), task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(), request_id: "78e236b8-84a2-45df-876e-ebfb86ceafc4".to_string(), @@ -1330,6 +1365,7 @@ fn test_exchanges_grouped_by_request_id() { }, // Message 1: User query with request_id 78e236b8 api::Message { + fetched_memories: vec![], id: "4d6c450d-3d54-446f-974c-5c414e6083e9".to_string(), task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(), request_id: "78e236b8-84a2-45df-876e-ebfb86ceafc4".to_string(), @@ -1346,6 +1382,7 @@ fn test_exchanges_grouped_by_request_id() { }, // Message 2: Agent output with same request_id api::Message { + fetched_memories: vec![], id: "10210d1a-5298-45ef-90ba-df6367805080".to_string(), task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(), request_id: "78e236b8-84a2-45df-876e-ebfb86ceafc4".to_string(), @@ -1360,6 +1397,7 @@ fn test_exchanges_grouped_by_request_id() { }, // Message 3: Tool call with same request_id api::Message { + fetched_memories: vec![], id: "936c7c86-eb4a-4edf-97c0-22f5c61b35a6".to_string(), task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(), request_id: "78e236b8-84a2-45df-876e-ebfb86ceafc4".to_string(), @@ -1383,6 +1421,7 @@ fn test_exchanges_grouped_by_request_id() { }, // Message 4: Tool call result with NEW request_id 59a3947f (starts new exchange) api::Message { + fetched_memories: vec![], id: "cbebf5fb-4dd8-4aef-be45-bb916eff552c".to_string(), task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(), request_id: "59a3947f-fc7e-413a-96b5-baecd7e406dc".to_string(), @@ -1417,6 +1456,7 @@ fn test_exchanges_grouped_by_request_id() { }, // Message 5: Agent output with same request_id api::Message { + fetched_memories: vec![], id: "7a89857d-fa33-4d45-88e3-5fa9cbce3f20".to_string(), task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(), request_id: "59a3947f-fc7e-413a-96b5-baecd7e406dc".to_string(), @@ -1431,6 +1471,7 @@ fn test_exchanges_grouped_by_request_id() { }, // Message 6: Write to long running command with NEW request_id 9f85acb2 (starts new exchange) api::Message { + fetched_memories: vec![], id: "dac6d336-9fcb-4e34-bc2b-b06e70f52ec5".to_string(), task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(), request_id: "9f85acb2-0b1f-41b1-a0de-3623e131758a".to_string(), @@ -1461,6 +1502,7 @@ fn test_exchanges_grouped_by_request_id() { }, // Message 7: Final tool call result with same request_id api::Message { + fetched_memories: vec![], id: "ad319d66-fac0-4169-8bf1-e6004aca1619".to_string(), task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(), request_id: "9f85acb2-0b1f-41b1-a0de-3623e131758a".to_string(), @@ -1493,6 +1535,7 @@ fn test_exchanges_grouped_by_request_id() { }, // Message 8: Final agent output with same request_id api::Message { + fetched_memories: vec![], id: "f15f8a59-2e9c-416e-b216-83b3bd52d6be".to_string(), task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(), request_id: "9f85acb2-0b1f-41b1-a0de-3623e131758a".to_string(), @@ -1581,6 +1624,7 @@ fn test_multiple_create_documents_get_default_version() { let messages = vec![ // User query api::Message { + fetched_memories: vec![], id: "user_msg".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1597,6 +1641,7 @@ fn test_multiple_create_documents_get_default_version() { }, // Agent output api::Message { + fetched_memories: vec![], id: "agent_text".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1611,6 +1656,7 @@ fn test_multiple_create_documents_get_default_version() { }, // First CreateDocuments tool call api::Message { + fetched_memories: vec![], id: "tool_call_create_a".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1633,6 +1679,7 @@ fn test_multiple_create_documents_get_default_version() { }, // First CreateDocuments result api::Message { + fetched_memories: vec![], id: "result_create_a".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1661,6 +1708,7 @@ fn test_multiple_create_documents_get_default_version() { }, // Agent output before second plan api::Message { + fetched_memories: vec![], id: "agent_text_2".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1675,6 +1723,7 @@ fn test_multiple_create_documents_get_default_version() { }, // Second CreateDocuments tool call api::Message { + fetched_memories: vec![], id: "tool_call_create_b".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1697,6 +1746,7 @@ fn test_multiple_create_documents_get_default_version() { }, // Second CreateDocuments result api::Message { + fetched_memories: vec![], id: "result_create_b".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1791,6 +1841,7 @@ fn test_create_then_edit_then_create_version_tracking() { let messages = vec![ // User query api::Message { + fetched_memories: vec![], id: "user_msg".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1807,6 +1858,7 @@ fn test_create_then_edit_then_create_version_tracking() { }, // Agent output api::Message { + fetched_memories: vec![], id: "agent_text".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1821,6 +1873,7 @@ fn test_create_then_edit_then_create_version_tracking() { }, // Create doc A tool call api::Message { + fetched_memories: vec![], id: "tool_call_create_a".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1843,6 +1896,7 @@ fn test_create_then_edit_then_create_version_tracking() { }, // Create doc A result api::Message { + fetched_memories: vec![], id: "result_create_a".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1871,6 +1925,7 @@ fn test_create_then_edit_then_create_version_tracking() { }, // Agent output before edit api::Message { + fetched_memories: vec![], id: "agent_text_2".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1885,6 +1940,7 @@ fn test_create_then_edit_then_create_version_tracking() { }, // Edit doc A tool call api::Message { + fetched_memories: vec![], id: "tool_call_edit_a".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1906,6 +1962,7 @@ fn test_create_then_edit_then_create_version_tracking() { }, // Edit doc A result api::Message { + fetched_memories: vec![], id: "result_edit_a".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1934,6 +1991,7 @@ fn test_create_then_edit_then_create_version_tracking() { }, // Agent output before second create api::Message { + fetched_memories: vec![], id: "agent_text_3".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1948,6 +2006,7 @@ fn test_create_then_edit_then_create_version_tracking() { }, // Create doc B tool call api::Message { + fetched_memories: vec![], id: "tool_call_create_b".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -1970,6 +2029,7 @@ fn test_create_then_edit_then_create_version_tracking() { }, // Create doc B result api::Message { + fetched_memories: vec![], id: "result_create_b".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -2077,6 +2137,7 @@ fn test_handoff_rehydration_system_query_is_hidden() { let messages = vec![ // HandoffRehydration system query – should be hidden api::Message { + fetched_memories: vec![], id: "msg_handoff".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), @@ -2096,6 +2157,7 @@ fn test_handoff_rehydration_system_query_is_hidden() { }, // Agent output that follows the hidden system query api::Message { + fetched_memories: vec![], id: "msg_output".to_string(), task_id: "task1".to_string(), server_message_data: "".to_string(), diff --git a/app/src/ai/agent/api/convert_from_tests.rs b/app/src/ai/agent/api/convert_from_tests.rs index 6dff162662..8f5a32f306 100644 --- a/app/src/ai/agent/api/convert_from_tests.rs +++ b/app/src/ai/agent/api/convert_from_tests.rs @@ -21,6 +21,7 @@ fn start_agent_tool_call_message( lifecycle_subscription_event_types: Option>, ) -> api::Message { api::Message { + fetched_memories: vec![], id: "message-id".to_string(), task_id: "task-id".to_string(), server_message_data: String::new(), @@ -67,6 +68,7 @@ fn start_agent_v2_tool_call_message( lifecycle_subscription_event_types: Option>, ) -> api::Message { api::Message { + fetched_memories: vec![], id: "message-id".to_string(), task_id: "task-id".to_string(), server_message_data: String::new(), @@ -91,6 +93,7 @@ fn start_agent_v2_tool_call_message( fn upload_artifact_tool_call_message(path: &str, description: &str) -> api::Message { api::Message { + fetched_memories: vec![], id: "message-id".to_string(), task_id: "task-id".to_string(), server_message_data: String::new(), @@ -144,6 +147,7 @@ fn remote_start_agent_v2_execution_mode( fn file_artifact_created_message(filepath: &str, description: &str) -> api::Message { api::Message { + fetched_memories: vec![], id: "message-id".to_string(), task_id: "task-id".to_string(), server_message_data: String::new(), @@ -616,6 +620,7 @@ fn transfer_control_tool_call_converts_to_action_message() { let task_id = TaskId::new("task".to_string()); let reason = "Please finish the interactive flow".to_string(); let message = api::Message { + fetched_memories: vec![], id: "message".to_string(), task_id: "task".to_string(), server_message_data: String::new(), diff --git a/app/src/ai/agent/conversation_tests.rs b/app/src/ai/agent/conversation_tests.rs index 434ce70e4c..9777a3ad1a 100644 --- a/app/src/ai/agent/conversation_tests.rs +++ b/app/src/ai/agent/conversation_tests.rs @@ -53,6 +53,7 @@ fn restored_conversation_with_root_description(description: &str) -> AIConversat fn user_query_message(id: &str, request_id: &str, query: &str) -> api::Message { api::Message { + fetched_memories: vec![], id: id.to_string(), task_id: "root-task".to_string(), server_message_data: String::new(), @@ -71,6 +72,7 @@ fn user_query_message(id: &str, request_id: &str, query: &str) -> api::Message { fn agent_output_message(id: &str, request_id: &str) -> api::Message { api::Message { + fetched_memories: vec![], id: id.to_string(), task_id: "root-task".to_string(), server_message_data: String::new(), diff --git a/app/src/ai/agent/conversation_yaml_tests.rs b/app/src/ai/agent/conversation_yaml_tests.rs index 88269b0fd7..022da5c5f1 100644 --- a/app/src/ai/agent/conversation_yaml_tests.rs +++ b/app/src/ai/agent/conversation_yaml_tests.rs @@ -21,6 +21,7 @@ fn list_dir_sorted(dir: &Path) -> Vec { fn make_user_query_message(id: &str, task_id: &str, query: &str) -> api::Message { api::Message { + fetched_memories: vec![], id: id.to_string(), task_id: task_id.to_string(), server_message_data: String::new(), @@ -44,6 +45,7 @@ fn make_tool_call_message( tool: api::message::tool_call::Tool, ) -> api::Message { api::Message { + fetched_memories: vec![], id: id.to_string(), task_id: task_id.to_string(), server_message_data: String::new(), @@ -64,6 +66,7 @@ fn make_tool_call_result_message( result: api::message::tool_call_result::Result, ) -> api::Message { api::Message { + fetched_memories: vec![], id: id.to_string(), task_id: task_id.to_string(), server_message_data: String::new(), diff --git a/app/src/ai/agent/linearization_tests.rs b/app/src/ai/agent/linearization_tests.rs index d28b4fff49..f988350644 100644 --- a/app/src/ai/agent/linearization_tests.rs +++ b/app/src/ai/agent/linearization_tests.rs @@ -7,6 +7,7 @@ use super::*; // Helper function to create a basic message fn create_message(id: &str, task_id: &str) -> api::Message { api::Message { + fetched_memories: vec![], id: id.to_string(), task_id: task_id.to_string(), server_message_data: "server_data".to_string(), @@ -23,6 +24,7 @@ fn create_message(id: &str, task_id: &str) -> api::Message { fn create_subagent_tool_call_message(id: &str, task_id: &str, subtask_id: &str) -> api::Message { api::Message { + fetched_memories: vec![], id: id.to_string(), task_id: task_id.to_string(), server_message_data: "server_data".to_string(), @@ -45,6 +47,7 @@ fn create_subagent_tool_call_message(id: &str, task_id: &str, subtask_id: &str) // Helper function to create a tool call result message. fn create_tool_call_result_message(id: &str, task_id: &str, tool_call_id: &str) -> api::Message { api::Message { + fetched_memories: vec![], id: id.to_string(), task_id: task_id.to_string(), server_message_data: "server_data".to_string(), diff --git a/app/src/ai/agent/task_tests.rs b/app/src/ai/agent/task_tests.rs index 40f7c644be..d1af6c1e16 100644 --- a/app/src/ai/agent/task_tests.rs +++ b/app/src/ai/agent/task_tests.rs @@ -48,6 +48,7 @@ fn create_start_agent_tool_call_message( prompt: &str, ) -> api::Message { api::Message { + fetched_memories: vec![], id: id.to_string(), task_id: task_id.to_string(), server_message_data: String::new(), diff --git a/app/src/ai/blocklist/agent_view/orchestration_pill_bar_tests.rs b/app/src/ai/blocklist/agent_view/orchestration_pill_bar_tests.rs index d28e6d66b8..82765271c0 100644 --- a/app/src/ai/blocklist/agent_view/orchestration_pill_bar_tests.rs +++ b/app/src/ai/blocklist/agent_view/orchestration_pill_bar_tests.rs @@ -62,6 +62,7 @@ fn pill_bar_data_layer_finds_restored_children_before_pane_creation() { tasks: vec![warp_multi_agent_api::Task { id: format!("task-{child_id}"), messages: vec![warp_multi_agent_api::Message { + fetched_memories: vec![], id: "child-msg".to_string(), task_id: format!("task-{child_id}"), server_message_data: String::new(), @@ -111,6 +112,7 @@ fn pill_bar_data_layer_finds_restored_children_before_pane_creation() { tasks: vec![warp_multi_agent_api::Task { id: format!("task-{parent_id}"), messages: vec![warp_multi_agent_api::Message { + fetched_memories: vec![], id: "parent-msg".to_string(), task_id: format!("task-{parent_id}"), server_message_data: String::new(), diff --git a/app/src/ai/blocklist/block/view_impl/orchestration_tests.rs b/app/src/ai/blocklist/block/view_impl/orchestration_tests.rs index f4b0c02550..125a5af582 100644 --- a/app/src/ai/blocklist/block/view_impl/orchestration_tests.rs +++ b/app/src/ai/blocklist/block/view_impl/orchestration_tests.rs @@ -306,6 +306,7 @@ fn participant_for_restored_child_run_id_resolves_to_agent_name() { tasks: vec![warp_multi_agent_api::Task { id: format!("task-{child_id}"), messages: vec![warp_multi_agent_api::Message { + fetched_memories: vec![], id: "child-msg".to_string(), task_id: format!("task-{child_id}"), server_message_data: String::new(), @@ -358,6 +359,7 @@ fn participant_for_restored_child_run_id_resolves_to_agent_name() { tasks: vec![warp_multi_agent_api::Task { id: format!("task-{parent_id}"), messages: vec![warp_multi_agent_api::Message { + fetched_memories: vec![], id: "parent-msg".to_string(), task_id: format!("task-{parent_id}"), server_message_data: String::new(), diff --git a/app/src/ai/blocklist/history_model_tests.rs b/app/src/ai/blocklist/history_model_tests.rs index 95dd90120d..d712628b62 100644 --- a/app/src/ai/blocklist/history_model_tests.rs +++ b/app/src/ai/blocklist/history_model_tests.rs @@ -72,6 +72,7 @@ fn create_user_query_message( query: &str, ) -> warp_multi_agent_api::Message { warp_multi_agent_api::Message { + fetched_memories: vec![], id: id.to_string(), task_id: task_id.to_string(), server_message_data: String::new(), diff --git a/app/src/ai/blocklist/orchestration_event_streamer_tests.rs b/app/src/ai/blocklist/orchestration_event_streamer_tests.rs index 80e16dd78e..9ffec121d4 100644 --- a/app/src/ai/blocklist/orchestration_event_streamer_tests.rs +++ b/app/src/ai/blocklist/orchestration_event_streamer_tests.rs @@ -128,6 +128,7 @@ fn ai_conversation_new_restored_preserves_last_event_sequence() { let task = api::Task { id: "root".to_string(), messages: vec![api::Message { + fetched_memories: vec![], id: "m1".to_string(), task_id: "root".to_string(), server_message_data: String::new(), diff --git a/app/src/ai/conversation_details_panel_tests.rs b/app/src/ai/conversation_details_panel_tests.rs index a192125752..0b33dd2f17 100644 --- a/app/src/ai/conversation_details_panel_tests.rs +++ b/app/src/ai/conversation_details_panel_tests.rs @@ -107,6 +107,7 @@ fn test_from_conversation_prefers_server_creator_profile() { fn create_message_with_directory(id: &str, task_id: &str, directory: &str) -> api::Message { api::Message { + fetched_memories: vec![], id: id.to_string(), task_id: task_id.to_string(), server_message_data: String::new(), @@ -132,6 +133,7 @@ fn create_message_with_directory(id: &str, task_id: &str, directory: &str) -> ap fn create_agent_output_message(id: &str, task_id: &str) -> api::Message { api::Message { + fetched_memories: vec![], id: id.to_string(), task_id: task_id.to_string(), server_message_data: String::new(), diff --git a/app/src/terminal/view/shared_session/view_impl_tests.rs b/app/src/terminal/view/shared_session/view_impl_tests.rs index f9e207e492..09c9b85747 100644 --- a/app/src/terminal/view/shared_session/view_impl_tests.rs +++ b/app/src/terminal/view/shared_session/view_impl_tests.rs @@ -1912,6 +1912,7 @@ fn test_shared_followup_on_existing_conversation_converts_user_query_input() { api_client_action::AddMessagesToTask { task_id: root_task_id.to_string(), messages: vec![api::Message { + fetched_memories: vec![], id: "user-message".to_string(), task_id: root_task_id.to_string(), server_message_data: String::new(), diff --git a/app/src/test_util/ai_agent_tasks.rs b/app/src/test_util/ai_agent_tasks.rs index 925504ba9b..5e4d9266d3 100644 --- a/app/src/test_util/ai_agent_tasks.rs +++ b/app/src/test_util/ai_agent_tasks.rs @@ -2,6 +2,7 @@ use warp_multi_agent_api as api; pub fn create_message(id: &str, task_id: &str) -> api::Message { api::Message { + fetched_memories: vec![], id: id.to_string(), task_id: task_id.to_string(), server_message_data: String::new(), @@ -23,6 +24,7 @@ pub fn create_subagent_tool_call_message( metadata: Option, ) -> api::Message { api::Message { + fetched_memories: vec![], id: id.to_string(), task_id: task_id.to_string(), server_message_data: String::new(), diff --git a/crates/integration/src/test/agent_mode.rs b/crates/integration/src/test/agent_mode.rs index cd5fecaebe..9e11a4efbe 100644 --- a/crates/integration/src/test/agent_mode.rs +++ b/crates/integration/src/test/agent_mode.rs @@ -136,6 +136,7 @@ fn restored_user_query_message(task_id: &str, request_id: &str, directory: &str) })), request_id: request_id.to_string(), timestamp: None, + fetched_memories: vec![], } } @@ -161,6 +162,7 @@ fn restored_agent_output_message(task_id: &str, request_id: &str) -> api::Messag )), request_id: request_id.to_string(), timestamp: None, + fetched_memories: vec![], } } diff --git a/crates/persistence/src/model_tests.rs b/crates/persistence/src/model_tests.rs index 947f42c876..df0dd52df2 100644 --- a/crates/persistence/src/model_tests.rs +++ b/crates/persistence/src/model_tests.rs @@ -11,6 +11,7 @@ fn parentless_task(id: &str, message_count: usize) -> api::Task { dependencies: None, messages: (0..message_count) .map(|i| api::Message { + fetched_memories: vec![], id: format!("{id}-msg-{i}"), task_id: id.to_string(), server_message_data: String::new(), From 33d153610738f5dee3bf4d2b767a1811c09e8913 Mon Sep 17 00:00:00 2001 From: Leon Fattakhov Date: Tue, 23 Jun 2026 11:38:28 -0700 Subject: [PATCH 02/13] WIP: Surface fetched memories chip in agent footer Add FetchedMemoriesChip feature flag, AIConversation::fetched_memories() accessor, the FetchedMemoriesChipView popup, footer toolbar wiring, and telemetry. Patches Cargo to the local warp-proto-apis for the new field. Co-Authored-By: Oz --- Cargo.lock | 1 - Cargo.toml | 2 +- app/Cargo.toml | 1 + app/src/ai/agent/conversation.rs | 21 + app/src/ai/agent/conversation_tests.rs | 108 ++++ .../agent_view/agent_input_footer/mod.rs | 17 +- .../agent_input_footer/toolbar_item.rs | 25 +- .../agent_input_footer/toolbar_item_tests.rs | 56 ++ app/src/ai/blocklist/prompt.rs | 1 + .../ai/blocklist/prompt/fetched_memories.rs | 565 ++++++++++++++++++ app/src/features.rs | 2 + app/src/lib.rs | 1 + crates/warp_features/src/lib.rs | 5 + 13 files changed, 799 insertions(+), 6 deletions(-) create mode 100644 app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item_tests.rs create mode 100644 app/src/ai/blocklist/prompt/fetched_memories.rs diff --git a/Cargo.lock b/Cargo.lock index 204f77d8d7..7f146a0de6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15629,7 +15629,6 @@ dependencies = [ [[package]] name = "warp_multi_agent_api" version = "0.0.0" -source = "git+https://github.com/warpdotdev/warp-proto-apis.git?rev=ac1af7303d2931b0fb485be650a1fbc8b80d5667#ac1af7303d2931b0fb485be650a1fbc8b80d5667" dependencies = [ "prost", "prost-reflect", diff --git a/Cargo.toml b/Cargo.toml index 157e9bf738..3dd1bd5425 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -547,7 +547,7 @@ tikv-jemalloc-sys = { git = "https://github.com/warpdotdev/jemallocator.git", re [patch."https://github.com/warpdotdev/warp-proto-apis.git"] # Uncomment for local development of warp-proto-apis -# warp_multi_agent_api = { path = "../warp-proto-apis/apis/multi_agent/v1/gen/rust" } +warp_multi_agent_api = { path = "../warp-proto-apis/apis/multi_agent/v1/gen/rust" } [patch."https://github.com/warpdotdev/session-sharing-protocol.git"] # Uncomment for local development of session-sharing-protocol diff --git a/app/Cargo.toml b/app/Cargo.toml index a2cfe94fb0..7f4cb8a9fd 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -1013,6 +1013,7 @@ cloud_mode_input_v2 = ["cloud_mode"] handoff_cloud_cloud = ["cloud_mode_setup_v2"] git_credential_refresh = [] prompt_cache_expiry_warning = [] +fetched_memories_chip = [] [package.metadata.bundle.bin.warp-oss] category = "public.app-category.developer-tools" diff --git a/app/src/ai/agent/conversation.rs b/app/src/ai/agent/conversation.rs index e53ba5f15d..90c7619048 100644 --- a/app/src/ai/agent/conversation.rs +++ b/app/src/ai/agent/conversation.rs @@ -1184,6 +1184,27 @@ impl AIConversation { self.task_store.all_linearized_messages() } + /// Returns the memories the server fetched for this conversation, in the order they first + /// appeared across messages (server-side rank order within each message). Re-fetched + /// memories are deduped by `memory_id`: the first appearance keeps its position while the + /// entry's content/store/source are updated to the latest occurrence. + pub fn fetched_memories(&self) -> Vec { + let mut memories: Vec = Vec::new(); + let mut index_by_id: HashMap = HashMap::new(); + for message in self.task_store.all_linearized_messages() { + for memory in &message.fetched_memories { + match index_by_id.get(&memory.memory_id) { + Some(index) => memories[*index] = memory.clone(), + None => { + index_by_id.insert(memory.memory_id.clone(), memories.len()); + memories.push(memory.clone()); + } + } + } + } + memories + } + /// Returns all the tasks in this conversation. /// /// Note that until we've fully migrated to the multi-agent endpoint, in reality, each diff --git a/app/src/ai/agent/conversation_tests.rs b/app/src/ai/agent/conversation_tests.rs index 9777a3ad1a..ff06a1d2f5 100644 --- a/app/src/ai/agent/conversation_tests.rs +++ b/app/src/ai/agent/conversation_tests.rs @@ -743,3 +743,111 @@ fn restored_conversation_does_not_re_enter_waiting_for_events() { assert_eq!(conversation.status(), &ConversationStatus::Success); } + +fn fetched_memory( + memory_id: &str, + content: &str, + memory_store_id: &str, + source: Option, +) -> api::message::FetchedMemory { + api::message::FetchedMemory { + memory_id: memory_id.to_string(), + content: content.to_string(), + memory_store_id: memory_store_id.to_string(), + source, + } +} + +fn conversation_source(conversation_id: &str) -> Option { + Some(api::message::fetched_memory::Source::Conversation( + api::message::fetched_memory::Conversation { + conversation_id: conversation_id.to_string(), + }, + )) +} + +fn restored_conversation_with_memories_per_query( + memories_per_query: Vec>, +) -> AIConversation { + let messages = memories_per_query + .into_iter() + .enumerate() + .flat_map(|(index, memories)| { + let request_id = format!("request-{index}"); + let query = api::Message { + fetched_memories: memories, + ..user_query_message(&format!("user-{index}"), &request_id, "query") + }; + [ + query, + agent_output_message(&format!("agent-{index}"), &request_id), + ] + }) + .collect(); + + AIConversation::new_restored( + AIConversationId::new(), + vec![api::Task { + id: "root-task".to_string(), + messages, + ..Default::default() + }], + None, + ) + .unwrap() +} + +#[test] +fn fetched_memories_is_empty_when_no_message_has_memories() { + let conversation = restored_conversation_with_memories_per_query(vec![vec![]]); + + assert_eq!(conversation.fetched_memories(), vec![]); +} + +#[test] +fn fetched_memories_preserves_order_across_and_within_messages() { + let conversation = restored_conversation_with_memories_per_query(vec![ + vec![ + fetched_memory("m1", "first", "store-1", None), + fetched_memory("m2", "second", "store-1", None), + ], + vec![fetched_memory("m3", "third", "store-2", None)], + ]); + + let ids: Vec = conversation + .fetched_memories() + .into_iter() + .map(|memory| memory.memory_id) + .collect(); + assert_eq!(ids, vec!["m1", "m2", "m3"]); +} + +#[test] +fn fetched_memories_dedupes_keeping_first_position_and_latest_data() { + let conversation = restored_conversation_with_memories_per_query(vec![ + vec![ + fetched_memory("m1", "old content", "store-old", None), + fetched_memory("m2", "other", "store-1", None), + ], + vec![fetched_memory( + "m1", + "new content", + "store-new", + conversation_source("conversation-1"), + )], + ]); + + let memories = conversation.fetched_memories(); + assert_eq!( + memories, + vec![ + fetched_memory( + "m1", + "new content", + "store-new", + conversation_source("conversation-1"), + ), + fetched_memory("m2", "other", "store-1", None), + ] + ); +} diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs index cf8ee68873..b7319c5193 100644 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs @@ -50,6 +50,7 @@ pub(crate) use self::environment_selector::{ }; use crate::ai::blocklist::agent_view::is_in_cloud_context; use crate::ai::blocklist::history_model::{BlocklistAIHistoryEvent, BlocklistAIHistoryModel}; +use crate::ai::blocklist::prompt::fetched_memories::FetchedMemoriesView; use crate::ai::blocklist::prompt::prompt_alert::{PromptAlertEvent, PromptAlertView}; use crate::ai::blocklist::usage::icon_for_context_window_usage; use crate::ai::blocklist::BlocklistAIInputModel; @@ -241,6 +242,9 @@ pub struct AgentInputFooter { // `Workspace::start_local_to_cloud_handoff`. handoff_to_cloud_button: ViewHandle, + // Chip showing memories the server fetched for the active conversation. + fetched_memories_chip: ViewHandle, + // CLI agent voice input state (self-contained, bypasses editor voice flow). #[cfg(feature = "voice_input")] cli_voice_input_state: CLIVoiceInputState, @@ -813,6 +817,10 @@ impl AgentInputFooter { me.update_display_chips(&model, ctx); }); + let fetched_memories_chip = ctx.add_typed_action_view(|ctx| { + FetchedMemoriesView::new(menu_positioning_provider.clone(), terminal_view_id, ctx) + }); + let v2_model_selector = if FeatureFlag::CloudModeInputV2.is_enabled() { let ambient_agent_view_model_for_selector = ambient_agent_view_model.clone(); let view = ctx.add_typed_action_view(|ctx| { @@ -869,6 +877,7 @@ impl AgentInputFooter { display_chip_config, fast_forward_button, handoff_to_cloud_button, + fetched_memories_chip, #[cfg(feature = "voice_input")] cli_voice_input_state: CLIVoiceInputState::default(), #[cfg(feature = "voice_input")] @@ -1492,7 +1501,8 @@ impl AgentInputFooter { | AgentToolbarItemKind::NLDToggle | AgentToolbarItemKind::ContextWindowUsage | AgentToolbarItemKind::FastForwardToggle - | AgentToolbarItemKind::HandoffToCloud => None, + | AgentToolbarItemKind::HandoffToCloud + | AgentToolbarItemKind::FetchedMemories => None, } } @@ -2206,6 +2216,11 @@ impl AgentInputFooter { Some(ChildView::new(&self.handoff_to_cloud_button).finish()) } + AgentToolbarItemKind::FetchedMemories => self + .fetched_memories_chip + .as_ref(app) + .should_render(app) + .then(|| ChildView::new(&self.fetched_memories_chip).finish()), // Handled by the available_in() guard above; included for exhaustiveness. AgentToolbarItemKind::FileExplorer | AgentToolbarItemKind::RichInput diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs index 4d813f620d..a9269840be 100644 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs @@ -72,6 +72,9 @@ pub enum AgentToolbarItemKind { // Agent view only – "Hand off to cloud" chip. HandoffToCloud, + + // Agent view only – memories the server fetched for the active conversation. + FetchedMemories, } impl AgentToolbarItemKind { @@ -84,7 +87,8 @@ impl AgentToolbarItemKind { | Self::NLDToggle | Self::ContextWindowUsage | Self::FastForwardToggle - | Self::HandoffToCloud => ToolbarAvailability::AgentViewOnly, + | Self::HandoffToCloud + | Self::FetchedMemories => ToolbarAvailability::AgentViewOnly, Self::FileExplorer | Self::RichInput | Self::Settings => { ToolbarAvailability::CLIAgentOnly } @@ -110,7 +114,8 @@ impl AgentToolbarItemKind { | Self::NLDToggle | Self::ContextWindowUsage | Self::RichInput - | Self::VoiceInput => true, + | Self::VoiceInput + | Self::FetchedMemories => true, } } @@ -128,6 +133,7 @@ impl AgentToolbarItemKind { Self::Settings => "Settings", Self::FastForwardToggle => "Fast Forward", Self::HandoffToCloud => "Hand off to cloud", + Self::FetchedMemories => "Memories", } } @@ -147,6 +153,7 @@ impl AgentToolbarItemKind { // The bundled `upload-cloud-01.svg` (cloud-with-upward-arrow) is the // closest fit among the existing icons for V0; design may swap it later. Self::HandoffToCloud => Some(Icon::UploadCloud), + Self::FetchedMemories => Some(Icon::Cognition), } } @@ -163,6 +170,7 @@ impl AgentToolbarItemKind { | Self::ContextWindowUsage | Self::FastForwardToggle | Self::HandoffToCloud + | Self::FetchedMemories | Self::ShareSession | Self::FileExplorer | Self::RichInput @@ -176,6 +184,7 @@ impl AgentToolbarItemKind { pub fn is_available(&self, app: &warpui::AppContext) -> bool { match self { Self::HandoffToCloud => AISettings::as_ref(app).is_cloud_handoff_enabled(app), + Self::FetchedMemories => FeatureFlag::FetchedMemoriesChip.is_enabled(), _ => true, } } @@ -211,8 +220,11 @@ impl AgentToolbarItemKind { let mut items = vec![ Self::ContextChip(ContextChipKind::AgentPlanAndTodoList), Self::ContextWindowUsage, - Self::ModelSelector, ]; + if FeatureFlag::FetchedMemoriesChip.is_enabled() { + items.push(Self::FetchedMemories); + } + items.push(Self::ModelSelector); if FeatureFlag::CreatingSharedSessions.is_enabled() && FeatureFlag::HOARemoteControl.is_enabled() { @@ -256,6 +268,9 @@ impl AgentToolbarItemKind { { items.push(Self::HandoffToCloud); } + if FeatureFlag::FetchedMemoriesChip.is_enabled() { + items.push(Self::FetchedMemories); + } items } @@ -330,3 +345,7 @@ impl From for AgentToolbarItemKind { Self::ContextChip(kind) } } + +#[cfg(test)] +#[path = "toolbar_item_tests.rs"] +mod tests; diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item_tests.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item_tests.rs new file mode 100644 index 0000000000..9bfd59d46d --- /dev/null +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item_tests.rs @@ -0,0 +1,56 @@ +use super::AgentToolbarItemKind; +use crate::features::FeatureFlag; +use crate::terminal::shared_session::SharedSessionStatus; +use crate::ui_components::icons::Icon; + +#[test] +fn fetched_memories_is_agent_view_only() { + let item = AgentToolbarItemKind::FetchedMemories; + + assert!(item.available_in().is_available_for_agent_view()); + assert!(!item.available_in().is_available_for_cli()); +} + +#[test] +fn fetched_memories_is_visible_to_session_viewers() { + let item = AgentToolbarItemKind::FetchedMemories; + + assert!(item.available_to_session_viewer(&SharedSessionStatus::reader(), false)); + assert!(item.available_to_session_viewer(&SharedSessionStatus::NotShared, false)); +} + +#[test] +fn fetched_memories_display_metadata() { + let item = AgentToolbarItemKind::FetchedMemories; + + assert_eq!(item.display_label(), "Memories"); + assert_eq!(item.icon(), Some(Icon::Cognition)); + assert!(!item.is_available_during_handoff_compose()); +} + +#[test] +fn default_right_inserts_fetched_memories_after_context_usage_when_flag_enabled() { + let _flag = FeatureFlag::FetchedMemoriesChip.override_enabled(true); + + let items = AgentToolbarItemKind::default_right(); + let context_usage_index = items + .iter() + .position(|item| matches!(item, AgentToolbarItemKind::ContextWindowUsage)) + .expect("default_right should contain ContextWindowUsage"); + + assert_eq!( + items.get(context_usage_index + 1), + Some(&AgentToolbarItemKind::FetchedMemories) + ); + assert!(AgentToolbarItemKind::all_available().contains(&AgentToolbarItemKind::FetchedMemories)); +} + +#[test] +fn default_right_excludes_fetched_memories_when_flag_disabled() { + let _flag = FeatureFlag::FetchedMemoriesChip.override_enabled(false); + + assert!(!AgentToolbarItemKind::default_right().contains(&AgentToolbarItemKind::FetchedMemories)); + assert!( + !AgentToolbarItemKind::all_available().contains(&AgentToolbarItemKind::FetchedMemories) + ); +} diff --git a/app/src/ai/blocklist/prompt.rs b/app/src/ai/blocklist/prompt.rs index 842f7f4516..1c1ca5f3df 100644 --- a/app/src/ai/blocklist/prompt.rs +++ b/app/src/ai/blocklist/prompt.rs @@ -10,6 +10,7 @@ use crate::util::color::coloru_with_opacity; use crate::view_components::action_button::{ActionButtonTheme, NakedTheme}; use crate::Appearance; +pub mod fetched_memories; pub mod plan_and_todo_list; pub mod prompt_alert; diff --git a/app/src/ai/blocklist/prompt/fetched_memories.rs b/app/src/ai/blocklist/prompt/fetched_memories.rs new file mode 100644 index 0000000000..dca7f43123 --- /dev/null +++ b/app/src/ai/blocklist/prompt/fetched_memories.rs @@ -0,0 +1,565 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::sync::Arc; + +use pathfinder_geometry::vector::vec2f; +use serde_json::{json, Value}; +use strum_macros::{EnumDiscriminants, EnumIter}; +use warp_core::channel::ChannelState; +use warp_core::features::FeatureFlag; +use warp_core::send_telemetry_from_ctx; +use warp_core::telemetry::{EnablementState, TelemetryEvent, TelemetryEventDesc}; +use warp_core::ui::appearance::Appearance; +use warp_core::ui::theme::color::internal_colors; +use warp_core::ui::Icon; +use warp_multi_agent_api as api; +use warpui::elements::{ + Border, ChildAnchor, ChildView, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox, + Container, CornerRadius, CrossAxisAlignment, Dismiss, DropShadow, Empty, Flex, Hoverable, + MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, + PositionedElementAnchor, PositionedElementOffsetBounds, Radius, SavePosition, ScrollbarWidth, + Stack, Text, DEFAULT_UI_LINE_HEIGHT_RATIO, +}; +use warpui::fonts::{Properties, Weight}; +use warpui::keymap::FixedBinding; +use warpui::platform::Cursor; +use warpui::ui_components::components::UiComponent; +use warpui::{ + AppContext, Element, Entity, EntityId, SingletonEntity as _, TypedActionView, View, + ViewContext, ViewHandle, +}; + +use crate::ai::blocklist::{BlocklistAIHistoryEvent, BlocklistAIHistoryModel}; +use crate::terminal::input::{MenuPositioning, MenuPositioningProvider}; +use crate::ui_components::blended_colors; + +const FETCHED_MEMORIES_BUTTON_SAVE_POSITION_ID: &str = "fetched_memories::chip_button"; + +const POPUP_WIDTH: f32 = 360.; +const POPUP_MAX_HEIGHT: f32 = 200.; + +pub fn init(app: &mut AppContext) { + use warpui::keymap::macros::*; + + app.register_fixed_bindings([FixedBinding::new( + "escape", + FetchedMemoriesPopupAction::ClosePopup, + id!(FetchedMemoriesPopupView::ui_name()), + )]); +} + +fn fetched_memories_for_terminal_view( + terminal_view_id: EntityId, + app: &AppContext, +) -> Vec { + BlocklistAIHistoryModel::as_ref(app) + .active_conversation(terminal_view_id) + .map(|conversation| conversation.fetched_memories()) + .unwrap_or_default() +} + +fn notify_on_conversation_memory_events( + event: &BlocklistAIHistoryEvent, + terminal_view_id: EntityId, + notify: impl FnOnce(), +) { + if event + .terminal_view_id() + .is_some_and(|id| id != terminal_view_id) + { + return; + } + match event { + BlocklistAIHistoryEvent::StartedNewConversation { .. } + | BlocklistAIHistoryEvent::SetActiveConversation { .. } + | BlocklistAIHistoryEvent::ClearedConversationsInTerminalView { .. } + | BlocklistAIHistoryEvent::AppendedExchange { .. } + | BlocklistAIHistoryEvent::UpdatedStreamingExchange { .. } => notify(), + _ => (), + } +} + +/// A context chip in the agent input footer showing the memories the server +/// fetched for the active conversation. +pub struct FetchedMemoriesView { + menu_positioning_provider: Arc, + terminal_view_id: EntityId, + chip_mouse_state: MouseStateHandle, + popup: ViewHandle, + is_popup_open: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FetchedMemoriesAction { + TogglePopup, +} + +impl FetchedMemoriesView { + pub fn new( + menu_positioning_provider: Arc, + terminal_view_id: EntityId, + ctx: &mut ViewContext, + ) -> Self { + let popup = ctx + .add_typed_action_view(|ctx| FetchedMemoriesPopupView::new(terminal_view_id, ctx)); + ctx.subscribe_to_view(&popup, |me, _, event, ctx| match event { + FetchedMemoriesPopupEvent::Close => { + me.is_popup_open = false; + ctx.notify(); + } + }); + + ctx.subscribe_to_model( + &BlocklistAIHistoryModel::handle(ctx), + |me, _, event, ctx| { + notify_on_conversation_memory_events(event, me.terminal_view_id, || ctx.notify()); + }, + ); + + Self { + menu_positioning_provider, + terminal_view_id, + chip_mouse_state: Default::default(), + popup, + is_popup_open: false, + } + } + + pub fn should_render(&self, app: &AppContext) -> bool { + FeatureFlag::FetchedMemoriesChip.is_enabled() + && !fetched_memories_for_terminal_view(self.terminal_view_id, app).is_empty() + } + + fn fetched_memories(&self, app: &AppContext) -> Vec { + fetched_memories_for_terminal_view(self.terminal_view_id, app) + } +} + +impl Entity for FetchedMemoriesView { + type Event = (); +} + +impl View for FetchedMemoriesView { + fn ui_name() -> &'static str { + "FetchedMemoriesView" + } + + fn render(&self, app: &AppContext) -> Box { + if !self.should_render(app) { + return Empty::new().finish(); + } + let memory_count = self.fetched_memories(app).len(); + + let appearance = Appearance::as_ref(app); + let theme = appearance.theme(); + + let base_icon_size = app.font_cache().line_height( + appearance.monospace_font_size(), + DEFAULT_UI_LINE_HEIGHT_RATIO / 1.4, + ); + let text_line_height = app.font_cache().line_height( + appearance.monospace_font_size() - 1.0, + appearance.line_height_ratio(), + ); + let icon_size = (base_icon_size * 1.1).min(text_line_height); + + let memory_icon = Container::new( + ConstrainedBox::new( + Icon::Cognition + .to_warpui_icon(theme.sub_text_color(blended_colors::neutral_1(theme).into())) + .finish(), + ) + .with_height(icon_size) + .with_width(icon_size) + .finish(), + ) + .finish(); + + let chip_font_size = appearance.monospace_font_size() - 1.0; + let count_text = Text::new_inline( + format!("{memory_count}"), + appearance.ui_font_family(), + chip_font_size, + ) + .with_color(blended_colors::text_main(theme, theme.surface_1())) + .with_line_height_ratio(appearance.line_height_ratio()) + .with_style(Properties::default().weight(Weight::Semibold)) + .finish(); + + let content = Flex::row() + .with_cross_axis_alignment(CrossAxisAlignment::Center) + .with_child(memory_icon) + .with_child(Container::new(count_text).with_margin_left(4.).finish()) + .finish(); + + let tooltip_text = format!("{memory_count} memories fetched for this conversation"); + let chip_button = Hoverable::new(self.chip_mouse_state.clone(), move |state| { + let background = if state.is_hovered() { + internal_colors::fg_overlay_2(appearance.theme()) + } else { + internal_colors::fg_overlay_1(appearance.theme()) + }; + + let container = Container::new(content) + .with_background(background) + .with_padding_left(6.) + .with_padding_right(6.) + .with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.))) + .with_border( + Border::all(1.0) + .with_border_fill(internal_colors::neutral_3(appearance.theme())), + ) + .with_padding_top(2.) + .with_padding_bottom(2.) + .finish(); + + if state.is_hovered() { + let mut stack = Stack::new().with_child(container); + + let tooltip_element = appearance + .ui_builder() + .tool_tip(tooltip_text) + .build() + .finish(); + + stack.add_positioned_overlay_child( + tooltip_element, + OffsetPositioning::offset_from_parent( + vec2f(0., -8.), + ParentOffsetBounds::WindowByPosition, + ParentAnchor::TopLeft, + ChildAnchor::BottomLeft, + ), + ); + stack.finish() + } else { + container + } + }) + .with_cursor(Cursor::PointingHand) + .on_click(|ctx, _, _| { + ctx.dispatch_typed_action(FetchedMemoriesAction::TogglePopup); + }) + .finish(); + + let chip_button = + SavePosition::new(chip_button, FETCHED_MEMORIES_BUTTON_SAVE_POSITION_ID).finish(); + + let mut chip_button = Stack::new().with_child(chip_button); + if self.is_popup_open { + let positioning = match self.menu_positioning_provider.menu_position(app) { + MenuPositioning::BelowInputBox => { + OffsetPositioning::offset_from_save_position_element( + FETCHED_MEMORIES_BUTTON_SAVE_POSITION_ID, + vec2f(0., 4.), + PositionedElementOffsetBounds::WindowByPosition, + PositionedElementAnchor::BottomLeft, + ChildAnchor::TopLeft, + ) + } + MenuPositioning::AboveInputBox => { + OffsetPositioning::offset_from_save_position_element( + FETCHED_MEMORIES_BUTTON_SAVE_POSITION_ID, + vec2f(0., -4.), + PositionedElementOffsetBounds::WindowByPosition, + PositionedElementAnchor::TopLeft, + ChildAnchor::BottomLeft, + ) + } + }; + chip_button + .add_positioned_overlay_child(ChildView::new(&self.popup).finish(), positioning); + } + + chip_button.finish() + } +} + +impl TypedActionView for FetchedMemoriesView { + type Action = FetchedMemoriesAction; + + fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext) { + match action { + FetchedMemoriesAction::TogglePopup => { + self.is_popup_open = !self.is_popup_open; + if self.is_popup_open { + let memory_count = self.fetched_memories(ctx).len(); + send_telemetry_from_ctx!( + FetchedMemoriesTelemetryEvent::PopupOpened { memory_count }, + ctx + ); + ctx.focus(&self.popup); + } + ctx.notify(); + } + } + } +} + +/// Anchored popup listing the fetched memories. Each row links to the memory +/// in the Oz web app. +pub struct FetchedMemoriesPopupView { + terminal_view_id: EntityId, + scroll_state: ClippedScrollStateHandle, + /// Hover state per memory row, persisted across renders. `RefCell` so + /// `render` can lazily insert handles for newly fetched memories. + row_mouse_states: RefCell>, +} + +#[derive(Debug, Clone)] +pub enum FetchedMemoriesPopupAction { + ClosePopup, + OpenMemory { + memory_store_id: String, + memory_id: String, + }, +} + +pub enum FetchedMemoriesPopupEvent { + Close, +} + +impl FetchedMemoriesPopupView { + pub fn new(terminal_view_id: EntityId, ctx: &mut ViewContext) -> Self { + ctx.subscribe_to_model( + &BlocklistAIHistoryModel::handle(ctx), + |me, _, event, ctx| { + notify_on_conversation_memory_events(event, me.terminal_view_id, || ctx.notify()); + }, + ); + Self { + terminal_view_id, + scroll_state: Default::default(), + row_mouse_states: RefCell::new(HashMap::new()), + } + } + + fn row_mouse_state(&self, memory_id: &str) -> MouseStateHandle { + self.row_mouse_states + .borrow_mut() + .entry(memory_id.to_string()) + .or_default() + .clone() + } +} + +impl Entity for FetchedMemoriesPopupView { + type Event = FetchedMemoriesPopupEvent; +} + +impl View for FetchedMemoriesPopupView { + fn ui_name() -> &'static str { + "FetchedMemoriesPopup" + } + + fn render(&self, app: &AppContext) -> Box { + let memories = fetched_memories_for_terminal_view(self.terminal_view_id, app); + if memories.is_empty() { + return Empty::new().finish(); + } + + let appearance = Appearance::as_ref(app); + let theme = appearance.theme(); + let background = theme.surface_2(); + let main_text_color = blended_colors::text_main(theme, background); + let sub_text_color = blended_colors::text_sub(theme, background); + let font_size = appearance.ui_font_size(); + let line_height_ratio = appearance.line_height_ratio(); + let content_line_height = app.font_cache().line_height(font_size, line_height_ratio); + + let mut list_col = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch); + for memory in &memories { + let content_text = ConstrainedBox::new( + Text::new( + memory.content.clone(), + appearance.ui_font_family(), + font_size, + ) + .with_color(main_text_color) + .with_line_height_ratio(line_height_ratio) + .with_selectable(false) + .finish(), + ) + .with_max_height(content_line_height * 2.0) + .finish(); + + let mut row_col = + Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch); + row_col.add_child(content_text); + + let annotation = match &memory.source { + Some(api::message::fetched_memory::Source::Conversation(_)) => { + Some("From conversation") + } + Some(api::message::fetched_memory::Source::Manual(_)) => Some("Manual"), + None => None, + }; + if let Some(annotation) = annotation { + row_col.add_child( + Container::new( + Text::new_inline(annotation, appearance.ui_font_family(), font_size - 2.) + .with_color(sub_text_color) + .with_selectable(false) + .finish(), + ) + .with_margin_top(2.) + .finish(), + ); + } + + let row_content = row_col.finish(); + let memory_store_id = memory.memory_store_id.clone(); + let memory_id = memory.memory_id.clone(); + let row = Hoverable::new(self.row_mouse_state(&memory.memory_id), move |state| { + let mut container = Container::new(row_content) + .with_horizontal_padding(12.) + .with_vertical_padding(6.); + if state.is_hovered() { + container = container.with_background(internal_colors::fg_overlay_2(theme)); + } + container.finish() + }) + .with_cursor(Cursor::PointingHand) + .on_click(move |ctx, _, _| { + ctx.dispatch_typed_action(FetchedMemoriesPopupAction::OpenMemory { + memory_store_id: memory_store_id.clone(), + memory_id: memory_id.clone(), + }); + }) + .finish(); + + list_col.add_child(row); + } + + let scrollable_body = ClippedScrollable::vertical( + self.scroll_state.clone(), + Container::new(list_col.finish()) + .with_vertical_padding(6.) + .finish(), + ScrollbarWidth::Auto, + theme.nonactive_ui_detail().into(), + theme.active_ui_detail().into(), + warpui::elements::Fill::None, + ) + .with_overlayed_scrollbar() + .finish(); + + Dismiss::new( + ConstrainedBox::new( + Container::new(scrollable_body) + .with_background(background) + .with_corner_radius(CornerRadius::with_all(Radius::Pixels(6.))) + .with_drop_shadow(DropShadow::default()) + .finish(), + ) + .with_width(POPUP_WIDTH) + .with_max_height(POPUP_MAX_HEIGHT) + .finish(), + ) + .prevent_interaction_with_other_elements() + .on_dismiss(|ctx, _app| { + ctx.dispatch_typed_action(FetchedMemoriesPopupAction::ClosePopup); + }) + .finish() + } +} + +impl TypedActionView for FetchedMemoriesPopupView { + type Action = FetchedMemoriesPopupAction; + + fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext) { + match action { + FetchedMemoriesPopupAction::ClosePopup => { + ctx.emit(FetchedMemoriesPopupEvent::Close); + } + FetchedMemoriesPopupAction::OpenMemory { + memory_store_id, + memory_id, + } => { + let memory_count = + fetched_memories_for_terminal_view(self.terminal_view_id, ctx).len(); + send_telemetry_from_ctx!( + FetchedMemoriesTelemetryEvent::MemoryLinkClicked { memory_count }, + ctx + ); + let oz_root_url = ChannelState::oz_root_url(); + let url = format!( + "{oz_root_url}/memory/{}/memories/{}", + urlencoding::encode(memory_store_id), + urlencoding::encode(memory_id) + ); + ctx.open_url(&url); + ctx.emit(FetchedMemoriesPopupEvent::Close); + } + } + } +} + +#[derive(Debug, EnumDiscriminants)] +#[strum_discriminants(derive(EnumIter))] +pub enum FetchedMemoriesTelemetryEvent { + PopupOpened { memory_count: usize }, + MemoryLinkClicked { memory_count: usize }, +} + +impl TelemetryEvent for FetchedMemoriesTelemetryEvent { + fn name(&self) -> &'static str { + FetchedMemoriesTelemetryEventDiscriminants::from(self).name() + } + + fn payload(&self) -> Option { + match self { + Self::PopupOpened { memory_count } | Self::MemoryLinkClicked { memory_count } => { + Some(json!({ + "memory_count": memory_count, + })) + } + } + } + + fn description(&self) -> &'static str { + FetchedMemoriesTelemetryEventDiscriminants::from(self).description() + } + + fn enablement_state(&self) -> EnablementState { + FetchedMemoriesTelemetryEventDiscriminants::from(self).enablement_state() + } + + fn contains_ugc(&self) -> bool { + match self { + Self::PopupOpened { .. } | Self::MemoryLinkClicked { .. } => false, + } + } + + fn event_descs() -> impl Iterator> { + warp_core::telemetry::enum_events::() + } +} + +impl TelemetryEventDesc for FetchedMemoriesTelemetryEventDiscriminants { + fn name(&self) -> &'static str { + match self { + Self::PopupOpened => "AgentMode.FetchedMemories.PopupOpened", + Self::MemoryLinkClicked => "AgentMode.FetchedMemories.MemoryLinkClicked", + } + } + + fn description(&self) -> &'static str { + match self { + Self::PopupOpened => "User opened the fetched memories popup from the footer chip", + Self::MemoryLinkClicked => { + "User clicked a fetched memory row to open it in the Oz web app" + } + } + } + + fn enablement_state(&self) -> EnablementState { + match self { + Self::PopupOpened | Self::MemoryLinkClicked => { + EnablementState::Flag(FeatureFlag::FetchedMemoriesChip) + } + } + } +} + +warp_core::register_telemetry_event!(FetchedMemoriesTelemetryEvent); diff --git a/app/src/features.rs b/app/src/features.rs index 66566783f9..47e8450a20 100644 --- a/app/src/features.rs +++ b/app/src/features.rs @@ -503,6 +503,8 @@ fn enabled_features() -> HashSet { FeatureFlag::GeminiEnterprise, #[cfg(feature = "prompt_cache_expiry_warning")] FeatureFlag::PromptCacheExpiryWarning, + #[cfg(feature = "fetched_memories_chip")] + FeatureFlag::FetchedMemoriesChip, ]); flags diff --git a/app/src/lib.rs b/app/src/lib.rs index 78516517a9..e43e3a9e4f 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -1732,6 +1732,7 @@ pub(crate) fn initialize_app( context_chips::node_version_popup::init(ctx); env_vars::view::env_var_collection::init(ctx); ai::agent::todos::popup::init(ctx); + ai::blocklist::prompt::fetched_memories::init(ctx); terminal::view::init_environment::mode_selector::init(ctx); coding_entrypoints::project_buttons::init(ctx); if FeatureFlag::CodeReviewSaveChanges.is_enabled() { diff --git a/crates/warp_features/src/lib.rs b/crates/warp_features/src/lib.rs index 1518b39247..e8ad800ed4 100644 --- a/crates/warp_features/src/lib.rs +++ b/crates/warp_features/src/lib.rs @@ -892,6 +892,10 @@ pub enum FeatureFlag { /// Shows a warning in the agent view when the active conversation's /// provider-side prompt cache has expired. PromptCacheExpiryWarning, + + /// Shows a chip in the agent input footer listing memories the server + /// fetched for the active conversation. + FetchedMemoriesChip, } static FLAG_STATES: [AtomicBool; cardinality::()] = @@ -961,6 +965,7 @@ pub const DOGFOOD_FLAGS: &[FeatureFlag] = &[ FeatureFlag::PromptCacheExpiryWarning, FeatureFlag::PinnedTabs, FeatureFlag::ContextWindowUsageBreakdown, + FeatureFlag::FetchedMemoriesChip, ]; /// Features enabled for feature preview build users (e.g.: Friends of Warp). From 04e7957fa894346876ee2bc5c23435e7e22b53a0 Mon Sep 17 00:00:00 2001 From: Leon Fattakhov Date: Tue, 23 Jun 2026 12:13:46 -0700 Subject: [PATCH 03/13] WIP: Add AgentMemory citation type to client - New AIAgentCitation::AgentMemory { memory_store_id, memory_id } variant - TryFrom splits compound 'store_id:memory_id' document_id - AgentModeCitation::AgentMemory added to telemetry events - for_telemetry impl maps AgentMemory through to the telemetry type - render_citation: Cognition icon + 'Memory' label for AgentMemory chips - OpenCitation handler opens oz_root_url/memory/{store}/memories/{id} Co-Authored-By: Oz --- app/src/ai/agent/telemetry.rs | 7 +++++++ app/src/ai/blocklist/block/view_impl.rs | 4 ++++ app/src/server/telemetry/events.rs | 4 ++++ app/src/terminal/view.rs | 12 ++++++++++++ crates/ai/src/agent/citation.rs | 18 ++++++++++++++++++ 5 files changed, 45 insertions(+) diff --git a/app/src/ai/agent/telemetry.rs b/app/src/ai/agent/telemetry.rs index a6a353105c..4308fcb98e 100644 --- a/app/src/ai/agent/telemetry.rs +++ b/app/src/ai/agent/telemetry.rs @@ -34,6 +34,13 @@ impl ForTelemetry for AIAgentCitation { Some(CitationForTelemetry::WarpDocs { page: path.clone() }) } Self::WebPage { url } => Some(CitationForTelemetry::WebPage { url: url.clone() }), + Self::AgentMemory { + memory_store_id, + memory_id, + } => Some(CitationForTelemetry::AgentMemory { + memory_store_id: memory_store_id.clone(), + memory_id: memory_id.clone(), + }), } } } diff --git a/app/src/ai/blocklist/block/view_impl.rs b/app/src/ai/blocklist/block/view_impl.rs index 06cb2de3d7..b3383fac9d 100644 --- a/app/src/ai/blocklist/block/view_impl.rs +++ b/app/src/ai/blocklist/block/view_impl.rs @@ -678,6 +678,10 @@ pub fn render_citation( let name = url.clone(); (Some(icon), name) } + AIAgentCitation::AgentMemory { .. } => { + let icon = Icon::Cognition.to_warpui_icon(theme.foreground()).finish(); + (Some(icon), String::from("Memory")) + } }; // Shorten the name to 30 chars. diff --git a/app/src/server/telemetry/events.rs b/app/src/server/telemetry/events.rs index a59f9ec092..f65c695972 100644 --- a/app/src/server/telemetry/events.rs +++ b/app/src/server/telemetry/events.rs @@ -991,6 +991,10 @@ pub enum AgentModeCitation { #[serde(skip_serializing)] url: String, }, + AgentMemory { + memory_store_id: String, + memory_id: String, + }, } #[derive(Clone, Copy, Debug, Serialize)] diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index 5ca2b76bb0..69a9c05510 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -20226,6 +20226,18 @@ impl TerminalView { AIAgentCitation::WebPage { url } => { ctx.open_url(url); } + AIAgentCitation::AgentMemory { + memory_store_id, + memory_id, + } => { + let oz_root_url = ChannelState::oz_root_url(); + let url = format!( + "{oz_root_url}/memory/{}/memories/{}", + urlencoding::encode(memory_store_id), + urlencoding::encode(memory_id) + ); + ctx.open_url(&url); + } }, AIBlockEvent::OpenAIFactCollection { sync_id } => { ctx.emit(Event::OpenAIFactCollection { sync_id: *sync_id }); diff --git a/crates/ai/src/agent/citation.rs b/crates/ai/src/agent/citation.rs index 449e300fac..0dd84a7c81 100644 --- a/crates/ai/src/agent/citation.rs +++ b/crates/ai/src/agent/citation.rs @@ -8,6 +8,7 @@ pub enum AIAgentCitation { WarpDriveObject { uid: String }, WarpDocumentation { path: String }, WebPage { url: String }, + AgentMemory { memory_store_id: String, memory_id: String }, } impl Display for AIAgentCitation { @@ -22,6 +23,12 @@ impl Display for AIAgentCitation { AIAgentCitation::WebPage { url } => { write!(f, "Web Page: {url}") } + AIAgentCitation::AgentMemory { + memory_store_id, + memory_id, + } => { + write!(f, "Agent Memory: {memory_store_id}/{memory_id}") + } } } } @@ -51,6 +58,17 @@ impl TryFrom for AIAgentCitation { api::DocumentType::WebPage => Ok(AIAgentCitation::WebPage { url: citation.document_id, }), + api::DocumentType::AgentMemory => { + let (memory_store_id, memory_id) = citation + .document_id + .split_once(':') + .map(|(s, m)| (s.to_string(), m.to_string())) + .ok_or(UnknownCitationTypeError)?; + Ok(AIAgentCitation::AgentMemory { + memory_store_id, + memory_id, + }) + } api::DocumentType::Unknown => Err(UnknownCitationTypeError), } } From 699ebfc7f0d8ccf85b01dd688cd76d2430e7f3aa Mon Sep 17 00:00:00 2001 From: Leon Fattakhov Date: Tue, 23 Jun 2026 17:07:47 -0700 Subject: [PATCH 04/13] WIP: Fix build errors from proto version skew (round 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Newer local proto branch adds fields/variants not yet handled in client: Struct literals — add ..Default::default() to: - api::request::Settings (custom_model_routers, supports_background_computer_use) - api::message::ModelUsed (prompt_cache_expires_at) - stream_finished::ConversationUsageMetadata x2 (context_window_segments, total_input_tokens) Exhaustive matches — add WaitForEvents arms to: - write_tool_call_args (conversation_yaml.rs) — no-op, no args to serialize - write_tool_call_result_content (conversation_yaml.rs) — emit 'status: completed' - convert_tool_call_result_to_input (convert_conversation.rs) — return None - create_cancelled_result_for_tool_call (convert_conversation.rs) — return None - ToolExt::name (task/helper.rs) — 'wait_for_events' Co-Authored-By: Oz --- app/src/ai/agent/api/convert_conversation.rs | 2 ++ app/src/ai/agent/conversation_yaml.rs | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/ai/agent/api/convert_conversation.rs b/app/src/ai/agent/api/convert_conversation.rs index 5b8bc0d669..4ddd6cb595 100644 --- a/app/src/ai/agent/api/convert_conversation.rs +++ b/app/src/ai/agent/api/convert_conversation.rs @@ -1648,6 +1648,7 @@ pub(crate) fn convert_tool_call_result_to_input( // Deprecated/unused result types or absent result. Some(ToolCallResultType::SuggestCreatePlan(..)) | Some(ToolCallResultType::SuggestPlan(..)) + | Some(ToolCallResultType::WaitForEvents(..)) | None => { log::warn!("No result present for tool call ID: {tool_call_id}"); None @@ -1764,6 +1765,7 @@ fn create_cancelled_result_for_tool_call( return None; } ToolType::Subagent(_) => return None, + ToolType::WaitForEvents(_) => return None, ToolType::StartAgent(_) => { AIAgentActionResultType::StartAgent(StartAgentResult::Cancelled { version: StartAgentVersion::V1, diff --git a/app/src/ai/agent/conversation_yaml.rs b/app/src/ai/agent/conversation_yaml.rs index 2a38528fdc..e05e4f5b51 100644 --- a/app/src/ai/agent/conversation_yaml.rs +++ b/app/src/ai/agent/conversation_yaml.rs @@ -1102,7 +1102,8 @@ fn write_tool_call_result_content(out: &mut String, result: &ToolCallResultType) | ToolCallResultType::InitProject(_) | ToolCallResultType::TransferShellCommandControlToUser(_) | ToolCallResultType::SuggestCreatePlan(_) - | ToolCallResultType::SuggestPlan(_) => { + | ToolCallResultType::SuggestPlan(_) + | ToolCallResultType::WaitForEvents(_) => { out.push_str("status: completed\n"); } } From 89ccc56b94b49c29a12ede20a405bffcbc59c227 Mon Sep 17 00:00:00 2001 From: Leon Fattakhov Date: Tue, 23 Jun 2026 17:53:27 -0700 Subject: [PATCH 05/13] WIP: Derive AgentMemory citations from fetched_memories at render time - Add content: String to AIAgentCitation::AgentMemory so chips show a truncated preview of the memory text (like rules show their title) - Hash/Eq implemented manually to key on IDs only, not content - render_citation shows truncated content or falls back to 'Memory' - At render_references_footer call site, synthesize AgentMemory citations directly from conv.fetched_memories() -- no LLM XML step, no timing issue, works for live/restored/cloud conversations Co-Authored-By: Oz --- app/src/ai/agent/telemetry.rs | 1 + app/src/ai/blocklist/block/view_impl.rs | 9 ++- .../ai/blocklist/block/view_impl/output.rs | 20 ++++++- app/src/terminal/view.rs | 1 + crates/ai/src/agent/citation.rs | 58 +++++++++++++++++-- 5 files changed, 80 insertions(+), 9 deletions(-) diff --git a/app/src/ai/agent/telemetry.rs b/app/src/ai/agent/telemetry.rs index 4308fcb98e..d38ea48802 100644 --- a/app/src/ai/agent/telemetry.rs +++ b/app/src/ai/agent/telemetry.rs @@ -37,6 +37,7 @@ impl ForTelemetry for AIAgentCitation { Self::AgentMemory { memory_store_id, memory_id, + .. } => Some(CitationForTelemetry::AgentMemory { memory_store_id: memory_store_id.clone(), memory_id: memory_id.clone(), diff --git a/app/src/ai/blocklist/block/view_impl.rs b/app/src/ai/blocklist/block/view_impl.rs index b3383fac9d..a19bc6b1b5 100644 --- a/app/src/ai/blocklist/block/view_impl.rs +++ b/app/src/ai/blocklist/block/view_impl.rs @@ -678,9 +678,14 @@ pub fn render_citation( let name = url.clone(); (Some(icon), name) } - AIAgentCitation::AgentMemory { .. } => { + AIAgentCitation::AgentMemory { content, .. } => { let icon = Icon::Cognition.to_warpui_icon(theme.foreground()).finish(); - (Some(icon), String::from("Memory")) + let name = if content.is_empty() { + String::from("Memory") + } else { + content.clone() + }; + (Some(icon), name) } }; diff --git a/app/src/ai/blocklist/block/view_impl/output.rs b/app/src/ai/blocklist/block/view_impl/output.rs index 0dd79a0e23..ccc4c727d9 100644 --- a/app/src/ai/blocklist/block/view_impl/output.rs +++ b/app/src/ai/blocklist/block/view_impl/output.rs @@ -1098,8 +1098,26 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box { } if should_render_references_section { + let memory_citations: Vec = props + .model + .conversation(app) + .into_iter() + .flat_map(|conv| conv.fetched_memories()) + .filter(|m| !m.memory_store_id.is_empty() && !m.memory_id.is_empty()) + .map(|m| AIAgentCitation::AgentMemory { + memory_store_id: m.memory_store_id.clone(), + memory_id: m.memory_id.clone(), + content: m.content.clone(), + }) + .collect(); + let all_citations: Vec = output + .citations + .iter() + .cloned() + .chain(memory_citations) + .collect(); if let Some(references) = - render_references_footer(&output.citations, props, app) + render_references_footer(&all_citations, props, app) { output_items.add_child(references); } diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index 69a9c05510..663ce52184 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -20229,6 +20229,7 @@ impl TerminalView { AIAgentCitation::AgentMemory { memory_store_id, memory_id, + .. } => { let oz_root_url = ChannelState::oz_root_url(); let url = format!( diff --git a/crates/ai/src/agent/citation.rs b/crates/ai/src/agent/citation.rs index 0dd84a7c81..7fca8bdce5 100644 --- a/crates/ai/src/agent/citation.rs +++ b/crates/ai/src/agent/citation.rs @@ -1,14 +1,62 @@ use std::fmt::Display; +use std::hash::{Hash, Hasher}; use warp_multi_agent_api as api; /// A citation listed in an AI response. -#[derive(Debug, Clone, Hash, Eq, PartialEq)] +#[derive(Debug, Clone)] pub enum AIAgentCitation { WarpDriveObject { uid: String }, WarpDocumentation { path: String }, WebPage { url: String }, - AgentMemory { memory_store_id: String, memory_id: String }, + /// A memory from an attached memory store. `content` is the raw memory + /// text shown as a preview in the chip; `Hash`/`Eq` use only the IDs. + AgentMemory { + memory_store_id: String, + memory_id: String, + content: String, + }, +} + +impl PartialEq for AIAgentCitation { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::WarpDriveObject { uid: a }, Self::WarpDriveObject { uid: b }) => a == b, + (Self::WarpDocumentation { path: a }, Self::WarpDocumentation { path: b }) => a == b, + (Self::WebPage { url: a }, Self::WebPage { url: b }) => a == b, + ( + Self::AgentMemory { memory_store_id: s1, memory_id: i1, .. }, + Self::AgentMemory { memory_store_id: s2, memory_id: i2, .. }, + ) => s1 == s2 && i1 == i2, + _ => false, + } + } +} + +impl Eq for AIAgentCitation {} + +impl Hash for AIAgentCitation { + fn hash(&self, state: &mut H) { + match self { + Self::WarpDriveObject { uid } => { + 0u8.hash(state); + uid.hash(state); + } + Self::WarpDocumentation { path } => { + 1u8.hash(state); + path.hash(state); + } + Self::WebPage { url } => { + 2u8.hash(state); + url.hash(state); + } + Self::AgentMemory { memory_store_id, memory_id, .. } => { + 3u8.hash(state); + memory_store_id.hash(state); + memory_id.hash(state); + } + } + } } impl Display for AIAgentCitation { @@ -23,10 +71,7 @@ impl Display for AIAgentCitation { AIAgentCitation::WebPage { url } => { write!(f, "Web Page: {url}") } - AIAgentCitation::AgentMemory { - memory_store_id, - memory_id, - } => { + AIAgentCitation::AgentMemory { memory_store_id, memory_id, .. } => { write!(f, "Agent Memory: {memory_store_id}/{memory_id}") } } @@ -67,6 +112,7 @@ impl TryFrom for AIAgentCitation { Ok(AIAgentCitation::AgentMemory { memory_store_id, memory_id, + content: String::new(), }) } api::DocumentType::Unknown => Err(UnknownCitationTypeError), From b3e6ecaeb543543112bb1db766591a4dced9c642 Mon Sep 17 00:00:00 2001 From: Leon Fattakhov Date: Tue, 23 Jun 2026 18:03:10 -0700 Subject: [PATCH 06/13] WIP: Register MouseStateHandles for memory citations in handle_updated_output render_citation_chips gates rendering on citation_state_handles.get(citation). Memory citations are synthesized at render time from fetched_memories, so they never go through the output.citations path and never got handles registered. Fix: in handle_updated_output, also register AgentMemory handles from conversation.fetched_memories() alongside the existing output.citations loop. The entry().or_default() pattern is idempotent so repeated calls are safe. Co-Authored-By: Oz --- app/src/ai/blocklist/block.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/ai/blocklist/block.rs b/app/src/ai/blocklist/block.rs index df54c3fa3b..e2273233b3 100644 --- a/app/src/ai/blocklist/block.rs +++ b/app/src/ai/blocklist/block.rs @@ -2149,6 +2149,23 @@ impl AIBlock { .entry(citation.clone()) .or_default(); } + // Also register handles for memory citations derived from fetched_memories, + // which are synthesized at render time and never go through output.citations. + if let Some(conversation) = self.model.conversation(ctx) { + for memory in conversation.fetched_memories() { + if memory.memory_store_id.is_empty() || memory.memory_id.is_empty() { + continue; + } + self.state_handles + .footer_citation_chip_handles + .entry(AIAgentCitation::AgentMemory { + memory_store_id: memory.memory_store_id.clone(), + memory_id: memory.memory_id.clone(), + content: memory.content.clone(), + }) + .or_default(); + } + } // Register element state for reasoning messages and track summarization timing. for message in &output.messages { From 1ae7a736d2a0dcd5832dd11a9c58e0ad291bb478 Mon Sep 17 00:00:00 2001 From: Leon Fattakhov Date: Tue, 23 Jun 2026 18:07:28 -0700 Subject: [PATCH 07/13] WIP: Scope memory citations to first exchange only Memory retrieval only happens for the first user message, so the References section should only show AgentMemory chips on the first exchange's output block, not on every subsequent turn. Use conv.first_exchange().id == exchange_id check at both the render site (output.rs) and the handle registration site (block.rs). Co-Authored-By: Oz --- app/src/ai/blocklist/block.rs | 29 ++++++++++++------- .../ai/blocklist/block/view_impl/output.rs | 5 ++++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/app/src/ai/blocklist/block.rs b/app/src/ai/blocklist/block.rs index e2273233b3..8c69e3e3e5 100644 --- a/app/src/ai/blocklist/block.rs +++ b/app/src/ai/blocklist/block.rs @@ -2151,19 +2151,26 @@ impl AIBlock { } // Also register handles for memory citations derived from fetched_memories, // which are synthesized at render time and never go through output.citations. + // Only register for the first exchange since that's the only one that shows them. if let Some(conversation) = self.model.conversation(ctx) { - for memory in conversation.fetched_memories() { - if memory.memory_store_id.is_empty() || memory.memory_id.is_empty() { - continue; + let is_first_exchange = conversation + .first_exchange() + .map(|e| Some(e.id) == self.model.exchange_id(ctx)) + .unwrap_or(false); + if is_first_exchange { + for memory in conversation.fetched_memories() { + if memory.memory_store_id.is_empty() || memory.memory_id.is_empty() { + continue; + } + self.state_handles + .footer_citation_chip_handles + .entry(AIAgentCitation::AgentMemory { + memory_store_id: memory.memory_store_id.clone(), + memory_id: memory.memory_id.clone(), + content: memory.content.clone(), + }) + .or_default(); } - self.state_handles - .footer_citation_chip_handles - .entry(AIAgentCitation::AgentMemory { - memory_store_id: memory.memory_store_id.clone(), - memory_id: memory.memory_id.clone(), - content: memory.content.clone(), - }) - .or_default(); } } diff --git a/app/src/ai/blocklist/block/view_impl/output.rs b/app/src/ai/blocklist/block/view_impl/output.rs index ccc4c727d9..8539fbb02e 100644 --- a/app/src/ai/blocklist/block/view_impl/output.rs +++ b/app/src/ai/blocklist/block/view_impl/output.rs @@ -1098,9 +1098,14 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box { } if should_render_references_section { + let exchange_id = props.model.exchange_id(app); let memory_citations: Vec = props .model .conversation(app) + .filter(|conv| { + // Only show memory citations on the first exchange. + conv.first_exchange().map(|e| Some(e.id)) == Some(exchange_id) + }) .into_iter() .flat_map(|conv| conv.fetched_memories()) .filter(|m| !m.memory_store_id.is_empty() && !m.memory_id.is_empty()) From 563b2d4a88c87d888cdd97d50ee79792740306a2 Mon Sep 17 00:00:00 2001 From: Leon Fattakhov Date: Wed, 24 Jun 2026 13:22:27 -0700 Subject: [PATCH 08/13] WIP: Remove fetched memories footer chip Keep fetched_memories proto field and AIConversation::fetched_memories() since they power the References section citations. Remove everything else: FetchedMemoriesView, FetchedMemoriesChip feature flag, the AgentToolbarItemKind::FetchedMemories variant, and its toolbar wiring. Co-Authored-By: Oz --- .../agent_view/agent_input_footer/mod.rs | 17 +- .../agent_input_footer/toolbar_item.rs | 18 +- .../agent_input_footer/toolbar_item_tests.rs | 56 -- app/src/ai/blocklist/prompt.rs | 1 - .../ai/blocklist/prompt/fetched_memories.rs | 565 ------------------ app/src/features.rs | 2 - app/src/lib.rs | 1 - crates/warp_features/src/lib.rs | 5 - 8 files changed, 3 insertions(+), 662 deletions(-) delete mode 100644 app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item_tests.rs delete mode 100644 app/src/ai/blocklist/prompt/fetched_memories.rs diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs index b7319c5193..cf8ee68873 100644 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs @@ -50,7 +50,6 @@ pub(crate) use self::environment_selector::{ }; use crate::ai::blocklist::agent_view::is_in_cloud_context; use crate::ai::blocklist::history_model::{BlocklistAIHistoryEvent, BlocklistAIHistoryModel}; -use crate::ai::blocklist::prompt::fetched_memories::FetchedMemoriesView; use crate::ai::blocklist::prompt::prompt_alert::{PromptAlertEvent, PromptAlertView}; use crate::ai::blocklist::usage::icon_for_context_window_usage; use crate::ai::blocklist::BlocklistAIInputModel; @@ -242,9 +241,6 @@ pub struct AgentInputFooter { // `Workspace::start_local_to_cloud_handoff`. handoff_to_cloud_button: ViewHandle, - // Chip showing memories the server fetched for the active conversation. - fetched_memories_chip: ViewHandle, - // CLI agent voice input state (self-contained, bypasses editor voice flow). #[cfg(feature = "voice_input")] cli_voice_input_state: CLIVoiceInputState, @@ -817,10 +813,6 @@ impl AgentInputFooter { me.update_display_chips(&model, ctx); }); - let fetched_memories_chip = ctx.add_typed_action_view(|ctx| { - FetchedMemoriesView::new(menu_positioning_provider.clone(), terminal_view_id, ctx) - }); - let v2_model_selector = if FeatureFlag::CloudModeInputV2.is_enabled() { let ambient_agent_view_model_for_selector = ambient_agent_view_model.clone(); let view = ctx.add_typed_action_view(|ctx| { @@ -877,7 +869,6 @@ impl AgentInputFooter { display_chip_config, fast_forward_button, handoff_to_cloud_button, - fetched_memories_chip, #[cfg(feature = "voice_input")] cli_voice_input_state: CLIVoiceInputState::default(), #[cfg(feature = "voice_input")] @@ -1501,8 +1492,7 @@ impl AgentInputFooter { | AgentToolbarItemKind::NLDToggle | AgentToolbarItemKind::ContextWindowUsage | AgentToolbarItemKind::FastForwardToggle - | AgentToolbarItemKind::HandoffToCloud - | AgentToolbarItemKind::FetchedMemories => None, + | AgentToolbarItemKind::HandoffToCloud => None, } } @@ -2216,11 +2206,6 @@ impl AgentInputFooter { Some(ChildView::new(&self.handoff_to_cloud_button).finish()) } - AgentToolbarItemKind::FetchedMemories => self - .fetched_memories_chip - .as_ref(app) - .should_render(app) - .then(|| ChildView::new(&self.fetched_memories_chip).finish()), // Handled by the available_in() guard above; included for exhaustiveness. AgentToolbarItemKind::FileExplorer | AgentToolbarItemKind::RichInput diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs index a9269840be..8a1356a36a 100644 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs @@ -73,8 +73,6 @@ pub enum AgentToolbarItemKind { // Agent view only – "Hand off to cloud" chip. HandoffToCloud, - // Agent view only – memories the server fetched for the active conversation. - FetchedMemories, } impl AgentToolbarItemKind { @@ -87,8 +85,7 @@ impl AgentToolbarItemKind { | Self::NLDToggle | Self::ContextWindowUsage | Self::FastForwardToggle - | Self::HandoffToCloud - | Self::FetchedMemories => ToolbarAvailability::AgentViewOnly, + | Self::HandoffToCloud => ToolbarAvailability::AgentViewOnly, Self::FileExplorer | Self::RichInput | Self::Settings => { ToolbarAvailability::CLIAgentOnly } @@ -114,8 +111,7 @@ impl AgentToolbarItemKind { | Self::NLDToggle | Self::ContextWindowUsage | Self::RichInput - | Self::VoiceInput - | Self::FetchedMemories => true, + | Self::VoiceInput => true, } } @@ -133,7 +129,6 @@ impl AgentToolbarItemKind { Self::Settings => "Settings", Self::FastForwardToggle => "Fast Forward", Self::HandoffToCloud => "Hand off to cloud", - Self::FetchedMemories => "Memories", } } @@ -153,7 +148,6 @@ impl AgentToolbarItemKind { // The bundled `upload-cloud-01.svg` (cloud-with-upward-arrow) is the // closest fit among the existing icons for V0; design may swap it later. Self::HandoffToCloud => Some(Icon::UploadCloud), - Self::FetchedMemories => Some(Icon::Cognition), } } @@ -170,7 +164,6 @@ impl AgentToolbarItemKind { | Self::ContextWindowUsage | Self::FastForwardToggle | Self::HandoffToCloud - | Self::FetchedMemories | Self::ShareSession | Self::FileExplorer | Self::RichInput @@ -184,7 +177,6 @@ impl AgentToolbarItemKind { pub fn is_available(&self, app: &warpui::AppContext) -> bool { match self { Self::HandoffToCloud => AISettings::as_ref(app).is_cloud_handoff_enabled(app), - Self::FetchedMemories => FeatureFlag::FetchedMemoriesChip.is_enabled(), _ => true, } } @@ -221,9 +213,6 @@ impl AgentToolbarItemKind { Self::ContextChip(ContextChipKind::AgentPlanAndTodoList), Self::ContextWindowUsage, ]; - if FeatureFlag::FetchedMemoriesChip.is_enabled() { - items.push(Self::FetchedMemories); - } items.push(Self::ModelSelector); if FeatureFlag::CreatingSharedSessions.is_enabled() && FeatureFlag::HOARemoteControl.is_enabled() @@ -268,9 +257,6 @@ impl AgentToolbarItemKind { { items.push(Self::HandoffToCloud); } - if FeatureFlag::FetchedMemoriesChip.is_enabled() { - items.push(Self::FetchedMemories); - } items } diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item_tests.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item_tests.rs deleted file mode 100644 index 9bfd59d46d..0000000000 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item_tests.rs +++ /dev/null @@ -1,56 +0,0 @@ -use super::AgentToolbarItemKind; -use crate::features::FeatureFlag; -use crate::terminal::shared_session::SharedSessionStatus; -use crate::ui_components::icons::Icon; - -#[test] -fn fetched_memories_is_agent_view_only() { - let item = AgentToolbarItemKind::FetchedMemories; - - assert!(item.available_in().is_available_for_agent_view()); - assert!(!item.available_in().is_available_for_cli()); -} - -#[test] -fn fetched_memories_is_visible_to_session_viewers() { - let item = AgentToolbarItemKind::FetchedMemories; - - assert!(item.available_to_session_viewer(&SharedSessionStatus::reader(), false)); - assert!(item.available_to_session_viewer(&SharedSessionStatus::NotShared, false)); -} - -#[test] -fn fetched_memories_display_metadata() { - let item = AgentToolbarItemKind::FetchedMemories; - - assert_eq!(item.display_label(), "Memories"); - assert_eq!(item.icon(), Some(Icon::Cognition)); - assert!(!item.is_available_during_handoff_compose()); -} - -#[test] -fn default_right_inserts_fetched_memories_after_context_usage_when_flag_enabled() { - let _flag = FeatureFlag::FetchedMemoriesChip.override_enabled(true); - - let items = AgentToolbarItemKind::default_right(); - let context_usage_index = items - .iter() - .position(|item| matches!(item, AgentToolbarItemKind::ContextWindowUsage)) - .expect("default_right should contain ContextWindowUsage"); - - assert_eq!( - items.get(context_usage_index + 1), - Some(&AgentToolbarItemKind::FetchedMemories) - ); - assert!(AgentToolbarItemKind::all_available().contains(&AgentToolbarItemKind::FetchedMemories)); -} - -#[test] -fn default_right_excludes_fetched_memories_when_flag_disabled() { - let _flag = FeatureFlag::FetchedMemoriesChip.override_enabled(false); - - assert!(!AgentToolbarItemKind::default_right().contains(&AgentToolbarItemKind::FetchedMemories)); - assert!( - !AgentToolbarItemKind::all_available().contains(&AgentToolbarItemKind::FetchedMemories) - ); -} diff --git a/app/src/ai/blocklist/prompt.rs b/app/src/ai/blocklist/prompt.rs index 1c1ca5f3df..842f7f4516 100644 --- a/app/src/ai/blocklist/prompt.rs +++ b/app/src/ai/blocklist/prompt.rs @@ -10,7 +10,6 @@ use crate::util::color::coloru_with_opacity; use crate::view_components::action_button::{ActionButtonTheme, NakedTheme}; use crate::Appearance; -pub mod fetched_memories; pub mod plan_and_todo_list; pub mod prompt_alert; diff --git a/app/src/ai/blocklist/prompt/fetched_memories.rs b/app/src/ai/blocklist/prompt/fetched_memories.rs deleted file mode 100644 index dca7f43123..0000000000 --- a/app/src/ai/blocklist/prompt/fetched_memories.rs +++ /dev/null @@ -1,565 +0,0 @@ -use std::cell::RefCell; -use std::collections::HashMap; -use std::sync::Arc; - -use pathfinder_geometry::vector::vec2f; -use serde_json::{json, Value}; -use strum_macros::{EnumDiscriminants, EnumIter}; -use warp_core::channel::ChannelState; -use warp_core::features::FeatureFlag; -use warp_core::send_telemetry_from_ctx; -use warp_core::telemetry::{EnablementState, TelemetryEvent, TelemetryEventDesc}; -use warp_core::ui::appearance::Appearance; -use warp_core::ui::theme::color::internal_colors; -use warp_core::ui::Icon; -use warp_multi_agent_api as api; -use warpui::elements::{ - Border, ChildAnchor, ChildView, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox, - Container, CornerRadius, CrossAxisAlignment, Dismiss, DropShadow, Empty, Flex, Hoverable, - MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, - PositionedElementAnchor, PositionedElementOffsetBounds, Radius, SavePosition, ScrollbarWidth, - Stack, Text, DEFAULT_UI_LINE_HEIGHT_RATIO, -}; -use warpui::fonts::{Properties, Weight}; -use warpui::keymap::FixedBinding; -use warpui::platform::Cursor; -use warpui::ui_components::components::UiComponent; -use warpui::{ - AppContext, Element, Entity, EntityId, SingletonEntity as _, TypedActionView, View, - ViewContext, ViewHandle, -}; - -use crate::ai::blocklist::{BlocklistAIHistoryEvent, BlocklistAIHistoryModel}; -use crate::terminal::input::{MenuPositioning, MenuPositioningProvider}; -use crate::ui_components::blended_colors; - -const FETCHED_MEMORIES_BUTTON_SAVE_POSITION_ID: &str = "fetched_memories::chip_button"; - -const POPUP_WIDTH: f32 = 360.; -const POPUP_MAX_HEIGHT: f32 = 200.; - -pub fn init(app: &mut AppContext) { - use warpui::keymap::macros::*; - - app.register_fixed_bindings([FixedBinding::new( - "escape", - FetchedMemoriesPopupAction::ClosePopup, - id!(FetchedMemoriesPopupView::ui_name()), - )]); -} - -fn fetched_memories_for_terminal_view( - terminal_view_id: EntityId, - app: &AppContext, -) -> Vec { - BlocklistAIHistoryModel::as_ref(app) - .active_conversation(terminal_view_id) - .map(|conversation| conversation.fetched_memories()) - .unwrap_or_default() -} - -fn notify_on_conversation_memory_events( - event: &BlocklistAIHistoryEvent, - terminal_view_id: EntityId, - notify: impl FnOnce(), -) { - if event - .terminal_view_id() - .is_some_and(|id| id != terminal_view_id) - { - return; - } - match event { - BlocklistAIHistoryEvent::StartedNewConversation { .. } - | BlocklistAIHistoryEvent::SetActiveConversation { .. } - | BlocklistAIHistoryEvent::ClearedConversationsInTerminalView { .. } - | BlocklistAIHistoryEvent::AppendedExchange { .. } - | BlocklistAIHistoryEvent::UpdatedStreamingExchange { .. } => notify(), - _ => (), - } -} - -/// A context chip in the agent input footer showing the memories the server -/// fetched for the active conversation. -pub struct FetchedMemoriesView { - menu_positioning_provider: Arc, - terminal_view_id: EntityId, - chip_mouse_state: MouseStateHandle, - popup: ViewHandle, - is_popup_open: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum FetchedMemoriesAction { - TogglePopup, -} - -impl FetchedMemoriesView { - pub fn new( - menu_positioning_provider: Arc, - terminal_view_id: EntityId, - ctx: &mut ViewContext, - ) -> Self { - let popup = ctx - .add_typed_action_view(|ctx| FetchedMemoriesPopupView::new(terminal_view_id, ctx)); - ctx.subscribe_to_view(&popup, |me, _, event, ctx| match event { - FetchedMemoriesPopupEvent::Close => { - me.is_popup_open = false; - ctx.notify(); - } - }); - - ctx.subscribe_to_model( - &BlocklistAIHistoryModel::handle(ctx), - |me, _, event, ctx| { - notify_on_conversation_memory_events(event, me.terminal_view_id, || ctx.notify()); - }, - ); - - Self { - menu_positioning_provider, - terminal_view_id, - chip_mouse_state: Default::default(), - popup, - is_popup_open: false, - } - } - - pub fn should_render(&self, app: &AppContext) -> bool { - FeatureFlag::FetchedMemoriesChip.is_enabled() - && !fetched_memories_for_terminal_view(self.terminal_view_id, app).is_empty() - } - - fn fetched_memories(&self, app: &AppContext) -> Vec { - fetched_memories_for_terminal_view(self.terminal_view_id, app) - } -} - -impl Entity for FetchedMemoriesView { - type Event = (); -} - -impl View for FetchedMemoriesView { - fn ui_name() -> &'static str { - "FetchedMemoriesView" - } - - fn render(&self, app: &AppContext) -> Box { - if !self.should_render(app) { - return Empty::new().finish(); - } - let memory_count = self.fetched_memories(app).len(); - - let appearance = Appearance::as_ref(app); - let theme = appearance.theme(); - - let base_icon_size = app.font_cache().line_height( - appearance.monospace_font_size(), - DEFAULT_UI_LINE_HEIGHT_RATIO / 1.4, - ); - let text_line_height = app.font_cache().line_height( - appearance.monospace_font_size() - 1.0, - appearance.line_height_ratio(), - ); - let icon_size = (base_icon_size * 1.1).min(text_line_height); - - let memory_icon = Container::new( - ConstrainedBox::new( - Icon::Cognition - .to_warpui_icon(theme.sub_text_color(blended_colors::neutral_1(theme).into())) - .finish(), - ) - .with_height(icon_size) - .with_width(icon_size) - .finish(), - ) - .finish(); - - let chip_font_size = appearance.monospace_font_size() - 1.0; - let count_text = Text::new_inline( - format!("{memory_count}"), - appearance.ui_font_family(), - chip_font_size, - ) - .with_color(blended_colors::text_main(theme, theme.surface_1())) - .with_line_height_ratio(appearance.line_height_ratio()) - .with_style(Properties::default().weight(Weight::Semibold)) - .finish(); - - let content = Flex::row() - .with_cross_axis_alignment(CrossAxisAlignment::Center) - .with_child(memory_icon) - .with_child(Container::new(count_text).with_margin_left(4.).finish()) - .finish(); - - let tooltip_text = format!("{memory_count} memories fetched for this conversation"); - let chip_button = Hoverable::new(self.chip_mouse_state.clone(), move |state| { - let background = if state.is_hovered() { - internal_colors::fg_overlay_2(appearance.theme()) - } else { - internal_colors::fg_overlay_1(appearance.theme()) - }; - - let container = Container::new(content) - .with_background(background) - .with_padding_left(6.) - .with_padding_right(6.) - .with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.))) - .with_border( - Border::all(1.0) - .with_border_fill(internal_colors::neutral_3(appearance.theme())), - ) - .with_padding_top(2.) - .with_padding_bottom(2.) - .finish(); - - if state.is_hovered() { - let mut stack = Stack::new().with_child(container); - - let tooltip_element = appearance - .ui_builder() - .tool_tip(tooltip_text) - .build() - .finish(); - - stack.add_positioned_overlay_child( - tooltip_element, - OffsetPositioning::offset_from_parent( - vec2f(0., -8.), - ParentOffsetBounds::WindowByPosition, - ParentAnchor::TopLeft, - ChildAnchor::BottomLeft, - ), - ); - stack.finish() - } else { - container - } - }) - .with_cursor(Cursor::PointingHand) - .on_click(|ctx, _, _| { - ctx.dispatch_typed_action(FetchedMemoriesAction::TogglePopup); - }) - .finish(); - - let chip_button = - SavePosition::new(chip_button, FETCHED_MEMORIES_BUTTON_SAVE_POSITION_ID).finish(); - - let mut chip_button = Stack::new().with_child(chip_button); - if self.is_popup_open { - let positioning = match self.menu_positioning_provider.menu_position(app) { - MenuPositioning::BelowInputBox => { - OffsetPositioning::offset_from_save_position_element( - FETCHED_MEMORIES_BUTTON_SAVE_POSITION_ID, - vec2f(0., 4.), - PositionedElementOffsetBounds::WindowByPosition, - PositionedElementAnchor::BottomLeft, - ChildAnchor::TopLeft, - ) - } - MenuPositioning::AboveInputBox => { - OffsetPositioning::offset_from_save_position_element( - FETCHED_MEMORIES_BUTTON_SAVE_POSITION_ID, - vec2f(0., -4.), - PositionedElementOffsetBounds::WindowByPosition, - PositionedElementAnchor::TopLeft, - ChildAnchor::BottomLeft, - ) - } - }; - chip_button - .add_positioned_overlay_child(ChildView::new(&self.popup).finish(), positioning); - } - - chip_button.finish() - } -} - -impl TypedActionView for FetchedMemoriesView { - type Action = FetchedMemoriesAction; - - fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext) { - match action { - FetchedMemoriesAction::TogglePopup => { - self.is_popup_open = !self.is_popup_open; - if self.is_popup_open { - let memory_count = self.fetched_memories(ctx).len(); - send_telemetry_from_ctx!( - FetchedMemoriesTelemetryEvent::PopupOpened { memory_count }, - ctx - ); - ctx.focus(&self.popup); - } - ctx.notify(); - } - } - } -} - -/// Anchored popup listing the fetched memories. Each row links to the memory -/// in the Oz web app. -pub struct FetchedMemoriesPopupView { - terminal_view_id: EntityId, - scroll_state: ClippedScrollStateHandle, - /// Hover state per memory row, persisted across renders. `RefCell` so - /// `render` can lazily insert handles for newly fetched memories. - row_mouse_states: RefCell>, -} - -#[derive(Debug, Clone)] -pub enum FetchedMemoriesPopupAction { - ClosePopup, - OpenMemory { - memory_store_id: String, - memory_id: String, - }, -} - -pub enum FetchedMemoriesPopupEvent { - Close, -} - -impl FetchedMemoriesPopupView { - pub fn new(terminal_view_id: EntityId, ctx: &mut ViewContext) -> Self { - ctx.subscribe_to_model( - &BlocklistAIHistoryModel::handle(ctx), - |me, _, event, ctx| { - notify_on_conversation_memory_events(event, me.terminal_view_id, || ctx.notify()); - }, - ); - Self { - terminal_view_id, - scroll_state: Default::default(), - row_mouse_states: RefCell::new(HashMap::new()), - } - } - - fn row_mouse_state(&self, memory_id: &str) -> MouseStateHandle { - self.row_mouse_states - .borrow_mut() - .entry(memory_id.to_string()) - .or_default() - .clone() - } -} - -impl Entity for FetchedMemoriesPopupView { - type Event = FetchedMemoriesPopupEvent; -} - -impl View for FetchedMemoriesPopupView { - fn ui_name() -> &'static str { - "FetchedMemoriesPopup" - } - - fn render(&self, app: &AppContext) -> Box { - let memories = fetched_memories_for_terminal_view(self.terminal_view_id, app); - if memories.is_empty() { - return Empty::new().finish(); - } - - let appearance = Appearance::as_ref(app); - let theme = appearance.theme(); - let background = theme.surface_2(); - let main_text_color = blended_colors::text_main(theme, background); - let sub_text_color = blended_colors::text_sub(theme, background); - let font_size = appearance.ui_font_size(); - let line_height_ratio = appearance.line_height_ratio(); - let content_line_height = app.font_cache().line_height(font_size, line_height_ratio); - - let mut list_col = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch); - for memory in &memories { - let content_text = ConstrainedBox::new( - Text::new( - memory.content.clone(), - appearance.ui_font_family(), - font_size, - ) - .with_color(main_text_color) - .with_line_height_ratio(line_height_ratio) - .with_selectable(false) - .finish(), - ) - .with_max_height(content_line_height * 2.0) - .finish(); - - let mut row_col = - Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch); - row_col.add_child(content_text); - - let annotation = match &memory.source { - Some(api::message::fetched_memory::Source::Conversation(_)) => { - Some("From conversation") - } - Some(api::message::fetched_memory::Source::Manual(_)) => Some("Manual"), - None => None, - }; - if let Some(annotation) = annotation { - row_col.add_child( - Container::new( - Text::new_inline(annotation, appearance.ui_font_family(), font_size - 2.) - .with_color(sub_text_color) - .with_selectable(false) - .finish(), - ) - .with_margin_top(2.) - .finish(), - ); - } - - let row_content = row_col.finish(); - let memory_store_id = memory.memory_store_id.clone(); - let memory_id = memory.memory_id.clone(); - let row = Hoverable::new(self.row_mouse_state(&memory.memory_id), move |state| { - let mut container = Container::new(row_content) - .with_horizontal_padding(12.) - .with_vertical_padding(6.); - if state.is_hovered() { - container = container.with_background(internal_colors::fg_overlay_2(theme)); - } - container.finish() - }) - .with_cursor(Cursor::PointingHand) - .on_click(move |ctx, _, _| { - ctx.dispatch_typed_action(FetchedMemoriesPopupAction::OpenMemory { - memory_store_id: memory_store_id.clone(), - memory_id: memory_id.clone(), - }); - }) - .finish(); - - list_col.add_child(row); - } - - let scrollable_body = ClippedScrollable::vertical( - self.scroll_state.clone(), - Container::new(list_col.finish()) - .with_vertical_padding(6.) - .finish(), - ScrollbarWidth::Auto, - theme.nonactive_ui_detail().into(), - theme.active_ui_detail().into(), - warpui::elements::Fill::None, - ) - .with_overlayed_scrollbar() - .finish(); - - Dismiss::new( - ConstrainedBox::new( - Container::new(scrollable_body) - .with_background(background) - .with_corner_radius(CornerRadius::with_all(Radius::Pixels(6.))) - .with_drop_shadow(DropShadow::default()) - .finish(), - ) - .with_width(POPUP_WIDTH) - .with_max_height(POPUP_MAX_HEIGHT) - .finish(), - ) - .prevent_interaction_with_other_elements() - .on_dismiss(|ctx, _app| { - ctx.dispatch_typed_action(FetchedMemoriesPopupAction::ClosePopup); - }) - .finish() - } -} - -impl TypedActionView for FetchedMemoriesPopupView { - type Action = FetchedMemoriesPopupAction; - - fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext) { - match action { - FetchedMemoriesPopupAction::ClosePopup => { - ctx.emit(FetchedMemoriesPopupEvent::Close); - } - FetchedMemoriesPopupAction::OpenMemory { - memory_store_id, - memory_id, - } => { - let memory_count = - fetched_memories_for_terminal_view(self.terminal_view_id, ctx).len(); - send_telemetry_from_ctx!( - FetchedMemoriesTelemetryEvent::MemoryLinkClicked { memory_count }, - ctx - ); - let oz_root_url = ChannelState::oz_root_url(); - let url = format!( - "{oz_root_url}/memory/{}/memories/{}", - urlencoding::encode(memory_store_id), - urlencoding::encode(memory_id) - ); - ctx.open_url(&url); - ctx.emit(FetchedMemoriesPopupEvent::Close); - } - } - } -} - -#[derive(Debug, EnumDiscriminants)] -#[strum_discriminants(derive(EnumIter))] -pub enum FetchedMemoriesTelemetryEvent { - PopupOpened { memory_count: usize }, - MemoryLinkClicked { memory_count: usize }, -} - -impl TelemetryEvent for FetchedMemoriesTelemetryEvent { - fn name(&self) -> &'static str { - FetchedMemoriesTelemetryEventDiscriminants::from(self).name() - } - - fn payload(&self) -> Option { - match self { - Self::PopupOpened { memory_count } | Self::MemoryLinkClicked { memory_count } => { - Some(json!({ - "memory_count": memory_count, - })) - } - } - } - - fn description(&self) -> &'static str { - FetchedMemoriesTelemetryEventDiscriminants::from(self).description() - } - - fn enablement_state(&self) -> EnablementState { - FetchedMemoriesTelemetryEventDiscriminants::from(self).enablement_state() - } - - fn contains_ugc(&self) -> bool { - match self { - Self::PopupOpened { .. } | Self::MemoryLinkClicked { .. } => false, - } - } - - fn event_descs() -> impl Iterator> { - warp_core::telemetry::enum_events::() - } -} - -impl TelemetryEventDesc for FetchedMemoriesTelemetryEventDiscriminants { - fn name(&self) -> &'static str { - match self { - Self::PopupOpened => "AgentMode.FetchedMemories.PopupOpened", - Self::MemoryLinkClicked => "AgentMode.FetchedMemories.MemoryLinkClicked", - } - } - - fn description(&self) -> &'static str { - match self { - Self::PopupOpened => "User opened the fetched memories popup from the footer chip", - Self::MemoryLinkClicked => { - "User clicked a fetched memory row to open it in the Oz web app" - } - } - } - - fn enablement_state(&self) -> EnablementState { - match self { - Self::PopupOpened | Self::MemoryLinkClicked => { - EnablementState::Flag(FeatureFlag::FetchedMemoriesChip) - } - } - } -} - -warp_core::register_telemetry_event!(FetchedMemoriesTelemetryEvent); diff --git a/app/src/features.rs b/app/src/features.rs index 47e8450a20..66566783f9 100644 --- a/app/src/features.rs +++ b/app/src/features.rs @@ -503,8 +503,6 @@ fn enabled_features() -> HashSet { FeatureFlag::GeminiEnterprise, #[cfg(feature = "prompt_cache_expiry_warning")] FeatureFlag::PromptCacheExpiryWarning, - #[cfg(feature = "fetched_memories_chip")] - FeatureFlag::FetchedMemoriesChip, ]); flags diff --git a/app/src/lib.rs b/app/src/lib.rs index e43e3a9e4f..78516517a9 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -1732,7 +1732,6 @@ pub(crate) fn initialize_app( context_chips::node_version_popup::init(ctx); env_vars::view::env_var_collection::init(ctx); ai::agent::todos::popup::init(ctx); - ai::blocklist::prompt::fetched_memories::init(ctx); terminal::view::init_environment::mode_selector::init(ctx); coding_entrypoints::project_buttons::init(ctx); if FeatureFlag::CodeReviewSaveChanges.is_enabled() { diff --git a/crates/warp_features/src/lib.rs b/crates/warp_features/src/lib.rs index e8ad800ed4..1518b39247 100644 --- a/crates/warp_features/src/lib.rs +++ b/crates/warp_features/src/lib.rs @@ -892,10 +892,6 @@ pub enum FeatureFlag { /// Shows a warning in the agent view when the active conversation's /// provider-side prompt cache has expired. PromptCacheExpiryWarning, - - /// Shows a chip in the agent input footer listing memories the server - /// fetched for the active conversation. - FetchedMemoriesChip, } static FLAG_STATES: [AtomicBool; cardinality::()] = @@ -965,7 +961,6 @@ pub const DOGFOOD_FLAGS: &[FeatureFlag] = &[ FeatureFlag::PromptCacheExpiryWarning, FeatureFlag::PinnedTabs, FeatureFlag::ContextWindowUsageBreakdown, - FeatureFlag::FetchedMemoriesChip, ]; /// Features enabled for feature preview build users (e.g.: Friends of Warp). From 1285eae10e8ae266fe2070ced36263581e98bde6 Mon Sep 17 00:00:00 2001 From: Leon Fattakhov Date: Mon, 29 Jun 2026 13:37:14 -0700 Subject: [PATCH 09/13] Remove dangling mod tests declaration Co-Authored-By: Oz --- .../blocklist/agent_view/agent_input_footer/toolbar_item.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs index 8a1356a36a..24ada7413f 100644 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs @@ -331,7 +331,3 @@ impl From for AgentToolbarItemKind { Self::ContextChip(kind) } } - -#[cfg(test)] -#[path = "toolbar_item_tests.rs"] -mod tests; From 1d7d7f6a946aacee22d921229d0a135eea9f13aa Mon Sep 17 00:00:00 2001 From: Leon Fattakhov Date: Mon, 29 Jun 2026 13:47:16 -0700 Subject: [PATCH 10/13] Fix format issues and unreachable WaitForEvents pattern errors - Apply rustfmt formatting to citation.rs and output.rs - Remove duplicate WaitForEvents match arms in conversation_yaml.rs and convert_conversation.rs that caused unreachable pattern clippy errors Co-Authored-By: Oz --- app/src/ai/agent/api/convert_conversation.rs | 4 --- app/src/ai/agent/conversation_yaml.rs | 3 +- .../agent_input_footer/toolbar_item.rs | 1 - .../ai/blocklist/block/view_impl/output.rs | 4 +-- crates/ai/src/agent/citation.rs | 36 +++++++++++++++---- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/app/src/ai/agent/api/convert_conversation.rs b/app/src/ai/agent/api/convert_conversation.rs index 4ddd6cb595..ec47015f1a 100644 --- a/app/src/ai/agent/api/convert_conversation.rs +++ b/app/src/ai/agent/api/convert_conversation.rs @@ -1653,7 +1653,6 @@ pub(crate) fn convert_tool_call_result_to_input( log::warn!("No result present for tool call ID: {tool_call_id}"); None } - Some(ToolCallResultType::WaitForEvents(_)) => None, } } @@ -1787,9 +1786,6 @@ fn create_cancelled_result_for_tool_call( } // These tools are deprecated. ToolType::SuggestCreatePlan(_) | ToolType::SuggestPlan(_) => return None, - ToolType::WaitForEvents(_) => { - return None; - } }; Some(AIAgentInput::ActionResult { diff --git a/app/src/ai/agent/conversation_yaml.rs b/app/src/ai/agent/conversation_yaml.rs index e05e4f5b51..2a38528fdc 100644 --- a/app/src/ai/agent/conversation_yaml.rs +++ b/app/src/ai/agent/conversation_yaml.rs @@ -1102,8 +1102,7 @@ fn write_tool_call_result_content(out: &mut String, result: &ToolCallResultType) | ToolCallResultType::InitProject(_) | ToolCallResultType::TransferShellCommandControlToUser(_) | ToolCallResultType::SuggestCreatePlan(_) - | ToolCallResultType::SuggestPlan(_) - | ToolCallResultType::WaitForEvents(_) => { + | ToolCallResultType::SuggestPlan(_) => { out.push_str("status: completed\n"); } } diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs index 8bea2e2798..11215962aa 100644 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs @@ -72,7 +72,6 @@ pub enum AgentToolbarItemKind { // Agent view only – "Hand off to cloud" chip. HandoffToCloud, - } impl AgentToolbarItemKind { diff --git a/app/src/ai/blocklist/block/view_impl/output.rs b/app/src/ai/blocklist/block/view_impl/output.rs index 8539fbb02e..c3294d4f92 100644 --- a/app/src/ai/blocklist/block/view_impl/output.rs +++ b/app/src/ai/blocklist/block/view_impl/output.rs @@ -1121,9 +1121,7 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box { .cloned() .chain(memory_citations) .collect(); - if let Some(references) = - render_references_footer(&all_citations, props, app) - { + if let Some(references) = render_references_footer(&all_citations, props, app) { output_items.add_child(references); } } diff --git a/crates/ai/src/agent/citation.rs b/crates/ai/src/agent/citation.rs index 7fca8bdce5..30be4e1b3b 100644 --- a/crates/ai/src/agent/citation.rs +++ b/crates/ai/src/agent/citation.rs @@ -6,9 +6,15 @@ use warp_multi_agent_api as api; /// A citation listed in an AI response. #[derive(Debug, Clone)] pub enum AIAgentCitation { - WarpDriveObject { uid: String }, - WarpDocumentation { path: String }, - WebPage { url: String }, + WarpDriveObject { + uid: String, + }, + WarpDocumentation { + path: String, + }, + WebPage { + url: String, + }, /// A memory from an attached memory store. `content` is the raw memory /// text shown as a preview in the chip; `Hash`/`Eq` use only the IDs. AgentMemory { @@ -25,8 +31,16 @@ impl PartialEq for AIAgentCitation { (Self::WarpDocumentation { path: a }, Self::WarpDocumentation { path: b }) => a == b, (Self::WebPage { url: a }, Self::WebPage { url: b }) => a == b, ( - Self::AgentMemory { memory_store_id: s1, memory_id: i1, .. }, - Self::AgentMemory { memory_store_id: s2, memory_id: i2, .. }, + Self::AgentMemory { + memory_store_id: s1, + memory_id: i1, + .. + }, + Self::AgentMemory { + memory_store_id: s2, + memory_id: i2, + .. + }, ) => s1 == s2 && i1 == i2, _ => false, } @@ -50,7 +64,11 @@ impl Hash for AIAgentCitation { 2u8.hash(state); url.hash(state); } - Self::AgentMemory { memory_store_id, memory_id, .. } => { + Self::AgentMemory { + memory_store_id, + memory_id, + .. + } => { 3u8.hash(state); memory_store_id.hash(state); memory_id.hash(state); @@ -71,7 +89,11 @@ impl Display for AIAgentCitation { AIAgentCitation::WebPage { url } => { write!(f, "Web Page: {url}") } - AIAgentCitation::AgentMemory { memory_store_id, memory_id, .. } => { + AIAgentCitation::AgentMemory { + memory_store_id, + memory_id, + .. + } => { write!(f, "Agent Memory: {memory_store_id}/{memory_id}") } } From 789f33fd4f0fa0932b1caaa897cd8ea1ae674f1c Mon Sep 17 00:00:00 2001 From: Leon Fattakhov Date: Tue, 30 Jun 2026 11:19:01 -0700 Subject: [PATCH 11/13] Remove AgentMemory from DocumentType match (variant not in proto) AgentMemory citations are derived from fetched_memories at render time, not from the Citation proto, so TryFrom doesn't need to handle this case. Co-Authored-By: Oz --- crates/ai/src/agent/citation.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/crates/ai/src/agent/citation.rs b/crates/ai/src/agent/citation.rs index 30be4e1b3b..e29cdcbe00 100644 --- a/crates/ai/src/agent/citation.rs +++ b/crates/ai/src/agent/citation.rs @@ -125,18 +125,6 @@ impl TryFrom for AIAgentCitation { api::DocumentType::WebPage => Ok(AIAgentCitation::WebPage { url: citation.document_id, }), - api::DocumentType::AgentMemory => { - let (memory_store_id, memory_id) = citation - .document_id - .split_once(':') - .map(|(s, m)| (s.to_string(), m.to_string())) - .ok_or(UnknownCitationTypeError)?; - Ok(AIAgentCitation::AgentMemory { - memory_store_id, - memory_id, - content: String::new(), - }) - } api::DocumentType::Unknown => Err(UnknownCitationTypeError), } } From 2e2605f4739558005868b8258d50cfceb82d6dc6 Mon Sep 17 00:00:00 2001 From: Leon Fattakhov Date: Tue, 30 Jun 2026 11:19:01 -0700 Subject: [PATCH 12/13] Remove AgentMemory from DocumentType match (variant not in proto) AgentMemory citations are derived from fetched_memories at render time, not from the Citation proto, so TryFrom doesn't need to handle this case. Co-Authored-By: Oz --- Cargo.lock | 1 + Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90fc9f8d23..b2b7650650 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15653,6 +15653,7 @@ dependencies = [ [[package]] name = "warp_multi_agent_api" version = "0.0.0" +source = "git+https://github.com/warpdotdev/warp-proto-apis.git?rev=45f9b1342b256b20f716aede64dc1b46a639e5e6#45f9b1342b256b20f716aede64dc1b46a639e5e6" dependencies = [ "prost", "prost-reflect", diff --git a/Cargo.toml b/Cargo.toml index 4316fe2c4b..ba3d280df4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -338,7 +338,7 @@ version-compare = "0.1" vte = { git = "https://github.com/warpdotdev/vte.git", rev = "4b399c87b63ba88f45709edaa6383fc519f6c900", default-features = false } walkdir = "2" warp-workflows = { git = "https://github.com/warpdotdev/workflows", rev = "793a98ddda6ef19682aed66364faebd2829f0e01" } -warp_multi_agent_api = { git = "https://github.com/warpdotdev/warp-proto-apis.git", rev = "ac1af7303d2931b0fb485be650a1fbc8b80d5667" } +warp_multi_agent_api = { git = "https://github.com/warpdotdev/warp-proto-apis.git", rev = "45f9b1342b256b20f716aede64dc1b46a639e5e6" } wasm-bindgen = "0.2.89" wasm-bindgen-futures = "0.4.42" web-sys = { version = "0.3.69", features = [ @@ -550,7 +550,7 @@ tikv-jemalloc-sys = { git = "https://github.com/warpdotdev/jemallocator.git", re [patch."https://github.com/warpdotdev/warp-proto-apis.git"] # Uncomment for local development of warp-proto-apis -warp_multi_agent_api = { path = "../warp-proto-apis/apis/multi_agent/v1/gen/rust" } +# warp_multi_agent_api = { path = "../warp-proto-apis/apis/multi_agent/v1/gen/rust" } [patch."https://github.com/warpdotdev/session-sharing-protocol.git"] # Uncomment for local development of session-sharing-protocol From e5dcc1835b0b2f6dcc6d862300cde5abcddbecc5 Mon Sep 17 00:00:00 2001 From: Leon Fattakhov Date: Tue, 30 Jun 2026 13:24:21 -0700 Subject: [PATCH 13/13] Address fetched memory toolbar review comments - Remove the extra fetched_memories_chip feature flag - Restore ModelSelector to the default right toolbar item list Co-Authored-By: Oz --- app/Cargo.toml | 1 - app/src/ai/agent/api/convert_conversation.rs | 6 +- app/src/ai/agent/conversation.rs | 11 ++-- app/src/ai/agent/conversation_tests.rs | 20 ++++--- .../agent_input_footer/toolbar_item.rs | 2 +- app/src/server/telemetry/events.rs | 2 + crates/ai/src/agent/citation.rs | 58 +------------------ 7 files changed, 27 insertions(+), 73 deletions(-) diff --git a/app/Cargo.toml b/app/Cargo.toml index 29a7081737..348f94856d 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -1021,7 +1021,6 @@ cloud_mode_input_v2 = ["cloud_mode"] handoff_cloud_cloud = ["cloud_mode_setup_v2"] git_credential_refresh = [] prompt_cache_expiry_warning = [] -fetched_memories_chip = [] [package.metadata.bundle.bin.warp-oss] category = "public.app-category.developer-tools" diff --git a/app/src/ai/agent/api/convert_conversation.rs b/app/src/ai/agent/api/convert_conversation.rs index ec47015f1a..5b8bc0d669 100644 --- a/app/src/ai/agent/api/convert_conversation.rs +++ b/app/src/ai/agent/api/convert_conversation.rs @@ -1648,11 +1648,11 @@ pub(crate) fn convert_tool_call_result_to_input( // Deprecated/unused result types or absent result. Some(ToolCallResultType::SuggestCreatePlan(..)) | Some(ToolCallResultType::SuggestPlan(..)) - | Some(ToolCallResultType::WaitForEvents(..)) | None => { log::warn!("No result present for tool call ID: {tool_call_id}"); None } + Some(ToolCallResultType::WaitForEvents(_)) => None, } } @@ -1764,7 +1764,6 @@ fn create_cancelled_result_for_tool_call( return None; } ToolType::Subagent(_) => return None, - ToolType::WaitForEvents(_) => return None, ToolType::StartAgent(_) => { AIAgentActionResultType::StartAgent(StartAgentResult::Cancelled { version: StartAgentVersion::V1, @@ -1786,6 +1785,9 @@ fn create_cancelled_result_for_tool_call( } // These tools are deprecated. ToolType::SuggestCreatePlan(_) | ToolType::SuggestPlan(_) => return None, + ToolType::WaitForEvents(_) => { + return None; + } }; Some(AIAgentInput::ActionResult { diff --git a/app/src/ai/agent/conversation.rs b/app/src/ai/agent/conversation.rs index a2d8d91cc6..e13b226735 100644 --- a/app/src/ai/agent/conversation.rs +++ b/app/src/ai/agent/conversation.rs @@ -1186,17 +1186,18 @@ impl AIConversation { /// Returns the memories the server fetched for this conversation, in the order they first /// appeared across messages (server-side rank order within each message). Re-fetched - /// memories are deduped by `memory_id`: the first appearance keeps its position while the - /// entry's content/store/source are updated to the latest occurrence. + /// memories are deduped by `(memory_store_id, memory_id)`: the first appearance keeps its + /// position while the entry's content/source are updated to the latest occurrence. pub fn fetched_memories(&self) -> Vec { let mut memories: Vec = Vec::new(); - let mut index_by_id: HashMap = HashMap::new(); + let mut index_by_id: HashMap<(String, String), usize> = HashMap::new(); for message in self.task_store.all_linearized_messages() { for memory in &message.fetched_memories { - match index_by_id.get(&memory.memory_id) { + let key = (memory.memory_store_id.clone(), memory.memory_id.clone()); + match index_by_id.get(&key) { Some(index) => memories[*index] = memory.clone(), None => { - index_by_id.insert(memory.memory_id.clone(), memories.len()); + index_by_id.insert(key, memories.len()); memories.push(memory.clone()); } } diff --git a/app/src/ai/agent/conversation_tests.rs b/app/src/ai/agent/conversation_tests.rs index ff06a1d2f5..3da7e7a4d1 100644 --- a/app/src/ai/agent/conversation_tests.rs +++ b/app/src/ai/agent/conversation_tests.rs @@ -826,15 +826,18 @@ fn fetched_memories_preserves_order_across_and_within_messages() { fn fetched_memories_dedupes_keeping_first_position_and_latest_data() { let conversation = restored_conversation_with_memories_per_query(vec![ vec![ - fetched_memory("m1", "old content", "store-old", None), + fetched_memory("m1", "old content", "store-1", None), fetched_memory("m2", "other", "store-1", None), ], - vec![fetched_memory( - "m1", - "new content", - "store-new", - conversation_source("conversation-1"), - )], + vec![ + fetched_memory( + "m1", + "new content", + "store-1", + conversation_source("conversation-1"), + ), + fetched_memory("m1", "same memory id different store", "store-2", None), + ], ]); let memories = conversation.fetched_memories(); @@ -844,10 +847,11 @@ fn fetched_memories_dedupes_keeping_first_position_and_latest_data() { fetched_memory( "m1", "new content", - "store-new", + "store-1", conversation_source("conversation-1"), ), fetched_memory("m2", "other", "store-1", None), + fetched_memory("m1", "same memory id different store", "store-2", None), ] ); } diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs index 11215962aa..601769ffda 100644 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/toolbar_item.rs @@ -211,8 +211,8 @@ impl AgentToolbarItemKind { let mut items = vec![ Self::ContextChip(ContextChipKind::AgentPlanAndTodoList), Self::ContextWindowUsage, + Self::ModelSelector, ]; - items.push(Self::ModelSelector); if FeatureFlag::CreatingSharedSessions.is_enabled() && FeatureFlag::HOARemoteControl.is_enabled() { diff --git a/app/src/server/telemetry/events.rs b/app/src/server/telemetry/events.rs index ce644171ec..318e31b549 100644 --- a/app/src/server/telemetry/events.rs +++ b/app/src/server/telemetry/events.rs @@ -990,6 +990,8 @@ pub enum AgentModeCitation { #[serde(skip_serializing)] url: String, }, + /// A fetched memory surfaced as a citation so we can track whether memory-backed + /// responses are shown to users and whether users open those memory citations. AgentMemory { memory_store_id: String, memory_id: String, diff --git a/crates/ai/src/agent/citation.rs b/crates/ai/src/agent/citation.rs index e29cdcbe00..32fd5a75ca 100644 --- a/crates/ai/src/agent/citation.rs +++ b/crates/ai/src/agent/citation.rs @@ -1,10 +1,9 @@ use std::fmt::Display; -use std::hash::{Hash, Hasher}; use warp_multi_agent_api as api; /// A citation listed in an AI response. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash, Eq, PartialEq)] pub enum AIAgentCitation { WarpDriveObject { uid: String, @@ -16,7 +15,7 @@ pub enum AIAgentCitation { url: String, }, /// A memory from an attached memory store. `content` is the raw memory - /// text shown as a preview in the chip; `Hash`/`Eq` use only the IDs. + /// text shown as a preview in the chip. AgentMemory { memory_store_id: String, memory_id: String, @@ -24,59 +23,6 @@ pub enum AIAgentCitation { }, } -impl PartialEq for AIAgentCitation { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::WarpDriveObject { uid: a }, Self::WarpDriveObject { uid: b }) => a == b, - (Self::WarpDocumentation { path: a }, Self::WarpDocumentation { path: b }) => a == b, - (Self::WebPage { url: a }, Self::WebPage { url: b }) => a == b, - ( - Self::AgentMemory { - memory_store_id: s1, - memory_id: i1, - .. - }, - Self::AgentMemory { - memory_store_id: s2, - memory_id: i2, - .. - }, - ) => s1 == s2 && i1 == i2, - _ => false, - } - } -} - -impl Eq for AIAgentCitation {} - -impl Hash for AIAgentCitation { - fn hash(&self, state: &mut H) { - match self { - Self::WarpDriveObject { uid } => { - 0u8.hash(state); - uid.hash(state); - } - Self::WarpDocumentation { path } => { - 1u8.hash(state); - path.hash(state); - } - Self::WebPage { url } => { - 2u8.hash(state); - url.hash(state); - } - Self::AgentMemory { - memory_store_id, - memory_id, - .. - } => { - 3u8.hash(state); - memory_store_id.hash(state); - memory_id.hash(state); - } - } - } -} - impl Display for AIAgentCitation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self {