Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
256 lines
8.2 KiB
Rust
256 lines
8.2 KiB
Rust
use agent_client_protocol::schema as acp;
|
|
use anyhow::Result;
|
|
use futures::{FutureExt as _, future::Shared};
|
|
use gpui::{App, AppContext, AsyncApp, Context, Entity, Task};
|
|
use language::LanguageRegistry;
|
|
use markdown::Markdown;
|
|
use project::Project;
|
|
use std::{
|
|
path::PathBuf,
|
|
process::ExitStatus,
|
|
sync::{
|
|
Arc,
|
|
atomic::{AtomicBool, Ordering},
|
|
},
|
|
time::Instant,
|
|
};
|
|
use task::Shell;
|
|
use util::get_default_system_shell_preferring_bash;
|
|
|
|
pub struct Terminal {
|
|
id: acp::TerminalId,
|
|
command: Entity<Markdown>,
|
|
working_dir: Option<PathBuf>,
|
|
terminal: Entity<terminal::Terminal>,
|
|
started_at: Instant,
|
|
output: Option<TerminalOutput>,
|
|
output_byte_limit: Option<usize>,
|
|
_output_task: Shared<Task<acp::TerminalExitStatus>>,
|
|
/// Flag indicating whether this terminal was stopped by explicit user action
|
|
/// (e.g., clicking the Stop button). This is set before kill() is called
|
|
/// so that code awaiting wait_for_exit() can check it deterministically.
|
|
user_stopped: Arc<AtomicBool>,
|
|
}
|
|
|
|
pub struct TerminalOutput {
|
|
pub ended_at: Instant,
|
|
pub exit_status: Option<ExitStatus>,
|
|
pub content: String,
|
|
pub original_content_len: usize,
|
|
pub content_line_count: usize,
|
|
}
|
|
|
|
impl Terminal {
|
|
pub fn new(
|
|
id: acp::TerminalId,
|
|
command_label: &str,
|
|
working_dir: Option<PathBuf>,
|
|
output_byte_limit: Option<usize>,
|
|
terminal: Entity<terminal::Terminal>,
|
|
language_registry: Arc<LanguageRegistry>,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let command_task = terminal.read(cx).wait_for_completed_task(cx);
|
|
Self {
|
|
id,
|
|
command: cx.new(|cx| {
|
|
Markdown::new(
|
|
format!("```\n{}\n```", command_label).into(),
|
|
Some(language_registry.clone()),
|
|
None,
|
|
cx,
|
|
)
|
|
}),
|
|
working_dir,
|
|
terminal,
|
|
started_at: Instant::now(),
|
|
output: None,
|
|
output_byte_limit,
|
|
user_stopped: Arc::new(AtomicBool::new(false)),
|
|
_output_task: cx
|
|
.spawn(async move |this, cx| {
|
|
let exit_status = command_task.await;
|
|
|
|
this.update(cx, |this, cx| {
|
|
let (content, original_content_len) = this.truncated_output(cx);
|
|
let content_line_count = this.terminal.read(cx).total_lines();
|
|
|
|
this.output = Some(TerminalOutput {
|
|
ended_at: Instant::now(),
|
|
exit_status,
|
|
content,
|
|
original_content_len,
|
|
content_line_count,
|
|
});
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
|
|
let exit_status = exit_status.map(portable_pty::ExitStatus::from);
|
|
|
|
acp::TerminalExitStatus::new()
|
|
.exit_code(exit_status.as_ref().map(|e| e.exit_code()))
|
|
.signal(exit_status.and_then(|e| e.signal().map(ToOwned::to_owned)))
|
|
})
|
|
.shared(),
|
|
}
|
|
}
|
|
|
|
pub fn id(&self) -> &acp::TerminalId {
|
|
&self.id
|
|
}
|
|
|
|
pub fn wait_for_exit(&self) -> Shared<Task<acp::TerminalExitStatus>> {
|
|
self._output_task.clone()
|
|
}
|
|
|
|
pub fn kill(&mut self, cx: &mut App) {
|
|
self.terminal.update(cx, |terminal, _cx| {
|
|
terminal.kill_active_task();
|
|
});
|
|
}
|
|
|
|
/// Marks this terminal as stopped by user action and then kills it.
|
|
/// This should be called when the user explicitly clicks a Stop button.
|
|
pub fn stop_by_user(&mut self, cx: &mut App) {
|
|
self.user_stopped.store(true, Ordering::SeqCst);
|
|
self.kill(cx);
|
|
}
|
|
|
|
/// Returns whether this terminal was stopped by explicit user action.
|
|
pub fn was_stopped_by_user(&self) -> bool {
|
|
self.user_stopped.load(Ordering::SeqCst)
|
|
}
|
|
|
|
pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
|
|
if let Some(output) = self.output.as_ref() {
|
|
let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
|
|
|
|
acp::TerminalOutputResponse::new(
|
|
output.content.clone(),
|
|
output.original_content_len > output.content.len(),
|
|
)
|
|
.exit_status(
|
|
acp::TerminalExitStatus::new()
|
|
.exit_code(exit_status.as_ref().map(|e| e.exit_code()))
|
|
.signal(exit_status.and_then(|e| e.signal().map(ToOwned::to_owned))),
|
|
)
|
|
} else {
|
|
let (current_content, original_len) = self.truncated_output(cx);
|
|
let truncated = current_content.len() < original_len;
|
|
acp::TerminalOutputResponse::new(current_content, truncated)
|
|
}
|
|
}
|
|
|
|
fn truncated_output(&self, cx: &App) -> (String, usize) {
|
|
let terminal = self.terminal.read(cx);
|
|
let mut content = terminal.get_content();
|
|
|
|
let original_content_len = content.len();
|
|
|
|
if let Some(limit) = self.output_byte_limit
|
|
&& content.len() > limit
|
|
{
|
|
let mut end_ix = limit.min(content.len());
|
|
while !content.is_char_boundary(end_ix) {
|
|
end_ix -= 1;
|
|
}
|
|
// Don't truncate mid-line, clear the remainder of the last line
|
|
end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
|
|
content.truncate(end_ix);
|
|
}
|
|
|
|
(content, original_content_len)
|
|
}
|
|
|
|
pub fn command(&self) -> &Entity<Markdown> {
|
|
&self.command
|
|
}
|
|
|
|
pub fn update_command_label(&self, label: &str, cx: &mut App) {
|
|
self.command.update(cx, |command, cx| {
|
|
command.replace(format!("```\n{}\n```", label), cx);
|
|
});
|
|
}
|
|
|
|
pub fn working_dir(&self) -> &Option<PathBuf> {
|
|
&self.working_dir
|
|
}
|
|
|
|
pub fn started_at(&self) -> Instant {
|
|
self.started_at
|
|
}
|
|
|
|
pub fn output(&self) -> Option<&TerminalOutput> {
|
|
self.output.as_ref()
|
|
}
|
|
|
|
pub fn inner(&self) -> &Entity<terminal::Terminal> {
|
|
&self.terminal
|
|
}
|
|
|
|
pub fn to_markdown(&self, cx: &App) -> String {
|
|
format!(
|
|
"Terminal:\n```\n{}\n```\n",
|
|
self.terminal.read(cx).get_content()
|
|
)
|
|
}
|
|
}
|
|
|
|
pub async fn create_terminal_entity(
|
|
command: String,
|
|
args: &[String],
|
|
env_vars: Vec<(String, String)>,
|
|
cwd: Option<PathBuf>,
|
|
project: &Entity<Project>,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<Entity<terminal::Terminal>> {
|
|
let mut env = if let Some(dir) = &cwd {
|
|
project
|
|
.update(cx, |project, cx| {
|
|
project.environment().update(cx, |env, cx| {
|
|
env.directory_environment(dir.clone().into(), cx)
|
|
})
|
|
})
|
|
.await
|
|
.unwrap_or_default()
|
|
} else {
|
|
Default::default()
|
|
};
|
|
|
|
// Disable pagers so agent/terminal commands don't hang behind interactive UIs
|
|
env.insert("PAGER".into(), "".into());
|
|
// Override user core.pager (e.g. delta) which Git prefers over PAGER
|
|
env.insert("GIT_PAGER".into(), "cat".into());
|
|
env.extend(env_vars);
|
|
|
|
// Use remote shell or default system shell, as appropriate
|
|
let shell = project
|
|
.update(cx, |project, cx| {
|
|
project
|
|
.remote_client()
|
|
.and_then(|r| r.read(cx).default_system_shell())
|
|
.map(Shell::Program)
|
|
})
|
|
.unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash()));
|
|
let is_windows = project.read_with(cx, |project, cx| project.path_style(cx).is_windows());
|
|
let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows)
|
|
.redirect_stdin_to_dev_null()
|
|
.build(Some(command.clone()), &args);
|
|
|
|
project
|
|
.update(cx, |project, cx| {
|
|
project.create_terminal_task(
|
|
task::SpawnInTerminal {
|
|
command: Some(task_command),
|
|
args: task_args,
|
|
cwd,
|
|
env,
|
|
..Default::default()
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
.await
|
|
}
|