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 ) -> Result<String, SessionError> {
197 let user_sessions = self
199 .sessions
200 .entry(user_key.to_string())
201 .or_insert_with(|| UserSessions {
202 sessions: DashMap::new(),
203 token_store: Arc::new(pep::MemoryTokenStore::new()),
204 active_session_id: Mutex::new(String::new()),
205 });
206
207 if user_sessions.sessions.len() >= self.max_sessions_per_user {
209 return Err(SessionError::MaxSessionsReached(self.max_sessions_per_user));
210 }
211
212 let (mut session, workflow_rx) = Session::new();
214
215 if let Some(ref config_toml) = self.config_toml {
217 session.config_toml = Some(config_toml.clone());
218 session.parse_auto_handoff_config();
219 if let Ok(table) = config_toml.parse::<toml::Value>() {
220 if let Some(name) = table
221 .get("agent")
222 .and_then(|a| a.get("name"))
223 .and_then(|n| n.as_str())
224 {
225 session.agent_name = name.to_string();
226 }
227 }
228 }
229
230 session.secrets = self.secrets.clone();
231 session.build_info = self.build_info.clone();
232
233 self.apply_user_isolation(&mut session, user_key);
235
236 session.session_name = session_name;
238
239 let (ws_tx_entry, _) = broadcast::channel::<String>(256);
241
242 let session_id = format!(
244 "session_{}_{}",
245 chrono::Utc::now().format("%Y_%m_%d_%H_%M"),
246 &uuid::Uuid::new_v4().to_string()[..8]
247 );
248
249 let now = chrono::Utc::now();
250
251 user_sessions.sessions.insert(
253 session_id.clone(),
254 UserSessionEntry {
255 session: Arc::new(Mutex::new(session)),
256 ws_tx: ws_tx_entry.clone(),
257 created_at: now,
258 last_active: Arc::new(Mutex::new(now)),
259 },
260 );
261
262 *user_sessions.active_session_id.lock().await = session_id.clone();
264
265 let session_arc = user_sessions
267 .sessions
268 .get(&session_id)
269 .map(|e| e.session.clone());
270 if let Some(session_arc) = session_arc {
271 self.spawn_user_drain_task(
272 session_id.clone(),
273 session_arc,
274 ws_tx_entry,
275 workflow_rx,
276 );
277 }
278
279 Ok(session_id)
280 }
281
282 pub async fn get_session(
285 &self,
286 user_key: &str,
287 session_id: &str,
288 ) -> Option<(Arc<Mutex<Session>>, broadcast::Sender<String>)> {
289 let user_sessions = self.sessions.get(user_key)?;
290 let entry = user_sessions.sessions.get(session_id)?;
291
292 let now = chrono::Utc::now();
294 *entry.last_active.lock().await = now;
295
296 Some((entry.session.clone(), entry.ws_tx.clone()))
297 }
298
299 pub async fn get_session_by_any_id(
317 &self,
318 user_key: &str,
319 id: &str,
320 ) -> Option<(String, Arc<Mutex<Session>>, broadcast::Sender<String>)> {
321 let user_sessions = self.sessions.get(user_key)?;
323 if let Some(entry) = user_sessions.sessions.get(id) {
324 let now = chrono::Utc::now();
326 *entry.last_active.lock().await = now;
327 return Some((id.to_string(), entry.session.clone(), entry.ws_tx.clone()));
328 }
329
330 for entry in user_sessions.sessions.iter() {
332 let session = entry.session.lock().await;
333 if session.session_id.as_deref() == Some(id) {
334 let key = entry.key().clone();
335 let ws_tx = entry.ws_tx.clone();
336 drop(session);
337 let now = chrono::Utc::now();
339 *entry.last_active.lock().await = now;
340 return Some((key, entry.session.clone(), ws_tx));
341 }
342 }
343
344 None
345 }
346
347 pub async fn list_sessions(&self, user_key: &str) -> Vec<SessionListItem> {
349 let Some(user_sessions) = self.sessions.get(user_key) else {
350 return Vec::new();
351 };
352
353 let mut items = Vec::new();
354 for entry in user_sessions.sessions.iter() {
355 let session = entry.session.lock().await;
356 let workflow_state = match session.workflow_state {
357 trustee_core::types::WorkflowState::Idle => "Idle",
358 trustee_core::types::WorkflowState::Running => "Running",
359 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
360 };
361 let last_active = entry.last_active.lock().await;
362 items.push(SessionListItem {
363 session_id: entry.key().clone(),
364 session_name: session.session_name.clone(),
365 workflow_state: workflow_state.to_string(),
366 created_at: entry.created_at.to_rfc3339(),
367 last_active: last_active.to_rfc3339(),
368 });
369 }
370 drop(user_sessions);
371
372 items.sort_by(|a, b| b.last_active.cmp(&a.last_active));
374 items
375 }
376
377 pub async fn destroy_session(
379 &self,
380 user_key: &str,
381 session_id: &str,
382 ) -> Result<(), SessionError> {
383 let user_sessions = self
384 .sessions
385 .get(user_key)
386 .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
387
388 {
390 let entry = user_sessions
391 .sessions
392 .get(session_id)
393 .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
394 let session = entry.session.lock().await;
395 if session.workflow_state != trustee_core::types::WorkflowState::Idle {
396 let state_str = match session.workflow_state {
397 trustee_core::types::WorkflowState::Running => "Running",
398 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
399 _ => "Unknown",
400 };
401 return Err(SessionError::NotIdle(state_str.to_string()));
402 }
403 }
404
405 user_sessions.sessions.remove(session_id);
407
408 let mut active_id = user_sessions.active_session_id.lock().await;
410 if &*active_id == session_id {
411 let mut newest: Option<(String, chrono::DateTime<chrono::Utc>)> = None;
413 for entry in user_sessions.sessions.iter() {
414 let la = entry.last_active.lock().await;
415 if newest.as_ref().map_or(true, |(_, t)| *la > *t) {
416 newest = Some((entry.key().clone(), *la));
417 }
418 }
419 *active_id = newest.map(|(id, _)| id).unwrap_or_default();
420 }
421
422 Ok(())
423 }
424
425 pub async fn ensure_active_session(
434 &self,
435 user_key: &str,
436 ) -> (
437 String,
438 Arc<Mutex<Session>>,
439 broadcast::Sender<String>,
440 Arc<pep::MemoryTokenStore>,
441 ) {
442 let token_store = {
444 let user_sessions = self
445 .sessions
446 .entry(user_key.to_string())
447 .or_insert_with(|| UserSessions {
448 sessions: DashMap::new(),
449 token_store: Arc::new(pep::MemoryTokenStore::new()),
450 active_session_id: Mutex::new(String::new()),
451 });
452 user_sessions.token_store.clone()
453 };
454
455 let active_id = {
457 let user_sessions = self.sessions.get(user_key).unwrap();
458 let guard = user_sessions.active_session_id.lock().await;
459 guard.clone()
460 };
461
462 if !active_id.is_empty() {
463 if let Some((session, ws_tx)) = self.get_session(user_key, &active_id).await {
464 return (active_id, session, ws_tx, token_store);
465 }
466 }
468
469 let existing_session: Option<(String, Arc<Mutex<Session>>, broadcast::Sender<String>)> = {
473 let user_sessions = self.sessions.get(user_key).unwrap();
474 let result = user_sessions.sessions.iter().next().map(|first| {
475 (
476 first.key().clone(),
477 first.session.clone(),
478 first.ws_tx.clone(),
479 )
480 });
481 result
482 };
483 if let Some((id, session, ws_tx)) = existing_session {
484 let now = chrono::Utc::now();
485 if let Some(entry) = self.sessions.get(user_key) {
486 if let Some(e) = entry.sessions.get(&id) {
487 *e.last_active.lock().await = now;
488 }
489 *entry.active_session_id.lock().await = id.clone();
490 }
491
492 return (id, session, ws_tx, token_store);
493 }
494
495 let session_id = self
497 .create_session(user_key, None)
498 .await
499 .unwrap_or_else(|_| "default".to_string());
500
501 let (session, ws_tx) = self
502 .get_session(user_key, &session_id)
503 .await
504 .expect("just-created session must exist");
505
506 (session_id, session, ws_tx, token_store)
507 }
508
509 pub async fn ensure_user_session(
512 &self,
513 user_key: &str,
514 ) -> (Arc<Mutex<Session>>, broadcast::Sender<String>, Arc<pep::MemoryTokenStore>) {
515 let (_id, session, ws_tx, token_store) = self.ensure_active_session(user_key).await;
516 (session, ws_tx, token_store)
517 }
518
519 pub async fn set_active_session(&self, user_key: &str, session_id: &str) {
521 if let Some(user_sessions) = self.sessions.get(user_key) {
522 if user_sessions.sessions.contains_key(session_id) {
523 *user_sessions.active_session_id.lock().await = session_id.to_string();
524 }
525 }
526 }
527
528 pub fn get_user_home_dir(&self, user_key: &str) -> Option<std::path::PathBuf> {
539 use sha2::{Digest, Sha256};
540 let mut hasher = Sha256::new();
541 hasher.update(user_key.as_bytes());
542 let hash_bytes = hasher.finalize();
543 let user_hash = format!(
544 "{:016x}",
545 u64::from_be_bytes(hash_bytes[..8].try_into().unwrap())
546 );
547 dirs::home_dir().map(|home| home.join(".trustee").join("users").join(&user_hash))
548 }
549
550 pub fn get_user_config_and_home(&self, user_key: &str) -> (Option<String>, Option<std::path::PathBuf>) {
555 (self.config_toml.clone(), self.get_user_home_dir(user_key))
556 }
557
558 fn apply_user_isolation(&self, session: &mut Session, user_key: &str) {
564 use sha2::{Digest, Sha256};
565 let mut hasher = Sha256::new();
566 hasher.update(user_key.as_bytes());
567 let hash_bytes = hasher.finalize();
568 let user_hash = format!(
569 "{:016x}",
570 u64::from_be_bytes(hash_bytes[..8].try_into().unwrap())
571 );
572
573 let user_home = if let Some(home) = dirs::home_dir() {
575 let user_home = home.join(".trustee").join("users").join(&user_hash);
576 session.home_dir = Some(user_home.clone());
577 Some(user_home)
578 } else {
579 None
580 };
581
582 session.project_id = Some(format!("web{}", &user_hash[..16]));
583
584 let shared_secrets = session.secrets.clone().unwrap_or_default();
591 let mut merged_secrets = shared_secrets.clone();
592
593 if let Some(ref user_home) = user_home {
594 let user_env_path = user_home.join(".env");
595 if user_env_path.exists() {
596 if let Ok(content) = std::fs::read_to_string(&user_env_path) {
597 for line in content.lines() {
598 let line = line.trim();
599 if line.is_empty() || line.starts_with('#') {
600 continue;
601 }
602 if let Some((key, value)) = line.split_once('=') {
603 let key = key.trim().to_string();
604 let value = value.trim()
605 .trim_matches('"')
606 .trim_matches('\'')
607 .to_string();
608 merged_secrets.insert(key, value);
609 }
610 }
611 tracing::debug!(
612 "Loaded {} per-user secrets from {}",
613 merged_secrets.len() - shared_secrets.len(),
614 user_env_path.display()
615 );
616 }
617 }
618 }
619
620 if let Some(ref user_home) = user_home {
626 let user_config_path = user_home.join("config").join("trustee.toml");
627 if user_config_path.exists() {
628 if let Ok(user_config_toml) = std::fs::read_to_string(&user_config_path) {
629 if let (Ok(mut shared), Ok(overlay)) = (
630 session.config_toml.as_ref()
631 .unwrap_or(&String::new())
632 .parse::<toml::Value>(),
633 user_config_toml.parse::<toml::Value>(),
634 ) {
635 deep_merge_toml(&mut shared, &overlay);
636 session.config_toml = toml::to_string(&shared).ok();
637 tracing::debug!("Merged per-user config from {}", user_config_path.display());
638 }
639 }
640 }
641 }
642
643 if let Some(ref mut config_toml) = session.config_toml {
649 substitute_env_vars(config_toml, &merged_secrets);
650 }
651
652 session.secrets = Some(shared_secrets);
659 }
660
661 fn spawn_user_drain_task(
663 &self,
664 session_id: String,
665 session: Arc<Mutex<Session>>,
666 ws_tx: broadcast::Sender<String>,
667 mut workflow_rx: mpsc::UnboundedReceiver<TuiMessage>,
668 ) {
669 tokio::spawn(async move {
670 while let Some(msg) = workflow_rx.recv().await {
671 {
672 let mut session = session.lock().await;
673 session.handle_workflow_message(msg.clone());
674
675 let state_str = match session.workflow_state {
676 trustee_core::types::WorkflowState::Idle => "Idle",
677 trustee_core::types::WorkflowState::Running => "Running",
678 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
679 };
680 let state_msg = serde_json::json!({
681 "type": "StateChanged",
682 "state": state_str
683 });
684 let _ = ws_tx.send(state_msg.to_string());
685 }
686
687 let json =
688 serde_json::to_string(&SerializableMessage(&msg)).unwrap_or_default();
689 let _ = ws_tx.send(json);
690 }
691 tracing::debug!("Drain task ended for session: {}", session_id);
692 });
693 }
694
695 pub fn spawn_drain_task(self, mut workflow_rx: mpsc::UnboundedReceiver<TuiMessage>) {
698 let default_user = self
700 .sessions
701 .get("default")
702 .expect("default user must exist");
703 let first_entry = default_user
704 .sessions
705 .iter()
706 .next()
707 .expect("default user must have at least one session");
708 let session = first_entry.session.clone();
709 let ws_tx = first_entry.ws_tx.clone();
710 let session_id = first_entry.key().clone();
711 drop(first_entry);
712 drop(default_user);
713
714 tokio::spawn(async move {
715 while let Some(msg) = workflow_rx.recv().await {
716 {
717 let mut session = session.lock().await;
718 session.handle_workflow_message(msg.clone());
719
720 let state_str = match session.workflow_state {
721 trustee_core::types::WorkflowState::Idle => "Idle",
722 trustee_core::types::WorkflowState::Running => "Running",
723 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
724 };
725 let state_msg = serde_json::json!({
726 "type": "StateChanged",
727 "state": state_str
728 });
729 let _ = ws_tx.send(state_msg.to_string());
730 }
731
732 let json =
733 serde_json::to_string(&SerializableMessage(&msg)).unwrap_or_default();
734 let _ = ws_tx.send(json);
735 }
736 tracing::debug!("Drain task ended for session: {}", session_id);
737 });
738 }
739
740 pub async fn resolve_user_key(&self, headers: &axum::http::HeaderMap) -> String {
742 let Some(ref auth) = self.auth else {
743 return "default".to_string();
744 };
745
746 if let Some(token) = headers
748 .get(axum::http::header::AUTHORIZATION)
749 .and_then(|v| v.to_str().ok())
750 .and_then(|v| v.strip_prefix("Bearer "))
751 .map(|s| s.to_string())
752 {
753 if token.starts_with("dev:") {
754 let parts: Vec<&str> = token.splitn(4, ':').collect();
755 if parts.len() >= 4 {
756 return format!("dev:{}", parts[1]);
757 }
758 }
759 if let Ok(claims) = auth.validate_token(&token).await {
760 return claims.sub;
761 }
762 }
763
764 let cookie_session_id = headers
766 .get(axum::http::header::COOKIE)
767 .and_then(|v| v.to_str().ok())
768 .and_then(|cookies| {
769 cookies
770 .split(';')
771 .map(|c| c.trim())
772 .find_map(|c| {
773 c.strip_prefix(&format!("{}=", auth.config.cookie_name))
774 .map(|s| s.to_string())
775 })
776 });
777
778 if let Some(session_id) = cookie_session_id {
779 if session_id.starts_with("dev:") {
780 let parts: Vec<&str> = session_id.splitn(4, ':').collect();
781 if parts.len() >= 4 {
782 return format!("dev:{}", parts[1]);
783 }
784 }
785
786 if let Ok(access_token) = auth.session_manager.get_token(&session_id).await {
787 if let Ok(claims) = auth.validate_token(&access_token).await {
788 return claims.sub;
789 }
790 }
791 }
792
793 "default".to_string()
794 }
795}
796
797struct SerializableMessage<'a>(&'a TuiMessage);
803
804impl<'a> serde::Serialize for SerializableMessage<'a> {
805 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
806 where
807 S: serde::Serializer,
808 {
809 use serde::ser::SerializeStruct;
810
811 match self.0 {
812 TuiMessage::OutputLine(line) => {
813 let mut s = serializer.serialize_struct("msg", 2)?;
814 s.serialize_field("type", "OutputLine")?;
815 s.serialize_field("line", line)?;
816 s.end()
817 }
818 TuiMessage::StreamDelta(delta) => {
819 let mut s = serializer.serialize_struct("msg", 2)?;
820 s.serialize_field("type", "StreamDelta")?;
821 s.serialize_field("delta", delta)?;
822 s.end()
823 }
824 TuiMessage::ReasoningDelta(delta) => {
825 let mut s = serializer.serialize_struct("msg", 2)?;
826 s.serialize_field("type", "ReasoningDelta")?;
827 s.serialize_field("delta", delta)?;
828 s.end()
829 }
830 TuiMessage::WorkflowCompleted => {
831 let mut s = serializer.serialize_struct("msg", 2)?;
832 s.serialize_field("type", "WorkflowCompleted")?;
833 s.serialize_field("state", "Idle")?;
834 s.end()
835 }
836 TuiMessage::WorkflowError(err) => {
837 let mut s = serializer.serialize_struct("msg", 2)?;
838 s.serialize_field("type", "WorkflowError")?;
839 s.serialize_field("error", err)?;
840 s.end()
841 }
842 TuiMessage::ResumeInfo(info) => match info {
843 Some(ri) => {
844 let mut s = serializer.serialize_struct("msg", 5)?;
845 s.serialize_field("type", "ResumeInfo")?;
846 s.serialize_field("state", "Idle")?;
847 s.serialize_field("session_id", &ri.session_id)?;
848 s.serialize_field("checkpoint_id", &ri.checkpoint_id)?;
849 s.serialize_field("iteration", &ri.iteration)?;
850 s.end()
851 }
852 None => {
853 let mut s = serializer.serialize_struct("msg", 2)?;
854 s.serialize_field("type", "ResumeInfo")?;
855 s.serialize_field("state", "Idle")?;
856 s.end()
857 }
858 },
859 TuiMessage::TodoUpdate(content) => {
860 let mut s = serializer.serialize_struct("msg", 2)?;
861 s.serialize_field("type", "TodoUpdate")?;
862 s.serialize_field("content", content)?;
863 s.end()
864 }
865 TuiMessage::WorkflowCancelled => {
866 let mut s = serializer.serialize_struct("msg", 2)?;
867 s.serialize_field("type", "WorkflowCancelled")?;
868 s.serialize_field("state", "Idle")?;
869 s.end()
870 }
871 TuiMessage::HandoffReady(briefing) => {
872 let mut s = serializer.serialize_struct("msg", 3)?;
873 s.serialize_field("type", "HandoffReady")?;
874 s.serialize_field("state", "Idle")?;
875 s.serialize_field("briefing", briefing)?;
876 s.end()
877 }
878 TuiMessage::HandoffFailed => {
879 let mut s = serializer.serialize_struct("msg", 2)?;
880 s.serialize_field("type", "HandoffFailed")?;
881 s.serialize_field("state", "Idle")?;
882 s.end()
883 }
884 TuiMessage::ToolPending {
885 tool_name,
886 hint,
887 } => {
888 let mut s = serializer.serialize_struct("msg", 3)?;
889 s.serialize_field("type", "ToolPending")?;
890 s.serialize_field("tool_name", tool_name)?;
891 s.serialize_field("hint", hint)?;
892 s.end()
893 }
894 TuiMessage::ToolDone {
895 tool_name,
896 success,
897 hint,
898 } => {
899 let mut s = serializer.serialize_struct("msg", 4)?;
900 s.serialize_field("type", "ToolDone")?;
901 s.serialize_field("tool_name", tool_name)?;
902 s.serialize_field("success", success)?;
903 s.serialize_field("hint", hint)?;
904 s.end()
905 }
906 TuiMessage::ContextTokensUpdated(count) => {
907 let mut s = serializer.serialize_struct("msg", 2)?;
908 s.serialize_field("type", "ContextTokensUpdated")?;
909 s.serialize_field("count", count)?;
910 s.end()
911 }
912 TuiMessage::McpServerStatus {
913 name,
914 connected,
915 tool_count,
916 error,
917 } => {
918 let mut s = serializer.serialize_struct("msg", 5)?;
919 s.serialize_field("type", "McpServerStatus")?;
920 s.serialize_field("name", name)?;
921 s.serialize_field("connected", connected)?;
922 s.serialize_field("tool_count", tool_count)?;
923 s.serialize_field("error", error)?;
924 s.end()
925 }
926 TuiMessage::SessionTitleUpdated(title) => {
927 let mut s = serializer.serialize_struct("msg", 2)?;
928 s.serialize_field("type", "SessionTitleUpdated")?;
929 s.serialize_field("title", title)?;
930 s.end()
931 }
932 }
933 }
934}
935
936fn deep_merge_toml(base: &mut toml::Value, overlay: &toml::Value) {
947 match (base, overlay) {
948 (toml::Value::Table(base_table), toml::Value::Table(overlay_table)) => {
949 for (key, overlay_val) in overlay_table {
950 match base_table.get_mut(key) {
951 Some(base_val) => {
952 deep_merge_toml(base_val, overlay_val);
954 }
955 None => {
956 base_table.insert(key.clone(), overlay_val.clone());
958 }
959 }
960 }
961 }
962 (base, overlay) => {
964 *base = overlay.clone();
965 }
966 }
967}
968
969fn substitute_env_vars(s: &mut String, secrets: &std::collections::HashMap<String, String>) {
974 let mut result = String::with_capacity(s.len());
976 let bytes = s.as_bytes();
977 let mut i = 0;
978
979 while i < bytes.len() {
980 if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'{' {
981 if let Some(end) = s[i + 2..].find('}') {
983 let var_name = &s[i + 2..i + 2 + end];
984 if let Some(value) = secrets.get(var_name) {
986 result.push_str(value);
987 } else if let Ok(value) = std::env::var(var_name) {
988 result.push_str(&value);
989 } else {
990 result.push_str(&s[i..i + 2 + end + 1]);
992 }
993 i = i + 2 + end + 1;
994 } else {
995 result.push('$');
997 i += 1;
998 }
999 } else {
1000 result.push(bytes[i] as char);
1001 i += 1;
1002 }
1003 }
1004
1005 *s = result;
1006}