Skip to content
Merged
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
9 changes: 9 additions & 0 deletions crates/goose-sdk/src/custom_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,15 @@ pub struct UpdateSessionProjectRequest {
pub project_id: Option<String>,
}

/// Rename a session.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/session/rename", response = EmptyResponse)]
#[serde(rename_all = "camelCase")]
pub struct RenameSessionRequest {
pub session_id: String,
pub title: String,
}

/// Archive a session (soft delete).
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/session/archive", response = EmptyResponse)]
Expand Down
5 changes: 5 additions & 0 deletions crates/goose/acp-meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@
"requestType": "UpdateSessionProjectRequest",
"responseType": "EmptyResponse"
},
{
"method": "_goose/session/rename",
"requestType": "RenameSessionRequest",
"responseType": "EmptyResponse"
},
{
"method": "_goose/session/archive",
"requestType": "ArchiveSessionRequest",
Expand Down
27 changes: 27 additions & 0 deletions crates/goose/acp-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,24 @@
"x-side": "agent",
"x-method": "_goose/session/update_project"
},
"RenameSessionRequest": {
"type": "object",
"properties": {
"sessionId": {
"type": "string"
},
"title": {
"type": "string"
}
},
"required": [
"sessionId",
"title"
],
"description": "Rename a session.",
"x-side": "agent",
"x-method": "_goose/session/rename"
},
"ArchiveSessionRequest": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -1582,6 +1600,15 @@
"description": "Params for _goose/session/update_project",
"title": "UpdateSessionProjectRequest"
},
{
"allOf": [
{
"$ref": "#/$defs/RenameSessionRequest"
}
],
"description": "Params for _goose/session/rename",
"title": "RenameSessionRequest"
},
{
"allOf": [
{
Expand Down
81 changes: 74 additions & 7 deletions crates/goose/src/acp/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,20 +171,51 @@ fn sid_short(id: &str) -> String {
}

fn thread_session_meta(
message_count: i64,
metadata: &crate::session::ThreadMetadata,
thread: &crate::session::Thread,
) -> serde_json::Map<String, serde_json::Value> {
let mut meta = serde_json::Map::new();
meta.insert(
"messageCount".to_string(),
serde_json::Value::Number(message_count.into()),
serde_json::Value::Number(thread.message_count.into()),
);
if let Some(ref pid) = metadata.project_id {
meta.insert(
"createdAt".to_string(),
serde_json::Value::String(thread.created_at.to_rfc3339()),
);
if let Some(ref archived_at) = thread.archived_at {
meta.insert(
"archivedAt".to_string(),
serde_json::Value::String(archived_at.to_rfc3339()),
);
}
meta.insert(
"userSetName".to_string(),
serde_json::Value::Bool(thread.user_set_name),
);
if let Some(ref pid) = thread.metadata.project_id {
meta.insert(
"projectId".to_string(),
serde_json::Value::String(pid.clone()),
);
}
if let Some(ref provider_id) = thread.metadata.provider_id {
meta.insert(
"providerId".to_string(),
serde_json::Value::String(provider_id.clone()),
);
}
if let Some(ref model_id) = thread.metadata.model_id {
meta.insert(
"modelId".to_string(),
serde_json::Value::String(model_id.clone()),
);
}
if let Some(ref persona_id) = thread.metadata.persona_id {
meta.insert(
"personaId".to_string(),
serde_json::Value::String(persona_id.clone()),
);
}
meta
}

Expand Down Expand Up @@ -1641,10 +1672,18 @@ impl GooseAcpAgent {
.and_then(|v| v.as_str())
.map(|s| s.to_string());

let persona_id = args
.meta
.as_ref()
.and_then(|m| m.get("personaId"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());

// Create the Thread — this IS the ACP session from the client's perspective.
let thread_metadata = crate::session::ThreadMetadata {
provider_id: requested_provider.clone(),
project_id,
persona_id,
mode: Some(self.goose_mode.to_string()),
..Default::default()
};
Expand Down Expand Up @@ -2188,6 +2227,21 @@ impl GooseAcpAgent {
let t_start = std::time::Instant::now();
debug!(target: "perf", sid = %sid, "perf: prompt start");

// Update persona_id on the thread if the client sent one in _meta.
let prompt_persona_id = args
.meta
.as_ref()
.and_then(|m| m.get("personaId"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let Some(ref pid) = prompt_persona_id {
let pid = pid.clone();
self.update_thread_metadata(&thread_id, move |meta| {
meta.persona_id = Some(pid);
})
.await?;
}

let cancel_token = CancellationToken::new();
let internal_session_id = self.internal_session_id(&thread_id).await?;

Expand Down Expand Up @@ -2413,7 +2467,7 @@ impl GooseAcpAgent {
let t_step = std::time::Instant::now();
let model_id_owned = model_id.to_string();
self.update_thread_metadata(thread_id, move |meta| {
meta.model_name = Some(model_id_owned);
meta.model_id = Some(model_id_owned);
})
.await?;
debug!(target: "perf", sid = %sid, ms = t_step.elapsed().as_millis() as u64, "perf: set_model update_thread_metadata");
Expand Down Expand Up @@ -2635,6 +2689,7 @@ impl GooseAcpAgent {
let provider_name_owned = provider_name.to_string();
self.update_thread_metadata(thread_id, move |meta| {
meta.provider_id = Some(provider_name_owned);
meta.model_id = None;
})
.await?;
debug!(target: "perf", sid = %sid, ms = t_step.elapsed().as_millis() as u64, "perf: update_provider update_thread_metadata");
Expand Down Expand Up @@ -2701,7 +2756,7 @@ impl GooseAcpAgent {
.as_deref()
.map(std::path::PathBuf::from)
.unwrap_or_default();
let meta = thread_session_meta(t.message_count, &t.metadata);
let meta = thread_session_meta(&t);
SessionInfo::new(SessionId::new(t.id), cwd)
.title(t.name)
.updated_at(t.updated_at.to_rfc3339())
Expand Down Expand Up @@ -2765,7 +2820,7 @@ impl GooseAcpAgent {
},
);

let meta = thread_session_meta(new_thread.message_count, &new_thread.metadata);
let meta = thread_session_meta(&new_thread);

let mut response = ForkSessionResponse::new(SessionId::new(new_thread_id))
.modes(mode_state)
Expand Down Expand Up @@ -3275,6 +3330,18 @@ impl GooseAcpAgent {
Ok(EmptyResponse {})
}

#[custom_method(RenameSessionRequest)]
async fn on_rename_session(
&self,
req: RenameSessionRequest,
) -> Result<EmptyResponse, sacp::Error> {
self.thread_manager
.update_thread(&req.session_id, Some(req.title), Some(true), None)
.await
Comment on lines +3338 to +3340
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 Synchronize session row when renaming a thread

The rename RPC updates only the thread record, but other backend flows still read the internal session record (for example on_export_session goes through session_manager.export_session). After a rename, list sessions can show the new title while exported/imported session payloads still carry the old one, creating backend state drift introduced by this endpoint. Update the linked internal session name/user-set flag alongside thread_manager.update_thread so both stores stay consistent.

Useful? React with 👍 / 👎.

.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
Ok(EmptyResponse {})
}

#[custom_method(ArchiveSessionRequest)]
async fn on_archive_session(
&self,
Expand Down
4 changes: 2 additions & 2 deletions crates/goose/src/session/thread_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ pub struct ThreadMetadata {
pub project_id: Option<String>,
#[serde(default)]
pub provider_id: Option<String>,
#[serde(default)]
pub model_name: Option<String>,
#[serde(default, alias = "model_name")]
pub model_id: Option<String>,
#[serde(default)]
pub mode: Option<String>,
#[serde(flatten)]
Expand Down
6 changes: 6 additions & 0 deletions crates/goose/tests/acp_common_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,18 @@ pub async fn run_list_sessions<C: Connection>() {
let mut response = conn.list_sessions().await.unwrap();
for s in &mut response.sessions {
s.updated_at = None;
// createdAt is a dynamic timestamp — verify it exists then remove for comparison.
if let Some(ref mut meta) = s.meta {
assert!(meta.get("createdAt").and_then(|v| v.as_str()).is_some());
meta.remove("createdAt");
}
}
let mut expected_meta = serde_json::Map::new();
expected_meta.insert(
"messageCount".to_string(),
serde_json::Value::Number(2.into()),
);
expected_meta.insert("userSetName".to_string(), serde_json::Value::Bool(false));
assert_eq!(
response,
ListSessionsResponse::new(vec![SessionInfo::new(
Expand Down
3 changes: 0 additions & 3 deletions ui/goose2/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,6 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
perfLog(
`[perf:newtab] createNewTab start (project=${project?.id ?? "none"})`,
);
const agentId = agentStore.activeAgentId ?? undefined;
const providerId =
project?.preferredProvider ?? agentStore.selectedProvider ?? "goose";
const sessionModelPreference =
Expand Down Expand Up @@ -312,7 +311,6 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
const session = await sessionStore.createSession({
title,
projectId: project?.id,
agentId,
providerId: sessionModelPreference.providerId,
workingDir,
modelId: sessionModelPreference.modelId,
Expand All @@ -327,7 +325,6 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
return session;
},
[
agentStore.activeAgentId,
agentStore.selectedProvider,
chatStore,
providerInventoryEntries,
Expand Down
Loading
Loading