opendev_web/routes/sessions/
mod.rs1mod filesystem;
4mod models;
5
6use axum::extract::{Path as AxumPath, State};
7use axum::routing::{get, post};
8use axum::{Json, Router};
9use serde::Deserialize;
10
11use crate::error::WebError;
12use crate::state::AppState;
13
14pub use filesystem::{BrowseDirectoryRequest, ListFilesQuery, VerifyPathRequest};
15pub use models::SessionModelUpdate;
16
17#[derive(Debug, Deserialize)]
19pub struct CreateSessionRequest {
20 #[serde(default)]
21 pub working_directory: Option<String>,
22}
23
24pub fn router() -> Router<AppState> {
26 Router::new()
27 .route("/api/sessions", get(list_sessions).post(create_session))
28 .route("/api/sessions/bridge-info", get(get_bridge_info))
29 .route("/api/sessions/files", get(filesystem::list_files))
30 .route("/api/sessions/verify-path", post(filesystem::verify_path))
31 .route(
32 "/api/sessions/browse-directory",
33 post(filesystem::browse_directory),
34 )
35 .route(
36 "/api/sessions/{id}",
37 get(get_session).delete(delete_session),
38 )
39 .route("/api/sessions/{id}/resume", post(resume_session))
40 .route("/api/sessions/{id}/messages", get(get_session_messages))
41 .route(
42 "/api/sessions/{id}/model",
43 get(models::get_session_model)
44 .put(models::update_session_model)
45 .delete(models::clear_session_model),
46 )
47}
48
49async fn list_sessions(State(state): State<AppState>) -> Result<Json<serde_json::Value>, WebError> {
51 let mgr = state.session_manager().await;
52 let index = mgr.index().read_index();
53
54 let sessions: Vec<serde_json::Value> = match index {
55 Some(idx) => idx
56 .entries
57 .iter()
58 .map(|entry| {
59 serde_json::json!({
60 "id": entry.session_id,
61 "created_at": entry.created,
62 "updated_at": entry.modified,
63 "message_count": entry.message_count,
64 "title": entry.title,
65 "working_directory": entry.working_directory,
66 })
67 })
68 .collect(),
69 None => Vec::new(),
70 };
71
72 Ok(Json(serde_json::json!(sessions)))
73}
74
75async fn create_session(
81 State(state): State<AppState>,
82 Json(payload): Json<CreateSessionRequest>,
83) -> Result<Json<serde_json::Value>, WebError> {
84 let requested_wd = payload.working_directory.clone();
85
86 let mut mgr = state.session_manager_mut().await;
87
88 if let Some(ref wd) = requested_wd
90 && let Some(index) = mgr.index().read_index()
91 {
92 let empty_match = index.entries.iter().find(|entry| {
93 entry.message_count == 0
94 && entry
95 .working_directory
96 .as_deref()
97 .map(|d| d == wd.as_str())
98 .unwrap_or(false)
99 });
100
101 if let Some(entry) = empty_match {
102 let candidate_id = entry.session_id.clone();
103
104 let is_stale = mgr
107 .current_session()
108 .map(|s| s.id == candidate_id && !s.messages.is_empty())
109 .unwrap_or(false);
110
111 if !is_stale {
112 if mgr.resume_session(&candidate_id).is_ok() {
114 return Ok(Json(serde_json::json!({
115 "id": candidate_id,
116 "status": "reused",
117 "message": "Reusing existing empty session",
118 })));
119 }
120 }
122 }
123 }
124
125 let session = mgr.create_session();
127 let session_id = session.id.clone();
128
129 if let Some(wd) = requested_wd
131 && let Some(session) = mgr.current_session_mut()
132 {
133 session.working_directory = Some(wd);
134 }
135
136 mgr.save_current()
138 .map_err(|e| WebError::Internal(format!("Failed to save session: {}", e)))?;
139
140 Ok(Json(serde_json::json!({
141 "id": session_id,
142 "status": "created",
143 })))
144}
145
146async fn get_session(
148 State(state): State<AppState>,
149 AxumPath(id): AxumPath<String>,
150) -> Result<Json<serde_json::Value>, WebError> {
151 let mgr = state.session_manager().await;
152 let session = mgr
153 .load_session(&id)
154 .map_err(|e| WebError::NotFound(format!("Session {} not found: {}", id, e)))?;
155
156 Ok(Json(serde_json::to_value(session.get_metadata()).map_err(
157 |e| WebError::Internal(format!("Failed to serialize session: {}", e)),
158 )?))
159}
160
161async fn delete_session(
163 State(state): State<AppState>,
164 AxumPath(id): AxumPath<String>,
165) -> Result<Json<serde_json::Value>, WebError> {
166 let mut mgr = state.session_manager_mut().await;
167
168 mgr.load_session(&id)
170 .map_err(|e| WebError::NotFound(format!("Session {} not found: {}", id, e)))?;
171
172 let session_dir = mgr.session_dir().to_path_buf();
174 let json_path = session_dir.join(format!("{}.json", id));
175 let jsonl_path = session_dir.join(format!("{}.jsonl", id));
176 let debug_path = session_dir.join(format!("{}.debug", id));
177
178 if json_path.exists() {
179 std::fs::remove_file(&json_path)
180 .map_err(|e| WebError::Internal(format!("Failed to delete session file: {}", e)))?;
181 }
182 if jsonl_path.exists() {
183 std::fs::remove_file(&jsonl_path).map_err(|e| {
184 WebError::Internal(format!("Failed to delete session transcript: {}", e))
185 })?;
186 }
187 if debug_path.exists() {
188 let _ = std::fs::remove_file(&debug_path);
189 }
190
191 mgr.index()
193 .remove_entry(&id)
194 .map_err(|e| WebError::Internal(format!("Failed to update index: {}", e)))?;
195
196 if mgr.current_session().map(|s| s.id == id).unwrap_or(false) {
198 mgr.set_current_session(opendev_models::Session::new());
199 }
200
201 Ok(Json(serde_json::json!({
202 "status": "success",
203 "message": format!("Session {} deleted", id),
204 })))
205}
206
207async fn resume_session(
209 State(state): State<AppState>,
210 AxumPath(id): AxumPath<String>,
211) -> Result<Json<serde_json::Value>, WebError> {
212 let mut mgr = state.session_manager_mut().await;
213 mgr.resume_session(&id)
214 .map_err(|e| WebError::NotFound(format!("Session {} not found: {}", id, e)))?;
215
216 Ok(Json(serde_json::json!({
217 "status": "resumed",
218 "session_id": id,
219 })))
220}
221
222async fn get_session_messages(
224 State(state): State<AppState>,
225 AxumPath(id): AxumPath<String>,
226) -> Result<Json<serde_json::Value>, WebError> {
227 let mgr = state.session_manager().await;
228 let session = mgr
229 .load_session(&id)
230 .map_err(|e| WebError::NotFound(format!("Session {} not found: {}", id, e)))?;
231
232 let messages: Vec<serde_json::Value> = session
233 .messages
234 .iter()
235 .map(|msg| {
236 let tool_calls: Vec<serde_json::Value> = msg
237 .tool_calls
238 .iter()
239 .map(|tc| {
240 let mut val = serde_json::json!({
241 "id": tc.id,
242 "name": tc.name,
243 "parameters": tc.parameters,
244 });
245 if let Some(ref result) = tc.result {
246 val["result"] = serde_json::json!(result);
247 }
248 if let Some(ref summary) = tc.result_summary {
249 val["result_summary"] = serde_json::json!(summary);
250 }
251 if let Some(ref error) = tc.error {
252 val["error"] = serde_json::json!(error);
253 }
254 if !tc.nested_tool_calls.is_empty() {
255 val["nested_tool_calls"] = serde_json::json!(tc.nested_tool_calls);
256 }
257 val
258 })
259 .collect();
260
261 let mut val = serde_json::json!({
262 "role": msg.role,
263 "content": msg.content,
264 "timestamp": msg.timestamp,
265 });
266 if !tool_calls.is_empty() {
267 val["tool_calls"] = serde_json::json!(tool_calls);
268 }
269 if let Some(ref reasoning) = msg.reasoning_content {
270 val["reasoning_content"] = serde_json::json!(reasoning);
271 }
272 if let Some(ref trace) = msg.thinking_trace {
273 val["thinking_trace"] = serde_json::json!(trace);
274 }
275 val
276 })
277 .collect();
278
279 Ok(Json(serde_json::json!(messages)))
280}
281
282async fn get_bridge_info(State(_state): State<AppState>) -> Json<serde_json::Value> {
284 Json(serde_json::json!({
287 "bridge_mode": false,
288 "session_id": null,
289 }))
290}
291
292#[cfg(test)]
293mod tests;