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:
2026-08-23 13:55:47 -05:00
parent f17642fc62
commit 7c106eecd5
147 changed files with 2208 additions and 1514 deletions
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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,
+5 -5
View File
@@ -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 {
+151
View 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")]
+2
View File
@@ -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 {
+13 -224
View File
@@ -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
View File
@@ -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>> {
+2
View File
@@ -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.
+8
View File
@@ -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,
+1 -1
View File
@@ -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.
+14 -4
View File
@@ -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!(
+8 -12
View File
@@ -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);
}
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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 {}),
+2 -2
View File
@@ -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
View File
@@ -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")]
+4 -4
View File
@@ -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(),
);
}
}
+20 -6
View File
@@ -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!(
+1 -1
View File
@@ -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,
+34 -34
View File
@@ -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 => {
+2 -2
View File
@@ -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 -1
View File
@@ -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: [
+1
View File
@@ -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;
+230
View File
@@ -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)
}
+1 -1
View File
@@ -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.",
},
]
+21 -22
View File
@@ -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);
})
}
}
+4 -4
View File
@@ -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),
+3 -3
View File
@@ -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(),
+2 -2
View File
@@ -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};
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -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.
+3 -3
View File
@@ -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,
}
+2 -2
View File
@@ -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"))
+3 -3
View File
@@ -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) {
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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";
+1 -1
View File
@@ -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};
+10 -9
View File
@@ -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);
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
pub enum RichContentType {
AIBlock,
EnterAgentView,
WarpifySuccessBlock,
WormholeSuccessBlock,
InlineAgentViewHeader,
AgentViewZeroState,
TerminalViewZeroState,
+35 -35
View File
@@ -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 =>
+2 -2
View File
@@ -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;
+5 -5
View File
@@ -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;
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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}:")),
}
}
+3 -5
View File
@@ -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
+50 -50
View File
@@ -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 => {
+30 -47
View File
@@ -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,
};
+12 -12
View File
@@ -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(),
}
+18 -16
View File
@@ -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
View File
@@ -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");
}
+15 -15
View File
@@ -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:?})")
+4 -4
View File
@@ -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,
}
}
}
@@ -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())
+3 -3
View File
@@ -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,
)
+3 -3
View File
@@ -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>,
+1 -1
View File
@@ -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);
}
}
}
}
+36 -36
View File
@@ -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,
@@ -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
@@ -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
)
@@ -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);
}
}
}
@@ -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 {
+2 -2
View File
@@ -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);
+3 -3
View File
@@ -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
);
+1 -1
View File
@@ -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();
+8 -4
View File
@@ -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")),
+6 -6
View File
@@ -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 {