feat: associate threads with projects (#8745)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Toohey
2026-04-22 19:08:55 +12:00
committed by GitHub
parent a7ccdd780d
commit c8b339e559
14 changed files with 160 additions and 30 deletions
+9
View File
@@ -181,6 +181,15 @@ pub struct RemoveSecretRequest {
pub key: String,
}
/// Update the project association for a session.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/session/update_project", response = EmptyResponse)]
#[serde(rename_all = "camelCase")]
pub struct UpdateSessionProjectRequest {
pub session_id: String,
pub project_id: Option<String>,
}
/// Archive a session (soft delete).
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/session/archive", response = EmptyResponse)]
+5
View File
@@ -90,6 +90,11 @@
"requestType": "ImportSessionRequest",
"responseType": "ImportSessionResponse"
},
{
"method": "_goose/session/update_project",
"requestType": "UpdateSessionProjectRequest",
"responseType": "EmptyResponse"
},
{
"method": "_goose/session/archive",
"requestType": "ArchiveSessionRequest",
+29
View File
@@ -669,6 +669,26 @@
"x-side": "agent",
"x-method": "_goose/session/import"
},
"UpdateSessionProjectRequest": {
"type": "object",
"properties": {
"sessionId": {
"type": "string"
},
"projectId": {
"type": [
"string",
"null"
]
}
},
"required": [
"sessionId"
],
"description": "Update the project association for a session.",
"x-side": "agent",
"x-method": "_goose/session/update_project"
},
"ArchiveSessionRequest": {
"type": "object",
"properties": {
@@ -1487,6 +1507,15 @@
"description": "Params for _goose/session/import",
"title": "ImportSessionRequest"
},
{
"allOf": [
{
"$ref": "#/$defs/UpdateSessionProjectRequest"
}
],
"description": "Params for _goose/session/update_project",
"title": "UpdateSessionProjectRequest"
},
{
"allOf": [
{
+42 -11
View File
@@ -153,6 +153,24 @@ fn sid_short(id: &str) -> String {
id.chars().take(8).collect()
}
fn thread_session_meta(
message_count: i64,
metadata: &crate::session::ThreadMetadata,
) -> 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()),
);
if let Some(ref pid) = metadata.project_id {
meta.insert(
"projectId".to_string(),
serde_json::Value::String(pid.clone()),
);
}
meta
}
fn extract_timeout_from_meta(meta: &Option<Meta>) -> Option<u64> {
meta.as_ref()
.and_then(|m| m.get("timeout"))
@@ -1539,9 +1557,17 @@ impl GooseAcpAgent {
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let project_id = args
.meta
.as_ref()
.and_then(|m| m.get("projectId"))
.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,
mode: Some(self.goose_mode.to_string()),
..Default::default()
};
@@ -2544,11 +2570,7 @@ impl GooseAcpAgent {
.as_deref()
.map(std::path::PathBuf::from)
.unwrap_or_default();
let mut meta = serde_json::Map::new();
meta.insert(
"messageCount".to_string(),
serde_json::Value::Number(t.message_count.into()),
);
let meta = thread_session_meta(t.message_count, &t.metadata);
SessionInfo::new(SessionId::new(t.id), cwd)
.title(t.name)
.updated_at(t.updated_at.to_rfc3339())
@@ -2613,11 +2635,7 @@ impl GooseAcpAgent {
},
);
let mut meta = serde_json::Map::new();
meta.insert(
"messageCount".to_string(),
serde_json::Value::Number(new_thread.message_count.into()),
);
let meta = thread_session_meta(new_thread.message_count, &new_thread.metadata);
let mut response = ForkSessionResponse::new(SessionId::new(new_thread_id))
.modes(mode_state)
@@ -3047,6 +3065,19 @@ impl GooseAcpAgent {
})
}
#[custom_method(UpdateSessionProjectRequest)]
async fn on_update_session_project(
&self,
req: UpdateSessionProjectRequest,
) -> Result<EmptyResponse, sacp::Error> {
let project_id = req.project_id;
self.update_thread_metadata(&req.session_id, move |meta| {
meta.project_id = project_id;
})
.await?;
Ok(EmptyResponse {})
}
#[custom_method(ArchiveSessionRequest)]
async fn on_archive_session(
&self,
@@ -3189,7 +3220,7 @@ impl GooseAcpAgent {
other => {
return Err(
sacp::Error::invalid_params().data(format!("Unsupported format: {other}"))
)
);
}
};