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: String,
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).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 }
79 })
80 .shared(),
81 }
82 }
83
84 pub fn id(&self) -> &acp::TerminalId {
85 &self.id
86 }
87
88 pub fn wait_for_exit(&self) -> Shared<Task<acp::TerminalExitStatus>> {
89 self._output_task.clone()
90 }
91
92 pub fn kill(&mut self, cx: &mut App) {
93 self.terminal.update(cx, |terminal, _cx| {
94 terminal.kill_active_task();
95 });
96 }
97
98 pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
99 if let Some(output) = self.output.as_ref() {
100 let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
101
102 acp::TerminalOutputResponse {
103 output: output.content.clone(),
104 truncated: output.original_content_len > output.content.len(),
105 exit_status: Some(acp::TerminalExitStatus {
106 exit_code: exit_status.as_ref().map(|e| e.exit_code()),
107 signal: exit_status.and_then(|e| e.signal().map(Into::into)),
108 }),
109 }
110 } else {
111 let (current_content, original_len) = self.truncated_output(cx);
112
113 acp::TerminalOutputResponse {
114 truncated: current_content.len() < original_len,
115 output: current_content,
116 exit_status: None,
117 }
118 }
119 }
120
121 fn truncated_output(&self, cx: &App) -> (String, usize) {
122 let terminal = self.terminal.read(cx);
123 let mut content = terminal.get_content();
124
125 let original_content_len = content.len();
126
127 if let Some(limit) = self.output_byte_limit
128 && content.len() > limit
129 {
130 let mut end_ix = limit.min(content.len());
131 while !content.is_char_boundary(end_ix) {
132 end_ix -= 1;
133 }
134 // Don't truncate mid-line, clear the remainder of the last line
135 end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
136 content.truncate(end_ix);
137 }
138
139 (content, original_content_len)
140 }
141
142 pub fn command(&self) -> &Entity<Markdown> {
143 &self.command
144 }
145
146 pub fn working_dir(&self) -> &Option<PathBuf> {
147 &self.working_dir
148 }
149
150 pub fn started_at(&self) -> Instant {
151 self.started_at
152 }
153
154 pub fn output(&self) -> Option<&TerminalOutput> {
155 self.output.as_ref()
156 }
157
158 pub fn inner(&self) -> &Entity<terminal::Terminal> {
159 &self.terminal
160 }
161
162 pub fn to_markdown(&self, cx: &App) -> String {
163 format!(
164 "Terminal:\n```\n{}\n```\n",
165 self.terminal.read(cx).get_content()
166 )
167 }
168}