use crate::{AgentTool, Thread, ToolCallEventStream, ToolInput}; use agent_client_protocol::schema as acp; use gpui::{App, SharedString, Task, WeakEntity}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; const MAX_TITLE_LEN: usize = 200; /// Updates the current session title. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct UpdateTitleToolInput { /// A concise, human-readable title for the current session. pub title: String, } pub struct UpdateTitleTool { thread: WeakEntity, } impl UpdateTitleTool { pub fn new(thread: WeakEntity) -> Self { Self { thread } } pub(crate) fn title_for_input( input: Result, ) -> SharedString { let Ok(input) = input else { return "Update title".into(); }; let Ok(title) = normalize_title(&input.title) else { return "Update title".into(); }; format!("Update title: {title}").into() } } impl AgentTool for UpdateTitleTool { type Input = UpdateTitleToolInput; type Output = String; const NAME: &'static str = "update_title"; fn kind() -> acp::ToolKind { acp::ToolKind::Think } fn initial_title( &self, input: Result, _cx: &mut App, ) -> SharedString { Self::title_for_input(input) } fn run( self: Arc, input: ToolInput, _event_stream: ToolCallEventStream, cx: &mut App, ) -> Task> { let thread = self.thread.clone(); cx.spawn(async move |cx| { let input = input.recv().await.map_err(|error| error.to_string())?; let title = normalize_title(&input.title)?; thread .update(cx, |thread, cx| { thread.set_title(title.into(), cx); }) .map_err(|error| error.to_string())?; Ok("Session title updated".to_string()) }) } fn replay( &self, input: Self::Input, _output: Self::Output, event_stream: ToolCallEventStream, cx: &mut App, ) -> anyhow::Result<()> { let title = self.initial_title(Ok(input), cx).to_string(); event_stream.update_fields(acp::ToolCallUpdateFields::new().title(title)); Ok(()) } } fn normalize_title(title: &str) -> Result { let title = title.lines().next().unwrap_or("").trim(); if title.is_empty() { return Err("Title cannot be empty".to_string()); } Ok(util::truncate_and_trailoff(title, MAX_TITLE_LEN)) } #[cfg(test)] mod tests { use super::*; use gpui::TestAppContext; #[test] fn test_normalize_title() { assert_eq!( normalize_title(" Title from model\nignored").unwrap(), "Title from model" ); assert!(normalize_title(" \nignored").is_err()); } #[gpui::test] async fn test_initial_title(cx: &mut TestAppContext) { let tool = UpdateTitleTool::new(WeakEntity::new_invalid()); let title = cx.update(|cx| { tool.initial_title( Ok(UpdateTitleToolInput { title: "Investigate title updates".to_string(), }), cx, ) }); assert_eq!( title, SharedString::from("Update title: Investigate title updates") ); let title = cx.update(|cx| { tool.initial_title( Ok(UpdateTitleToolInput { title: " ".to_string(), }), cx, ) }); assert_eq!(title, SharedString::from("Update title")); } }