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 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 fn command(&self) -> &Entity<Markdown> {
147 &self.command
148 }
149
150 pub fn working_dir(&self) -> &Option<PathBuf> {
151 &self.working_dir
152 }
153
154 pub fn started_at(&self) -> Instant {
155 self.started_at
156 }
157
158 pub fn output(&self) -> Option<&TerminalOutput> {
159 self.output.as_ref()
160 }
161
162 pub 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}