1use agent_client_protocol as acp;
  2
  3use futures::{FutureExt as _, future::Shared};
  4use gpui::{App, AppContext, Context, Entity, Task};
  5use language::LanguageRegistry;
  6use markdown::Markdown;
  7use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant};
  8
  9pub struct Terminal {
 10    id: acp::TerminalId,
 11    command: Entity<Markdown>,
 12    working_dir: Option<PathBuf>,
 13    terminal: Entity<terminal::Terminal>,
 14    started_at: Instant,
 15    output: Option<TerminalOutput>,
 16    output_byte_limit: Option<usize>,
 17    _output_task: Shared<Task<acp::TerminalExitStatus>>,
 18}
 19
 20pub struct TerminalOutput {
 21    pub ended_at: Instant,
 22    pub exit_status: Option<ExitStatus>,
 23    pub content: String,
 24    pub original_content_len: usize,
 25    pub content_line_count: usize,
 26}
 27
 28impl Terminal {
 29    pub fn new(
 30        id: acp::TerminalId,
 31        command_label: &str,
 32        working_dir: Option<PathBuf>,
 33        output_byte_limit: Option<usize>,
 34        terminal: Entity<terminal::Terminal>,
 35        language_registry: Arc<LanguageRegistry>,
 36        cx: &mut Context<Self>,
 37    ) -> Self {
 38        let command_task = terminal.read(cx).wait_for_completed_task(cx);
 39        Self {
 40            id,
 41            command: cx.new(|cx| {
 42                Markdown::new(
 43                    format!("```\n{}\n```", command_label).into(),
 44                    Some(language_registry.clone()),
 45                    None,
 46                    cx,
 47                )
 48            }),
 49            working_dir,
 50            terminal,
 51            started_at: Instant::now(),
 52            output: None,
 53            output_byte_limit,
 54            _output_task: cx
 55                .spawn(async move |this, cx| {
 56                    let exit_status = command_task.await;
 57
 58                    this.update(cx, |this, cx| {
 59                        let (content, original_content_len) = this.truncated_output(cx);
 60                        let content_line_count = this.terminal.read(cx).total_lines();
 61
 62                        this.output = Some(TerminalOutput {
 63                            ended_at: Instant::now(),
 64                            exit_status,
 65                            content,
 66                            original_content_len,
 67                            content_line_count,
 68                        });
 69                        cx.notify();
 70                    })
 71                    .ok();
 72
 73                    let exit_status = exit_status.map(portable_pty::ExitStatus::from);
 74
 75                    acp::TerminalExitStatus {
 76                        exit_code: exit_status.as_ref().map(|e| e.exit_code()),
 77                        signal: exit_status.and_then(|e| e.signal().map(Into::into)),
 78                        meta: None,
 79                    }
 80                })
 81                .shared(),
 82        }
 83    }
 84
 85    pub const fn id(&self) -> &acp::TerminalId {
 86        &self.id
 87    }
 88
 89    pub fn wait_for_exit(&self) -> Shared<Task<acp::TerminalExitStatus>> {
 90        self._output_task.clone()
 91    }
 92
 93    pub fn kill(&mut self, cx: &mut App) {
 94        self.terminal.update(cx, |terminal, _cx| {
 95            terminal.kill_active_task();
 96        });
 97    }
 98
 99    pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
100        if let Some(output) = self.output.as_ref() {
101            let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
102
103            acp::TerminalOutputResponse {
104                output: output.content.clone(),
105                truncated: output.original_content_len > output.content.len(),
106                exit_status: Some(acp::TerminalExitStatus {
107                    exit_code: exit_status.as_ref().map(|e| e.exit_code()),
108                    signal: exit_status.and_then(|e| e.signal().map(Into::into)),
109                    meta: None,
110                }),
111                meta: None,
112            }
113        } else {
114            let (current_content, original_len) = self.truncated_output(cx);
115
116            acp::TerminalOutputResponse {
117                truncated: current_content.len() < original_len,
118                output: current_content,
119                exit_status: None,
120                meta: None,
121            }
122        }
123    }
124
125    fn truncated_output(&self, cx: &App) -> (String, usize) {
126        let terminal = self.terminal.read(cx);
127        let mut content = terminal.get_content();
128
129        let original_content_len = content.len();
130
131        if let Some(limit) = self.output_byte_limit
132            && content.len() > limit
133        {
134            let mut end_ix = limit.min(content.len());
135            while !content.is_char_boundary(end_ix) {
136                end_ix -= 1;
137            }
138            // Don't truncate mid-line, clear the remainder of the last line
139            end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
140            content.truncate(end_ix);
141        }
142
143        (content, original_content_len)
144    }
145
146    pub const fn command(&self) -> &Entity<Markdown> {
147        &self.command
148    }
149
150    pub const fn working_dir(&self) -> &Option<PathBuf> {
151        &self.working_dir
152    }
153
154    pub const fn started_at(&self) -> Instant {
155        self.started_at
156    }
157
158    pub const fn output(&self) -> Option<&TerminalOutput> {
159        self.output.as_ref()
160    }
161
162    pub const fn inner(&self) -> &Entity<terminal::Terminal> {
163        &self.terminal
164    }
165
166    pub fn to_markdown(&self, cx: &App) -> String {
167        format!(
168            "Terminal:\n```\n{}\n```\n",
169            self.terminal.read(cx).get_content()
170        )
171    }
172}