1use std::sync::Arc;
16
17use dashmap::DashMap;
18use tokio::sync::{broadcast, mpsc, Mutex};
19use trustee_core::session::Session;
20use trustee_core::types::TuiMessage;
21
22use crate::auth::AuthState;
23
24pub struct UserSessionEntry {
30 pub session: Arc<Mutex<Session>>,
32 pub ws_tx: broadcast::Sender<String>,
34 pub created_at: chrono::DateTime<chrono::Utc>,
36 pub last_active: Arc<Mutex<chrono::DateTime<chrono::Utc>>>,
39}
40
41pub struct UserSessions {
43 pub sessions: DashMap<String, UserSessionEntry>,
45 pub token_store: Arc<pep::MemoryTokenStore>,
47 pub active_session_id: Mutex<String>,
49}
50
51#[derive(Debug, serde::Serialize)]
53pub struct SessionListItem {
54 pub session_id: String,
55 pub session_name: Option<String>,
56 pub workflow_state: String,
57 pub created_at: String,
58 pub last_active: String,
59 pub handoff_count: u32,
64}
65
66#[derive(Debug)]
68pub enum SessionError {
69 MaxSessionsReached(usize),
71 NotFound(String),
73 NotIdle(String),
75}
76
77impl std::fmt::Display for SessionError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 SessionError::MaxSessionsReached(n) => {
81 write!(f, "Maximum {} sessions per user reached", n)
82 }
83 SessionError::NotFound(id) => write!(f, "Session {} not found", id),
84 SessionError::NotIdle(state) => write!(f, "Session is not idle (state: {})", state),
85 }
86 }
87}
88
89impl std::error::Error for SessionError {}
90
91pub type SessionRegistry = Arc<DashMap<String, UserSessions>>;
93
94#[derive(Clone)]
100pub struct ServerState {
101 pub sessions: SessionRegistry,
103 pub ws_tx: broadcast::Sender<String>,
105 pub auth: Option<Arc<AuthState>>,
107 pub config_toml: Option<String>,
109 pub secrets: Option<std::collections::HashMap<String, String>>,
111 pub build_info: Option<trustee_core::types::BuildInfo>,
113 pub workflow_semaphore: Arc<tokio::sync::Semaphore>,
116 pub max_sessions_per_user: usize,
118}
119
120impl ServerState {
121 pub fn new(
123 session: Session,
124 ws_tx: broadcast::Sender<String>,
125 auth: Option<Arc<AuthState>>,
126 ) -> Self {
127 let sessions = Arc::new(DashMap::new());
128
129 let token_store = Arc::new(pep::MemoryTokenStore::new());
131 let (ws_tx_entry, _) = broadcast::channel::<String>(256);
132
133 let now = chrono::Utc::now();
134 let initial_entry = UserSessionEntry {
135 session: Arc::new(Mutex::new(session)),
136 ws_tx: ws_tx_entry,
137 created_at: now,
138 last_active: Arc::new(Mutex::new(now)),
139 };
140
141 let user_sessions = UserSessions {
142 sessions: DashMap::new(),
143 token_store,
144 active_session_id: Mutex::new(String::new()),
145 };
146 user_sessions.sessions.insert("default".to_string(), initial_entry);
147
148 sessions.insert("default".to_string(), user_sessions);
149
150 Self {
151 sessions,
152 ws_tx,
153 auth,
154 config_toml: None,
155 secrets: None,
156 build_info: None,
157 workflow_semaphore: Arc::new(tokio::sync::Semaphore::new(8)),
158 max_sessions_per_user: 4,
159 }
160 }
161
162 pub fn with_config_toml(mut self, config_toml: String) -> Self {
163 self.config_toml = Some(config_toml);
164 self
165 }
166
167 pub fn with_secrets(mut self, secrets: std::collections::HashMap<String, String>) -> Self {
168 self.secrets = Some(secrets);
169 self
170 }
171
172 pub fn with_build_info(mut self, build_info: trustee_core::types::BuildInfo) -> Self {
173 self.build_info = Some(build_info);
174 self
175 }
176
177 pub fn with_max_concurrent_workflows(mut self, max: usize) -> Self {
178 self.workflow_semaphore = Arc::new(tokio::sync::Semaphore::new(max));
179 self
180 }
181
182 pub fn with_max_sessions_per_user(mut self, max: usize) -> Self {
184 self.max_sessions_per_user = max;
185 self
186 }
187
188 pub async fn create_session(
198 &self,
199 user_key: &str,
200 session_name: Option<String>,
201 identity: Option<String>,
202 activate: bool,
203 ) -> Result<String, SessionError> {
204 let user_sessions = self
206 .sessions
207 .entry(user_key.to_string())
208 .or_insert_with(|| UserSessions {
209 sessions: DashMap::new(),
210 token_store: Arc::new(pep::MemoryTokenStore::new()),
211 active_session_id: Mutex::new(String::new()),
212 });
213
214 if user_sessions.sessions.len() >= self.max_sessions_per_user {
216 return Err(SessionError::MaxSessionsReached(self.max_sessions_per_user));
217 }
218
219 let (mut session, workflow_rx) = Session::new();
221
222 if let Some(ref config_toml) = self.config_toml {
224 session.config_toml = Some(config_toml.clone());
225 session.parse_auto_handoff_config();
226 if let Ok(table) = config_toml.parse::<toml::Value>() {
227 if let Some(name) = table
228 .get("agent")
229 .and_then(|a| a.get("name"))
230 .and_then(|n| n.as_str())
231 {
232 session.agent_name = name.to_string();
233 }
234 }
235 }
236
237 session.secrets = self.secrets.clone();
238 session.build_info = self.build_info.clone();
239
240 self.apply_user_isolation(&mut session, user_key);
242
243 session.session_name = session_name;
245
246 session.identity = identity;
248
249 let (ws_tx_entry, _) = broadcast::channel::<String>(256);
251
252 let session_id = format!(
254 "session_{}_{}",
255 chrono::Utc::now().format("%Y_%m_%d_%H_%M"),
256 &uuid::Uuid::new_v4().to_string()[..8]
257 );
258
259 let now = chrono::Utc::now();
260
261 user_sessions.sessions.insert(
263 session_id.clone(),
264 UserSessionEntry {
265 session: Arc::new(Mutex::new(session)),
266 ws_tx: ws_tx_entry.clone(),
267 created_at: now,
268 last_active: Arc::new(Mutex::new(now)),
269 },
270 );
271
272 if activate {
281 *user_sessions.active_session_id.lock().await = session_id.clone();
282 }
283
284 let session_arc = user_sessions
286 .sessions
287 .get(&session_id)
288 .map(|e| e.session.clone());
289 if let Some(session_arc) = session_arc {
290 self.spawn_user_drain_task(
291 session_id.clone(),
292 session_arc,
293 ws_tx_entry,
294 workflow_rx,
295 );
296 }
297
298 Ok(session_id)
299 }
300
301 pub async fn get_session(
304 &self,
305 user_key: &str,
306 session_id: &str,
307 ) -> Option<(Arc<Mutex<Session>>, broadcast::Sender<String>)> {
308 let user_sessions = self.sessions.get(user_key)?;
309 let entry = user_sessions.sessions.get(session_id)?;
310
311 let now = chrono::Utc::now();
313 *entry.last_active.lock().await = now;
314
315 Some((entry.session.clone(), entry.ws_tx.clone()))
316 }
317
318 pub async fn get_session_by_any_id(
336 &self,
337 user_key: &str,
338 id: &str,
339 ) -> Option<(String, Arc<Mutex<Session>>, broadcast::Sender<String>)> {
340 let user_sessions = self.sessions.get(user_key)?;
342 if let Some(entry) = user_sessions.sessions.get(id) {
343 let now = chrono::Utc::now();
345 *entry.last_active.lock().await = now;
346 return Some((id.to_string(), entry.session.clone(), entry.ws_tx.clone()));
347 }
348
349 for entry in user_sessions.sessions.iter() {
351 let session = entry.session.lock().await;
352 if session.session_id.as_deref() == Some(id) {
353 let key = entry.key().clone();
354 let ws_tx = entry.ws_tx.clone();
355 drop(session);
356 let now = chrono::Utc::now();
358 *entry.last_active.lock().await = now;
359 return Some((key, entry.session.clone(), ws_tx));
360 }
361 }
362
363 None
364 }
365
366 pub async fn list_sessions(&self, user_key: &str) -> Vec<SessionListItem> {
368 let Some(user_sessions) = self.sessions.get(user_key) else {
369 return Vec::new();
370 };
371
372 let mut items = Vec::new();
373 for entry in user_sessions.sessions.iter() {
374 let session = entry.session.lock().await;
375 let workflow_state = match session.workflow_state {
376 trustee_core::types::WorkflowState::Idle => "Idle",
377 trustee_core::types::WorkflowState::Running => "Running",
378 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
379 };
380 let last_active = entry.last_active.lock().await;
381 items.push(SessionListItem {
382 session_id: entry.key().clone(),
383 session_name: session.session_name.clone(),
384 workflow_state: workflow_state.to_string(),
385 created_at: entry.created_at.to_rfc3339(),
386 last_active: last_active.to_rfc3339(),
387 handoff_count: session.handoff_count,
388 });
389 }
390 drop(user_sessions);
391
392 items.sort_by(|a, b| b.last_active.cmp(&a.last_active));
394 items
395 }
396
397 pub async fn destroy_session(
399 &self,
400 user_key: &str,
401 session_id: &str,
402 ) -> Result<(), SessionError> {
403 let user_sessions = self
404 .sessions
405 .get(user_key)
406 .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
407
408 {
410 let entry = user_sessions
411 .sessions
412 .get(session_id)
413 .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
414 let session = entry.session.lock().await;
415 if session.workflow_state != trustee_core::types::WorkflowState::Idle {
416 let state_str = match session.workflow_state {
417 trustee_core::types::WorkflowState::Running => "Running",
418 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
419 _ => "Unknown",
420 };
421 return Err(SessionError::NotIdle(state_str.to_string()));
422 }
423 }
424
425 user_sessions.sessions.remove(session_id);
427
428 let mut active_id = user_sessions.active_session_id.lock().await;
430 if &*active_id == session_id {
431 let mut newest: Option<(String, chrono::DateTime<chrono::Utc>)> = None;
433 for entry in user_sessions.sessions.iter() {
434 let la = entry.last_active.lock().await;
435 if newest.as_ref().map_or(true, |(_, t)| *la > *t) {
436 newest = Some((entry.key().clone(), *la));
437 }
438 }
439 *active_id = newest.map(|(id, _)| id).unwrap_or_default();
440 }
441
442 Ok(())
443 }
444
445 pub async fn ensure_active_session(
454 &self,
455 user_key: &str,
456 ) -> (
457 String,
458 Arc<Mutex<Session>>,
459 broadcast::Sender<String>,
460 Arc<pep::MemoryTokenStore>,
461 ) {
462 let token_store = {
464 let user_sessions = self
465 .sessions
466 .entry(user_key.to_string())
467 .or_insert_with(|| UserSessions {
468 sessions: DashMap::new(),
469 token_store: Arc::new(pep::MemoryTokenStore::new()),
470 active_session_id: Mutex::new(String::new()),
471 });
472 user_sessions.token_store.clone()
473 };
474
475 let active_id = {
477 let user_sessions = self.sessions.get(user_key).unwrap();
478 let guard = user_sessions.active_session_id.lock().await;
479 guard.clone()
480 };
481
482 if !active_id.is_empty() {
483 if let Some((session, ws_tx)) = self.get_session(user_key, &active_id).await {
484 return (active_id, session, ws_tx, token_store);
485 }
486 }
488
489 let existing_session: Option<(String, Arc<Mutex<Session>>, broadcast::Sender<String>)> = {
493 let user_sessions = self.sessions.get(user_key).unwrap();
494 let result = user_sessions.sessions.iter().next().map(|first| {
495 (
496 first.key().clone(),
497 first.session.clone(),
498 first.ws_tx.clone(),
499 )
500 });
501 result
502 };
503 if let Some((id, session, ws_tx)) = existing_session {
504 let now = chrono::Utc::now();
505 if let Some(entry) = self.sessions.get(user_key) {
506 if let Some(e) = entry.sessions.get(&id) {
507 *e.last_active.lock().await = now;
508 }
509 *entry.active_session_id.lock().await = id.clone();
510 }
511
512 return (id, session, ws_tx, token_store);
513 }
514
515 let session_id = self
517 .create_session(user_key, None, None, true)
518 .await
519 .unwrap_or_else(|_| "default".to_string());
520
521 let (session, ws_tx) = self
522 .get_session(user_key, &session_id)
523 .await
524 .expect("just-created session must exist");
525
526 (session_id, session, ws_tx, token_store)
527 }
528
529 pub async fn ensure_user_session(
532 &self,
533 user_key: &str,
534 ) -> (Arc<Mutex<Session>>, broadcast::Sender<String>, Arc<pep::MemoryTokenStore>) {
535 let (_id, session, ws_tx, token_store) = self.ensure_active_session(user_key).await;
536 (session, ws_tx, token_store)
537 }
538
539 pub async fn set_active_session(&self, user_key: &str, session_id: &str) {
541 if let Some(user_sessions) = self.sessions.get(user_key) {
542 if user_sessions.sessions.contains_key(session_id) {
543 *user_sessions.active_session_id.lock().await = session_id.to_string();
544 }
545 }
546 }
547
548 pub fn get_user_home_dir(&self, user_key: &str) -> Option<std::path::PathBuf> {
559 use sha2::{Digest, Sha256};
560 let mut hasher = Sha256::new();
561 hasher.update(user_key.as_bytes());
562 let hash_bytes = hasher.finalize();
563 let user_hash = format!(
564 "{:016x}",
565 u64::from_be_bytes(hash_bytes[..8].try_into().unwrap())
566 );
567 dirs::home_dir().map(|home| home.join(".trustee").join("users").join(&user_hash))
568 }
569
570 pub fn get_user_config_and_home(&self, user_key: &str) -> (Option<String>, Option<std::path::PathBuf>) {
575 (self.config_toml.clone(), self.get_user_home_dir(user_key))
576 }
577
578 fn apply_user_isolation(&self, session: &mut Session, user_key: &str) {
584 use sha2::{Digest, Sha256};
585 let mut hasher = Sha256::new();
586 hasher.update(user_key.as_bytes());
587 let hash_bytes = hasher.finalize();
588 let user_hash = format!(
589 "{:016x}",
590 u64::from_be_bytes(hash_bytes[..8].try_into().unwrap())
591 );
592
593 let user_home = if let Some(home) = dirs::home_dir() {
595 let user_home = home.join(".trustee").join("users").join(&user_hash);
596 session.home_dir = Some(user_home.clone());
597 Some(user_home)
598 } else {
599 None
600 };
601
602 session.project_id = Some(format!("web{}", &user_hash[..16]));
603
604 let shared_secrets = session.secrets.clone().unwrap_or_default();
611 let mut merged_secrets = shared_secrets.clone();
612
613 if let Some(ref user_home) = user_home {
614 if let Ok(merged) = self.load_user_secrets(user_home, &merged_secrets) {
615 merged_secrets = merged;
616 }
617 }
618
619 if let Some(ref user_home) = user_home {
621 if let Some(merged) = self.merge_user_config(user_home, session.config_toml.as_deref()) {
622 session.config_toml = Some(merged);
623 tracing::debug!("Merged per-user config into session");
624 }
625 }
626
627 if let Some(ref mut config_toml) = session.config_toml {
629 substitute_env_vars(config_toml, &merged_secrets);
630 }
631
632 session.secrets = Some(shared_secrets);
634 }
635
636 fn load_user_secrets(
639 &self,
640 user_home: &std::path::Path,
641 base: &std::collections::HashMap<String, String>,
642 ) -> std::io::Result<std::collections::HashMap<String, String>> {
643 let user_env_path = user_home.join(".env");
644 if !user_env_path.exists() {
645 return Ok(base.clone());
646 }
647 let content = std::fs::read_to_string(&user_env_path)?;
648 let mut merged = base.clone();
649 for line in content.lines() {
650 let line = line.trim();
651 if line.is_empty() || line.starts_with('#') {
652 continue;
653 }
654 if let Some((key, value)) = line.split_once('=') {
655 let key = key.trim().to_string();
656 let value = value
657 .trim()
658 .trim_matches('"')
659 .trim_matches('\'')
660 .to_string();
661 merged.insert(key, value);
662 }
663 }
664 tracing::debug!("Loaded per-user secrets from {}", user_env_path.display());
665 Ok(merged)
666 }
667
668 fn merge_user_config(
671 &self,
672 user_home: &std::path::Path,
673 shared_config: Option<&str>,
674 ) -> Option<String> {
675 let user_config_path = user_home.join("config").join("trustee.toml");
676 if !user_config_path.exists() {
677 return None;
678 }
679 let user_config_toml = std::fs::read_to_string(&user_config_path).ok()?;
680 let shared = shared_config
681 .unwrap_or("")
682 .parse::<toml::Value>()
683 .ok()?;
684 let overlay = user_config_toml.parse::<toml::Value>().ok()?;
685 let mut shared = shared;
686 deep_merge_toml(&mut shared, &overlay);
687 let merged = toml::to_string(&shared).ok()?;
688 tracing::debug!("Merged per-user config from {}", user_config_path.display());
689 Some(merged)
690 }
691
692 pub fn resolve_user_config(&self, user_key: &str) -> Option<String> {
701 let config_toml = self.config_toml.clone()?;
702
703 let user_home = self.get_user_home_dir(user_key)?;
705
706 let mut merged_secrets = self.secrets.clone().unwrap_or_default();
708 if let Ok(merged) = self.load_user_secrets(&user_home, &merged_secrets) {
709 merged_secrets = merged;
710 }
711
712 let mut resolved = config_toml;
714 if let Some(merged) = self.merge_user_config(&user_home, Some(&resolved)) {
715 resolved = merged;
716 }
717
718 substitute_env_vars(&mut resolved, &merged_secrets);
720
721 Some(resolved)
722 }
723
724 fn spawn_user_drain_task(
726 &self,
727 session_id: String,
728 session: Arc<Mutex<Session>>,
729 ws_tx: broadcast::Sender<String>,
730 mut workflow_rx: mpsc::UnboundedReceiver<TuiMessage>,
731 ) {
732 let mut last_broadcast_state: Option<String> = None;
738 tokio::spawn(async move {
739 while let Some(msg) = workflow_rx.recv().await {
740 {
741 let mut session = session.lock().await;
742 session.handle_workflow_message(msg.clone());
743
744 let state_str = match session.workflow_state {
745 trustee_core::types::WorkflowState::Idle => "Idle",
746 trustee_core::types::WorkflowState::Running => "Running",
747 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
748 };
749 if last_broadcast_state.as_deref() != Some(state_str) {
750 last_broadcast_state = Some(state_str.to_string());
751 let state_msg = serde_json::json!({
752 "type": "StateChanged",
753 "state": state_str
754 });
755 let _ = ws_tx.send(state_msg.to_string());
756 }
757 }
758
759 let json =
760 serde_json::to_string(&SerializableMessage(&msg)).unwrap_or_default();
761 let _ = ws_tx.send(json);
762 }
763 tracing::debug!("Drain task ended for session: {}", session_id);
764 });
765 }
766
767 pub fn spawn_drain_task(self, mut workflow_rx: mpsc::UnboundedReceiver<TuiMessage>) {
770 let default_user = self
772 .sessions
773 .get("default")
774 .expect("default user must exist");
775 let first_entry = default_user
776 .sessions
777 .iter()
778 .next()
779 .expect("default user must have at least one session");
780 let session = first_entry.session.clone();
781 let ws_tx = first_entry.ws_tx.clone();
782 let session_id = first_entry.key().clone();
783 drop(first_entry);
784 drop(default_user);
785
786 let mut last_broadcast_state = Some("Running".to_string());
790 tokio::spawn(async move {
791 while let Some(msg) = workflow_rx.recv().await {
792 {
793 let mut session = session.lock().await;
794 session.handle_workflow_message(msg.clone());
795
796 let state_str = match session.workflow_state {
797 trustee_core::types::WorkflowState::Idle => "Idle",
798 trustee_core::types::WorkflowState::Running => "Running",
799 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
800 };
801 if last_broadcast_state.as_deref() != Some(state_str) {
802 last_broadcast_state = Some(state_str.to_string());
803 let state_msg = serde_json::json!({
804 "type": "StateChanged",
805 "state": state_str
806 });
807 let _ = ws_tx.send(state_msg.to_string());
808 }
809 }
810
811 let json =
812 serde_json::to_string(&SerializableMessage(&msg)).unwrap_or_default();
813 let _ = ws_tx.send(json);
814 }
815 tracing::debug!("Drain task ended for session: {}", session_id);
816 });
817 }
818
819 pub async fn resolve_user_key(&self, headers: &axum::http::HeaderMap) -> String {
821 let Some(ref auth) = self.auth else {
822 return "default".to_string();
823 };
824
825 if let Some(token) = headers
827 .get(axum::http::header::AUTHORIZATION)
828 .and_then(|v| v.to_str().ok())
829 .and_then(|v| v.strip_prefix("Bearer "))
830 .map(|s| s.to_string())
831 {
832 if token.starts_with("dev:") {
833 let parts: Vec<&str> = token.splitn(4, ':').collect();
834 if parts.len() >= 4 {
835 return format!("dev:{}", parts[1]);
836 }
837 }
838 if let Ok(claims) = auth.validate_token(&token).await {
839 return claims.sub;
840 }
841 }
842
843 let cookie_session_id = headers
845 .get(axum::http::header::COOKIE)
846 .and_then(|v| v.to_str().ok())
847 .and_then(|cookies| {
848 cookies
849 .split(';')
850 .map(|c| c.trim())
851 .find_map(|c| {
852 c.strip_prefix(&format!("{}=", auth.config.cookie_name))
853 .map(|s| s.to_string())
854 })
855 });
856
857 if let Some(session_id) = cookie_session_id {
858 if session_id.starts_with("dev:") {
859 let parts: Vec<&str> = session_id.splitn(4, ':').collect();
860 if parts.len() >= 4 {
861 return format!("dev:{}", parts[1]);
862 }
863 }
864
865 if let Ok(access_token) = auth.session_manager.get_token(&session_id).await {
866 if let Ok(claims) = auth.validate_token(&access_token).await {
867 return claims.sub;
868 }
869 }
870 }
871
872 "default".to_string()
873 }
874}
875
876struct SerializableMessage<'a>(&'a TuiMessage);
882
883impl<'a> serde::Serialize for SerializableMessage<'a> {
884 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
885 where
886 S: serde::Serializer,
887 {
888 use serde::ser::SerializeStruct;
889
890 match self.0 {
891 TuiMessage::OutputLine(line) => {
892 let mut s = serializer.serialize_struct("msg", 2)?;
893 s.serialize_field("type", "OutputLine")?;
894 s.serialize_field("line", line)?;
895 s.end()
896 }
897 TuiMessage::StreamDelta(delta) => {
898 let mut s = serializer.serialize_struct("msg", 2)?;
899 s.serialize_field("type", "StreamDelta")?;
900 s.serialize_field("delta", delta)?;
901 s.end()
902 }
903 TuiMessage::ReasoningDelta(delta) => {
904 let mut s = serializer.serialize_struct("msg", 2)?;
905 s.serialize_field("type", "ReasoningDelta")?;
906 s.serialize_field("delta", delta)?;
907 s.end()
908 }
909 TuiMessage::WorkflowCompleted => {
910 let mut s = serializer.serialize_struct("msg", 2)?;
911 s.serialize_field("type", "WorkflowCompleted")?;
912 s.serialize_field("state", "Idle")?;
913 s.end()
914 }
915 TuiMessage::WorkflowError(err) => {
916 let mut s = serializer.serialize_struct("msg", 2)?;
917 s.serialize_field("type", "WorkflowError")?;
918 s.serialize_field("error", err)?;
919 s.end()
920 }
921 TuiMessage::ResumeInfo(info) => match info {
922 Some(ri) => {
923 let mut s = serializer.serialize_struct("msg", 5)?;
924 s.serialize_field("type", "ResumeInfo")?;
925 s.serialize_field("state", "Idle")?;
926 s.serialize_field("session_id", &ri.session_id)?;
927 s.serialize_field("checkpoint_id", &ri.checkpoint_id)?;
928 s.serialize_field("iteration", &ri.iteration)?;
929 s.end()
930 }
931 None => {
932 let mut s = serializer.serialize_struct("msg", 2)?;
933 s.serialize_field("type", "ResumeInfo")?;
934 s.serialize_field("state", "Idle")?;
935 s.end()
936 }
937 },
938 TuiMessage::TodoUpdate(content) => {
939 let mut s = serializer.serialize_struct("msg", 2)?;
940 s.serialize_field("type", "TodoUpdate")?;
941 s.serialize_field("content", content)?;
942 s.end()
943 }
944 TuiMessage::WorkflowCancelled => {
945 let mut s = serializer.serialize_struct("msg", 2)?;
946 s.serialize_field("type", "WorkflowCancelled")?;
947 s.serialize_field("state", "Idle")?;
948 s.end()
949 }
950 TuiMessage::HandoffReady(briefing) => {
951 let mut s = serializer.serialize_struct("msg", 3)?;
952 s.serialize_field("type", "HandoffReady")?;
953 s.serialize_field("state", "Idle")?;
954 s.serialize_field("briefing", briefing)?;
955 s.end()
956 }
957 TuiMessage::SessionRotated { old, new } => {
958 let mut s = serializer.serialize_struct("msg", 3)?;
959 s.serialize_field("type", "SessionRotated")?;
960 s.serialize_field("old", old)?;
961 s.serialize_field("new", new)?;
962 s.end()
963 }
964 TuiMessage::HandoffFailed => {
965 let mut s = serializer.serialize_struct("msg", 2)?;
966 s.serialize_field("type", "HandoffFailed")?;
967 s.serialize_field("state", "Idle")?;
968 s.end()
969 }
970 TuiMessage::ToolPending {
971 tool_name,
972 hint,
973 } => {
974 let mut s = serializer.serialize_struct("msg", 3)?;
975 s.serialize_field("type", "ToolPending")?;
976 s.serialize_field("tool_name", tool_name)?;
977 s.serialize_field("hint", hint)?;
978 s.end()
979 }
980 TuiMessage::ToolDone {
981 tool_name,
982 success,
983 hint,
984 } => {
985 let mut s = serializer.serialize_struct("msg", 4)?;
986 s.serialize_field("type", "ToolDone")?;
987 s.serialize_field("tool_name", tool_name)?;
988 s.serialize_field("success", success)?;
989 s.serialize_field("hint", hint)?;
990 s.end()
991 }
992 TuiMessage::ContextTokensUpdated(count) => {
993 let mut s = serializer.serialize_struct("msg", 2)?;
994 s.serialize_field("type", "ContextTokensUpdated")?;
995 s.serialize_field("count", count)?;
996 s.end()
997 }
998 TuiMessage::McpServerStatus {
999 name,
1000 connected,
1001 tool_count,
1002 error,
1003 } => {
1004 let mut s = serializer.serialize_struct("msg", 5)?;
1005 s.serialize_field("type", "McpServerStatus")?;
1006 s.serialize_field("name", name)?;
1007 s.serialize_field("connected", connected)?;
1008 s.serialize_field("tool_count", tool_count)?;
1009 s.serialize_field("error", error)?;
1010 s.end()
1011 }
1012 TuiMessage::SessionTitleUpdated(title) => {
1013 let mut s = serializer.serialize_struct("msg", 2)?;
1014 s.serialize_field("type", "SessionTitleUpdated")?;
1015 s.serialize_field("title", title)?;
1016 s.end()
1017 }
1018 }
1019 }
1020}
1021
1022fn deep_merge_toml(base: &mut toml::Value, overlay: &toml::Value) {
1033 match (base, overlay) {
1034 (toml::Value::Table(base_table), toml::Value::Table(overlay_table)) => {
1035 for (key, overlay_val) in overlay_table {
1036 match base_table.get_mut(key) {
1037 Some(base_val) => {
1038 deep_merge_toml(base_val, overlay_val);
1040 }
1041 None => {
1042 base_table.insert(key.clone(), overlay_val.clone());
1044 }
1045 }
1046 }
1047 }
1048 (base, overlay) => {
1050 *base = overlay.clone();
1051 }
1052 }
1053}
1054
1055fn substitute_env_vars(s: &mut String, secrets: &std::collections::HashMap<String, String>) {
1060 let mut result = String::with_capacity(s.len());
1062 let bytes = s.as_bytes();
1063 let mut i = 0;
1064
1065 while i < bytes.len() {
1066 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
1067 if let Some(end) = s[i + 2..].find('}') {
1069 let var_name = &s[i + 2..i + 2 + end];
1070 if let Some(value) = secrets.get(var_name) {
1072 result.push_str(value);
1073 } else if let Ok(value) = std::env::var(var_name) {
1074 result.push_str(&value);
1075 } else {
1076 result.push_str(&s[i..i + 2 + end + 1]);
1078 }
1079 i = i + 2 + end + 1;
1080 } else {
1081 result.push('$');
1083 i += 1;
1084 }
1085 } else {
1086 result.push(bytes[i] as char);
1087 i += 1;
1088 }
1089 }
1090
1091 *s = result;
1092}