feat: expand Galaxy agent and remote tooling
Add Wormhole remote helpers, provider and agent improvements, filesystem diagnostics, model metadata support, and schema-aware settings IntelliSense.
This commit is contained in:
@@ -71,7 +71,7 @@ fn get_supported_tools(params: &RequestParams) -> Vec<api::ToolType> {
|
||||
api::ToolType::SearchCodebase,
|
||||
]);
|
||||
}
|
||||
Some(SessionType::WarpifiedRemote { host_id: Some(_) }) => {
|
||||
Some(SessionType::WormholedRemote { host_id: Some(_) }) => {
|
||||
// Remote session with a known host — enable tools that route
|
||||
// through RemoteServerClient. The host_id is only populated
|
||||
// after a successful connection handshake, so its presence is a
|
||||
@@ -81,7 +81,7 @@ fn get_supported_tools(params: &RequestParams) -> Vec<api::ToolType> {
|
||||
supported_tools.push(api::ToolType::SearchCodebase);
|
||||
}
|
||||
}
|
||||
Some(SessionType::WarpifiedRemote { host_id: None }) => {}
|
||||
Some(SessionType::WormholedRemote { host_id: None }) => {}
|
||||
}
|
||||
|
||||
if FeatureFlag::ListSkills.is_enabled() {
|
||||
@@ -113,13 +113,13 @@ fn get_supported_cli_agent_tools(params: &RequestParams) -> Vec<api::ToolType> {
|
||||
supported_cli_agent_tools
|
||||
.extend(&[api::ToolType::ReadFiles, api::ToolType::SearchCodebase]);
|
||||
}
|
||||
Some(SessionType::WarpifiedRemote { host_id: Some(_) }) => {
|
||||
Some(SessionType::WormholedRemote { host_id: Some(_) }) => {
|
||||
supported_cli_agent_tools.push(api::ToolType::ReadFiles);
|
||||
if FeatureFlag::RemoteCodebaseIndexing.is_enabled() {
|
||||
supported_cli_agent_tools.push(api::ToolType::SearchCodebase);
|
||||
}
|
||||
}
|
||||
Some(SessionType::WarpifiedRemote { host_id: None }) => {}
|
||||
Some(SessionType::WormholedRemote { host_id: None }) => {}
|
||||
}
|
||||
|
||||
supported_cli_agent_tools
|
||||
|
||||
@@ -59,7 +59,7 @@ fn request_params_with_ask_user_question_enabled(ask_user_question_enabled: bool
|
||||
fn request_params_for_remote(host_id: Option<HostId>) -> RequestParams {
|
||||
let mut params = request_params_with_ask_user_question_enabled(false);
|
||||
params.session_context =
|
||||
SessionContext::new_with_session_type_for_test(Some(SessionType::WarpifiedRemote {
|
||||
SessionContext::new_with_session_type_for_test(Some(SessionType::WormholedRemote {
|
||||
host_id,
|
||||
}));
|
||||
params
|
||||
|
||||
@@ -289,7 +289,7 @@ static DEFAULT_TIPS: LazyLock<Vec<AgentTip>> = LazyLock::new(|| {
|
||||
},
|
||||
AgentTip {
|
||||
description: "Wormhole a remote SSH session to enable the agent inside that environment.".to_string(),
|
||||
link: Some("https://docs.warp.dev/terminal/warpify".to_string()),
|
||||
link: None,
|
||||
binding_name: None,
|
||||
action: None,
|
||||
kind: AgentTipKind::General,
|
||||
|
||||
@@ -115,7 +115,7 @@ impl ReadFilesExecutor {
|
||||
// Check if this is a remote session with a connected host.
|
||||
let session_type = self.active_session.as_ref(ctx).session_type(ctx);
|
||||
let host_request_handle = match &session_type {
|
||||
Some(SessionType::WarpifiedRemote {
|
||||
Some(SessionType::WormholedRemote {
|
||||
host_id: Some(host_id),
|
||||
}) => Some(
|
||||
remote_server::manager::RemoteServerManager::as_ref(ctx)
|
||||
@@ -127,7 +127,7 @@ impl ReadFilesExecutor {
|
||||
// Remote session without a usable remote server connection. File reading
|
||||
// requires either local access or a connected remote server, neither
|
||||
// of which is available.
|
||||
if matches!(session_type, Some(SessionType::WarpifiedRemote { .. }))
|
||||
if matches!(session_type, Some(SessionType::WormholedRemote { .. }))
|
||||
&& host_request_handle.is_none()
|
||||
{
|
||||
return ActionExecution::Sync(AIAgentActionResultType::ReadFiles(
|
||||
|
||||
@@ -154,7 +154,7 @@ fn disconnected_remote_session_does_not_fall_back_to_client_global_bundled_skill
|
||||
sessions.register_session_for_test(
|
||||
SessionInfo::new_for_test()
|
||||
.with_id(session_id)
|
||||
.with_session_type(BootstrapSessionType::WarpifiedRemote),
|
||||
.with_session_type(BootstrapSessionType::WormholedRemote),
|
||||
);
|
||||
});
|
||||
let (_model_events_tx, model_events_rx) = unbounded();
|
||||
@@ -235,7 +235,7 @@ fn remote_session_reads_remote_bundled_skill_catalog() {
|
||||
sessions.register_session_for_test(
|
||||
SessionInfo::new_for_test()
|
||||
.with_id(session_id)
|
||||
.with_session_type(BootstrapSessionType::WarpifiedRemote),
|
||||
.with_session_type(BootstrapSessionType::WormholedRemote),
|
||||
);
|
||||
});
|
||||
let session = sessions
|
||||
|
||||
@@ -370,7 +370,7 @@ impl RequestFileEditsExecutor {
|
||||
})
|
||||
.collect();
|
||||
let diff_session_type = match self.active_session.as_ref(ctx).session_type(ctx) {
|
||||
Some(SessionType::WarpifiedRemote {
|
||||
Some(SessionType::WormholedRemote {
|
||||
host_id: Some(host_id),
|
||||
}) => DiffSessionType::Remote(host_id.clone()),
|
||||
_ => DiffSessionType::Local,
|
||||
|
||||
@@ -360,7 +360,7 @@ fn format_session_location(session: &Session, working_directory: Option<&str>) -
|
||||
let hostname = session.hostname();
|
||||
match session_type {
|
||||
SessionType::Local => Some(display_path),
|
||||
SessionType::WarpifiedRemote { .. } => Some(format!("{user}@{hostname}:{display_path}")),
|
||||
SessionType::WormholedRemote { .. } => Some(format!("{user}@{hostname}:{display_path}")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,7 +510,7 @@ fn current_working_directory_for_zero_state(terminal_model: &TerminalModel) -> O
|
||||
.is_some_and(|pending_session_info| {
|
||||
matches!(
|
||||
pending_session_info.session_type,
|
||||
BootstrapSessionType::WarpifiedRemote
|
||||
BootstrapSessionType::WormholedRemote
|
||||
)
|
||||
});
|
||||
(!terminal_model.block_list().is_bootstrapped() && !is_bootstrapping_remote_shell)
|
||||
|
||||
@@ -65,7 +65,7 @@ use crate::terminal::model_events::{ModelEvent, ModelEventDispatcher};
|
||||
use crate::terminal::view::ambient_agent::{
|
||||
is_cloud_agent_pre_first_exchange, AmbientAgentViewModel, AmbientAgentViewModelEvent,
|
||||
};
|
||||
use crate::terminal::warpify::render::LEFT_STRIPE_WIDTH;
|
||||
use crate::terminal::wormhole::render::LEFT_STRIPE_WIDTH;
|
||||
use crate::terminal::{
|
||||
TerminalModel, CANCEL_COMMAND_KEYBINDING, TOGGLE_AUTOEXECUTE_MODE_KEYBINDING,
|
||||
TOGGLE_HIDE_CLI_RESPONSES_KEYBINDING, TOGGLE_QUEUE_NEXT_PROMPT_KEYBINDING,
|
||||
|
||||
@@ -314,11 +314,11 @@ impl SessionContext {
|
||||
&self.current_working_directory
|
||||
}
|
||||
|
||||
/// Returns the remote host ID if this is a `WarpifiedRemote` session with
|
||||
/// Returns the remote host ID if this is a `WormholedRemote` session with
|
||||
/// a connected `RemoteServerClient`.
|
||||
pub fn host_id(&self) -> Option<&galaxy_core::HostId> {
|
||||
match &self.session_type {
|
||||
Some(SessionType::WarpifiedRemote { host_id }) => host_id.as_ref(),
|
||||
Some(SessionType::WormholedRemote { host_id }) => host_id.as_ref(),
|
||||
Some(SessionType::Local) | None => None,
|
||||
}
|
||||
}
|
||||
@@ -326,17 +326,17 @@ impl SessionContext {
|
||||
/// Returns `true` if this is a remote session (regardless of whether
|
||||
/// the remote server client is connected).
|
||||
pub fn is_remote(&self) -> bool {
|
||||
matches!(self.session_type, Some(SessionType::WarpifiedRemote { .. }))
|
||||
matches!(self.session_type, Some(SessionType::WormholedRemote { .. }))
|
||||
}
|
||||
|
||||
pub fn skill_path_origin(&self) -> SkillPathOrigin {
|
||||
match &self.session_type {
|
||||
Some(SessionType::WarpifiedRemote {
|
||||
Some(SessionType::WormholedRemote {
|
||||
host_id: Some(host_id),
|
||||
}) => SkillPathOrigin::Remote {
|
||||
host_id: host_id.clone(),
|
||||
},
|
||||
Some(SessionType::WarpifiedRemote { host_id: None }) => SkillPathOrigin::Unavailable,
|
||||
Some(SessionType::WormholedRemote { host_id: None }) => SkillPathOrigin::Unavailable,
|
||||
Some(SessionType::Local) | None => SkillPathOrigin::Local,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! This module contains rendering functions for various requested inline actions that have not yet
|
||||
//! been transformed into a [`View`] component. This currently encompasses UI for file retrieval,
|
||||
//! environmental variable collection, and SSH Warpification, to name a few.
|
||||
//! environmental variable collection, and SSH Wormholing, to name a few.
|
||||
//!
|
||||
//! There's quite a bit of duplication between function-based inline actions and view-based inline
|
||||
//! actions. Moreover, the header rendering functions here don't make use of the HeaderConfig.
|
||||
|
||||
@@ -408,7 +408,7 @@ impl PassiveSuggestionsModel {
|
||||
.active_session
|
||||
.as_ref(ctx)
|
||||
.session_type(ctx)
|
||||
.map(|session_type| matches!(session_type, SessionType::WarpifiedRemote { .. }))
|
||||
.map(|session_type| matches!(session_type, SessionType::WormholedRemote { .. }))
|
||||
.unwrap_or(true);
|
||||
if !can_read_file || should_skip_for_remote {
|
||||
let reason = if !can_read_file {
|
||||
|
||||
@@ -1037,6 +1037,157 @@ impl PersistedWorkspace {
|
||||
);
|
||||
}
|
||||
|
||||
/// Ensures Galaxy's own settings file has schema-backed TOML language support.
|
||||
/// This managed server is intentionally not persisted as a code workspace.
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub fn ensure_settings_toml_lsp(&mut self, file_path: PathBuf, ctx: &mut ModelContext<Self>) {
|
||||
if file_path != crate::settings::user_preferences_toml_file_path() {
|
||||
return;
|
||||
}
|
||||
|
||||
let server_type = LSPServerType::Tombi;
|
||||
let Some(workspace_root) = file_path.parent().map(Path::to_path_buf) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if LspManagerModel::as_ref(ctx).server_registered(&workspace_root, server_type, ctx) {
|
||||
LspManagerModel::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.start_all(workspace_root, ctx);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if self.lsp_installation_status.get(&server_type)
|
||||
== Some(&LSPInstallationStatus::Installing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.lsp_installation_status
|
||||
.insert(server_type, LSPInstallationStatus::Installing);
|
||||
ctx.emit(PersistedWorkspaceEvent::InstallStatusUpdate {
|
||||
server_type,
|
||||
status: LSPInstallationStatus::Installing,
|
||||
});
|
||||
|
||||
let path_future = LocalShellState::handle(ctx).update(ctx, |shell_state, ctx| {
|
||||
shell_state.get_interactive_path_env_var(ctx)
|
||||
});
|
||||
let http_client = ServerApiProvider::as_ref(ctx).get_http_client();
|
||||
let file_path_for_install = file_path.clone();
|
||||
|
||||
ctx.spawn(
|
||||
async move {
|
||||
let path_env_var = path_future.await;
|
||||
let executor = lsp::CommandBuilder::new(path_env_var.clone());
|
||||
let candidate = server_type.candidate(http_client.clone());
|
||||
|
||||
if !candidate.is_installed(&executor).await {
|
||||
let metadata = candidate.fetch_latest_server_metadata().await?;
|
||||
candidate.install(metadata, &executor).await?;
|
||||
}
|
||||
|
||||
let schema_path = crate::settings::schema_export::ensure_runtime_settings_schema()?;
|
||||
let schema_uri = url::Url::from_file_path(&schema_path)
|
||||
.map_err(|()| anyhow::anyhow!("Invalid settings schema path: {}", schema_path.display()))?;
|
||||
let file_match = file_path_for_install.to_string_lossy().into_owned();
|
||||
|
||||
Ok::<_, anyhow::Error>((
|
||||
path_env_var,
|
||||
schema_uri.to_string(),
|
||||
file_match,
|
||||
))
|
||||
},
|
||||
move |me, result, ctx| match result {
|
||||
Ok((path_env_var, schema_uri, file_match)) => {
|
||||
me.lsp_installation_status
|
||||
.insert(server_type, LSPInstallationStatus::Installed);
|
||||
ctx.emit(PersistedWorkspaceEvent::InstallStatusUpdate {
|
||||
server_type,
|
||||
status: LSPInstallationStatus::Installed,
|
||||
});
|
||||
|
||||
let log_relative_path =
|
||||
crate::code::lsp_logs::relative_log_path(server_type, &workspace_root);
|
||||
let http_client = ServerApiProvider::as_ref(ctx).get_http_client();
|
||||
let config = LspServerConfig::new(
|
||||
server_type,
|
||||
workspace_root.clone(),
|
||||
path_env_var,
|
||||
ChannelState::app_id().application_name().to_string(),
|
||||
http_client,
|
||||
)
|
||||
.with_log_relative_path(log_relative_path)
|
||||
.with_post_initialize_notification(
|
||||
"tombi/associateSchema",
|
||||
serde_json::json!({
|
||||
"title": "Galaxy Settings",
|
||||
"description": "Galaxy's generated settings schema",
|
||||
"uri": schema_uri,
|
||||
"fileMatch": [file_match],
|
||||
"tomlVersion": "v1.1.0",
|
||||
"force": true,
|
||||
}),
|
||||
);
|
||||
|
||||
let manager = LspManagerModel::handle(ctx);
|
||||
manager.update(ctx, |manager, ctx| {
|
||||
manager.register(workspace_root.clone(), config, ctx);
|
||||
});
|
||||
|
||||
let workspace_root_display = workspace_root.display().to_string();
|
||||
if let Some(server) = manager
|
||||
.as_ref(ctx)
|
||||
.servers_for_workspace(&workspace_root)
|
||||
.and_then(|servers| servers.last())
|
||||
.cloned()
|
||||
{
|
||||
ctx.subscribe_to_model(&server, move |_, _, event, ctx| {
|
||||
if let LspEvent::Failed(error) = event {
|
||||
if let Some(window_id) = WindowManager::as_ref(ctx).active_window() {
|
||||
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
||||
toast_stack.add_ephemeral_toast(
|
||||
DismissibleToast::error(format!(
|
||||
"Failed to start TOML language support for {workspace_root_display}: {error}"
|
||||
)),
|
||||
window_id,
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
manager.update(ctx, |manager, ctx| {
|
||||
manager.start_all(workspace_root, ctx);
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn!("Failed to prepare TOML language support: {error:#}");
|
||||
me.lsp_installation_status
|
||||
.insert(server_type, LSPInstallationStatus::NotInstalled);
|
||||
ctx.emit(PersistedWorkspaceEvent::InstallStatusUpdate {
|
||||
server_type,
|
||||
status: LSPInstallationStatus::NotInstalled,
|
||||
});
|
||||
|
||||
if let Some(window_id) = WindowManager::as_ref(ctx).active_window() {
|
||||
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
||||
toast_stack.add_ephemeral_toast(
|
||||
DismissibleToast::error(format!(
|
||||
"Failed to prepare TOML language support: {error}"
|
||||
)),
|
||||
window_id,
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Starts all enabled LSP servers for the given file path.
|
||||
/// This looks up the workspace root and starts any servers that are enabled but not yet running.
|
||||
#[cfg(feature = "local_fs")]
|
||||
|
||||
@@ -159,12 +159,14 @@ mod appimage {
|
||||
}
|
||||
|
||||
mod package_manager {
|
||||
use anyhow::{bail, Result};
|
||||
use galaxyui::elements::{Container, FormattedTextElement, HighlightedHyperlink};
|
||||
use galaxyui::{Element, SingletonEntity as _};
|
||||
use markdown_parser::{
|
||||
FormattedText, FormattedTextFragment, FormattedTextHeader, FormattedTextLine,
|
||||
};
|
||||
|
||||
use super::{PackageManager, CURRENT_EXE};
|
||||
use crate::appearance::Appearance;
|
||||
|
||||
pub struct AutoupdateContextBlock {
|
||||
|
||||
@@ -1,160 +1,34 @@
|
||||
//! Generates a JSON Schema file describing Warp's user-facing settings.
|
||||
//! Generates a JSON Schema file describing Galaxy's user-facing settings.
|
||||
//!
|
||||
//! Usage:
|
||||
//! ```
|
||||
//! cargo run --bin generate_settings_schema -- [--channel dev|preview|stable] [output_path]
|
||||
//! ```
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::io::Write;
|
||||
|
||||
use galaxy_core::features::{
|
||||
FeatureFlag, DEBUG_FLAGS, DOGFOOD_FLAGS, PREVIEW_FLAGS, RELEASE_FLAGS,
|
||||
};
|
||||
use schemars::SchemaGenerator;
|
||||
use serde_json::{Map, Value};
|
||||
use settings::schema::SettingSchemaEntry;
|
||||
use galaxy::settings::schema_export::generate_settings_schema;
|
||||
|
||||
/// Ensures all `inventory::submit!` registrations from the app crate's
|
||||
/// dependency tree are linked into the binary.
|
||||
///
|
||||
/// Binary targets only link crate code that is transitively referenced.
|
||||
/// Without an explicit reference to the `warp` library, the linker will
|
||||
/// not include most of the app's object files and the `inventory`
|
||||
/// submissions they contain.
|
||||
fn ensure_settings_linked() {
|
||||
let _ = std::hint::black_box(galaxy::settings::RESTORE_SESSION);
|
||||
}
|
||||
|
||||
/// Recursively strips `minimum`, `maximum`, and `format` from integer and
|
||||
/// number schemas. schemars derives these from Rust type bounds (e.g. `u8`
|
||||
/// → `minimum: 0, maximum: 255, format: "uint8"`), which are misleading
|
||||
/// for settings whose valid domain is narrower than the type allows.
|
||||
fn strip_numeric_metadata(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
let is_numeric = map
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|t| t == "integer" || t == "number");
|
||||
|
||||
if is_numeric {
|
||||
map.remove("minimum");
|
||||
map.remove("maximum");
|
||||
map.remove("format");
|
||||
}
|
||||
|
||||
for val in map.values_mut() {
|
||||
strip_numeric_metadata(val);
|
||||
}
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
for val in arr {
|
||||
strip_numeric_metadata(val);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes `{"enum": [], "type": "string"}` entries from `oneOf` arrays.
|
||||
/// schemars emits an empty enum bucket for externally-tagged enums when all
|
||||
/// unit variants have individual descriptions (and are therefore promoted to
|
||||
/// separate `oneOf` branches with `const`). The empty bucket is unreachable
|
||||
/// and confuses schema consumers.
|
||||
fn strip_empty_enum_entries(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
if let Some(Value::Array(one_of)) = map.get_mut("oneOf") {
|
||||
one_of.retain(|entry| {
|
||||
!matches!(entry, Value::Object(obj)
|
||||
if obj.get("enum").is_some_and(|e| e.as_array().is_some_and(|a| a.is_empty()))
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
for val in map.values_mut() {
|
||||
strip_empty_enum_entries(val);
|
||||
}
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
for val in arr {
|
||||
strip_empty_enum_entries(val);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn active_flags_for_channel(channel: &str) -> HashSet<FeatureFlag> {
|
||||
let mut flags = HashSet::new();
|
||||
|
||||
let flag_lists: &[&[FeatureFlag]] = match channel {
|
||||
"stable" => &[RELEASE_FLAGS],
|
||||
"preview" => &[RELEASE_FLAGS, PREVIEW_FLAGS],
|
||||
"dev" => &[RELEASE_FLAGS, PREVIEW_FLAGS, DOGFOOD_FLAGS, DEBUG_FLAGS],
|
||||
other => {
|
||||
eprintln!("Unknown channel '{other}', defaulting to dev");
|
||||
&[RELEASE_FLAGS, PREVIEW_FLAGS, DOGFOOD_FLAGS, DEBUG_FLAGS]
|
||||
}
|
||||
};
|
||||
|
||||
for list in flag_lists {
|
||||
for flag in *list {
|
||||
flags.insert(*flag);
|
||||
}
|
||||
}
|
||||
|
||||
flags
|
||||
}
|
||||
|
||||
/// Creates intermediate hierarchy objects so that a setting at e.g.
|
||||
/// `appearance.text` is nested under `properties.appearance.properties.text.properties`.
|
||||
fn ensure_hierarchy<'a>(
|
||||
root_properties: &'a mut Map<String, Value>,
|
||||
hierarchy: &str,
|
||||
) -> &'a mut Map<String, Value> {
|
||||
let segments: Vec<&str> = hierarchy.split('.').collect();
|
||||
let mut current = root_properties;
|
||||
|
||||
for segment in segments {
|
||||
// Ensure the segment object exists
|
||||
let entry = current.entry(segment.to_string()).or_insert_with(|| {
|
||||
Value::Object({
|
||||
let mut m = Map::new();
|
||||
m.insert("type".to_string(), Value::String("object".to_string()));
|
||||
m.insert("properties".to_string(), Value::Object(Map::new()));
|
||||
m
|
||||
})
|
||||
});
|
||||
|
||||
// Navigate into its properties
|
||||
current = entry
|
||||
.as_object_mut()
|
||||
.expect("hierarchy node should be an object")
|
||||
.entry("properties")
|
||||
.or_insert_with(|| Value::Object(Map::new()))
|
||||
.as_object_mut()
|
||||
.expect("properties should be an object");
|
||||
}
|
||||
|
||||
current
|
||||
}
|
||||
|
||||
fn main() {
|
||||
ensure_settings_linked();
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
let mut channel = "dev";
|
||||
let mut output_path: Option<&str> = None;
|
||||
let mut i = 1;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
let mut index = 1;
|
||||
|
||||
while index < args.len() {
|
||||
match args[index].as_str() {
|
||||
"--channel" => {
|
||||
i += 1;
|
||||
if i < args.len() {
|
||||
channel = &args[i];
|
||||
index += 1;
|
||||
if index < args.len() {
|
||||
channel = &args[index];
|
||||
}
|
||||
}
|
||||
arg if !arg.starts_with('-') => {
|
||||
@@ -165,101 +39,16 @@ fn main() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
let active_flags = active_flags_for_channel(channel);
|
||||
let mut generator = SchemaGenerator::default();
|
||||
let mut root_properties = Map::new();
|
||||
let mut entry_count = 0;
|
||||
|
||||
for entry in inventory::iter::<SettingSchemaEntry> {
|
||||
// Skip private settings
|
||||
if entry.is_private {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip settings whose feature flag is not active
|
||||
if let Some(flag) = entry.feature_flag {
|
||||
if !active_flags.contains(&flag) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let type_schema = (entry.schema_fn)(&mut generator);
|
||||
|
||||
let mut schema_value: Value = type_schema.to_value();
|
||||
|
||||
// Compute default value — prefer file default over serde default
|
||||
let default_json = (entry.file_default_value_fn)();
|
||||
|
||||
if let Ok(default_value) = serde_json::from_str::<Value>(&default_json) {
|
||||
if let Some(obj) = schema_value.as_object_mut() {
|
||||
obj.insert("default".to_string(), default_value);
|
||||
}
|
||||
}
|
||||
|
||||
// Always overwrite description with the macro-provided one
|
||||
if !entry.description.is_empty() {
|
||||
if let Some(obj) = schema_value.as_object_mut() {
|
||||
obj.insert(
|
||||
"description".to_string(),
|
||||
Value::String(entry.description.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Place the setting in the hierarchy
|
||||
let target = if let Some(hierarchy) = entry.hierarchy {
|
||||
ensure_hierarchy(&mut root_properties, hierarchy)
|
||||
} else {
|
||||
&mut root_properties
|
||||
};
|
||||
|
||||
target.insert(entry.storage_key.to_string(), schema_value);
|
||||
entry_count += 1;
|
||||
}
|
||||
|
||||
// Collect $defs from the generator
|
||||
let defs_map = generator.take_definitions(true);
|
||||
|
||||
// Assemble the root document
|
||||
let mut root = Map::new();
|
||||
root.insert(
|
||||
"$schema".to_string(),
|
||||
Value::String("https://json-schema.org/draft/2020-12/schema".to_string()),
|
||||
);
|
||||
root.insert(
|
||||
"title".to_string(),
|
||||
Value::String("Galaxy Settings".to_string()),
|
||||
);
|
||||
root.insert(
|
||||
"description".to_string(),
|
||||
Value::String(format!(
|
||||
"JSON Schema for Galaxy settings ({channel} channel, {entry_count} settings)"
|
||||
)),
|
||||
);
|
||||
root.insert("type".to_string(), Value::String("object".to_string()));
|
||||
root.insert("properties".to_string(), Value::Object(root_properties));
|
||||
|
||||
if !defs_map.is_empty() {
|
||||
root.insert("$defs".to_string(), Value::Object(defs_map));
|
||||
}
|
||||
|
||||
// Strip type-derived numeric metadata (minimum, maximum, format) that
|
||||
// schemars emits from Rust primitive bounds (e.g. u8 → max 255).
|
||||
// These leak implementation details rather than semantic constraints.
|
||||
let mut root_value = Value::Object(root);
|
||||
strip_numeric_metadata(&mut root_value);
|
||||
strip_empty_enum_entries(&mut root_value);
|
||||
|
||||
let output = serde_json::to_string_pretty(&root_value).expect("schema should serialize");
|
||||
let (output, entry_count) = generate_settings_schema(channel);
|
||||
|
||||
if let Some(path) = output_path {
|
||||
let mut file = std::fs::File::create(path)
|
||||
.unwrap_or_else(|e| panic!("Failed to create output file '{path}': {e}"));
|
||||
.unwrap_or_else(|error| panic!("Failed to create output file '{path}': {error}"));
|
||||
file.write_all(output.as_bytes())
|
||||
.unwrap_or_else(|e| panic!("Failed to write to '{path}': {e}"));
|
||||
.unwrap_or_else(|error| panic!("Failed to write to '{path}': {error}"));
|
||||
eprintln!("Wrote {entry_count} settings to {path}");
|
||||
} else {
|
||||
println!("{output}");
|
||||
|
||||
+42
-18
@@ -187,6 +187,13 @@ fn fuzzy_match(target: &str, query: &str) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn completion_documentation(item: &CompletionItem) -> Option<String> {
|
||||
match item.documentation.as_ref()? {
|
||||
lsp_types::Documentation::String(documentation) => Some(documentation.clone()),
|
||||
lsp_types::Documentation::MarkupContent(documentation) => Some(documentation.value.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalCodeEditorView {
|
||||
pub(super) fn is_completion_enabled() -> bool {
|
||||
FeatureFlag::LspCompletion.is_enabled()
|
||||
@@ -254,12 +261,13 @@ impl LocalCodeEditorView {
|
||||
};
|
||||
|
||||
if let Some(trigger) = trigger {
|
||||
self.request_completion(cursor_offset, trigger, ctx);
|
||||
self.request_completion(cursor_offset, cursor_offset, trigger, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn request_completion(
|
||||
&mut self,
|
||||
request_offset: CharOffset,
|
||||
trigger_offset: CharOffset,
|
||||
trigger: CompletionTrigger,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
@@ -279,7 +287,7 @@ impl LocalCodeEditorView {
|
||||
let lsp_position = self
|
||||
.editor()
|
||||
.as_ref(ctx)
|
||||
.offset_to_lsp_position(trigger_offset, ctx);
|
||||
.offset_to_lsp_position(request_offset, ctx);
|
||||
|
||||
let future =
|
||||
match lsp_server
|
||||
@@ -318,7 +326,7 @@ impl LocalCodeEditorView {
|
||||
}
|
||||
|
||||
let word_start = self.find_word_start(offset, ctx);
|
||||
self.request_completion(word_start, CompletionTrigger::Invoked, ctx);
|
||||
self.request_completion(offset, word_start, CompletionTrigger::Invoked, ctx);
|
||||
}
|
||||
|
||||
/// Find the start of the current identifier word by walking backwards from `offset`.
|
||||
@@ -440,7 +448,7 @@ impl LocalCodeEditorView {
|
||||
|
||||
/// Resolve documentation for the currently selected completion item.
|
||||
pub(super) fn resolve_selected_completion_docs(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let raw_item = match &self.completion_state {
|
||||
let (item_index, raw_item) = match &self.completion_state {
|
||||
CompletionState::Showing {
|
||||
items,
|
||||
filtered_indices,
|
||||
@@ -458,14 +466,22 @@ impl LocalCodeEditorView {
|
||||
{
|
||||
return;
|
||||
}
|
||||
items[item_idx].raw_item.clone()
|
||||
(item_idx, items[item_idx].raw_item.clone())
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if let Some(documentation) = completion_documentation(&raw_item) {
|
||||
self.set_resolved_completion_docs(item_index, documentation, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(lsp_server) = &self.lsp_server else {
|
||||
return;
|
||||
};
|
||||
if !lsp_server.as_ref(ctx).supports_completion_resolve() {
|
||||
return;
|
||||
}
|
||||
|
||||
let future = match lsp_server.as_ref(ctx).completion_resolve(raw_item) {
|
||||
Ok(future) => future,
|
||||
@@ -473,8 +489,8 @@ impl LocalCodeEditorView {
|
||||
};
|
||||
|
||||
let abort_handle = ctx
|
||||
.spawn(future, |me, result, ctx| {
|
||||
me.handle_completion_resolve_response(result, ctx);
|
||||
.spawn(future, move |me, result, ctx| {
|
||||
me.handle_completion_resolve_response(item_index, result, ctx);
|
||||
})
|
||||
.abort_handle();
|
||||
|
||||
@@ -492,6 +508,7 @@ impl LocalCodeEditorView {
|
||||
|
||||
fn handle_completion_resolve_response(
|
||||
&mut self,
|
||||
item_index: usize,
|
||||
result: anyhow::Result<CompletionItem>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
@@ -500,22 +517,29 @@ impl LocalCodeEditorView {
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let doc_string = match resolved_item.documentation {
|
||||
Some(lsp_types::Documentation::String(s)) => s,
|
||||
Some(lsp_types::Documentation::MarkupContent(m)) => m.value,
|
||||
None => return,
|
||||
let Some(documentation) = completion_documentation(&resolved_item) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if doc_string.trim().is_empty() {
|
||||
self.set_resolved_completion_docs(item_index, documentation, ctx);
|
||||
}
|
||||
|
||||
fn set_resolved_completion_docs(
|
||||
&mut self,
|
||||
item_index: usize,
|
||||
documentation: String,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if documentation.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let formatted = match markdown_parser::parse_markdown(&doc_string) {
|
||||
let formatted = match markdown_parser::parse_markdown(&documentation) {
|
||||
Ok(text) => text,
|
||||
Err(_) => {
|
||||
use markdown_parser::{FormattedTextFragment, FormattedTextLine};
|
||||
FormattedText::new([FormattedTextLine::Line(vec![
|
||||
FormattedTextFragment::plain_text(doc_string),
|
||||
FormattedTextFragment::plain_text(documentation),
|
||||
])])
|
||||
}
|
||||
};
|
||||
@@ -529,9 +553,9 @@ impl LocalCodeEditorView {
|
||||
} = &mut self.completion_state
|
||||
{
|
||||
*resolve_abort_handle = None;
|
||||
if let Some(&item_idx) = filtered_indices.get(*selected_index) {
|
||||
if filtered_indices.get(*selected_index) == Some(&item_index) {
|
||||
*resolved_docs = Some(ResolvedDocumentation {
|
||||
item_index: item_idx,
|
||||
item_index,
|
||||
text: formatted,
|
||||
scroll_state: ClippedScrollStateHandle::default(),
|
||||
});
|
||||
@@ -552,7 +576,7 @@ impl LocalCodeEditorView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Manually trigger completion (Ctrl+Alt+Space).
|
||||
/// Manually trigger completion (Ctrl+Space in the code editor).
|
||||
pub(super) fn trigger_completion_manually(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
if !Self::is_completion_enabled() {
|
||||
return;
|
||||
@@ -562,7 +586,7 @@ impl LocalCodeEditorView {
|
||||
}
|
||||
let cursor_offset = self.editor().as_ref(ctx).cursor_head_offset(ctx);
|
||||
let word_start = self.find_word_start(cursor_offset, ctx);
|
||||
self.request_completion(word_start, CompletionTrigger::Invoked, ctx);
|
||||
self.request_completion(cursor_offset, word_start, CompletionTrigger::Invoked, ctx);
|
||||
}
|
||||
|
||||
pub(super) fn render_completion_menu(&self, app: &AppContext) -> Option<Box<dyn Element>> {
|
||||
|
||||
@@ -158,6 +158,8 @@ pub enum CodeEditorEvent {
|
||||
CompletionNavigateDown,
|
||||
/// Emitted when Tab/Enter is pressed and completion_intercept_keys is active.
|
||||
CompletionConfirm,
|
||||
/// Emitted when the manual completion keybinding is pressed.
|
||||
CompletionTrigger,
|
||||
}
|
||||
|
||||
/// Store all states related to displaying the editor content.
|
||||
|
||||
@@ -78,6 +78,11 @@ pub fn init(app: &mut AppContext) {
|
||||
CodeEditorViewAction::VimShiftEnter,
|
||||
text_entry.clone() & id!("Vim"),
|
||||
),
|
||||
FixedBinding::new(
|
||||
"ctrl-space",
|
||||
CodeEditorViewAction::TriggerCompletion,
|
||||
editable_state.clone(),
|
||||
),
|
||||
FixedBinding::new(
|
||||
"backspace",
|
||||
CodeEditorViewAction::Backspace,
|
||||
@@ -711,6 +716,7 @@ pub enum CodeEditorViewAction {
|
||||
ShiftTab,
|
||||
ShowFindBar,
|
||||
ShowGoToLine,
|
||||
TriggerCompletion,
|
||||
Escape,
|
||||
VimEnter,
|
||||
VimTab,
|
||||
@@ -795,6 +801,7 @@ impl CodeEditorViewAction {
|
||||
| Self::Copy
|
||||
| Self::ShowFindBar
|
||||
| Self::ShowGoToLine
|
||||
| Self::TriggerCompletion
|
||||
| Self::Escape
|
||||
| Self::HiddenSectionExpansion { .. }
|
||||
| Self::AddDiffHunkContext { .. }
|
||||
@@ -1064,6 +1071,7 @@ impl TypedActionView for CodeEditorView {
|
||||
|
||||
ShowFindBar => self.show_find_bar(ctx),
|
||||
ShowGoToLine => self.show_goto_line(ctx),
|
||||
TriggerCompletion => ctx.emit(CodeEditorEvent::CompletionTrigger),
|
||||
Escape => self.escape(ctx),
|
||||
HiddenSectionExpansion {
|
||||
line_range,
|
||||
|
||||
@@ -2965,7 +2965,7 @@ impl View for FileTreeView {
|
||||
if let CodingPanelEnablementState::RemoteSession { has_remote_server } = self.enablement
|
||||
{
|
||||
// When the session has a remote server connection (Auto SSH
|
||||
// Warpification / mode 1), show a loading state — the server
|
||||
// Wormholing / mode 1), show a loading state — the server
|
||||
// may push repo metadata momentarily. For other SSH modes
|
||||
// (tmux, subshell) no data will arrive, so show the disabled
|
||||
// error instead.
|
||||
|
||||
@@ -1562,13 +1562,23 @@ impl GlobalBufferModel {
|
||||
.flatten();
|
||||
|
||||
// If we have a previous version that wasn't synced, we need to do a full sync.
|
||||
let needs_full_sync = previous_version.is_some_and(|prev| {
|
||||
last_synced.is_none() || last_synced.is_some_and(|synced| synced < prev)
|
||||
});
|
||||
let server_requires_full_sync = lsp_server.as_ref(ctx).requires_full_document_sync();
|
||||
let needs_full_sync = server_requires_full_sync
|
||||
|| previous_version.is_some_and(|prev| {
|
||||
last_synced.is_none() || last_synced.is_some_and(|synced| synced < prev)
|
||||
});
|
||||
|
||||
let deltas_len = deltas.len();
|
||||
|
||||
if needs_full_sync {
|
||||
if server_requires_full_sync {
|
||||
lsp_server.as_ref(ctx).log_to_server_log(
|
||||
LspServerLogLevel::Debug,
|
||||
format!(
|
||||
"didChange -> server: REQUIRED full-sync file={} send_version={current_version} deltas={deltas_len}",
|
||||
path.display()
|
||||
),
|
||||
);
|
||||
} else if needs_full_sync {
|
||||
lsp_server.as_ref(ctx).log_to_server_log(
|
||||
LspServerLogLevel::Info,
|
||||
format!(
|
||||
|
||||
@@ -114,11 +114,6 @@ pub fn init(app: &mut AppContext) {
|
||||
LocalCodeEditorAction::StartRename,
|
||||
id!("LocalCodeEditorView"),
|
||||
),
|
||||
FixedBinding::new(
|
||||
"ctrl-alt-space",
|
||||
LocalCodeEditorAction::TriggerCompletion,
|
||||
id!("LocalCodeEditorView"),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -216,8 +211,6 @@ pub enum LocalCodeEditorAction {
|
||||
OpenCodeActions,
|
||||
/// Start LSP rename at cursor (F2).
|
||||
StartRename,
|
||||
/// Manually trigger completion (Ctrl+Alt+Space).
|
||||
TriggerCompletion,
|
||||
/// Hover over a completion item by display index.
|
||||
CompletionHoverItem(usize),
|
||||
/// Confirm completion via mouse click.
|
||||
@@ -510,6 +503,9 @@ impl LocalCodeEditorView {
|
||||
CodeEditorEvent::CompletionConfirm => {
|
||||
me.confirm_completion(ctx);
|
||||
}
|
||||
CodeEditorEvent::CompletionTrigger => {
|
||||
me.trigger_completion_manually(ctx);
|
||||
}
|
||||
CodeEditorEvent::VimGotoDefinition
|
||||
| CodeEditorEvent::VimFindReferences
|
||||
| CodeEditorEvent::VimShowHover => {
|
||||
@@ -995,9 +991,12 @@ impl LocalCodeEditorView {
|
||||
// If the LSP is not registered, try to start it via PersistedWorkspace.
|
||||
#[cfg(feature = "local_fs")]
|
||||
{
|
||||
use crate::ai::persisted_workspace::LspTask;
|
||||
PersistedWorkspace::handle(ctx).update(ctx, |workspace, ctx| {
|
||||
workspace.execute_lsp_task(LspTask::Spawn { file_path: path }, ctx);
|
||||
if path == crate::settings::user_preferences_toml_file_path() {
|
||||
workspace.ensure_settings_toml_lsp(path, ctx);
|
||||
} else {
|
||||
workspace.execute_lsp_task(LspTask::Spawn { file_path: path }, ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
@@ -2487,9 +2486,6 @@ impl TypedActionView for LocalCodeEditorView {
|
||||
LocalCodeEditorAction::StartRename => {
|
||||
self.start_rename(ctx);
|
||||
}
|
||||
LocalCodeEditorAction::TriggerCompletion => {
|
||||
self.trigger_completion_manually(ctx);
|
||||
}
|
||||
LocalCodeEditorAction::CompletionHoverItem(display_index) => {
|
||||
self.handle_completion_hover_item(*display_index, ctx);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ pub(crate) enum CodingPanelEnablementState {
|
||||
/// The active session is on a remote host.
|
||||
///
|
||||
/// `has_remote_server` is `true` when the session is registered with
|
||||
/// `RemoteServerManager` (i.e. Auto SSH Warpification / mode 1). When
|
||||
/// `RemoteServerManager` (i.e. Auto SSH Wormholing / mode 1). When
|
||||
/// `true`, remote repo metadata may arrive and the file tree should show
|
||||
/// a loading state. When `false` (tmux or subshell SSH), no data will
|
||||
/// arrive and the file tree should show a disabled message.
|
||||
|
||||
@@ -101,7 +101,7 @@ impl SessionContext {
|
||||
.filter_map(|res| res.and_then(EngineDirEntry::try_from).ok())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
SessionType::WarpifiedRemote { .. } => {
|
||||
SessionType::WormholedRemote { .. } => {
|
||||
let env_vars = self
|
||||
.session
|
||||
.path()
|
||||
|
||||
@@ -91,7 +91,7 @@ pub fn ssh_session(ctx: &GeneratorContext) -> Option<ChipValue> {
|
||||
if session.is_ssh_wrapper_session()
|
||||
|| matches!(
|
||||
session.session_type(),
|
||||
crate::terminal::model::session::SessionType::WarpifiedRemote { .. }
|
||||
crate::terminal::model::session::SessionType::WormholedRemote { .. }
|
||||
)
|
||||
{
|
||||
let user = session.user();
|
||||
|
||||
@@ -43,7 +43,7 @@ fn test_remote_sessions() {
|
||||
let local_session = Session::test();
|
||||
let remote_session = Session::new(
|
||||
SessionInfo::new_for_test()
|
||||
.with_session_type(BootstrapSessionType::WarpifiedRemote)
|
||||
.with_session_type(BootstrapSessionType::WormholedRemote)
|
||||
.with_hostname("remote-host".to_string())
|
||||
.with_user("remote-user".to_string()),
|
||||
Arc::new(TestCommandExecutor {}),
|
||||
|
||||
@@ -1638,10 +1638,10 @@ impl DisplayChip {
|
||||
.as_ref()
|
||||
.map(|ctx| match ctx.session.session_type() {
|
||||
SessionType::Local => true,
|
||||
SessionType::WarpifiedRemote { host_id: Some(_) } => {
|
||||
SessionType::WormholedRemote { host_id: Some(_) } => {
|
||||
FeatureFlag::RemoteCodeReview.is_enabled()
|
||||
}
|
||||
SessionType::WarpifiedRemote { host_id: None } => false,
|
||||
SessionType::WormholedRemote { host_id: None } => false,
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
|
||||
+2
-2
@@ -465,8 +465,8 @@ fn enabled_features() -> HashSet<FeatureFlag> {
|
||||
FeatureFlag::CLIAgentRichInput,
|
||||
#[cfg(feature = "transfer_control_tool")]
|
||||
FeatureFlag::TransferControlTool,
|
||||
#[cfg(feature = "warpify_footer")]
|
||||
FeatureFlag::WarpifyFooter,
|
||||
#[cfg(feature = "wormhole_footer")]
|
||||
FeatureFlag::WormholeFooter,
|
||||
#[cfg(feature = "solo_user_byok")]
|
||||
FeatureFlag::SoloUserByok,
|
||||
#[cfg(feature = "billing_and_usage_page_v2")]
|
||||
|
||||
@@ -92,7 +92,7 @@ pub fn enter_local_subshell_command(shell: &str) -> TestStep {
|
||||
}
|
||||
|
||||
pub fn assert_subshell_banner_is_showing() -> TestStep {
|
||||
TestStep::new("Assert the Warpify banner is visible")
|
||||
TestStep::new("Assert the Wormhole banner is visible")
|
||||
.add_assertion(move |app, window_id| {
|
||||
let terminal_view = single_terminal_view(app, window_id);
|
||||
terminal_view.read(app, |view, _ctx| {
|
||||
@@ -102,7 +102,7 @@ pub fn assert_subshell_banner_is_showing() -> TestStep {
|
||||
.block_list_mut()
|
||||
.active_block()
|
||||
.block_banner(),
|
||||
Some(WithinBlockBanner::WarpifyBanner(..))
|
||||
Some(WithinBlockBanner::WormholeBanner(..))
|
||||
))
|
||||
})
|
||||
})
|
||||
@@ -132,10 +132,10 @@ pub fn assert_subshell_is_bootstrapped(tab_index: usize, pane_index: usize) -> T
|
||||
};
|
||||
|
||||
match rich_content_type {
|
||||
Some(RichContentType::WarpifySuccessBlock) => {}
|
||||
Some(RichContentType::WormholeSuccessBlock) => {}
|
||||
_ => {
|
||||
return AssertionOutcome::failure(
|
||||
"Warpify success block wasn't added to the blocklist".to_owned(),
|
||||
"Wormhole success block wasn't added to the blocklist".to_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,19 @@ impl RemoteTransport for SshTransport {
|
||||
fn check_binary(&self) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send>> {
|
||||
let socket_path = self.socket_path.clone();
|
||||
Box::pin(async move {
|
||||
let cmd = remote_server::setup::binary_check_command();
|
||||
let binary = remote_server::setup::remote_server_binary();
|
||||
let expected_helper_version = if remote_server::setup::uses_static_linux_helper() {
|
||||
let platform = detect_remote_platform(&socket_path).await?;
|
||||
installation::local_helper_version(&platform).await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let cmd = match expected_helper_version {
|
||||
Some(version) => format!(
|
||||
"{binary} --version >/dev/null && test \"$(cat {binary}.wormhole-version 2>/dev/null)\" = \"{version}\""
|
||||
),
|
||||
None => remote_server::setup::binary_check_command(),
|
||||
};
|
||||
log::info!("Running binary check: {cmd}");
|
||||
let output = remote_server::ssh::run_ssh_command(
|
||||
&socket_path,
|
||||
@@ -161,16 +173,18 @@ impl RemoteTransport for SshTransport {
|
||||
)
|
||||
.await?;
|
||||
// `<binary> --version` exits 0 when present, executable, and
|
||||
// functional. Exit 127 means the binary was not found, and 126
|
||||
// means it exists but is not executable. Any other non-zero
|
||||
// exit (e.g. SSH exit 255 for a dead connection, or signal
|
||||
// termination) is treated as a transport-level failure.
|
||||
// functional. Static helpers additionally compare their bundled
|
||||
// build marker, where exit 1 means the remote copy is stale.
|
||||
// Exit 127 means the binary was not found, and 126 means it exists
|
||||
// but is not executable. Any other non-zero exit (e.g. SSH exit
|
||||
// 255 for a dead connection, or signal termination) is treated as
|
||||
// a transport-level failure.
|
||||
let code = output.status.code();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
log::info!("Binary check result: exit={code:?} stdout={stdout}");
|
||||
match code {
|
||||
Some(0) => Ok(true),
|
||||
Some(126) | Some(127) => Ok(false),
|
||||
Some(1) | Some(126) | Some(127) => Ok(false),
|
||||
Some(code) => {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
Err(Error::Other(anyhow::anyhow!(
|
||||
|
||||
@@ -4,6 +4,8 @@ mod scp_fallback;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use galaxy_core::channel::{Channel, ChannelState};
|
||||
use remote_server::setup::RemotePlatform;
|
||||
use remote_server::ssh::SshCommandError;
|
||||
use remote_server::transport::{Error, InstallOutcome, InstallSource};
|
||||
|
||||
@@ -13,28 +15,38 @@ use remote_server::transport::{Error, InstallOutcome, InstallSource};
|
||||
pub(super) async fn install_binary(socket_path: &Path) -> InstallOutcome {
|
||||
let binary_path = remote_server::setup::remote_server_binary();
|
||||
log::info!("Installing remote server binary to {binary_path}");
|
||||
let mut outcome = match install_on_server(socket_path).await {
|
||||
Ok(()) => InstallOutcome {
|
||||
source: Some(InstallSource::Server),
|
||||
result: Ok(()),
|
||||
},
|
||||
Err(server_err) => {
|
||||
if scp_fallback::should_try_install(&server_err) {
|
||||
log::info!("Remote server install failed; falling back to SCP upload");
|
||||
match scp_fallback::install(socket_path).await {
|
||||
Ok(()) => InstallOutcome {
|
||||
source: Some(InstallSource::Client),
|
||||
result: Ok(()),
|
||||
},
|
||||
Err(e) => InstallOutcome {
|
||||
source: Some(InstallSource::Client),
|
||||
result: Err(e),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
InstallOutcome {
|
||||
source: Some(InstallSource::Server),
|
||||
result: Err(server_err),
|
||||
let mut outcome = if matches!(ChannelState::channel(), Channel::Local | Channel::Oss) {
|
||||
// Local-first builds never contact Warp's release service. Their
|
||||
// statically linked Linux helpers are bundled with Galaxy and copied
|
||||
// through the SSH connection instead.
|
||||
InstallOutcome {
|
||||
source: Some(InstallSource::Client),
|
||||
result: scp_fallback::install_local_helper(socket_path).await,
|
||||
}
|
||||
} else {
|
||||
match install_on_server(socket_path).await {
|
||||
Ok(()) => InstallOutcome {
|
||||
source: Some(InstallSource::Server),
|
||||
result: Ok(()),
|
||||
},
|
||||
Err(server_err) => {
|
||||
if scp_fallback::should_try_install(&server_err) {
|
||||
log::info!("Remote server install failed; falling back to SCP upload");
|
||||
match scp_fallback::install(socket_path).await {
|
||||
Ok(()) => InstallOutcome {
|
||||
source: Some(InstallSource::Client),
|
||||
result: Ok(()),
|
||||
},
|
||||
Err(e) => InstallOutcome {
|
||||
source: Some(InstallSource::Client),
|
||||
result: Err(e),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
InstallOutcome {
|
||||
source: Some(InstallSource::Server),
|
||||
result: Err(server_err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,6 +85,13 @@ pub(super) async fn install_binary(socket_path: &Path) -> InstallOutcome {
|
||||
outcome
|
||||
}
|
||||
|
||||
/// Returns the build marker for the static helper bundled for `platform`, if
|
||||
/// this client has one. The SSH transport uses this to avoid copying the helper
|
||||
/// when the matching build is already installed remotely.
|
||||
pub(super) async fn local_helper_version(platform: &RemotePlatform) -> Option<String> {
|
||||
scp_fallback::local_helper_version(platform).await
|
||||
}
|
||||
|
||||
/// Runs the install script on the remote host to download and install the
|
||||
/// binary directly from the CDN.
|
||||
async fn install_on_server(socket_path: &Path) -> Result<(), Error> {
|
||||
|
||||
@@ -8,6 +8,9 @@ use remote_server::setup::RemotePlatform;
|
||||
use remote_server::transport::Error;
|
||||
|
||||
const REMOTE_SERVER_TARBALL_CACHE_FILE_NAME: &str = "oz.tar.gz";
|
||||
const WORMHOLE_HELPER_TARBALL_FILE_NAME: &str = "galaxy-wormhole.tar.gz";
|
||||
const WORMHOLE_HELPER_VERSION_FILE_NAME: &str = "galaxy-wormhole.version";
|
||||
const WORMHOLE_HELPERS_DIR_ENV: &str = "GALAXY_WORMHOLE_HELPERS_DIR";
|
||||
|
||||
const REMOTE_SERVER_TARBALL_DOWNLOAD_ATTEMPTS: usize = 3;
|
||||
// The local SCP fallback download can run over slow or captive networks. Match
|
||||
@@ -25,6 +28,29 @@ pub(super) fn should_try_install(error: &Error) -> bool {
|
||||
!matches!(error, Error::ScriptFailed { exit_code, .. } if *exit_code == 2)
|
||||
}
|
||||
|
||||
/// Installs the static Linux helper shipped with local-first Galaxy builds.
|
||||
/// No network download is attempted: the selected artifact is copied directly
|
||||
/// through the existing SSH control connection.
|
||||
pub(super) async fn install_local_helper(socket_path: &Path) -> Result<(), Error> {
|
||||
let platform = super::super::detect_remote_platform(socket_path).await?;
|
||||
let client_tarball_path = local_helper_tarball(&platform).ok_or_else(|| {
|
||||
Error::Other(anyhow::anyhow!(
|
||||
"Galaxy does not contain a Wormhole helper for Linux {}. Expected {} under the bundled resources or {}.",
|
||||
platform.arch.as_str(),
|
||||
helper_relative_path(&platform)
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_else(|| "a supported Linux platform directory".to_string()),
|
||||
WORMHOLE_HELPERS_DIR_ENV,
|
||||
))
|
||||
})?;
|
||||
|
||||
log::info!(
|
||||
"Using bundled Wormhole helper at {}",
|
||||
client_tarball_path.display()
|
||||
);
|
||||
install_tarball(socket_path, &client_tarball_path).await
|
||||
}
|
||||
|
||||
/// Installs the remote server via SCP fallback.
|
||||
///
|
||||
/// The tarball is downloaded or reused from the local cache first, then uploaded
|
||||
@@ -36,6 +62,10 @@ pub(super) async fn install(socket_path: &Path) -> Result<(), Error> {
|
||||
let client_tarball_path = cached_remote_server_tarball(&platform)
|
||||
.await
|
||||
.map_err(Error::Other)?;
|
||||
install_tarball(socket_path, &client_tarball_path).await
|
||||
}
|
||||
|
||||
async fn install_tarball(socket_path: &Path, client_tarball_path: &Path) -> Result<(), Error> {
|
||||
let timeout = remote_server::setup::SCP_INSTALL_TIMEOUT;
|
||||
let install_dir = remote_server::setup::remote_server_dir();
|
||||
let remote_tarball_name = format!("oz-upload-{}.tar.gz", uuid::Uuid::new_v4());
|
||||
@@ -63,7 +93,7 @@ pub(super) async fn install(socket_path: &Path) -> Result<(), Error> {
|
||||
log::info!("Uploading tarball to remote at {remote_tarball_path}");
|
||||
remote_server::ssh::scp_upload(
|
||||
socket_path,
|
||||
&client_tarball_path,
|
||||
client_tarball_path,
|
||||
&remote_tarball_path,
|
||||
timeout,
|
||||
)
|
||||
@@ -88,6 +118,58 @@ pub(super) async fn install(socket_path: &Path) -> Result<(), Error> {
|
||||
}
|
||||
}
|
||||
|
||||
fn helper_relative_path(platform: &RemotePlatform) -> Option<PathBuf> {
|
||||
if !matches!(&platform.os, remote_server::setup::RemoteOs::Linux) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
PathBuf::from(format!("linux-{}", platform.arch.as_str()))
|
||||
.join(WORMHOLE_HELPER_TARBALL_FILE_NAME),
|
||||
)
|
||||
}
|
||||
|
||||
fn helper_roots() -> Vec<PathBuf> {
|
||||
let mut roots = Vec::new();
|
||||
if let Some(path) = std::env::var_os(WORMHOLE_HELPERS_DIR_ENV) {
|
||||
roots.push(path.into());
|
||||
}
|
||||
if let Some(resources_dir) = galaxy_core::paths::bundled_resources_dir() {
|
||||
roots.push(resources_dir.join("wormhole-helpers"));
|
||||
}
|
||||
if cfg!(debug_assertions) {
|
||||
roots.push(
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.expect("app manifest directory should have a workspace parent")
|
||||
.join("resources")
|
||||
.join("wormhole-helpers"),
|
||||
);
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
||||
fn local_helper_tarball(platform: &RemotePlatform) -> Option<PathBuf> {
|
||||
let relative_path = helper_relative_path(platform)?;
|
||||
helper_roots()
|
||||
.into_iter()
|
||||
.map(|root| root.join(&relative_path))
|
||||
.find(|path| {
|
||||
std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file() && metadata.len() > 0)
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn local_helper_version(platform: &RemotePlatform) -> Option<String> {
|
||||
let tarball = local_helper_tarball(platform)?;
|
||||
let version_path = tarball.parent()?.join(WORMHOLE_HELPER_VERSION_FILE_NAME);
|
||||
let version = async_fs::read_to_string(version_path).await.ok()?;
|
||||
let version = version.trim();
|
||||
if version.is_empty() || !version.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return None;
|
||||
}
|
||||
Some(version.to_owned())
|
||||
}
|
||||
|
||||
fn remote_server_tarball_cache_root() -> PathBuf {
|
||||
galaxy_core::paths::cache_dir()
|
||||
.join("remote-server")
|
||||
@@ -124,6 +206,11 @@ async fn is_valid_cached_tarball(path: &Path) -> bool {
|
||||
/// Reuses an existing cached tarball when available; otherwise downloads the
|
||||
/// tarball into the cache and returns the newly cached path.
|
||||
async fn cached_remote_server_tarball(platform: &RemotePlatform) -> anyhow::Result<PathBuf> {
|
||||
if let Some(path) = local_helper_tarball(platform) {
|
||||
log::info!("Using bundled Wormhole helper at {}", path.display());
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
let cache_path = remote_server_tarball_cache_path(platform);
|
||||
if is_valid_cached_tarball(&cache_path).await {
|
||||
log::info!(
|
||||
|
||||
@@ -2749,7 +2749,7 @@ impl RootView {
|
||||
}
|
||||
|
||||
/// Insert a command that should create a subshell. If we support bootstrapping AKA
|
||||
/// "warpifying" its [`ShellType`], set a flag to automatically bootstrap it when the command's
|
||||
/// "wormholing" its [`ShellType`], set a flag to automatically bootstrap it when the command's
|
||||
/// block receives the [`AfterBlockStarted`] event.
|
||||
pub fn insert_subshell_command_and_bootstrap_if_supported(
|
||||
&mut self,
|
||||
|
||||
@@ -1739,7 +1739,7 @@ pub enum TelemetryEvent {
|
||||
AddAddedSubshellCommand,
|
||||
RemoveAddedSubshellCommand,
|
||||
ReceivedSubshellRcFileDcs,
|
||||
ToggleSshWarpification {
|
||||
ToggleSshWormholing {
|
||||
enabled: bool,
|
||||
},
|
||||
/// User changed the SSH extension install mode.
|
||||
@@ -1751,11 +1751,11 @@ pub enum TelemetryEvent {
|
||||
SshRemoteServerChoiceDoNotAskAgainToggled {
|
||||
checked: bool,
|
||||
},
|
||||
WarpifyFooterShown {
|
||||
WormholeFooterShown {
|
||||
is_ssh: bool,
|
||||
},
|
||||
AgentToolbarDismissed,
|
||||
WarpifyFooterAcceptedWarpify {
|
||||
WormholeFooterAcceptedWormhole {
|
||||
is_ssh: bool,
|
||||
},
|
||||
ShowAliasExpansionBanner,
|
||||
@@ -1861,7 +1861,7 @@ pub enum TelemetryEvent {
|
||||
team_uid: ServerId,
|
||||
},
|
||||
CopyObjectToClipboard(TelemetryCloudObjectType),
|
||||
OpenAndWarpifyDockerSubshell {
|
||||
OpenAndWormholeDockerSubshell {
|
||||
/// Some variant if we support this shell type, and None otherwise.
|
||||
shell_type: Option<ShellType>,
|
||||
},
|
||||
@@ -3438,8 +3438,8 @@ impl TelemetryEvent {
|
||||
Some(json!({ "remember": remember }))
|
||||
}
|
||||
TelemetryEvent::AgentToolbarDismissed => None,
|
||||
TelemetryEvent::WarpifyFooterShown { is_ssh }
|
||||
| TelemetryEvent::WarpifyFooterAcceptedWarpify { is_ssh } => {
|
||||
TelemetryEvent::WormholeFooterShown { is_ssh }
|
||||
| TelemetryEvent::WormholeFooterAcceptedWormhole { is_ssh } => {
|
||||
Some(json!({ "is_ssh": is_ssh }))
|
||||
}
|
||||
TelemetryEvent::ToggleSameLinePrompt { enabled } => Some(json!({ "enabled": enabled })),
|
||||
@@ -3512,7 +3512,7 @@ impl TelemetryEvent {
|
||||
TelemetryEvent::CopyObjectToClipboard(object_type) => {
|
||||
Some(json!({ "object_type": object_type }))
|
||||
}
|
||||
TelemetryEvent::OpenAndWarpifyDockerSubshell { shell_type } => {
|
||||
TelemetryEvent::OpenAndWormholeDockerSubshell { shell_type } => {
|
||||
Some(json!({ "shell_type": shell_type }))
|
||||
}
|
||||
TelemetryEvent::ToggleBlockFilterQuery { enabled, source } => {
|
||||
@@ -3536,7 +3536,7 @@ impl TelemetryEvent {
|
||||
TelemetryEvent::ToggleNewWindowsAtCustomSize { enabled } => {
|
||||
Some(json!({"enabled": enabled}))
|
||||
}
|
||||
TelemetryEvent::ToggleSshWarpification { enabled } => Some(json!({"enabled": enabled})),
|
||||
TelemetryEvent::ToggleSshWormholing { enabled } => Some(json!({"enabled": enabled})),
|
||||
TelemetryEvent::SetSshExtensionInstallMode { mode } => Some(json!({"mode": mode})),
|
||||
TelemetryEvent::SshRemoteServerChoiceDoNotAskAgainToggled { checked } => {
|
||||
Some(json!({"checked": checked}))
|
||||
@@ -5053,9 +5053,9 @@ impl TelemetryEvent {
|
||||
| TelemetryEvent::AddAddedSubshellCommand
|
||||
| TelemetryEvent::RemoveAddedSubshellCommand
|
||||
| TelemetryEvent::ReceivedSubshellRcFileDcs
|
||||
| TelemetryEvent::WarpifyFooterShown { .. }
|
||||
| TelemetryEvent::WormholeFooterShown { .. }
|
||||
| TelemetryEvent::AgentToolbarDismissed
|
||||
| TelemetryEvent::WarpifyFooterAcceptedWarpify { .. }
|
||||
| TelemetryEvent::WormholeFooterAcceptedWormhole { .. }
|
||||
| TelemetryEvent::ShowAliasExpansionBanner
|
||||
| TelemetryEvent::EnableAliasExpansionFromBanner
|
||||
| TelemetryEvent::DismissAliasExpansionBanner
|
||||
@@ -5099,7 +5099,7 @@ impl TelemetryEvent {
|
||||
| TelemetryEvent::LogOut
|
||||
| TelemetryEvent::InviteTeammates { .. }
|
||||
| TelemetryEvent::CopyObjectToClipboard(_)
|
||||
| TelemetryEvent::OpenAndWarpifyDockerSubshell { .. }
|
||||
| TelemetryEvent::OpenAndWormholeDockerSubshell { .. }
|
||||
| TelemetryEvent::UpdateBlockFilterQuery
|
||||
| TelemetryEvent::UpdateBlockFilterQueryContextLines { .. }
|
||||
| TelemetryEvent::ToggleBlockFilterQuery { .. }
|
||||
@@ -5174,7 +5174,7 @@ impl TelemetryEvent {
|
||||
| TelemetryEvent::MCPServerSpawned { .. }
|
||||
| TelemetryEvent::MCPToolCallAccepted { .. }
|
||||
| TelemetryEvent::ExecutedWarpDrivePrompt { .. }
|
||||
| TelemetryEvent::ToggleSshWarpification { .. }
|
||||
| TelemetryEvent::ToggleSshWormholing { .. }
|
||||
| TelemetryEvent::SetSshExtensionInstallMode { .. }
|
||||
| TelemetryEvent::SshRemoteServerChoiceDoNotAskAgainToggled { .. }
|
||||
| TelemetryEvent::SettingsImportInitiated
|
||||
@@ -5604,12 +5604,12 @@ impl TelemetryEventDesc for TelemetryEventDiscriminants {
|
||||
Self::TriggerSubshellBootstrap => EnablementState::Always,
|
||||
Self::AddDenylistedSubshellCommand => EnablementState::Always,
|
||||
Self::RemoveDenylistedSubshellCommand => EnablementState::Always,
|
||||
Self::ToggleSshWarpification => EnablementState::Always,
|
||||
Self::ToggleSshWormholing => EnablementState::Always,
|
||||
Self::SetSshExtensionInstallMode => EnablementState::Always,
|
||||
Self::SshRemoteServerChoiceDoNotAskAgainToggled => EnablementState::Always,
|
||||
Self::WarpifyFooterShown
|
||||
Self::WormholeFooterShown
|
||||
| Self::AgentToolbarDismissed
|
||||
| Self::WarpifyFooterAcceptedWarpify => EnablementState::Always,
|
||||
| Self::WormholeFooterAcceptedWormhole => EnablementState::Always,
|
||||
Self::AddAddedSubshellCommand => EnablementState::Always,
|
||||
Self::RemoveAddedSubshellCommand => EnablementState::Always,
|
||||
Self::ReceivedSubshellRcFileDcs => EnablementState::Always,
|
||||
@@ -5640,7 +5640,7 @@ impl TelemetryEventDesc for TelemetryEventDiscriminants {
|
||||
Self::SettingsImportInitiated => EnablementState::Always,
|
||||
Self::InviteTeammates => EnablementState::Always,
|
||||
Self::CopyObjectToClipboard => EnablementState::Always,
|
||||
Self::OpenAndWarpifyDockerSubshell => EnablementState::Always,
|
||||
Self::OpenAndWormholeDockerSubshell => EnablementState::Always,
|
||||
Self::UpdateBlockFilterQuery => EnablementState::Always,
|
||||
Self::UpdateBlockFilterQueryContextLines => EnablementState::Always,
|
||||
Self::ToggleBlockFilterQuery => EnablementState::Always,
|
||||
@@ -6110,14 +6110,14 @@ impl TelemetryEventDesc for TelemetryEventDiscriminants {
|
||||
Self::AddAddedSubshellCommand => "Add Added Subshell Command",
|
||||
Self::RemoveAddedSubshellCommand => "Remove Added Subshell Command",
|
||||
Self::ReceivedSubshellRcFileDcs => "Received Subshell RC File DCS",
|
||||
Self::ToggleSshWarpification => "Toggle SSH Warpification",
|
||||
Self::ToggleSshWormholing => "Toggle SSH Wormholing",
|
||||
Self::SetSshExtensionInstallMode => "Set SSH Extension Install Mode",
|
||||
Self::SshRemoteServerChoiceDoNotAskAgainToggled => {
|
||||
"SSH Remote Server Choice Do Not Ask Again Toggled"
|
||||
}
|
||||
Self::WarpifyFooterShown => "Warpify Footer Shown",
|
||||
Self::WormholeFooterShown => "Wormhole Footer Shown",
|
||||
Self::AgentToolbarDismissed => "Agent Toolbar Dismissed",
|
||||
Self::WarpifyFooterAcceptedWarpify => "Warpify Footer Accepted Warpify",
|
||||
Self::WormholeFooterAcceptedWormhole => "Wormhole Footer Accepted Wormhole",
|
||||
Self::ShowAliasExpansionBanner => "Show Alias Expansion Banner",
|
||||
Self::DismissAliasExpansionBanner => "Dismiss Alias Expansion Banner",
|
||||
Self::EnableAliasExpansionFromBanner => "Enable Alias Expansion From Banner",
|
||||
@@ -6155,7 +6155,7 @@ impl TelemetryEventDesc for TelemetryEventDiscriminants {
|
||||
Self::SettingsImportInitiated => "Settings Import Initiated",
|
||||
Self::InviteTeammates => "Invited Teammates",
|
||||
Self::CopyObjectToClipboard => "Copy Object To Clipboard",
|
||||
Self::OpenAndWarpifyDockerSubshell => "OpenAndWarpifyDockerSubshell",
|
||||
Self::OpenAndWormholeDockerSubshell => "OpenAndWormholeDockerSubshell",
|
||||
Self::UpdateBlockFilterQuery => "Update Block Filter Query",
|
||||
Self::ToggleBlockFilterQuery => "Toggle Block Filter Query",
|
||||
Self::ToggleBlockFilterCaseSensitivity => "Toggle Block Filter Case Sensitivity",
|
||||
@@ -6790,28 +6790,28 @@ impl TelemetryEventDesc for TelemetryEventDiscriminants {
|
||||
"Enabled or disabled preserving the active tab color"
|
||||
}
|
||||
Self::ShowSubshellBanner => {
|
||||
"Displayed the banner asking whether Warp should Warpify the current session via Warp's subshell wrapper"
|
||||
"Displayed the banner asking whether Galaxy should Wormhole the current session via Galaxy's subshell wrapper"
|
||||
}
|
||||
Self::DeclineSubshellBootstrap => {
|
||||
"Developer declined the Warp banner to Warpify the current session"
|
||||
"Developer declined the Galaxy banner to Wormhole the current session"
|
||||
}
|
||||
Self::TriggerSubshellBootstrap => {
|
||||
"Attempted to Warpify the current session via Warp's subshell wrapper"
|
||||
"Attempted to Wormhole the current session via Galaxy's subshell wrapper"
|
||||
}
|
||||
Self::AddDenylistedSubshellCommand => {
|
||||
"Explicitly prevent a command from being Warpified via Warp's subshell wrapper"
|
||||
"Explicitly prevent a command from being Wormholed via Galaxy's subshell wrapper"
|
||||
}
|
||||
Self::RemoveDenylistedSubshellCommand => {
|
||||
"Removed a command from the list of commands to IGNORE when trying to Warpify via Warp's subshell wrapper"
|
||||
"Removed a command from the list of commands to IGNORE when trying to Wormhole via Galaxy's subshell wrapper"
|
||||
}
|
||||
Self::AddAddedSubshellCommand => {
|
||||
"Added a command to be automatically Warpified via Warp's subshell wrapper"
|
||||
"Added a command to be automatically Wormholed via Galaxy's subshell wrapper"
|
||||
}
|
||||
Self::RemoveAddedSubshellCommand => {
|
||||
"Removed a command from the list of commands to automatically Warpify via Warp's subshell wrapper"
|
||||
"Removed a command from the list of commands to automatically Wormhole via Galaxy's subshell wrapper"
|
||||
}
|
||||
Self::ReceivedSubshellRcFileDcs => "Spawned a subshell to be automatically Warpified",
|
||||
Self::ToggleSshWarpification => "Changed the setting for SSH sessions to be warified",
|
||||
Self::ReceivedSubshellRcFileDcs => "Spawned a subshell to be automatically Wormholed",
|
||||
Self::ToggleSshWormholing => "Changed the setting for SSH sessions to be wormholed",
|
||||
Self::SetSshExtensionInstallMode => {
|
||||
"Changed the SSH extension install mode (always ask / always allow / always skip)"
|
||||
}
|
||||
@@ -6819,11 +6819,11 @@ impl TelemetryEventDesc for TelemetryEventDiscriminants {
|
||||
"Toggled the 'Don't ask me this again' checkbox on the SSH remote-server choice block"
|
||||
}
|
||||
Self::AgentModeRatedResponse => "User rated an Agent Mode response",
|
||||
Self::WarpifyFooterShown => {
|
||||
"Displayed the warpify footer for a detected subshell or SSH session"
|
||||
Self::WormholeFooterShown => {
|
||||
"Displayed the wormhole footer for a detected subshell or SSH session"
|
||||
}
|
||||
Self::AgentToolbarDismissed => "User dismissed the use-agent toolbar",
|
||||
Self::WarpifyFooterAcceptedWarpify => "User clicked Warpify in the warpify footer",
|
||||
Self::WormholeFooterAcceptedWormhole => "User clicked Wormhole in the wormhole footer",
|
||||
Self::ShowAliasExpansionBanner => {
|
||||
"Displayed the banner asking whether Warp should automatically expand aliases within the Input Editor"
|
||||
}
|
||||
@@ -6903,8 +6903,8 @@ impl TelemetryEventDesc for TelemetryEventDiscriminants {
|
||||
Self::SettingsImportInitiated => "Started the import settings flow for new users",
|
||||
Self::InviteTeammates => "Sent emails to invite teammates to join Warp Drive team",
|
||||
Self::CopyObjectToClipboard => "Copied an object to the user's keyboard",
|
||||
Self::OpenAndWarpifyDockerSubshell => {
|
||||
"Warpifying a docker subshell from using the docker extension"
|
||||
Self::OpenAndWormholeDockerSubshell => {
|
||||
"Wormholing a docker subshell from using the docker extension"
|
||||
}
|
||||
Self::UpdateBlockFilterQuery => "When a new filter is applied to a block",
|
||||
Self::UpdateBlockFilterQueryContextLines => {
|
||||
|
||||
@@ -33,7 +33,7 @@ use crate::terminal::safe_mode_settings::SafeModeSettings;
|
||||
use crate::terminal::session_settings::{SessionSettings, SessionSettingsChangedEvent};
|
||||
use crate::terminal::settings::TerminalSettings;
|
||||
use crate::terminal::shared_session::settings::SharedSessionSettings;
|
||||
use crate::terminal::warpify::settings::WarpifySettings;
|
||||
use crate::terminal::wormhole::settings::WormholeSettings;
|
||||
use crate::terminal::BlockListSettings;
|
||||
use crate::undo_close::UndoCloseSettings;
|
||||
use crate::window_settings::WindowSettings;
|
||||
@@ -86,7 +86,7 @@ pub fn register_all_settings(ctx: &mut AppContext) {
|
||||
AppIconSettings::register(ctx);
|
||||
AppEditorSettings::register(ctx);
|
||||
InputSettings::register(ctx);
|
||||
WarpifySettings::register(ctx);
|
||||
WormholeSettings::register(ctx);
|
||||
AltScreenReporting::register(ctx);
|
||||
UndoCloseSettings::register(ctx);
|
||||
SshSettings::register(ctx);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use galaxyui::platform::linux;
|
||||
use settings::macros::define_settings_group;
|
||||
use settings::{SupportedPlatforms, SyncToCloud};
|
||||
use settings::{Setting as _, SupportedPlatforms, SyncToCloud};
|
||||
|
||||
define_settings_group!(LinuxAppConfiguration,
|
||||
settings: [
|
||||
|
||||
@@ -28,6 +28,7 @@ mod onboarding;
|
||||
mod pane;
|
||||
mod privacy;
|
||||
mod same_line_prompt_block;
|
||||
pub mod schema_export;
|
||||
mod scroll;
|
||||
mod select;
|
||||
mod ssh;
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context;
|
||||
use galaxy_core::channel::{Channel, ChannelState};
|
||||
use galaxy_core::features::{
|
||||
FeatureFlag, DEBUG_FLAGS, DOGFOOD_FLAGS, PREVIEW_FLAGS, RELEASE_FLAGS,
|
||||
};
|
||||
use schemars::SchemaGenerator;
|
||||
use serde_json::{Map, Value};
|
||||
use settings::schema::SettingSchemaEntry;
|
||||
|
||||
fn strip_numeric_metadata(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
let is_numeric = map
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|value_type| value_type == "integer" || value_type == "number");
|
||||
|
||||
if is_numeric {
|
||||
map.remove("minimum");
|
||||
map.remove("maximum");
|
||||
map.remove("format");
|
||||
}
|
||||
|
||||
for value in map.values_mut() {
|
||||
strip_numeric_metadata(value);
|
||||
}
|
||||
}
|
||||
Value::Array(values) => {
|
||||
for value in values {
|
||||
strip_numeric_metadata(value);
|
||||
}
|
||||
}
|
||||
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_empty_enum_entries(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
if let Some(Value::Array(one_of)) = map.get_mut("oneOf") {
|
||||
one_of.retain(|entry| {
|
||||
!matches!(entry, Value::Object(object)
|
||||
if object.get("enum").is_some_and(|value| value.as_array().is_some_and(|values| values.is_empty())))
|
||||
});
|
||||
}
|
||||
|
||||
for value in map.values_mut() {
|
||||
strip_empty_enum_entries(value);
|
||||
}
|
||||
}
|
||||
Value::Array(values) => {
|
||||
for value in values {
|
||||
strip_empty_enum_entries(value);
|
||||
}
|
||||
}
|
||||
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn active_flags_for_channel(channel: &str) -> HashSet<FeatureFlag> {
|
||||
let mut flags = HashSet::new();
|
||||
|
||||
let flag_lists: &[&[FeatureFlag]] = match channel {
|
||||
"stable" => &[RELEASE_FLAGS],
|
||||
"preview" => &[RELEASE_FLAGS, PREVIEW_FLAGS],
|
||||
"dev" => &[RELEASE_FLAGS, PREVIEW_FLAGS, DOGFOOD_FLAGS, DEBUG_FLAGS],
|
||||
other => {
|
||||
log::warn!("Unknown settings schema channel '{other}', defaulting to dev");
|
||||
&[RELEASE_FLAGS, PREVIEW_FLAGS, DOGFOOD_FLAGS, DEBUG_FLAGS]
|
||||
}
|
||||
};
|
||||
|
||||
for list in flag_lists {
|
||||
flags.extend(*list);
|
||||
}
|
||||
|
||||
flags
|
||||
}
|
||||
|
||||
fn ensure_hierarchy<'a>(
|
||||
root_properties: &'a mut Map<String, Value>,
|
||||
hierarchy: &str,
|
||||
) -> &'a mut Map<String, Value> {
|
||||
let mut current = root_properties;
|
||||
|
||||
for segment in hierarchy.split('.') {
|
||||
let entry = current.entry(segment.to_string()).or_insert_with(|| {
|
||||
Value::Object({
|
||||
let mut map = Map::new();
|
||||
map.insert("type".to_string(), Value::String("object".to_string()));
|
||||
map.insert("properties".to_string(), Value::Object(Map::new()));
|
||||
map
|
||||
})
|
||||
});
|
||||
|
||||
current = entry
|
||||
.as_object_mut()
|
||||
.expect("hierarchy node should be an object")
|
||||
.entry("properties")
|
||||
.or_insert_with(|| Value::Object(Map::new()))
|
||||
.as_object_mut()
|
||||
.expect("properties should be an object");
|
||||
}
|
||||
|
||||
current
|
||||
}
|
||||
|
||||
/// Generates the user-facing JSON schema for Galaxy's TOML settings.
|
||||
pub fn generate_settings_schema(channel: &str) -> (String, usize) {
|
||||
let active_flags = active_flags_for_channel(channel);
|
||||
let mut generator = SchemaGenerator::default();
|
||||
let mut root_properties = Map::new();
|
||||
let mut entry_count = 0;
|
||||
|
||||
for entry in inventory::iter::<SettingSchemaEntry> {
|
||||
if entry.is_private {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(flag) = entry.feature_flag {
|
||||
if !active_flags.contains(&flag) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let type_schema = (entry.schema_fn)(&mut generator);
|
||||
let mut schema_value: Value = type_schema.to_value();
|
||||
let default_json = (entry.file_default_value_fn)();
|
||||
|
||||
if let Ok(default_value) = serde_json::from_str::<Value>(&default_json) {
|
||||
if let Some(object) = schema_value.as_object_mut() {
|
||||
object.insert("default".to_string(), default_value);
|
||||
}
|
||||
}
|
||||
|
||||
if !entry.description.is_empty() {
|
||||
if let Some(object) = schema_value.as_object_mut() {
|
||||
object.insert(
|
||||
"description".to_string(),
|
||||
Value::String(entry.description.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let target = if let Some(hierarchy) = entry.hierarchy {
|
||||
ensure_hierarchy(&mut root_properties, hierarchy)
|
||||
} else {
|
||||
&mut root_properties
|
||||
};
|
||||
|
||||
target.insert(entry.storage_key.to_string(), schema_value);
|
||||
entry_count += 1;
|
||||
}
|
||||
|
||||
let definitions = generator.take_definitions(true);
|
||||
let mut root = Map::new();
|
||||
root.insert(
|
||||
"$schema".to_string(),
|
||||
Value::String("https://json-schema.org/draft/2020-12/schema".to_string()),
|
||||
);
|
||||
root.insert(
|
||||
"title".to_string(),
|
||||
Value::String("Galaxy Settings".to_string()),
|
||||
);
|
||||
root.insert(
|
||||
"description".to_string(),
|
||||
Value::String(format!(
|
||||
"JSON Schema for Galaxy settings ({channel} channel, {entry_count} settings)"
|
||||
)),
|
||||
);
|
||||
root.insert("type".to_string(), Value::String("object".to_string()));
|
||||
root.insert("properties".to_string(), Value::Object(root_properties));
|
||||
|
||||
if !definitions.is_empty() {
|
||||
root.insert("$defs".to_string(), Value::Object(definitions));
|
||||
}
|
||||
|
||||
let mut root_value = Value::Object(root);
|
||||
strip_numeric_metadata(&mut root_value);
|
||||
strip_empty_enum_entries(&mut root_value);
|
||||
|
||||
(
|
||||
serde_json::to_string_pretty(&root_value).expect("settings schema should serialize"),
|
||||
entry_count,
|
||||
)
|
||||
}
|
||||
|
||||
fn runtime_schema_channel() -> &'static str {
|
||||
match ChannelState::channel() {
|
||||
Channel::Stable => "stable",
|
||||
Channel::Preview => "preview",
|
||||
Channel::Dev | Channel::Local | Channel::Oss | Channel::Integration => "dev",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the bundled settings schema, or generates a current local copy for development runs.
|
||||
pub fn ensure_runtime_settings_schema() -> anyhow::Result<PathBuf> {
|
||||
if let Some(schema_path) = galaxy_core::paths::bundled_resources_dir()
|
||||
.map(|resources| resources.join("settings_schema.json"))
|
||||
.filter(|path| path.is_file())
|
||||
{
|
||||
return Ok(schema_path);
|
||||
}
|
||||
|
||||
let schema_path = galaxy_core::paths::config_local_dir().join("settings_schema.json");
|
||||
let (schema, _) = generate_settings_schema(runtime_schema_channel());
|
||||
let existing_schema = std::fs::read_to_string(&schema_path).ok();
|
||||
|
||||
if existing_schema.as_deref() != Some(schema.as_str()) {
|
||||
if let Some(parent) = schema_path.parent() {
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!(
|
||||
"Failed to create settings schema directory {}",
|
||||
parent.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
std::fs::write(&schema_path, schema).with_context(|| {
|
||||
format!(
|
||||
"Failed to write Galaxy settings schema to {}",
|
||||
schema_path.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(schema_path)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ define_settings_group!(SshSettings,
|
||||
sync_to_cloud: SyncToCloud::Never,
|
||||
private: false,
|
||||
storage_key: "ReuseExistingSshControlMaster",
|
||||
toml_path: "warpify.ssh.reuse_existing_control_master",
|
||||
toml_path: "wormhole.ssh.reuse_existing_control_master",
|
||||
description: "Whether the legacy SSH wrapper attaches to an existing SSH ControlMaster for the destination host instead of always creating its own.",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -40,7 +40,7 @@ use settings_page::{
|
||||
HEADER_PADDING,
|
||||
};
|
||||
use teams_page::{TeamsPageView, TeamsPageViewEvent};
|
||||
use warpify_page::{WarpifyPageAction, WarpifyPageView};
|
||||
use wormhole_page::{WormholePageAction, WormholePageView};
|
||||
|
||||
use self::telemetry::SettingsTelemetryEvent;
|
||||
use crate::ai::custom_model_routers::CustomModelRouter;
|
||||
@@ -95,7 +95,7 @@ mod teams_page;
|
||||
mod telemetry;
|
||||
pub mod update_environment_form;
|
||||
mod warp_drive_page;
|
||||
mod warpify_page;
|
||||
mod wormhole_page;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub(crate) use ai_page::cli_agent_settings_widget_id;
|
||||
@@ -233,7 +233,7 @@ pub enum SettingsSection {
|
||||
Scripting,
|
||||
Teams,
|
||||
WarpDrive,
|
||||
Warpify,
|
||||
Wormhole,
|
||||
/// Internal backing-page identifier for AISettingsPageView. Multiple subpages
|
||||
/// (WarpAgent, AgentProfiles, Knowledge, ThirdPartyCLIAgents) share this single
|
||||
/// backing page, so this variant is needed as the key in `settings_pages`.
|
||||
@@ -286,7 +286,7 @@ impl Display for SettingsSection {
|
||||
SettingsSection::ProviderChatGPTSubscription => write!(f, "ChatGPT Subscription"),
|
||||
SettingsSection::ProviderBedrock => write!(f, "Bedrock"),
|
||||
SettingsSection::ProviderACP => write!(f, "ACP"),
|
||||
SettingsSection::Warpify => write!(f, "Wormhole"),
|
||||
SettingsSection::Wormhole => write!(f, "Wormhole"),
|
||||
SettingsSection::CodeIndexing => write!(f, "Indexing and projects"),
|
||||
SettingsSection::EditorAndCodeReview => write!(f, "Editor and Code Review"),
|
||||
_ => write!(f, "{self:?}"),
|
||||
@@ -407,7 +407,6 @@ impl FromStr for SettingsSection {
|
||||
"Privacy" => Ok(Self::Privacy),
|
||||
"Galaxy Control" | "Scripting" => Ok(Self::Scripting),
|
||||
"Teams" => Ok(Self::Teams),
|
||||
"Warpify" => Ok(Self::Warpify),
|
||||
"WarpDrive" | "Galaxy Drive" => Ok(Self::WarpDrive),
|
||||
"Oz" | "Warp Agent" | "Galaxy Agent" => Ok(Self::WarpAgent),
|
||||
"Profiles" | "AgentProfiles" => Ok(Self::AgentProfiles),
|
||||
@@ -423,7 +422,7 @@ impl FromStr for SettingsSection {
|
||||
"Indexing and projects" | "CodeIndexing" => Ok(Self::CodeIndexing),
|
||||
"Editor and Code Review" | "EditorAndCodeReview" => Ok(Self::EditorAndCodeReview),
|
||||
"Experiments" => Ok(Self::Experiments),
|
||||
"Wormhole" => Ok(Self::Warpify),
|
||||
"Wormhole" => Ok(Self::Wormhole),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
@@ -488,7 +487,7 @@ pub mod flags {
|
||||
pub const SCROLL_REPORTING_CONTEXT_FLAG: &str = "Scroll_Reporting";
|
||||
pub const FOCUS_REPORTING_CONTEXT_FLAG: &str = "Focus_Reporting";
|
||||
pub const SSH_REUSE_CONTROL_MASTER_CONTEXT_FLAG: &str = "SSH_Reuse_Control_Master";
|
||||
pub const SSH_WARPIFICATION_CONTEXT_FLAG: &str = "SSH_Warpification";
|
||||
pub const SSH_WORMHOLING_CONTEXT_FLAG: &str = "SSH_Wormholing";
|
||||
pub const NOTIFICATIONS_CONTEXT_FLAG: &str = "Notifications_Enabled";
|
||||
pub const LONG_RUNNING_NOTIFICATIONS_FLAG: &str = "Long_Running_Notifications";
|
||||
pub const AGENT_TASK_COMPLETED_NOTIFICATIONS_FLAG: &str = "Agent_Task_Completed_Notifications";
|
||||
@@ -610,7 +609,7 @@ pub mod flags {
|
||||
pub const IS_AUTOINDEXING_ENABLED: &str = "IsAutoIndexingEnabled";
|
||||
pub const LIGATURE_RENDERING_CONTEXT_FLAG: &str = "Ligature_Rendering_Enabled";
|
||||
pub const HAS_SETTINGS_TO_IMPORT_FLAG: &str = "HasSettingsToImport";
|
||||
/// The user's setting enabled UDI, but we may show a classic input (e.g. ssh/subshell warpification)
|
||||
/// The user's setting enabled UDI, but we may show a classic input (e.g. ssh/subshell wormholing)
|
||||
pub const UNIVERSAL_DEVELOPER_INPUT_ENABLED: &str = "UniversalDeveloperInputEnabled";
|
||||
pub const AGENT_MODE_INPUT: &str = "InputAgentMode";
|
||||
pub const TERMINAL_MODE_INPUT: &str = "InputTerminalMode";
|
||||
@@ -657,7 +656,7 @@ pub fn init_actions_from_parent_view<T: Action + Clone>(
|
||||
) {
|
||||
appearance_page::init_actions_from_parent_view(app, context, builder);
|
||||
features_page::init_actions_from_parent_view(app, context, builder);
|
||||
warpify_page::init_actions_from_parent_view(app, context, builder);
|
||||
wormhole_page::init_actions_from_parent_view(app, context, builder);
|
||||
privacy_page::init_actions_from_parent_view(app, context, builder);
|
||||
ai_page::init_actions_from_parent_view(app, context, builder);
|
||||
code_page::init_actions_from_parent_view(app, context, builder);
|
||||
@@ -965,7 +964,7 @@ pub enum SettingsAction {
|
||||
AI(AISettingsPageAction),
|
||||
Code(CodeSettingsPageAction),
|
||||
WarpDrive(warp_drive_page::WarpDriveSettingsPageAction),
|
||||
WarpifyPageToggle(WarpifyPageAction),
|
||||
WormholePageToggle(WormholePageAction),
|
||||
Tab,
|
||||
Split(Direction),
|
||||
ToggleMaximizePane,
|
||||
@@ -1108,7 +1107,7 @@ macro_rules! update_page {
|
||||
SettingsPageViewHandle::Appearance(handle) => $ctx.update_view(handle, $update),
|
||||
SettingsPageViewHandle::Features(handle) => $ctx.update_view(handle, $update),
|
||||
SettingsPageViewHandle::Keybindings(handle) => $ctx.update_view(handle, $update),
|
||||
SettingsPageViewHandle::Warpify(handle) => $ctx.update_view(handle, $update),
|
||||
SettingsPageViewHandle::Wormhole(handle) => $ctx.update_view(handle, $update),
|
||||
SettingsPageViewHandle::Privacy(handle) => $ctx.update_view(handle, $update),
|
||||
SettingsPageViewHandle::Scripting(handle) => $ctx.update_view(handle, $update),
|
||||
SettingsPageViewHandle::AI(handle) => $ctx.update_view(handle, $update),
|
||||
@@ -1195,9 +1194,9 @@ impl SettingsView {
|
||||
me.handle_code_page_event(event, ctx);
|
||||
});
|
||||
|
||||
let warpify_page_handle = ctx.add_typed_action_view(WarpifyPageView::new);
|
||||
ctx.subscribe_to_view(&warpify_page_handle, |me, _, event, ctx| {
|
||||
me.handle_warpify_page_event(event, ctx);
|
||||
let wormhole_page_handle = ctx.add_typed_action_view(WormholePageView::new);
|
||||
ctx.subscribe_to_view(&wormhole_page_handle, |me, _, event, ctx| {
|
||||
me.handle_wormhole_page_event(event, ctx);
|
||||
});
|
||||
|
||||
// Render the privacy page only if telemetry opt-out is enabled.
|
||||
@@ -1256,7 +1255,7 @@ impl SettingsView {
|
||||
SettingsPage::new(appearance_page_handle),
|
||||
SettingsPage::new(features_page_handle),
|
||||
SettingsPage::new(keybindings_handle),
|
||||
SettingsPage::new(warpify_page_handle),
|
||||
SettingsPage::new(wormhole_page_handle),
|
||||
SettingsPage::new(warp_drive_page_handle),
|
||||
];
|
||||
|
||||
@@ -1291,7 +1290,7 @@ impl SettingsView {
|
||||
SettingsNavItem::Page(SettingsSection::Appearance),
|
||||
SettingsNavItem::Page(SettingsSection::Features),
|
||||
SettingsNavItem::Page(SettingsSection::Keybindings),
|
||||
SettingsNavItem::Page(SettingsSection::Warpify),
|
||||
SettingsNavItem::Page(SettingsSection::Wormhole),
|
||||
SettingsNavItem::Page(SettingsSection::WarpDrive),
|
||||
SettingsNavItem::Page(SettingsSection::Privacy),
|
||||
SettingsNavItem::Page(SettingsSection::About),
|
||||
@@ -1702,7 +1701,7 @@ impl SettingsView {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_warpify_page_event(
|
||||
fn handle_wormhole_page_event(
|
||||
&mut self,
|
||||
event: &SettingsPageEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
@@ -1938,7 +1937,7 @@ impl SettingsView {
|
||||
SettingsPageViewHandle::Appearance(v) => v.as_ref(app).should_render(app),
|
||||
SettingsPageViewHandle::About(v) => v.as_ref(app).should_render(app),
|
||||
SettingsPageViewHandle::Privacy(v) => v.as_ref(app).should_render(app),
|
||||
SettingsPageViewHandle::Warpify(v) => v.as_ref(app).should_render(app),
|
||||
SettingsPageViewHandle::Wormhole(v) => v.as_ref(app).should_render(app),
|
||||
SettingsPageViewHandle::Scripting(v) => v.as_ref(app).should_render(app),
|
||||
SettingsPageViewHandle::AI(v) => v.as_ref(app).should_render(app),
|
||||
SettingsPageViewHandle::MCPServers(v) => v.as_ref(app).should_render(app),
|
||||
@@ -2518,11 +2517,11 @@ impl TypedActionView for SettingsView {
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsAction::WarpifyPageToggle(warpify_action) => {
|
||||
if let Some(warpify_page) = self.settings_page(SettingsSection::Warpify) {
|
||||
if let SettingsPageViewHandle::Warpify(view) = &warpify_page.view_handle {
|
||||
SettingsAction::WormholePageToggle(wormhole_action) => {
|
||||
if let Some(wormhole_page) = self.settings_page(SettingsSection::Wormhole) {
|
||||
if let SettingsPageViewHandle::Wormhole(view) = &wormhole_page.view_handle {
|
||||
view.update(ctx, |view, ctx| {
|
||||
view.handle_action(warpify_action, ctx);
|
||||
view.handle_action(wormhole_action, ctx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ fn top_level_sections_map_to_themselves() {
|
||||
SettingsSection::Scripting,
|
||||
SettingsSection::Teams,
|
||||
SettingsSection::WarpDrive,
|
||||
SettingsSection::Warpify,
|
||||
SettingsSection::Wormhole,
|
||||
] {
|
||||
assert!(!section.is_subpage(), "{section:?} should be top-level");
|
||||
assert_eq!(section.parent_page_section(), section);
|
||||
@@ -101,7 +101,7 @@ fn current_settings_display_names_round_trip() {
|
||||
for (section, display_name) in [
|
||||
(SettingsSection::Scripting, "Galaxy Control"),
|
||||
(SettingsSection::WarpDrive, "Galaxy Drive"),
|
||||
(SettingsSection::Warpify, "Wormhole"),
|
||||
(SettingsSection::Wormhole, "Wormhole"),
|
||||
(SettingsSection::WarpAgent, "Galaxy Agent"),
|
||||
(SettingsSection::AgentProfiles, "Profiles"),
|
||||
(SettingsSection::AgentMCPServers, "MCP servers"),
|
||||
@@ -135,7 +135,7 @@ fn legacy_settings_names_remain_parseable() {
|
||||
for (name, expected) in [
|
||||
("Scripting", SettingsSection::Scripting),
|
||||
("WarpDrive", SettingsSection::WarpDrive),
|
||||
("Warpify", SettingsSection::Warpify),
|
||||
("Wormhole", SettingsSection::Wormhole),
|
||||
("Oz", SettingsSection::WarpAgent),
|
||||
("Warp Agent", SettingsSection::WarpAgent),
|
||||
("AgentProfiles", SettingsSection::AgentProfiles),
|
||||
@@ -191,7 +191,7 @@ fn realistic_nav_items() -> Vec<SettingsNavItem> {
|
||||
SettingsNavItem::Page(SettingsSection::Appearance),
|
||||
SettingsNavItem::Page(SettingsSection::Features),
|
||||
SettingsNavItem::Page(SettingsSection::Keybindings),
|
||||
SettingsNavItem::Page(SettingsSection::Warpify),
|
||||
SettingsNavItem::Page(SettingsSection::Wormhole),
|
||||
SettingsNavItem::Page(SettingsSection::WarpDrive),
|
||||
SettingsNavItem::Page(SettingsSection::Privacy),
|
||||
SettingsNavItem::Page(SettingsSection::About),
|
||||
|
||||
@@ -37,7 +37,7 @@ use super::privacy_page::PrivacyPageView;
|
||||
use super::scripting_page::ScriptingSettingsPageView;
|
||||
use super::teams_page::TeamsPageView;
|
||||
use super::warp_drive_page::WarpDriveSettingsPageView;
|
||||
use super::warpify_page::WarpifyPageView;
|
||||
use super::wormhole_page::WormholePageView;
|
||||
use super::SettingsSection;
|
||||
use crate::appearance::Appearance;
|
||||
use crate::settings::CloudPreferencesSettings;
|
||||
@@ -100,7 +100,7 @@ pub enum SettingsPageViewHandle {
|
||||
About(ViewHandle<AboutPageView>),
|
||||
Code(ViewHandle<CodeSettingsPageView>),
|
||||
Privacy(ViewHandle<PrivacyPageView>),
|
||||
Warpify(ViewHandle<WarpifyPageView>),
|
||||
Wormhole(ViewHandle<WormholePageView>),
|
||||
Scripting(ViewHandle<ScriptingSettingsPageView>),
|
||||
AI(ViewHandle<AISettingsPageView>),
|
||||
MCPServers(ViewHandle<MCPServersSettingsPageView>),
|
||||
@@ -119,7 +119,7 @@ impl SettingsPageViewHandle {
|
||||
About(view_handle) => ChildView::new(view_handle).finish(),
|
||||
Code(view_handle) => ChildView::new(view_handle).finish(),
|
||||
Privacy(view_handle) => ChildView::new(view_handle).finish(),
|
||||
Warpify(view_handle) => ChildView::new(view_handle).finish(),
|
||||
Wormhole(view_handle) => ChildView::new(view_handle).finish(),
|
||||
Scripting(view_handle) => ChildView::new(view_handle).finish(),
|
||||
AI(view_handle) => ChildView::new(view_handle).finish(),
|
||||
MCPServers(view_handle) => ChildView::new(view_handle).finish(),
|
||||
|
||||
@@ -3,9 +3,7 @@ use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxyui::elements::{
|
||||
Container, Flex, FormattedTextElement, HighlightedHyperlink, MouseStateHandle, ParentElement,
|
||||
};
|
||||
use galaxyui::elements::{Container, Flex, MouseStateHandle, ParentElement, Text};
|
||||
use galaxyui::keymap::ContextPredicate;
|
||||
use galaxyui::presenter::ChildView;
|
||||
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
||||
@@ -14,7 +12,6 @@ use galaxyui::{
|
||||
Action, AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView, View,
|
||||
ViewContext, ViewHandle,
|
||||
};
|
||||
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
|
||||
use regex::Regex;
|
||||
use settings::{Setting, ToggleableSetting};
|
||||
use strum::IntoEnumIterator;
|
||||
@@ -29,9 +26,9 @@ use super::{flags, SettingsAction, SettingsSection, ToggleSettingActionPair};
|
||||
use crate::appearance::Appearance;
|
||||
use crate::server::telemetry::TelemetryEvent;
|
||||
use crate::settings::{ReuseExistingSshControlMaster, SshSettings};
|
||||
use crate::terminal::warpify::settings::{
|
||||
EnableSshWarpification, SshExtensionInstallMode, SshExtensionInstallModeSetting,
|
||||
WarpifySettings, WarpifySettingsChangedEvent,
|
||||
use crate::terminal::wormhole::settings::{
|
||||
EnableSshWormholing, SshExtensionInstallMode, SshExtensionInstallModeSetting, WormholeSettings,
|
||||
WormholeSettingsChangedEvent,
|
||||
};
|
||||
use crate::ui_components::blended_colors;
|
||||
use crate::view_components::dropdown::{Dropdown, DropdownItem};
|
||||
@@ -43,19 +40,19 @@ pub fn init_actions_from_parent_view<T: Action + Clone>(
|
||||
context: &ContextPredicate,
|
||||
builder: fn(SettingsAction) -> T,
|
||||
) {
|
||||
// Add all of the toggle settings from the Warpify Page that you want to show up on the Command Palette here.
|
||||
// Add all of the toggle settings from the Wormhole Page that you want to show up on the Command Palette here.
|
||||
let mut toggle_binding_pairs = vec![];
|
||||
if WarpifySettings::as_ref(app)
|
||||
.enable_ssh_warpification
|
||||
if WormholeSettings::as_ref(app)
|
||||
.enable_ssh_wormholing
|
||||
.is_supported_on_current_platform()
|
||||
{
|
||||
toggle_binding_pairs.push(ToggleSettingActionPair::new(
|
||||
"SSH Warpification",
|
||||
builder(SettingsAction::WarpifyPageToggle(
|
||||
WarpifyPageAction::ToggleSshWarpification,
|
||||
"SSH Wormholing",
|
||||
builder(SettingsAction::WormholePageToggle(
|
||||
WormholePageAction::ToggleSshWormholing,
|
||||
)),
|
||||
context,
|
||||
flags::SSH_WARPIFICATION_CONTEXT_FLAG,
|
||||
flags::SSH_WORMHOLING_CONTEXT_FLAG,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -68,17 +65,17 @@ const ITEM_VERTICAL_SPACING: f32 = 24.;
|
||||
const BUILT_IN_TEXT_INPUT_MARGIN: f32 = 10.;
|
||||
const SPACE_AFTER_TEXT_INPUT: f32 = ITEM_VERTICAL_SPACING - BUILT_IN_TEXT_INPUT_MARGIN;
|
||||
|
||||
const SSH_REUSE_CONTROL_MASTER_DESCRIPTION: &str = "Attach to a live SSH ControlMaster you already have configured for the destination host instead of creating a Warp-owned one. Takes effect in new tabs.";
|
||||
const SSH_REUSE_CONTROL_MASTER_DESCRIPTION: &str = "Attach to a live SSH ControlMaster you already have configured for the destination host instead of creating a Galaxy-owned one. Takes effect in new tabs.";
|
||||
|
||||
const SSH_EXTENSION_INSTALL_MODE_DESCRIPTION: &str =
|
||||
"Controls the installation behavior for Galaxy's SSH extension when a remote host doesn't have it installed.";
|
||||
"Controls how Galaxy installs the Wormhole helper when a remote host doesn't have it.";
|
||||
|
||||
/// This page lets users configure when they get asked to warpify a session. Some shell commands
|
||||
/// This page lets users configure when they get asked to wormhole a session. Some shell commands
|
||||
/// are recognized by default. Users can add new shell commands, or prevent the default ones from
|
||||
/// asking. Users can also enable the SSH wrapper, and add hosts to a denylist.
|
||||
/// This page is essentially the View for the SubshellSettings model, as well as the SshSettings
|
||||
/// related to warpification.
|
||||
pub struct WarpifyPageView {
|
||||
/// related to wormholing.
|
||||
pub struct WormholePageView {
|
||||
page: PageType<Self>,
|
||||
/// This needs to mirror the length of SubshellSettings::added_remove_button_states.
|
||||
remove_added_command_button_states: Vec<MouseStateHandle>,
|
||||
@@ -87,19 +84,19 @@ pub struct WarpifyPageView {
|
||||
remove_denylisted_command_button_states: Vec<MouseStateHandle>,
|
||||
add_denylisted_commands_editor: ViewHandle<SubmittableTextInput>,
|
||||
|
||||
ssh_extension_install_mode_dropdown: ViewHandle<Dropdown<WarpifyPageAction>>,
|
||||
ssh_extension_install_mode_dropdown: ViewHandle<Dropdown<WormholePageAction>>,
|
||||
}
|
||||
|
||||
impl WarpifyPageView {
|
||||
impl WormholePageView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let warpify_settings_handle = WarpifySettings::handle(ctx);
|
||||
let wormhole_settings_handle = WormholeSettings::handle(ctx);
|
||||
|
||||
ctx.observe(&warpify_settings_handle, Self::update_button_states);
|
||||
ctx.subscribe_to_model(&warpify_settings_handle, move |me, model, event, ctx| {
|
||||
ctx.observe(&wormhole_settings_handle, Self::update_button_states);
|
||||
ctx.subscribe_to_model(&wormhole_settings_handle, move |me, model, event, ctx| {
|
||||
me.update_button_states(model, ctx);
|
||||
if matches!(
|
||||
event,
|
||||
WarpifySettingsChangedEvent::SshExtensionInstallModeSetting { .. }
|
||||
WormholeSettingsChangedEvent::SshExtensionInstallModeSetting { .. }
|
||||
) {
|
||||
me.update_dropdown(ctx);
|
||||
}
|
||||
@@ -143,20 +140,20 @@ impl WarpifyPageView {
|
||||
ssh_extension_install_mode_dropdown,
|
||||
};
|
||||
|
||||
instance.update_button_states(warpify_settings_handle, ctx);
|
||||
instance.update_button_states(wormhole_settings_handle, ctx);
|
||||
instance
|
||||
}
|
||||
|
||||
fn build_page(ctx: &mut ViewContext<Self>) -> PageType<Self> {
|
||||
let mut categories = vec![
|
||||
Category::new("", vec![Box::new(TitleWidget::default())]),
|
||||
Category::new("", vec![Box::new(TitleWidget)]),
|
||||
Category::new("Subshells", vec![Box::new(SubshellsWidget::default())])
|
||||
.with_subtitle("Subshells supported: bash, zsh, and fish."),
|
||||
];
|
||||
|
||||
let warpify_settings = WarpifySettings::as_ref(ctx);
|
||||
if warpify_settings
|
||||
.enable_ssh_warpification
|
||||
let wormhole_settings = WormholeSettings::as_ref(ctx);
|
||||
if wormhole_settings
|
||||
.enable_ssh_wormholing
|
||||
.is_supported_on_current_platform()
|
||||
{
|
||||
categories.push(
|
||||
@@ -171,16 +168,16 @@ impl WarpifyPageView {
|
||||
/// its delete button in the View.
|
||||
fn update_button_states(
|
||||
&mut self,
|
||||
warpify_settings_handle: ModelHandle<WarpifySettings>,
|
||||
wormhole_settings_handle: ModelHandle<WormholeSettings>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let warpify_settings = warpify_settings_handle.as_ref(ctx);
|
||||
self.remove_denylisted_command_button_states = warpify_settings
|
||||
let wormhole_settings = wormhole_settings_handle.as_ref(ctx);
|
||||
self.remove_denylisted_command_button_states = wormhole_settings
|
||||
.subshell_command_denylist
|
||||
.iter()
|
||||
.map(|_| Default::default())
|
||||
.collect();
|
||||
self.remove_added_command_button_states = warpify_settings
|
||||
self.remove_added_command_button_states = wormhole_settings
|
||||
.added_subshell_commands
|
||||
.iter()
|
||||
.map(|_| Default::default())
|
||||
@@ -189,16 +186,16 @@ impl WarpifyPageView {
|
||||
}
|
||||
|
||||
/// Syncs the install-mode dropdown selection with the current
|
||||
/// `WarpifySettings::ssh_extension_install_mode` value (e.g. after it
|
||||
/// `WormholeSettings::ssh_extension_install_mode` value (e.g. after it
|
||||
/// was changed from the SSH remote server choice view).
|
||||
fn update_dropdown(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let current_mode = *WarpifySettings::as_ref(ctx)
|
||||
let current_mode = *WormholeSettings::as_ref(ctx)
|
||||
.ssh_extension_install_mode
|
||||
.value();
|
||||
self.ssh_extension_install_mode_dropdown
|
||||
.update(ctx, |dropdown, ctx| {
|
||||
dropdown.set_selected_by_action(
|
||||
WarpifyPageAction::SetSshExtensionInstallMode(current_mode),
|
||||
WormholePageAction::SetSshExtensionInstallMode(current_mode),
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
@@ -212,8 +209,8 @@ impl WarpifyPageView {
|
||||
) {
|
||||
match event {
|
||||
SubmittableTextInputEvent::Submit(new_command) => {
|
||||
WarpifySettings::handle(ctx).update(ctx, |warpify_settings, ctx| {
|
||||
warpify_settings.add_subshell_command(new_command, ctx);
|
||||
WormholeSettings::handle(ctx).update(ctx, |wormhole_settings, ctx| {
|
||||
wormhole_settings.add_subshell_command(new_command, ctx);
|
||||
});
|
||||
|
||||
send_telemetry_from_ctx!(TelemetryEvent::AddAddedSubshellCommand, ctx);
|
||||
@@ -230,8 +227,8 @@ impl WarpifyPageView {
|
||||
) {
|
||||
match event {
|
||||
SubmittableTextInputEvent::Submit(new_command) => {
|
||||
WarpifySettings::handle(ctx).update(ctx, |warpify_settings, ctx| {
|
||||
warpify_settings.denylist_subshell_command(new_command, ctx);
|
||||
WormholeSettings::handle(ctx).update(ctx, |wormhole_settings, ctx| {
|
||||
wormhole_settings.denylist_subshell_command(new_command, ctx);
|
||||
});
|
||||
|
||||
send_telemetry_from_ctx!(TelemetryEvent::AddDenylistedSubshellCommand, ctx);
|
||||
@@ -242,20 +239,20 @@ impl WarpifyPageView {
|
||||
|
||||
fn remove_denylisted_command(&self, index: usize, ctx: &mut ViewContext<Self>) {
|
||||
send_telemetry_from_ctx!(TelemetryEvent::RemoveDenylistedSubshellCommand, ctx);
|
||||
WarpifySettings::handle(ctx).update(ctx, |warpify, ctx| {
|
||||
warpify.remove_denylisted_subshell_command(index, ctx)
|
||||
WormholeSettings::handle(ctx).update(ctx, |wormhole, ctx| {
|
||||
wormhole.remove_denylisted_subshell_command(index, ctx)
|
||||
});
|
||||
}
|
||||
|
||||
fn remove_added_command(&self, index: usize, ctx: &mut ViewContext<Self>) {
|
||||
send_telemetry_from_ctx!(TelemetryEvent::RemoveAddedSubshellCommand, ctx);
|
||||
WarpifySettings::handle(ctx).update(ctx, |warpify, ctx| {
|
||||
warpify.remove_added_subshell_command(index, ctx)
|
||||
WormholeSettings::handle(ctx).update(ctx, |wormhole, ctx| {
|
||||
wormhole.remove_added_subshell_command(index, ctx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for WarpifyPageView {
|
||||
impl Entity for WormholePageView {
|
||||
type Event = SettingsPageEvent;
|
||||
}
|
||||
|
||||
@@ -272,25 +269,23 @@ fn build_sub_sub_title(title: &str, appearance: &Appearance) -> Container {
|
||||
|
||||
const SSH_EXTENSION_DROPDOWN_WIDTH: f32 = 250.;
|
||||
|
||||
impl WarpifyPageView {
|
||||
impl WormholePageView {
|
||||
fn create_ssh_extension_install_mode_dropdown(
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> ViewHandle<Dropdown<WarpifyPageAction>> {
|
||||
let items: Vec<DropdownItem<WarpifyPageAction>> = SshExtensionInstallMode::iter()
|
||||
) -> ViewHandle<Dropdown<WormholePageAction>> {
|
||||
let items: Vec<DropdownItem<WormholePageAction>> = SshExtensionInstallMode::iter()
|
||||
.map(|mode| {
|
||||
DropdownItem::new(
|
||||
mode.display_name(),
|
||||
WarpifyPageAction::SetSshExtensionInstallMode(mode),
|
||||
WormholePageAction::SetSshExtensionInstallMode(mode),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let current_mode = *WarpifySettings::as_ref(ctx)
|
||||
let current_mode = *WormholeSettings::as_ref(ctx)
|
||||
.ssh_extension_install_mode
|
||||
.value();
|
||||
let enable_ssh_warpification = *WarpifySettings::as_ref(ctx)
|
||||
.enable_ssh_warpification
|
||||
.value();
|
||||
let enable_ssh_wormholing = *WormholeSettings::as_ref(ctx).enable_ssh_wormholing.value();
|
||||
|
||||
ctx.add_typed_action_view(move |ctx| {
|
||||
let mut dropdown = Dropdown::new(ctx);
|
||||
@@ -298,10 +293,10 @@ impl WarpifyPageView {
|
||||
dropdown.set_menu_width(SSH_EXTENSION_DROPDOWN_WIDTH, ctx);
|
||||
dropdown.add_items(items, ctx);
|
||||
dropdown.set_selected_by_action(
|
||||
WarpifyPageAction::SetSshExtensionInstallMode(current_mode),
|
||||
WormholePageAction::SetSshExtensionInstallMode(current_mode),
|
||||
ctx,
|
||||
);
|
||||
if !enable_ssh_warpification {
|
||||
if !enable_ssh_wormholing {
|
||||
dropdown.set_disabled(ctx);
|
||||
}
|
||||
dropdown
|
||||
@@ -352,9 +347,9 @@ impl WarpifyPageView {
|
||||
}
|
||||
}
|
||||
|
||||
impl View for WarpifyPageView {
|
||||
impl View for WormholePageView {
|
||||
fn ui_name() -> &'static str {
|
||||
"WarpifyPageView"
|
||||
"WormholePageView"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
@@ -363,41 +358,38 @@ impl View for WarpifyPageView {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum WarpifyPageAction {
|
||||
pub enum WormholePageAction {
|
||||
RemoveAddedCommand(usize),
|
||||
RemoveDenylistedCommand(usize),
|
||||
ToggleSshWarpification,
|
||||
ToggleSshWormholing,
|
||||
/// Toggles whether the legacy SSH wrapper attaches to an existing
|
||||
/// ControlMaster for the destination host instead of creating its own.
|
||||
ToggleReuseSshControlMaster,
|
||||
/// Set the SSH extension installation mode (always ask / always install / always skip).
|
||||
SetSshExtensionInstallMode(SshExtensionInstallMode),
|
||||
OpenUrl(String),
|
||||
}
|
||||
|
||||
impl TypedActionView for WarpifyPageView {
|
||||
type Action = WarpifyPageAction;
|
||||
impl TypedActionView for WormholePageView {
|
||||
type Action = WormholePageAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
use WarpifyPageAction::*;
|
||||
use WormholePageAction::*;
|
||||
match action {
|
||||
RemoveDenylistedCommand(index) => self.remove_denylisted_command(*index, ctx),
|
||||
RemoveAddedCommand(index) => self.remove_added_command(*index, ctx),
|
||||
ToggleSshWarpification => {
|
||||
WarpifySettings::handle(ctx).update(ctx, |ssh_settings, ctx| {
|
||||
ToggleSshWormholing => {
|
||||
WormholeSettings::handle(ctx).update(ctx, |ssh_settings, ctx| {
|
||||
report_if_error!(ssh_settings
|
||||
.enable_ssh_warpification
|
||||
.enable_ssh_wormholing
|
||||
.toggle_and_save_value(ctx));
|
||||
send_telemetry_from_ctx!(
|
||||
TelemetryEvent::ToggleSshWarpification {
|
||||
enabled: *ssh_settings.enable_ssh_warpification.value(),
|
||||
TelemetryEvent::ToggleSshWormholing {
|
||||
enabled: *ssh_settings.enable_ssh_wormholing.value(),
|
||||
},
|
||||
ctx
|
||||
);
|
||||
});
|
||||
let enabled = *WarpifySettings::as_ref(ctx)
|
||||
.enable_ssh_warpification
|
||||
.value();
|
||||
let enabled = *WormholeSettings::as_ref(ctx).enable_ssh_wormholing.value();
|
||||
self.ssh_extension_install_mode_dropdown
|
||||
.update(ctx, |dropdown, ctx| {
|
||||
if enabled {
|
||||
@@ -425,8 +417,8 @@ impl TypedActionView for WarpifyPageView {
|
||||
});
|
||||
}
|
||||
SetSshExtensionInstallMode(mode) => {
|
||||
WarpifySettings::handle(ctx).update(ctx, |warpify_settings, ctx| {
|
||||
report_if_error!(warpify_settings
|
||||
WormholeSettings::handle(ctx).update(ctx, |wormhole_settings, ctx| {
|
||||
report_if_error!(wormhole_settings
|
||||
.ssh_extension_install_mode
|
||||
.set_value(*mode, ctx));
|
||||
send_telemetry_from_ctx!(
|
||||
@@ -437,16 +429,13 @@ impl TypedActionView for WarpifyPageView {
|
||||
);
|
||||
});
|
||||
}
|
||||
OpenUrl(url) => {
|
||||
ctx.open_url(url.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingsPageMeta for WarpifyPageView {
|
||||
impl SettingsPageMeta for WormholePageView {
|
||||
fn section() -> SettingsSection {
|
||||
SettingsSection::Warpify
|
||||
SettingsSection::Wormhole
|
||||
}
|
||||
|
||||
fn should_render(&self, _ctx: &AppContext) -> bool {
|
||||
@@ -466,53 +455,39 @@ impl SettingsPageMeta for WarpifyPageView {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ViewHandle<WarpifyPageView>> for SettingsPageViewHandle {
|
||||
fn from(view_handle: ViewHandle<WarpifyPageView>) -> Self {
|
||||
SettingsPageViewHandle::Warpify(view_handle)
|
||||
impl From<ViewHandle<WormholePageView>> for SettingsPageViewHandle {
|
||||
fn from(view_handle: ViewHandle<WormholePageView>) -> Self {
|
||||
SettingsPageViewHandle::Wormhole(view_handle)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TitleWidget {
|
||||
learn_more_highlight_index: HighlightedHyperlink,
|
||||
}
|
||||
struct TitleWidget;
|
||||
|
||||
impl TitleWidget {
|
||||
fn render_top_of_page(&self, appearance: &Appearance, _app: &AppContext) -> Box<dyn Element> {
|
||||
let warpify_description = vec![
|
||||
FormattedTextFragment::plain_text(
|
||||
"Configure whether Galaxy attempts to \u{201c}Wormhole\u{201d} (add support for blocks, \
|
||||
input modes, etc) certain shells. ",
|
||||
),
|
||||
FormattedTextFragment::hyperlink(
|
||||
"Learn more",
|
||||
"https://docs.warp.dev/terminal/warpify/subshells",
|
||||
),
|
||||
];
|
||||
|
||||
let warpify_description = FormattedTextElement::new(
|
||||
FormattedText::new([FormattedTextLine::Line(warpify_description)]),
|
||||
let wormhole_description = Text::new(
|
||||
"Configure whether Galaxy attempts to \u{201c}Wormhole\u{201d} supported shells, adding blocks, full text editing, completions, and other Galaxy features."
|
||||
.to_string(),
|
||||
appearance.ui_font_family(),
|
||||
CONTENT_FONT_SIZE,
|
||||
appearance.ui_font_family(),
|
||||
appearance.ui_font_family(),
|
||||
blended_colors::text_sub(appearance.theme(), appearance.theme().surface_1()),
|
||||
self.learn_more_highlight_index.clone(),
|
||||
)
|
||||
.with_hyperlink_font_color(appearance.theme().accent().into_solid())
|
||||
.register_default_click_handlers(|url, _, ctx| {
|
||||
ctx.open_url(&url.url);
|
||||
})
|
||||
.soft_wrap(true)
|
||||
.with_color(blended_colors::text_sub(
|
||||
appearance.theme(),
|
||||
appearance.theme().surface_1(),
|
||||
))
|
||||
.finish();
|
||||
|
||||
Flex::column()
|
||||
.with_child(render_page_title("Wormhole", HEADER_FONT_SIZE, appearance))
|
||||
.with_child(warpify_description)
|
||||
.with_child(wormhole_description)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingsWidget for TitleWidget {
|
||||
type View = WarpifyPageView;
|
||||
type View = WormholePageView;
|
||||
|
||||
fn search_terms(&self) -> &str {
|
||||
"ssh subshell galaxify session"
|
||||
@@ -536,20 +511,20 @@ struct SubshellsWidget {}
|
||||
impl SubshellsWidget {
|
||||
fn render_subshells_section(
|
||||
&self,
|
||||
view: &WarpifyPageView,
|
||||
view: &WormholePageView,
|
||||
appearance: &Appearance,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let mut column = Flex::column();
|
||||
|
||||
let warpify_settings = WarpifySettings::as_ref(app);
|
||||
let wormhole_settings = WormholeSettings::as_ref(app);
|
||||
|
||||
column.add_child(
|
||||
view.build_input_list(
|
||||
"Added commands",
|
||||
&warpify_settings.added_subshell_commands,
|
||||
&wormhole_settings.added_subshell_commands,
|
||||
&view.remove_added_command_button_states,
|
||||
WarpifyPageAction::RemoveAddedCommand,
|
||||
WormholePageAction::RemoveAddedCommand,
|
||||
&view.add_added_commands_editor,
|
||||
appearance,
|
||||
)
|
||||
@@ -559,9 +534,9 @@ impl SubshellsWidget {
|
||||
column.add_child(
|
||||
view.build_input_list(
|
||||
"Denylisted commands",
|
||||
&warpify_settings.subshell_command_denylist,
|
||||
&wormhole_settings.subshell_command_denylist,
|
||||
&view.remove_denylisted_command_button_states,
|
||||
WarpifyPageAction::RemoveDenylistedCommand,
|
||||
WormholePageAction::RemoveDenylistedCommand,
|
||||
&view.add_denylisted_commands_editor,
|
||||
appearance,
|
||||
)
|
||||
@@ -574,7 +549,7 @@ impl SubshellsWidget {
|
||||
}
|
||||
|
||||
impl SettingsWidget for SubshellsWidget {
|
||||
type View = WarpifyPageView;
|
||||
type View = WormholePageView;
|
||||
|
||||
fn search_terms(&self) -> &str {
|
||||
"galaxify subshell"
|
||||
@@ -594,13 +569,13 @@ impl SettingsWidget for SubshellsWidget {
|
||||
|
||||
#[derive(Default)]
|
||||
struct SSHWidget {
|
||||
enable_ssh_warpification_switch_state: SwitchStateHandle,
|
||||
enable_ssh_wormholing_switch_state: SwitchStateHandle,
|
||||
reuse_control_master_switch_state: SwitchStateHandle,
|
||||
local_only_icon_tooltip_states: RefCell<HashMap<String, MouseStateHandle>>,
|
||||
}
|
||||
|
||||
impl SettingsWidget for SSHWidget {
|
||||
type View = WarpifyPageView;
|
||||
type View = WormholePageView;
|
||||
|
||||
fn search_terms(&self) -> &str {
|
||||
"galaxify ssh"
|
||||
@@ -618,31 +593,29 @@ impl SettingsWidget for SSHWidget {
|
||||
.theme()
|
||||
.sub_text_color(appearance.theme().surface_2());
|
||||
|
||||
let enable_ssh_warpification = *WarpifySettings::as_ref(app)
|
||||
.enable_ssh_warpification
|
||||
.value();
|
||||
let enable_ssh_wormholing = *WormholeSettings::as_ref(app).enable_ssh_wormholing.value();
|
||||
|
||||
add_setting(
|
||||
&mut column,
|
||||
&WarpifySettings::as_ref(app).enable_ssh_warpification,
|
||||
&WormholeSettings::as_ref(app).enable_ssh_wormholing,
|
||||
move || {
|
||||
render_body_item::<WarpifyPageAction>(
|
||||
render_body_item::<WormholePageAction>(
|
||||
"Wormhole SSH Sessions".into(),
|
||||
None,
|
||||
LocalOnlyIconState::for_setting(
|
||||
EnableSshWarpification::storage_key(),
|
||||
EnableSshWarpification::sync_to_cloud(),
|
||||
EnableSshWormholing::storage_key(),
|
||||
EnableSshWormholing::sync_to_cloud(),
|
||||
&mut self.local_only_icon_tooltip_states.borrow_mut(),
|
||||
app,
|
||||
),
|
||||
ToggleState::Enabled,
|
||||
appearance,
|
||||
ui_builder
|
||||
.switch(self.enable_ssh_warpification_switch_state.clone())
|
||||
.check(enable_ssh_warpification)
|
||||
.switch(self.enable_ssh_wormholing_switch_state.clone())
|
||||
.check(enable_ssh_wormholing)
|
||||
.build()
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(WarpifyPageAction::ToggleSshWarpification);
|
||||
ctx.dispatch_typed_action(WormholePageAction::ToggleSshWormholing);
|
||||
})
|
||||
.finish(),
|
||||
None,
|
||||
@@ -651,18 +624,18 @@ impl SettingsWidget for SSHWidget {
|
||||
);
|
||||
|
||||
if FeatureFlag::SshRemoteServer.is_enabled() {
|
||||
let label_color_override = if !enable_ssh_warpification {
|
||||
let label_color_override = if !enable_ssh_wormholing {
|
||||
Some(appearance.theme().disabled_ui_text_color())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
add_setting(
|
||||
&mut column,
|
||||
&WarpifySettings::as_ref(app).ssh_extension_install_mode,
|
||||
&WormholeSettings::as_ref(app).ssh_extension_install_mode,
|
||||
move || {
|
||||
Container::new(render_dropdown_item(
|
||||
appearance,
|
||||
"Install SSH extension",
|
||||
"Install Wormhole helper",
|
||||
Some(SSH_EXTENSION_INSTALL_MODE_DESCRIPTION),
|
||||
None,
|
||||
LocalOnlyIconState::for_setting(
|
||||
@@ -688,7 +661,7 @@ impl SettingsWidget for SSHWidget {
|
||||
&SshSettings::as_ref(app).reuse_existing_control_master,
|
||||
move || {
|
||||
let mut column = Flex::column();
|
||||
column.add_child(render_body_item::<WarpifyPageAction>(
|
||||
column.add_child(render_body_item::<WormholePageAction>(
|
||||
"Reuse existing SSH ControlMaster".into(),
|
||||
None,
|
||||
LocalOnlyIconState::for_setting(
|
||||
@@ -697,19 +670,19 @@ impl SettingsWidget for SSHWidget {
|
||||
&mut self.local_only_icon_tooltip_states.borrow_mut(),
|
||||
app,
|
||||
),
|
||||
enable_ssh_warpification.into(),
|
||||
enable_ssh_wormholing.into(),
|
||||
appearance,
|
||||
ui_builder
|
||||
.switch(self.reuse_control_master_switch_state.clone())
|
||||
.check(reuse_existing_control_master)
|
||||
.with_disabled(!enable_ssh_warpification)
|
||||
.with_disabled(!enable_ssh_wormholing)
|
||||
.build()
|
||||
.on_click(move |ctx, _, _| {
|
||||
if !enable_ssh_warpification {
|
||||
if !enable_ssh_wormholing {
|
||||
return;
|
||||
}
|
||||
ctx.dispatch_typed_action(
|
||||
WarpifyPageAction::ToggleReuseSshControlMaster,
|
||||
WormholePageAction::ToggleReuseSshControlMaster,
|
||||
);
|
||||
})
|
||||
.finish(),
|
||||
@@ -58,7 +58,7 @@ use super::view::{
|
||||
BlocklistAIRenderContext, InlineBannerId, RichContentMetadata, SeparatorId,
|
||||
SharedSessionBanners, TerminalEditor, TerminalViewRenderContext, BLOCK_BANNER_HEIGHT,
|
||||
};
|
||||
use super::warpify::render::{draw_flag_pole, render_subshell_flag};
|
||||
use super::wormhole::render::{draw_flag_pole, render_subshell_flag};
|
||||
use super::{heights_approx_eq, TerminalModel, HEIGHT_FUDGE_FACTOR_LINES};
|
||||
use crate::ai::blocklist::agent_view::{agent_view_bg_fill, AgentViewState};
|
||||
use crate::ai::blocklist::{ai_brand_color, ATTACH_AS_AGENT_MODE_CONTEXT_TEXT};
|
||||
@@ -86,7 +86,7 @@ use crate::terminal::model::selection::{SelectAction, SelectionPoint};
|
||||
use crate::terminal::model::terminal_model::BlockIndex;
|
||||
use crate::terminal::safe_mode_settings::get_secret_obfuscation_mode;
|
||||
use crate::terminal::view::TerminalAction;
|
||||
use crate::terminal::warpify::SubshellSource;
|
||||
use crate::terminal::wormhole::SubshellSource;
|
||||
use crate::terminal::{grid_renderer, SizeInfo};
|
||||
use crate::themes::theme::{Fill, WarpTheme};
|
||||
use crate::ui_components::{self, icons as UIIcon};
|
||||
|
||||
@@ -10,7 +10,7 @@ use rand::Rng;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use super::{
|
||||
model::session::{BootstrapSessionType, SessionInfo},
|
||||
warpify::settings::{PIPENV_SUBSHELL_COMMAND_REGEX, POETRY_SUBSHELL_COMMAND_REGEX},
|
||||
wormhole::settings::{PIPENV_SUBSHELL_COMMAND_REGEX, POETRY_SUBSHELL_COMMAND_REGEX},
|
||||
};
|
||||
use crate::env_vars::{EnvVar, EnvVarExt};
|
||||
use crate::terminal::session_settings::SessionSettings;
|
||||
@@ -99,7 +99,7 @@ pub fn should_use_rc_file_bootstrap_method(
|
||||
&& shell_type == ShellType::Zsh)
|
||||
|| is_msys2
|
||||
}
|
||||
BootstrapSessionType::WarpifiedRemote => false,
|
||||
BootstrapSessionType::WormholedRemote => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ pub struct CLIAgentEvent {
|
||||
const VERSIONED_PARSERS: &[EventParser] = &[v1::parse];
|
||||
|
||||
/// The current CLI agent protocol version this build of Warp supports.
|
||||
/// Exported as the `WARP_CLI_AGENT_PROTOCOL_VERSION` env var on the PTY
|
||||
/// Exported as the `GALAXY_CLI_AGENT_PROTOCOL_VERSION` env var on the PTY
|
||||
/// so plugins can negotiate a compatible payload format.
|
||||
#[cfg_attr(not(feature = "local_tty"), allow(dead_code))]
|
||||
pub const fn current_protocol_version() -> u32 {
|
||||
|
||||
@@ -130,7 +130,7 @@ pub struct CLIAgentSession {
|
||||
/// `None` if the plugin predates version reporting or Codex is using OSC9 fallback.
|
||||
pub plugin_version: Option<String>,
|
||||
/// `None` when the session is local.
|
||||
/// `Some("user@hostname")` when running over SSH (warpified or legacy).
|
||||
/// `Some("user@hostname")` when running over SSH (wormholed or legacy).
|
||||
/// Used as a key for per-host plugin install failure tracking.
|
||||
pub remote_host: Option<String>,
|
||||
/// Draft text saved from the rich input composer when it was closed.
|
||||
|
||||
@@ -167,9 +167,9 @@ pub enum TerminalMode {
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SshLoginStatus {
|
||||
/// We have some evidence login is complete but should check again.
|
||||
RecheckBeforeWarpifying,
|
||||
RecheckBeforeWormholing,
|
||||
/// We have high confidence login is complete.
|
||||
ReadyToWarpify,
|
||||
ReadyToWormhole,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -247,7 +247,7 @@ pub enum BlockType {
|
||||
/// This is a block containing background process output.
|
||||
Background(Arc<SerializedBlock>),
|
||||
|
||||
/// This is a block containing static/hardcoded content (e.g. the subshell Warpification
|
||||
/// This is a block containing static/hardcoded content (e.g. the subshell Wormholing
|
||||
/// welcome block).
|
||||
Static,
|
||||
}
|
||||
|
||||
@@ -505,7 +505,7 @@ fn test_multiple_machines() {
|
||||
SessionInfo::new_for_test()
|
||||
.with_id(0)
|
||||
.with_shell_type(ShellType::Zsh)
|
||||
.with_session_type(BootstrapSessionType::WarpifiedRemote)
|
||||
.with_session_type(BootstrapSessionType::WormholedRemote)
|
||||
.with_hostname("prod".to_string())
|
||||
.with_user("user".to_string())
|
||||
.with_ssh_socket_path(PathBuf::from("~/.ssh/12345"))
|
||||
@@ -517,7 +517,7 @@ fn test_multiple_machines() {
|
||||
SessionInfo::new_for_test()
|
||||
.with_id(1)
|
||||
.with_shell_type(ShellType::Zsh)
|
||||
.with_session_type(BootstrapSessionType::WarpifiedRemote)
|
||||
.with_session_type(BootstrapSessionType::WormholedRemote)
|
||||
.with_hostname("dev".to_string())
|
||||
.with_user("user2".to_string())
|
||||
.with_ssh_socket_path(PathBuf::from("~/.ssh/12345"))
|
||||
|
||||
@@ -141,7 +141,7 @@ use super::view::queued_prompts_panel::{QueuedPromptsPanelEvent, QueuedPromptsPa
|
||||
use super::view::{
|
||||
ExecuteCommandEvent, SyncInputType, TerminalAction, PADDING_LEFT as TERMINAL_VIEW_PADDING_LEFT,
|
||||
};
|
||||
use super::warpify::SubshellSource;
|
||||
use super::wormhole::SubshellSource;
|
||||
use super::{prompt, History, HistoryEntry, SizeInfo, TerminalModel, UpArrowHistoryConfig};
|
||||
use crate::ai::agent::conversation::AIConversationId;
|
||||
use crate::ai::agent::{
|
||||
@@ -11829,13 +11829,13 @@ impl Input {
|
||||
|
||||
// CLI agent rich input in shell mode (! prefix) should allow completions
|
||||
// even though the active block is a long-running command.
|
||||
// However, completions are disabled on warpified remote hosts because
|
||||
// However, completions are disabled on wormholed remote hosts because
|
||||
// in-band generators don't work in this context (with CLI agent).
|
||||
let is_cli_agent_shell_mode = self.is_locked_in_shell_mode(ctx)
|
||||
&& CLIAgentSessionsModel::as_ref(ctx).is_input_open(self.terminal_view_id)
|
||||
&& !self
|
||||
.active_session(ctx)
|
||||
.is_some_and(|s| matches!(s.session_type(), SessionType::WarpifiedRemote { .. }));
|
||||
.is_some_and(|s| matches!(s.session_type(), SessionType::WormholedRemote { .. }));
|
||||
|
||||
// If the cursor is in a valid completion position, go into CompletionSuggestions mode
|
||||
if (is_command_grid_active || is_cli_agent_shell_mode) && self.can_query_history(ctx) {
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::terminal::input::common::{
|
||||
use crate::terminal::input::{get_input_box_top_border_width, InputDropTargetData};
|
||||
use crate::terminal::settings::{SpacingMode, TerminalSettings};
|
||||
use crate::terminal::view::TerminalAction;
|
||||
use crate::terminal::warpify::render::{render_subshell_flag, render_subshell_flag_pole};
|
||||
use crate::terminal::wormhole::render::{render_subshell_flag, render_subshell_flag_pole};
|
||||
|
||||
impl Input {
|
||||
/// Renders the classic input. This is used when the user has 'Honor PS1' enabled in settings,
|
||||
|
||||
@@ -31,8 +31,8 @@ pub struct LineEditorStatus {
|
||||
///
|
||||
/// When receiving an end prompt marker in zsh, this is used as a proxy to determine if the
|
||||
/// session is bootstrapped -- the prompt markers are emitted by zsh regardless of whether or
|
||||
/// not its a Warpified session, so to in order properly signal downstream that the line editor
|
||||
/// (for Warpified sessions) is active, we must check if there was a corresponding precmd
|
||||
/// not its a Wormholed session, so to in order properly signal downstream that the line editor
|
||||
/// (for Wormholed sessions) is active, we must check if there was a corresponding precmd
|
||||
/// emitted prior to the end prompt marker.
|
||||
///
|
||||
/// Precmd is always emitted before prompt markers.
|
||||
|
||||
@@ -53,7 +53,7 @@ use crate::terminal::session_settings::{SessionSettings, ToolbarChipSelection};
|
||||
use crate::terminal::shared_session::sharer::network::Network;
|
||||
use crate::terminal::shared_session::{IsSharedSessionCreator, SharedSessionStatus};
|
||||
use crate::terminal::shell::ShellName;
|
||||
use crate::terminal::warpify::settings::WarpifySettings;
|
||||
use crate::terminal::wormhole::settings::WormholeSettings;
|
||||
use crate::terminal::writeable_pty::pty_controller::{EventLoopSendError, EventLoopSender};
|
||||
use crate::terminal::writeable_pty::terminal_manager_util::{
|
||||
init_pty_controller_model, init_remote_server_controller, wire_up_pty_controller_with_surface,
|
||||
@@ -740,13 +740,11 @@ impl<S> TerminalManager<S> {
|
||||
.contains(&ContextChipKind::NodeVersion)
|
||||
};
|
||||
|
||||
// `enable_ssh_warpification` is the single source of truth for whether the SSH
|
||||
// wrapper is active. The bootstrap scripts check `WARP_USE_SSH_WRAPPER` (derived
|
||||
// `enable_ssh_wormholing` is the single source of truth for whether the SSH
|
||||
// wrapper is active. The bootstrap scripts check `GALAXY_USE_SSH_WRAPPER` (derived
|
||||
// from this value) before invoking `warp_ssh_helper`, which spawns the ControlMaster
|
||||
// and opens agent-protocol channels.
|
||||
let enable_ssh_wrapper = *WarpifySettings::as_ref(ctx)
|
||||
.enable_ssh_warpification
|
||||
.value();
|
||||
let enable_ssh_wrapper = *WormholeSettings::as_ref(ctx).enable_ssh_wormholing.value();
|
||||
|
||||
// Only meaningful when the legacy ControlMaster wrapper is active.
|
||||
let reuse_ssh_control_master = enable_ssh_wrapper
|
||||
|
||||
@@ -306,7 +306,7 @@ fn build_host_shell_command(
|
||||
// Whether the SSH wrapper should attach to an existing ControlMaster
|
||||
// for the destination host instead of always creating its own.
|
||||
builder.env(
|
||||
"WARP_SSH_REUSE_CONTROL_MASTER",
|
||||
"GALAXY_SSH_REUSE_CONTROL_MASTER",
|
||||
if reuse_ssh_control_master { "1" } else { "0" },
|
||||
);
|
||||
|
||||
@@ -784,8 +784,8 @@ fn build_docker_sandbox_command(
|
||||
// TODO(advait): audit this list. It currently mirrors what the
|
||||
// pre-refactor host-shell `spawn` set when the starter happened to
|
||||
// be a Docker sandbox, so behaviour is unchanged from before the
|
||||
// split. Many of these (e.g. `WARP_USE_SSH_WRAPPER`,
|
||||
// `SSH_SOCKET_DIR`, `HISTFILESIZE`, `WARP_IS_LOCAL_SHELL_SESSION`)
|
||||
// split. Many of these (e.g. `GALAXY_USE_SSH_WRAPPER`,
|
||||
// `SSH_SOCKET_DIR`, `HISTFILESIZE`, `GALAXY_IS_LOCAL_SHELL_SESSION`)
|
||||
// are set on the *host* `sbx` process and may or may not propagate
|
||||
// into the container depending on `sbx`'s env passthrough rules.
|
||||
// Once we've validated what the container bootstrap actually needs,
|
||||
@@ -813,7 +813,7 @@ fn build_docker_sandbox_command(
|
||||
if enable_ssh_wrapper { "1" } else { "0" },
|
||||
);
|
||||
builder.env(
|
||||
"WARP_SSH_REUSE_CONTROL_MASTER",
|
||||
"GALAXY_SSH_REUSE_CONTROL_MASTER",
|
||||
if reuse_ssh_control_master { "1" } else { "0" },
|
||||
);
|
||||
builder.env("SSH_SOCKET_DIR", ssh_socket_dir());
|
||||
|
||||
@@ -18,15 +18,15 @@ use crate::terminal::local_tty::PtyOptions;
|
||||
const HONOR_PS1_NAME: &str = "WARP_HONOR_PS1";
|
||||
const PROMPT_NODE_VERSION_ENABLED_NAME: &str = "WARP_PROMPT_NODE_VERSION_ENABLED";
|
||||
const INITIAL_WORKING_DIR_NAME: &str = "WARP_INITIAL_WORKING_DIR";
|
||||
const USE_SSH_WRAPPER_NAME: &str = "WARP_USE_SSH_WRAPPER";
|
||||
const SSH_REUSE_CONTROL_MASTER_NAME: &str = "WARP_SSH_REUSE_CONTROL_MASTER";
|
||||
const USE_SSH_WRAPPER_NAME: &str = "GALAXY_USE_SSH_WRAPPER";
|
||||
const SSH_REUSE_CONTROL_MASTER_NAME: &str = "GALAXY_SSH_REUSE_CONTROL_MASTER";
|
||||
const SHELL_DEBUG_MODE_NAME: &str = "WARP_SHELL_DEBUG_MODE";
|
||||
const TERM_PROGRAM_NAME: &str = "TERM_PROGRAM";
|
||||
const IS_LOCAL_SESSION_NAME: &str = "WARP_IS_LOCAL_SHELL_SESSION";
|
||||
const IS_LOCAL_SESSION_NAME: &str = "GALAXY_IS_LOCAL_SHELL_SESSION";
|
||||
const SSH_SOCKET_DIR: &str = "SSH_SOCKET_DIR";
|
||||
const PATH_APPEND_NAME: &str = "WARP_PATH_APPEND";
|
||||
const CLIENT_VERSION_NAME: &str = "WARP_CLIENT_VERSION";
|
||||
const CLI_AGENT_PROTOCOL_VERSION_NAME: &str = "WARP_CLI_AGENT_PROTOCOL_VERSION";
|
||||
const CLIENT_VERSION_NAME: &str = "GALAXY_CLIENT_VERSION";
|
||||
const CLI_AGENT_PROTOCOL_VERSION_NAME: &str = "GALAXY_CLI_AGENT_PROTOCOL_VERSION";
|
||||
const WSLENV: &str = "WSLENV";
|
||||
const HISTIGNORE: &str = "HISTIGNORE";
|
||||
|
||||
|
||||
@@ -79,8 +79,8 @@ pub mod ssh;
|
||||
pub mod terminal_manager;
|
||||
mod terminal_size_element;
|
||||
pub mod view;
|
||||
pub mod warpify;
|
||||
mod waterfall_gap_element;
|
||||
pub mod wormhole;
|
||||
mod writeable_pty;
|
||||
#[cfg(feature = "tui")]
|
||||
pub use writeable_pty::{PtyIntent, PtyIntentEvent, TerminalSurface};
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::terminal::model::block::BlockSection;
|
||||
use crate::terminal::model::index::{Direction, Point, Side};
|
||||
use crate::terminal::model::selection::{ExpandedSelectionRange, Selection, SelectionDirection};
|
||||
use crate::terminal::model::terminal_model::{BlockIndex, WithinBlock};
|
||||
use crate::terminal::warpify::success_block::WarpifySuccessBlock;
|
||||
use crate::terminal::wormhole::success_block::WormholeSuccessBlock;
|
||||
use crate::terminal::GridType;
|
||||
|
||||
/// A selection that can span multiple blocks (and thus grids). Here row is the number of lines from
|
||||
@@ -998,12 +998,13 @@ impl BlockList {
|
||||
}
|
||||
|
||||
if let Some(active_window_id) = app.windows().active_window() {
|
||||
if let Some(ssh_block) = app
|
||||
.view_with_id::<WarpifySuccessBlock>(active_window_id, *view_id)
|
||||
{
|
||||
let warpify_success_block = app.view(&ssh_block);
|
||||
if let Some(ssh_block) = app.view_with_id::<WormholeSuccessBlock>(
|
||||
active_window_id,
|
||||
*view_id,
|
||||
) {
|
||||
let wormhole_success_block = app.view(&ssh_block);
|
||||
if let Some(selected_text) =
|
||||
warpify_success_block.selected_text()
|
||||
wormhole_success_block.selected_text()
|
||||
{
|
||||
selected_texts.push(selected_text);
|
||||
}
|
||||
@@ -1123,10 +1124,10 @@ impl BlockList {
|
||||
}
|
||||
|
||||
if let Some(ssh_block) =
|
||||
app.view_with_id::<WarpifySuccessBlock>(active_window_id, view_id)
|
||||
app.view_with_id::<WormholeSuccessBlock>(active_window_id, view_id)
|
||||
{
|
||||
let warpify_success_block = app.view(&ssh_block);
|
||||
if let Some(selected_text) = warpify_success_block.selected_text() {
|
||||
let wormhole_success_block = app.view(&ssh_block);
|
||||
if let Some(selected_text) = wormhole_success_block.selected_text() {
|
||||
selected_texts.push(selected_text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
pub enum RichContentType {
|
||||
AIBlock,
|
||||
EnterAgentView,
|
||||
WarpifySuccessBlock,
|
||||
WormholeSuccessBlock,
|
||||
InlineAgentViewHeader,
|
||||
AgentViewZeroState,
|
||||
TerminalViewZeroState,
|
||||
|
||||
@@ -40,7 +40,7 @@ use crate::remote_server::manager::{RemoteServerManager, RemoteServerManagerEven
|
||||
use crate::server::telemetry::{BootstrappingInfo, TelemetryEvent};
|
||||
use crate::terminal::event::{ExecutedExecutorCommandEvent, RemoteServerSetupState};
|
||||
use crate::terminal::shell::{Shell, ShellType};
|
||||
use crate::terminal::warpify::SubshellSource;
|
||||
use crate::terminal::wormhole::SubshellSource;
|
||||
use crate::terminal::{History, ShellHost, ShellLaunchData};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
@@ -361,7 +361,7 @@ impl Sessions {
|
||||
let session = Arc::new(session);
|
||||
self.sessions.insert(session.id(), session.clone());
|
||||
|
||||
// For warpified-remote sessions, pick up the current host_id from
|
||||
// For wormholed-remote sessions, pick up the current host_id from
|
||||
// the manager so session.remote_host_id() is populated without
|
||||
// waiting for the next SessionConnected event. The
|
||||
// RemoteServerCommandExecutor already has its client baked in, so
|
||||
@@ -370,7 +370,7 @@ impl Sessions {
|
||||
if FeatureFlag::SshRemoteServer.is_enabled()
|
||||
&& matches!(
|
||||
session_info.session_type,
|
||||
BootstrapSessionType::WarpifiedRemote
|
||||
BootstrapSessionType::WormholedRemote
|
||||
)
|
||||
{
|
||||
if let Some(host_id) = RemoteServerManager::as_ref(ctx).host_id_for_session(session_id)
|
||||
@@ -518,7 +518,7 @@ impl Sessions {
|
||||
impl From<SessionType> for command_corrections::SessionType {
|
||||
fn from(session_type: SessionType) -> Self {
|
||||
match session_type {
|
||||
SessionType::WarpifiedRemote { .. } => command_corrections::SessionType::Remote,
|
||||
SessionType::WormholedRemote { .. } => command_corrections::SessionType::Remote,
|
||||
SessionType::Local => command_corrections::SessionType::Local,
|
||||
}
|
||||
}
|
||||
@@ -527,20 +527,20 @@ impl From<SessionType> for command_corrections::SessionType {
|
||||
impl From<&SessionType> for command_corrections::SessionType {
|
||||
fn from(session_type: &SessionType) -> Self {
|
||||
match session_type {
|
||||
SessionType::WarpifiedRemote { .. } => command_corrections::SessionType::Remote,
|
||||
SessionType::WormholedRemote { .. } => command_corrections::SessionType::Remote,
|
||||
SessionType::Local => command_corrections::SessionType::Local,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a session was established by Warp's in-band SSH wrapper — the shell function our
|
||||
/// Whether a session was established by Galaxy's in-band SSH wrapper — the shell function our
|
||||
/// bootstrap injects that intercepts `ssh`, sets up a ControlMaster connection, and bootstraps
|
||||
/// the remote shell. This applies to all SSH warpification today: the remote-server SSH
|
||||
/// the remote shell. This applies to all SSH wormholing today: the remote-server SSH
|
||||
/// extension also runs on top of a wrapper session (reusing the ControlMaster socket for its
|
||||
/// proxy and for the `RemoteCommandExecutor` fallback).
|
||||
///
|
||||
/// `No` covers local sessions, subshells, and remote sessions warpified *without* the wrapper
|
||||
/// (e.g. via the auto-warpify RC snippet inside an unwrapped `ssh` session), which carry no
|
||||
/// `No` covers local sessions, subshells, and remote sessions wormholed *without* the wrapper
|
||||
/// (e.g. via the auto-wormhole RC snippet inside an unwrapped `ssh` session), which carry no
|
||||
/// ControlMaster socket.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum IsSSHWrapperSession {
|
||||
@@ -550,7 +550,7 @@ pub enum IsSSHWrapperSession {
|
||||
socket_path: PathBuf,
|
||||
/// `true` when `socket_path` points at a ControlMaster the user
|
||||
/// already had running (the SSH wrapper attached to it instead of
|
||||
/// creating a Warp-owned one). Warp must not tear down such a
|
||||
/// creating a Galaxy-owned one). Galaxy must not tear down such a
|
||||
/// master on session exit.
|
||||
external_control_master: bool,
|
||||
},
|
||||
@@ -651,7 +651,7 @@ impl SessionInfo {
|
||||
matches!(&is_ssh_wrapper_session, IsSSHWrapperSession::Yes { .. }),
|
||||
);
|
||||
|
||||
let spawning_session_id = if matches!(session_type, BootstrapSessionType::WarpifiedRemote)
|
||||
let spawning_session_id = if matches!(session_type, BootstrapSessionType::WormholedRemote)
|
||||
|| subshell_info.is_some()
|
||||
{
|
||||
active_block_session_id
|
||||
@@ -699,7 +699,7 @@ impl SessionInfo {
|
||||
{
|
||||
BootstrapSessionType::Local
|
||||
} else {
|
||||
BootstrapSessionType::WarpifiedRemote
|
||||
BootstrapSessionType::WormholedRemote
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -715,7 +715,7 @@ impl SessionInfo {
|
||||
_is_ssh_session: bool,
|
||||
) -> BootstrapSessionType {
|
||||
// When the `remote_tty` feature is enabled--the session is always considered remote.
|
||||
BootstrapSessionType::WarpifiedRemote
|
||||
BootstrapSessionType::WormholedRemote
|
||||
}
|
||||
|
||||
/// Returns a fully populated [`SessionInfo`] containing data derived from the given
|
||||
@@ -859,26 +859,26 @@ impl SessionInfo {
|
||||
/// which happens *after* the session is bootstrapped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum BootstrapSessionType {
|
||||
/// The session host is the same host where Warp is running.
|
||||
/// The session host is the same host where Galaxy is running.
|
||||
Local,
|
||||
|
||||
/// The session host is a different host from where Warp is running.
|
||||
WarpifiedRemote,
|
||||
/// The session host is a different host from where Galaxy is running.
|
||||
WormholedRemote,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SessionType {
|
||||
/// The session host is the same host where Warp is running.
|
||||
/// The session host is the same host where Galaxy is running.
|
||||
Local,
|
||||
|
||||
/// The session host is a different host from where Warp is running.
|
||||
/// Note that we only know this for sure when we Warpify a block.
|
||||
/// The session host is a different host from where Galaxy is running.
|
||||
/// Note that we only know this for sure when we Wormhole a block.
|
||||
///
|
||||
/// `host_id` is `Some` when the remote server feature flag is enabled and
|
||||
/// `RemoteServerManager` has completed the connection handshake. It is
|
||||
/// `None` when the feature flag is off or the connection hasn't been
|
||||
/// established yet.
|
||||
WarpifiedRemote {
|
||||
WormholedRemote {
|
||||
host_id: Option<galaxy_core::HostId>,
|
||||
},
|
||||
}
|
||||
@@ -887,7 +887,7 @@ impl From<BootstrapSessionType> for SessionType {
|
||||
fn from(bst: BootstrapSessionType) -> Self {
|
||||
match bst {
|
||||
BootstrapSessionType::Local => SessionType::Local,
|
||||
BootstrapSessionType::WarpifiedRemote => SessionType::WarpifiedRemote { host_id: None },
|
||||
BootstrapSessionType::WormholedRemote => SessionType::WormholedRemote { host_id: None },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -964,11 +964,11 @@ impl Session {
|
||||
self.session_type.lock().clone()
|
||||
}
|
||||
|
||||
/// Updates the `host_id` on a `WarpifiedRemote` session type after the
|
||||
/// Updates the `host_id` on a `WormholedRemote` session type after the
|
||||
/// remote server handshake completes (or clears it on disconnect).
|
||||
pub fn set_remote_host_id(&self, host_id: Option<galaxy_core::HostId>) {
|
||||
let mut st = self.session_type.lock();
|
||||
if let SessionType::WarpifiedRemote { host_id: ref mut h } = *st {
|
||||
if let SessionType::WormholedRemote { host_id: ref mut h } = *st {
|
||||
*h = host_id;
|
||||
}
|
||||
}
|
||||
@@ -1012,9 +1012,9 @@ impl Session {
|
||||
self.info.host_info.clone()
|
||||
}
|
||||
|
||||
/// Returns whether this session was established by Warp's in-band SSH wrapper (see
|
||||
/// [`IsSSHWrapperSession`]). Note this stays `false` for remote sessions warpified via
|
||||
/// the auto-warpify RC snippet inside an unwrapped `ssh` session.
|
||||
/// Returns whether this session was established by Galaxy's in-band SSH wrapper (see
|
||||
/// [`IsSSHWrapperSession`]). Note this stays `false` for remote sessions wormholed via
|
||||
/// the auto-wormhole RC snippet inside an unwrapped `ssh` session.
|
||||
pub fn is_ssh_wrapper_session(&self) -> bool {
|
||||
matches!(
|
||||
self.info.is_ssh_wrapper_session,
|
||||
@@ -1023,7 +1023,7 @@ impl Session {
|
||||
}
|
||||
|
||||
pub fn is_subshell_or_ssh(&self) -> bool {
|
||||
matches!(self.session_type(), SessionType::WarpifiedRemote { .. })
|
||||
matches!(self.session_type(), SessionType::WormholedRemote { .. })
|
||||
|| self.is_ssh_wrapper_session()
|
||||
|| self.subshell_info().is_some()
|
||||
}
|
||||
@@ -1539,7 +1539,7 @@ impl Session {
|
||||
self.read_history_for_local_session(is_kaspersky_running)
|
||||
.await
|
||||
}
|
||||
BootstrapSessionType::WarpifiedRemote => self.read_history_for_remote_session().await,
|
||||
BootstrapSessionType::WormholedRemote => self.read_history_for_remote_session().await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1635,22 +1635,22 @@ impl Session {
|
||||
/// Converts the given directory into a [`typed_path::TypedPathBuf`].
|
||||
pub fn convert_directory_to_typed_path_buf(&self, pwd: String) -> TypedPathBuf {
|
||||
// We need to determine whether this session requires windows file paths
|
||||
// or unix file paths. This needs to be resilient to warpified ssh. Some examples:
|
||||
// or unix file paths. This needs to be resilient to wormholed ssh. Some examples:
|
||||
// - bash on mac ---> unix
|
||||
// - powershell on linux ---> unix
|
||||
// - powershell on windows ---> windows
|
||||
// - wsl on windows ---> unix
|
||||
// - warpified zsh --> unix
|
||||
// - wormholed zsh --> unix
|
||||
|
||||
// If the host architecture is unix, we can infer unix file paths. This would break
|
||||
// if we supported warpifying a powershell-on-windows SSH session.
|
||||
// if we supported wormholing a powershell-on-windows SSH session.
|
||||
if cfg!(unix) {
|
||||
return TypedPathBuf::from_unix(pwd);
|
||||
}
|
||||
|
||||
// We assume that we're on Windows.
|
||||
match self.shell_family() {
|
||||
// Cases: WSL, MSYS2, warpified bash
|
||||
// Cases: WSL, MSYS2, wormholed bash
|
||||
ShellFamily::Posix => TypedPathBuf::from_unix(pwd),
|
||||
// Cases: powershell sessions
|
||||
ShellFamily::PowerShell => TypedPathBuf::from_windows(pwd),
|
||||
@@ -1671,7 +1671,7 @@ impl Display for Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the hostname for the local machine where Warp is running.
|
||||
/// Returns the hostname for the local machine where Galaxy is running.
|
||||
pub fn get_local_hostname() -> Result<String> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(not(target_family = "wasm"))] {
|
||||
@@ -1786,7 +1786,7 @@ pub mod testing {
|
||||
|
||||
pub fn with_ssh_socket_path(mut self, socket_path: PathBuf) -> Self {
|
||||
if let BootstrapSessionType::Local = self.session_type {
|
||||
self.session_type = BootstrapSessionType::WarpifiedRemote;
|
||||
self.session_type = BootstrapSessionType::WormholedRemote;
|
||||
}
|
||||
self.is_ssh_wrapper_session = IsSSHWrapperSession::Yes {
|
||||
socket_path,
|
||||
@@ -1856,7 +1856,7 @@ pub mod testing {
|
||||
|
||||
pub fn test_remote() -> Self {
|
||||
let info = SessionInfo::new_for_test()
|
||||
.with_session_type(BootstrapSessionType::WarpifiedRemote)
|
||||
.with_session_type(BootstrapSessionType::WormholedRemote)
|
||||
.with_shell_type(ShellType::Bash); // We only support UNIX-based remote sessions.
|
||||
let session_type = SessionType::from(info.session_type.clone());
|
||||
Self {
|
||||
|
||||
@@ -100,12 +100,12 @@ impl ActiveSession {
|
||||
/// the connected host ID.
|
||||
pub fn location_for_path(&self, path: &str, app: &AppContext) -> Option<LocalOrRemotePath> {
|
||||
match self.session_type(app) {
|
||||
Some(SessionType::WarpifiedRemote {
|
||||
Some(SessionType::WormholedRemote {
|
||||
host_id: Some(host_id),
|
||||
}) => StandardizedPath::try_new(path)
|
||||
.ok()
|
||||
.map(|path| LocalOrRemotePath::Remote(RemotePath::new(host_id, path))),
|
||||
Some(SessionType::WarpifiedRemote { host_id: None }) => None,
|
||||
Some(SessionType::WormholedRemote { host_id: None }) => None,
|
||||
Some(SessionType::Local) | None => {
|
||||
let path =
|
||||
dunce::canonicalize(Path::new(path)).unwrap_or_else(|_| PathBuf::from(path));
|
||||
|
||||
@@ -289,7 +289,7 @@ fn new_command_executor_for_local_tty_session(
|
||||
}
|
||||
}
|
||||
}
|
||||
BootstrapSessionType::WarpifiedRemote
|
||||
BootstrapSessionType::WormholedRemote
|
||||
if is_ssh_wrapper_session
|
||||
&& !FeatureFlag::InBandGeneratorsForSSH.is_enabled()
|
||||
&& !force_use_in_band_generators =>
|
||||
|
||||
@@ -114,11 +114,11 @@ fn test_malicious_histfile_path_does_not_execute_injected_commands() {
|
||||
let malicious_histfile = format!("/tmp/x'; touch {marker}; echo '");
|
||||
|
||||
let session_info = SessionInfo::new_for_test()
|
||||
.with_session_type(BootstrapSessionType::WarpifiedRemote)
|
||||
.with_session_type(BootstrapSessionType::WormholedRemote)
|
||||
.with_histfile(Some(malicious_histfile));
|
||||
let session = Session::new(session_info, Arc::new(TestCommandExecutor::default()));
|
||||
|
||||
// read_history for a WarpifiedRemote session calls read_history_from_file,
|
||||
// read_history for a WormholedRemote session calls read_history_from_file,
|
||||
// which builds `cat '{escaped_path}'` and executes it via TestCommandExecutor
|
||||
let _ = session.read_history(false).await;
|
||||
|
||||
|
||||
@@ -351,7 +351,7 @@ enum IsReceivingHook {
|
||||
No,
|
||||
}
|
||||
|
||||
/// Information needed to render a warpify "success" block upon successful subshell bootstrap.
|
||||
/// Information needed to render a wormhole "success" block upon successful subshell bootstrap.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SubshellSuccessBlockInfo {
|
||||
/// The ID of the newly bootstrapped subshell session.
|
||||
@@ -2256,7 +2256,7 @@ impl TerminalModel {
|
||||
/// a line of output that is not a known SSH output, we consider that to be some mild evidence that
|
||||
/// login is complete. Though, because that output line might be a false alarm (i.e., it could be
|
||||
/// an SSH banner OR a line like "Permission denied."), we wait some amount of time and check again
|
||||
/// before indicating we're ready for warpification.
|
||||
/// before indicating we're ready for wormholing.
|
||||
pub fn check_for_end_of_ssh_login(&mut self, confirmation_check: bool) {
|
||||
let Some(mut ssh_login_state) = self.notify_on_end_of_ssh_login.clone() else {
|
||||
return;
|
||||
@@ -2279,7 +2279,7 @@ impl TerminalModel {
|
||||
SshLoginState::LastLogin | SshLoginState::PromptDetected => {
|
||||
self.event_proxy
|
||||
.send_terminal_event(Event::DetectedEndOfSshLogin(
|
||||
SshLoginStatus::ReadyToWarpify,
|
||||
SshLoginStatus::ReadyToWormhole,
|
||||
));
|
||||
|
||||
ssh_login_state.notification_state = SshLoginNotificationState::Completed;
|
||||
@@ -2290,7 +2290,7 @@ impl TerminalModel {
|
||||
if ssh_login_state.notification_state == SshLoginNotificationState::Monitoring {
|
||||
self.event_proxy
|
||||
.send_terminal_event(Event::DetectedEndOfSshLogin(
|
||||
SshLoginStatus::RecheckBeforeWarpifying,
|
||||
SshLoginStatus::RecheckBeforeWormholing,
|
||||
));
|
||||
|
||||
// We want to avoid emitting redundant events for the initial check.
|
||||
@@ -2300,7 +2300,7 @@ impl TerminalModel {
|
||||
} else {
|
||||
self.event_proxy
|
||||
.send_terminal_event(Event::DetectedEndOfSshLogin(
|
||||
SshLoginStatus::ReadyToWarpify,
|
||||
SshLoginStatus::ReadyToWormhole,
|
||||
));
|
||||
|
||||
ssh_login_state.notification_state = SshLoginNotificationState::Completed;
|
||||
|
||||
@@ -381,7 +381,7 @@ pub enum ModelEvent {
|
||||
ExecutedInBandCommand(ExecutedExecutorCommandEvent),
|
||||
/// 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.
|
||||
/// useful for detecting when an ssh session becomes ready for wormholing.
|
||||
DetectedEndOfSshLogin(SshLoginStatus),
|
||||
InitSubshell(InitSubshellEvent),
|
||||
/// Emitted when the user's RC file has been executed in a subshell.
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn user_and_host_name_string(
|
||||
) -> Option<String> {
|
||||
match session_type {
|
||||
SessionType::Local => None,
|
||||
SessionType::WarpifiedRemote { .. } => Some(format!("{user}@{hostname}:")),
|
||||
SessionType::WormholedRemote { .. } => Some(format!("{user}@{hostname}:")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -251,13 +251,11 @@ impl PromptRenderHelper {
|
||||
RemoteServerSetupState::Checking => "Starting shell...".to_string(),
|
||||
RemoteServerSetupState::Installing {
|
||||
progress_percent: Some(p),
|
||||
} => format!("Installing Warp SSH Extension... ({p}%)"),
|
||||
} => format!("Installing Wormhole helper... ({p}%)"),
|
||||
RemoteServerSetupState::Installing {
|
||||
progress_percent: None,
|
||||
} => "Installing Warp SSH Extension...".to_string(),
|
||||
RemoteServerSetupState::Updating => {
|
||||
"Updating Warp SSH Extension...".to_string()
|
||||
}
|
||||
} => "Installing Wormhole helper...".to_string(),
|
||||
RemoteServerSetupState::Updating => "Updating Wormhole helper...".to_string(),
|
||||
RemoteServerSetupState::Initializing => "Initializing...".to_string(),
|
||||
RemoteServerSetupState::Ready => "Starting shell...".to_string(),
|
||||
// Failed and Unsupported both fall back to the wrapper-only SSH
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::appearance::Appearance;
|
||||
use crate::terminal::model::ansi::WarpificationUnavailableReason;
|
||||
use crate::terminal::warpify;
|
||||
use crate::terminal::warpify::render::apply_spacing_styles;
|
||||
use crate::terminal::warpify::render::build_description_row;
|
||||
use crate::terminal::warpify::settings::WarpifySettings;
|
||||
use crate::terminal::model::ansi::WormholingUnavailableReason;
|
||||
use crate::terminal::wormhole;
|
||||
use crate::terminal::wormhole::render::apply_spacing_styles;
|
||||
use crate::terminal::wormhole::render::build_description_row;
|
||||
use crate::terminal::wormhole::settings::WormholeSettings;
|
||||
use crate::ui_components::icons::Icon as UiIcon;
|
||||
use galaxy_core::channel::ChannelState;
|
||||
use galaxy_core::ui::theme::GalaxyTheme;
|
||||
@@ -35,7 +35,7 @@ const UNSUPPORTED_TMUX_VERSION_ERROR: &str =
|
||||
"The tmux version available on the remote machine is below 3.0. Please install tmux 3.0 or greater using a different method and try again.";
|
||||
const TMUX_FAILED_ERROR: &str =
|
||||
"tmux failed to execute on the remote machine. Please re-install tmux and try again.";
|
||||
const WARPIFY_TIMEOUT_ERROR: &str = "Wormholing the session hit a timeout.";
|
||||
const WORMHOLE_TIMEOUT_ERROR: &str = "Wormholing the session hit a timeout.";
|
||||
const UNSUPPORTED_SHELL_ERROR: &str =
|
||||
"Unsupported shell. Please set bash, zsh, or fish as your default shell and try again.";
|
||||
const TMUX_INSTALL_FAILED_ERROR: &str =
|
||||
@@ -55,28 +55,28 @@ fn get_ssh_github_issue_url(title: &str) -> String {
|
||||
format!("{url}&title={title}")
|
||||
}
|
||||
|
||||
impl WarpificationUnavailableReason {
|
||||
impl WormholingUnavailableReason {
|
||||
fn error_message(&self) -> &'static str {
|
||||
match self {
|
||||
WarpificationUnavailableReason::TmuxNotInstalled { .. } => TMUX_NOT_INSTALLED_ERROR,
|
||||
WarpificationUnavailableReason::UnsupportedTmuxVersion { .. } => {
|
||||
WormholingUnavailableReason::TmuxNotInstalled { .. } => TMUX_NOT_INSTALLED_ERROR,
|
||||
WormholingUnavailableReason::UnsupportedTmuxVersion { .. } => {
|
||||
UNSUPPORTED_TMUX_VERSION_ERROR
|
||||
}
|
||||
WarpificationUnavailableReason::TmuxFailed => TMUX_FAILED_ERROR,
|
||||
WarpificationUnavailableReason::Timeout { .. } => WARPIFY_TIMEOUT_ERROR,
|
||||
WarpificationUnavailableReason::UnsupportedShell { .. } => UNSUPPORTED_SHELL_ERROR,
|
||||
WarpificationUnavailableReason::TmuxInstallFailed { .. } => TMUX_INSTALL_FAILED_ERROR,
|
||||
WormholingUnavailableReason::TmuxFailed => TMUX_FAILED_ERROR,
|
||||
WormholingUnavailableReason::Timeout { .. } => WORMHOLE_TIMEOUT_ERROR,
|
||||
WormholingUnavailableReason::UnsupportedShell { .. } => UNSUPPORTED_SHELL_ERROR,
|
||||
WormholingUnavailableReason::TmuxInstallFailed { .. } => TMUX_INSTALL_FAILED_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_title(&self) -> &'static str {
|
||||
match self {
|
||||
WarpificationUnavailableReason::TmuxNotInstalled { .. } => "tmux Not Installed",
|
||||
WarpificationUnavailableReason::UnsupportedTmuxVersion { .. } => {
|
||||
WormholingUnavailableReason::TmuxNotInstalled { .. } => "tmux Not Installed",
|
||||
WormholingUnavailableReason::UnsupportedTmuxVersion { .. } => {
|
||||
"Unsupported Tmux Version"
|
||||
}
|
||||
WarpificationUnavailableReason::TmuxFailed => "tmux Failed",
|
||||
WarpificationUnavailableReason::Timeout {
|
||||
WormholingUnavailableReason::TmuxFailed => "tmux Failed",
|
||||
WormholingUnavailableReason::Timeout {
|
||||
is_tmux_install, ..
|
||||
} => {
|
||||
if *is_tmux_install {
|
||||
@@ -85,34 +85,34 @@ impl WarpificationUnavailableReason {
|
||||
"SSH Wormhole Timeout"
|
||||
}
|
||||
}
|
||||
WarpificationUnavailableReason::UnsupportedShell { .. } => "Unsupported Shell",
|
||||
WarpificationUnavailableReason::TmuxInstallFailed { .. } => "tmux Install Failed",
|
||||
WormholingUnavailableReason::UnsupportedShell { .. } => "Unsupported Shell",
|
||||
WormholingUnavailableReason::TmuxInstallFailed { .. } => "tmux Install Failed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SshErrorBlockEvent {
|
||||
ContinueWithoutWarpification,
|
||||
WarpifyWithoutTmux,
|
||||
ContinueWithoutWormholing,
|
||||
WormholeWithoutTmux,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SshErrorBlockAction {
|
||||
ContinueWithoutWarpification,
|
||||
WarpifyWithoutTmux,
|
||||
ContinueWithoutWormholing,
|
||||
WormholeWithoutTmux,
|
||||
OpenUrl(String),
|
||||
AddSshHostToDenylist(String),
|
||||
Focus,
|
||||
}
|
||||
|
||||
pub struct SshErrorBlock {
|
||||
error_reason: WarpificationUnavailableReason,
|
||||
error_reason: WormholingUnavailableReason,
|
||||
ssh_host: Option<String>,
|
||||
warpify_without_tmux_button_mouse_state: MouseStateHandle,
|
||||
wormhole_without_tmux_button_mouse_state: MouseStateHandle,
|
||||
continue_button_mouse_state: MouseStateHandle,
|
||||
report_link_highlight_index: HighlightedHyperlink,
|
||||
never_warpify_mouse_state_handle: MouseStateHandle,
|
||||
never_wormhole_mouse_state_handle: MouseStateHandle,
|
||||
block_mouse_state: MouseStateHandle,
|
||||
is_focused: bool,
|
||||
}
|
||||
@@ -123,17 +123,17 @@ pub fn init(app: &mut AppContext) {
|
||||
app.register_fixed_bindings([
|
||||
FixedBinding::new(
|
||||
"enter",
|
||||
SshErrorBlockAction::WarpifyWithoutTmux,
|
||||
SshErrorBlockAction::WormholeWithoutTmux,
|
||||
id!(SshErrorBlock::ui_name()),
|
||||
),
|
||||
FixedBinding::new(
|
||||
"escape",
|
||||
SshErrorBlockAction::ContinueWithoutWarpification,
|
||||
SshErrorBlockAction::ContinueWithoutWormholing,
|
||||
id!(SshErrorBlock::ui_name()),
|
||||
),
|
||||
FixedBinding::new(
|
||||
"ctrl-c",
|
||||
SshErrorBlockAction::ContinueWithoutWarpification,
|
||||
SshErrorBlockAction::ContinueWithoutWormholing,
|
||||
id!(SshErrorBlock::ui_name()),
|
||||
),
|
||||
]);
|
||||
@@ -141,14 +141,14 @@ pub fn init(app: &mut AppContext) {
|
||||
|
||||
impl SshErrorBlock {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new(error_reason: WarpificationUnavailableReason, ssh_host: Option<String>) -> Self {
|
||||
pub fn new(error_reason: WormholingUnavailableReason, ssh_host: Option<String>) -> Self {
|
||||
Self {
|
||||
error_reason,
|
||||
ssh_host,
|
||||
warpify_without_tmux_button_mouse_state: Default::default(),
|
||||
wormhole_without_tmux_button_mouse_state: Default::default(),
|
||||
continue_button_mouse_state: Default::default(),
|
||||
report_link_highlight_index: Default::default(),
|
||||
never_warpify_mouse_state_handle: Default::default(),
|
||||
never_wormhole_mouse_state_handle: Default::default(),
|
||||
block_mouse_state: Default::default(),
|
||||
is_focused: false,
|
||||
}
|
||||
@@ -162,8 +162,8 @@ impl SshErrorBlock {
|
||||
fn should_show_report_to_warp_button(&self) -> bool {
|
||||
matches!(
|
||||
self.error_reason,
|
||||
WarpificationUnavailableReason::Timeout { .. }
|
||||
| WarpificationUnavailableReason::TmuxInstallFailed { .. }
|
||||
WormholingUnavailableReason::Timeout { .. }
|
||||
| WormholingUnavailableReason::TmuxInstallFailed { .. }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ impl SshErrorBlock {
|
||||
theme: &GalaxyTheme,
|
||||
appearance: &Appearance,
|
||||
) -> Box<dyn Element> {
|
||||
let header_contents = warpify::render::build_header_row(
|
||||
let header_contents = wormhole::render::build_header_row(
|
||||
"Error Wormholing session",
|
||||
Icon::new(UiIcon::AlertTriangle.into(), theme.ui_error_color()),
|
||||
theme,
|
||||
@@ -182,11 +182,11 @@ impl SshErrorBlock {
|
||||
.with_margin_right(8.)
|
||||
.finish();
|
||||
|
||||
let right_hand_size = warpify::render::render_never_warpify_ssh_link(
|
||||
let right_hand_size = wormhole::render::render_never_wormhole_ssh_link(
|
||||
&self.ssh_host,
|
||||
app,
|
||||
appearance,
|
||||
self.never_warpify_mouse_state_handle.clone(),
|
||||
self.never_wormhole_mouse_state_handle.clone(),
|
||||
move |ctx, ssh_host| {
|
||||
ctx.dispatch_typed_action(SshErrorBlockAction::AddSshHostToDenylist(
|
||||
ssh_host.to_owned(),
|
||||
@@ -204,7 +204,7 @@ impl SshErrorBlock {
|
||||
row.add_child(right_hand_size);
|
||||
}
|
||||
|
||||
warpify::render::apply_spacing_styles(Container::new(row.finish())).finish()
|
||||
wormhole::render::apply_spacing_styles(Container::new(row.finish())).finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ impl View for SshErrorBlock {
|
||||
|
||||
content.add_child(self.render_title_ui(app, theme, appearance));
|
||||
|
||||
content.add_child(warpify::render::description_row(
|
||||
content.add_child(wormhole::render::description_row(
|
||||
self.error_reason.error_message(),
|
||||
theme,
|
||||
appearance,
|
||||
@@ -256,7 +256,7 @@ impl View for SshErrorBlock {
|
||||
ui_builder
|
||||
.button(
|
||||
ButtonVariant::Accent,
|
||||
self.warpify_without_tmux_button_mouse_state.clone(),
|
||||
self.wormhole_without_tmux_button_mouse_state.clone(),
|
||||
)
|
||||
.with_centered_text_label("Wormhole without TMUX".into())
|
||||
.with_style(UiComponentStyles {
|
||||
@@ -266,7 +266,7 @@ impl View for SshErrorBlock {
|
||||
.build()
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(SshErrorBlockAction::WarpifyWithoutTmux)
|
||||
ctx.dispatch_typed_action(SshErrorBlockAction::WormholeWithoutTmux)
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
@@ -287,7 +287,7 @@ impl View for SshErrorBlock {
|
||||
.build()
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(SshErrorBlockAction::ContinueWithoutWarpification)
|
||||
ctx.dispatch_typed_action(SshErrorBlockAction::ContinueWithoutWormholing)
|
||||
})
|
||||
.finish(),
|
||||
);
|
||||
@@ -331,21 +331,21 @@ impl TypedActionView for SshErrorBlock {
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
SshErrorBlockAction::WarpifyWithoutTmux => {
|
||||
ctx.emit(SshErrorBlockEvent::WarpifyWithoutTmux)
|
||||
SshErrorBlockAction::WormholeWithoutTmux => {
|
||||
ctx.emit(SshErrorBlockEvent::WormholeWithoutTmux)
|
||||
}
|
||||
SshErrorBlockAction::ContinueWithoutWarpification => {
|
||||
ctx.emit(SshErrorBlockEvent::ContinueWithoutWarpification)
|
||||
SshErrorBlockAction::ContinueWithoutWormholing => {
|
||||
ctx.emit(SshErrorBlockEvent::ContinueWithoutWormholing)
|
||||
}
|
||||
SshErrorBlockAction::OpenUrl(url) => {
|
||||
ctx.open_url(url);
|
||||
}
|
||||
SshErrorBlockAction::AddSshHostToDenylist(ssh_host) => {
|
||||
let settings = WarpifySettings::handle(ctx);
|
||||
settings.update(ctx, |warpify, ctx| {
|
||||
warpify.denylist_ssh_host(ssh_host, ctx);
|
||||
let settings = WormholeSettings::handle(ctx);
|
||||
settings.update(ctx, |wormhole, ctx| {
|
||||
wormhole.denylist_ssh_host(ssh_host, ctx);
|
||||
});
|
||||
ctx.emit(SshErrorBlockEvent::ContinueWithoutWarpification);
|
||||
ctx.emit(SshErrorBlockEvent::ContinueWithoutWormholing);
|
||||
ctx.notify()
|
||||
}
|
||||
SshErrorBlockAction::Focus => {
|
||||
|
||||
@@ -6,14 +6,13 @@ use crate::ai::blocklist::inline_action::requested_script::{RequestedScriptStatu
|
||||
use crate::appearance::Appearance;
|
||||
use crate::terminal::model::ansi::SystemDetails;
|
||||
use crate::terminal::model::escape_sequences;
|
||||
use crate::terminal::warpify::render;
|
||||
use crate::terminal::warpify::settings::WarpifySettings;
|
||||
use crate::terminal::wormhole::render;
|
||||
use crate::terminal::wormhole::settings::WormholeSettings;
|
||||
use crate::ui_components::blended_colors;
|
||||
use crate::ui_components::icons::Icon as UiIcon;
|
||||
use galaxy_core::ui::theme::GalaxyTheme;
|
||||
use galaxyui::elements::{
|
||||
FormattedTextElement, HighlightedHyperlink, Hoverable, Icon, MainAxisAlignment, MainAxisSize,
|
||||
MouseStateHandle,
|
||||
Hoverable, Icon, MainAxisAlignment, MainAxisSize, MouseStateHandle, Text,
|
||||
};
|
||||
use galaxyui::keymap::FixedBinding;
|
||||
use galaxyui::ui_components::toggle_menu::ToggleMenuStateHandle;
|
||||
@@ -22,10 +21,6 @@ use galaxyui::{
|
||||
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
use galaxyui::{BlurContext, FocusContext};
|
||||
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
|
||||
|
||||
pub const WHY_INSTALL_TMUX_URL: &str =
|
||||
"https://docs.warp.dev/terminal/warpify/ssh#why-do-i-need-tmux-on-the-remote-machine";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TmuxInstallMethod {
|
||||
@@ -35,7 +30,7 @@ pub struct TmuxInstallMethod {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SshInstallTmuxBlockEvent {
|
||||
InstallTmuxAndWarpify(TmuxInstallMethod),
|
||||
InstallTmuxAndWormhole(TmuxInstallMethod),
|
||||
ToggleScriptVisibility,
|
||||
Cancel,
|
||||
Interrupt,
|
||||
@@ -88,15 +83,14 @@ impl SshKeyEvent {
|
||||
|
||||
pub struct SshInstallTmuxBlock {
|
||||
requested_script_mouse_states: RequestedScriptMouseStates,
|
||||
why_install_tmux_highlight_index: HighlightedHyperlink,
|
||||
never_warpify_mouse_state_handle: MouseStateHandle,
|
||||
never_wormhole_mouse_state_handle: MouseStateHandle,
|
||||
block_mouse_state: MouseStateHandle,
|
||||
is_focused: bool,
|
||||
is_collapsed: bool,
|
||||
show_tmux_install_block: bool,
|
||||
script_status: RequestedScriptStatus,
|
||||
system_details: SystemDetails,
|
||||
/// The script to install tmux locally, in a ~/.warp directory
|
||||
/// The script to install tmux locally, in a ~/.galaxy directory
|
||||
tmux_local_install_script: String,
|
||||
ssh_host: Option<String>,
|
||||
ssh_command: String,
|
||||
@@ -166,8 +160,7 @@ impl SshInstallTmuxBlock {
|
||||
) -> Self {
|
||||
Self {
|
||||
requested_script_mouse_states: Default::default(),
|
||||
why_install_tmux_highlight_index: Default::default(),
|
||||
never_warpify_mouse_state_handle: Default::default(),
|
||||
never_wormhole_mouse_state_handle: Default::default(),
|
||||
block_mouse_state: Default::default(),
|
||||
is_focused: false,
|
||||
is_collapsed: true,
|
||||
@@ -220,7 +213,7 @@ impl SshInstallTmuxBlock {
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.script_status = RequestedScriptStatus::Running;
|
||||
ctx.emit(SshInstallTmuxBlockEvent::InstallTmuxAndWarpify(
|
||||
ctx.emit(SshInstallTmuxBlockEvent::InstallTmuxAndWormhole(
|
||||
install_method,
|
||||
));
|
||||
ctx.notify()
|
||||
@@ -261,7 +254,7 @@ impl SshInstallTmuxBlock {
|
||||
content: tmux_system_install_script.to_string(),
|
||||
},
|
||||
TitledScript {
|
||||
title: "Install to ~/.warp".to_string(),
|
||||
title: "Install to ~/.galaxy".to_string(),
|
||||
content: self.tmux_local_install_script.clone(),
|
||||
},
|
||||
*is_first_script_active,
|
||||
@@ -320,7 +313,7 @@ impl SshInstallTmuxBlock {
|
||||
) -> Box<dyn Element> {
|
||||
let header_contents = render::build_header_row(
|
||||
"Install tmux?",
|
||||
Icon::new(UiIcon::Warp.into(), theme.active_ui_detail()),
|
||||
Icon::new(UiIcon::GalaxyLogo.into(), theme.active_ui_detail()),
|
||||
theme,
|
||||
appearance,
|
||||
)
|
||||
@@ -331,11 +324,11 @@ impl SshInstallTmuxBlock {
|
||||
|
||||
let right_hand_size = is_awaiting_action
|
||||
.then(|| {
|
||||
render::render_never_warpify_ssh_link(
|
||||
render::render_never_wormhole_ssh_link(
|
||||
&self.ssh_host,
|
||||
app,
|
||||
appearance,
|
||||
self.never_warpify_mouse_state_handle.clone(),
|
||||
self.never_wormhole_mouse_state_handle.clone(),
|
||||
move |ctx, ssh_host| {
|
||||
ctx.dispatch_typed_action(SshInstallTmuxBlockAction::AddSshHostToDenylist(
|
||||
ssh_host.to_owned(),
|
||||
@@ -382,30 +375,20 @@ impl View for SshInstallTmuxBlock {
|
||||
"In order to Wormhole your SSH session, tmux must be installed. "
|
||||
};
|
||||
|
||||
let warpify_description = vec![
|
||||
FormattedTextFragment::plain_text(explanation),
|
||||
FormattedTextFragment::hyperlink("Why do I need tmux?", WHY_INSTALL_TMUX_URL),
|
||||
];
|
||||
|
||||
let text_color =
|
||||
blended_colors::text_sub(appearance.theme(), appearance.theme().surface_1());
|
||||
|
||||
let warpify_description = FormattedTextElement::new(
|
||||
FormattedText::new([FormattedTextLine::Line(warpify_description)]),
|
||||
let wormhole_description = Text::new(
|
||||
explanation.to_string(),
|
||||
appearance.monospace_font_family(),
|
||||
appearance.monospace_font_size(),
|
||||
appearance.monospace_font_family(),
|
||||
appearance.monospace_font_family(),
|
||||
text_color,
|
||||
self.why_install_tmux_highlight_index.clone(),
|
||||
)
|
||||
.with_hyperlink_font_color(appearance.theme().accent().into_solid())
|
||||
.register_default_click_handlers(|url, _, ctx| {
|
||||
ctx.open_url(&url.url);
|
||||
})
|
||||
.soft_wrap(true)
|
||||
.with_color(text_color)
|
||||
.finish();
|
||||
|
||||
content
|
||||
.add_child(render::apply_spacing_styles(Container::new(warpify_description)).finish());
|
||||
.add_child(render::apply_spacing_styles(Container::new(wormhole_description)).finish());
|
||||
|
||||
if let Some(root_install_state) = &self.system_install_state {
|
||||
content.add_child(self.render_system_install_ui(root_install_state, app));
|
||||
@@ -490,9 +473,9 @@ impl TypedActionView for SshInstallTmuxBlock {
|
||||
ctx.emit(SshInstallTmuxBlockEvent::Interrupt);
|
||||
}
|
||||
(SshInstallTmuxBlockAction::AddSshHostToDenylist(ssh_host), true) => {
|
||||
let settings = WarpifySettings::handle(ctx);
|
||||
settings.update(ctx, |warpify, ctx| {
|
||||
warpify.denylist_ssh_host(ssh_host, ctx);
|
||||
let settings = WormholeSettings::handle(ctx);
|
||||
settings.update(ctx, |wormhole, ctx| {
|
||||
wormhole.denylist_ssh_host(ssh_host, ctx);
|
||||
});
|
||||
ctx.emit(SshInstallTmuxBlockEvent::Cancel);
|
||||
ctx.notify();
|
||||
@@ -519,16 +502,16 @@ pub fn install_tmux_script(system: &SystemDetails, app: &AppContext) -> Option<S
|
||||
system.shell.as_str(),
|
||||
) {
|
||||
("Linux", _, "bash" | "zsh") => {
|
||||
bundled_asset!("ssh/bash_zsh/install_tmux_and_warpify_linux.sh")
|
||||
bundled_asset!("ssh/bash_zsh/install_tmux_and_wormhole_linux.sh")
|
||||
}
|
||||
("Linux", _, "fish") => {
|
||||
bundled_asset!("ssh/fish/install_tmux_and_warpify_linux.sh")
|
||||
bundled_asset!("ssh/fish/install_tmux_and_wormhole_linux.sh")
|
||||
}
|
||||
("Darwin", "homebrew", "bash" | "zsh") => {
|
||||
bundled_asset!("ssh/bash_zsh/install_tmux_and_warpify_brew.sh")
|
||||
bundled_asset!("ssh/bash_zsh/install_tmux_and_wormhole_brew.sh")
|
||||
}
|
||||
("Darwin", "homebrew", "fish") => {
|
||||
bundled_asset!("ssh/fish/install_tmux_and_warpify_brew.sh")
|
||||
bundled_asset!("ssh/fish/install_tmux_and_wormhole_brew.sh")
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
@@ -555,19 +538,19 @@ pub fn install_root_tmux_script(
|
||||
system.shell.as_str(),
|
||||
) {
|
||||
("Linux", "apt", "bash" | "zsh") => {
|
||||
bundled_asset!("ssh/bash_zsh/root/install_tmux_and_warpify_apt.sh")
|
||||
bundled_asset!("ssh/bash_zsh/root/install_tmux_and_wormhole_apt.sh")
|
||||
}
|
||||
("Linux", "dnf", "bash" | "zsh") => {
|
||||
bundled_asset!("ssh/bash_zsh/root/install_tmux_and_warpify_dnf.sh")
|
||||
bundled_asset!("ssh/bash_zsh/root/install_tmux_and_wormhole_dnf.sh")
|
||||
}
|
||||
("Linux", "pacman", "bash" | "zsh") => {
|
||||
bundled_asset!("ssh/bash_zsh/root/install_tmux_and_warpify_pacman.sh")
|
||||
bundled_asset!("ssh/bash_zsh/root/install_tmux_and_wormhole_pacman.sh")
|
||||
}
|
||||
("Linux", "yum", "bash" | "zsh") => {
|
||||
bundled_asset!("ssh/bash_zsh/root/install_tmux_and_warpify_yum.sh")
|
||||
bundled_asset!("ssh/bash_zsh/root/install_tmux_and_wormhole_yum.sh")
|
||||
}
|
||||
("Linux", "zypper", "bash" | "zsh") => {
|
||||
bundled_asset!("ssh/bash_zsh/root/install_tmux_and_warpify_zypper.sh")
|
||||
bundled_asset!("ssh/bash_zsh/root/install_tmux_and_wormhole_zypper.sh")
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ use galaxy_core::{features::FeatureFlag, settings::Setting};
|
||||
use galaxy_util::path::ShellFamily;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::terminal::warpify::settings::WarpifySettings;
|
||||
use crate::terminal::wormhole::settings::WormholeSettings;
|
||||
|
||||
/// The different possible outcomes of detecting an interactive SSH session.
|
||||
/// Also the payload for the [`crate::server::telemetry::TelemetryEvent::SshInteractiveSessionDetected`] event.
|
||||
@@ -12,8 +12,8 @@ pub enum SshInteractiveSessionDetected {
|
||||
FeatureDisabled,
|
||||
#[serde(rename = "host_denylisted")]
|
||||
HostDenylisted,
|
||||
#[serde(rename = "warpify_prompt")]
|
||||
ShouldPromptWarpification {
|
||||
#[serde(rename = "wormhole_prompt")]
|
||||
ShouldPromptWormholing {
|
||||
#[serde(skip)]
|
||||
command: String,
|
||||
#[serde(skip)]
|
||||
@@ -21,17 +21,17 @@ pub enum SshInteractiveSessionDetected {
|
||||
},
|
||||
}
|
||||
|
||||
/// Determines whether a host could be warpified.
|
||||
pub fn evaluate_warpify_ssh_host(
|
||||
/// Determines whether a host could be wormholed.
|
||||
pub fn evaluate_wormhole_ssh_host(
|
||||
command: &str,
|
||||
ssh_host: Option<&str>,
|
||||
shell_family: ShellFamily,
|
||||
warpify_settings: &WarpifySettings,
|
||||
wormhole_settings: &WormholeSettings,
|
||||
) -> SshInteractiveSessionDetected {
|
||||
let should_prompt_ssh_tmux_wrapper = *warpify_settings.enable_ssh_warpification.value()
|
||||
&& *warpify_settings.use_ssh_tmux_wrapper.value();
|
||||
let matches_subshell = warpify_settings.is_denylisted_subshell_command(command)
|
||||
|| warpify_settings.is_compatible_subshell_command(command, shell_family);
|
||||
let should_prompt_ssh_tmux_wrapper = *wormhole_settings.enable_ssh_wormholing.value()
|
||||
&& *wormhole_settings.use_ssh_tmux_wrapper.value();
|
||||
let matches_subshell = wormhole_settings.is_denylisted_subshell_command(command)
|
||||
|| wormhole_settings.is_compatible_subshell_command(command, shell_family);
|
||||
if !should_prompt_ssh_tmux_wrapper
|
||||
|| matches_subshell
|
||||
|| !FeatureFlag::SSHTmuxWrapper.is_enabled()
|
||||
@@ -40,12 +40,12 @@ pub fn evaluate_warpify_ssh_host(
|
||||
}
|
||||
|
||||
if let Some(ssh_host) = ssh_host {
|
||||
if warpify_settings.is_ssh_host_denylisted(ssh_host) {
|
||||
if wormhole_settings.is_ssh_host_denylisted(ssh_host) {
|
||||
return SshInteractiveSessionDetected::HostDenylisted;
|
||||
}
|
||||
}
|
||||
|
||||
SshInteractiveSessionDetected::ShouldPromptWarpification {
|
||||
SshInteractiveSessionDetected::ShouldPromptWormholing {
|
||||
host: ssh_host.map(|host| host.to_owned()),
|
||||
command: command.to_string(),
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ pub fn check_ssh_login_state(block_output: &str) -> SshLoginState {
|
||||
}
|
||||
|
||||
/// Represents the parsed components of an interactive SSH command.
|
||||
/// For some [`SshWarpifyCommand`]s, we do not support parsing
|
||||
/// For some [`SshWormholeCommand`]s, we do not support parsing
|
||||
/// a host or port In these cases, we can still parse to a valid
|
||||
/// empty `InteractiveSshCommand` to indicate that we did
|
||||
/// successfully detect an interactive SSH command.
|
||||
@@ -150,25 +150,25 @@ pub enum SshLikeCommand {
|
||||
/// Represents the different kinds of commands we recognize as starting an interactive SSH
|
||||
/// session. `Ssh` means a literal `ssh` command, where all other commands (e.g. `gcloud
|
||||
/// compute ssh`) are categorized as SSH-like commands.
|
||||
pub enum SshWarpifyCommand {
|
||||
pub enum SshWormholeCommand {
|
||||
Ssh,
|
||||
SshLike(SshLikeCommand),
|
||||
}
|
||||
|
||||
impl SshWarpifyCommand {
|
||||
impl SshWormholeCommand {
|
||||
/// Not a literal `ssh` command, but another command that starts an interactive SSH
|
||||
/// session.
|
||||
pub fn is_ssh_like_command(&self) -> bool {
|
||||
matches!(self, SshWarpifyCommand::SshLike(_))
|
||||
matches!(self, SshWormholeCommand::SshLike(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl SshWarpifyCommand {
|
||||
pub fn matches(command: &str) -> Option<SshWarpifyCommand> {
|
||||
impl SshWormholeCommand {
|
||||
pub fn matches(command: &str) -> Option<SshWormholeCommand> {
|
||||
let tokens = normalized_command_tokens(command)?;
|
||||
match tokens.as_slice() {
|
||||
[command, arguments @ ..] if command == "ssh" && !arguments.is_empty() => {
|
||||
Some(SshWarpifyCommand::Ssh)
|
||||
Some(SshWormholeCommand::Ssh)
|
||||
}
|
||||
[command, compute, ssh, arguments @ ..]
|
||||
if command == "gcloud"
|
||||
@@ -176,12 +176,14 @@ impl SshWarpifyCommand {
|
||||
&& ssh == "ssh"
|
||||
&& !arguments.is_empty() =>
|
||||
{
|
||||
Some(SshWarpifyCommand::SshLike(SshLikeCommand::Gcloud))
|
||||
Some(SshWormholeCommand::SshLike(SshLikeCommand::Gcloud))
|
||||
}
|
||||
[command, ssh, arguments @ ..]
|
||||
if command == "eb" && ssh == "ssh" && !arguments.is_empty() =>
|
||||
{
|
||||
Some(SshWarpifyCommand::SshLike(SshLikeCommand::ElasticBeanstalk))
|
||||
Some(SshWormholeCommand::SshLike(
|
||||
SshLikeCommand::ElasticBeanstalk,
|
||||
))
|
||||
}
|
||||
[command, compute, ssh, arguments @ ..]
|
||||
if command == "doctl"
|
||||
@@ -189,7 +191,7 @@ impl SshWarpifyCommand {
|
||||
&& ssh == "ssh"
|
||||
&& !arguments.is_empty() =>
|
||||
{
|
||||
Some(SshWarpifyCommand::SshLike(
|
||||
Some(SshWormholeCommand::SshLike(
|
||||
SshLikeCommand::DigitalOceanDroplet,
|
||||
))
|
||||
}
|
||||
@@ -199,15 +201,15 @@ impl SshWarpifyCommand {
|
||||
}
|
||||
|
||||
pub fn parse_interactive_ssh_command(command: &str) -> Option<InteractiveSshCommand> {
|
||||
match SshWarpifyCommand::matches(command) {
|
||||
Some(SshWarpifyCommand::Ssh) => InteractiveSshCommand::parse_ssh_command(command),
|
||||
Some(SshWarpifyCommand::SshLike(SshLikeCommand::Gcloud)) => {
|
||||
match SshWormholeCommand::matches(command) {
|
||||
Some(SshWormholeCommand::Ssh) => InteractiveSshCommand::parse_ssh_command(command),
|
||||
Some(SshWormholeCommand::SshLike(SshLikeCommand::Gcloud)) => {
|
||||
Some(InteractiveSshCommand::default())
|
||||
}
|
||||
Some(SshWarpifyCommand::SshLike(SshLikeCommand::ElasticBeanstalk)) => {
|
||||
Some(SshWormholeCommand::SshLike(SshLikeCommand::ElasticBeanstalk)) => {
|
||||
Some(InteractiveSshCommand::default())
|
||||
}
|
||||
Some(SshWarpifyCommand::SshLike(SshLikeCommand::DigitalOceanDroplet)) => {
|
||||
Some(SshWormholeCommand::SshLike(SshLikeCommand::DigitalOceanDroplet)) => {
|
||||
Some(InteractiveSshCommand::default())
|
||||
}
|
||||
None => None,
|
||||
@@ -299,7 +301,7 @@ fn executable_name(executable: &str) -> String {
|
||||
.to_ascii_lowercase()
|
||||
}
|
||||
|
||||
/// Creates an sftp command that copies a given local file into the pwd in the warpified ssh session.
|
||||
/// Creates an sftp command that copies a given local file into the pwd in the wormholed ssh session.
|
||||
pub fn transfer_file_sftp_command(
|
||||
local_file_path: String,
|
||||
ssh_host: String,
|
||||
|
||||
@@ -6,8 +6,7 @@ use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
|
||||
use crate::ai::blocklist::inline_action::requested_action::RenderableAction;
|
||||
use crate::appearance::Appearance;
|
||||
use crate::terminal::shell::ShellType;
|
||||
use crate::terminal::warpify;
|
||||
use crate::terminal::warpify::render::SSH_DOCS_URL;
|
||||
use crate::terminal::wormhole;
|
||||
use crate::ui_components::icons::Icon as UiIcon;
|
||||
use galaxyui::elements::{HighlightedHyperlink, Hoverable, Icon, MouseStateHandle};
|
||||
use galaxyui::keymap::FixedBinding;
|
||||
@@ -18,19 +17,19 @@ use galaxyui::{
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SshWarpifyBlockEvent {
|
||||
WarpifySession,
|
||||
pub enum SshWormholeBlockEvent {
|
||||
WormholeSession,
|
||||
Cancel,
|
||||
Interrupt,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum SshWarpifyBlockAction {
|
||||
pub enum SshWormholeBlockAction {
|
||||
Interrupt,
|
||||
Focus,
|
||||
}
|
||||
|
||||
pub struct SshWarpifyBlock {
|
||||
pub struct SshWormholeBlock {
|
||||
block_mouse_state: MouseStateHandle,
|
||||
ssh_command: String,
|
||||
}
|
||||
@@ -40,12 +39,12 @@ pub fn init(app: &mut AppContext) {
|
||||
|
||||
app.register_fixed_bindings([FixedBinding::new(
|
||||
"ctrl-c",
|
||||
SshWarpifyBlockAction::Interrupt,
|
||||
id!(SshWarpifyBlock::ui_name()),
|
||||
SshWormholeBlockAction::Interrupt,
|
||||
id!(SshWormholeBlock::ui_name()),
|
||||
)]);
|
||||
}
|
||||
|
||||
impl SshWarpifyBlock {
|
||||
impl SshWormholeBlock {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new(ssh_command: String) -> Self {
|
||||
Self {
|
||||
@@ -60,18 +59,18 @@ impl SshWarpifyBlock {
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for SshWarpifyBlock {
|
||||
type Event = SshWarpifyBlockEvent;
|
||||
impl Entity for SshWormholeBlock {
|
||||
type Event = SshWormholeBlockEvent;
|
||||
}
|
||||
|
||||
impl SshWarpifyBlock {
|
||||
impl SshWormholeBlock {
|
||||
fn render_title_ui(&self, theme: &GalaxyTheme, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let icon = Icon::new(UiIcon::Warp.into(), theme.active_ui_detail());
|
||||
warpify::render::header_row("Wormholing SSH Session...", icon, theme, appearance)
|
||||
let icon = Icon::new(UiIcon::GalaxyLogo.into(), theme.active_ui_detail());
|
||||
wormhole::render::header_row("Wormholing SSH Session...", icon, theme, appearance)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn warpify_description(
|
||||
pub fn wormhole_description(
|
||||
app: &AppContext,
|
||||
hyperlink_index: &HighlightedHyperlink,
|
||||
) -> Box<dyn Element> {
|
||||
@@ -80,21 +79,16 @@ pub fn warpify_description(
|
||||
|
||||
let description = FormattedText::new(vec![FormattedTextLine::Line(vec![
|
||||
FormattedTextFragment::plain_text(
|
||||
"Bring Galaxy's features to your remote session. Blocks, full text editing, auto-complete, Oz, and more. "
|
||||
"Bring Galaxy's features to your remote session: blocks, full text editing, completions, Oz, and more."
|
||||
),
|
||||
FormattedTextFragment::hyperlink("Learn more", SSH_DOCS_URL),
|
||||
])]);
|
||||
warpify::render::build_description_row(description, theme, appearance, hyperlink_index.clone())
|
||||
.with_hyperlink_font_color(appearance.theme().accent().into_solid())
|
||||
.register_default_click_handlers(|url, _, ctx| {
|
||||
ctx.open_url(&url.url);
|
||||
})
|
||||
wormhole::render::build_description_row(description, theme, appearance, hyperlink_index.clone())
|
||||
.finish()
|
||||
}
|
||||
|
||||
impl View for SshWarpifyBlock {
|
||||
impl View for SshWormholeBlock {
|
||||
fn ui_name() -> &'static str {
|
||||
"SshWarpifyBlock"
|
||||
"SshWormholeBlock"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
@@ -124,39 +118,39 @@ impl View for SshWarpifyBlock {
|
||||
.finish()
|
||||
})
|
||||
.on_click(|ctx, _, _| {
|
||||
ctx.dispatch_typed_action(SshWarpifyBlockAction::Focus);
|
||||
ctx.dispatch_typed_action(SshWormholeBlockAction::Focus);
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for SshWarpifyBlock {
|
||||
type Action = SshWarpifyBlockAction;
|
||||
impl TypedActionView for SshWormholeBlock {
|
||||
type Action = SshWormholeBlockAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
SshWarpifyBlockAction::Interrupt => {
|
||||
ctx.emit(SshWarpifyBlockEvent::Interrupt);
|
||||
SshWormholeBlockAction::Interrupt => {
|
||||
ctx.emit(SshWormholeBlockEvent::Interrupt);
|
||||
}
|
||||
SshWarpifyBlockAction::Focus => {
|
||||
SshWormholeBlockAction::Focus => {
|
||||
self.focus(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the begin_warpify_ssh_session script into a string.
|
||||
pub fn begin_warpify_ssh_session_command(app: &AppContext) -> String {
|
||||
/// Convert the begin_wormhole_ssh_session script into a string.
|
||||
pub fn begin_wormhole_ssh_session_command(app: &AppContext) -> String {
|
||||
let asset = bundled_asset!("bootstrap/unknown_init_subshell.sh");
|
||||
|
||||
match AssetCache::as_ref(app).load_asset::<String>(asset) {
|
||||
AssetState::Loaded { data } => data.to_string().replace("HOOK_NAME", "InitSsh"),
|
||||
_ => panic!("ssh begin warpify script should be available as a string"),
|
||||
_ => panic!("ssh begin wormhole script should be available as a string"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the warpify_ssh_session script into a string.
|
||||
pub fn warpify_ssh_session_command(
|
||||
/// Convert the wormhole_ssh_session script into a string.
|
||||
pub fn wormhole_ssh_session_command(
|
||||
uname: &str,
|
||||
shell_type: ShellType,
|
||||
app: &AppContext,
|
||||
@@ -164,14 +158,14 @@ pub fn warpify_ssh_session_command(
|
||||
let asset = match (uname, shell_type) {
|
||||
// Mac scripts must be less than 1020 characters due to macOS 15+ pty issue
|
||||
("Darwin", ShellType::Zsh | ShellType::Bash) => {
|
||||
bundled_asset!("ssh/bash_zsh/warpify_ssh_session_mac.sh")
|
||||
bundled_asset!("ssh/bash_zsh/wormhole_ssh_session_mac.sh")
|
||||
}
|
||||
// Mac scripts must be less than 1020 characters due to macOS 15+ pty issue
|
||||
("Darwin", ShellType::Fish) => bundled_asset!("ssh/fish/warpify_ssh_session_mac.sh"),
|
||||
("Darwin", ShellType::Fish) => bundled_asset!("ssh/fish/wormhole_ssh_session_mac.sh"),
|
||||
(_, ShellType::Zsh | ShellType::Bash) => {
|
||||
bundled_asset!("ssh/bash_zsh/warpify_ssh_session.sh")
|
||||
bundled_asset!("ssh/bash_zsh/wormhole_ssh_session.sh")
|
||||
}
|
||||
(_, ShellType::Fish) => bundled_asset!("ssh/fish/warpify_ssh_session.sh"),
|
||||
(_, ShellType::Fish) => bundled_asset!("ssh/fish/wormhole_ssh_session.sh"),
|
||||
// PowerShell is not supported yet.
|
||||
(_, ShellType::PowerShell) => return None,
|
||||
};
|
||||
@@ -179,9 +173,9 @@ pub fn warpify_ssh_session_command(
|
||||
// Todo(Jack): look into avoiding an allocation here.
|
||||
match AssetCache::as_ref(app).load_asset::<String>(asset) {
|
||||
AssetState::Loaded { data } => Some(data.to_string()),
|
||||
_ => panic!("ssh warpify script should be available as a string"),
|
||||
_ => panic!("ssh wormhole script should be available as a string"),
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
#[path = "warpify_test.rs"]
|
||||
#[path = "wormhole_test.rs"]
|
||||
mod tests;
|
||||
@@ -37,50 +37,50 @@ fn get_script(asset_source: AssetSource, ctx: &AppContext) -> String {
|
||||
#[cfg_attr(windows, ignore = "TODO(CORE-3626)")]
|
||||
#[test]
|
||||
/// See [assert_script_is_short_enough_mac] for more information.
|
||||
fn test_mac_warpification_script_size() {
|
||||
fn test_mac_wormholing_script_size() {
|
||||
App::test(Assets, |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
app.read(|ctx| {
|
||||
assert_script_is_short_enough_mac(
|
||||
&begin_warpify_ssh_session_command(ctx),
|
||||
&begin_wormhole_ssh_session_command(ctx),
|
||||
"unknown_init_subshell.sh",
|
||||
false,
|
||||
);
|
||||
|
||||
assert_script_is_short_enough_mac(
|
||||
&get_script(
|
||||
bundled_asset!("ssh/bash_zsh/install_tmux_and_warpify_brew.sh"),
|
||||
bundled_asset!("ssh/bash_zsh/install_tmux_and_wormhole_brew.sh"),
|
||||
ctx,
|
||||
),
|
||||
"install_tmux_and_warpify_brew.sh",
|
||||
"install_tmux_and_wormhole_brew.sh",
|
||||
false,
|
||||
);
|
||||
assert_script_is_short_enough_mac(
|
||||
&get_script(
|
||||
bundled_asset!("ssh/fish/install_tmux_and_warpify_brew.sh"),
|
||||
bundled_asset!("ssh/fish/install_tmux_and_wormhole_brew.sh"),
|
||||
ctx,
|
||||
),
|
||||
"fish/install_tmux_and_warpify_brew.sh",
|
||||
"fish/install_tmux_and_wormhole_brew.sh",
|
||||
false,
|
||||
);
|
||||
|
||||
assert_script_is_short_enough_mac(
|
||||
&warpify_ssh_session_command("Darwin", ShellType::Zsh, ctx)
|
||||
&wormhole_ssh_session_command("Darwin", ShellType::Zsh, ctx)
|
||||
.expect("Should get Darwin zsh script"),
|
||||
"zsh warpify",
|
||||
"zsh wormhole",
|
||||
true,
|
||||
);
|
||||
assert_script_is_short_enough_mac(
|
||||
&warpify_ssh_session_command("Darwin", ShellType::Bash, ctx)
|
||||
&wormhole_ssh_session_command("Darwin", ShellType::Bash, ctx)
|
||||
.expect("Should get Darwin bash script"),
|
||||
"bash warpify",
|
||||
"bash wormhole",
|
||||
true,
|
||||
);
|
||||
assert_script_is_short_enough_mac(
|
||||
&warpify_ssh_session_command("Darwin", ShellType::Fish, ctx)
|
||||
&wormhole_ssh_session_command("Darwin", ShellType::Fish, ctx)
|
||||
.expect("Should get Darwin fish script"),
|
||||
"fish warpify",
|
||||
"fish wormhole",
|
||||
true,
|
||||
)
|
||||
});
|
||||
@@ -126,23 +126,23 @@ impl AtContextMenuDisabledReason {
|
||||
let session_type = session.session_type();
|
||||
let has_connected_remote_server = matches!(
|
||||
session_type,
|
||||
SessionType::WarpifiedRemote { host_id: Some(_) }
|
||||
SessionType::WormholedRemote { host_id: Some(_) }
|
||||
);
|
||||
// The @ menu requires repo metadata which is only available for:
|
||||
// - Local sessions
|
||||
// - WarpifiedRemote sessions with a connected remote server (host_id is Some)
|
||||
// - WormholedRemote sessions with a connected remote server (host_id is Some)
|
||||
//
|
||||
// Block when:
|
||||
// - SSH wrapper session without a remote server upgrade
|
||||
// - WarpifiedRemote still connecting (host_id is None)
|
||||
// - WormholedRemote still connecting (host_id is None)
|
||||
//
|
||||
// Note: is_ssh_wrapper_session() is set at bootstrap time and stays true
|
||||
// even after the session transitions to WarpifiedRemote with a host_id.
|
||||
// even after the session transitions to WormholedRemote with a host_id.
|
||||
// So we must check has_connected_remote_server first to avoid
|
||||
// incorrectly blocking upgraded sessions.
|
||||
let is_ssh_without_remote_server = !has_connected_remote_server
|
||||
&& (session.is_ssh_wrapper_session()
|
||||
|| matches!(session_type, SessionType::WarpifiedRemote { host_id: None }));
|
||||
|| matches!(session_type, SessionType::WormholedRemote { host_id: None }));
|
||||
let is_subshell = session.subshell_info().is_some();
|
||||
(is_ssh_without_remote_server, is_subshell)
|
||||
})
|
||||
|
||||
+144
-158
@@ -67,13 +67,13 @@ use std::sync::Arc;
|
||||
use std::thread::JoinHandle;
|
||||
use std::time::Duration;
|
||||
|
||||
use action::RememberForWarpification;
|
||||
use action::RememberForWormholing;
|
||||
pub use action::{AgentOnboardingVersion, OnboardingIntention, OnboardingVersion, TerminalAction};
|
||||
use ai::api_keys::{ApiKeyManager, AwsCredentialsState};
|
||||
use ai::index::full_source_code_embedding::manager::{BuildSource, CodebaseIndexManager};
|
||||
use async_channel::{Receiver, Sender};
|
||||
use base64::Engine as _;
|
||||
use block_banner::{render_warpification_banner, WarpifyBannerState};
|
||||
use block_banner::{render_wormholing_banner, WormholeBannerState};
|
||||
pub use block_banner::{WithinBlockBanner, BLOCK_BANNER_HEIGHT};
|
||||
use block_onboarding::onboarding_drive_sharing_block::OnboardingDriveSharingBlock;
|
||||
use bookmarks::render_floating_block_snapshot;
|
||||
@@ -192,10 +192,9 @@ use super::model::secrets::RichContentSecretTooltipInfo;
|
||||
use super::model::selection::ExpandedSelectionRange;
|
||||
use super::model::session::SessionBootstrappedEvent;
|
||||
use super::settings::AltScreenPaddingMode;
|
||||
use super::ssh::util::{parse_interactive_ssh_command, InteractiveSshCommand, SshWarpifyCommand};
|
||||
use super::warpify::success_block::{WarpifySuccessBlock, WarpifySuccessBlockEvent};
|
||||
use super::warpify::trigger_state::{SshBlockState, WarpifyState};
|
||||
use super::warpify::WarpificationSource;
|
||||
use super::ssh::util::{parse_interactive_ssh_command, InteractiveSshCommand, SshWormholeCommand};
|
||||
use super::wormhole::success_block::{WormholeSuccessBlock, WormholeSuccessBlockEvent};
|
||||
use super::wormhole::trigger_state::{SshBlockState, WormholeState};
|
||||
use super::{cli_agent, CLIAgent, GridType};
|
||||
use crate::ai::agent::api::ServerConversationToken;
|
||||
use crate::ai::agent::conversation::{AIConversation, AIConversationId, ConversationStatus};
|
||||
@@ -484,10 +483,10 @@ use crate::terminal::view::ssh_tmux_deprecation_banner::{
|
||||
};
|
||||
use crate::terminal::view::telemetry::PromptSuggestionFallbackReason;
|
||||
use crate::terminal::view::zero_state_block::TerminalViewZeroStateBlock;
|
||||
use crate::terminal::warpify::render::render_subshell_separator;
|
||||
use crate::terminal::warpify::settings::WarpifySettings;
|
||||
use crate::terminal::warpify::SubshellSource;
|
||||
use crate::terminal::waterfall_gap_element::WaterfallGapElement;
|
||||
use crate::terminal::wormhole::render::render_subshell_separator;
|
||||
use crate::terminal::wormhole::settings::WormholeSettings;
|
||||
use crate::terminal::wormhole::SubshellSource;
|
||||
use crate::terminal::writeable_pty::{PtyIntent, PtyIntentEvent, TerminalSurface};
|
||||
use crate::terminal::{
|
||||
block_list_element::BlockHoverAction,
|
||||
@@ -634,10 +633,6 @@ const KNOWN_ISSUES_URL: &str =
|
||||
const PROMPT_COMPATIBILITY_URL: &str =
|
||||
"https://docs.warp.dev/terminal/appearance/prompt#custom-prompt-compatibility-table";
|
||||
|
||||
/// Link to troubleshooting steps for ControlMaster errors.
|
||||
const CONTROLMASTER_ISSUES_URL: &str =
|
||||
"https://docs.warp.dev/terminal/warpify/ssh-legacy#troubleshooting";
|
||||
|
||||
/// Link to instructions on how to update p10k.
|
||||
const P10K_UPDATE_INSTRUCTIONS_URL: &str =
|
||||
"https://github.com/romkatv/powerlevel10k#how-do-i-update-powerlevel10k";
|
||||
@@ -676,10 +671,10 @@ enum Osc52ClipboardBlockedType {
|
||||
/// Key used in user defaults to save whether the user has seen the banner.
|
||||
pub const ALIAS_EXPANSION_BANNER_SEEN_KEY: &str = "AliasExpansionBannerSeen";
|
||||
|
||||
/// Delay between receiving preexec hook for a command we want to auto-warpify
|
||||
/// and triggering the warpification (subshell bootstrapping).
|
||||
/// Delay between receiving preexec hook for a command we want to auto-wormhole
|
||||
/// and triggering the wormholing (subshell bootstrapping).
|
||||
/// Reached this number after experimenting with different values to find a reliable delay.
|
||||
const AUTO_WARPIFY_DELAY: u64 = 1000;
|
||||
const AUTO_WORMHOLE_DELAY: u64 = 1000;
|
||||
|
||||
/// Binding names to be customized if the user indicates they prefer
|
||||
/// Emacs-style keybindings instead of IDE-style keybindings.
|
||||
@@ -2755,7 +2750,7 @@ pub struct TerminalView {
|
||||
|
||||
onboarding_callout_view: Option<ViewHandle<onboarding::OnboardingCalloutView>>,
|
||||
|
||||
/// The type of the subshell that we will bootstrap/"warpify"" on the next [`AfterBlockStarted`]
|
||||
/// The type of the subshell that we will bootstrap/"wormhole"" on the next [`AfterBlockStarted`]
|
||||
/// terminal model event. Will only be `Some` with a [`ShellType`] we can bootstrap.
|
||||
pending_auto_bootstrap_shell_type: Option<ShellType>,
|
||||
env_vars: Vec<EnvVar>,
|
||||
@@ -2817,7 +2812,7 @@ pub struct TerminalView {
|
||||
|
||||
find_model: ModelHandle<TerminalFindModel>,
|
||||
|
||||
warpify_state: WarpifyState,
|
||||
wormhole_state: WormholeState,
|
||||
|
||||
/// The keystroke bound to canceling a command.
|
||||
///
|
||||
@@ -3947,12 +3942,12 @@ impl TerminalView {
|
||||
|
||||
let control_master_error_banner = ctx.add_typed_action_view(|_| {
|
||||
Banner::new_permanently_dismissible(BannerTextContent::formatted_text(vec![
|
||||
FormattedTextFragment::plain_text("Seems like your completions are not working ("),
|
||||
FormattedTextFragment::hyperlink("more info", CONTROLMASTER_ISSUES_URL),
|
||||
FormattedTextFragment::plain_text("). Enabling the SSH extension in "),
|
||||
FormattedTextFragment::plain_text(
|
||||
"Your completions may not be working. Enabling the Wormhole helper in ",
|
||||
),
|
||||
FormattedTextFragment::hyperlink_action(
|
||||
"settings",
|
||||
TerminalAction::ShowWarpifySettings,
|
||||
TerminalAction::ShowWormholeSettings,
|
||||
),
|
||||
FormattedTextFragment::plain_text(" may resolve this issue."),
|
||||
]))
|
||||
@@ -4436,7 +4431,7 @@ impl TerminalView {
|
||||
input_position_id,
|
||||
input_hoverable_handle: Default::default(),
|
||||
find_model,
|
||||
warpify_state: Default::default(),
|
||||
wormhole_state: Default::default(),
|
||||
cancel_command_keystroke: keybinding_name_to_keystroke(CANCEL_COMMAND_KEYBINDING, ctx),
|
||||
is_file_drop_target: false,
|
||||
is_ssh_file_uploader: false,
|
||||
@@ -4582,7 +4577,7 @@ impl TerminalView {
|
||||
me.show_ssh_remote_server_failed_banner(
|
||||
*session_id,
|
||||
remote_server::transport::UserFacingError {
|
||||
body: "Failed to start SSH extension".into(),
|
||||
body: "Failed to start Wormhole helper".into(),
|
||||
detail: if error.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -9116,7 +9111,7 @@ impl TerminalView {
|
||||
/// events, allow it to handle the event.
|
||||
///
|
||||
/// TODO(CORE-3415): We should probably remove the FixedBindings for ctrl-c
|
||||
/// in the SSH warpification blocks and handle them here as well.
|
||||
/// in the SSH wormholing blocks and handle them here as well.
|
||||
fn maybe_handle_ctrl_c_in_rich_content_block(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
if self.active_ai_block(ctx).is_some() {
|
||||
self.cancel_active_conversation_via_status_bar(ctx);
|
||||
@@ -9192,7 +9187,7 @@ impl TerminalView {
|
||||
/// the workspace to derive `PendingRemoteSession` without storing
|
||||
/// mutable state on the workspace itself.
|
||||
pub fn has_pending_ssh_command(&self) -> bool {
|
||||
self.warpify_state.get_pending_ssh_host().is_some() && self.is_long_running()
|
||||
self.wormhole_state.get_pending_ssh_host().is_some() && self.is_long_running()
|
||||
}
|
||||
|
||||
/// Like `is_long_running`, but also requires the user to be in control of the command
|
||||
@@ -9779,7 +9774,7 @@ impl TerminalView {
|
||||
.is_some_and(|session| {
|
||||
matches!(
|
||||
session.session_type(),
|
||||
SessionType::WarpifiedRemote {
|
||||
SessionType::WormholedRemote {
|
||||
host_id: Some(_),
|
||||
..
|
||||
}
|
||||
@@ -9839,7 +9834,7 @@ impl TerminalView {
|
||||
triggered_by_rc_file_snippet: bool,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.dismiss_warpify_banner(&RememberForWarpification::DoNotRememberSubshellCommand, ctx);
|
||||
self.dismiss_wormhole_banner(&RememberForWormholing::DoNotRememberSubshellCommand, ctx);
|
||||
|
||||
// Record the active long-running block so we can hide it later once the remote
|
||||
// actually confirms subshell bootstrap is in progress.
|
||||
@@ -9852,7 +9847,7 @@ impl TerminalView {
|
||||
.is_active_and_long_running()
|
||||
{
|
||||
let block_id = model.block_list().active_block_id().clone();
|
||||
self.warpify_state.set_block_id(block_id);
|
||||
self.wormhole_state.set_block_id(block_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9875,7 +9870,7 @@ impl TerminalView {
|
||||
|
||||
/// Util method to update the ssh block, with a lock
|
||||
fn update_long_running_ssh_block_with_lock(&self, f: impl FnOnce(&mut Block)) -> bool {
|
||||
if let Some(block_id) = self.warpify_state.block_id() {
|
||||
if let Some(block_id) = self.wormhole_state.block_id() {
|
||||
if let Some(block) = self
|
||||
.model
|
||||
.lock()
|
||||
@@ -9897,15 +9892,15 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
fn clear_ssh_blocks(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.dismiss_warpify_banner(&RememberForWarpification::DoNotRememberSSHHost, ctx);
|
||||
if let Some(ssh_block) = self.warpify_state.ssh_block_state() {
|
||||
self.dismiss_wormhole_banner(&RememberForWormholing::DoNotRememberSSHHost, ctx);
|
||||
if let Some(ssh_block) = self.wormhole_state.ssh_block_state() {
|
||||
let view_id = ssh_block.get_block_view_id();
|
||||
|
||||
self.remove_ssh_block_by_id(view_id);
|
||||
|
||||
self.redetermine_global_focus(ctx);
|
||||
|
||||
self.warpify_state.clear_ssh_block_state();
|
||||
self.wormhole_state.clear_ssh_block_state();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9915,7 +9910,6 @@ impl TerminalView {
|
||||
spawning_command,
|
||||
subshell_info,
|
||||
shell,
|
||||
session_type,
|
||||
..
|
||||
}: SessionBootstrappedEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
@@ -9929,18 +9923,8 @@ impl TerminalView {
|
||||
});
|
||||
}
|
||||
|
||||
let warpification_source = match session_type {
|
||||
BootstrapSessionType::WarpifiedRemote => WarpificationSource::Ssh,
|
||||
BootstrapSessionType::Local => WarpificationSource::Subshell,
|
||||
};
|
||||
let ssh_success_block_handle = ctx.add_typed_action_view(|ctx| {
|
||||
WarpifySuccessBlock::new(
|
||||
warpification_source,
|
||||
spawning_command,
|
||||
subshell_info,
|
||||
shell,
|
||||
ctx,
|
||||
)
|
||||
WormholeSuccessBlock::new(spawning_command, subshell_info, shell, ctx)
|
||||
});
|
||||
ctx.subscribe_to_view(&ssh_success_block_handle, move |me, _, event, ctx| {
|
||||
me.handle_ssh_success_block_events(event, ctx);
|
||||
@@ -9948,9 +9932,9 @@ impl TerminalView {
|
||||
|
||||
self.clear_ssh_blocks(ctx);
|
||||
self.insert_rich_content(
|
||||
Some(RichContentType::WarpifySuccessBlock),
|
||||
Some(RichContentType::WormholeSuccessBlock),
|
||||
ssh_success_block_handle.clone(),
|
||||
Some(RichContentMetadata::WarpifySuccessBlock {
|
||||
Some(RichContentMetadata::WormholeSuccessBlock {
|
||||
bootstrap_success_block_handle: ssh_success_block_handle.clone(),
|
||||
}),
|
||||
RichContentInsertionPosition::Append {
|
||||
@@ -9958,30 +9942,30 @@ impl TerminalView {
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
self.warpify_state
|
||||
.set_ssh_block_state(SshBlockState::WarpifySuccess {
|
||||
self.wormhole_state
|
||||
.set_ssh_block_state(SshBlockState::WormholeSuccess {
|
||||
handle: ssh_success_block_handle,
|
||||
});
|
||||
let active_session_id = self.active_block_session_id();
|
||||
self.warpify_state.on_warpify_start(active_session_id);
|
||||
self.wormhole_state.on_wormhole_start(active_session_id);
|
||||
self.refresh_warp_prompt(ctx);
|
||||
}
|
||||
|
||||
fn handle_ssh_success_block_events(
|
||||
&mut self,
|
||||
event: &WarpifySuccessBlockEvent,
|
||||
event: &WormholeSuccessBlockEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
WarpifySuccessBlockEvent::OpenWarpifySettings => {
|
||||
ctx.emit(Event::OpenSettings(SettingsSection::Warpify));
|
||||
WormholeSuccessBlockEvent::OpenWormholeSettings => {
|
||||
ctx.emit(Event::OpenSettings(SettingsSection::Wormhole));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss_warpify_banner(
|
||||
fn dismiss_wormhole_banner(
|
||||
&mut self,
|
||||
remember_command: &RememberForWarpification,
|
||||
remember_command: &RememberForWormholing,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
{
|
||||
@@ -9989,54 +9973,54 @@ impl TerminalView {
|
||||
model.block_list_mut().set_active_block_banner(None);
|
||||
}
|
||||
|
||||
// Also clear the warpify footer so it doesn't linger after warpification
|
||||
// Also clear the wormhole footer so it doesn't linger after wormholing
|
||||
// starts, fails, or is cancelled.
|
||||
if FeatureFlag::WarpifyFooter.is_enabled() {
|
||||
if FeatureFlag::WormholeFooter.is_enabled() {
|
||||
self.use_agent_footer.update(ctx, |footer, ctx| {
|
||||
footer.clear_warpify(ctx);
|
||||
footer.clear_wormhole(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
match remember_command {
|
||||
RememberForWarpification::RememberSubshellCommand(command) => {
|
||||
WarpifySettings::handle(ctx).update(ctx, |warpify, ctx| {
|
||||
warpify.denylist_subshell_command(command, ctx);
|
||||
RememberForWormholing::RememberSubshellCommand(command) => {
|
||||
WormholeSettings::handle(ctx).update(ctx, |wormhole, ctx| {
|
||||
wormhole.denylist_subshell_command(command, ctx);
|
||||
});
|
||||
}
|
||||
RememberForWarpification::RememberSSHHost(host) => {
|
||||
WarpifySettings::handle(ctx).update(ctx, |warpify, ctx| {
|
||||
warpify.denylist_ssh_host(host, ctx);
|
||||
RememberForWormholing::RememberSSHHost(host) => {
|
||||
WormholeSettings::handle(ctx).update(ctx, |wormhole, ctx| {
|
||||
wormhole.denylist_ssh_host(host, ctx);
|
||||
});
|
||||
}
|
||||
RememberForWarpification::DoNotRememberSubshellCommand
|
||||
| RememberForWarpification::DoNotRememberSSHHost => {}
|
||||
RememberForWormholing::DoNotRememberSubshellCommand
|
||||
| RememberForWormholing::DoNotRememberSSHHost => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_warpify_banner(
|
||||
fn show_wormhole_banner(
|
||||
&mut self,
|
||||
command: String,
|
||||
title: &str,
|
||||
lowercase_title: &str,
|
||||
warpify_keybinding: Option<Keystroke>,
|
||||
wormhole_keybinding: Option<Keystroke>,
|
||||
telemetry_event: TelemetryEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if FeatureFlag::WarpifyFooter.is_enabled() {
|
||||
if FeatureFlag::WormholeFooter.is_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut model = self.model.lock();
|
||||
|
||||
// Shared session viewers can't initiate warpification currently.
|
||||
// Don't show the warpify banner when an agent is monitoring the command either.
|
||||
// Shared session viewers can't initiate wormholing currently.
|
||||
// Don't show the wormhole banner when an agent is monitoring the command either.
|
||||
if model.shared_session_status().is_viewer()
|
||||
|| model.block_list().active_block().is_agent_monitoring()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let a11y_message = match &warpify_keybinding {
|
||||
let a11y_message = match &wormhole_keybinding {
|
||||
Some(keystroke) => format!(
|
||||
"You can press {} to Wormhole this {} for more Galaxy features.",
|
||||
keystroke.displayed(),
|
||||
@@ -10047,8 +10031,8 @@ impl TerminalView {
|
||||
|
||||
model
|
||||
.block_list_mut()
|
||||
.set_active_block_banner(Some(WithinBlockBanner::WarpifyBanner(
|
||||
WarpifyBannerState::new(command, warpify_keybinding),
|
||||
.set_active_block_banner(Some(WithinBlockBanner::WormholeBanner(
|
||||
WormholeBannerState::new(command, wormhole_keybinding),
|
||||
)));
|
||||
|
||||
let a11y_content = AccessibilityContent::new(
|
||||
@@ -11310,7 +11294,7 @@ impl TerminalView {
|
||||
/// Returns true if the block is considered remote.
|
||||
///
|
||||
/// Note that we don't know for sure if a block is remote, because we can only detect
|
||||
/// warpified remote blocks.
|
||||
/// wormholed remote blocks.
|
||||
///
|
||||
/// For some organizations, we accept a regex list that we run against commands to
|
||||
/// further make the determination.
|
||||
@@ -11320,7 +11304,7 @@ impl TerminalView {
|
||||
command: Option<&str>,
|
||||
app: &AppContext,
|
||||
) -> bool {
|
||||
let is_warpified_remote = session_id
|
||||
let is_wormholed_remote = session_id
|
||||
.map(|id| {
|
||||
self.sessions
|
||||
.as_ref(app)
|
||||
@@ -11330,7 +11314,7 @@ impl TerminalView {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if is_warpified_remote {
|
||||
if is_wormholed_remote {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11980,7 +11964,8 @@ impl TerminalView {
|
||||
|
||||
// If this block ran a possible subshell command, and it exited before the 1s timer
|
||||
// completed, abort showing the banner.
|
||||
if let Some(abort_handle) = self.warpify_state.take_subshell_banner_abort_handle() {
|
||||
if let Some(abort_handle) = self.wormhole_state.take_subshell_banner_abort_handle()
|
||||
{
|
||||
abort_handle.abort();
|
||||
}
|
||||
|
||||
@@ -12022,9 +12007,9 @@ impl TerminalView {
|
||||
self.on_user_block_completed(&block_completed_event.block_id, ctx);
|
||||
}
|
||||
|
||||
// Clear any stale warpify footer so it doesn't leak into the next command's footer rendering.
|
||||
// Clear any stale wormhole footer so it doesn't leak into the next command's footer rendering.
|
||||
self.use_agent_footer.update(ctx, |footer, ctx| {
|
||||
footer.clear_warpify(ctx);
|
||||
footer.clear_wormhole(ctx);
|
||||
});
|
||||
self.hide_use_agent_footer_in_blocklist(ctx);
|
||||
if matches!(block_completed_event.block_type, BlockType::User(_)) {
|
||||
@@ -12109,7 +12094,7 @@ impl TerminalView {
|
||||
self.drop_hidden_passive_ai_blocks(ctx);
|
||||
|
||||
// If the first word of the command is a shell alias, expand it
|
||||
// for subshell/SSH detection. This enables warpification for
|
||||
// for subshell/SSH detection. This enables wormholing for
|
||||
// aliased SSH commands (e.g. `alias myssh='ssh user@host'`).
|
||||
let expanded_command = self
|
||||
.active_block_session_id()
|
||||
@@ -12119,19 +12104,19 @@ impl TerminalView {
|
||||
let alias_value = session.alias_value(first_word)?;
|
||||
Some(format!("{alias_value}{rest}"))
|
||||
});
|
||||
let warpify_command = expanded_command.as_deref().unwrap_or(command.as_str());
|
||||
let wormhole_command = expanded_command.as_deref().unwrap_or(command.as_str());
|
||||
|
||||
// Check if the current running command spawns a subshell eligible for Warpification.
|
||||
// Check if the current running command spawns a subshell eligible for Wormholing.
|
||||
let shell_family = self.shell_family(ctx);
|
||||
let warpify_settings = WarpifySettings::as_ref(ctx);
|
||||
let is_compatible_subshell_command = warpify_settings
|
||||
let wormhole_settings = WormholeSettings::as_ref(ctx);
|
||||
let is_compatible_subshell_command = wormhole_settings
|
||||
.is_compatible_subshell_command(command, shell_family)
|
||||
|| warpify_settings
|
||||
.is_compatible_subshell_command(warpify_command, shell_family);
|
||||
let command_is_denylisted = warpify_settings
|
||||
|| wormhole_settings
|
||||
.is_compatible_subshell_command(wormhole_command, shell_family);
|
||||
let command_is_denylisted = wormhole_settings
|
||||
.is_denylisted_subshell_command(command)
|
||||
|| warpify_settings.is_denylisted_subshell_command(warpify_command);
|
||||
// Never warpify or surface warpification for agent-requested commands.
|
||||
|| wormhole_settings.is_denylisted_subshell_command(wormhole_command);
|
||||
// Never wormhole or surface wormholing for agent-requested commands.
|
||||
let has_ai_metadata = self
|
||||
.model
|
||||
.lock()
|
||||
@@ -12142,30 +12127,30 @@ impl TerminalView {
|
||||
|
||||
if is_compatible_subshell_command {
|
||||
if command_is_denylisted || has_ai_metadata {
|
||||
// Don't auto-warpify or surface warpification for these commands.
|
||||
// Don't auto-wormhole or surface wormholing for these commands.
|
||||
} else if let Some(shell_type) = self.pending_auto_bootstrap_shell_type.take() {
|
||||
// If there is a subshell we're waiting to bootstrap until we receive
|
||||
// the preexec hook, now we can bootstrap it.
|
||||
let auto_warpify_abort_handle = ctx.spawn_abortable(
|
||||
Timer::after(Duration::from_millis(AUTO_WARPIFY_DELAY)),
|
||||
let auto_wormhole_abort_handle = ctx.spawn_abortable(
|
||||
Timer::after(Duration::from_millis(AUTO_WORMHOLE_DELAY)),
|
||||
move |me, _, ctx| {
|
||||
me.trigger_subshell_bootstrap(Some(shell_type), false, ctx);
|
||||
},
|
||||
|_, _| (),
|
||||
);
|
||||
self.warpify_state
|
||||
.add_auto_warpify_abort_handle(auto_warpify_abort_handle);
|
||||
self.wormhole_state
|
||||
.add_auto_wormhole_abort_handle(auto_wormhole_abort_handle);
|
||||
} else {
|
||||
// Wait 1 second before showing the banner, just to make sure the
|
||||
// command stays running for a bit. If the command fails instantly,
|
||||
// we don't want to flicker the banner away so quickly.
|
||||
let command = command.clone();
|
||||
self.warpify_state
|
||||
self.wormhole_state
|
||||
.add_subshell_banner_abort_handle(ctx.spawn_abortable(
|
||||
Timer::after(*SUBSHELL_BANNER_DELAY_DURATION),
|
||||
|view, _, ctx| {
|
||||
if FeatureFlag::WarpifyFooter.is_enabled() {
|
||||
view.show_warpify_footer(ctx);
|
||||
if FeatureFlag::WormholeFooter.is_enabled() {
|
||||
view.show_wormhole_footer(ctx);
|
||||
} else {
|
||||
view.handle_action(
|
||||
&TerminalAction::ShowSubshellBanner(command),
|
||||
@@ -12179,14 +12164,14 @@ impl TerminalView {
|
||||
} else {
|
||||
if !has_ai_metadata {
|
||||
if let Some(ssh_host) =
|
||||
parse_interactive_ssh_command(warpify_command).map(|cmd| cmd.host)
|
||||
parse_interactive_ssh_command(wormhole_command).map(|cmd| cmd.host)
|
||||
{
|
||||
self.warpify_state
|
||||
.set_pending_ssh_host(warpify_command.to_string(), ssh_host);
|
||||
self.wormhole_state
|
||||
.set_pending_ssh_host(wormhole_command.to_string(), ssh_host);
|
||||
self.model.lock().start_notify_on_end_of_ssh_login();
|
||||
ctx.emit(Event::TerminalViewStateChanged);
|
||||
} else {
|
||||
self.warpify_state.clear_pending_ssh_host();
|
||||
self.wormhole_state.clear_pending_ssh_host();
|
||||
|
||||
ctx.spawn(
|
||||
Timer::after(Duration::from_millis(
|
||||
@@ -12284,14 +12269,14 @@ impl TerminalView {
|
||||
cloud_workflow_id,
|
||||
cloud_env_var_collection_id,
|
||||
}) => {
|
||||
// To automatically warpify a subshell, we run the relevant command
|
||||
// To automatically wormhole a subshell, we run the relevant command
|
||||
// subshell and create a future to delay bootstrapping the subshell long enough for
|
||||
// the command to complete. We receive AfterBlockCompleted if the subshell command
|
||||
// returns an error or the user exits the subshell. Here we abort the future to
|
||||
// avoid an attempt to trigger bootstrapping if the subshell command failed. If the
|
||||
// future already resolved, abort has no effect. We handle this as early as possible
|
||||
// because the abort is time sensitive.
|
||||
self.warpify_state.abort_auto_warpify();
|
||||
self.wormhole_state.abort_auto_wormhole();
|
||||
|
||||
let active_session = self
|
||||
.active_block_session_id()
|
||||
@@ -12366,14 +12351,14 @@ impl TerminalView {
|
||||
}
|
||||
let active_session_id = self.active_block_session_id();
|
||||
if let Some(block_id) = self
|
||||
.warpify_state
|
||||
.get_completed_warpify_session_id(active_session_id, ctx)
|
||||
.wormhole_state
|
||||
.get_completed_wormhole_session_id(active_session_id, ctx)
|
||||
{
|
||||
self.remove_ssh_block_by_id(block_id);
|
||||
}
|
||||
|
||||
self.dismiss_warpify_banner(
|
||||
&RememberForWarpification::DoNotRememberSubshellCommand,
|
||||
self.dismiss_wormhole_banner(
|
||||
&RememberForWormholing::DoNotRememberSubshellCommand,
|
||||
ctx,
|
||||
);
|
||||
|
||||
@@ -12868,7 +12853,7 @@ impl TerminalView {
|
||||
.active_block()
|
||||
.agent_interaction_metadata()
|
||||
.is_some();
|
||||
// Never warpify for agent-requested commands.
|
||||
// Never wormhole for agent-requested commands.
|
||||
if has_ai_metadata {
|
||||
return;
|
||||
}
|
||||
@@ -13063,8 +13048,8 @@ impl TerminalView {
|
||||
me.remove_ssh_remote_server_choice_block(session_id, ctx);
|
||||
ctx.emit(Event::RemoteServerSkipRequested { session_id });
|
||||
}
|
||||
SshRemoteServerChoiceViewEvent::OpenWarpifySettings => {
|
||||
ctx.emit(Event::OpenSettings(SettingsSection::Warpify));
|
||||
SshRemoteServerChoiceViewEvent::OpenWormholeSettings => {
|
||||
ctx.emit(Event::OpenSettings(SettingsSection::Wormhole));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13259,7 +13244,7 @@ impl TerminalView {
|
||||
|
||||
// Clear the pending flag up front so the notice is shown at most once, even if the
|
||||
// banner is dismissed without interaction or the session ends early.
|
||||
WarpifySettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
WormholeSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
settings.mark_tmux_deprecation_notice_shown(ctx);
|
||||
});
|
||||
|
||||
@@ -13690,7 +13675,7 @@ impl TerminalView {
|
||||
self.update_incompatible_configuration_banner(session.shell().plugins(), ctx);
|
||||
|
||||
if let Some(subshell_info) = session.subshell_info() {
|
||||
self.warpify_state
|
||||
self.wormhole_state
|
||||
.add_subshell_separator(subshell_info, self.model.clone(), ctx);
|
||||
}
|
||||
|
||||
@@ -13772,22 +13757,23 @@ impl TerminalView {
|
||||
.spawn(async move { session_clone2.load_all_builtins().await })
|
||||
.detach();
|
||||
|
||||
// If we were waiting for a successful warpification, it's come. Stop the timeout.
|
||||
self.warpify_state.abort_ssh_warpify_timeout();
|
||||
// If we were waiting for a successful wormholing, it's come. Stop the timeout.
|
||||
self.wormhole_state.abort_ssh_wormhole_timeout();
|
||||
|
||||
let is_warpified_remote = matches!(
|
||||
let is_wormholed_remote = matches!(
|
||||
bootstrap_event.session_type,
|
||||
BootstrapSessionType::WarpifiedRemote
|
||||
BootstrapSessionType::WormholedRemote
|
||||
);
|
||||
if bootstrap_event.subshell_info.is_some() {
|
||||
self.add_bootstrap_success_block(bootstrap_event, ctx);
|
||||
}
|
||||
|
||||
// Show the one-time tmux deprecation notice when an SSH session successfully
|
||||
// warpifies. The end-of-ssh-login path (`handle_detected_end_of_ssh_login`) only
|
||||
// fires for sessions that stay unwarpified, since warpification replaces the
|
||||
// wormholes. The end-of-ssh-login path (`handle_detected_end_of_ssh_login`) only
|
||||
// fires for sessions that stay unwormholed, since wormholing replaces the
|
||||
// original ssh block before login detection can confirm completion.
|
||||
if is_warpified_remote && WarpifySettings::as_ref(ctx).should_show_tmux_deprecation_notice()
|
||||
if is_wormholed_remote
|
||||
&& WormholeSettings::as_ref(ctx).should_show_tmux_deprecation_notice()
|
||||
{
|
||||
self.show_ssh_tmux_deprecation_banner(session_id, ctx);
|
||||
}
|
||||
@@ -15201,7 +15187,7 @@ impl TerminalView {
|
||||
// https://github.com/warpdotdev/command-corrections/blob/df7848d4fb3da7883623e959889a296a07d88053/src/rules/cd/mod.rs#L31-L36
|
||||
// We don't currently support dynamic rules over SSH, so we should not attempt to correct commands if
|
||||
// inside ssh session.
|
||||
let is_ssh_command = SshWarpifyCommand::matches(input).is_some();
|
||||
let is_ssh_command = SshWormholeCommand::matches(input).is_some();
|
||||
if is_ssh_command {
|
||||
return vec![];
|
||||
}
|
||||
@@ -19105,7 +19091,7 @@ impl TerminalView {
|
||||
.and_then(|id| self.sessions.as_ref(ctx).get(id))
|
||||
{
|
||||
if let Some(info) = session.subshell_info() {
|
||||
self.warpify_state
|
||||
self.wormhole_state
|
||||
.add_subshell_separator(info, self.model.clone(), ctx);
|
||||
}
|
||||
}
|
||||
@@ -20152,9 +20138,9 @@ impl TerminalView {
|
||||
env_var_collection_block.clear_selection(ctx);
|
||||
});
|
||||
}
|
||||
Some(RichContentMetadata::WarpifySuccessBlock { .. }) => {
|
||||
// TODO(Simon): We should be checking for WarpifySuccessBlocks here as well.
|
||||
// The `WarpifySuccessBlock` implements a `SelectableArea`.
|
||||
Some(RichContentMetadata::WormholeSuccessBlock { .. }) => {
|
||||
// TODO(Simon): We should be checking for WormholeSuccessBlocks here as well.
|
||||
// The `WormholeSuccessBlock` implements a `SelectableArea`.
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -23365,7 +23351,7 @@ impl TerminalView {
|
||||
} else {
|
||||
// Remote session: pair CWD with the session's host_id.
|
||||
let host_id = match session.session_type() {
|
||||
SessionType::WarpifiedRemote { host_id } => host_id,
|
||||
SessionType::WormholedRemote { host_id } => host_id,
|
||||
SessionType::Local => return None,
|
||||
}?;
|
||||
let std_path = StandardizedPath::try_new(cwd_str).ok()?;
|
||||
@@ -24110,7 +24096,7 @@ impl TerminalView {
|
||||
|
||||
let mut subshell_separators = HashMap::new();
|
||||
|
||||
for (id, command) in self.warpify_state.get_subshell_separators() {
|
||||
for (id, command) in self.wormhole_state.get_subshell_separators() {
|
||||
subshell_separators.insert(*id, render_subshell_separator(command.clone(), appearance));
|
||||
}
|
||||
|
||||
@@ -24122,8 +24108,8 @@ impl TerminalView {
|
||||
.active_block()
|
||||
.block_banner()
|
||||
.map(|banner| match banner {
|
||||
WithinBlockBanner::WarpifyBanner(state) => {
|
||||
render_warpification_banner(state, appearance)
|
||||
WithinBlockBanner::WormholeBanner(state) => {
|
||||
render_wormholing_banner(state, appearance)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -25397,7 +25383,7 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
/// Replace the terminal input buffer with the given command that is meant to open a subshell.
|
||||
/// Set a flag that we should automatically bootstrap AKA "warpify" the subshell when we
|
||||
/// Set a flag that we should automatically bootstrap AKA "wormhole" the subshell when we
|
||||
/// receive the [`AfterBlockStarted`] event.
|
||||
pub fn insert_subshell_command_and_bootstrap_if_supported(
|
||||
&mut self,
|
||||
@@ -25631,7 +25617,7 @@ impl TerminalView {
|
||||
shell_type: ShellType,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
// Attempt to auto warpify the subshell when bootstrapped
|
||||
// Attempt to auto wormhole the subshell when bootstrapped
|
||||
self.pending_auto_bootstrap_shell_type = Some(shell_type);
|
||||
|
||||
self.input.update(ctx, |input, ctx| {
|
||||
@@ -25853,7 +25839,7 @@ impl TerminalView {
|
||||
ctx: &mut ViewContext<TerminalView>,
|
||||
) {
|
||||
match check_type {
|
||||
SshLoginStatus::RecheckBeforeWarpifying => {
|
||||
SshLoginStatus::RecheckBeforeWormholing => {
|
||||
// After we receive a line of output from ssh that is NOT prompting for user input (unlike "Enter passphrase: "),
|
||||
// we wait and repeat the check after a small delay in case the state returned to something that's user-input bound.
|
||||
// For example, say the output that kicked off this event was "Permission denied, please try again." and
|
||||
@@ -25875,11 +25861,11 @@ impl TerminalView {
|
||||
},
|
||||
);
|
||||
}
|
||||
SshLoginStatus::ReadyToWarpify => {
|
||||
// The tmux-based SSH warpification flow has been removed in favor of the
|
||||
SshLoginStatus::ReadyToWormhole => {
|
||||
// The tmux-based SSH wormholing flow has been removed in favor of the
|
||||
// remote-server SSH extension. If this user had previously opted into the tmux
|
||||
// wrapper, show them a one-time deprecation notice on their next SSH session.
|
||||
if WarpifySettings::as_ref(ctx).should_show_tmux_deprecation_notice() {
|
||||
if WormholeSettings::as_ref(ctx).should_show_tmux_deprecation_notice() {
|
||||
if let Some(session_id) = self.active_block_session_id() {
|
||||
self.show_ssh_tmux_deprecation_banner(session_id, ctx);
|
||||
}
|
||||
@@ -25922,22 +25908,22 @@ impl TerminalView {
|
||||
let alias_value = session.alias_value(first_word)?;
|
||||
Some(format!("{alias_value}{rest}"))
|
||||
});
|
||||
let warpify_command = expanded_command.as_deref().unwrap_or(command);
|
||||
let wormhole_command = expanded_command.as_deref().unwrap_or(command);
|
||||
let shell_family = self.shell_family_for_password_prompt_polling(ctx);
|
||||
let warpify_settings = WarpifySettings::as_ref(ctx);
|
||||
let is_compatible_subshell_command = warpify_settings
|
||||
let wormhole_settings = WormholeSettings::as_ref(ctx);
|
||||
let is_compatible_subshell_command = wormhole_settings
|
||||
.is_compatible_subshell_command(command, shell_family)
|
||||
|| warpify_settings.is_compatible_subshell_command(warpify_command, shell_family);
|
||||
|| wormhole_settings.is_compatible_subshell_command(wormhole_command, shell_family);
|
||||
|
||||
!is_compatible_subshell_command
|
||||
}
|
||||
|
||||
/// Shows the warpify footer for a detected subshell command.
|
||||
fn show_warpify_footer(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
/// Shows the wormhole footer for a detected subshell command.
|
||||
fn show_wormhole_footer(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let model = self.model.lock();
|
||||
|
||||
// Shared session viewers can't initiate warpification currently.
|
||||
// Don't show the warpify footer when an agent is monitoring the command either.
|
||||
// Shared session viewers can't initiate wormholing currently.
|
||||
// Don't show the wormhole footer when an agent is monitoring the command either.
|
||||
if model.shared_session_status().is_viewer()
|
||||
|| model.block_list().active_block().is_agent_monitoring()
|
||||
{
|
||||
@@ -25946,11 +25932,11 @@ impl TerminalView {
|
||||
drop(model);
|
||||
|
||||
self.use_agent_footer.update(ctx, |footer, ctx| {
|
||||
footer.show_warpify(ctx);
|
||||
footer.show_wormhole(ctx);
|
||||
});
|
||||
self.maybe_show_use_agent_footer_in_blocklist(ctx);
|
||||
|
||||
send_telemetry_from_ctx!(TelemetryEvent::WarpifyFooterShown { is_ssh: false }, ctx);
|
||||
send_telemetry_from_ctx!(TelemetryEvent::WormholeFooterShown { is_ssh: false }, ctx);
|
||||
}
|
||||
|
||||
fn show_initialization_block(&mut self) {
|
||||
@@ -26330,7 +26316,7 @@ impl TypedActionView for TerminalView {
|
||||
"Showed initialization block",
|
||||
GalaxyA11yRole::TextareaRole,
|
||||
)),
|
||||
ShowWarpifySettings => Custom(AccessibilityContent::new_without_help(
|
||||
ShowWormholeSettings => Custom(AccessibilityContent::new_without_help(
|
||||
"Opened Wormhole Settings",
|
||||
GalaxyA11yRole::ButtonRole,
|
||||
)),
|
||||
@@ -26380,7 +26366,7 @@ impl TypedActionView for TerminalView {
|
||||
| ControlSequence(_)
|
||||
| TriggerSubshellBootstrap
|
||||
| ShowSubshellBanner(_)
|
||||
| DismissWarpifyBanner(_)
|
||||
| DismissWormholeBanner(_)
|
||||
| OpenBlockListContextMenu
|
||||
| AliasExpansionBanner(_)
|
||||
| VimModeBanner(_)
|
||||
@@ -26891,21 +26877,21 @@ impl TypedActionView for TerminalView {
|
||||
TriggerSubshellBootstrap => self.trigger_subshell_bootstrap(None, false, ctx),
|
||||
ShowSubshellBanner(command) => {
|
||||
// Abort handle is no longer needed since we've waited the 1s already.
|
||||
self.warpify_state.take_subshell_banner_abort_handle();
|
||||
self.wormhole_state.take_subshell_banner_abort_handle();
|
||||
|
||||
let warpify_keybinding =
|
||||
keybinding_name_to_keystroke("terminal:warpify_subshell", ctx);
|
||||
self.show_warpify_banner(
|
||||
let wormhole_keybinding =
|
||||
keybinding_name_to_keystroke("terminal:wormhole_subshell", ctx);
|
||||
self.show_wormhole_banner(
|
||||
command.to_owned(),
|
||||
"Subshell",
|
||||
"subshell",
|
||||
warpify_keybinding,
|
||||
wormhole_keybinding,
|
||||
TelemetryEvent::ShowSubshellBanner,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
DismissWarpifyBanner(remember) => {
|
||||
self.dismiss_warpify_banner(remember, ctx);
|
||||
DismissWormholeBanner(remember) => {
|
||||
self.dismiss_wormhole_banner(remember, ctx);
|
||||
if !remember.is_ssh() {
|
||||
send_telemetry_from_ctx!(
|
||||
TelemetryEvent::DeclineSubshellBootstrap {
|
||||
@@ -27142,7 +27128,7 @@ impl TypedActionView for TerminalView {
|
||||
LoadAgentModeConversation => {
|
||||
self.load_agent_mode_conversation(ctx);
|
||||
}
|
||||
ShowWarpifySettings => ctx.emit(Event::OpenSettings(SettingsSection::Warpify)),
|
||||
ShowWormholeSettings => ctx.emit(Event::OpenSettings(SettingsSection::Wormhole)),
|
||||
DeleteAttachment { index } => {
|
||||
self.ai_context_model.update(ctx, |context_model, ctx| {
|
||||
context_model.remove_pending_attachment(*index, ctx);
|
||||
@@ -28374,15 +28360,15 @@ impl View for TerminalView {
|
||||
context.set.insert(init::ROOT_CLOUD_MODE_PANE_KEY);
|
||||
}
|
||||
|
||||
if let Some(WithinBlockBanner::WarpifyBanner(_)) =
|
||||
if let Some(WithinBlockBanner::WormholeBanner(_)) =
|
||||
model_lock.block_list().active_block().block_banner()
|
||||
{
|
||||
context.set.insert("SubshellBanner");
|
||||
}
|
||||
|
||||
// Also set the warpify context when the footer (flag-gated replacement
|
||||
// Also set the wormhole context when the footer (flag-gated replacement
|
||||
// for the in-block banner) is active, so the ctrl-i keybinding works.
|
||||
if self.use_agent_footer.as_ref(app).is_warpify_active(app) {
|
||||
if self.use_agent_footer.as_ref(app).is_wormhole_active(app) {
|
||||
context.set.insert("SubshellBanner");
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ pub enum OnboardingVersion {
|
||||
/// This represents whether entering a subshell for a particular command should become automatic in
|
||||
/// the future, or to ask again.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RememberForWarpification {
|
||||
pub enum RememberForWormholing {
|
||||
/// If yes, need to transmit the command itself so it can be persisted to user-defaults
|
||||
RememberSubshellCommand(String),
|
||||
RememberSSHHost(String),
|
||||
@@ -75,22 +75,22 @@ pub enum RememberForWarpification {
|
||||
DoNotRememberSSHHost,
|
||||
}
|
||||
|
||||
impl RememberForWarpification {
|
||||
impl RememberForWormholing {
|
||||
pub fn as_bool(&self) -> bool {
|
||||
match self {
|
||||
RememberForWarpification::RememberSubshellCommand(_) => true,
|
||||
RememberForWarpification::RememberSSHHost(_) => true,
|
||||
RememberForWarpification::DoNotRememberSubshellCommand => false,
|
||||
RememberForWarpification::DoNotRememberSSHHost => false,
|
||||
RememberForWormholing::RememberSubshellCommand(_) => true,
|
||||
RememberForWormholing::RememberSSHHost(_) => true,
|
||||
RememberForWormholing::DoNotRememberSubshellCommand => false,
|
||||
RememberForWormholing::DoNotRememberSSHHost => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_ssh(&self) -> bool {
|
||||
match self {
|
||||
RememberForWarpification::RememberSSHHost(_) => true,
|
||||
RememberForWarpification::DoNotRememberSSHHost => true,
|
||||
RememberForWarpification::RememberSubshellCommand(_) => false,
|
||||
RememberForWarpification::DoNotRememberSubshellCommand => false,
|
||||
RememberForWormholing::RememberSSHHost(_) => true,
|
||||
RememberForWormholing::DoNotRememberSSHHost => true,
|
||||
RememberForWormholing::RememberSubshellCommand(_) => false,
|
||||
RememberForWormholing::DoNotRememberSubshellCommand => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,8 +284,8 @@ pub enum TerminalAction {
|
||||
},
|
||||
/// Starts a subshell in the active session.
|
||||
TriggerSubshellBootstrap,
|
||||
/// If the user says "no" to Warpification, possibly requesting not to be asked again
|
||||
DismissWarpifyBanner(RememberForWarpification),
|
||||
/// If the user says "no" to Wormholing, possibly requesting not to be asked again
|
||||
DismissWormholeBanner(RememberForWormholing),
|
||||
/// Triggers the banner asking to turn the running block into a subshell. The String is the
|
||||
/// command that the user entered.
|
||||
ShowSubshellBanner(String),
|
||||
@@ -342,7 +342,7 @@ pub enum TerminalAction {
|
||||
GenerateCodebaseIndex,
|
||||
/// This is for debugging, dev only for now
|
||||
LoadAgentModeConversation,
|
||||
ShowWarpifySettings,
|
||||
ShowWormholeSettings,
|
||||
/// Removes a pending attachment (image or file) by index in the unified list.
|
||||
DeleteAttachment {
|
||||
index: usize,
|
||||
@@ -622,7 +622,7 @@ impl fmt::Debug for TerminalAction {
|
||||
OpenBlockListContextMenu => f.write_str("OpenBlockListContextMenu"),
|
||||
AskAIAssistant { block_index } => write!(f, "AskAIAssistant({block_index:?})"),
|
||||
TriggerSubshellBootstrap => f.write_str("TriggerSubshellBootstrap"),
|
||||
DismissWarpifyBanner(remember) => write!(f, "DismissWarpifyBanner({remember:?})"),
|
||||
DismissWormholeBanner(remember) => write!(f, "DismissWormholeBanner({remember:?})"),
|
||||
ShowSubshellBanner(_) => f.write_str("ShowSubshellBanner"),
|
||||
InsertMostRecentCommandCorrection => f.write_str("InsertMostRecentCommandCorrection"),
|
||||
AliasExpansionBanner(action) => write!(f, "AliasExpansionBanner({action:?}"),
|
||||
@@ -682,7 +682,7 @@ impl fmt::Debug for TerminalAction {
|
||||
ShowInitializationBlock => write!(f, "ShowInitializationBlock"),
|
||||
GenerateCodebaseIndex => write!(f, "GenerateIndexForRepo"),
|
||||
LoadAgentModeConversation => write!(f, "LoadAgentModeConversation"),
|
||||
ShowWarpifySettings => write!(f, "ShowWarpifySettings"),
|
||||
ShowWormholeSettings => write!(f, "ShowWormholeSettings"),
|
||||
DeleteAttachment { index } => write!(f, "DeleteAttachment({index:?})"),
|
||||
OpenAttachmentLightbox { index } => {
|
||||
write!(f, "OpenAttachmentLightbox({index:?})")
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
//! without a LayoutContext. Use the exported BLOCK_BANNER_HEIGHT const when the banner height
|
||||
//! needs to be taken into account.
|
||||
|
||||
mod warpify;
|
||||
mod wormhole;
|
||||
|
||||
use galaxyui::elements::{
|
||||
ConstrainedBox, Container, CornerRadius, Hoverable, MouseState, MouseStateHandle,
|
||||
ParentElement, Radius, Stack,
|
||||
};
|
||||
use galaxyui::Element;
|
||||
pub use warpify::*;
|
||||
pub use wormhole::*;
|
||||
|
||||
use crate::themes::theme::GalaxyTheme;
|
||||
|
||||
@@ -25,13 +25,13 @@ const BANNER_H_PADDING: f32 = 8.;
|
||||
pub const BLOCK_BANNER_HEIGHT: f32 = CONSTRAINED_BANNER_HEIGHT + BANNER_TOP_MARGIN;
|
||||
|
||||
pub enum WithinBlockBanner {
|
||||
WarpifyBanner(WarpifyBannerState),
|
||||
WormholeBanner(WormholeBannerState),
|
||||
}
|
||||
|
||||
impl WithinBlockBanner {
|
||||
pub fn banner_height(&self) -> f32 {
|
||||
match self {
|
||||
WithinBlockBanner::WarpifyBanner(_) => BLOCK_BANNER_HEIGHT,
|
||||
WithinBlockBanner::WormholeBanner(_) => BLOCK_BANNER_HEIGHT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-21
@@ -10,14 +10,14 @@ use pathfinder_color::ColorU;
|
||||
|
||||
use super::render_block_banner;
|
||||
use crate::appearance::Appearance;
|
||||
use crate::terminal::view::{RememberForWarpification, TerminalAction};
|
||||
use crate::terminal::view::{RememberForWormholing, TerminalAction};
|
||||
use crate::themes::theme::Fill;
|
||||
use crate::ui_components::blended_colors;
|
||||
|
||||
const CLOSE_BUTTON_DIAMETER: f32 = 20.0;
|
||||
const STANDARD_PADDING: f32 = 8.0;
|
||||
|
||||
pub struct WarpifyBannerState {
|
||||
pub struct WormholeBannerState {
|
||||
/// The subshell command that triggered the banner.
|
||||
pub command: String,
|
||||
pub height: f32,
|
||||
@@ -25,19 +25,19 @@ pub struct WarpifyBannerState {
|
||||
pub dont_ask_button_mouse_state: MouseStateHandle,
|
||||
pub dismiss_button_mouse_state: MouseStateHandle,
|
||||
|
||||
/// This keybinding gets rendered in the Warpification banner, but we can't look it up
|
||||
/// This keybinding gets rendered in the Wormholing banner, but we can't look it up
|
||||
/// during render as a &mut AppContext is not available then. This needs to get
|
||||
/// looked up during action handling and cached here.
|
||||
pub initialize_warpify_keybinding: Option<Keystroke>,
|
||||
pub initialize_wormhole_keybinding: Option<Keystroke>,
|
||||
pub hover_state: MouseStateHandle,
|
||||
}
|
||||
|
||||
impl WarpifyBannerState {
|
||||
pub fn new(command: String, initialize_warpify_keybinding: Option<Keystroke>) -> Self {
|
||||
impl WormholeBannerState {
|
||||
pub fn new(command: String, initialize_wormhole_keybinding: Option<Keystroke>) -> Self {
|
||||
Self {
|
||||
command,
|
||||
height: 0.0,
|
||||
initialize_warpify_keybinding,
|
||||
initialize_wormhole_keybinding,
|
||||
accept_button_mouse_state: Default::default(),
|
||||
dont_ask_button_mouse_state: Default::default(),
|
||||
dismiss_button_mouse_state: Default::default(),
|
||||
@@ -46,18 +46,18 @@ impl WarpifyBannerState {
|
||||
}
|
||||
|
||||
pub fn title(&self) -> &str {
|
||||
"Warpify subshell"
|
||||
"Wormhole subshell"
|
||||
}
|
||||
|
||||
pub fn action(&self) -> TerminalAction {
|
||||
TerminalAction::TriggerSubshellBootstrap
|
||||
}
|
||||
|
||||
fn remember_for_warpification(&self, should_remember: bool) -> RememberForWarpification {
|
||||
fn remember_for_wormholing(&self, should_remember: bool) -> RememberForWormholing {
|
||||
if should_remember {
|
||||
RememberForWarpification::RememberSubshellCommand(self.command.to_owned())
|
||||
RememberForWormholing::RememberSubshellCommand(self.command.to_owned())
|
||||
} else {
|
||||
RememberForWarpification::DoNotRememberSubshellCommand
|
||||
RememberForWormholing::DoNotRememberSubshellCommand
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,18 +65,18 @@ impl WarpifyBannerState {
|
||||
/// This banner is shown when the user runs a command which is recognized as a subshell-compatible
|
||||
/// command. It asks if they want to bootstrap a subshell and, if so, whether we should ask again
|
||||
/// next time they run the same command.
|
||||
pub fn render_warpification_banner(
|
||||
state: &WarpifyBannerState,
|
||||
pub fn render_wormholing_banner(
|
||||
state: &WormholeBannerState,
|
||||
appearance: &Appearance,
|
||||
) -> Box<dyn Element> {
|
||||
let yes_button = render_yes_button(
|
||||
state,
|
||||
&state.initialize_warpify_keybinding,
|
||||
&state.initialize_wormhole_keybinding,
|
||||
&state.accept_button_mouse_state,
|
||||
appearance,
|
||||
);
|
||||
|
||||
let remember = state.remember_for_warpification(true);
|
||||
let remember = state.remember_for_wormholing(true);
|
||||
let dont_ask_button = Container::new(
|
||||
appearance
|
||||
.ui_builder()
|
||||
@@ -87,7 +87,7 @@ pub fn render_warpification_banner(
|
||||
.with_text_label("Do not show again".to_owned())
|
||||
.build()
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(TerminalAction::DismissWarpifyBanner(
|
||||
ctx.dispatch_typed_action(TerminalAction::DismissWormholeBanner(
|
||||
remember.to_owned(),
|
||||
));
|
||||
})
|
||||
@@ -96,7 +96,7 @@ pub fn render_warpification_banner(
|
||||
.with_margin_right(16.)
|
||||
.finish();
|
||||
|
||||
let do_not_remember = state.remember_for_warpification(false);
|
||||
let do_not_remember = state.remember_for_wormholing(false);
|
||||
let close_button = appearance
|
||||
.ui_builder()
|
||||
.close_button(
|
||||
@@ -105,7 +105,7 @@ pub fn render_warpification_banner(
|
||||
)
|
||||
.build()
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(TerminalAction::DismissWarpifyBanner(
|
||||
ctx.dispatch_typed_action(TerminalAction::DismissWormholeBanner(
|
||||
do_not_remember.to_owned(),
|
||||
));
|
||||
})
|
||||
@@ -132,12 +132,12 @@ pub fn render_warpification_banner(
|
||||
}
|
||||
|
||||
fn render_yes_button(
|
||||
state: &WarpifyBannerState,
|
||||
initialize_warpification_keybinding: &Option<Keystroke>,
|
||||
state: &WormholeBannerState,
|
||||
initialize_wormholing_keybinding: &Option<Keystroke>,
|
||||
mouse_state: &MouseStateHandle,
|
||||
appearance: &Appearance,
|
||||
) -> Box<dyn Element> {
|
||||
let yes_button = match initialize_warpification_keybinding {
|
||||
let yes_button = match initialize_wormholing_keybinding {
|
||||
Some(keystroke) => appearance
|
||||
.ui_builder()
|
||||
.keyboard_shortcut_button(state.title().to_owned(), keystroke, mouse_state.clone())
|
||||
@@ -81,8 +81,8 @@ pub fn init(app: &mut AppContext) {
|
||||
app.register_binding_validator::<TerminalView>(is_binding_pty_compliant);
|
||||
|
||||
init_overlapping_keybindings(app);
|
||||
// Register input mode bindings before warpify bindings so ctrl-i warpifies
|
||||
// instead of opening inline agent when a warpify banner is visible.
|
||||
// Register input mode bindings before wormhole bindings so ctrl-i wormholes
|
||||
// instead of opening inline agent when a wormhole banner is visible.
|
||||
register_input_mode_bindings(app);
|
||||
|
||||
app.register_fixed_bindings([
|
||||
@@ -320,7 +320,7 @@ pub fn init(app: &mut AppContext) {
|
||||
| (id!("Terminal") & !id!("IMEOpen") & id!(flags::CLI_AGENT_RICH_INPUT_OPEN)),
|
||||
),
|
||||
EditableBinding::new(
|
||||
"terminal:warpify_subshell",
|
||||
"terminal:wormhole_subshell",
|
||||
"Wormhole subshell",
|
||||
TerminalAction::TriggerSubshellBootstrap,
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::terminal::view::init_environment::InitEnvironmentBlock;
|
||||
use crate::terminal::view::ssh_remote_server_choice_view::SshRemoteServerChoiceView;
|
||||
use crate::terminal::view::ssh_remote_server_failed_banner::SshRemoteServerFailedBanner;
|
||||
use crate::terminal::view::ssh_tmux_deprecation_banner::SshTmuxDeprecationBanner;
|
||||
use crate::terminal::warpify::success_block::WarpifySuccessBlock;
|
||||
use crate::terminal::wormhole::success_block::WormholeSuccessBlock;
|
||||
use crate::terminal::TerminalView;
|
||||
|
||||
/// Specifies where to insert rich content in the blocklist.
|
||||
@@ -249,8 +249,8 @@ pub enum RichContentMetadata {
|
||||
SshTmuxDeprecationBanner {
|
||||
handle: ViewHandle<SshTmuxDeprecationBanner>,
|
||||
},
|
||||
WarpifySuccessBlock {
|
||||
bootstrap_success_block_handle: ViewHandle<WarpifySuccessBlock>,
|
||||
WormholeSuccessBlock {
|
||||
bootstrap_success_block_handle: ViewHandle<WormholeSuccessBlock>,
|
||||
},
|
||||
TelemetryBanner {
|
||||
telemetry_banner_handle: ViewHandle<TelemetryBanner>,
|
||||
|
||||
@@ -187,7 +187,7 @@ impl FileUpload {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an sftp command that copies a given local file into the PWD of the warpified ssh session, if any.
|
||||
/// Creates an sftp command that copies a given local file into the PWD of the wormholed ssh session, if any.
|
||||
fn transfer_file_sftp_command(&self, file_upload: &FileUploadInfo) -> String {
|
||||
// "sftp "
|
||||
let mut command = String::from("sftp ");
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
//! Inline block view that asks the user whether they want to install
|
||||
//! Warp's SSH extension on the remote host the shell just connected to,
|
||||
//! Wormhole's remote helper on the host the shell just connected to,
|
||||
//! or continue without installing (falling back to the existing
|
||||
//! ControlMaster warpification path).
|
||||
//! ControlMaster wormholing path).
|
||||
//!
|
||||
//! Designed from frame 6050:2448 of the Figma file
|
||||
//! [Remote session initialization](https://www.figma.com/design/r0BO9cTZCK6pDE6qerg2K0/Remote-session-initialization).
|
||||
//!
|
||||
//! The view owns:
|
||||
//! - a child [`KeyboardNavigableButtons`] handle for the two selectable
|
||||
//! cards ("Install Warp's SSH extension" / "Continue without installing"),
|
||||
//! cards ("Install Wormhole helper" / "Continue without installing"),
|
||||
//! - the [`SessionId`] this prompt is scoped to (used for event forwarding),
|
||||
//! - the current "Don't ask me this again" checked state (purely local to
|
||||
//! this prompt instance; persisted to `ssh_extension_install_mode` only
|
||||
@@ -37,7 +37,7 @@ use crate::ai::blocklist::inline_action::inline_action_header::{
|
||||
};
|
||||
use crate::server::telemetry::TelemetryEvent;
|
||||
use crate::terminal::model::session::SessionId;
|
||||
use crate::terminal::warpify::settings::{SshExtensionInstallMode, WarpifySettings};
|
||||
use crate::terminal::wormhole::settings::{SshExtensionInstallMode, WormholeSettings};
|
||||
use crate::ui_components::blended_colors;
|
||||
use crate::{send_telemetry_from_ctx, Appearance};
|
||||
|
||||
@@ -48,14 +48,14 @@ pub enum SshRemoteServerChoiceViewAction {
|
||||
Install,
|
||||
Skip,
|
||||
ToggleDoNotAskAgain,
|
||||
OpenWarpifySettings,
|
||||
OpenWormholeSettings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SshRemoteServerChoiceViewEvent {
|
||||
Install,
|
||||
Skip,
|
||||
OpenWarpifySettings,
|
||||
OpenWormholeSettings,
|
||||
}
|
||||
|
||||
/// Choice block prompting the user to install the remote-server binary on the remote host or skip.
|
||||
@@ -74,7 +74,7 @@ impl SshRemoteServerChoiceView {
|
||||
let buttons = ctx.add_typed_action_view(|_| {
|
||||
KeyboardNavigableButtons::new(vec![
|
||||
rich_navigation_button(
|
||||
"Install Galaxy's SSH extension".to_string(),
|
||||
"Install Wormhole helper".to_string(),
|
||||
Some(
|
||||
"Install Galaxy's extension to enable agent features like file browsing, \
|
||||
code review, and intelligent command completions in this session."
|
||||
@@ -171,14 +171,16 @@ impl SshRemoteServerChoiceView {
|
||||
.with_child(Container::new(checkbox_label).with_margin_left(4.).finish())
|
||||
.finish();
|
||||
|
||||
// Right: "Manage Warpify settings" link.
|
||||
// Right: "Manage Wormhole settings" link.
|
||||
let manage_settings_link = appearance
|
||||
.ui_builder()
|
||||
.link(
|
||||
"Manage Wormhole settings".into(),
|
||||
None,
|
||||
Some(Box::new(|ctx| {
|
||||
ctx.dispatch_typed_action(SshRemoteServerChoiceViewAction::OpenWarpifySettings);
|
||||
ctx.dispatch_typed_action(
|
||||
SshRemoteServerChoiceViewAction::OpenWormholeSettings,
|
||||
);
|
||||
})),
|
||||
self.manage_settings_mouse_state.clone(),
|
||||
)
|
||||
@@ -264,7 +266,7 @@ impl TypedActionView for SshRemoteServerChoiceView {
|
||||
SshRemoteServerChoiceViewAction::Install => {
|
||||
if self.do_not_ask_again {
|
||||
let mode = SshExtensionInstallMode::AlwaysInstall;
|
||||
WarpifySettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
WormholeSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
if let Err(e) = settings.ssh_extension_install_mode.set_value(mode, ctx) {
|
||||
log::error!("Failed to persist ssh_extension_install_mode: {e}");
|
||||
}
|
||||
@@ -281,7 +283,7 @@ impl TypedActionView for SshRemoteServerChoiceView {
|
||||
SshRemoteServerChoiceViewAction::Skip => {
|
||||
if self.do_not_ask_again {
|
||||
let mode = SshExtensionInstallMode::NeverInstall;
|
||||
WarpifySettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
WormholeSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
if let Err(e) = settings.ssh_extension_install_mode.set_value(mode, ctx) {
|
||||
log::error!("Failed to persist ssh_extension_install_mode: {e}");
|
||||
}
|
||||
@@ -305,8 +307,8 @@ impl TypedActionView for SshRemoteServerChoiceView {
|
||||
);
|
||||
ctx.notify();
|
||||
}
|
||||
SshRemoteServerChoiceViewAction::OpenWarpifySettings => {
|
||||
ctx.emit(SshRemoteServerChoiceViewEvent::OpenWarpifySettings);
|
||||
SshRemoteServerChoiceViewAction::OpenWormholeSettings => {
|
||||
ctx.emit(SshRemoteServerChoiceViewEvent::OpenWormholeSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Banner shown when the remote-server binary check, installation, or connection fails on the remote host.
|
||||
//! We fall back to the existing Warpification behavior and display this banner so the user knows why advanced features are unavailable.
|
||||
//! We fall back to the existing Wormholing behavior and display this banner so the user knows why advanced features are unavailable.
|
||||
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::AnsiColorIdentifier;
|
||||
@@ -15,11 +15,11 @@ use crate::terminal::model::session::SessionId;
|
||||
use crate::ui_components::icons::Icon;
|
||||
use crate::Appearance;
|
||||
|
||||
const BANNER_TITLE: &str = "Couldn't connect to the Warp SSH extension";
|
||||
const BANNER_TITLE: &str = "Couldn't connect to the Wormhole helper";
|
||||
|
||||
const BANNER_BODY: &str =
|
||||
"While advanced features like file browsing and code review are currently \
|
||||
disabled, the rest of your Warpified experience is fully available.";
|
||||
disabled, the rest of your Wormholed experience is fully available.";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SshRemoteServerFailedBannerAction {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! One-time inline banner shown to users who had previously opted into the now-deprecated
|
||||
//! tmux-based SSH warpification flow. It explains that tmux SSH warpification has been turned
|
||||
//! off in favor of Warp's SSH extension (remote server) and links to the docs.
|
||||
//! tmux-based SSH wormholing flow. It explains that tmux SSH wormholing has been turned
|
||||
//! off in favor of Galaxy's SSH extension (remote server).
|
||||
//!
|
||||
//! The banner is shown at most once per affected user: it is gated on the
|
||||
//! `ssh_tmux_deprecation_notice_pending` setting, which is set by a one-time migration and
|
||||
@@ -8,29 +8,25 @@
|
||||
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use warpui::elements::{
|
||||
Align, ConstrainedBox, Container, CrossAxisAlignment, Flex, Hoverable, MainAxisAlignment,
|
||||
ConstrainedBox, Container, CrossAxisAlignment, Flex, Hoverable, MainAxisAlignment,
|
||||
MainAxisSize, MouseStateHandle, ParentElement, Shrinkable, Text,
|
||||
};
|
||||
use warpui::platform::Cursor;
|
||||
use warpui::ui_components::components::{UiComponent, UiComponentStyles};
|
||||
use warpui::{AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext};
|
||||
|
||||
use crate::terminal::model::session::SessionId;
|
||||
use crate::terminal::warpify::render::SSH_DOCS_URL;
|
||||
use crate::ui_components::icons::Icon;
|
||||
use crate::Appearance;
|
||||
|
||||
const BANNER_TITLE: &str = "Tmux SSH warpification has been deprecated";
|
||||
const BANNER_TITLE: &str = "Legacy tmux SSH wormholing has been retired";
|
||||
|
||||
const BANNER_BODY: &str = "Warp now connects to remote sessions using the SSH extension, which is \
|
||||
const BANNER_BODY: &str =
|
||||
"Galaxy now connects to remote sessions using the SSH extension, which is \
|
||||
more robust than the tmux-based flow. The tmux option has been removed.";
|
||||
|
||||
const LEARN_MORE_LABEL: &str = "Learn more";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SshTmuxDeprecationBannerAction {
|
||||
Dismiss,
|
||||
LearnMore,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -40,7 +36,6 @@ pub enum SshTmuxDeprecationBannerEvent {
|
||||
|
||||
pub struct SshTmuxDeprecationBanner {
|
||||
session_id: SessionId,
|
||||
learn_more_mouse_state: MouseStateHandle,
|
||||
close_mouse_state: MouseStateHandle,
|
||||
}
|
||||
|
||||
@@ -48,7 +43,6 @@ impl SshTmuxDeprecationBanner {
|
||||
pub fn new(session_id: SessionId) -> Self {
|
||||
Self {
|
||||
session_id,
|
||||
learn_more_mouse_state: MouseStateHandle::default(),
|
||||
close_mouse_state: MouseStateHandle::default(),
|
||||
}
|
||||
}
|
||||
@@ -72,13 +66,12 @@ impl View for SshTmuxDeprecationBanner {
|
||||
let theme = appearance.theme();
|
||||
let fg_color = theme.foreground().into_solid();
|
||||
let muted_color = internal_colors::neutral_5(theme);
|
||||
let accent_color = theme.accent().into_solid();
|
||||
let font_size = appearance.monospace_font_size();
|
||||
let small_font_size = font_size - 2.;
|
||||
|
||||
// Warp icon to match the other warpification blocks.
|
||||
// Galaxy icon to match the other wormholing blocks.
|
||||
let icon = Container::new(
|
||||
ConstrainedBox::new(Icon::Warp.to_warpui_icon(fg_color.into()).finish())
|
||||
ConstrainedBox::new(Icon::GalaxyLogo.to_warpui_icon(fg_color.into()).finish())
|
||||
.with_width(16.)
|
||||
.with_height(16.)
|
||||
.finish(),
|
||||
@@ -103,26 +96,6 @@ impl View for SshTmuxDeprecationBanner {
|
||||
.with_color(muted_color)
|
||||
.finish();
|
||||
|
||||
let learn_more = appearance
|
||||
.ui_builder()
|
||||
.link(
|
||||
LEARN_MORE_LABEL.into(),
|
||||
None,
|
||||
Some(Box::new(|ctx| {
|
||||
ctx.dispatch_typed_action(SshTmuxDeprecationBannerAction::LearnMore);
|
||||
})),
|
||||
self.learn_more_mouse_state.clone(),
|
||||
)
|
||||
.soft_wrap(false)
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(small_font_size),
|
||||
font_family_id: Some(appearance.ui_font_family()),
|
||||
font_color: Some(accent_color),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
// Close (X) button
|
||||
let close_icon_color = muted_color;
|
||||
let close = Hoverable::new(self.close_mouse_state.clone(), move |_| {
|
||||
@@ -158,26 +131,17 @@ impl View for SshTmuxDeprecationBanner {
|
||||
.with_child(close_container)
|
||||
.finish();
|
||||
|
||||
// Body text + learn more link, indented past the icon to align with the title.
|
||||
// Body text, indented past the icon to align with the title.
|
||||
let body_container = Container::new(body)
|
||||
.with_margin_top(2.)
|
||||
.with_margin_left(24.)
|
||||
.finish();
|
||||
|
||||
// Wrap the link in a left-aligned `Align` so its hover/underline region hugs the
|
||||
// link text instead of stretching to the full banner width (the parent column uses
|
||||
// `CrossAxisAlignment::Stretch`).
|
||||
let learn_more_container = Container::new(Align::new(learn_more).left().finish())
|
||||
.with_margin_top(4.)
|
||||
.with_margin_left(24.)
|
||||
.finish();
|
||||
|
||||
let content = Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(header_row)
|
||||
.with_child(body_container)
|
||||
.with_child(learn_more_container)
|
||||
.finish();
|
||||
|
||||
Container::new(content)
|
||||
@@ -195,10 +159,6 @@ impl TypedActionView for SshTmuxDeprecationBanner {
|
||||
SshTmuxDeprecationBannerAction::Dismiss => {
|
||||
ctx.emit(SshTmuxDeprecationBannerEvent::Dismissed);
|
||||
}
|
||||
SshTmuxDeprecationBannerAction::LearnMore => {
|
||||
ctx.open_url(SSH_DOCS_URL);
|
||||
ctx.emit(SshTmuxDeprecationBannerEvent::Dismissed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::terminal::shared_session::{
|
||||
SharedSessionActionSource, SharedSessionScrollbackType, SharedSessionSource,
|
||||
};
|
||||
use crate::util::image::{infer_mime_type, MAX_IMAGE_SIZE_BYTES_FOR_CLI_AGENT, MIME_SNIFF_BYTES};
|
||||
mod warpify_footer;
|
||||
mod wormhole_footer;
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
@@ -44,7 +44,7 @@ use galaxyui::{
|
||||
};
|
||||
use parking_lot::FairMutex;
|
||||
use pathfinder_color::ColorU;
|
||||
use warpify_footer::{WarpifyFooterView, WarpifyFooterViewEvent};
|
||||
use wormhole_footer::{WormholeFooterView, WormholeFooterViewEvent};
|
||||
|
||||
use super::{RichContentInsertionPosition, TerminalAction, TerminalView};
|
||||
use crate::ai::blocklist::agent_view::agent_view_bg_fill;
|
||||
@@ -267,11 +267,11 @@ impl TerminalView {
|
||||
UseAgentToolbarEvent::HideRichInput => {
|
||||
self.close_cli_agent_rich_input_and_disable_auto_toggle(ctx);
|
||||
}
|
||||
UseAgentToolbarEvent::Warpify => {
|
||||
UseAgentToolbarEvent::Wormhole => {
|
||||
self.hide_use_agent_footer_in_blocklist(ctx);
|
||||
self.handle_action(&TerminalAction::TriggerSubshellBootstrap, ctx);
|
||||
send_telemetry_from_ctx!(
|
||||
TelemetryEvent::WarpifyFooterAcceptedWarpify { is_ssh: false },
|
||||
TelemetryEvent::WormholeFooterAcceptedWormhole { is_ssh: false },
|
||||
ctx
|
||||
);
|
||||
}
|
||||
@@ -295,8 +295,8 @@ impl TerminalView {
|
||||
) -> bool {
|
||||
let ai_settings = AISettings::as_ref(app);
|
||||
|
||||
// If the warpify footer is active, a subshell was detected and we should show the footer.
|
||||
if self.use_agent_footer.as_ref(app).is_warpify_active(app) {
|
||||
// If the wormhole footer is active, a subshell was detected and we should show the footer.
|
||||
if self.use_agent_footer.as_ref(app).is_wormhole_active(app) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -421,7 +421,7 @@ impl TerminalView {
|
||||
|
||||
if !self.model.lock().is_alt_screen_active() {
|
||||
self.use_agent_footer.update(ctx, |footer, ctx| {
|
||||
footer.clear_warpify(ctx);
|
||||
footer.clear_wormhole(ctx);
|
||||
});
|
||||
self.hide_use_agent_footer_in_blocklist(ctx);
|
||||
}
|
||||
@@ -1046,8 +1046,8 @@ pub struct UseAgentToolbar {
|
||||
// Shared agent input footer (renders CLI agent mode when a CLI session is active).
|
||||
agent_input_footer: ViewHandle<AgentInputFooter>,
|
||||
|
||||
// Warpify footer UI (shown when a subshell/SSH command is detected).
|
||||
warpify_footer_view: ViewHandle<WarpifyFooterView>,
|
||||
// Wormhole footer UI (shown when a subshell/SSH command is detected).
|
||||
wormhole_footer_view: ViewHandle<WormholeFooterView>,
|
||||
|
||||
// `true` if the user has dismissed the footer.
|
||||
//
|
||||
@@ -1120,11 +1120,11 @@ impl UseAgentToolbar {
|
||||
me.handle_agent_input_footer_event(event, ctx);
|
||||
});
|
||||
|
||||
let warpify_footer_view =
|
||||
ctx.add_typed_action_view(|ctx| WarpifyFooterView::new(terminal_model.clone(), ctx));
|
||||
let wormhole_footer_view =
|
||||
ctx.add_typed_action_view(|ctx| WormholeFooterView::new(terminal_model.clone(), ctx));
|
||||
|
||||
ctx.subscribe_to_view(&warpify_footer_view, |me, _, event, ctx| {
|
||||
me.handle_warpify_footer_event(event, ctx);
|
||||
ctx.subscribe_to_view(&wormhole_footer_view, |me, _, event, ctx| {
|
||||
me.handle_wormhole_footer_event(event, ctx);
|
||||
});
|
||||
|
||||
ctx.subscribe_to_model(model_event_dispatcher, |me, _, event, ctx| {
|
||||
@@ -1150,7 +1150,7 @@ impl UseAgentToolbar {
|
||||
dismiss_button,
|
||||
dont_show_again_button,
|
||||
agent_input_footer,
|
||||
warpify_footer_view,
|
||||
wormhole_footer_view,
|
||||
terminal_model,
|
||||
did_user_dismiss: false,
|
||||
}
|
||||
@@ -1186,19 +1186,19 @@ impl UseAgentToolbar {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_warpify_footer_event(
|
||||
fn handle_wormhole_footer_event(
|
||||
&mut self,
|
||||
event: &WarpifyFooterViewEvent,
|
||||
event: &WormholeFooterViewEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
WarpifyFooterViewEvent::Warpify => {
|
||||
ctx.emit(UseAgentToolbarEvent::Warpify);
|
||||
WormholeFooterViewEvent::Wormhole => {
|
||||
ctx.emit(UseAgentToolbarEvent::Wormhole);
|
||||
}
|
||||
WarpifyFooterViewEvent::UseAgent => {
|
||||
WormholeFooterViewEvent::UseAgent => {
|
||||
ctx.emit(UseAgentToolbarEvent::UseAgent);
|
||||
}
|
||||
WarpifyFooterViewEvent::Dismiss => {
|
||||
WormholeFooterViewEvent::Dismiss => {
|
||||
ctx.emit(UseAgentToolbarEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
@@ -1207,7 +1207,7 @@ impl UseAgentToolbar {
|
||||
pub(in crate::terminal) fn notify_and_notify_children(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
ctx.notify();
|
||||
self.agent_input_footer.update(ctx, |_, ctx| ctx.notify());
|
||||
self.warpify_footer_view.update(ctx, |_, ctx| ctx.notify());
|
||||
self.wormhole_footer_view.update(ctx, |_, ctx| ctx.notify());
|
||||
self.button.update(ctx, |_, ctx| ctx.notify());
|
||||
self.give_control_back_button
|
||||
.update(ctx, |_, ctx| ctx.notify());
|
||||
@@ -1227,26 +1227,26 @@ impl UseAgentToolbar {
|
||||
.map(|session| session.agent)
|
||||
}
|
||||
|
||||
/// Activates the warpify footer. When active, the footer shows the
|
||||
/// warpify view instead of the CLI agent or regular "Use agent" views.
|
||||
pub(in crate::terminal) fn show_warpify(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.warpify_footer_view.update(ctx, |view, ctx| {
|
||||
/// Activates the wormhole footer. When active, the footer shows the
|
||||
/// wormhole view instead of the CLI agent or regular "Use agent" views.
|
||||
pub(in crate::terminal) fn show_wormhole(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.wormhole_footer_view.update(ctx, |view, ctx| {
|
||||
view.show(ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
/// Deactivates the warpify footer so it reverts to its default behavior.
|
||||
pub(in crate::terminal) fn clear_warpify(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.warpify_footer_view.update(ctx, |view, ctx| {
|
||||
/// Deactivates the wormhole footer so it reverts to its default behavior.
|
||||
pub(in crate::terminal) fn clear_wormhole(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.wormhole_footer_view.update(ctx, |view, ctx| {
|
||||
view.clear(ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
/// Returns whether the warpify footer is currently active.
|
||||
pub(in crate::terminal) fn is_warpify_active(&self, app: &AppContext) -> bool {
|
||||
self.warpify_footer_view.as_ref(app).is_active()
|
||||
/// Returns whether the wormhole footer is currently active.
|
||||
pub(in crate::terminal) fn is_wormhole_active(&self, app: &AppContext) -> bool {
|
||||
self.wormhole_footer_view.as_ref(app).is_active()
|
||||
}
|
||||
|
||||
/// Returns whether there's a current CLI agent (like Claude Code).
|
||||
@@ -1272,8 +1272,8 @@ pub enum UseAgentToolbarEvent {
|
||||
OpenRichInput,
|
||||
/// Hide the rich input editor (same as Escape).
|
||||
HideRichInput,
|
||||
/// User chose to warpify the subshell.
|
||||
Warpify,
|
||||
/// User chose to wormhole the subshell.
|
||||
Wormhole,
|
||||
/// User chose to use the agent.
|
||||
UseAgent,
|
||||
StartRemoteControl {
|
||||
@@ -1292,9 +1292,9 @@ impl View for UseAgentToolbar {
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
// If the warpify footer is active, delegate rendering to the warpify footer view.
|
||||
if self.warpify_footer_view.as_ref(app).is_active() {
|
||||
return ChildView::new(&self.warpify_footer_view).finish();
|
||||
// If the wormhole footer is active, delegate rendering to the wormhole footer view.
|
||||
if self.wormhole_footer_view.as_ref(app).is_active() {
|
||||
return ChildView::new(&self.wormhole_footer_view).finish();
|
||||
}
|
||||
|
||||
// Hide the toolbar entirely when CLI rich input is open,
|
||||
|
||||
+33
-33
@@ -15,28 +15,28 @@ use crate::view_components::action_button::{
|
||||
};
|
||||
|
||||
/// Footer view rendered for detected subshell commands, offering both
|
||||
/// "Warpify" and "Use agent" buttons in a horizontal row.
|
||||
pub(super) struct WarpifyFooterView {
|
||||
/// "Wormhole" and "Use agent" buttons in a horizontal row.
|
||||
pub(super) struct WormholeFooterView {
|
||||
terminal_model: Arc<FairMutex<TerminalModel>>,
|
||||
warpify_button: ViewHandle<ActionButton>,
|
||||
wormhole_button: ViewHandle<ActionButton>,
|
||||
use_agent_button: ViewHandle<ActionButton>,
|
||||
dismiss_button: ViewHandle<ActionButton>,
|
||||
/// Whether the footer is currently offering subshell warpification.
|
||||
/// Whether the footer is currently offering subshell wormholing.
|
||||
is_active: bool,
|
||||
}
|
||||
|
||||
impl WarpifyFooterView {
|
||||
impl WormholeFooterView {
|
||||
pub fn new(terminal_model: Arc<FairMutex<TerminalModel>>, ctx: &mut ViewContext<Self>) -> Self {
|
||||
let button_size = ButtonSize::XSmall;
|
||||
|
||||
let warpify_button = ctx.add_typed_action_view(|_ctx| {
|
||||
let wormhole_button = ctx.add_typed_action_view(|_ctx| {
|
||||
ActionButton::new("Wormhole subshell", AgentFooterButtonTheme::new(None))
|
||||
.with_icon(Icon::Warp)
|
||||
.with_icon(Icon::GalaxyLogo)
|
||||
.with_size(button_size)
|
||||
.with_tooltip("Enable Galaxy shell integration in this session")
|
||||
.with_tooltip_alignment(TooltipAlignment::Left)
|
||||
.on_click(|ctx| {
|
||||
ctx.dispatch_typed_action(WarpifyFooterViewAction::Warpify);
|
||||
ctx.dispatch_typed_action(WormholeFooterViewAction::Wormhole);
|
||||
})
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ impl WarpifyFooterView {
|
||||
.with_tooltip("Ask the Galaxy agent to assist")
|
||||
.with_tooltip_alignment(TooltipAlignment::Left)
|
||||
.on_click(|ctx| {
|
||||
ctx.dispatch_typed_action(WarpifyFooterViewAction::UseAgent);
|
||||
ctx.dispatch_typed_action(WormholeFooterViewAction::UseAgent);
|
||||
})
|
||||
});
|
||||
|
||||
@@ -56,24 +56,24 @@ impl WarpifyFooterView {
|
||||
ActionButton::new("Dismiss", AgentFooterButtonTheme::new(None))
|
||||
.with_size(button_size)
|
||||
.on_click(|ctx| {
|
||||
ctx.dispatch_typed_action(WarpifyFooterViewAction::Dismiss);
|
||||
ctx.dispatch_typed_action(WormholeFooterViewAction::Dismiss);
|
||||
})
|
||||
});
|
||||
|
||||
Self {
|
||||
terminal_model,
|
||||
warpify_button,
|
||||
wormhole_button,
|
||||
use_agent_button,
|
||||
dismiss_button,
|
||||
is_active: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Activates the footer so it offers subshell warpification.
|
||||
/// Activates the footer so it offers subshell wormholing.
|
||||
pub fn show(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.warpify_button.update(ctx, |button, ctx| {
|
||||
self.wormhole_button.update(ctx, |button, ctx| {
|
||||
button.set_keybinding(
|
||||
Some(KeystrokeSource::Binding("terminal:warpify_subshell")),
|
||||
Some(KeystrokeSource::Binding("terminal:wormhole_subshell")),
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
@@ -81,7 +81,7 @@ impl WarpifyFooterView {
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
/// Returns whether the footer is currently offering subshell warpification.
|
||||
/// Returns whether the footer is currently offering subshell wormholing.
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.is_active
|
||||
}
|
||||
@@ -89,7 +89,7 @@ impl WarpifyFooterView {
|
||||
/// Deactivates the footer.
|
||||
pub fn clear(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.is_active = false;
|
||||
self.warpify_button.update(ctx, |button, ctx| {
|
||||
self.wormhole_button.update(ctx, |button, ctx| {
|
||||
button.set_keybinding(None, ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
@@ -97,25 +97,25 @@ impl WarpifyFooterView {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum WarpifyFooterViewAction {
|
||||
Warpify,
|
||||
pub enum WormholeFooterViewAction {
|
||||
Wormhole,
|
||||
UseAgent,
|
||||
Dismiss,
|
||||
}
|
||||
|
||||
pub enum WarpifyFooterViewEvent {
|
||||
Warpify,
|
||||
pub enum WormholeFooterViewEvent {
|
||||
Wormhole,
|
||||
UseAgent,
|
||||
Dismiss,
|
||||
}
|
||||
|
||||
impl Entity for WarpifyFooterView {
|
||||
type Event = WarpifyFooterViewEvent;
|
||||
impl Entity for WormholeFooterView {
|
||||
type Event = WormholeFooterViewEvent;
|
||||
}
|
||||
|
||||
impl View for WarpifyFooterView {
|
||||
impl View for WormholeFooterView {
|
||||
fn ui_name() -> &'static str {
|
||||
"WarpifyFooterView"
|
||||
"WormholeFooterView"
|
||||
}
|
||||
|
||||
fn render(&self, _app: &AppContext) -> Box<dyn Element> {
|
||||
@@ -125,7 +125,7 @@ impl View for WarpifyFooterView {
|
||||
.with_spacing(4.)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(ChildView::new(&self.warpify_button).finish())
|
||||
.with_child(ChildView::new(&self.wormhole_button).finish())
|
||||
.with_child(ChildView::new(&self.use_agent_button).finish())
|
||||
.with_child(Expanded::new(1., Empty::new().finish()).finish())
|
||||
.with_child(ChildView::new(&self.dismiss_button).finish());
|
||||
@@ -144,24 +144,24 @@ impl View for WarpifyFooterView {
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for WarpifyFooterView {
|
||||
type Action = WarpifyFooterViewAction;
|
||||
impl TypedActionView for WormholeFooterView {
|
||||
type Action = WormholeFooterViewAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
WarpifyFooterViewAction::Warpify => {
|
||||
WormholeFooterViewAction::Wormhole => {
|
||||
if self.is_active {
|
||||
self.clear(ctx);
|
||||
ctx.emit(WarpifyFooterViewEvent::Warpify);
|
||||
ctx.emit(WormholeFooterViewEvent::Wormhole);
|
||||
}
|
||||
}
|
||||
WarpifyFooterViewAction::UseAgent => {
|
||||
WormholeFooterViewAction::UseAgent => {
|
||||
self.clear(ctx);
|
||||
ctx.emit(WarpifyFooterViewEvent::UseAgent);
|
||||
ctx.emit(WormholeFooterViewEvent::UseAgent);
|
||||
}
|
||||
WarpifyFooterViewAction::Dismiss => {
|
||||
WormholeFooterViewAction::Dismiss => {
|
||||
self.clear(ctx);
|
||||
ctx.emit(WarpifyFooterViewEvent::Dismiss);
|
||||
ctx.emit(WormholeFooterViewEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,6 @@ use crate::terminal::model::terminal_model::SubshellInitializationInfo;
|
||||
use crate::terminal::shell::ShellType;
|
||||
use crate::ASSETS;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WarpificationSource {
|
||||
Ssh,
|
||||
Subshell,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum SubshellSource {
|
||||
Command(String),
|
||||
@@ -34,7 +28,7 @@ fn get_subshell_bootstrap_success_block_path(shell_type: ShellType) -> Option<&'
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns OutputGrid bytes to be rendered in the hardcoded "Warpified subshell" block that's added
|
||||
/// Returns OutputGrid bytes to be rendered in the hardcoded "Wormholed subshell" block that's added
|
||||
/// to the blocklist upon successful subshell bootstrap.
|
||||
///
|
||||
/// The exact block contents varies based on whether or not the session is local or remote, in
|
||||
@@ -13,7 +13,7 @@ use pathfinder_color::ColorU;
|
||||
use pathfinder_geometry::rect::RectF;
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
|
||||
use super::settings::WarpifySettings;
|
||||
use super::settings::WormholeSettings;
|
||||
use super::SubshellSource;
|
||||
use crate::ai::blocklist::inline_action::inline_action_icons;
|
||||
use crate::ui_components::blended_colors;
|
||||
@@ -31,8 +31,6 @@ const WARP_DRIVE_ENV_VAR_COLLECTION_ICON_COLOR: u32 = 0xC464FFFF;
|
||||
const ICON_MARGIN: f32 = 4.;
|
||||
const TERMINAL_ICON: &str = "bundled/svg/terminal.svg";
|
||||
pub const HORIZONTAL_TEXT_MARGIN: f32 = 20.;
|
||||
pub const SSH_DOCS_URL: &str = "https://docs.warp.dev/terminal/warpify/ssh";
|
||||
pub const SUBSHELL_DOCS_URL: &str = "https://docs.warp.dev/terminal/warpify/subshells";
|
||||
|
||||
/// Errored blocks have a red stripe, and subshells have a gray one.
|
||||
pub const LEFT_STRIPE_WIDTH: f32 = 5.;
|
||||
@@ -92,7 +90,7 @@ fn green_check_icon(appearance: &Appearance, size: f32) -> Box<dyn Element> {
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// UI helper to render the ssh command that caused the warpification prompt.
|
||||
/// UI helper to render the ssh command that caused the wormholing prompt.
|
||||
pub fn build_command_row(
|
||||
command: String,
|
||||
theme: &GalaxyTheme,
|
||||
@@ -164,21 +162,21 @@ pub fn description_row(
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Renders a "Never Warpify this host" link or nothing.
|
||||
pub fn render_never_warpify_ssh_link(
|
||||
/// Renders a "Never Wormhole this host" link or nothing.
|
||||
pub fn render_never_wormhole_ssh_link(
|
||||
ssh_host: &Option<String>,
|
||||
app: &AppContext,
|
||||
appearance: &Appearance,
|
||||
mouse_state_handle: MouseStateHandle,
|
||||
on_never_warpify: fn(&mut EventContext<'_>, ssh_host: String),
|
||||
on_never_wormhole: fn(&mut EventContext<'_>, ssh_host: String),
|
||||
) -> Option<Box<dyn Element>> {
|
||||
let Some(ssh_host) = ssh_host else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let settings = WarpifySettings::handle(app);
|
||||
let settings = WormholeSettings::handle(app);
|
||||
if settings.as_ref(app).is_ssh_host_denylisted(ssh_host) {
|
||||
// Should only happen if user manually attempts to Warpify a denylisted host.
|
||||
// Should only happen if user manually attempts to Wormhole a denylisted host.
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -189,7 +187,7 @@ pub fn render_never_warpify_ssh_link(
|
||||
None,
|
||||
Some(Box::new({
|
||||
let ssh_host = ssh_host.clone();
|
||||
move |ctx| on_never_warpify(ctx, ssh_host.to_owned())
|
||||
move |ctx| on_never_wormhole(ctx, ssh_host.to_owned())
|
||||
})),
|
||||
mouse_state_handle,
|
||||
)
|
||||
@@ -9,65 +9,65 @@ use settings::{
|
||||
};
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
use crate::terminal::ssh::util::{parse_interactive_ssh_command, SshWarpifyCommand};
|
||||
use crate::terminal::ssh::util::{parse_interactive_ssh_command, SshWormholeCommand};
|
||||
|
||||
// Cannot directly use Vec<Regex> here b/c Regex doesn't impl Eq, Serialize, and Deserialize.
|
||||
maybe_define_setting!(AddedSubshellCommands, group: WarpifySettings, {
|
||||
maybe_define_setting!(AddedSubshellCommands, group: WormholeSettings, {
|
||||
type: Vec<String>,
|
||||
default: Vec::new(),
|
||||
supported_platforms: SupportedPlatforms::ALL,
|
||||
sync_to_cloud: SyncToCloud::Never,
|
||||
private: false,
|
||||
toml_path: "warpify.subshells.added_subshell_commands",
|
||||
toml_path: "wormhole.subshells.added_subshell_commands",
|
||||
description: "Additional regex patterns for commands that should be recognized as subshells.",
|
||||
});
|
||||
|
||||
maybe_define_setting!(SubshellCommandsDenylist, group: WarpifySettings, {
|
||||
maybe_define_setting!(SubshellCommandsDenylist, group: WormholeSettings, {
|
||||
type: Vec<String>,
|
||||
default: Vec::new(),
|
||||
supported_platforms: SupportedPlatforms::ALL,
|
||||
sync_to_cloud: SyncToCloud::Never,
|
||||
private: false,
|
||||
toml_path: "warpify.subshells.subshell_commands_denylist",
|
||||
description: "Commands that should not trigger the subshell warpification prompt.",
|
||||
toml_path: "wormhole.subshells.subshell_commands_denylist",
|
||||
description: "Commands that should not trigger the subshell wormholing prompt.",
|
||||
});
|
||||
|
||||
maybe_define_setting!(SshHostsDenylist, group: WarpifySettings, {
|
||||
maybe_define_setting!(SshHostsDenylist, group: WormholeSettings, {
|
||||
type: Vec<String>,
|
||||
default: Vec::new(),
|
||||
supported_platforms: SupportedPlatforms::ALL,
|
||||
sync_to_cloud: SyncToCloud::Never,
|
||||
private: false,
|
||||
toml_path: "warpify.ssh.ssh_hosts_denylist",
|
||||
description: "SSH hosts that should not trigger the warpification prompt.",
|
||||
toml_path: "wormhole.ssh.ssh_hosts_denylist",
|
||||
description: "SSH hosts that should not trigger the wormholing prompt.",
|
||||
});
|
||||
|
||||
maybe_define_setting!(EnableSshWarpification, group: WarpifySettings, {
|
||||
maybe_define_setting!(EnableSshWormholing, group: WormholeSettings, {
|
||||
type: bool,
|
||||
default: true,
|
||||
supported_platforms: SupportedPlatforms::ALL,
|
||||
sync_to_cloud: SyncToCloud::Never,
|
||||
private: false,
|
||||
toml_path: "warpify.ssh.enable_ssh_warpification",
|
||||
toml_path: "wormhole.ssh.enable_ssh_wormholing",
|
||||
description: "Whether to enable Galaxy features in SSH sessions.",
|
||||
});
|
||||
|
||||
// NOTE: This setting has been unified into `enable_ssh_warpification` and is no
|
||||
// NOTE: This setting has been unified into `enable_ssh_wormholing` and is no
|
||||
// longer surfaced in the UI or used to gate any behavior. It is retained only
|
||||
// so the one-time migration (see `register`) can read a user's previous value
|
||||
// and forward it to `enable_ssh_warpification`. It can be deleted in a future
|
||||
// and forward it to `enable_ssh_wormholing`. It can be deleted in a future
|
||||
// release once the migration has shipped to all users.
|
||||
// The storage key and TOML path are intentionally kept identical to the old
|
||||
// `SshSettings::enable_ssh_wrapper` field for backward compatibility.
|
||||
maybe_define_setting!(EnableSshWrapper, group: WarpifySettings, {
|
||||
maybe_define_setting!(EnableSshWrapper, group: WormholeSettings, {
|
||||
type: bool,
|
||||
default: true,
|
||||
supported_platforms: SupportedPlatforms::ALL,
|
||||
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes),
|
||||
private: false,
|
||||
storage_key: "EnableSSHWrapper",
|
||||
toml_path: "warpify.ssh.enable_legacy_ssh_wrapper",
|
||||
description: "Deprecated: unified into enable_ssh_warpification. Retained only for one-time migration.",
|
||||
toml_path: "wormhole.ssh.enable_legacy_ssh_wrapper",
|
||||
description: "Deprecated: unified into enable_ssh_wormholing. Retained only for one-time migration.",
|
||||
});
|
||||
|
||||
// NOTE: The tmux-based SSH wrapper is deprecated in favor of the remote-server SSH
|
||||
@@ -75,31 +75,31 @@ maybe_define_setting!(EnableSshWrapper, group: WarpifySettings, {
|
||||
// it is retained only so the one-time deprecation migration (see `register`) can read a
|
||||
// user's previous opt-in and reset it. It can be deleted in a future release once the
|
||||
// migration has shipped to all users.
|
||||
maybe_define_setting!(UseSshTmuxWrapper, group: WarpifySettings, {
|
||||
maybe_define_setting!(UseSshTmuxWrapper, group: WormholeSettings, {
|
||||
type: bool,
|
||||
default: false,
|
||||
supported_platforms: SupportedPlatforms::OR(SupportedPlatforms::MAC.into(), SupportedPlatforms::LINUX.into()),
|
||||
sync_to_cloud: SyncToCloud::Never,
|
||||
private: false,
|
||||
toml_path: "warpify.ssh.use_ssh_tmux_wrapper",
|
||||
description: "Deprecated: whether to use a tmux-based wrapper for SSH warpification.",
|
||||
toml_path: "wormhole.ssh.use_ssh_tmux_wrapper",
|
||||
description: "Deprecated: whether to use a tmux-based wrapper for SSH wormholing.",
|
||||
});
|
||||
|
||||
// When set, the user previously opted into the now-deprecated tmux SSH wrapper and should
|
||||
// be shown a one-time inline banner pointing them to the remote-server SSH extension on
|
||||
// their next interactive SSH session. Set by the migration in `register`; cleared once the
|
||||
// banner has been shown.
|
||||
maybe_define_setting!(SshTmuxDeprecationNoticePending, group: WarpifySettings, {
|
||||
maybe_define_setting!(SshTmuxDeprecationNoticePending, group: WormholeSettings, {
|
||||
type: bool,
|
||||
default: false,
|
||||
supported_platforms: SupportedPlatforms::OR(SupportedPlatforms::MAC.into(), SupportedPlatforms::LINUX.into()),
|
||||
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes),
|
||||
private: false,
|
||||
toml_path: "warpify.ssh.ssh_tmux_deprecation_notice_pending",
|
||||
toml_path: "wormhole.ssh.ssh_tmux_deprecation_notice_pending",
|
||||
description: "Internal: whether to show the one-time tmux SSH deprecation notice.",
|
||||
});
|
||||
|
||||
/// Controls how Warp handles the SSH extension (remote server binary) when connecting
|
||||
/// Controls how Galaxy handles the SSH extension (remote server binary) when connecting
|
||||
/// to a remote host that does not already have it installed.
|
||||
#[derive(
|
||||
Default,
|
||||
@@ -115,7 +115,7 @@ maybe_define_setting!(SshTmuxDeprecationNoticePending, group: WarpifySettings, {
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[schemars(
|
||||
description = "Controls SSH extension installation behavior.",
|
||||
description = "Controls Wormhole helper installation behavior.",
|
||||
rename_all = "snake_case"
|
||||
)]
|
||||
pub enum SshExtensionInstallMode {
|
||||
@@ -124,18 +124,18 @@ pub enum SshExtensionInstallMode {
|
||||
AlwaysAsk,
|
||||
/// Automatically install and connect without prompting.
|
||||
AlwaysInstall,
|
||||
/// Never install; fall back to wrapper-only SSH warpification.
|
||||
/// Never install; fall back to wrapper-only SSH wormholing.
|
||||
NeverInstall,
|
||||
}
|
||||
|
||||
maybe_define_setting!(SshExtensionInstallModeSetting, group: WarpifySettings, {
|
||||
maybe_define_setting!(SshExtensionInstallModeSetting, group: WormholeSettings, {
|
||||
type: SshExtensionInstallMode,
|
||||
default: SshExtensionInstallMode::default(),
|
||||
supported_platforms: SupportedPlatforms::ALL,
|
||||
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes),
|
||||
private: false,
|
||||
toml_path: "warpify.ssh.ssh_extension_install_mode",
|
||||
description: "Controls SSH extension installation behavior.",
|
||||
toml_path: "wormhole.ssh.ssh_extension_install_mode",
|
||||
description: "Controls Wormhole helper installation behavior.",
|
||||
});
|
||||
|
||||
impl SshExtensionInstallMode {
|
||||
@@ -151,7 +151,7 @@ impl SshExtensionInstallMode {
|
||||
/// Normally we use the define_settings_group! macro for singleton models of settings like this.
|
||||
/// However, this model needs to do some extra processing on the added_subshell_commands and store
|
||||
/// an enriched representation in parsed_added_subshell_commands.
|
||||
pub struct WarpifySettings {
|
||||
pub struct WormholeSettings {
|
||||
/// A list of regexes that users can add to define new subshell-compatible commands. This
|
||||
/// represents the raw, serialized value. Therefore, it is Vec<String>.
|
||||
pub added_subshell_commands: AddedSubshellCommands,
|
||||
@@ -161,9 +161,9 @@ pub struct WarpifySettings {
|
||||
/// needs to be kept up-to-date as added_subshell_commands changes. See the Self::register
|
||||
/// method for how this is done.
|
||||
pub parsed_added_subshell_commands: Vec<Result<Regex, regex::Error>>,
|
||||
/// A list of commands that we shouldn't attempt to warpify. These can be added either b/c the
|
||||
/// A list of commands that we shouldn't attempt to wormhole. These can be added either b/c the
|
||||
/// "don't ask again" button was clicked in the trigger banner, or it was added explicitly on
|
||||
/// the Warpify settings page. This represents the raw, serialized value.
|
||||
/// the Wormhole settings page. This represents the raw, serialized value.
|
||||
pub subshell_command_denylist: SubshellCommandsDenylist,
|
||||
/// This is subshell_command_denylist compiled to actual executable Regex. This is a Result as we
|
||||
/// cannot guarantee the values are valid regex. Even if we prevent them in the UI from entering
|
||||
@@ -172,11 +172,11 @@ pub struct WarpifySettings {
|
||||
/// method for how this is done.
|
||||
pub parsed_subshell_command_denylist: Vec<Result<Regex, regex::Error>>,
|
||||
|
||||
/// A list of hosts that we shouldn't attempt to warpify. This supports regex.
|
||||
/// A list of hosts that we shouldn't attempt to wormhole. This supports regex.
|
||||
/// These can be added either b/c the "don't ask again" button was clicked in the trigger banner,
|
||||
/// or it was added explicitly on the Warpify settings page.
|
||||
/// or it was added explicitly on the Wormhole settings page.
|
||||
/// While this could live in the `SshSettings` group, the custom processing shared with the other
|
||||
/// subshell logic better justifies it living in the `WarpifySettings` group.
|
||||
/// subshell logic better justifies it living in the `WormholeSettings` group.
|
||||
pub ssh_hosts_denylist: SshHostsDenylist,
|
||||
/// This is ssh_hosts_denylist compiled to actual executable Regex. This is a Result as we
|
||||
/// cannot guarantee the values are valid regex. Even if we prevent them in the UI from entering
|
||||
@@ -185,10 +185,10 @@ pub struct WarpifySettings {
|
||||
/// method for how this is done.
|
||||
pub parsed_ssh_hosts_denylist: Vec<Result<Regex, regex::Error>>,
|
||||
|
||||
/// This setting controls whether we should ever warpify ssh sessions.
|
||||
pub enable_ssh_warpification: EnableSshWarpification,
|
||||
/// This setting controls whether we should ever wormhole ssh sessions.
|
||||
pub enable_ssh_wormholing: EnableSshWormholing,
|
||||
|
||||
/// Deprecated: unified into `enable_ssh_warpification`. Retained only so the one-time
|
||||
/// Deprecated: unified into `enable_ssh_wormholing`. Retained only so the one-time
|
||||
/// migration in `register` can read and forward a user's previous opt-out. Not used to
|
||||
/// gate any behavior.
|
||||
pub enable_ssh_wrapper: EnableSshWrapper,
|
||||
@@ -238,7 +238,7 @@ lazy_static! {
|
||||
// Matches commands that spawn a pipenv subshell.
|
||||
PIPENV_SUBSHELL_COMMAND_REGEX.clone(),
|
||||
|
||||
// https://github.com/warpdotdev/Warp/issues/2736
|
||||
// Matches aws-vault's subshell-spawning exec command.
|
||||
Regex::new(r"^aws-vault\s+exec\b").expect("aws-vault regex invalid"),
|
||||
|
||||
// https://flox.dev/docs/reference/command-reference/flox-activate/
|
||||
@@ -251,7 +251,7 @@ lazy_static! {
|
||||
/// define_settings_group! macro, which is the basic template for user-defaults-backed settings.
|
||||
/// I have separated this stuff from the other impl block, which contains the subshell-specific
|
||||
/// logic, because this is basically boilerplate.
|
||||
impl WarpifySettings {
|
||||
impl WormholeSettings {
|
||||
fn new_from_storage(ctx: &mut ModelContext<Self>) -> Self {
|
||||
let added_subshell_commands = AddedSubshellCommands::new_from_storage(ctx);
|
||||
let subshell_command_denylist = SubshellCommandsDenylist::new_from_storage(ctx);
|
||||
@@ -267,7 +267,7 @@ impl WarpifySettings {
|
||||
subshell_command_denylist,
|
||||
parsed_ssh_hosts_denylist: Self::parse_ssh_hosts_denylist(&ssh_hosts_denylist),
|
||||
ssh_hosts_denylist,
|
||||
enable_ssh_warpification: EnableSshWarpification::new_from_storage(ctx),
|
||||
enable_ssh_wormholing: EnableSshWormholing::new_from_storage(ctx),
|
||||
enable_ssh_wrapper: EnableSshWrapper::new_from_storage(ctx),
|
||||
use_ssh_tmux_wrapper: UseSshTmuxWrapper::new_from_storage(ctx),
|
||||
ssh_tmux_deprecation_notice_pending: SshTmuxDeprecationNoticePending::new_from_storage(
|
||||
@@ -294,7 +294,7 @@ impl WarpifySettings {
|
||||
subshell_command_denylist,
|
||||
parsed_ssh_hosts_denylist: Self::parse_ssh_hosts_denylist(&ssh_hosts_denylist),
|
||||
ssh_hosts_denylist,
|
||||
enable_ssh_warpification: EnableSshWarpification::new(None),
|
||||
enable_ssh_wormholing: EnableSshWormholing::new(None),
|
||||
enable_ssh_wrapper: EnableSshWrapper::new(None),
|
||||
use_ssh_tmux_wrapper: UseSshTmuxWrapper::new(None),
|
||||
ssh_tmux_deprecation_notice_pending: SshTmuxDeprecationNoticePending::new(None),
|
||||
@@ -309,37 +309,37 @@ impl WarpifySettings {
|
||||
let handle = ctx.add_singleton_model(Self::new_from_storage);
|
||||
ctx.subscribe_to_model(&handle, |settings, event, ctx| {
|
||||
settings.update(ctx, |me, _| match event {
|
||||
WarpifySettingsChangedEvent::AddedSubshellCommands { .. } => {
|
||||
WormholeSettingsChangedEvent::AddedSubshellCommands { .. } => {
|
||||
me.parsed_added_subshell_commands =
|
||||
Self::parse_added_subshell_commands(&me.added_subshell_commands)
|
||||
}
|
||||
WarpifySettingsChangedEvent::SubshellCommandsDenylist { .. } => {
|
||||
WormholeSettingsChangedEvent::SubshellCommandsDenylist { .. } => {
|
||||
me.parsed_subshell_command_denylist =
|
||||
Self::parse_subshell_command_denylist(&me.subshell_command_denylist)
|
||||
}
|
||||
WarpifySettingsChangedEvent::SshHostsDenylist { .. } => {
|
||||
WormholeSettingsChangedEvent::SshHostsDenylist { .. } => {
|
||||
me.parsed_ssh_hosts_denylist =
|
||||
Self::parse_ssh_hosts_denylist(&me.ssh_hosts_denylist)
|
||||
}
|
||||
WarpifySettingsChangedEvent::EnableSshWarpification { .. } => {}
|
||||
WarpifySettingsChangedEvent::EnableSshWrapper { .. } => {}
|
||||
WarpifySettingsChangedEvent::UseSshTmuxWrapper { .. } => {}
|
||||
WarpifySettingsChangedEvent::SshTmuxDeprecationNoticePending { .. } => {}
|
||||
WarpifySettingsChangedEvent::SshExtensionInstallModeSetting { .. } => {}
|
||||
WormholeSettingsChangedEvent::EnableSshWormholing { .. } => {}
|
||||
WormholeSettingsChangedEvent::EnableSshWrapper { .. } => {}
|
||||
WormholeSettingsChangedEvent::UseSshTmuxWrapper { .. } => {}
|
||||
WormholeSettingsChangedEvent::SshTmuxDeprecationNoticePending { .. } => {}
|
||||
WormholeSettingsChangedEvent::SshExtensionInstallModeSetting { .. } => {}
|
||||
});
|
||||
});
|
||||
|
||||
// One-time migration: if the user had explicitly set the legacy `enable_ssh_wrapper`
|
||||
// setting to `false` (via `warpify.ssh.enable_legacy_ssh_wrapper = false` in their
|
||||
// setting to `false` (via `wormhole.ssh.enable_legacy_ssh_wrapper = false` in their
|
||||
// TOML config or the old `EnableSSHWrapper` storage key), honour that intent by
|
||||
// disabling `enable_ssh_warpification` — the canonical setting that now controls the
|
||||
// disabling `enable_ssh_wormholing` — the canonical setting that now controls the
|
||||
// same behaviour. Resetting `enable_ssh_wrapper` back to its default (`true`) ensures
|
||||
// the migration does not run again on subsequent launches.
|
||||
handle.update(ctx, |me, ctx| {
|
||||
if me.enable_ssh_wrapper.is_value_explicitly_set() && !*me.enable_ssh_wrapper.value() {
|
||||
if let Err(e) = me.enable_ssh_warpification.set_value(false, ctx) {
|
||||
if let Err(e) = me.enable_ssh_wormholing.set_value(false, ctx) {
|
||||
log::error!(
|
||||
"Failed to migrate enable_ssh_wrapper → enable_ssh_warpification: {e}"
|
||||
"Failed to migrate enable_ssh_wrapper → enable_ssh_wormholing: {e}"
|
||||
);
|
||||
}
|
||||
if let Err(e) = me.enable_ssh_wrapper.set_value(true, ctx) {
|
||||
@@ -366,7 +366,7 @@ impl WarpifySettings {
|
||||
});
|
||||
|
||||
register_settings_events!(
|
||||
WarpifySettings,
|
||||
WormholeSettings,
|
||||
added_subshell_commands,
|
||||
AddedSubshellCommands,
|
||||
handle.clone(),
|
||||
@@ -374,7 +374,7 @@ impl WarpifySettings {
|
||||
);
|
||||
|
||||
register_settings_events!(
|
||||
WarpifySettings,
|
||||
WormholeSettings,
|
||||
subshell_command_denylist,
|
||||
SubshellCommandsDenylist,
|
||||
handle.clone(),
|
||||
@@ -382,15 +382,15 @@ impl WarpifySettings {
|
||||
);
|
||||
|
||||
register_settings_events!(
|
||||
WarpifySettings,
|
||||
enable_ssh_warpification,
|
||||
EnableSshWarpification,
|
||||
WormholeSettings,
|
||||
enable_ssh_wormholing,
|
||||
EnableSshWormholing,
|
||||
handle.clone(),
|
||||
ctx
|
||||
);
|
||||
|
||||
register_settings_events!(
|
||||
WarpifySettings,
|
||||
WormholeSettings,
|
||||
enable_ssh_wrapper,
|
||||
EnableSshWrapper,
|
||||
handle.clone(),
|
||||
@@ -398,7 +398,7 @@ impl WarpifySettings {
|
||||
);
|
||||
|
||||
register_settings_events!(
|
||||
WarpifySettings,
|
||||
WormholeSettings,
|
||||
use_ssh_tmux_wrapper,
|
||||
UseSshTmuxWrapper,
|
||||
handle.clone(),
|
||||
@@ -406,7 +406,7 @@ impl WarpifySettings {
|
||||
);
|
||||
|
||||
register_settings_events!(
|
||||
WarpifySettings,
|
||||
WormholeSettings,
|
||||
ssh_tmux_deprecation_notice_pending,
|
||||
SshTmuxDeprecationNoticePending,
|
||||
handle.clone(),
|
||||
@@ -414,7 +414,7 @@ impl WarpifySettings {
|
||||
);
|
||||
|
||||
register_settings_events!(
|
||||
WarpifySettings,
|
||||
WormholeSettings,
|
||||
ssh_extension_install_mode,
|
||||
SshExtensionInstallModeSetting,
|
||||
handle.clone(),
|
||||
@@ -422,7 +422,7 @@ impl WarpifySettings {
|
||||
);
|
||||
|
||||
register_settings_events!(
|
||||
WarpifySettings,
|
||||
WormholeSettings,
|
||||
ssh_hosts_denylist,
|
||||
SshHostsDenylist,
|
||||
handle,
|
||||
@@ -432,9 +432,9 @@ impl WarpifySettings {
|
||||
}
|
||||
|
||||
/// This is also something that would normally be generated by
|
||||
/// define_settings_group!(WarpifySettings). Since we didn't use that macro we define it manually
|
||||
/// define_settings_group!(WormholeSettings). Since we didn't use that macro we define it manually
|
||||
/// here. It's the event emitted by the setter methods when a setting value changes.
|
||||
pub enum WarpifySettingsChangedEvent {
|
||||
pub enum WormholeSettingsChangedEvent {
|
||||
AddedSubshellCommands {
|
||||
change_event_reason: ChangeEventReason,
|
||||
},
|
||||
@@ -444,7 +444,7 @@ pub enum WarpifySettingsChangedEvent {
|
||||
SshHostsDenylist {
|
||||
change_event_reason: ChangeEventReason,
|
||||
},
|
||||
EnableSshWarpification {
|
||||
EnableSshWormholing {
|
||||
change_event_reason: ChangeEventReason,
|
||||
},
|
||||
EnableSshWrapper {
|
||||
@@ -461,15 +461,15 @@ pub enum WarpifySettingsChangedEvent {
|
||||
},
|
||||
}
|
||||
|
||||
impl Entity for WarpifySettings {
|
||||
type Event = WarpifySettingsChangedEvent;
|
||||
impl Entity for WormholeSettings {
|
||||
type Event = WormholeSettingsChangedEvent;
|
||||
}
|
||||
|
||||
impl SingletonEntity for WarpifySettings {}
|
||||
impl SingletonEntity for WormholeSettings {}
|
||||
|
||||
/// This is the other impl block for this model. This one contains the actual subshell-specific
|
||||
/// logic.
|
||||
impl WarpifySettings {
|
||||
impl WormholeSettings {
|
||||
fn is_built_in_subshell_match(command: &str) -> bool {
|
||||
for command_regex in SUBSHELL_COMMAND_REGEXES.iter() {
|
||||
if command_regex.is_match(command) {
|
||||
@@ -494,7 +494,7 @@ impl WarpifySettings {
|
||||
return true;
|
||||
}
|
||||
|
||||
if SshWarpifyCommand::matches(command).is_some_and(|command| command.is_ssh_like_command())
|
||||
if SshWormholeCommand::matches(command).is_some_and(|command| command.is_ssh_like_command())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -505,8 +505,8 @@ impl WarpifySettings {
|
||||
}
|
||||
}
|
||||
|
||||
// While in-band generators are our best option for warpifying ssh sessions from powershell, hard-code
|
||||
// the warpify subshell banner to show up.
|
||||
// While in-band generators are our best option for wormholing ssh sessions from powershell, hard-code
|
||||
// the wormhole subshell banner to show up.
|
||||
if matches!(shell_family, ShellFamily::PowerShell)
|
||||
&& parse_interactive_ssh_command(command).is_some()
|
||||
{
|
||||
@@ -602,7 +602,7 @@ impl WarpifySettings {
|
||||
new_added_commands_list.push(command_to_add.trim().to_owned());
|
||||
|
||||
// The set_value method generated by the maybe_define_setting! macro will take
|
||||
// care of emitting the WarpifySettingsChangedEvent::AddedSubshellCommands event to keep
|
||||
// care of emitting the WormholeSettingsChangedEvent::AddedSubshellCommands event to keep
|
||||
// parsed_added_subshell_commands in sync.
|
||||
self.added_subshell_commands
|
||||
.set_value(new_added_commands_list, ctx)
|
||||
@@ -611,7 +611,7 @@ impl WarpifySettings {
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
/// Check if the user has asked us to remember a command and avoid asking to warpify a subshell.
|
||||
/// Check if the user has asked us to remember a command and avoid asking to wormhole a subshell.
|
||||
pub fn is_denylisted_subshell_command(&self, command: &str) -> bool {
|
||||
let command = command.trim();
|
||||
self.parsed_subshell_command_denylist
|
||||
+20
-20
@@ -1,7 +1,7 @@
|
||||
use settings::Setting;
|
||||
use warpui::{App, SingletonEntity};
|
||||
|
||||
use super::WarpifySettings;
|
||||
use super::WormholeSettings;
|
||||
use crate::test_util::settings::initialize_settings_for_tests;
|
||||
|
||||
#[test]
|
||||
@@ -10,12 +10,12 @@ fn test_parsed_subshell_commands_updated_via_self_subscription() {
|
||||
initialize_settings_for_tests(&mut app);
|
||||
|
||||
app.read(|ctx| {
|
||||
assert!(WarpifySettings::as_ref(ctx)
|
||||
assert!(WormholeSettings::as_ref(ctx)
|
||||
.parsed_added_subshell_commands
|
||||
.is_empty());
|
||||
});
|
||||
|
||||
WarpifySettings::handle(&app).update(&mut app, |settings, ctx| {
|
||||
WormholeSettings::handle(&app).update(&mut app, |settings, ctx| {
|
||||
settings
|
||||
.added_subshell_commands
|
||||
.set_value(vec!["^my-custom-shell$".to_string()], ctx)
|
||||
@@ -24,7 +24,7 @@ fn test_parsed_subshell_commands_updated_via_self_subscription() {
|
||||
|
||||
// The parsed field must now contain the compiled regex.
|
||||
app.read(|ctx| {
|
||||
let parsed = &WarpifySettings::as_ref(ctx).parsed_added_subshell_commands;
|
||||
let parsed = &WormholeSettings::as_ref(ctx).parsed_added_subshell_commands;
|
||||
assert_eq!(
|
||||
parsed.len(),
|
||||
1,
|
||||
@@ -41,14 +41,14 @@ fn test_parsed_subshell_commands_updated_via_self_subscription() {
|
||||
|
||||
/// Verify that a user who previously set `enable_legacy_ssh_wrapper = false`
|
||||
/// (old `SshSettings::enable_ssh_wrapper`) has that opt-out forwarded to
|
||||
/// `enable_ssh_warpification` on first launch after the migration.
|
||||
/// `enable_ssh_wormholing` on first launch after the migration.
|
||||
#[test]
|
||||
fn test_enable_ssh_wrapper_false_migrates_to_enable_ssh_warpification_false() {
|
||||
fn test_enable_ssh_wrapper_false_migrates_to_enable_ssh_wormholing_false() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_settings_for_tests(&mut app);
|
||||
|
||||
// Simulate a user who had explicitly opted out of the legacy SSH wrapper.
|
||||
WarpifySettings::handle(&app).update(&mut app, |settings, ctx| {
|
||||
WormholeSettings::handle(&app).update(&mut app, |settings, ctx| {
|
||||
settings
|
||||
.enable_ssh_wrapper
|
||||
.set_value(false, ctx)
|
||||
@@ -63,13 +63,13 @@ fn test_enable_ssh_wrapper_false_migrates_to_enable_ssh_warpification_false() {
|
||||
// Simpler approach: confirm the migration logic produces the right state
|
||||
// by applying it explicitly here.
|
||||
app.update(|ctx| {
|
||||
WarpifySettings::handle(ctx).update(ctx, |me, ctx| {
|
||||
WormholeSettings::handle(ctx).update(ctx, |me, ctx| {
|
||||
if me.enable_ssh_wrapper.is_value_explicitly_set()
|
||||
&& !*me.enable_ssh_wrapper.value()
|
||||
{
|
||||
me.enable_ssh_warpification
|
||||
me.enable_ssh_wormholing
|
||||
.set_value(false, ctx)
|
||||
.expect("migration set enable_ssh_warpification");
|
||||
.expect("migration set enable_ssh_wormholing");
|
||||
me.enable_ssh_wrapper
|
||||
.set_value(true, ctx)
|
||||
.expect("migration reset enable_ssh_wrapper");
|
||||
@@ -78,10 +78,10 @@ fn test_enable_ssh_wrapper_false_migrates_to_enable_ssh_warpification_false() {
|
||||
});
|
||||
|
||||
app.read(|ctx| {
|
||||
let settings = WarpifySettings::as_ref(ctx);
|
||||
let settings = WormholeSettings::as_ref(ctx);
|
||||
assert!(
|
||||
!*settings.enable_ssh_warpification.value(),
|
||||
"enable_ssh_warpification should be false after migration"
|
||||
!*settings.enable_ssh_wormholing.value(),
|
||||
"enable_ssh_wormholing should be false after migration"
|
||||
);
|
||||
// The wrapper is reset to true so the migration condition
|
||||
// (`!*enable_ssh_wrapper.value()`) won't fire again on the next launch.
|
||||
@@ -94,22 +94,22 @@ fn test_enable_ssh_wrapper_false_migrates_to_enable_ssh_warpification_false() {
|
||||
}
|
||||
|
||||
/// Verify that the default state (no legacy setting present) does not
|
||||
/// spuriously disable `enable_ssh_warpification`.
|
||||
/// spuriously disable `enable_ssh_wormholing`.
|
||||
#[test]
|
||||
fn test_enable_ssh_wrapper_default_does_not_affect_enable_ssh_warpification() {
|
||||
fn test_enable_ssh_wrapper_default_does_not_affect_enable_ssh_wormholing() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_settings_for_tests(&mut app);
|
||||
|
||||
app.read(|ctx| {
|
||||
let settings = WarpifySettings::as_ref(ctx);
|
||||
let settings = WormholeSettings::as_ref(ctx);
|
||||
// Neither setting should be explicitly set — both default to true.
|
||||
assert!(
|
||||
!settings.enable_ssh_wrapper.is_value_explicitly_set(),
|
||||
"enable_ssh_wrapper should not be explicitly set in a fresh install"
|
||||
);
|
||||
assert!(
|
||||
*settings.enable_ssh_warpification.value(),
|
||||
"enable_ssh_warpification should remain true when no migration is needed"
|
||||
*settings.enable_ssh_wormholing.value(),
|
||||
"enable_ssh_wormholing should remain true when no migration is needed"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -133,7 +133,7 @@ fn test_wsl_subshell_detection_success() {
|
||||
.iter()
|
||||
.for_each(|cmd| {
|
||||
assert!(
|
||||
WarpifySettings::is_built_in_subshell_match(cmd),
|
||||
WormholeSettings::is_built_in_subshell_match(cmd),
|
||||
"{} failed to match",
|
||||
*cmd
|
||||
)
|
||||
@@ -164,7 +164,7 @@ fn test_wsl_subshell_detection_fail() {
|
||||
.iter()
|
||||
.for_each(|cmd| {
|
||||
assert!(
|
||||
!WarpifySettings::is_built_in_subshell_match(cmd),
|
||||
!WormholeSettings::is_built_in_subshell_match(cmd),
|
||||
"{} accidentally matched",
|
||||
*cmd
|
||||
)
|
||||
+51
-99
@@ -7,14 +7,13 @@ use galaxy_core::ui::theme::GalaxyTheme;
|
||||
use parking_lot::RwLock;
|
||||
use warpui::elements::{
|
||||
Border, Container, CrossAxisAlignment, Flex, Icon, MainAxisAlignment, MainAxisSize,
|
||||
MouseStateHandle, ParentElement, SelectableArea, SelectionHandle, Text,
|
||||
ParentElement, SelectableArea, SelectionHandle, Text,
|
||||
};
|
||||
use warpui::ui_components::components::{UiComponent, UiComponentStyles};
|
||||
use warpui::{AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext};
|
||||
|
||||
use super::render::{HORIZONTAL_TEXT_MARGIN, SSH_DOCS_URL, SUBSHELL_DOCS_URL};
|
||||
use super::settings::WarpifySettings;
|
||||
use super::{render, subshell_bootstrap_success_block_bytes, WarpificationSource};
|
||||
use super::render::HORIZONTAL_TEXT_MARGIN;
|
||||
use super::settings::WormholeSettings;
|
||||
use super::{render, subshell_bootstrap_success_block_bytes};
|
||||
use crate::ai::agent::ProgrammingLanguage;
|
||||
use crate::ai::blocklist::code_block::{render_runnable_code_snippet, CodeSnippetButtonHandles};
|
||||
use crate::appearance::Appearance;
|
||||
@@ -27,20 +26,19 @@ use crate::workspace::WorkspaceAction;
|
||||
const VERTICAL_TEXT_MARGIN: f32 = 16.;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum WarpifySuccessBlockEvent {
|
||||
OpenWarpifySettings,
|
||||
pub enum WormholeSuccessBlockEvent {
|
||||
OpenWormholeSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum WarpifySuccessBlockAction {
|
||||
ClearAutoWarpifySnippet,
|
||||
OpenWarpifySettings,
|
||||
OpenUrl(String),
|
||||
pub enum WormholeSuccessBlockAction {
|
||||
ClearAutoWormholeSnippet,
|
||||
OpenWormholeSettings,
|
||||
}
|
||||
|
||||
struct AutoWarpifySnippet {
|
||||
struct AutoWormholeSnippet {
|
||||
/// On subshell initialization, this will contain the output grid to display,
|
||||
/// containing info like how to auto-warpify the subshell.
|
||||
/// containing info like how to auto-wormhole the subshell.
|
||||
output_grid: Cow<'static, str>,
|
||||
/// The output grid needs to be selectable to allow users to copy the command to their clipboard.
|
||||
selection_handle: SelectionHandle,
|
||||
@@ -52,23 +50,20 @@ struct AutoWarpifySnippet {
|
||||
can_write_to_rc: bool,
|
||||
}
|
||||
|
||||
pub struct WarpifySuccessBlock {
|
||||
source: WarpificationSource,
|
||||
pub struct WormholeSuccessBlock {
|
||||
spawning_command: String,
|
||||
learn_more_link_mouse_states: MouseStateHandle,
|
||||
auto_warpify_snippet: Option<AutoWarpifySnippet>,
|
||||
auto_wormhole_snippet: Option<AutoWormholeSnippet>,
|
||||
}
|
||||
|
||||
impl WarpifySuccessBlock {
|
||||
impl WormholeSuccessBlock {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new(
|
||||
source: WarpificationSource,
|
||||
spawning_command: String,
|
||||
subshell_info: Option<SubshellInitializationInfo>,
|
||||
shell: Shell,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
ctx.subscribe_to_model(&WarpifySettings::handle(ctx), move |_, _, _, ctx| {
|
||||
ctx.subscribe_to_model(&WormholeSettings::handle(ctx), move |_, _, _, ctx| {
|
||||
ctx.notify();
|
||||
});
|
||||
|
||||
@@ -76,17 +71,17 @@ impl WarpifySuccessBlock {
|
||||
// getting the OS to write to the correct RC file.
|
||||
let remote_os = TargetOS::Linux;
|
||||
|
||||
let is_auto_warpify_configured = subshell_info
|
||||
let is_auto_wormhole_configured = subshell_info
|
||||
.as_ref()
|
||||
.map(|info| info.was_triggered_by_rc_file_snippet)
|
||||
.unwrap_or_default();
|
||||
|
||||
let auto_warpify_snippet = if is_auto_warpify_configured {
|
||||
let auto_wormhole_snippet = if is_auto_wormhole_configured {
|
||||
None
|
||||
} else {
|
||||
subshell_info.and_then(|subshell_info| {
|
||||
// If warpification wasn't triggered automatically, show a snippet about
|
||||
// how to automatically warpify.
|
||||
// If wormholing wasn't triggered automatically, show a snippet about
|
||||
// how to automatically wormhole.
|
||||
(!subshell_info.was_triggered_by_rc_file_snippet).then(|| {
|
||||
let (command, is_executable) = subshell_bootstrap_success_block_bytes(
|
||||
&subshell_info,
|
||||
@@ -108,8 +103,8 @@ impl WarpifySuccessBlock {
|
||||
})
|
||||
})
|
||||
};
|
||||
let auto_warpify_snippet = auto_warpify_snippet.map(|(output_grid, can_write_to_rc)| {
|
||||
AutoWarpifySnippet {
|
||||
let auto_wormhole_snippet = auto_wormhole_snippet.map(|(output_grid, can_write_to_rc)| {
|
||||
AutoWormholeSnippet {
|
||||
description: (if !output_grid.is_empty() {
|
||||
"Run the following to automatically Wormhole in the future:"
|
||||
} else {
|
||||
@@ -125,15 +120,13 @@ impl WarpifySuccessBlock {
|
||||
});
|
||||
|
||||
Self {
|
||||
source,
|
||||
learn_more_link_mouse_states: Default::default(),
|
||||
spawning_command,
|
||||
auto_warpify_snippet,
|
||||
auto_wormhole_snippet,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_text(&self) -> Option<String> {
|
||||
self.auto_warpify_snippet
|
||||
self.auto_wormhole_snippet
|
||||
.as_ref()
|
||||
.and_then(|snippet| snippet.selected_text.read().clone())
|
||||
}
|
||||
@@ -156,18 +149,12 @@ impl WarpifySuccessBlock {
|
||||
) -> Box<dyn Element> {
|
||||
let header_contents = render::build_header_row(
|
||||
"Session Wormholed",
|
||||
Icon::new(UiIcon::Warp.into(), theme.active_ui_detail()),
|
||||
Icon::new(UiIcon::GalaxyLogo.into(), theme.active_ui_detail()),
|
||||
theme,
|
||||
appearance,
|
||||
)
|
||||
.with_margin_right(8.)
|
||||
.finish();
|
||||
let header_contents = Container::new(
|
||||
Flex::row()
|
||||
.with_children([header_contents, self.render_learn_more_link(appearance)])
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
Container::new(
|
||||
Flex::row()
|
||||
@@ -182,45 +169,13 @@ impl WarpifySuccessBlock {
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_learn_more_link(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let url = match self.source {
|
||||
WarpificationSource::Ssh => SSH_DOCS_URL,
|
||||
WarpificationSource::Subshell => SUBSHELL_DOCS_URL,
|
||||
};
|
||||
|
||||
let font_family_id = appearance.monospace_font_family();
|
||||
let font_size = appearance.monospace_font_size();
|
||||
appearance
|
||||
.ui_builder()
|
||||
.link(
|
||||
"Learn more".into(),
|
||||
None,
|
||||
Some(Box::new({
|
||||
move |ctx| {
|
||||
ctx.dispatch_typed_action(WarpifySuccessBlockAction::OpenUrl(
|
||||
url.to_owned(),
|
||||
));
|
||||
}
|
||||
})),
|
||||
self.learn_more_link_mouse_states.clone(),
|
||||
)
|
||||
.soft_wrap(false)
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(font_size),
|
||||
font_family_id: Some(font_family_id),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish()
|
||||
/// Fired when a block ends and we are not in a Wormholed session.
|
||||
pub fn on_wormholed_session_complete(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.clear_auto_wormhole_snippet(ctx);
|
||||
}
|
||||
|
||||
/// Fired when a block ends and we are not in a Warpified session.
|
||||
pub fn on_warpified_session_complete(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.clear_auto_warpify_snippet(ctx);
|
||||
}
|
||||
|
||||
pub fn clear_auto_warpify_snippet(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.auto_warpify_snippet = None;
|
||||
pub fn clear_auto_wormhole_snippet(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.auto_wormhole_snippet = None;
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
@@ -231,16 +186,16 @@ impl WarpifySuccessBlock {
|
||||
appearance: &Appearance,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
let theme = appearance.theme();
|
||||
let auto_warpify_snippet = self.auto_warpify_snippet.as_ref()?;
|
||||
let auto_wormhole_snippet = self.auto_wormhole_snippet.as_ref()?;
|
||||
|
||||
if auto_warpify_snippet.output_grid.is_empty() {
|
||||
if auto_wormhole_snippet.output_grid.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let shell_language = ProgrammingLanguage::Shell(auto_warpify_snippet.shell_type);
|
||||
let shell_language = ProgrammingLanguage::Shell(auto_wormhole_snippet.shell_type);
|
||||
let runnable_command = render_runnable_code_snippet(
|
||||
&auto_warpify_snippet.output_grid,
|
||||
if auto_warpify_snippet.can_write_to_rc {
|
||||
&auto_wormhole_snippet.output_grid,
|
||||
if auto_wormhole_snippet.can_write_to_rc {
|
||||
Some(&shell_language)
|
||||
} else {
|
||||
None
|
||||
@@ -251,7 +206,7 @@ impl WarpifySuccessBlock {
|
||||
code_snippet.to_string(),
|
||||
));
|
||||
|
||||
ctx.dispatch_typed_action(WarpifySuccessBlockAction::ClearAutoWarpifySnippet);
|
||||
ctx.dispatch_typed_action(WormholeSuccessBlockAction::ClearAutoWormholeSnippet);
|
||||
}
|
||||
})),
|
||||
Some(Box::new({
|
||||
@@ -259,19 +214,19 @@ impl WarpifySuccessBlock {
|
||||
ctx.dispatch_typed_action(WorkspaceAction::CopyTextToClipboard(code_snippet));
|
||||
}
|
||||
})),
|
||||
Some(auto_warpify_snippet.code_snippet_handles.clone()),
|
||||
Some(auto_wormhole_snippet.code_snippet_handles.clone()),
|
||||
app,
|
||||
);
|
||||
|
||||
let semantic_selection = SemanticSelection::as_ref(app);
|
||||
let selected_text = auto_warpify_snippet.selected_text.clone();
|
||||
let selected_text = auto_wormhole_snippet.selected_text.clone();
|
||||
|
||||
// TODO(Simon): Implement full selection and copying functionality for the WarpifySuccessBlock.
|
||||
// TODO(Simon): Implement full selection and copying functionality for the WormholeSuccessBlock.
|
||||
// Look to the `EnvVarCollectionBlock` for the existing implementation paradigm. We don't
|
||||
// yet have a robust way of ensuring that every aspect of text selection is implemented
|
||||
// properly, so be extra careful not to miss any details!
|
||||
let output_grid = SelectableArea::new(
|
||||
auto_warpify_snippet.selection_handle.clone(),
|
||||
auto_wormhole_snippet.selection_handle.clone(),
|
||||
move |selection_args, _, _| {
|
||||
*selected_text.write() = selection_args.selection;
|
||||
},
|
||||
@@ -285,7 +240,7 @@ impl WarpifySuccessBlock {
|
||||
.with_child(
|
||||
Container::new(
|
||||
Text::new(
|
||||
auto_warpify_snippet.description.clone(),
|
||||
auto_wormhole_snippet.description.clone(),
|
||||
appearance.monospace_font_family(),
|
||||
appearance.monospace_font_size(),
|
||||
)
|
||||
@@ -307,15 +262,15 @@ impl WarpifySuccessBlock {
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for WarpifySuccessBlock {
|
||||
type Event = WarpifySuccessBlockEvent;
|
||||
impl Entity for WormholeSuccessBlock {
|
||||
type Event = WormholeSuccessBlockEvent;
|
||||
}
|
||||
|
||||
pub const WARPIFY_SUCCESS_BLOCK_VISIBLE_KEY: &str = "WarpifySuccessBlockVisible";
|
||||
pub const WORMHOLE_SUCCESS_BLOCK_VISIBLE_KEY: &str = "WormholeSuccessBlockVisible";
|
||||
|
||||
impl View for WarpifySuccessBlock {
|
||||
impl View for WormholeSuccessBlock {
|
||||
fn ui_name() -> &'static str {
|
||||
"WarpifySuccessBlock"
|
||||
"WormholeSuccessBlock"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
@@ -340,19 +295,16 @@ impl View for WarpifySuccessBlock {
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for WarpifySuccessBlock {
|
||||
type Action = WarpifySuccessBlockAction;
|
||||
impl TypedActionView for WormholeSuccessBlock {
|
||||
type Action = WormholeSuccessBlockAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
WarpifySuccessBlockAction::OpenWarpifySettings => {
|
||||
ctx.emit(WarpifySuccessBlockEvent::OpenWarpifySettings);
|
||||
WormholeSuccessBlockAction::OpenWormholeSettings => {
|
||||
ctx.emit(WormholeSuccessBlockEvent::OpenWormholeSettings);
|
||||
}
|
||||
WarpifySuccessBlockAction::OpenUrl(url) => {
|
||||
ctx.open_url(url);
|
||||
}
|
||||
WarpifySuccessBlockAction::ClearAutoWarpifySnippet => {
|
||||
self.clear_auto_warpify_snippet(ctx);
|
||||
WormholeSuccessBlockAction::ClearAutoWormholeSnippet => {
|
||||
self.clear_auto_wormhole_snippet(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
-39
@@ -6,7 +6,7 @@ use galaxyui::r#async::SpawnedFutureHandle;
|
||||
use galaxyui::{EntityId, SingletonEntity as _, ViewContext, ViewHandle};
|
||||
use parking_lot::FairMutex;
|
||||
|
||||
use super::success_block::WarpifySuccessBlock;
|
||||
use super::success_block::WormholeSuccessBlock;
|
||||
use crate::terminal::model::block::BlockId;
|
||||
use crate::terminal::model::session::SessionId;
|
||||
use crate::terminal::model::terminal_model::SubshellInitializationInfo;
|
||||
@@ -40,8 +40,8 @@ impl SubshellSeparatorState {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SshBlockState {
|
||||
WarpifySuccess {
|
||||
handle: ViewHandle<WarpifySuccessBlock>,
|
||||
WormholeSuccess {
|
||||
handle: ViewHandle<WormholeSuccessBlock>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -52,18 +52,18 @@ impl SshBlockState {
|
||||
|
||||
pub fn get_block_view_id(&self) -> EntityId {
|
||||
match self {
|
||||
SshBlockState::WarpifySuccess { handle, .. } => handle.id(),
|
||||
SshBlockState::WormholeSuccess { handle, .. } => handle.id(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_warpified_session_complete(
|
||||
pub fn on_wormholed_session_complete(
|
||||
&self,
|
||||
ctx: &mut ViewContext<TerminalView>,
|
||||
) -> Option<EntityId> {
|
||||
match self {
|
||||
SshBlockState::WarpifySuccess { handle } => {
|
||||
SshBlockState::WormholeSuccess { handle } => {
|
||||
handle.update(ctx, |block, ctx| {
|
||||
block.on_warpified_session_complete(ctx);
|
||||
block.on_wormholed_session_complete(ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -71,29 +71,29 @@ impl SshBlockState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Temporary state used to trigger Warpification.
|
||||
/// Temporary state used to trigger Wormholing.
|
||||
#[derive(Default)]
|
||||
struct WarpifyTriggerState {
|
||||
struct WormholeTriggerState {
|
||||
block_id: Option<BlockId>,
|
||||
|
||||
/// Lets us abort an attempt to auto warpify if the subshell command
|
||||
/// Lets us abort an attempt to auto wormhole if the subshell command
|
||||
/// hasn't completed.
|
||||
auto_warpify_abort_handle: Option<SpawnedFutureHandle>,
|
||||
auto_wormhole_abort_handle: Option<SpawnedFutureHandle>,
|
||||
|
||||
/// The subshell banner waits 1s before showing. This is to see that the command stays running
|
||||
/// for a while without exiting. We store the abort handle here so that the
|
||||
/// TerminalEvent::BlockCompleted event can abort the banner.
|
||||
subshell_banner_abort_handle: Option<SpawnedFutureHandle>,
|
||||
|
||||
/// The command which may trigger ssh Warpification
|
||||
/// The command which may trigger ssh Wormholing
|
||||
pending_command: Option<String>,
|
||||
/// The Host which may trigger ssh Warpification
|
||||
pending_warpify_ssh_host: Option<String>,
|
||||
/// The Host which may trigger ssh Wormholing
|
||||
pending_wormhole_ssh_host: Option<String>,
|
||||
|
||||
/// Which, if any, SSH block is currently added to the blocklist.
|
||||
ssh_block_state: Option<SshBlockState>,
|
||||
|
||||
ssh_warpify_timeout_handle: Option<SpawnedFutureHandle>,
|
||||
ssh_wormhole_timeout_handle: Option<SpawnedFutureHandle>,
|
||||
|
||||
shell_type: Option<ShellType>,
|
||||
|
||||
@@ -101,17 +101,17 @@ struct WarpifyTriggerState {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WarpifyState {
|
||||
pub struct WormholeState {
|
||||
session_id: Option<SessionId>,
|
||||
|
||||
pending_state: Option<WarpifyTriggerState>,
|
||||
pending_state: Option<WormholeTriggerState>,
|
||||
/// Stores the metadata needed to render any separators above the first block of a subshell.
|
||||
subshell_separator_state: SubshellSeparatorState,
|
||||
/// A unique-enough ID that is used to validate that a timeout is still valid.
|
||||
timeout_id: u8,
|
||||
}
|
||||
|
||||
impl WarpifyState {
|
||||
impl WormholeState {
|
||||
pub fn delete_state(&mut self) {
|
||||
self.pending_state.take();
|
||||
}
|
||||
@@ -180,32 +180,32 @@ impl WarpifyState {
|
||||
.and_then(|state| state.subshell_banner_abort_handle.take())
|
||||
}
|
||||
|
||||
pub fn add_auto_warpify_abort_handle(&mut self, spawned_future_handle: SpawnedFutureHandle) {
|
||||
pub fn add_auto_wormhole_abort_handle(&mut self, spawned_future_handle: SpawnedFutureHandle) {
|
||||
let pending_state = self.pending_state.get_or_insert_with(Default::default);
|
||||
pending_state.auto_warpify_abort_handle = Some(spawned_future_handle);
|
||||
pending_state.auto_wormhole_abort_handle = Some(spawned_future_handle);
|
||||
}
|
||||
|
||||
pub fn abort_auto_warpify(&mut self) {
|
||||
pub fn abort_auto_wormhole(&mut self) {
|
||||
if let Some(abort_handle) = self
|
||||
.pending_state
|
||||
.as_mut()
|
||||
.and_then(|state| state.auto_warpify_abort_handle.take())
|
||||
.and_then(|state| state.auto_wormhole_abort_handle.take())
|
||||
{
|
||||
abort_handle.abort();
|
||||
};
|
||||
}
|
||||
|
||||
pub fn add_ssh_warpify_timeout_handle(&mut self, spawned_future_handle: SpawnedFutureHandle) {
|
||||
pub fn add_ssh_wormhole_timeout_handle(&mut self, spawned_future_handle: SpawnedFutureHandle) {
|
||||
let pending_state = self.pending_state.get_or_insert_with(Default::default);
|
||||
pending_state.ssh_warpify_timeout_handle = Some(spawned_future_handle);
|
||||
pending_state.ssh_wormhole_timeout_handle = Some(spawned_future_handle);
|
||||
}
|
||||
|
||||
pub fn abort_ssh_warpify_timeout(&mut self) {
|
||||
pub fn abort_ssh_wormhole_timeout(&mut self) {
|
||||
self.replace_timeout_id();
|
||||
if let Some(handle) = self
|
||||
.pending_state
|
||||
.as_mut()
|
||||
.and_then(|state| state.ssh_warpify_timeout_handle.take())
|
||||
.and_then(|state| state.ssh_wormhole_timeout_handle.take())
|
||||
{
|
||||
handle.abort();
|
||||
};
|
||||
@@ -231,31 +231,31 @@ impl WarpifyState {
|
||||
pub fn get_pending_ssh_host(&self) -> Option<String> {
|
||||
self.pending_state
|
||||
.as_ref()
|
||||
.and_then(|state: &WarpifyTriggerState| state.pending_warpify_ssh_host.clone())
|
||||
.and_then(|state: &WormholeTriggerState| state.pending_wormhole_ssh_host.clone())
|
||||
}
|
||||
|
||||
pub fn get_pending_ssh_command(&self) -> Option<String> {
|
||||
self.pending_state
|
||||
.as_ref()
|
||||
.and_then(|state: &WarpifyTriggerState| state.pending_command.clone())
|
||||
.and_then(|state: &WormholeTriggerState| state.pending_command.clone())
|
||||
}
|
||||
|
||||
pub fn take_pending_ssh_host(&mut self) -> Option<String> {
|
||||
self.pending_state
|
||||
.as_mut()
|
||||
.and_then(|state: &mut WarpifyTriggerState| state.pending_warpify_ssh_host.take())
|
||||
.and_then(|state: &mut WormholeTriggerState| state.pending_wormhole_ssh_host.take())
|
||||
}
|
||||
|
||||
pub fn clear_pending_ssh_host(&mut self) {
|
||||
if let Some(ref mut pending_state) = self.pending_state.as_mut() {
|
||||
pending_state.pending_warpify_ssh_host = None;
|
||||
pending_state.pending_wormhole_ssh_host = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_pending_ssh_host(&mut self, command: String, ssh_host: Option<String>) {
|
||||
let pending_state = self.pending_state.get_or_insert_with(Default::default);
|
||||
pending_state.pending_command = Some(command);
|
||||
pending_state.pending_warpify_ssh_host = ssh_host;
|
||||
pending_state.pending_wormhole_ssh_host = ssh_host;
|
||||
}
|
||||
|
||||
pub fn set_block_id(&mut self, block_id: BlockId) {
|
||||
@@ -290,10 +290,10 @@ impl WarpifyState {
|
||||
}
|
||||
|
||||
/// Called once whenever we get a local block completed, as opposed to a remote ssh block
|
||||
/// and we have a Warpify Success block.
|
||||
fn on_warpified_session_complete(
|
||||
/// and we have a Wormhole Success block.
|
||||
fn on_wormholed_session_complete(
|
||||
&mut self,
|
||||
state: WarpifyTriggerState,
|
||||
state: WormholeTriggerState,
|
||||
ctx: &mut ViewContext<TerminalView>,
|
||||
) -> Option<EntityId> {
|
||||
self.clear_ssh_block_state();
|
||||
@@ -301,16 +301,16 @@ impl WarpifyState {
|
||||
let Some(block) = &state.ssh_block_state else {
|
||||
return None;
|
||||
};
|
||||
block.on_warpified_session_complete(ctx)
|
||||
block.on_wormholed_session_complete(ctx)
|
||||
}
|
||||
|
||||
pub fn on_warpify_start(&mut self, active_session_id: Option<SessionId>) {
|
||||
pub fn on_wormhole_start(&mut self, active_session_id: Option<SessionId>) {
|
||||
self.session_id = active_session_id;
|
||||
}
|
||||
|
||||
/// Called whenever a block is completed, to determine whether a Warpified session
|
||||
/// Called whenever a block is completed, to determine whether a Wormholed session
|
||||
/// has been completed.
|
||||
pub fn get_completed_warpify_session_id(
|
||||
pub fn get_completed_wormhole_session_id(
|
||||
&mut self,
|
||||
active_session_id: Option<SessionId>,
|
||||
ctx: &mut ViewContext<TerminalView>,
|
||||
@@ -319,7 +319,7 @@ impl WarpifyState {
|
||||
return None;
|
||||
}
|
||||
if let Some(state) = self.pending_state.take() {
|
||||
return self.on_warpified_session_complete(state, ctx);
|
||||
return self.on_wormholed_session_complete(state, ctx);
|
||||
};
|
||||
None
|
||||
}
|
||||
@@ -20,7 +20,7 @@ use crate::server::server_api::ServerApiProvider;
|
||||
use crate::settings::PrivacySettings;
|
||||
use crate::terminal::model::session::{IsSSHWrapperSession, SessionInfo};
|
||||
use crate::terminal::model_events::{ModelEvent, ModelEventDispatcher};
|
||||
use crate::terminal::warpify::settings::{SshExtensionInstallMode, WarpifySettings};
|
||||
use crate::terminal::wormhole::settings::{SshExtensionInstallMode, WormholeSettings};
|
||||
use crate::{send_telemetry_from_ctx, TelemetryEvent};
|
||||
|
||||
/// Per-SSH-init state machine. Encoding the state as an enum makes invalid
|
||||
@@ -310,7 +310,7 @@ impl<T: EventLoopSender> RemoteServerController<T> {
|
||||
});
|
||||
}
|
||||
Ok(false) => {
|
||||
let install_mode = *WarpifySettings::as_ref(ctx)
|
||||
let install_mode = *WormholeSettings::as_ref(ctx)
|
||||
.ssh_extension_install_mode
|
||||
.value();
|
||||
match install_mode {
|
||||
|
||||
@@ -46,7 +46,7 @@ pub fn initialize_settings_for_tests_with_mode(
|
||||
use crate::terminal::session_settings::SessionSettings;
|
||||
use crate::terminal::settings::TerminalSettings;
|
||||
use crate::terminal::shared_session::settings::SharedSessionSettings;
|
||||
use crate::terminal::warpify::settings::WarpifySettings;
|
||||
use crate::terminal::wormhole::settings::WormholeSettings;
|
||||
use crate::terminal::BlockListSettings;
|
||||
use crate::undo_close::UndoCloseSettings;
|
||||
use crate::user_config::WarpConfig;
|
||||
@@ -104,7 +104,7 @@ pub fn initialize_settings_for_tests_with_mode(
|
||||
ScrollSettings::register(app);
|
||||
SelectionSettings::register(app);
|
||||
app.update(|ctx| {
|
||||
WarpifySettings::register(ctx);
|
||||
WormholeSettings::register(ctx);
|
||||
});
|
||||
SessionSettings::register(app);
|
||||
SshSettings::register(app);
|
||||
|
||||
@@ -28,7 +28,7 @@ impl TryFrom<String> for DockerContainerId {
|
||||
))
|
||||
} else if input.chars().any(|c| !c.is_ascii_hexdigit()) {
|
||||
Err(anyhow!(
|
||||
"Could not find valid docker container id to open warpified shell"
|
||||
"Could not find valid docker container id to open wormholed shell"
|
||||
))
|
||||
} else {
|
||||
Ok(DockerContainerId(input))
|
||||
@@ -44,7 +44,7 @@ impl Display for DockerContainerId {
|
||||
|
||||
/// Given a Url with query parameters in the correct format, dispatch an action to create a new tab
|
||||
/// (or open a new window if there is no window), then run a command to open a subshell into the
|
||||
/// specified Docker container, and then warpify that new subshell.
|
||||
/// specified Docker container, and then wormhole that new subshell.
|
||||
pub fn open_docker_container(url: &Url, ctx: &mut AppContext) -> Result<()> {
|
||||
let query_params: HashMap<String, String> = url
|
||||
.query_pairs()
|
||||
@@ -109,7 +109,7 @@ pub fn open_docker_container(url: &Url, ctx: &mut AppContext) -> Result<()> {
|
||||
);
|
||||
|
||||
send_telemetry_from_app_ctx!(
|
||||
TelemetryEvent::OpenAndWarpifyDockerSubshell { shell_type },
|
||||
TelemetryEvent::OpenAndWormholeDockerSubshell { shell_type },
|
||||
ctx
|
||||
);
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ lazy_static! {
|
||||
/// compliant. We weren't always diligent about avoiding bindings that could conflict with
|
||||
/// character codes, unfortunately some bindings on Mac currently conflict with the PTY. We have
|
||||
/// this allowlist to special case these legacy actions for the purposes of binding validation.
|
||||
pub static ref MAC_PTY_NON_COMPLIANT_ACTIONS: HashSet<&'static str> = HashSet::from_iter(["terminal:warpify_subshell", "terminal:open_block_list_context_menu_via_keybinding"]);
|
||||
pub static ref MAC_PTY_NON_COMPLIANT_ACTIONS: HashSet<&'static str> = HashSet::from_iter(["terminal:wormhole_subshell", "terminal:open_block_list_context_menu_via_keybinding"]);
|
||||
|
||||
/// Set of actions on Windows that should be considered valid bindings even though they aren't
|
||||
/// PTY compliant. Windows users expect pasting to work using both `ctrl-v` and `ctrl-shift-v`,
|
||||
|
||||
@@ -9,6 +9,7 @@ use galaxy_util::path::LineAndColumnArg;
|
||||
use galaxyui::AppContext;
|
||||
|
||||
use super::Editor;
|
||||
use super::Editor::*;
|
||||
|
||||
static INSTALLED_EDITOR_METADATA: OnceLock<HashMap<Editor, EditorMetadata>> = OnceLock::new();
|
||||
|
||||
|
||||
@@ -1309,7 +1309,9 @@ pub fn init(app: &mut AppContext) {
|
||||
},
|
||||
)
|
||||
.with_enabled(|| FeatureFlag::AgentMode.is_enabled())
|
||||
.with_context_predicate(id!("Workspace") & id!(flags::IS_ANY_AI_ENABLED))
|
||||
.with_context_predicate(
|
||||
id!("Workspace") & id!(flags::IS_ANY_AI_ENABLED) & !id!("CodeEditorView"),
|
||||
)
|
||||
.with_group(bindings::BindingGroup::WarpAi.as_str())
|
||||
.with_custom_action(CustomAction::NewAgentModePane),
|
||||
EditableBinding::new(
|
||||
@@ -1318,7 +1320,9 @@ pub fn init(app: &mut AppContext) {
|
||||
WorkspaceAction::ToggleAIAssistant,
|
||||
)
|
||||
.with_enabled(|| !FeatureFlag::AgentMode.is_enabled())
|
||||
.with_context_predicate(id!("Workspace") & id!(flags::IS_ANY_AI_ENABLED))
|
||||
.with_context_predicate(
|
||||
id!("Workspace") & id!(flags::IS_ANY_AI_ENABLED) & !id!("CodeEditorView"),
|
||||
)
|
||||
.with_group(bindings::BindingGroup::WarpAi.as_str())
|
||||
// We use the same custom action as AM so that we don't have
|
||||
// two mac menu items for AM vs Warp AI since they are mutually exclusive.
|
||||
@@ -1583,10 +1587,10 @@ fn add_open_setting_pages_as_editable_binding(app: &mut AppContext) {
|
||||
.with_group(bindings::BindingGroup::Settings.as_str())
|
||||
.with_context_predicate(id!("Workspace")),
|
||||
EditableBinding::new(
|
||||
"workspace:show_settings_warpify_page",
|
||||
"workspace:show_settings_wormhole_page",
|
||||
BindingDescription::new("Open Settings: Wormhole")
|
||||
.with_custom_description(bindings::MAC_MENUS_CONTEXT, "Configure Wormhole..."),
|
||||
WorkspaceAction::ShowSettingsPage(SettingsSection::Warpify),
|
||||
WorkspaceAction::ShowSettingsPage(SettingsSection::Wormhole),
|
||||
)
|
||||
.with_group(bindings::BindingGroup::Settings.as_str())
|
||||
.with_context_predicate(id!("Workspace")),
|
||||
|
||||
@@ -418,7 +418,7 @@ use crate::terminal::view::{
|
||||
OnboardingIntention, OnboardingVersion, SyncEvent, SyncInputType, TerminalAction,
|
||||
NOTIFICATIONS_TROUBLESHOOT_URL,
|
||||
};
|
||||
use crate::terminal::warpify::settings::WarpifySettings;
|
||||
use crate::terminal::wormhole::settings::WormholeSettings;
|
||||
use crate::terminal::{self, BlockListSettings, SizeInfo, TerminalModel, TerminalView};
|
||||
use crate::themes::theme::{AnsiColorIdentifier, RespectSystemTheme, ThemeKind};
|
||||
use crate::themes::theme_chooser::{ThemeChooser, ThemeChooserEvent, ThemeChooserMode};
|
||||
@@ -17039,7 +17039,7 @@ impl Workspace {
|
||||
}
|
||||
|
||||
/// Insert the given command that should open a subshell. And set a flag that we should
|
||||
/// automatically bootstrap AKA "warpify" that subshell if we support it. No-op if there is
|
||||
/// automatically bootstrap AKA "wormhole" that subshell if we support it. No-op if there is
|
||||
/// no active terminal session.
|
||||
pub fn insert_subshell_command_and_bootstrap_if_supported(
|
||||
&mut self,
|
||||
@@ -17140,7 +17140,7 @@ impl Workspace {
|
||||
|
||||
// Check whether this remote session has an active remote server
|
||||
// connection (or is in the process of connecting). This is only
|
||||
// true for Auto SSH Warpification (mode 1) sessions where
|
||||
// true for Auto SSH Wormholing (mode 1) sessions where
|
||||
// `connect_session` was called at `InitShell` time.
|
||||
let has_remote_server = is_remote
|
||||
&& FeatureFlag::SshRemoteServer.is_enabled()
|
||||
@@ -22566,7 +22566,7 @@ impl Workspace {
|
||||
let reporting_setings = AltScreenReporting::as_ref(app);
|
||||
let general_settings = GeneralSettings::as_ref(app);
|
||||
let theme_settings = ThemeSettings::as_ref(app);
|
||||
let warpify_settings = WarpifySettings::as_ref(app);
|
||||
let wormhole_settings = WormholeSettings::as_ref(app);
|
||||
let terminal_settings = TerminalSettings::as_ref(app);
|
||||
let window_settings = WindowSettings::as_ref(app);
|
||||
let pane_settings = PaneSettings::as_ref(app);
|
||||
@@ -22618,8 +22618,8 @@ impl Workspace {
|
||||
.set
|
||||
.insert(flags::SSH_REUSE_CONTROL_MASTER_CONTEXT_FLAG);
|
||||
}
|
||||
if *warpify_settings.enable_ssh_warpification.value() {
|
||||
context.set.insert(flags::SSH_WARPIFICATION_CONTEXT_FLAG);
|
||||
if *wormhole_settings.enable_ssh_wormholing.value() {
|
||||
context.set.insert(flags::SSH_WORMHOLING_CONTEXT_FLAG);
|
||||
}
|
||||
|
||||
if keys_settings.extra_meta_keys.left_alt {
|
||||
|
||||
Reference in New Issue
Block a user