Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 45 additions & 11 deletions crates/goose/src/acp/server/new_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ struct InitialSessionConfig {
extension_data: ExtensionData,
recipe: Option<Recipe>,
user_recipe_values: Option<HashMap<String, String>>,
meta: NewSessionMetaFields,
}

/// Session fields read from `_meta` on `session/new` that are applied to the
/// session row after it is created.
struct NewSessionMetaFields {
project_id: Option<String>,
/// Client-supplied title, recorded as user-set so goose's own name
/// generation leaves it alone. `None` when a recipe title took precedence.
client_title: Option<String>,
}

impl GooseAcpAgent {
Expand All @@ -30,25 +39,25 @@ impl GooseAcpAgent {
) -> Result<NewSessionResponse, agent_client_protocol::Error> {
validate_absolute_cwd(&args.cwd)?;
let config = Config::global();
let project_id = meta_string(args.meta.as_ref(), "projectId")?;
let session_type = match meta_string(args.meta.as_ref(), "client")? {
Some(_) => SessionType::User,
None => SessionType::Acp,
};
let current_mode: GooseMode = config.get_goose_mode().unwrap_or_default();
let recipe = self.resolve_recipe_from_meta(args.meta.as_ref()).await?;
let session_name = match recipe.as_ref() {
Some((recipe, _)) if !recipe.title.trim().is_empty() => recipe.title.clone(),
_ => "New Chat".to_string(),
};
let meta = new_session_meta_fields(args.meta.as_ref(), recipe.as_ref())?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the new title path to the self-test recipe

This introduces new ACP session-creation behavior, but the commit does not update goose-self-test.yaml; a repo-wide search for sessionTitle finds coverage only in the Rust handler and tests. Add an appropriate self-test scenario so the feature receives the repository-required end-to-end validation.

AGENTS.md reference: AGENTS.md:L70-L71

Useful? React with 👍 / 👎.

let session_name = recipe_title(recipe.as_ref())
.map(str::to_string)
.or_else(|| meta.client_title.clone())
.unwrap_or_else(|| "New Chat".to_string());

let session = self
.session_manager
.create_session(args.cwd.clone(), session_name, session_type, current_mode)
.await
.internal_err_ctx("Failed to create session")?;
match self
.finish_new_session_setup(cx, config, &session, args, recipe, project_id)
.finish_new_session_setup(cx, config, &session, args, recipe, meta)
.await
{
Ok(response) => Ok(response),
Expand All @@ -66,10 +75,10 @@ impl GooseAcpAgent {
session: &Session,
args: NewSessionRequest,
recipe: Option<(Recipe, PathBuf)>,
project_id: Option<String>,
meta: NewSessionMetaFields,
) -> Result<NewSessionResponse, agent_client_protocol::Error> {
let rendered_recipe = self
.configure_new_session(cx, config, session, args, recipe, project_id)
.configure_new_session(cx, config, session, args, recipe, meta)
.await?;

let reloaded_session = self.reload_session(&session.id).await?;
Expand Down Expand Up @@ -115,7 +124,7 @@ impl GooseAcpAgent {
session: &Session,
args: NewSessionRequest,
recipe: Option<(Recipe, PathBuf)>,
project_id: Option<String>,
meta: NewSessionMetaFields,
) -> Result<Option<Recipe>, agent_client_protocol::Error> {
let (rendered, user_recipe_values) = self
.render_recipe_for_session(cx, &session.id, recipe.as_ref())
Expand Down Expand Up @@ -144,7 +153,7 @@ impl GooseAcpAgent {
extension_data,
recipe: recipe.map(|(recipe, _)| recipe),
user_recipe_values,
project_id,
meta,
},
)
.await?;
Expand Down Expand Up @@ -215,9 +224,12 @@ impl GooseAcpAgent {
if config.user_recipe_values.is_some() {
builder = builder.user_recipe_values(config.user_recipe_values);
}
if let Some(project_id) = config.project_id {
if let Some(project_id) = config.meta.project_id {
builder = builder.project_id(Some(project_id));
}
if let Some(client_title) = config.meta.client_title {
builder = builder.user_provided_name(client_title);
}
builder
.apply()
.await
Expand Down Expand Up @@ -251,6 +263,28 @@ fn model_config_from_recipe_settings(
.internal_err_ctx("Failed to build model config from recipe settings")
}

fn recipe_title(recipe: Option<&(Recipe, PathBuf)>) -> Option<&str> {
recipe
.map(|(recipe, _)| recipe.title.trim())
.filter(|title| !title.is_empty())
}

fn new_session_meta_fields(
meta: Option<&Meta>,
recipe: Option<&(Recipe, PathBuf)>,
) -> Result<NewSessionMetaFields, agent_client_protocol::Error> {
let session_title = meta_string(meta, "sessionTitle")?
.map(|title| title.trim().to_string())
.filter(|title| !title.is_empty());
Ok(NewSessionMetaFields {
project_id: meta_string(meta, "projectId")?,
// A recipe title is a server-side declaration, so it keeps the
// precedence it has today and a client title only replaces the
// "New Chat" fallback.
client_title: session_title.filter(|_| recipe_title(recipe).is_none()),
})
}

fn meta_goose_extensions(
meta: Option<&Meta>,
) -> Result<Option<Vec<GooseExtension>>, agent_client_protocol::Error> {
Expand Down
8 changes: 5 additions & 3 deletions crates/goose/tests/acp_common_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ use std::sync::Arc;
use std::time::Duration;

const SHELL_TEST_CONTENT: &str = "test-shell-content-98765";
const TURN_CONTEXT_CLOSE: &str = r#"</turn-context>\n"#;
const OPENAI_SESSION_NAME_RESPONSE: &str = r#"data: {"id":"chatcmpl-test","object":"chat.completion.chunk","created":1766229303,"model":"gpt-5-nano","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
pub const TURN_CONTEXT_CLOSE: &str = r#"</turn-context>\n"#;
/// Session name produced by `OPENAI_SESSION_NAME_RESPONSE`.
pub const GENERATED_SESSION_TITLE: &str = "Generated Test Title";
pub const OPENAI_SESSION_NAME_RESPONSE: &str = r#"data: {"id":"chatcmpl-test","object":"chat.completion.chunk","created":1766229303,"model":"gpt-5-nano","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-test","object":"chat.completion.chunk","created":1766229303,"model":"gpt-5-nano","choices":[{"index":0,"delta":{"content":"Generated Test Title"},"finish_reason":null}]}

Expand Down Expand Up @@ -157,7 +159,7 @@ pub async fn run_session_name_update_notification<C: Connection>() {
_ => None,
})
.expect("expected generated session name notification");
assert_eq!(update.0.as_deref(), Some("Generated Test Title"));
assert_eq!(update.0.as_deref(), Some(GENERATED_SESSION_TITLE));
assert!(update.1.is_some());
assert!(update.2.unwrap_or_default() >= 1);
assert_eq!(*update.3, Some(false));
Expand Down
Loading
Loading