Skip to main content

vtcode_core/tools/exec_session/
record.rs

1//! Execution backends: launch modes, completion commands, session records.
2
3use super::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ExecSessionBackend {
7    Pipe,
8    Pty,
9}
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub(crate) enum ExecSessionLaunchMode {
13    Foreground,
14    UserBackground,
15    ManagedBackground,
16}
17
18impl ExecSessionLaunchMode {
19    pub(crate) fn is_background(self) -> bool {
20        !matches!(self, Self::Foreground)
21    }
22
23    pub(crate) fn reserves_background_slot(self) -> bool {
24        matches!(self, Self::UserBackground)
25    }
26
27    fn shows_in_background_drawer(self) -> bool {
28        matches!(self, Self::UserBackground)
29    }
30
31    pub(crate) fn sets_foreground_session(self) -> bool {
32        matches!(self, Self::Foreground)
33    }
34}
35
36pub(crate) fn bounded_completion_command(command: &str) -> String {
37    let without_controls: String = command
38        .chars()
39        .map(|character| if character.is_control() { ' ' } else { character })
40        .collect();
41    let collapsed = vtcode_commons::formatting::collapse_whitespace(&without_controls);
42    let redacted = vtcode_commons::sanitizer::redact_secrets(collapsed);
43    if redacted.len() <= EXEC_SESSION_COMPLETION_COMMAND_MAX_BYTES {
44        return redacted;
45    }
46    vtcode_commons::formatting::truncate_byte_budget(
47        &redacted,
48        EXEC_SESSION_COMPLETION_COMMAND_MAX_BYTES.saturating_sub(3),
49        "...",
50    )
51}
52
53/// Bounded data used by the Local Agents drawer for one raw command session.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct ExecSessionUiSnapshot {
56    pub metadata: VTCodeExecSession,
57    pub preview: String,
58    pub termination_requested: bool,
59    pub updated_at: chrono::DateTime<Utc>,
60}
61
62/// Notification emitted after a background process has been confirmed exited.
63///
64/// The watcher owns the terminal transition, so a record emits at most one
65/// notification even if multiple lifecycle checks observe the same exit.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct ExecSessionCompletionEvent {
68    /// Stable exec-session identity.
69    pub session_id: ExecSessionId,
70    /// Shell command associated with the session.
71    pub command: String,
72    /// Whether the session belongs to the managed subagent background flow.
73    /// User-launched background exec sessions are delivered directly to the
74    /// parent run loop; managed sessions are forwarded by their controller.
75    pub managed_background: bool,
76    /// Whether the runtime intentionally terminated the session.
77    pub termination_requested: bool,
78    /// Confirmed process exit status.
79    pub exit_code: i32,
80}
81
82pub(crate) struct ExecSessionRecord {
83    pub(crate) metadata: VTCodeExecSession,
84    pub(crate) backend: ExecSessionBackend,
85    _pty_guard: Option<PtySessionGuard>,
86    pub(crate) foreground_pty_counter: ParkingMutex<Option<Arc<AtomicUsize>>>,
87    pub(crate) background: AtomicBool,
88    pub(crate) background_promoted_from_foreground: AtomicBool,
89    pub(crate) show_in_background_drawer: AtomicBool,
90    pub(crate) background_slot_reserved: AtomicBool,
91    pub(crate) background_completion_published: AtomicBool,
92    pub(crate) termination_requested: AtomicBool,
93    pub(crate) completed_at: ParkingMutex<Option<chrono::DateTime<Utc>>>,
94    preview: ParkingMutex<SessionPreviewState>,
95    pub(crate) output_read_lock: Mutex<()>,
96    pub(crate) background_watch: ParkingMutex<Option<JoinHandle<()>>>,
97    pub(crate) foreground_watch: ParkingMutex<Option<JoinHandle<()>>>,
98}
99
100impl ExecSessionRecord {
101    pub(crate) fn new(
102        metadata: VTCodeExecSession,
103        backend: ExecSessionBackend,
104        pty_guard: Option<PtySessionGuard>,
105        launch_mode: ExecSessionLaunchMode,
106        background_slot_reserved: bool,
107    ) -> Self {
108        Self {
109            metadata,
110            backend,
111            _pty_guard: pty_guard,
112            foreground_pty_counter: ParkingMutex::new(None),
113            background: AtomicBool::new(launch_mode.is_background()),
114            background_promoted_from_foreground: AtomicBool::new(false),
115            show_in_background_drawer: AtomicBool::new(launch_mode.shows_in_background_drawer()),
116            background_slot_reserved: AtomicBool::new(background_slot_reserved),
117            background_completion_published: AtomicBool::new(false),
118            termination_requested: AtomicBool::new(false),
119            completed_at: ParkingMutex::new(None),
120            preview: ParkingMutex::new(SessionPreviewState::default()),
121            output_read_lock: Mutex::new(()),
122            background_watch: ParkingMutex::new(None),
123            foreground_watch: ParkingMutex::new(None),
124        }
125    }
126
127    pub(crate) fn remember_output(&self, output: Option<&str>, drain: bool) {
128        let Some(output) = output else {
129            return;
130        };
131
132        let mut preview = self.preview.lock();
133        if drain {
134            preview.retained.append(output);
135            preview.pending = None;
136        } else {
137            preview.pending = Some(output.to_string());
138        }
139    }
140
141    pub(crate) fn preview(&self) -> String {
142        let preview = self.preview.lock();
143        let mut retained = preview.retained.clone();
144        if let Some(pending) = preview.pending.as_deref() {
145            retained.append(pending);
146        }
147        retained.render()
148    }
149}