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}
60
61#[derive(Debug)]
63pub enum SessionError {
64 MaxSessionsReached(usize),
66 NotFound(String),
68 NotIdle(String),
70}
71
72impl std::fmt::Display for SessionError {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 match self {
75 SessionError::MaxSessionsReached(n) => {
76 write!(f, "Maximum {} sessions per user reached", n)
77 }
78 SessionError::NotFound(id) => write!(f, "Session {} not found", id),
79 SessionError::NotIdle(state) => write!(f, "Session is not idle (state: {})", state),
80 }
81 }
82}
83
84impl std::error::Error for SessionError {}
85
86pub type SessionRegistry = Arc<DashMap<String, UserSessions>>;
88
89#[derive(Clone)]
95pub struct ServerState {
96 pub sessions: SessionRegistry,
98 pub ws_tx: broadcast::Sender<String>,
100 pub auth: Option<Arc<AuthState>>,
102 pub config_toml: Option<String>,
104 pub secrets: Option<std::collections::HashMap<String, String>>,
106 pub build_info: Option<trustee_core::types::BuildInfo>,
108 pub workflow_semaphore: Arc<tokio::sync::Semaphore>,
111 pub max_sessions_per_user: usize,
113}
114
115impl ServerState {
116 pub fn new(
118 session: Session,
119 ws_tx: broadcast::Sender<String>,
120 auth: Option<Arc<AuthState>>,
121 ) -> Self {
122 let sessions = Arc::new(DashMap::new());
123
124 let token_store = Arc::new(pep::MemoryTokenStore::new());
126 let (ws_tx_entry, _) = broadcast::channel::<String>(256);
127
128 let now = chrono::Utc::now();
129 let initial_entry = UserSessionEntry {
130 session: Arc::new(Mutex::new(session)),
131 ws_tx: ws_tx_entry,
132 created_at: now,
133 last_active: Arc::new(Mutex::new(now)),
134 };
135
136 let user_sessions = UserSessions {
137 sessions: DashMap::new(),
138 token_store,
139 active_session_id: Mutex::new(String::new()),
140 };
141 user_sessions.sessions.insert("default".to_string(), initial_entry);
142
143 sessions.insert("default".to_string(), user_sessions);
144
145 Self {
146 sessions,
147 ws_tx,
148 auth,
149 config_toml: None,
150 secrets: None,
151 build_info: None,
152 workflow_semaphore: Arc::new(tokio::sync::Semaphore::new(8)),
153 max_sessions_per_user: 4,
154 }
155 }
156
157 pub fn with_config_toml(mut self, config_toml: String) -> Self {
158 self.config_toml = Some(config_toml);
159 self
160 }
161
162 pub fn with_secrets(mut self, secrets: std::collections::HashMap<String, String>) -> Self {
163 self.secrets = Some(secrets);
164 self
165 }
166
167 pub fn with_build_info(mut self, build_info: trustee_core::types::BuildInfo) -> Self {
168 self.build_info = Some(build_info);
169 self
170 }
171
172 pub fn with_max_concurrent_workflows(mut self, max: usize) -> Self {
173 self.workflow_semaphore = Arc::new(tokio::sync::Semaphore::new(max));
174 self
175 }
176
177 pub fn with_max_sessions_per_user(mut self, max: usize) -> Self {
179 self.max_sessions_per_user = max;
180 self
181 }
182
183 pub async fn create_session(
193 &self,
194 user_key: &str,
195 session_name: Option<String>,
196 identity: Option<String>,
197 ) -> Result<String, SessionError> {
198 let user_sessions = self
200 .sessions
201 .entry(user_key.to_string())
202 .or_insert_with(|| UserSessions {
203 sessions: DashMap::new(),
204 token_store: Arc::new(pep::MemoryTokenStore::new()),
205 active_session_id: Mutex::new(String::new()),
206 });
207
208 if user_sessions.sessions.len() >= self.max_sessions_per_user {
210 return Err(SessionError::MaxSessionsReached(self.max_sessions_per_user));
211 }
212
213 let (mut session, workflow_rx) = Session::new();
215
216 if let Some(ref config_toml) = self.config_toml {
218 session.config_toml = Some(config_toml.clone());
219 session.parse_auto_handoff_config();
220 if let Ok(table) = config_toml.parse::<toml::Value>() {
221 if let Some(name) = table
222 .get("agent")
223 .and_then(|a| a.get("name"))
224 .and_then(|n| n.as_str())
225 {
226 session.agent_name = name.to_string();
227 }
228 }
229 }
230
231 session.secrets = self.secrets.clone();
232 session.build_info = self.build_info.clone();
233
234 self.apply_user_isolation(&mut session, user_key);
236
237 session.session_name = session_name;
239
240 session.identity = identity;
242
243 let (ws_tx_entry, _) = broadcast::channel::<String>(256);
245
246 let session_id = format!(
248 "session_{}_{}",
249 chrono::Utc::now().format("%Y_%m_%d_%H_%M"),
250 &uuid::Uuid::new_v4().to_string()[..8]
251 );
252
253 let now = chrono::Utc::now();
254
255 user_sessions.sessions.insert(
257 session_id.clone(),
258 UserSessionEntry {
259 session: Arc::new(Mutex::new(session)),
260 ws_tx: ws_tx_entry.clone(),
261 created_at: now,
262 last_active: Arc::new(Mutex::new(now)),
263 },
264 );
265
266 *user_sessions.active_session_id.lock().await = session_id.clone();
268
269 let session_arc = user_sessions
271 .sessions
272 .get(&session_id)
273 .map(|e| e.session.clone());
274 if let Some(session_arc) = session_arc {
275 self.spawn_user_drain_task(
276 session_id.clone(),
277 session_arc,
278 ws_tx_entry,
279 workflow_rx,
280 );
281 }
282
283 Ok(session_id)
284 }
285
286 pub async fn get_session(
289 &self,
290 user_key: &str,
291 session_id: &str,
292 ) -> Option<(Arc<Mutex<Session>>, broadcast::Sender<String>)> {
293 let user_sessions = self.sessions.get(user_key)?;
294 let entry = user_sessions.sessions.get(session_id)?;
295
296 let now = chrono::Utc::now();
298 *entry.last_active.lock().await = now;
299
300 Some((entry.session.clone(), entry.ws_tx.clone()))
301 }
302
303 pub async fn get_session_by_any_id(
321 &self,
322 user_key: &str,
323 id: &str,
324 ) -> Option<(String, Arc<Mutex<Session>>, broadcast::Sender<String>)> {
325 let user_sessions = self.sessions.get(user_key)?;
327 if let Some(entry) = user_sessions.sessions.get(id) {
328 let now = chrono::Utc::now();
330 *entry.last_active.lock().await = now;
331 return Some((id.to_string(), entry.session.clone(), entry.ws_tx.clone()));
332 }
333
334 for entry in user_sessions.sessions.iter() {
336 let session = entry.session.lock().await;
337 if session.session_id.as_deref() == Some(id) {
338 let key = entry.key().clone();
339 let ws_tx = entry.ws_tx.clone();
340 drop(session);
341 let now = chrono::Utc::now();
343 *entry.last_active.lock().await = now;
344 return Some((key, entry.session.clone(), ws_tx));
345 }
346 }
347
348 None
349 }
350
351 pub async fn list_sessions(&self, user_key: &str) -> Vec<SessionListItem> {
353 let Some(user_sessions) = self.sessions.get(user_key) else {
354 return Vec::new();
355 };
356
357 let mut items = Vec::new();
358 for entry in user_sessions.sessions.iter() {
359 let session = entry.session.lock().await;
360 let workflow_state = match session.workflow_state {
361 trustee_core::types::WorkflowState::Idle => "Idle",
362 trustee_core::types::WorkflowState::Running => "Running",
363 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
364 };
365 let last_active = entry.last_active.lock().await;
366 items.push(SessionListItem {
367 session_id: entry.key().clone(),
368 session_name: session.session_name.clone(),
369 workflow_state: workflow_state.to_string(),
370 created_at: entry.created_at.to_rfc3339(),
371 last_active: last_active.to_rfc3339(),
372 });
373 }
374 drop(user_sessions);
375
376 items.sort_by(|a, b| b.last_active.cmp(&a.last_active));
378 items
379 }
380
381 pub async fn destroy_session(
383 &self,
384 user_key: &str,
385 session_id: &str,
386 ) -> Result<(), SessionError> {
387 let user_sessions = self
388 .sessions
389 .get(user_key)
390 .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
391
392 {
394 let entry = user_sessions
395 .sessions
396 .get(session_id)
397 .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
398 let session = entry.session.lock().await;
399 if session.workflow_state != trustee_core::types::WorkflowState::Idle {
400 let state_str = match session.workflow_state {
401 trustee_core::types::WorkflowState::Running => "Running",
402 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
403 _ => "Unknown",
404 };
405 return Err(SessionError::NotIdle(state_str.to_string()));
406 }
407 }
408
409 user_sessions.sessions.remove(session_id);
411
412 let mut active_id = user_sessions.active_session_id.lock().await;
414 if &*active_id == session_id {
415 let mut newest: Option<(String, chrono::DateTime<chrono::Utc>)> = None;
417 for entry in user_sessions.sessions.iter() {
418 let la = entry.last_active.lock().await;
419 if newest.as_ref().map_or(true, |(_, t)| *la > *t) {
420 newest = Some((entry.key().clone(), *la));
421 }
422 }
423 *active_id = newest.map(|(id, _)| id).unwrap_or_default();
424 }
425
426 Ok(())
427 }
428
429 pub async fn ensure_active_session(
438 &self,
439 user_key: &str,
440 ) -> (
441 String,
442 Arc<Mutex<Session>>,
443 broadcast::Sender<String>,
444 Arc<pep::MemoryTokenStore>,
445 ) {
446 let token_store = {
448 let user_sessions = self
449 .sessions
450 .entry(user_key.to_string())
451 .or_insert_with(|| UserSessions {
452 sessions: DashMap::new(),
453 token_store: Arc::new(pep::MemoryTokenStore::new()),
454 active_session_id: Mutex::new(String::new()),
455 });
456 user_sessions.token_store.clone()
457 };
458
459 let active_id = {
461 let user_sessions = self.sessions.get(user_key).unwrap();
462 let guard = user_sessions.active_session_id.lock().await;
463 guard.clone()
464 };
465
466 if !active_id.is_empty() {
467 if let Some((session, ws_tx)) = self.get_session(user_key, &active_id).await {
468 return (active_id, session, ws_tx, token_store);
469 }
470 }
472
473 let existing_session: Option<(String, Arc<Mutex<Session>>, broadcast::Sender<String>)> = {
477 let user_sessions = self.sessions.get(user_key).unwrap();
478 let result = user_sessions.sessions.iter().next().map(|first| {
479 (
480 first.key().clone(),
481 first.session.clone(),
482 first.ws_tx.clone(),
483 )
484 });
485 result
486 };
487 if let Some((id, session, ws_tx)) = existing_session {
488 let now = chrono::Utc::now();
489 if let Some(entry) = self.sessions.get(user_key) {
490 if let Some(e) = entry.sessions.get(&id) {
491 *e.last_active.lock().await = now;
492 }
493 *entry.active_session_id.lock().await = id.clone();
494 }
495
496 return (id, session, ws_tx, token_store);
497 }
498
499 let session_id = self
501 .create_session(user_key, None, None)
502 .await
503 .unwrap_or_else(|_| "default".to_string());
504
505 let (session, ws_tx) = self
506 .get_session(user_key, &session_id)
507 .await
508 .expect("just-created session must exist");
509
510 (session_id, session, ws_tx, token_store)
511 }
512
513 pub async fn ensure_user_session(
516 &self,
517 user_key: &str,
518 ) -> (Arc<Mutex<Session>>, broadcast::Sender<String>, Arc<pep::MemoryTokenStore>) {
519 let (_id, session, ws_tx, token_store) = self.ensure_active_session(user_key).await;
520 (session, ws_tx, token_store)
521 }
522
523 pub async fn set_active_session(&self, user_key: &str, session_id: &str) {
525 if let Some(user_sessions) = self.sessions.get(user_key) {
526 if user_sessions.sessions.contains_key(session_id) {
527 *user_sessions.active_session_id.lock().await = session_id.to_string();
528 }
529 }
530 }
531
532 pub fn get_user_home_dir(&self, user_key: &str) -> Option<std::path::PathBuf> {
543 use sha2::{Digest, Sha256};
544 let mut hasher = Sha256::new();
545 hasher.update(user_key.as_bytes());
546 let hash_bytes = hasher.finalize();
547 let user_hash = format!(
548 "{:016x}",
549 u64::from_be_bytes(hash_bytes[..8].try_into().unwrap())
550 );
551 dirs::home_dir().map(|home| home.join(".trustee").join("users").join(&user_hash))
552 }
553
554 pub fn get_user_config_and_home(&self, user_key: &str) -> (Option<String>, Option<std::path::PathBuf>) {
559 (self.config_toml.clone(), self.get_user_home_dir(user_key))
560 }
561
562 fn apply_user_isolation(&self, session: &mut Session, user_key: &str) {
568 use sha2::{Digest, Sha256};
569 let mut hasher = Sha256::new();
570 hasher.update(user_key.as_bytes());
571 let hash_bytes = hasher.finalize();
572 let user_hash = format!(
573 "{:016x}",
574 u64::from_be_bytes(hash_bytes[..8].try_into().unwrap())
575 );
576
577 let user_home = if let Some(home) = dirs::home_dir() {
579 let user_home = home.join(".trustee").join("users").join(&user_hash);
580 session.home_dir = Some(user_home.clone());
581 Some(user_home)
582 } else {
583 None
584 };
585
586 session.project_id = Some(format!("web{}", &user_hash[..16]));
587
588 let shared_secrets = session.secrets.clone().unwrap_or_default();
595 let mut merged_secrets = shared_secrets.clone();
596
597 if let Some(ref user_home) = user_home {
598 if let Ok(merged) = self.load_user_secrets(user_home, &merged_secrets) {
599 merged_secrets = merged;
600 }
601 }
602
603 if let Some(ref user_home) = user_home {
605 if let Some(merged) = self.merge_user_config(user_home, session.config_toml.as_deref()) {
606 session.config_toml = Some(merged);
607 tracing::debug!("Merged per-user config into session");
608 }
609 }
610
611 if let Some(ref mut config_toml) = session.config_toml {
613 substitute_env_vars(config_toml, &merged_secrets);
614 }
615
616 session.secrets = Some(shared_secrets);
618 }
619
620 fn load_user_secrets(
623 &self,
624 user_home: &std::path::Path,
625 base: &std::collections::HashMap<String, String>,
626 ) -> std::io::Result<std::collections::HashMap<String, String>> {
627 let user_env_path = user_home.join(".env");
628 if !user_env_path.exists() {
629 return Ok(base.clone());
630 }
631 let content = std::fs::read_to_string(&user_env_path)?;
632 let mut merged = base.clone();
633 for line in content.lines() {
634 let line = line.trim();
635 if line.is_empty() || line.starts_with('#') {
636 continue;
637 }
638 if let Some((key, value)) = line.split_once('=') {
639 let key = key.trim().to_string();
640 let value = value
641 .trim()
642 .trim_matches('"')
643 .trim_matches('\'')
644 .to_string();
645 merged.insert(key, value);
646 }
647 }
648 tracing::debug!("Loaded per-user secrets from {}", user_env_path.display());
649 Ok(merged)
650 }
651
652 fn merge_user_config(
655 &self,
656 user_home: &std::path::Path,
657 shared_config: Option<&str>,
658 ) -> Option<String> {
659 let user_config_path = user_home.join("config").join("trustee.toml");
660 if !user_config_path.exists() {
661 return None;
662 }
663 let user_config_toml = std::fs::read_to_string(&user_config_path).ok()?;
664 let shared = shared_config
665 .unwrap_or("")
666 .parse::<toml::Value>()
667 .ok()?;
668 let overlay = user_config_toml.parse::<toml::Value>().ok()?;
669 let mut shared = shared;
670 deep_merge_toml(&mut shared, &overlay);
671 let merged = toml::to_string(&shared).ok()?;
672 tracing::debug!("Merged per-user config from {}", user_config_path.display());
673 Some(merged)
674 }
675
676 pub fn resolve_user_config(&self, user_key: &str) -> Option<String> {
685 let config_toml = self.config_toml.clone()?;
686
687 let user_home = self.get_user_home_dir(user_key)?;
689
690 let mut merged_secrets = self.secrets.clone().unwrap_or_default();
692 if let Ok(merged) = self.load_user_secrets(&user_home, &merged_secrets) {
693 merged_secrets = merged;
694 }
695
696 let mut resolved = config_toml;
698 if let Some(merged) = self.merge_user_config(&user_home, Some(&resolved)) {
699 resolved = merged;
700 }
701
702 substitute_env_vars(&mut resolved, &merged_secrets);
704
705 Some(resolved)
706 }
707
708 fn spawn_user_drain_task(
710 &self,
711 session_id: String,
712 session: Arc<Mutex<Session>>,
713 ws_tx: broadcast::Sender<String>,
714 mut workflow_rx: mpsc::UnboundedReceiver<TuiMessage>,
715 ) {
716 tokio::spawn(async move {
717 while let Some(msg) = workflow_rx.recv().await {
718 {
719 let mut session = session.lock().await;
720 session.handle_workflow_message(msg.clone());
721
722 let state_str = match session.workflow_state {
723 trustee_core::types::WorkflowState::Idle => "Idle",
724 trustee_core::types::WorkflowState::Running => "Running",
725 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
726 };
727 let state_msg = serde_json::json!({
728 "type": "StateChanged",
729 "state": state_str
730 });
731 let _ = ws_tx.send(state_msg.to_string());
732 }
733
734 let json =
735 serde_json::to_string(&SerializableMessage(&msg)).unwrap_or_default();
736 let _ = ws_tx.send(json);
737 }
738 tracing::debug!("Drain task ended for session: {}", session_id);
739 });
740 }
741
742 pub fn spawn_drain_task(self, mut workflow_rx: mpsc::UnboundedReceiver<TuiMessage>) {
745 let default_user = self
747 .sessions
748 .get("default")
749 .expect("default user must exist");
750 let first_entry = default_user
751 .sessions
752 .iter()
753 .next()
754 .expect("default user must have at least one session");
755 let session = first_entry.session.clone();
756 let ws_tx = first_entry.ws_tx.clone();
757 let session_id = first_entry.key().clone();
758 drop(first_entry);
759 drop(default_user);
760
761 tokio::spawn(async move {
762 while let Some(msg) = workflow_rx.recv().await {
763 {
764 let mut session = session.lock().await;
765 session.handle_workflow_message(msg.clone());
766
767 let state_str = match session.workflow_state {
768 trustee_core::types::WorkflowState::Idle => "Idle",
769 trustee_core::types::WorkflowState::Running => "Running",
770 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
771 };
772 let state_msg = serde_json::json!({
773 "type": "StateChanged",
774 "state": state_str
775 });
776 let _ = ws_tx.send(state_msg.to_string());
777 }
778
779 let json =
780 serde_json::to_string(&SerializableMessage(&msg)).unwrap_or_default();
781 let _ = ws_tx.send(json);
782 }
783 tracing::debug!("Drain task ended for session: {}", session_id);
784 });
785 }
786
787 pub async fn resolve_user_key(&self, headers: &axum::http::HeaderMap) -> String {
789 let Some(ref auth) = self.auth else {
790 return "default".to_string();
791 };
792
793 if let Some(token) = headers
795 .get(axum::http::header::AUTHORIZATION)
796 .and_then(|v| v.to_str().ok())
797 .and_then(|v| v.strip_prefix("Bearer "))
798 .map(|s| s.to_string())
799 {
800 if token.starts_with("dev:") {
801 let parts: Vec<&str> = token.splitn(4, ':').collect();
802 if parts.len() >= 4 {
803 return format!("dev:{}", parts[1]);
804 }
805 }
806 if let Ok(claims) = auth.validate_token(&token).await {
807 return claims.sub;
808 }
809 }
810
811 let cookie_session_id = headers
813 .get(axum::http::header::COOKIE)
814 .and_then(|v| v.to_str().ok())
815 .and_then(|cookies| {
816 cookies
817 .split(';')
818 .map(|c| c.trim())
819 .find_map(|c| {
820 c.strip_prefix(&format!("{}=", auth.config.cookie_name))
821 .map(|s| s.to_string())
822 })
823 });
824
825 if let Some(session_id) = cookie_session_id {
826 if session_id.starts_with("dev:") {
827 let parts: Vec<&str> = session_id.splitn(4, ':').collect();
828 if parts.len() >= 4 {
829 return format!("dev:{}", parts[1]);
830 }
831 }
832
833 if let Ok(access_token) = auth.session_manager.get_token(&session_id).await {
834 if let Ok(claims) = auth.validate_token(&access_token).await {
835 return claims.sub;
836 }
837 }
838 }
839
840 "default".to_string()
841 }
842}
843
844struct SerializableMessage<'a>(&'a TuiMessage);
850
851impl<'a> serde::Serialize for SerializableMessage<'a> {
852 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
853 where
854 S: serde::Serializer,
855 {
856 use serde::ser::SerializeStruct;
857
858 match self.0 {
859 TuiMessage::OutputLine(line) => {
860 let mut s = serializer.serialize_struct("msg", 2)?;
861 s.serialize_field("type", "OutputLine")?;
862 s.serialize_field("line", line)?;
863 s.end()
864 }
865 TuiMessage::StreamDelta(delta) => {
866 let mut s = serializer.serialize_struct("msg", 2)?;
867 s.serialize_field("type", "StreamDelta")?;
868 s.serialize_field("delta", delta)?;
869 s.end()
870 }
871 TuiMessage::ReasoningDelta(delta) => {
872 let mut s = serializer.serialize_struct("msg", 2)?;
873 s.serialize_field("type", "ReasoningDelta")?;
874 s.serialize_field("delta", delta)?;
875 s.end()
876 }
877 TuiMessage::WorkflowCompleted => {
878 let mut s = serializer.serialize_struct("msg", 2)?;
879 s.serialize_field("type", "WorkflowCompleted")?;
880 s.serialize_field("state", "Idle")?;
881 s.end()
882 }
883 TuiMessage::WorkflowError(err) => {
884 let mut s = serializer.serialize_struct("msg", 2)?;
885 s.serialize_field("type", "WorkflowError")?;
886 s.serialize_field("error", err)?;
887 s.end()
888 }
889 TuiMessage::ResumeInfo(info) => match info {
890 Some(ri) => {
891 let mut s = serializer.serialize_struct("msg", 5)?;
892 s.serialize_field("type", "ResumeInfo")?;
893 s.serialize_field("state", "Idle")?;
894 s.serialize_field("session_id", &ri.session_id)?;
895 s.serialize_field("checkpoint_id", &ri.checkpoint_id)?;
896 s.serialize_field("iteration", &ri.iteration)?;
897 s.end()
898 }
899 None => {
900 let mut s = serializer.serialize_struct("msg", 2)?;
901 s.serialize_field("type", "ResumeInfo")?;
902 s.serialize_field("state", "Idle")?;
903 s.end()
904 }
905 },
906 TuiMessage::TodoUpdate(content) => {
907 let mut s = serializer.serialize_struct("msg", 2)?;
908 s.serialize_field("type", "TodoUpdate")?;
909 s.serialize_field("content", content)?;
910 s.end()
911 }
912 TuiMessage::WorkflowCancelled => {
913 let mut s = serializer.serialize_struct("msg", 2)?;
914 s.serialize_field("type", "WorkflowCancelled")?;
915 s.serialize_field("state", "Idle")?;
916 s.end()
917 }
918 TuiMessage::HandoffReady(briefing) => {
919 let mut s = serializer.serialize_struct("msg", 3)?;
920 s.serialize_field("type", "HandoffReady")?;
921 s.serialize_field("state", "Idle")?;
922 s.serialize_field("briefing", briefing)?;
923 s.end()
924 }
925 TuiMessage::HandoffFailed => {
926 let mut s = serializer.serialize_struct("msg", 2)?;
927 s.serialize_field("type", "HandoffFailed")?;
928 s.serialize_field("state", "Idle")?;
929 s.end()
930 }
931 TuiMessage::ToolPending {
932 tool_name,
933 hint,
934 } => {
935 let mut s = serializer.serialize_struct("msg", 3)?;
936 s.serialize_field("type", "ToolPending")?;
937 s.serialize_field("tool_name", tool_name)?;
938 s.serialize_field("hint", hint)?;
939 s.end()
940 }
941 TuiMessage::ToolDone {
942 tool_name,
943 success,
944 hint,
945 } => {
946 let mut s = serializer.serialize_struct("msg", 4)?;
947 s.serialize_field("type", "ToolDone")?;
948 s.serialize_field("tool_name", tool_name)?;
949 s.serialize_field("success", success)?;
950 s.serialize_field("hint", hint)?;
951 s.end()
952 }
953 TuiMessage::ContextTokensUpdated(count) => {
954 let mut s = serializer.serialize_struct("msg", 2)?;
955 s.serialize_field("type", "ContextTokensUpdated")?;
956 s.serialize_field("count", count)?;
957 s.end()
958 }
959 TuiMessage::McpServerStatus {
960 name,
961 connected,
962 tool_count,
963 error,
964 } => {
965 let mut s = serializer.serialize_struct("msg", 5)?;
966 s.serialize_field("type", "McpServerStatus")?;
967 s.serialize_field("name", name)?;
968 s.serialize_field("connected", connected)?;
969 s.serialize_field("tool_count", tool_count)?;
970 s.serialize_field("error", error)?;
971 s.end()
972 }
973 TuiMessage::SessionTitleUpdated(title) => {
974 let mut s = serializer.serialize_struct("msg", 2)?;
975 s.serialize_field("type", "SessionTitleUpdated")?;
976 s.serialize_field("title", title)?;
977 s.end()
978 }
979 }
980 }
981}
982
983fn deep_merge_toml(base: &mut toml::Value, overlay: &toml::Value) {
994 match (base, overlay) {
995 (toml::Value::Table(base_table), toml::Value::Table(overlay_table)) => {
996 for (key, overlay_val) in overlay_table {
997 match base_table.get_mut(key) {
998 Some(base_val) => {
999 deep_merge_toml(base_val, overlay_val);
1001 }
1002 None => {
1003 base_table.insert(key.clone(), overlay_val.clone());
1005 }
1006 }
1007 }
1008 }
1009 (base, overlay) => {
1011 *base = overlay.clone();
1012 }
1013 }
1014}
1015
1016fn substitute_env_vars(s: &mut String, secrets: &std::collections::HashMap<String, String>) {
1021 let mut result = String::with_capacity(s.len());
1023 let bytes = s.as_bytes();
1024 let mut i = 0;
1025
1026 while i < bytes.len() {
1027 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
1028 if let Some(end) = s[i + 2..].find('}') {
1030 let var_name = &s[i + 2..i + 2 + end];
1031 if let Some(value) = secrets.get(var_name) {
1033 result.push_str(value);
1034 } else if let Ok(value) = std::env::var(var_name) {
1035 result.push_str(&value);
1036 } else {
1037 result.push_str(&s[i..i + 2 + end + 1]);
1039 }
1040 i = i + 2 + end + 1;
1041 } else {
1042 result.push('$');
1044 i += 1;
1045 }
1046 } else {
1047 result.push(bytes[i] as char);
1048 i += 1;
1049 }
1050 }
1051
1052 *s = result;
1053}