aboutsummaryrefslogtreecommitdiff
path: root/src/pane.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-15 03:58:07 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-15 04:02:14 +0300
commit2c7c8beb079c5fd58a2e53efd2058df050ad9f55 (patch)
tree6756f671dc728d915b1d6ebececa8cf7f70b66fb /src/pane.rs
parent88370e1516182ab519d22a12fa82d5780a40fee6 (diff)
feat: add floating popup panes
refs #1125
Diffstat (limited to 'src/pane.rs')
-rw-r--r--src/pane.rs118
1 files changed, 90 insertions, 28 deletions
diff --git a/src/pane.rs b/src/pane.rs
index 8f71031f..5bb6fe4e 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -65,21 +65,26 @@ fn apply_pane_terminal_env(cmd: &mut CommandBuilder) {
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct PaneLaunchEnv {
extra: Vec<(String, String)>,
- identity: Option<PaneLaunchIdentity>,
+ identity: PaneLaunchIdentity,
}
-#[derive(Debug, Clone, PartialEq, Eq)]
-struct PaneLaunchIdentity {
- workspace_id: String,
- tab_id: String,
- pane_id: String,
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
+enum PaneLaunchIdentity {
+ #[default]
+ Inherit,
+ Managed {
+ workspace_id: String,
+ tab_id: String,
+ pane_id: String,
+ },
+ OmitPane,
}
impl PaneLaunchEnv {
pub(crate) fn from_extra(extra: Vec<(String, String)>) -> Self {
Self {
extra,
- identity: None,
+ identity: PaneLaunchIdentity::Inherit,
}
}
@@ -89,11 +94,16 @@ impl PaneLaunchEnv {
tab_id: String,
pane_id: String,
) -> Self {
- self.identity = Some(PaneLaunchIdentity {
+ self.identity = PaneLaunchIdentity::Managed {
workspace_id,
tab_id,
pane_id,
- });
+ };
+ self
+ }
+
+ pub(crate) fn without_pane_identity(mut self) -> Self {
+ self.identity = PaneLaunchIdentity::OmitPane;
self
}
}
@@ -104,13 +114,20 @@ fn apply_pane_launch_env(cmd: &mut CommandBuilder, launch_env: &PaneLaunchEnv) {
}
cmd.env(crate::HERDR_ENV_VAR, crate::HERDR_ENV_VALUE);
crate::integration::apply_pane_base_env(cmd);
- if let Some(identity) = &launch_env.identity {
- cmd.env(
- crate::integration::HERDR_WORKSPACE_ID_ENV_VAR,
- &identity.workspace_id,
- );
- cmd.env(crate::integration::HERDR_TAB_ID_ENV_VAR, &identity.tab_id);
- cmd.env(crate::integration::HERDR_PANE_ID_ENV_VAR, &identity.pane_id);
+ match &launch_env.identity {
+ PaneLaunchIdentity::Inherit => {}
+ PaneLaunchIdentity::Managed {
+ workspace_id,
+ tab_id,
+ pane_id,
+ } => {
+ cmd.env(crate::integration::HERDR_WORKSPACE_ID_ENV_VAR, workspace_id);
+ cmd.env(crate::integration::HERDR_TAB_ID_ENV_VAR, tab_id);
+ cmd.env(crate::integration::HERDR_PANE_ID_ENV_VAR, pane_id);
+ }
+ PaneLaunchIdentity::OmitPane => {
+ cmd.env_remove(crate::integration::HERDR_PANE_ID_ENV_VAR);
+ }
}
}
@@ -127,6 +144,12 @@ struct SpawnInitialState<'a> {
windows_powershell_prompt_cwd_reporting: bool,
}
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) enum AgentDetection {
+ Enabled,
+ Disabled,
+}
+
fn active_pending_release(
pending_release: &Mutex<Option<PendingAgentRelease>>,
now: std::time::Instant,
@@ -917,7 +940,7 @@ pub struct PaneRuntime {
pending_release: Arc<Mutex<Option<PendingAgentRelease>>>,
preserve_processes_on_drop: bool,
// Task handles for deterministic shutdown
- detect_handle: tokio::task::AbortHandle,
+ detect_handle: Option<tokio::task::AbortHandle>,
}
enum PaneRuntimeIo {
@@ -1061,7 +1084,9 @@ impl Drop for PaneRuntime {
fn drop(&mut self) {
// Abort detection task immediately and terminate the owned session.
// The PTY actor shuts down before the process/session policy runs.
- self.detect_handle.abort();
+ if let Some(handle) = &self.detect_handle {
+ handle.abort();
+ }
self.io.shutdown();
if !self.preserve_processes_on_drop {
shutdown_pane_processes(
@@ -1411,7 +1436,9 @@ fn publish_reported_cwd(
impl PaneRuntime {
pub fn shutdown(mut self) {
- self.detect_handle.abort();
+ if let Some(handle) = self.detect_handle.take() {
+ handle.abort();
+ }
self.io.shutdown();
shutdown_pane_processes(
self.pane_id,
@@ -1435,7 +1462,9 @@ impl PaneRuntime {
"failed to release PTY actor after handoff commit; dropping runtime will still close the actor handle"
);
}
- self.detect_handle.abort();
+ if let Some(handle) = self.detect_handle.take() {
+ handle.abort();
+ }
self.preserve_processes_on_drop = true;
}
@@ -1571,6 +1600,7 @@ impl PaneRuntime {
history_ansi: initial_history_ansi,
windows_powershell_prompt_cwd_reporting,
},
+ AgentDetection::Enabled,
)
}
@@ -1583,6 +1613,7 @@ impl PaneRuntime {
cwd: std::path::PathBuf,
command: &str,
launch_env: &PaneLaunchEnv,
+ agent_detection: AgentDetection,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
events: mpsc::Sender<AppEvent>,
@@ -1605,9 +1636,12 @@ impl PaneRuntime {
cmd,
"failed to spawn command pane",
SpawnInitialState::default(),
+ agent_detection,
)
}
+ // Runtime construction needs to thread PTY size, environment, theme, render hooks, and detection policy together.
+ #[allow(clippy::too_many_arguments)]
pub fn spawn_argv_command(
pane_id: PaneId,
rows: u16,
@@ -1615,6 +1649,7 @@ impl PaneRuntime {
cwd: std::path::PathBuf,
argv: &[String],
launch_env: &PaneLaunchEnv,
+ agent_detection: AgentDetection,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
events: mpsc::Sender<AppEvent>,
@@ -1646,6 +1681,7 @@ impl PaneRuntime {
cmd,
"failed to spawn argv command pane",
SpawnInitialState::default(),
+ agent_detection,
)
}
@@ -1791,10 +1827,12 @@ impl PaneRuntime {
detect_reset_notify,
pending_release,
preserve_processes_on_drop: true,
- detect_handle,
+ detect_handle: Some(detect_handle),
})
}
+ // Runtime construction needs to thread PTY size, environment, theme, render hooks, and detection policy together.
+ #[allow(clippy::too_many_arguments)]
fn spawn_command_builder(
pane_id: PaneId,
rows: u16,
@@ -1807,6 +1845,7 @@ impl PaneRuntime {
cmd: CommandBuilder,
spawn_error_message: &'static str,
initial_state: SpawnInitialState<'_>,
+ agent_detection: AgentDetection,
) -> std::io::Result<Self> {
crate::logging::pane_spawn_started(pane_id.raw(), rows, cols, scrollback_limit_bytes);
@@ -1881,7 +1920,9 @@ impl PaneRuntime {
let shell_pid = child_pid.load(Ordering::Acquire);
let result =
terminal.process_pty_bytes(pane_id, shell_pid, bytes, &response_writer);
- observe_detection_content_change(bytes, &detection_content_seq);
+ if agent_detection == AgentDetection::Enabled {
+ observe_detection_content_change(bytes, &detection_content_seq);
+ }
if result.request_render && !render_dirty.swap(true, Ordering::AcqRel) {
render_notify.notify_one();
}
@@ -1924,7 +1965,9 @@ impl PaneRuntime {
};
// --- Detection task ---
- let (detect_handle, detect_reset_notify, pending_release) = {
+ let (detect_handle, detect_reset_notify, pending_release) = if agent_detection
+ == AgentDetection::Enabled
+ {
use crate::detect;
use std::time::{Duration, Instant};
@@ -2282,7 +2325,13 @@ impl PaneRuntime {
}
}
});
- (handle.abort_handle(), detect_reset_notify, pending_release)
+ (
+ Some(handle.abort_handle()),
+ detect_reset_notify,
+ pending_release,
+ )
+ } else {
+ (None, Arc::new(Notify::new()), Arc::new(Mutex::new(None)))
};
Ok(Self {
@@ -2322,6 +2371,11 @@ impl PaneRuntime {
self.detect_reset_notify.clone()
}
+ #[cfg(test)]
+ pub(crate) fn agent_detection_enabled_for_test(&self) -> bool {
+ self.detect_handle.is_some()
+ }
+
pub fn set_full_lifecycle_authority_active(&self, active: bool) {
let previous = self
.full_lifecycle_authority_active
@@ -2538,6 +2592,14 @@ impl PaneRuntime {
}
pub async fn send_paste(&self, text: String) -> Result<(), mpsc::error::SendError<Bytes>> {
+ self.send_bytes(self.paste_payload(text)).await
+ }
+
+ pub fn try_send_paste(&self, text: String) -> Result<(), mpsc::error::TrySendError<Bytes>> {
+ self.try_send_bytes(self.paste_payload(text))
+ }
+
+ fn paste_payload(&self, text: String) -> Bytes {
let bracketed = self
.input_state()
.map(|state| state.bracketed_paste)
@@ -2547,7 +2609,7 @@ impl PaneRuntime {
} else {
text
};
- self.send_bytes(Bytes::from(payload)).await
+ Bytes::from(payload)
}
pub fn try_send_focus_event(&self, event: crate::ghostty::FocusEvent) -> bool {
@@ -2743,7 +2805,7 @@ impl PaneRuntime {
detect_reset_notify: Arc::new(Notify::new()),
pending_release: Arc::new(Mutex::new(None)),
preserve_processes_on_drop: true,
- detect_handle: tokio::spawn(async {}).abort_handle(),
+ detect_handle: Some(tokio::spawn(async {}).abort_handle()),
},
rx,
)
@@ -3205,7 +3267,7 @@ mod tests {
detect_reset_notify: Arc::new(Notify::new()),
pending_release: Arc::new(Mutex::new(None)),
preserve_processes_on_drop: true,
- detect_handle: tokio::spawn(async {}).abort_handle(),
+ detect_handle: Some(tokio::spawn(async {}).abort_handle()),
};
assert!(runtime.try_send_focus_event(crate::ghostty::FocusEvent::Gained));
@@ -3236,7 +3298,7 @@ mod tests {
detect_reset_notify: Arc::new(Notify::new()),
pending_release: Arc::new(Mutex::new(None)),
preserve_processes_on_drop: true,
- detect_handle: tokio::spawn(async {}).abort_handle(),
+ detect_handle: Some(tokio::spawn(async {}).abort_handle()),
};
assert!(!runtime.try_send_focus_event(crate::ghostty::FocusEvent::Gained));