|
5 | 5 | //! - GET /teams/:name — Get team detail (config + members) |
6 | 6 | //! - GET /teams/:name/inbox — Get team inbox messages |
7 | 7 | //! - GET /teams/:name/cost — Get team cost breakdown (resolves member sessions) |
| 8 | +//! - GET /teams/:name/sidechains?session_id=xxx — Get team member sidechains |
8 | 9 |
|
9 | | -use axum::extract::{Path, State}; |
| 10 | +use axum::extract::{Path, Query, State}; |
10 | 11 | use axum::routing::get; |
11 | 12 | use axum::{Json, Router}; |
| 13 | +use serde::Deserialize; |
12 | 14 | use std::sync::Arc; |
13 | 15 |
|
14 | 16 | use crate::error::{ApiError, ApiResult}; |
15 | 17 | use crate::routes::sessions::resolve_session_file_path; |
16 | 18 | use crate::state::AppState; |
17 | | -use crate::teams::{InboxMessage, TeamCostBreakdown, TeamDetail, TeamSummary}; |
| 19 | +use crate::teams::{InboxMessage, TeamCostBreakdown, TeamDetail, TeamMemberSidechain, TeamSummary}; |
18 | 20 |
|
19 | 21 | /// GET /api/teams — List all teams. |
20 | 22 | #[utoipa::path(get, path = "/api/teams", tag = "teams", |
@@ -132,10 +134,57 @@ pub async fn get_team_cost( |
132 | 134 | Ok(Json(cost)) |
133 | 135 | } |
134 | 136 |
|
| 137 | +#[derive(Debug, Deserialize)] |
| 138 | +pub struct TeamSidechainsQuery { |
| 139 | + pub session_id: String, |
| 140 | +} |
| 141 | + |
| 142 | +/// GET /api/teams/:name/sidechains?session_id=xxx — Get team member sidechains. |
| 143 | +/// |
| 144 | +/// Resolves sidechain `.meta.json` / `.jsonl` pairs inside the session directory |
| 145 | +/// to enumerate each member's spawned sub-conversations. |
| 146 | +#[utoipa::path(get, path = "/api/teams/{name}/sidechains", tag = "teams", |
| 147 | + params( |
| 148 | + ("name" = String, Path, description = "Team name"), |
| 149 | + ("session_id" = String, Query, description = "Lead session ID"), |
| 150 | + ), |
| 151 | + responses( |
| 152 | + (status = 200, description = "Team member sidechains", body = serde_json::Value), |
| 153 | + (status = 404, description = "Team not found"), |
| 154 | + ) |
| 155 | +)] |
| 156 | +pub async fn get_team_sidechains( |
| 157 | + State(state): State<Arc<AppState>>, |
| 158 | + Path(name): Path<String>, |
| 159 | + Query(query): Query<TeamSidechainsQuery>, |
| 160 | +) -> ApiResult<Json<Vec<TeamMemberSidechain>>> { |
| 161 | + // Verify team exists |
| 162 | + let _team = state |
| 163 | + .teams |
| 164 | + .get(&name) |
| 165 | + .ok_or_else(|| ApiError::NotFound(format!("Team '{}' not found", name)))?; |
| 166 | + |
| 167 | + // Resolve session JSONL path, then get its parent directory |
| 168 | + let session_path = resolve_session_file_path(&state, &query.session_id).await?; |
| 169 | + let session_dir = session_path |
| 170 | + .parent() |
| 171 | + .ok_or_else(|| ApiError::Internal("Session path has no parent directory".into()))? |
| 172 | + .to_path_buf(); |
| 173 | + |
| 174 | + // Heavy I/O: read meta.json + count JSONL lines on blocking thread |
| 175 | + let sidechains = |
| 176 | + tokio::task::spawn_blocking(move || crate::teams::resolve_team_sidechains(&session_dir)) |
| 177 | + .await |
| 178 | + .map_err(|e| ApiError::Internal(format!("Join error: {e}")))?; |
| 179 | + |
| 180 | + Ok(Json(sidechains)) |
| 181 | +} |
| 182 | + |
135 | 183 | pub fn router() -> Router<Arc<AppState>> { |
136 | 184 | Router::new() |
137 | 185 | .route("/teams", get(list_teams)) |
138 | 186 | .route("/teams/{name}", get(get_team)) |
139 | 187 | .route("/teams/{name}/inbox", get(get_team_inbox)) |
140 | 188 | .route("/teams/{name}/cost", get(get_team_cost)) |
| 189 | + .route("/teams/{name}/sidechains", get(get_team_sidechains)) |
141 | 190 | } |
0 commit comments