Skip to main content

atomr_agents_coding_cli_harness/
session.rs

1//! Long-lived interactive sessions.
2//!
3//! One `InteractiveSessionHandle` corresponds to a tmux session that
4//! wraps a single CLI process; the harness fans terminal bytes out
5//! through a broadcast channel and ingests keystrokes / resizes via
6//! mpsc.
7
8use std::sync::Arc;
9
10use parking_lot::RwLock;
11use tokio::sync::{broadcast, mpsc};
12
13use atomr_agents_coding_cli_core::{CliRequest, CliSessionId, CliVendorKind};
14
15/// Frame the harness shows to clients (over WebSocket in the web companion).
16#[derive(Debug, Clone)]
17pub enum SessionEvent {
18    /// Raw PTY bytes (UTF-8 or 8-bit safe — the client renders as ANSI).
19    Bytes(Vec<u8>),
20    /// Process exited.
21    Exited { code: Option<i32> },
22}
23
24/// Frames clients send back.
25#[derive(Debug, Clone)]
26pub enum SessionTransport {
27    Stdin(Vec<u8>),
28    Resize { cols: u16, rows: u16 },
29    Detach,
30}
31
32pub struct InteractiveSessionHandle {
33    pub id: CliSessionId,
34    pub vendor: CliVendorKind,
35    pub tmux_session: String,
36    pub started_at: chrono::DateTime<chrono::Utc>,
37    pub request: CliRequest,
38
39    /// Subscribe for PTY byte broadcast.
40    pub events: broadcast::Sender<SessionEvent>,
41    /// Send keystrokes / resizes / detach into the session.
42    pub input: mpsc::Sender<SessionTransport>,
43
44    /// Whether the session task has signalled completion. Kept here
45    /// so the registry can prune cleanly.
46    pub closed: Arc<parking_lot::Mutex<bool>>,
47}
48
49impl InteractiveSessionHandle {
50    pub fn subscribe(&self) -> broadcast::Receiver<SessionEvent> {
51        self.events.subscribe()
52    }
53
54    pub async fn send_stdin(&self, bytes: Vec<u8>) -> bool {
55        self.input.send(SessionTransport::Stdin(bytes)).await.is_ok()
56    }
57
58    pub async fn resize(&self, cols: u16, rows: u16) -> bool {
59        self.input
60            .send(SessionTransport::Resize { cols, rows })
61            .await
62            .is_ok()
63    }
64
65    pub async fn detach(&self) -> bool {
66        self.input.send(SessionTransport::Detach).await.is_ok()
67    }
68}
69
70/// Concurrent registry of active sessions — shared by the harness and
71/// the web companion.
72#[derive(Default, Clone)]
73pub struct SessionRegistry {
74    inner: Arc<RwLock<Vec<Arc<InteractiveSessionHandle>>>>,
75}
76
77impl SessionRegistry {
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    pub fn insert(&self, h: Arc<InteractiveSessionHandle>) {
83        self.inner.write().push(h);
84    }
85
86    pub fn get(&self, id: &CliSessionId) -> Option<Arc<InteractiveSessionHandle>> {
87        self.inner.read().iter().find(|s| &s.id == id).cloned()
88    }
89
90    pub fn list(&self) -> Vec<Arc<InteractiveSessionHandle>> {
91        self.inner.read().clone()
92    }
93
94    pub fn remove(&self, id: &CliSessionId) {
95        self.inner.write().retain(|s| &s.id != id);
96    }
97
98    pub fn len(&self) -> usize {
99        self.inner.read().len()
100    }
101    pub fn is_empty(&self) -> bool {
102        self.inner.read().is_empty()
103    }
104}