1use agent_client_protocol as acp;
2use anyhow::Result;
3use feature_flags::{FeatureFlagAppExt, TerminalSandboxFeatureFlag};
4use futures::{FutureExt as _, future::Shared};
5use gpui::{App, AppContext, AsyncApp, Context, Entity, Task};
6use language::LanguageRegistry;
7use markdown::Markdown;
8use project::Project;
9use settings::Settings;
10use std::{
11 path::PathBuf,
12 process::ExitStatus,
13 sync::{
14 Arc,
15 atomic::{AtomicBool, Ordering},
16 },
17 time::Instant,
18};
19use task::Shell;
20use util::get_default_system_shell_preferring_bash;
21
22pub struct Terminal {
23 id: acp::TerminalId,
24 command: Entity<Markdown>,
25 working_dir: Option<PathBuf>,
26 terminal: Entity<terminal::Terminal>,
27 started_at: Instant,
28 output: Option<TerminalOutput>,
29 output_byte_limit: Option<usize>,
30 _output_task: Shared<Task<acp::TerminalExitStatus>>,
31 /// Flag indicating whether this terminal was stopped by explicit user action
32 /// (e.g., clicking the Stop button). This is set before kill() is called
33 /// so that code awaiting wait_for_exit() can check it deterministically.
34 user_stopped: Arc<AtomicBool>,
35}
36
37pub struct TerminalOutput {
38 pub ended_at: Instant,
39 pub exit_status: Option<ExitStatus>,
40 pub content: String,
41 pub original_content_len: usize,
42 pub content_line_count: usize,
43}
44
45impl Terminal {
46 pub fn new(
47 id: acp::TerminalId,
48 command_label: &str,
49 working_dir: Option<PathBuf>,
50 output_byte_limit: Option<usize>,
51 terminal: Entity<terminal::Terminal>,
52 language_registry: Arc<LanguageRegistry>,
53 cx: &mut Context<Self>,
54 ) -> Self {
55 let command_task = terminal.read(cx).wait_for_completed_task(cx);
56 Self {
57 id,
58 command: cx.new(|cx| {
59 Markdown::new(
60 format!("```\n{}\n```", command_label).into(),
61 Some(language_registry.clone()),
62 None,
63 cx,
64 )
65 }),
66 working_dir,
67 terminal,
68 started_at: Instant::now(),
69 output: None,
70 output_byte_limit,
71 user_stopped: Arc::new(AtomicBool::new(false)),
72 _output_task: cx
73 .spawn(async move |this, cx| {
74 let exit_status = command_task.await;
75
76 this.update(cx, |this, cx| {
77 let (content, original_content_len) = this.truncated_output(cx);
78 let content_line_count = this.terminal.read(cx).total_lines();
79
80 this.output = Some(TerminalOutput {
81 ended_at: Instant::now(),
82 exit_status,
83 content,
84 original_content_len,
85 content_line_count,
86 });
87 cx.notify();
88 })
89 .ok();
90
91 let exit_status = exit_status.map(portable_pty::ExitStatus::from);
92
93 acp::TerminalExitStatus::new()
94 .exit_code(exit_status.as_ref().map(|e| e.exit_code()))
95 .signal(exit_status.and_then(|e| e.signal().map(ToOwned::to_owned)))
96 })
97 .shared(),
98 }
99 }
100
101 pub fn id(&self) -> &acp::TerminalId {
102 &self.id
103 }
104
105 pub fn wait_for_exit(&self) -> Shared<Task<acp::TerminalExitStatus>> {
106 self._output_task.clone()
107 }
108
109 pub fn kill(&mut self, cx: &mut App) {
110 self.terminal.update(cx, |terminal, _cx| {
111 terminal.kill_active_task();
112 });
113 }
114
115 /// Marks this terminal as stopped by user action and then kills it.
116 /// This should be called when the user explicitly clicks a Stop button.
117 pub fn stop_by_user(&mut self, cx: &mut App) {
118 self.user_stopped.store(true, Ordering::SeqCst);
119 self.kill(cx);
120 }
121
122 /// Returns whether this terminal was stopped by explicit user action.
123 pub fn was_stopped_by_user(&self) -> bool {
124 self.user_stopped.load(Ordering::SeqCst)
125 }
126
127 pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
128 if let Some(output) = self.output.as_ref() {
129 let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
130
131 acp::TerminalOutputResponse::new(
132 output.content.clone(),
133 output.original_content_len > output.content.len(),
134 )
135 .exit_status(
136 acp::TerminalExitStatus::new()
137 .exit_code(exit_status.as_ref().map(|e| e.exit_code()))
138 .signal(exit_status.and_then(|e| e.signal().map(ToOwned::to_owned))),
139 )
140 } else {
141 let (current_content, original_len) = self.truncated_output(cx);
142 let truncated = current_content.len() < original_len;
143 acp::TerminalOutputResponse::new(current_content, truncated)
144 }
145 }
146
147 fn truncated_output(&self, cx: &App) -> (String, usize) {
148 let terminal = self.terminal.read(cx);
149 let mut content = terminal.get_content();
150
151 let original_content_len = content.len();
152
153 if let Some(limit) = self.output_byte_limit
154 && content.len() > limit
155 {
156 let mut end_ix = limit.min(content.len());
157 while !content.is_char_boundary(end_ix) {
158 end_ix -= 1;
159 }
160 // Don't truncate mid-line, clear the remainder of the last line
161 end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
162 content.truncate(end_ix);
163 }
164
165 (content, original_content_len)
166 }
167
168 pub fn command(&self) -> &Entity<Markdown> {
169 &self.command
170 }
171
172 pub fn update_command_label(&self, label: &str, cx: &mut App) {
173 self.command.update(cx, |command, cx| {
174 command.replace(format!("```\n{}\n```", label), cx);
175 });
176 }
177
178 pub fn working_dir(&self) -> &Option<PathBuf> {
179 &self.working_dir
180 }
181
182 pub fn started_at(&self) -> Instant {
183 self.started_at
184 }
185
186 pub fn output(&self) -> Option<&TerminalOutput> {
187 self.output.as_ref()
188 }
189
190 pub fn inner(&self) -> &Entity<terminal::Terminal> {
191 &self.terminal
192 }
193
194 pub fn to_markdown(&self, cx: &App) -> String {
195 format!(
196 "Terminal:\n```\n{}\n```\n",
197 self.terminal.read(cx).get_content()
198 )
199 }
200}
201
202pub async fn create_terminal_entity(
203 command: String,
204 args: &[String],
205 env_vars: Vec<(String, String)>,
206 cwd: Option<PathBuf>,
207 project: &Entity<Project>,
208 cx: &mut AsyncApp,
209) -> Result<Entity<terminal::Terminal>> {
210 let mut env = if let Some(dir) = &cwd {
211 project
212 .update(cx, |project, cx| {
213 project.environment().update(cx, |env, cx| {
214 env.directory_environment(dir.clone().into(), cx)
215 })
216 })
217 .await
218 .unwrap_or_default()
219 } else {
220 Default::default()
221 };
222
223 // Disable pagers so agent/terminal commands don't hang behind interactive UIs
224 env.insert("PAGER".into(), "".into());
225 // Override user core.pager (e.g. delta) which Git prefers over PAGER
226 env.insert("GIT_PAGER".into(), "cat".into());
227 env.extend(env_vars);
228
229 // Use remote shell or default system shell, as appropriate
230 let shell = project
231 .update(cx, |project, cx| {
232 project
233 .remote_client()
234 .and_then(|r| r.read(cx).default_system_shell())
235 .map(Shell::Program)
236 })
237 .unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash()));
238 let is_windows = project.read_with(cx, |project, cx| project.path_style(cx).is_windows());
239 let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows)
240 .redirect_stdin_to_dev_null()
241 .build(Some(command.clone()), &args);
242
243 // Resolve sandbox config for the agent terminal tool (feature-flagged)
244 let sandbox_config = project.update(cx, |project, cx| {
245 if !cx.has_flag::<TerminalSandboxFeatureFlag>() {
246 return None;
247 }
248 let settings_location = cwd.as_ref().and_then(|cwd| {
249 let path: Arc<std::path::Path> = Arc::from(cwd.as_ref());
250 project
251 .find_worktree(&path, cx)
252 .map(|(worktree, _)| settings::SettingsLocation {
253 worktree_id: worktree.read(cx).id(),
254 path: util::rel_path::RelPath::empty(),
255 })
256 });
257 let settings = terminal::terminal_settings::TerminalSettings::get(settings_location, cx);
258 settings.sandbox.as_ref().and_then(|sandbox| {
259 let project_dir = cwd
260 .clone()
261 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| ".".into()));
262 sandbox::SandboxConfig::resolve_if_enabled(
263 sandbox,
264 settings::SandboxApplyTo::Tool,
265 project_dir,
266 )
267 })
268 });
269
270 project
271 .update(cx, |project, cx| {
272 project.create_terminal_task(
273 task::SpawnInTerminal {
274 command: Some(task_command),
275 args: task_args,
276 cwd,
277 env,
278 ..Default::default()
279 },
280 sandbox_config,
281 cx,
282 )
283 })
284 .await
285}