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
-1
View File
@@ -5,7 +5,6 @@ mod mouse;
mod screenshot;
use async_trait::async_trait;
use galaxyui::r#async::Timer;
use galaxyui_core::r#async::Timer;
use pathfinder_geometry::vector::Vector2I;
use x11rb::connection::Connection;
+2 -2
View File
@@ -814,8 +814,8 @@ pub enum FeatureFlag {
/// Enables Custom Inference endpoints for enterprise users.
CustomInferenceEndpointsEnterprise,
/// Replaces the in-block warpification banner with a warpify footer.
WarpifyFooter,
/// Replaces the in-block wormholing banner with a wormhole footer.
WormholeFooter,
/// Enables conversation retrieval via the CLI (oz run conversation get, oz run get --conversation).
ConversationApi,
@@ -1332,8 +1332,8 @@ enum Experiment {
SSH_REMOTE_SERVER_EXPERIMENT
SUGGESTED_CODE_DIFFS_CONTROL
SUGGESTED_CODE_DIFFS_EXPERIMENT
TMUX_SSH_WARPIFICATION_CONTROL
TMUX_SSH_WARPIFICATION_EXPERIMENT
TMUX_SSH_WORMHOLING_CONTROL
TMUX_SSH_WORMHOLING_EXPERIMENT
UPGRADE_TO_PRO_MODAL_CONTROL
UPGRADE_TO_PRO_MODAL_EXPERIMENT_NO_PROMO
UPGRADE_TO_PRO_MODAL_EXPERIMENT_WITH_PROMO
+1 -1
View File
@@ -335,7 +335,7 @@ impl ShellType {
///
/// The returned [`TypedPathBuf`]s are encoded for the target OS rather than the
/// host OS, because the resulting path is rendered into a shell command executed
/// on the target (e.g. via SSH or Auto-Warpify). A plain `PathBuf` would pick the
/// on the target (e.g. via SSH or Auto-Wormhole). A plain `PathBuf` would pick the
/// host's separator and produce strings like `~\.zshrc` on a Windows host when
/// targeting a Unix shell, which the remote shell cannot resolve. Encoding for
/// the target OS lets `TypedPathBuf` enforce the correct separator.
@@ -274,7 +274,7 @@ fn test_should_add_command_to_history() {
/// Regression test for https://github.com/warpdotdev/warp/issues/10474.
///
/// `rc_file_paths` is rendered into a shell command that runs on the *target*
/// (e.g. an SSH remote, or a subshell during Auto-Warpify). The path separator
/// (e.g. an SSH remote, or a subshell during Auto-Wormhole). The path separator
/// must therefore depend on the target OS, not the host that built Warp.
/// Previously this used `PathBuf::join`, which uses the host's separator and
/// produced strings like `~\.zshrc` on a Windows host targeting a Unix shell —
@@ -308,7 +308,7 @@ fn test_rc_file_paths_use_target_os_separator() {
);
}
// On Windows the only Auto-Warpify-supported shell is PowerShell; Unix
// On Windows the only Auto-Wormhole-supported shell is PowerShell; Unix
// shells deliberately return no rc paths.
// The leading separator follows target OS; the literal interior slashes
// are left as-is (PowerShell accepts forward slashes on Windows).
+2 -2
View File
@@ -51,8 +51,8 @@ pub enum Experiment {
SplitCreditCostExperiment,
WindowsLaunchControl,
WindowsLaunchExperiment,
TmuxSshWarpificationControl,
TmuxSshWarpificationExperiment,
TmuxSshWormholingControl,
TmuxSshWormholingExperiment,
UsageBasedPricingControl,
UsageBasedPricingExperiment,
CodebaseContextExperiment,
+1 -1
View File
@@ -24,7 +24,7 @@ use warp::integration_testing::terminal::{
wait_until_bootstrapped_single_pane_for_tab,
};
use warp::terminal::shell::ShellType;
use warp::terminal::warpify::settings::{SshExtensionInstallMode, SshExtensionInstallModeSetting};
use warp::terminal::wormhole::settings::{SshExtensionInstallMode, SshExtensionInstallModeSetting};
use super::{new_builder, Builder};
+2 -2
View File
@@ -17,7 +17,7 @@ use warp::integration_testing::terminal::wait_until_bootstrapped_single_pane_for
use warp::integration_testing::view_getters::single_input_view_for_tab;
use warp::root_view::SubshellCommandArg;
use warp::terminal::shell::ShellType;
use warp::terminal::warpify::settings::AddedSubshellCommands;
use warp::terminal::wormhole::settings::AddedSubshellCommands;
use super::{new_builder, Builder};
use crate::util::skip_if_powershell_core_2303;
@@ -83,7 +83,7 @@ generate_can_bootstrap_remote_subshell_for_shell!(test_can_bootstrap_remote_bash
// generate_can_bootstrap_remote_subshell_for_shell!(test_can_bootstrap_remote_fish_subshell, "fish");
// Test the flow of creating a new window and running a command that should create a subshell and
// automaticall bootstrapping AKA "warpifying" that subshell.
// automaticall bootstrapping AKA "wormholing" that subshell.
pub fn test_can_auto_bootstrap() -> Builder {
const SUBSHELL_COMMAND: &str = "zsh";
+41 -22
View File
@@ -182,21 +182,41 @@ impl JsonRpcService {
request_error_code: i64,
) -> Result<()> {
if let Ok(request) = serde_json::from_str::<AnyRequest>(message) {
let should_ack = matches!(
request.method,
let params = if let Some(params) = request.params {
match serde_json::from_str(params.get()) {
Ok(value) => value,
Err(e) => {
log::warn!("Failed to parse params for {} request: {e}", request.method);
Value::Null
}
}
} else {
Value::Null
};
let acknowledged_result = match request.method {
"workspace/configuration" => {
let item_count = params
.get("items")
.and_then(Value::as_array)
.map_or(0, Vec::len);
Some(Value::Array(vec![serde_json::json!({}); item_count]))
}
"window/workDoneProgress/create"
| "client/registerCapability"
| "client/unregisterCapability"
);
| "client/registerCapability"
| "client/unregisterCapability" => Some(Value::Null),
_ => None,
};
let should_ack = acknowledged_result.is_some();
// Handle specific server -> client requests that we can safely acknowledge.
// Some LSP servers crash if we return an error.
let response = if should_ack {
let response = if let Some(result) = acknowledged_result {
log::debug!("Acknowledging {} request", request.method);
serde_json::json!({
"jsonrpc": JSON_RPC_VERSION,
"id": request.id,
"result": null
"result": result
})
} else {
// For other requests, return an error
@@ -223,21 +243,6 @@ impl JsonRpcService {
.clone();
if let Some(handler) = handler {
let params = if let Some(params) = request.params {
match serde_json::from_str(params.get()) {
Ok(value) => value,
Err(e) => {
log::warn!(
"Failed to parse params for {} request: {e}",
request.method
);
Value::Null
}
}
} else {
Value::Null
};
if let Err(e) = handler(request.method.to_string(), params, request.id) {
log::warn!("Server request handler error for {}: {e}", request.method);
}
@@ -357,6 +362,20 @@ impl JsonRpcService {
Ok(())
}
/// Sends a notification and waits until it has been written to the transport.
///
/// Use this when a later request depends on the server having observed the
/// notification first, such as document synchronization before completion.
pub async fn send_notification_and_wait(&self, method: String, params: Value) -> Result<()> {
let notification = Notification {
jsonrpc: JSON_RPC_VERSION,
method,
params,
};
let content = serde_json::to_string(&notification)?;
self.transport.write(&content).await
}
pub async fn shutdown(&self, timeout: std::time::Duration) -> Result<()> {
self.transport.shutdown(timeout).await
}
+26
View File
@@ -35,6 +35,7 @@ pub enum LanguageId {
JavaScriptReact,
C,
Cpp,
Toml,
}
impl LanguageId {
@@ -55,6 +56,7 @@ impl LanguageId {
// compile_commands.json is present, clangd will use the correct language
// regardless of the languageId we send.
"h" | "H" | "hh" | "hpp" | "hxx" => Some(Self::Cpp),
"toml" => Some(Self::Toml),
_ => None,
}
}
@@ -72,6 +74,7 @@ impl LanguageId {
LanguageId::JavaScriptReact => "javascriptreact",
LanguageId::C => "c",
LanguageId::Cpp => "cpp",
LanguageId::Toml => "toml",
}
}
@@ -86,6 +89,7 @@ impl LanguageId {
| LanguageId::JavaScript
| LanguageId::JavaScriptReact => LSPServerType::TypeScriptLanguageServer,
LanguageId::C | LanguageId::Cpp => LSPServerType::Clangd,
LanguageId::Toml => LSPServerType::Tombi,
}
}
}
@@ -105,6 +109,8 @@ pub struct LspServerConfig {
client: Arc<http_client::Client>,
/// Optional path relative to the LSP log namespace for server stderr output.
log_relative_path: Option<PathBuf>,
/// Notifications sent immediately after the server completes LSP initialization.
post_initialize_notifications: Vec<(String, serde_json::Value)>,
}
impl fmt::Debug for LspServerConfig {
@@ -115,6 +121,10 @@ impl fmt::Debug for LspServerConfig {
.field("path_env_var", &self.path_env_var)
.field("client_name", &self.client_name)
.field("log_relative_path", &self.log_relative_path)
.field(
"post_initialize_notifications",
&self.post_initialize_notifications,
)
.finish()
}
}
@@ -134,6 +144,7 @@ impl LspServerConfig {
client_name,
client,
log_relative_path: None,
post_initialize_notifications: Vec::new(),
}
}
@@ -147,6 +158,21 @@ impl LspServerConfig {
self.log_relative_path.as_ref()
}
/// Adds a custom notification to send after the language server initializes.
pub fn with_post_initialize_notification(
mut self,
method: impl Into<String>,
params: serde_json::Value,
) -> Self {
self.post_initialize_notifications
.push((method.into(), params));
self
}
pub(crate) fn post_initialize_notifications(&self) -> &[(String, serde_json::Value)] {
&self.post_initialize_notifications
}
/// Returns the initial workspace path.
pub fn initial_workspace(&self) -> &Path {
&self.initial_workspace
+8 -1
View File
@@ -145,6 +145,8 @@ where
pub enum AssetKind {
/// A gzip-compressed file (e.g., `rust-analyzer-aarch64-apple-darwin.gz`)
Gz,
/// A gzip-compressed tar archive (e.g., `tombi-cli-1.4.1-aarch64-apple-darwin.tar.gz`)
TarGz,
/// A zip archive (e.g., `rust-analyzer-x86_64-pc-windows-msvc.zip`)
Zip,
}
@@ -153,7 +155,9 @@ pub enum AssetKind {
impl AssetKind {
/// Determines the asset kind from a file name based on its extension.
pub fn from_filename(filename: &str) -> Option<Self> {
if filename.ends_with(".gz") && !filename.ends_with(".tar.gz") {
if filename.ends_with(".tar.gz") {
Some(AssetKind::TarGz)
} else if filename.ends_with(".gz") {
Some(AssetKind::Gz)
} else if filename.ends_with(".zip") {
Some(AssetKind::Zip)
@@ -263,6 +267,9 @@ pub async fn install_from_github(
let binary_path = install_dir.join(&binary_name);
node_runtime::extract_gz(&bytes, &binary_path).await?;
}
AssetKind::TarGz => {
node_runtime::extract_tar_gz(&bytes, &install_dir)?;
}
AssetKind::Zip => {
if binary_finder.is_some() {
// Extract the full archive when using a custom binary finder
+7
View File
@@ -78,6 +78,7 @@ pub async fn spawn_lsp_service(
logger: Option<SimpleLogger>,
) -> Result<LspServiceInitializationResult> {
let workspace_root = config.initial_workspace().to_path_buf();
let post_initialize_notifications = config.post_initialize_notifications().to_vec();
let resolved = match config.command_and_params().await {
Ok(resolved) => resolved,
@@ -119,6 +120,12 @@ pub async fn spawn_lsp_service(
return Err(e);
}
for (method, params) in post_initialize_notifications {
service
.send_custom_notification_and_wait(method, params)
.await?;
}
Ok(LspServiceInitializationResult {
service,
channel: notify_rx,
+37 -1
View File
@@ -16,7 +16,8 @@ use jsonrpc::ServerNotificationEvent;
use lsp_types::notification::{self, Notification};
use lsp_types::{
CompletionItem, CompletionTriggerKind, FormattingOptions, NumberOrString, ProgressParams,
ProgressParamsValue, PublishDiagnosticsParams, Range as LspRange, WorkDoneProgress,
ProgressParamsValue, PublishDiagnosticsParams, Range as LspRange, TextDocumentSyncCapability,
TextDocumentSyncKind, WorkDoneProgress,
};
#[cfg(not(target_arch = "wasm32"))]
use simple_logger::manager::LogManager;
@@ -531,6 +532,41 @@ impl LspServerModel {
})
}
/// Whether the server requires each document change to contain the full document.
pub fn requires_full_document_sync(&self) -> bool {
let Ok(service) = self.service() else {
return false;
};
let Some(sync) = service
.server_capabilities()
.as_ref()
.and_then(|capabilities| capabilities.text_document_sync.as_ref())
else {
return false;
};
match sync {
TextDocumentSyncCapability::Kind(kind) => *kind == TextDocumentSyncKind::FULL,
TextDocumentSyncCapability::Options(options) => {
options.change == Some(TextDocumentSyncKind::FULL)
}
}
}
/// Whether the server can resolve additional details for completion items.
pub fn supports_completion_resolve(&self) -> bool {
let Ok(service) = self.service() else {
return false;
};
service
.server_capabilities()
.as_ref()
.and_then(|capabilities| capabilities.completion_provider.as_ref())
.and_then(|options| options.resolve_provider)
.unwrap_or(false)
}
pub fn did_change_watched_files(&self, events: Vec<WatchedFileChangeEvent>) -> Result<()> {
let service = self.service()?;
service.workspace_watched_files_changed(events)
+1
View File
@@ -2,4 +2,5 @@ pub mod clangd;
pub mod go;
pub mod pyright;
pub mod rust;
pub mod tombi;
pub mod typescript_language_server;
+212
View File
@@ -0,0 +1,212 @@
use std::path::Path;
#[cfg(feature = "local_fs")]
use std::path::PathBuf;
use std::sync::Arc;
use async_trait::async_trait;
#[cfg(feature = "local_fs")]
use crate::install::{
fetch_latest_metadata_from_github_dynamic_asset, install_from_github, AssetKind,
};
use crate::language_server_candidate::{LanguageServerCandidate, LanguageServerMetadata};
use crate::CommandBuilder;
#[cfg(feature = "local_fs")]
const SERVER_NAME: &str = "tombi";
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
pub struct TombiCandidate {
client: Arc<http_client::Client>,
}
impl TombiCandidate {
pub fn new(client: Arc<http_client::Client>) -> Self {
Self { client }
}
#[cfg(feature = "local_fs")]
pub async fn find_installed_binary_in_data_dir() -> Option<PathBuf> {
let install_root = galaxy_core::paths::data_dir().join(SERVER_NAME);
let entries = std::fs::read_dir(install_root).ok()?;
for entry in entries.flatten() {
let version_dir = entry.path();
if !version_dir.is_dir() {
continue;
}
let Some(binary_path) = find_binary(&version_dir) else {
continue;
};
if binary_is_working(&binary_path).await {
return Some(binary_path);
}
}
None
}
}
#[cfg(feature = "local_fs")]
fn asset_target() -> anyhow::Result<&'static str> {
match (std::env::consts::OS, std::env::consts::ARCH) {
("macos", "aarch64") => Ok("aarch64-apple-darwin"),
("macos", "x86_64") => Ok("x86_64-apple-darwin"),
("linux", "aarch64") => Ok("aarch64-unknown-linux-musl"),
("linux", "x86_64") => Ok("x86_64-unknown-linux-musl"),
("windows", "aarch64") => Ok("aarch64-pc-windows-msvc"),
("windows", "x86_64") => Ok("x86_64-pc-windows-msvc"),
(os, arch) => anyhow::bail!("Unsupported platform for Tombi: {os}/{arch}"),
}
}
#[cfg(feature = "local_fs")]
fn asset_name(tag: &str) -> anyhow::Result<String> {
let version = tag.strip_prefix('v').unwrap_or(tag);
let extension = if cfg!(windows) { "zip" } else { "tar.gz" };
Ok(format!(
"tombi-cli-{version}-{}.{extension}",
asset_target()?
))
}
#[cfg(feature = "local_fs")]
fn find_binary(install_dir: &Path) -> Option<PathBuf> {
let binary_name = if cfg!(windows) {
"tombi.exe"
} else {
SERVER_NAME
};
let direct_path = install_dir.join(binary_name);
if direct_path.is_file() {
return Some(direct_path);
}
std::fs::read_dir(install_dir)
.ok()?
.flatten()
.filter_map(|entry| {
let path = entry.path();
path.is_dir().then(|| path.join(binary_name))
})
.find(|path| path.is_file())
}
#[cfg(feature = "local_fs")]
async fn binary_is_working(binary_path: &Path) -> bool {
if !binary_path.is_file() {
return false;
}
let mut command = command::r#async::Command::new(binary_path);
command.arg("--version");
command
.output()
.await
.map(|output| output.status.success())
.unwrap_or(false)
}
#[async_trait]
#[cfg(feature = "local_fs")]
impl LanguageServerCandidate for TombiCandidate {
async fn should_suggest_for_repo(&self, path: &Path, _executor: &CommandBuilder) -> bool {
let Ok(entries) = std::fs::read_dir(path) else {
return false;
};
entries.flatten().any(|entry| {
let file_path = entry.path();
file_path.is_file()
&& file_path
.extension()
.and_then(|extension| extension.to_str())
== Some("toml")
})
}
async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool {
Self::find_installed_binary_in_data_dir().await.is_some()
}
async fn is_installed_on_path(&self, executor: &CommandBuilder) -> bool {
executor
.command(SERVER_NAME)
.arg("--version")
.output()
.await
.map(|output| output.status.success())
.unwrap_or(false)
}
async fn install(
&self,
metadata: LanguageServerMetadata,
_executor: &CommandBuilder,
) -> anyhow::Result<()> {
let asset_name = asset_name(&metadata.version)?;
let asset_kind = AssetKind::from_filename(&asset_name)
.ok_or_else(|| anyhow::anyhow!("Unsupported archive format for asset: {asset_name}"))?;
let binary_path = install_from_github(
&self.client,
&metadata,
SERVER_NAME,
asset_kind,
Some(find_binary),
)
.await?;
if !binary_is_working(&binary_path).await {
anyhow::bail!(
"Installed Tombi binary at {} failed its version check",
binary_path.display()
);
}
Ok(())
}
async fn fetch_latest_server_metadata(&self) -> anyhow::Result<LanguageServerMetadata> {
let target = asset_target()?;
let extension = if cfg!(windows) { "zip" } else { "tar.gz" };
fetch_latest_metadata_from_github_dynamic_asset(
&self.client,
"tombi-toml",
"tombi",
|tag| {
let version = tag.strip_prefix('v').unwrap_or(tag);
format!("tombi-cli-{version}-{target}.{extension}")
},
)
.await
}
}
#[async_trait]
#[cfg(not(feature = "local_fs"))]
impl LanguageServerCandidate for TombiCandidate {
async fn should_suggest_for_repo(&self, _path: &Path, _executor: &CommandBuilder) -> bool {
false
}
async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool {
false
}
async fn is_installed_on_path(&self, _executor: &CommandBuilder) -> bool {
false
}
async fn install(
&self,
_metadata: LanguageServerMetadata,
_executor: &CommandBuilder,
) -> anyhow::Result<()> {
anyhow::bail!("Tombi installation is unavailable without local filesystem support")
}
async fn fetch_latest_server_metadata(&self) -> anyhow::Result<LanguageServerMetadata> {
anyhow::bail!("Tombi metadata is unavailable without local filesystem support")
}
}
+25 -4
View File
@@ -170,7 +170,8 @@ impl LspService {
let response = self.send_request::<request::Initialize>(params).await?;
self.server_capabilities = Some(response.capabilities);
self.send_notification::<notification::Initialized>(InitializedParams {})?;
self.send_notification_and_wait::<notification::Initialized>(InitializedParams {})
.await?;
self.subscribe::<notification::Progress>().await;
self.subscribe::<notification::PublishDiagnostics>().await;
@@ -297,6 +298,23 @@ impl LspService {
.send_notification(N::METHOD.to_string(), params)
}
async fn send_notification_and_wait<N: Notification>(&self, params: N::Params) -> Result<()> {
let params = serde_json::to_value(params)?;
self.jsonrpc_service
.send_notification_and_wait(N::METHOD.to_string(), params)
.await
}
pub(crate) async fn send_custom_notification_and_wait(
&self,
method: impl Into<String>,
params: Value,
) -> Result<()> {
self.jsonrpc_service
.send_notification_and_wait(method.into(), params)
.await
}
async fn send_request<R: Request>(&self, params: R::Params) -> Result<R::Result> {
let params = serde_json::to_value(params)?;
let request = self.send_request_internal(R::METHOD.to_string(), params);
@@ -538,7 +556,8 @@ impl<'a> TextDocumentService<'a> {
};
self.service
.send_notification::<notification::DidOpenTextDocument>(did_open_params)
.send_notification_and_wait::<notification::DidOpenTextDocument>(did_open_params)
.await
}
pub async fn did_close(&self, path: &Path) -> Result<()> {
@@ -560,7 +579,8 @@ impl<'a> TextDocumentService<'a> {
};
self.service
.send_notification::<notification::DidCloseTextDocument>(did_close_params)
.send_notification_and_wait::<notification::DidCloseTextDocument>(did_close_params)
.await
}
pub async fn did_change(
@@ -592,7 +612,8 @@ impl<'a> TextDocumentService<'a> {
};
self.service
.send_notification::<notification::DidChangeTextDocument>(did_change_params)
.send_notification_and_wait::<notification::DidChangeTextDocument>(did_change_params)
.await
}
pub async fn definition(
+18 -1
View File
@@ -15,6 +15,7 @@ use crate::servers::clangd::ClangdCandidate;
use crate::servers::go::GoPlsCandidate;
use crate::servers::pyright::PyrightCandidate;
use crate::servers::rust::RustAnalyzerCandidate;
use crate::servers::tombi::TombiCandidate;
use crate::servers::typescript_language_server::TypeScriptLanguageServerCandidate;
#[cfg(not(target_arch = "wasm32"))]
use crate::CommandBuilder;
@@ -45,6 +46,7 @@ pub enum LSPServerType {
Pyright,
TypeScriptLanguageServer,
Clangd,
Tombi,
}
/// Provides server-specific configuration for each LSP server type.
@@ -112,6 +114,12 @@ impl LSPServerType {
binary_path: path,
prepend_args: vec![],
}),
LSPServerType::Tombi => TombiCandidate::find_installed_binary_in_data_dir()
.await
.map(|path| CustomBinaryConfig {
binary_path: path,
prepend_args: vec![],
}),
}
}
@@ -135,6 +143,7 @@ impl LSPServerType {
LSPServerType::Pyright => "pyright-langserver",
LSPServerType::TypeScriptLanguageServer => "typescript-language-server",
LSPServerType::Clangd => "clangd",
LSPServerType::Tombi => "tombi",
}
}
@@ -144,6 +153,7 @@ impl LSPServerType {
match self {
LSPServerType::RustAnalyzer | LSPServerType::GoPls | LSPServerType::Clangd => vec![],
LSPServerType::Pyright | LSPServerType::TypeScriptLanguageServer => vec!["--stdio"],
LSPServerType::Tombi => vec!["lsp", "--offline"],
}
}
@@ -157,6 +167,7 @@ impl LSPServerType {
LSPServerType::Pyright => vec!["--stdio"],
LSPServerType::TypeScriptLanguageServer => vec!["--stdio"],
LSPServerType::Clangd => vec![],
LSPServerType::Tombi => vec!["lsp", "--offline"],
}
}
@@ -175,6 +186,7 @@ impl LSPServerType {
]
}
LSPServerType::Clangd => vec![LanguageId::C, LanguageId::Cpp],
LSPServerType::Tombi => vec![LanguageId::Toml],
}
}
@@ -219,7 +231,11 @@ impl LSPServerType {
)
.await
}
_ => None,
LSPServerType::Tombi => None,
LSPServerType::RustAnalyzer
| LSPServerType::GoPls
| LSPServerType::Pyright
| LSPServerType::Clangd => None,
}
}
@@ -232,6 +248,7 @@ impl LSPServerType {
Box::new(TypeScriptLanguageServerCandidate::new(client))
}
LSPServerType::Clangd => Box::new(ClangdCandidate::new(client)),
LSPServerType::Tombi => Box::new(TombiCandidate::new(client)),
}
}
@@ -93,10 +93,17 @@ tar -xzf "$tmpdir/oz.tar.gz" -C "$tmpdir"
# The executable and its resources are siblings in the artifact. Exclude the
# resources tree from the search: bundled skills may ship companion files
# whose names also start with `oz`.
bin=$(find "$tmpdir" -type f -name 'oz*' ! -name '*.tar.gz' ! -path '*/resources/*' | head -n1)
bin=$(find "$tmpdir" -type f -name '{binary_name}*' ! -name '*.tar.gz' ! -path '*/resources/*' | head -n1)
# Continue accepting legacy archives while existing release infrastructure is
# migrated to Galaxy-named helper artifacts.
if [ -z "$bin" ]; then
bin=$(find "$tmpdir" -type f -name 'oz*' ! -name '*.tar.gz' ! -path '*/resources/*' | head -n1)
fi
if [ -z "$bin" ]; then echo "no binary found in tarball" >&2; exit 1; fi
chmod +x "$bin"
version_marker="$(dirname "$bin")/wormhole-version"
# Install the resources tree at the global, version-independent location
# the daemon reads. `$tmpdir` lives inside `$install_dir`, so the `mv` is a
# same-filesystem rename. Installed before the binary so an interrupted
@@ -110,3 +117,8 @@ if [ -d "$resources" ]; then
fi
mv "$bin" "$install_dir/{binary_name}{version_suffix}"
if [ -f "$version_marker" ]; then
mv "$version_marker" "$install_dir/{binary_name}{version_suffix}.wormhole-version"
else
rm -f "$install_dir/{binary_name}{version_suffix}.wormhole-version"
fi
+4 -1
View File
@@ -1860,7 +1860,10 @@ impl RemoteServerManager {
// surfaced as `None`, which the controller treats as
// inconclusive (fail open).
let preinstall = match &platform {
Some(p) if matches!(p.os, RemoteOs::Linux) => {
Some(p)
if matches!(p.os, RemoteOs::Linux)
&& !crate::setup::uses_static_linux_helper() =>
{
match transport.run_preinstall_check().await {
Ok(r) => Some(r),
Err(e) => {
+12 -1
View File
@@ -507,7 +507,18 @@ pub fn binary_check_command() -> String {
/// the next install overwrites it, and an older daemon that is still
/// running parsed its skills at startup.
pub fn remote_server_removal_command() -> String {
format!("rm -f {}", remote_server_binary())
let binary = remote_server_binary();
format!("rm -f {binary} {binary}.wormhole-version")
}
/// Returns whether this channel ships a statically linked Linux Wormhole
/// helper with the client instead of downloading a glibc-linked CLI artifact.
///
/// Static helpers do not depend on the remote host's libc implementation, so
/// the glibc compatibility gate used by the hosted release channels does not
/// apply to them.
pub fn uses_static_linux_helper() -> bool {
matches!(ChannelState::channel(), Channel::Local | Channel::Oss)
}
/// Returns the version string used to pin remote-server installs on
+4 -4
View File
@@ -53,7 +53,7 @@ pub struct InstallOutcome {
#[derive(Clone, Debug)]
pub struct UserFacingError {
/// Always-visible explanation of what went wrong,
/// e.g. "Failed to install SSH extension".
/// e.g. "Failed to install Wormhole helper".
pub body: String,
/// Optional technical detail shown to the user (stderr,
/// timeout duration, unsupported OS/arch). `None` when the
@@ -77,9 +77,9 @@ impl SetupStage {
match self {
Self::DetectPlatform => "detect remote platform",
Self::PreinstallCheck => "run preinstall check",
Self::CheckBinary => "verify SSH extension",
Self::InstallBinary => "install SSH extension",
Self::Launch => "start SSH extension",
Self::CheckBinary => "verify Wormhole helper",
Self::InstallBinary => "install Wormhole helper",
Self::Launch => "start Wormhole helper",
}
}
}