Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+528
View File
@@ -0,0 +1,528 @@
use crate::server::telemetry::ImageProtocol;
use crate::terminal::model::session::Sessions;
use crate::terminal::event::{
AfterBlockCompletedEvent, BlockCompletedEvent, BlockMetadataReceivedEvent, Event,
ExecutedExecutorCommandEvent, InitSshEvent, InitSubshellEvent, SourcedRcFileInSubshellEvent,
TerminalMode,
};
use crate::terminal::ClipboardType;
use async_channel::Receiver;
use instant::Instant;
use std::sync::Arc;
use crate::remote_server::manager::RemoteServerManager;
use warpui::SingletonEntity;
use warpui::{Entity, ModelContext, ModelHandle};
use super::event::SshLoginStatus;
use super::model::ansi::{FinishUpdateValue, WarpificationUnavailableReason};
use super::model::block::BlockId;
use super::model::completions::ShellCompletion;
use super::model::terminal_model::{ExitReason, TmuxControlModeContext, TmuxInstallationState};
use super::model::tmux::commands::TmuxCommand;
use super::{
event::BootstrappedEvent,
model::{
ansi,
session::{IsLegacySSHSession, SessionId, SessionInfo},
terminal_model::{CommandType, HandlerEvent},
},
};
use crate::features::FeatureFlag;
use crate::terminal::shell::ShellType;
use crate::{send_telemetry_from_ctx, TelemetryEvent};
/// Model that dispatches events that have been emitted by the [`crate::terminal::TerminalModel`],
/// allowing other models/views to subscribe to `TerminalModel` events like it would any other
/// entity within the UI framework.
pub struct ModelEventDispatcher {
last_start_prompt_marker: Option<PromptKind>,
active_session_id: Option<SessionId>,
sessions: ModelHandle<Sessions>,
}
impl ModelEventDispatcher {
pub fn new(
model_events_rx: Receiver<Event>,
sessions: ModelHandle<Sessions>,
ctx: &mut ModelContext<Self>,
) -> Self {
ctx.spawn_stream_local(
model_events_rx,
Self::handle_terminal_model_event,
|_, _| (),
);
Self {
active_session_id: None,
last_start_prompt_marker: None,
sessions,
}
}
/// Returns the active session to which the PTY is currently attached.
///
/// The active session is the session corresponding to the session ID included in the most
/// recent Precmd payload.
pub fn active_session_id(&self) -> Option<SessionId> {
self.active_session_id
}
/// Sets the active session ID directly, for use in unit tests where there's no `Precmd` event.
#[cfg(test)]
pub fn set_active_session_id(&mut self, session_id: SessionId) {
self.active_session_id = Some(session_id);
}
/// Emits the corresponding `ModelEvent` for the received `HandlerEvent` emitted by
/// `TerminalModel` when some `ansi::Handler` method is called.
fn handle_terminal_model_event(&mut self, event: Event, ctx: &mut ModelContext<Self>) {
let event_to_emit = match event {
Event::Handler(HandlerEvent::InitShell {
pending_session_info,
}) => {
self.sessions.update(ctx, |sessions, ctx| {
sessions.register_pending_session(pending_session_info.as_ref(), ctx);
});
let is_legacy_ssh = matches!(
pending_session_info.is_legacy_ssh_session,
IsLegacySSHSession::Yes { .. }
);
if FeatureFlag::SshRemoteServer.is_enabled() && is_legacy_ssh {
ModelEvent::SshInitShell {
pending_session_info,
}
} else {
ModelEvent::Handler(AnsiHandlerEvent::InitShell {
pending_session_info,
})
}
}
Event::Handler(HandlerEvent::Bootstrapped(bootstrapped_event)) => {
let session_id = bootstrapped_event.session_info.session_id;
let is_subshell = bootstrapped_event.session_info.subshell_info.is_some();
// Always initialize the session synchronously. When the
// `SshRemoteServer` flag is enabled, the remote-server client
// is wired up independently: `Sessions::new` subscribes to
// `RemoteServerManagerEvent::SessionConnected` and attaches the
// client to the session's `RemoteServerCommandExecutor` when
// the connection lands, so it's safe to initialize the session
// before the remote server finishes connecting.
self.complete_bootstrapped_session(bootstrapped_event, ctx);
ModelEvent::Handler(AnsiHandlerEvent::Bootstrapped {
session_id,
is_subshell,
})
}
Event::RemoteServerSetupStateChanged { session_id, state } => {
self.sessions.update(ctx, |sessions, ctx| {
sessions.set_remote_server_setup_state(session_id, state.clone());
ctx.notify();
});
return;
}
Event::RemoteServerReady { session_id } => {
log::info!("Remote server ready for session {session_id:?}");
return;
}
Event::RemoteServerFailed { session_id, error } => {
log::warn!(
"Remote server setup failed for session {session_id:?}, falling back to \
ControlMaster: {error}"
);
return;
}
Event::Handler(HandlerEvent::PromptStart) => {
self.last_start_prompt_marker = Some(PromptKind::Left);
ModelEvent::Handler(AnsiHandlerEvent::StartPrompt)
}
Event::Handler(HandlerEvent::RPromptStart) => {
self.last_start_prompt_marker = Some(PromptKind::Right);
ModelEvent::Handler(AnsiHandlerEvent::StartRPrompt)
}
Event::Handler(HandlerEvent::PromptEnd) => match self.last_start_prompt_marker.take() {
None | Some(PromptKind::Left) => ModelEvent::Handler(AnsiHandlerEvent::EndPrompt),
Some(PromptKind::Right) => ModelEvent::Handler(AnsiHandlerEvent::EndRPrompt),
},
Event::Handler(HandlerEvent::Precmd {
session_id,
handled_after_inband,
env_vars,
}) => {
// Update the active session to the one that corresponds to the received SessionId.
self.active_session_id = session_id;
// Update the active session's environment variables
if let Some(session_id) = session_id {
// We set the environment variables here, which triggers the prompt to refresh
// as certain chips depend on environment variables. We specifically want to
// avoid triggering prompt refreshes for in-band commands because otherwise we'll
// create a loop if updating the prompt involves running an in-band command.
// This is similar to how we handle ModelEvent::AfterBlockCompleted in terminal_view.rs
if !handled_after_inband {
self.sessions.update(ctx, |sessions, ctx| {
sessions.set_env_vars_for_session(session_id, env_vars, ctx)
});
}
}
ModelEvent::Handler(AnsiHandlerEvent::Precmd)
}
Event::Handler(HandlerEvent::Preexec) => ModelEvent::Handler(AnsiHandlerEvent::Preexec),
Event::Handler(HandlerEvent::CommandFinished { command_type }) => match command_type {
CommandType::InBandCommand => {
ModelEvent::Handler(AnsiHandlerEvent::InBandCommandFinished)
}
CommandType::User => ModelEvent::Handler(AnsiHandlerEvent::UserCommandFinished),
_ => return,
},
Event::Handler(HandlerEvent::SetMode {
mode: ansi::Mode::BracketedPaste,
}) => ModelEvent::Handler(AnsiHandlerEvent::SetBracketedPaste),
Event::Handler(HandlerEvent::UnsetMode {
mode: ansi::Mode::BracketedPaste,
}) => ModelEvent::Handler(AnsiHandlerEvent::UnsetBracketedPaste),
Event::Handler(HandlerEvent::StartTmuxControlMode) => {
ModelEvent::Handler(AnsiHandlerEvent::StartTmuxControlMode)
}
Event::Handler(HandlerEvent::EndTmuxControlMode) => {
ModelEvent::Handler(AnsiHandlerEvent::EndTmuxControlMode)
}
Event::Handler(HandlerEvent::TmuxControlModeReady {
primary_pane,
context,
}) => {
{
if let Some(TmuxControlModeContext::WarpInitiatedForSsh(control_mode)) = context
{
let duration_ms = Instant::now()
.duration_since(control_mode.start_time)
.as_millis()
// Clip large durations to u64::MAX
.min(u64::MAX as u128) as u64;
send_telemetry_from_ctx!(
TelemetryEvent::SshTmuxWarpificationSuccess {
duration_ms,
tmux_installation: control_mode.tmux_installation,
},
ctx
);
}
}
ModelEvent::Handler(AnsiHandlerEvent::TmuxControlModeReady { primary_pane })
}
Event::Handler(HandlerEvent::RunTmuxCommand(command)) => {
ModelEvent::Handler(AnsiHandlerEvent::RunTmuxCommand(command))
}
Event::CompletionsFinished(res) => ModelEvent::CompletionsFinished(res),
Event::MouseCursorDirty => ModelEvent::MouseCursorDirty,
Event::Title(title) => ModelEvent::Title(title),
Event::VisibleBootstrapBlock => ModelEvent::VisibleBootstrapBlock,
Event::BlockCompleted(block_completed_event) => {
ModelEvent::BlockCompleted(block_completed_event)
}
Event::AfterBlockCompleted(after_block_completed_event) => {
ModelEvent::AfterBlockCompleted(after_block_completed_event)
}
Event::AfterBlockStarted {
block_id,
command,
is_for_in_band_command,
} => ModelEvent::AfterBlockStarted {
block_id,
command,
is_for_in_band_command,
},
Event::BlockMetadataReceived(block_metadata_received_event) => {
ModelEvent::BlockMetadataReceived(block_metadata_received_event)
}
Event::BackgroundBlockStarted => ModelEvent::BackgroundBlockStarted,
Event::ClipboardStore(clipboard_type, text) => {
ModelEvent::ClipboardStore(clipboard_type, text)
}
Event::ClipboardLoad(clipboard_type, clipboard_load) => {
ModelEvent::ClipboardLoad(clipboard_type, clipboard_load)
}
Event::CursorBlinkingChange(is_blinking) => {
ModelEvent::CursorBlinkingChange(is_blinking)
}
Event::TerminalClear => ModelEvent::TerminalClear,
Event::TmuxControlModeReady { primary_pane } => {
ModelEvent::TmuxControlModeReady { primary_pane }
}
Event::DetectedEndOfSshLogin(check_type) => {
ModelEvent::DetectedEndOfSshLogin(check_type)
}
Event::RemoteWarpificationIsUnavailable(reason) => {
ModelEvent::RemoteWarpificationIsUnavailable(reason)
}
Event::SshTmuxInstaller(tmux_installation) => {
ModelEvent::SshTmuxInstaller(tmux_installation)
}
Event::TmuxInstallFailed { line, command } => {
ModelEvent::TmuxInstallFailed { line, command }
}
Event::Bell => ModelEvent::Bell,
Event::Exit { reason } => ModelEvent::Exit { reason },
Event::PreInteractiveSSHSession => ModelEvent::PreInteractiveSSHSession,
Event::SSH(ssh) => ModelEvent::SSH(ssh),
Event::SSHControlMasterError => ModelEvent::SSHControlMasterError,
Event::TerminalModeSwapped(terminal_mode) => {
ModelEvent::TerminalModeSwapped(terminal_mode)
}
Event::ExecutedInBandCommand(executed_in_band_command_event) => {
ModelEvent::ExecutedInBandCommand(executed_in_band_command_event)
}
Event::InitSubshell(init_subshell_event) => {
ModelEvent::InitSubshell(init_subshell_event)
}
Event::SourcedRcFileInSubshell(sourced_rc_file_in_subshell_event) => {
ModelEvent::SourcedRcFileInSubshell(sourced_rc_file_in_subshell_event)
}
Event::InitSsh(init_ssh_event) => ModelEvent::InitSsh(init_ssh_event),
Event::PromptUpdated => ModelEvent::PromptUpdated,
Event::HonorPS1OutOfSync => ModelEvent::HonorPS1OutOfSync,
Event::Typeahead => ModelEvent::Typeahead,
Event::FinishUpdate(data) => ModelEvent::FinishUpdate(data),
Event::TextSelectionChanged => ModelEvent::SelectedTextChanged,
Event::ShellSpawned(shell_type) => ModelEvent::ShellSpawned(shell_type),
Event::SendCompletionsPrompt => ModelEvent::SendCompletionsPrompt,
Event::ImageReceived {
image_id,
image_data,
image_protocol,
} => ModelEvent::ImageReceived {
image_id,
image_data,
image_protocol,
},
Event::BootstrapPrecmdDone => ModelEvent::BootstrapPrecmdDone,
Event::AgentTaggedInChanged { is_tagged_in } => {
ModelEvent::AgentTaggedInChanged { is_tagged_in }
}
Event::PluggableNotification { title, body } => {
ModelEvent::PluggableNotification { title, body }
}
Event::ExitShell { session_id } => ModelEvent::ExitShell { session_id },
_ => return,
};
ctx.emit(event_to_emit);
}
/// Finalizes session initialization by calling `Sessions::initialize_bootstrapped_session`.
///
/// For legacy SSH sessions with the `SshRemoteServer` flag, this also
/// sends the `SessionBootstrapped` notification to the remote server via
/// the manager.
fn complete_bootstrapped_session(
&mut self,
event: BootstrappedEvent,
ctx: &mut ModelContext<Self>,
) {
let BootstrappedEvent {
session_info,
spawning_command,
restored_block_commands,
rcfiles_duration_seconds,
} = event;
let (is_legacy_ssh, session_id, shell_type_name, shell_path) = (
matches!(
session_info.is_legacy_ssh_session,
IsLegacySSHSession::Yes { .. }
),
session_info.session_id,
session_info.shell.shell_type().name().to_owned(),
session_info.shell.shell_path().clone(),
);
self.sessions.update(ctx, |sessions, ctx| {
sessions.initialize_bootstrapped_session(
*session_info,
spawning_command,
restored_block_commands,
rcfiles_duration_seconds,
ctx,
);
});
if FeatureFlag::SshRemoteServer.is_enabled() && is_legacy_ssh {
RemoteServerManager::handle(ctx).update(ctx, |mgr, _ctx| {
mgr.notify_session_bootstrapped(
session_id,
&shell_type_name,
shell_path.as_deref(),
);
});
}
}
/// Emits an event so `TerminalView` can render the remote server block.
pub fn request_remote_server_block(
&mut self,
session_id: SessionId,
ctx: &mut ModelContext<Self>,
) {
ctx.emit(ModelEvent::RemoteServerBlockRequested { session_id });
}
}
/// The type of prompt for which a `PromptStart` event has been received.
enum PromptKind {
Left,
Right,
}
/// Set of events that were dispatched from the [`crate::terminal::TerminalModel`] while parsing
/// PTY output.
pub enum ModelEvent {
MouseCursorDirty,
Title(String),
VisibleBootstrapBlock,
/// Performs the minimal work necessary to show that a block has completed.
/// Treat this as a performance-sensitive path.
BlockCompleted(BlockCompletedEvent),
/// Meant for more expensive operations that can be delayed without negatively
/// affecting the UI.
AfterBlockCompleted(AfterBlockCompletedEvent),
/// Send on DProtoHook::Preexec, but only for blocks after bootstrapping
AfterBlockStarted {
block_id: BlockId,
command: String,
is_for_in_band_command: bool,
},
/// Sent when a new block is created.
BlockMetadataReceived(BlockMetadataReceivedEvent),
/// Sent after a background block is started and added to the block list.
BackgroundBlockStarted,
ClipboardStore(ClipboardType, String),
ClipboardLoad(
ClipboardType,
Arc<dyn Fn(&str) -> String + Sync + Send + 'static>,
),
CursorBlinkingChange(bool),
TerminalClear,
Bell,
Exit {
reason: ExitReason,
},
/// An indication that we are about to initiate an interactive SSH session
/// (which may or may not use the SSH wrapper).
PreInteractiveSSHSession,
/// An indication that a successful SSH connection was initiated via the
/// SSH wrapper. The argument is the name of the remote shell.
SSH(String),
/// Sent when the model detects an SSH ControlMaster error, which means that
/// completions reliant on command execution will not work.
SSHControlMasterError,
TerminalModeSwapped(TerminalMode),
ExecutedInBandCommand(ExecutedExecutorCommandEvent),
TmuxControlModeReady {
primary_pane: u32,
},
/// Sent when a line of output from an interactive ssh session indicates login is complete.
/// A line such as "Last login: Wed Oct 30" for example indicates login is complete. This is
/// useful for detecting when an ssh session becomes ready for warpification.
DetectedEndOfSshLogin(SshLoginStatus),
RemoteWarpificationIsUnavailable(WarpificationUnavailableReason),
SshTmuxInstaller(TmuxInstallationState),
TmuxInstallFailed {
line: String,
command: String,
},
InitSubshell(InitSubshellEvent),
/// Emitted when the user's RC file has been executed in a subshell.
SourcedRcFileInSubshell(SourcedRcFileInSubshellEvent),
InitSsh(InitSshEvent),
/// Emitted when the active block's prompt has been updated.
PromptUpdated,
/// Emitted when the honor_ps1 state of the shell is out-of-sync with Warp's settings.
/// This can happen in cases such as when the user changes between PS1 and Warp prompt inside
/// of an SSH session (the bindkeys are sent to the SSH session but not the local session, so
/// they are out-of-sync when the user exits SSH).
HonorPS1OutOfSync,
/// Emitted when the terminal model receives typeahead output from the PTY.
/// "Typeahead" are characters that were written to the PTY during long-running command execution
/// close to the end of the its execution, such that these characters were not actually read by
/// the running program. The shell stores these characters, inserts them into its internal line
/// buffer, and re-echoes them after Precmd.
Typeahead,
/// Events that correspond to a specific ansi handler hook while parsing PTY output.
///
/// These events make it possible for other models/views to subscribe to PTY output events that
/// are otherwise solely handled on the event loop thread by `TerminalModel`; since PTY output
/// handling logic is mostly executed on that event loop thread, they would otherwise be
/// inaccessible to views/models.
Handler(AnsiHandlerEvent),
FinishUpdate(FinishUpdateValue),
SelectedTextChanged,
ShellSpawned(ShellType),
CompletionsFinished(Vec<ShellCompletion>),
SendCompletionsPrompt,
ImageReceived {
image_id: u32,
image_data: Vec<u8>,
image_protocol: ImageProtocol,
},
BootstrapPrecmdDone,
AgentTaggedInChanged {
is_tagged_in: bool,
},
/// A pluggable notification triggered via OSC 9 or OSC 777 escape sequences.
PluggableNotification {
title: Option<String>,
body: String,
},
/// Emitted when an SSH session's `InitShell` is intercepted by the
/// `SshRemoteServer` feature flag. `RemoteServerController` subscribes to
/// this instead of `Handler(InitShell)` so `PtyController` never sees it.
SshInitShell {
pending_session_info: Box<SessionInfo>,
},
/// Emitted by `ModelEventDispatcher::request_remote_server_block`
/// when the remote-server binary is missing and the user must choose.
RemoteServerBlockRequested {
session_id: SessionId,
},
/// Emitted right before the remote shell for a session exits. Used to
/// tear down per-session resources (e.g. the remote-server-proxy ssh
/// child) before the outer ssh tunnel starts closing.
ExitShell {
session_id: SessionId,
},
}
#[derive(Clone, Debug)]
pub enum AnsiHandlerEvent {
InitShell {
pending_session_info: Box<SessionInfo>,
},
Bootstrapped {
session_id: SessionId,
is_subshell: bool,
},
Precmd,
Preexec,
UserCommandFinished,
InBandCommandFinished,
StartPrompt,
StartRPrompt,
EndPrompt,
EndRPrompt,
SetBracketedPaste,
UnsetBracketedPaste,
StartTmuxControlMode,
TmuxControlModeReady {
primary_pane: u32,
},
EndTmuxControlMode,
RunTmuxCommand(TmuxCommand),
}
impl Entity for ModelEventDispatcher {
type Event = ModelEvent;
}