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 ) -> Result<String, SessionError> {
203 let user_sessions = self
205 .sessions
206 .entry(user_key.to_string())
207 .or_insert_with(|| UserSessions {
208 sessions: DashMap::new(),
209 token_store: Arc::new(pep::MemoryTokenStore::new()),
210 active_session_id: Mutex::new(String::new()),
211 });
212
213 if user_sessions.sessions.len() >= self.max_sessions_per_user {
215 return Err(SessionError::MaxSessionsReached(self.max_sessions_per_user));
216 }
217
218 let (mut session, workflow_rx) = Session::new();
220
221 if let Some(ref config_toml) = self.config_toml {
223 session.config_toml = Some(config_toml.clone());
224 session.parse_auto_handoff_config();
225 if let Ok(table) = config_toml.parse::<toml::Value>() {
226 if let Some(name) = table
227 .get("agent")
228 .and_then(|a| a.get("name"))
229 .and_then(|n| n.as_str())
230 {
231 session.agent_name = name.to_string();
232 }
233 }
234 }
235
236 session.secrets = self.secrets.clone();
237 session.build_info = self.build_info.clone();
238
239 self.apply_user_isolation(&mut session, user_key);
241
242 session.session_name = session_name;
244
245 session.identity = identity;
247
248 let (ws_tx_entry, _) = broadcast::channel::<String>(256);
250
251 let session_id = format!(
253 "session_{}_{}",
254 chrono::Utc::now().format("%Y_%m_%d_%H_%M"),
255 &uuid::Uuid::new_v4().to_string()[..8]
256 );
257
258 let now = chrono::Utc::now();
259
260 user_sessions.sessions.insert(
262 session_id.clone(),
263 UserSessionEntry {
264 session: Arc::new(Mutex::new(session)),
265 ws_tx: ws_tx_entry.clone(),
266 created_at: now,
267 last_active: Arc::new(Mutex::new(now)),
268 },
269 );
270
271 *user_sessions.active_session_id.lock().await = session_id.clone();
273
274 let session_arc = user_sessions
276 .sessions
277 .get(&session_id)
278 .map(|e| e.session.clone());
279 if let Some(session_arc) = session_arc {
280 self.spawn_user_drain_task(
281 session_id.clone(),
282 session_arc,
283 ws_tx_entry,
284 workflow_rx,
285 );
286 }
287
288 Ok(session_id)
289 }
290
291 pub async fn get_session(
294 &self,
295 user_key: &str,
296 session_id: &str,
297 ) -> Option<(Arc<Mutex<Session>>, broadcast::Sender<String>)> {
298 let user_sessions = self.sessions.get(user_key)?;
299 let entry = user_sessions.sessions.get(session_id)?;
300
301 let now = chrono::Utc::now();
303 *entry.last_active.lock().await = now;
304
305 Some((entry.session.clone(), entry.ws_tx.clone()))
306 }
307
308 pub async fn get_session_by_any_id(
326 &self,
327 user_key: &str,
328 id: &str,
329 ) -> Option<(String, Arc<Mutex<Session>>, broadcast::Sender<String>)> {
330 let user_sessions = self.sessions.get(user_key)?;
332 if let Some(entry) = user_sessions.sessions.get(id) {
333 let now = chrono::Utc::now();
335 *entry.last_active.lock().await = now;
336 return Some((id.to_string(), entry.session.clone(), entry.ws_tx.clone()));
337 }
338
339 for entry in user_sessions.sessions.iter() {
341 let session = entry.session.lock().await;
342 if session.session_id.as_deref() == Some(id) {
343 let key = entry.key().clone();
344 let ws_tx = entry.ws_tx.clone();
345 drop(session);
346 let now = chrono::Utc::now();
348 *entry.last_active.lock().await = now;
349 return Some((key, entry.session.clone(), ws_tx));
350 }
351 }
352
353 None
354 }
355
356 pub async fn list_sessions(&self, user_key: &str) -> Vec<SessionListItem> {
358 let Some(user_sessions) = self.sessions.get(user_key) else {
359 return Vec::new();
360 };
361
362 let mut items = Vec::new();
363 for entry in user_sessions.sessions.iter() {
364 let session = entry.session.lock().await;
365 let workflow_state = match session.workflow_state {
366 trustee_core::types::WorkflowState::Idle => "Idle",
367 trustee_core::types::WorkflowState::Running => "Running",
368 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
369 };
370 let last_active = entry.last_active.lock().await;
371 items.push(SessionListItem {
372 session_id: entry.key().clone(),
373 session_name: session.session_name.clone(),
374 workflow_state: workflow_state.to_string(),
375 created_at: entry.created_at.to_rfc3339(),
376 last_active: last_active.to_rfc3339(),
377 handoff_count: session.handoff_count,
378 });
379 }
380 drop(user_sessions);
381
382 items.sort_by(|a, b| b.last_active.cmp(&a.last_active));
384 items
385 }
386
387 pub async fn destroy_session(
389 &self,
390 user_key: &str,
391 session_id: &str,
392 ) -> Result<(), SessionError> {
393 let user_sessions = self
394 .sessions
395 .get(user_key)
396 .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
397
398 {
400 let entry = user_sessions
401 .sessions
402 .get(session_id)
403 .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
404 let session = entry.session.lock().await;
405 if session.workflow_state != trustee_core::types::WorkflowState::Idle {
406 let state_str = match session.workflow_state {
407 trustee_core::types::WorkflowState::Running => "Running",
408 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
409 _ => "Unknown",
410 };
411 return Err(SessionError::NotIdle(state_str.to_string()));
412 }
413 }
414
415 user_sessions.sessions.remove(session_id);
417
418 let mut active_id = user_sessions.active_session_id.lock().await;
420 if &*active_id == session_id {
421 let mut newest: Option<(String, chrono::DateTime<chrono::Utc>)> = None;
423 for entry in user_sessions.sessions.iter() {
424 let la = entry.last_active.lock().await;
425 if newest.as_ref().map_or(true, |(_, t)| *la > *t) {
426 newest = Some((entry.key().clone(), *la));
427 }
428 }
429 *active_id = newest.map(|(id, _)| id).unwrap_or_default();
430 }
431
432 Ok(())
433 }
434
435 pub async fn ensure_active_session(
444 &self,
445 user_key: &str,
446 ) -> (
447 String,
448 Arc<Mutex<Session>>,
449 broadcast::Sender<String>,
450 Arc<pep::MemoryTokenStore>,
451 ) {
452 let token_store = {
454 let user_sessions = self
455 .sessions
456 .entry(user_key.to_string())
457 .or_insert_with(|| UserSessions {
458 sessions: DashMap::new(),
459 token_store: Arc::new(pep::MemoryTokenStore::new()),
460 active_session_id: Mutex::new(String::new()),
461 });
462 user_sessions.token_store.clone()
463 };
464
465 let active_id = {
467 let user_sessions = self.sessions.get(user_key).unwrap();
468 let guard = user_sessions.active_session_id.lock().await;
469 guard.clone()
470 };
471
472 if !active_id.is_empty() {
473 if let Some((session, ws_tx)) = self.get_session(user_key, &active_id).await {
474 return (active_id, session, ws_tx, token_store);
475 }
476 }
478
479 let existing_session: Option<(String, Arc<Mutex<Session>>, broadcast::Sender<String>)> = {
483 let user_sessions = self.sessions.get(user_key).unwrap();
484 let result = user_sessions.sessions.iter().next().map(|first| {
485 (
486 first.key().clone(),
487 first.session.clone(),
488 first.ws_tx.clone(),
489 )
490 });
491 result
492 };
493 if let Some((id, session, ws_tx)) = existing_session {
494 let now = chrono::Utc::now();
495 if let Some(entry) = self.sessions.get(user_key) {
496 if let Some(e) = entry.sessions.get(&id) {
497 *e.last_active.lock().await = now;
498 }
499 *entry.active_session_id.lock().await = id.clone();
500 }
501
502 return (id, session, ws_tx, token_store);
503 }
504
505 let session_id = self
507 .create_session(user_key, None, None)
508 .await
509 .unwrap_or_else(|_| "default".to_string());
510
511 let (session, ws_tx) = self
512 .get_session(user_key, &session_id)
513 .await
514 .expect("just-created session must exist");
515
516 (session_id, session, ws_tx, token_store)
517 }
518
519 pub async fn ensure_user_session(
522 &self,
523 user_key: &str,
524 ) -> (Arc<Mutex<Session>>, broadcast::Sender<String>, Arc<pep::MemoryTokenStore>) {
525 let (_id, session, ws_tx, token_store) = self.ensure_active_session(user_key).await;
526 (session, ws_tx, token_store)
527 }
528
529 pub async fn set_active_session(&self, user_key: &str, session_id: &str) {
531 if let Some(user_sessions) = self.sessions.get(user_key) {
532 if user_sessions.sessions.contains_key(session_id) {
533 *user_sessions.active_session_id.lock().await = session_id.to_string();
534 }
535 }
536 }
537
538 pub fn get_user_home_dir(&self, user_key: &str) -> Option<std::path::PathBuf> {
549 use sha2::{Digest, Sha256};
550 let mut hasher = Sha256::new();
551 hasher.update(user_key.as_bytes());
552 let hash_bytes = hasher.finalize();
553 let user_hash = format!(
554 "{:016x}",
555 u64::from_be_bytes(hash_bytes[..8].try_into().unwrap())
556 );
557 dirs::home_dir().map(|home| home.join(".trustee").join("users").join(&user_hash))
558 }
559
560 pub fn get_user_config_and_home(&self, user_key: &str) -> (Option<String>, Option<std::path::PathBuf>) {
565 (self.config_toml.clone(), self.get_user_home_dir(user_key))
566 }
567
568 fn apply_user_isolation(&self, session: &mut Session, user_key: &str) {
574 use sha2::{Digest, Sha256};
575 let mut hasher = Sha256::new();
576 hasher.update(user_key.as_bytes());
577 let hash_bytes = hasher.finalize();
578 let user_hash = format!(
579 "{:016x}",
580 u64::from_be_bytes(hash_bytes[..8].try_into().unwrap())
581 );
582
583 let user_home = if let Some(home) = dirs::home_dir() {
585 let user_home = home.join(".trustee").join("users").join(&user_hash);
586 session.home_dir = Some(user_home.clone());
587 Some(user_home)
588 } else {
589 None
590 };
591
592 session.project_id = Some(format!("web{}", &user_hash[..16]));
593
594 let shared_secrets = session.secrets.clone().unwrap_or_default();
601 let mut merged_secrets = shared_secrets.clone();
602
603 if let Some(ref user_home) = user_home {
604 if let Ok(merged) = self.load_user_secrets(user_home, &merged_secrets) {
605 merged_secrets = merged;
606 }
607 }
608
609 if let Some(ref user_home) = user_home {
611 if let Some(merged) = self.merge_user_config(user_home, session.config_toml.as_deref()) {
612 session.config_toml = Some(merged);
613 tracing::debug!("Merged per-user config into session");
614 }
615 }
616
617 if let Some(ref mut config_toml) = session.config_toml {
619 substitute_env_vars(config_toml, &merged_secrets);
620 }
621
622 session.secrets = Some(shared_secrets);
624 }
625
626 fn load_user_secrets(
629 &self,
630 user_home: &std::path::Path,
631 base: &std::collections::HashMap<String, String>,
632 ) -> std::io::Result<std::collections::HashMap<String, String>> {
633 let user_env_path = user_home.join(".env");
634 if !user_env_path.exists() {
635 return Ok(base.clone());
636 }
637 let content = std::fs::read_to_string(&user_env_path)?;
638 let mut merged = base.clone();
639 for line in content.lines() {
640 let line = line.trim();
641 if line.is_empty() || line.starts_with('#') {
642 continue;
643 }
644 if let Some((key, value)) = line.split_once('=') {
645 let key = key.trim().to_string();
646 let value = value
647 .trim()
648 .trim_matches('"')
649 .trim_matches('\'')
650 .to_string();
651 merged.insert(key, value);
652 }
653 }
654 tracing::debug!("Loaded per-user secrets from {}", user_env_path.display());
655 Ok(merged)
656 }
657
658 fn merge_user_config(
661 &self,
662 user_home: &std::path::Path,
663 shared_config: Option<&str>,
664 ) -> Option<String> {
665 let user_config_path = user_home.join("config").join("trustee.toml");
666 if !user_config_path.exists() {
667 return None;
668 }
669 let user_config_toml = std::fs::read_to_string(&user_config_path).ok()?;
670 let shared = shared_config
671 .unwrap_or("")
672 .parse::<toml::Value>()
673 .ok()?;
674 let overlay = user_config_toml.parse::<toml::Value>().ok()?;
675 let mut shared = shared;
676 deep_merge_toml(&mut shared, &overlay);
677 let merged = toml::to_string(&shared).ok()?;
678 tracing::debug!("Merged per-user config from {}", user_config_path.display());
679 Some(merged)
680 }
681
682 pub fn resolve_user_config(&self, user_key: &str) -> Option<String> {
691 let config_toml = self.config_toml.clone()?;
692
693 let user_home = self.get_user_home_dir(user_key)?;
695
696 let mut merged_secrets = self.secrets.clone().unwrap_or_default();
698 if let Ok(merged) = self.load_user_secrets(&user_home, &merged_secrets) {
699 merged_secrets = merged;
700 }
701
702 let mut resolved = config_toml;
704 if let Some(merged) = self.merge_user_config(&user_home, Some(&resolved)) {
705 resolved = merged;
706 }
707
708 substitute_env_vars(&mut resolved, &merged_secrets);
710
711 Some(resolved)
712 }
713
714 fn spawn_user_drain_task(
716 &self,
717 session_id: String,
718 session: Arc<Mutex<Session>>,
719 ws_tx: broadcast::Sender<String>,
720 mut workflow_rx: mpsc::UnboundedReceiver<TuiMessage>,
721 ) {
722 let mut last_broadcast_state: Option<String> = None;
728 tokio::spawn(async move {
729 while let Some(msg) = workflow_rx.recv().await {
730 {
731 let mut session = session.lock().await;
732 session.handle_workflow_message(msg.clone());
733
734 let state_str = match session.workflow_state {
735 trustee_core::types::WorkflowState::Idle => "Idle",
736 trustee_core::types::WorkflowState::Running => "Running",
737 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
738 };
739 if last_broadcast_state.as_deref() != Some(state_str) {
740 last_broadcast_state = Some(state_str.to_string());
741 let state_msg = serde_json::json!({
742 "type": "StateChanged",
743 "state": state_str
744 });
745 let _ = ws_tx.send(state_msg.to_string());
746 }
747 }
748
749 let json =
750 serde_json::to_string(&SerializableMessage(&msg)).unwrap_or_default();
751 let _ = ws_tx.send(json);
752 }
753 tracing::debug!("Drain task ended for session: {}", session_id);
754 });
755 }
756
757 pub fn spawn_drain_task(self, mut workflow_rx: mpsc::UnboundedReceiver<TuiMessage>) {
760 let default_user = self
762 .sessions
763 .get("default")
764 .expect("default user must exist");
765 let first_entry = default_user
766 .sessions
767 .iter()
768 .next()
769 .expect("default user must have at least one session");
770 let session = first_entry.session.clone();
771 let ws_tx = first_entry.ws_tx.clone();
772 let session_id = first_entry.key().clone();
773 drop(first_entry);
774 drop(default_user);
775
776 let mut last_broadcast_state = Some("Running".to_string());
780 tokio::spawn(async move {
781 while let Some(msg) = workflow_rx.recv().await {
782 {
783 let mut session = session.lock().await;
784 session.handle_workflow_message(msg.clone());
785
786 let state_str = match session.workflow_state {
787 trustee_core::types::WorkflowState::Idle => "Idle",
788 trustee_core::types::WorkflowState::Running => "Running",
789 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
790 };
791 if last_broadcast_state.as_deref() != Some(state_str) {
792 last_broadcast_state = Some(state_str.to_string());
793 let state_msg = serde_json::json!({
794 "type": "StateChanged",
795 "state": state_str
796 });
797 let _ = ws_tx.send(state_msg.to_string());
798 }
799 }
800
801 let json =
802 serde_json::to_string(&SerializableMessage(&msg)).unwrap_or_default();
803 let _ = ws_tx.send(json);
804 }
805 tracing::debug!("Drain task ended for session: {}", session_id);
806 });
807 }
808
809 pub async fn resolve_user_key(&self, headers: &axum::http::HeaderMap) -> String {
811 let Some(ref auth) = self.auth else {
812 return "default".to_string();
813 };
814
815 if let Some(token) = headers
817 .get(axum::http::header::AUTHORIZATION)
818 .and_then(|v| v.to_str().ok())
819 .and_then(|v| v.strip_prefix("Bearer "))
820 .map(|s| s.to_string())
821 {
822 if token.starts_with("dev:") {
823 let parts: Vec<&str> = token.splitn(4, ':').collect();
824 if parts.len() >= 4 {
825 return format!("dev:{}", parts[1]);
826 }
827 }
828 if let Ok(claims) = auth.validate_token(&token).await {
829 return claims.sub;
830 }
831 }
832
833 let cookie_session_id = headers
835 .get(axum::http::header::COOKIE)
836 .and_then(|v| v.to_str().ok())
837 .and_then(|cookies| {
838 cookies
839 .split(';')
840 .map(|c| c.trim())
841 .find_map(|c| {
842 c.strip_prefix(&format!("{}=", auth.config.cookie_name))
843 .map(|s| s.to_string())
844 })
845 });
846
847 if let Some(session_id) = cookie_session_id {
848 if session_id.starts_with("dev:") {
849 let parts: Vec<&str> = session_id.splitn(4, ':').collect();
850 if parts.len() >= 4 {
851 return format!("dev:{}", parts[1]);
852 }
853 }
854
855 if let Ok(access_token) = auth.session_manager.get_token(&session_id).await {
856 if let Ok(claims) = auth.validate_token(&access_token).await {
857 return claims.sub;
858 }
859 }
860 }
861
862 "default".to_string()
863 }
864}
865
866struct SerializableMessage<'a>(&'a TuiMessage);
872
873impl<'a> serde::Serialize for SerializableMessage<'a> {
874 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
875 where
876 S: serde::Serializer,
877 {
878 use serde::ser::SerializeStruct;
879
880 match self.0 {
881 TuiMessage::OutputLine(line) => {
882 let mut s = serializer.serialize_struct("msg", 2)?;
883 s.serialize_field("type", "OutputLine")?;
884 s.serialize_field("line", line)?;
885 s.end()
886 }
887 TuiMessage::StreamDelta(delta) => {
888 let mut s = serializer.serialize_struct("msg", 2)?;
889 s.serialize_field("type", "StreamDelta")?;
890 s.serialize_field("delta", delta)?;
891 s.end()
892 }
893 TuiMessage::ReasoningDelta(delta) => {
894 let mut s = serializer.serialize_struct("msg", 2)?;
895 s.serialize_field("type", "ReasoningDelta")?;
896 s.serialize_field("delta", delta)?;
897 s.end()
898 }
899 TuiMessage::WorkflowCompleted => {
900 let mut s = serializer.serialize_struct("msg", 2)?;
901 s.serialize_field("type", "WorkflowCompleted")?;
902 s.serialize_field("state", "Idle")?;
903 s.end()
904 }
905 TuiMessage::WorkflowError(err) => {
906 let mut s = serializer.serialize_struct("msg", 2)?;
907 s.serialize_field("type", "WorkflowError")?;
908 s.serialize_field("error", err)?;
909 s.end()
910 }
911 TuiMessage::ResumeInfo(info) => match info {
912 Some(ri) => {
913 let mut s = serializer.serialize_struct("msg", 5)?;
914 s.serialize_field("type", "ResumeInfo")?;
915 s.serialize_field("state", "Idle")?;
916 s.serialize_field("session_id", &ri.session_id)?;
917 s.serialize_field("checkpoint_id", &ri.checkpoint_id)?;
918 s.serialize_field("iteration", &ri.iteration)?;
919 s.end()
920 }
921 None => {
922 let mut s = serializer.serialize_struct("msg", 2)?;
923 s.serialize_field("type", "ResumeInfo")?;
924 s.serialize_field("state", "Idle")?;
925 s.end()
926 }
927 },
928 TuiMessage::TodoUpdate(content) => {
929 let mut s = serializer.serialize_struct("msg", 2)?;
930 s.serialize_field("type", "TodoUpdate")?;
931 s.serialize_field("content", content)?;
932 s.end()
933 }
934 TuiMessage::WorkflowCancelled => {
935 let mut s = serializer.serialize_struct("msg", 2)?;
936 s.serialize_field("type", "WorkflowCancelled")?;
937 s.serialize_field("state", "Idle")?;
938 s.end()
939 }
940 TuiMessage::HandoffReady(briefing) => {
941 let mut s = serializer.serialize_struct("msg", 3)?;
942 s.serialize_field("type", "HandoffReady")?;
943 s.serialize_field("state", "Idle")?;
944 s.serialize_field("briefing", briefing)?;
945 s.end()
946 }
947 TuiMessage::SessionRotated { old, new } => {
948 let mut s = serializer.serialize_struct("msg", 3)?;
949 s.serialize_field("type", "SessionRotated")?;
950 s.serialize_field("old", old)?;
951 s.serialize_field("new", new)?;
952 s.end()
953 }
954 TuiMessage::HandoffFailed => {
955 let mut s = serializer.serialize_struct("msg", 2)?;
956 s.serialize_field("type", "HandoffFailed")?;
957 s.serialize_field("state", "Idle")?;
958 s.end()
959 }
960 TuiMessage::ToolPending {
961 tool_name,
962 hint,
963 } => {
964 let mut s = serializer.serialize_struct("msg", 3)?;
965 s.serialize_field("type", "ToolPending")?;
966 s.serialize_field("tool_name", tool_name)?;
967 s.serialize_field("hint", hint)?;
968 s.end()
969 }
970 TuiMessage::ToolDone {
971 tool_name,
972 success,
973 hint,
974 } => {
975 let mut s = serializer.serialize_struct("msg", 4)?;
976 s.serialize_field("type", "ToolDone")?;
977 s.serialize_field("tool_name", tool_name)?;
978 s.serialize_field("success", success)?;
979 s.serialize_field("hint", hint)?;
980 s.end()
981 }
982 TuiMessage::ContextTokensUpdated(count) => {
983 let mut s = serializer.serialize_struct("msg", 2)?;
984 s.serialize_field("type", "ContextTokensUpdated")?;
985 s.serialize_field("count", count)?;
986 s.end()
987 }
988 TuiMessage::McpServerStatus {
989 name,
990 connected,
991 tool_count,
992 error,
993 } => {
994 let mut s = serializer.serialize_struct("msg", 5)?;
995 s.serialize_field("type", "McpServerStatus")?;
996 s.serialize_field("name", name)?;
997 s.serialize_field("connected", connected)?;
998 s.serialize_field("tool_count", tool_count)?;
999 s.serialize_field("error", error)?;
1000 s.end()
1001 }
1002 TuiMessage::SessionTitleUpdated(title) => {
1003 let mut s = serializer.serialize_struct("msg", 2)?;
1004 s.serialize_field("type", "SessionTitleUpdated")?;
1005 s.serialize_field("title", title)?;
1006 s.end()
1007 }
1008 }
1009 }
1010}
1011
1012fn deep_merge_toml(base: &mut toml::Value, overlay: &toml::Value) {
1023 match (base, overlay) {
1024 (toml::Value::Table(base_table), toml::Value::Table(overlay_table)) => {
1025 for (key, overlay_val) in overlay_table {
1026 match base_table.get_mut(key) {
1027 Some(base_val) => {
1028 deep_merge_toml(base_val, overlay_val);
1030 }
1031 None => {
1032 base_table.insert(key.clone(), overlay_val.clone());
1034 }
1035 }
1036 }
1037 }
1038 (base, overlay) => {
1040 *base = overlay.clone();
1041 }
1042 }
1043}
1044
1045fn substitute_env_vars(s: &mut String, secrets: &std::collections::HashMap<String, String>) {
1050 let mut result = String::with_capacity(s.len());
1052 let bytes = s.as_bytes();
1053 let mut i = 0;
1054
1055 while i < bytes.len() {
1056 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
1057 if let Some(end) = s[i + 2..].find('}') {
1059 let var_name = &s[i + 2..i + 2 + end];
1060 if let Some(value) = secrets.get(var_name) {
1062 result.push_str(value);
1063 } else if let Ok(value) = std::env::var(var_name) {
1064 result.push_str(&value);
1065 } else {
1066 result.push_str(&s[i..i + 2 + end + 1]);
1068 }
1069 i = i + 2 + end + 1;
1070 } else {
1071 result.push('$');
1073 i += 1;
1074 }
1075 } else {
1076 result.push(bytes[i] as char);
1077 i += 1;
1078 }
1079 }
1080
1081 *s = result;
1082}