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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -51,8 +51,8 @@ pub enum Experiment {
|
||||
SplitCreditCostExperiment,
|
||||
WindowsLaunchControl,
|
||||
WindowsLaunchExperiment,
|
||||
TmuxSshWarpificationControl,
|
||||
TmuxSshWarpificationExperiment,
|
||||
TmuxSshWormholingControl,
|
||||
TmuxSshWormholingExperiment,
|
||||
UsageBasedPricingControl,
|
||||
UsageBasedPricingExperiment,
|
||||
CodebaseContextExperiment,
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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(¬ification)?;
|
||||
self.transport.write(&content).await
|
||||
}
|
||||
|
||||
pub async fn shutdown(&self, timeout: std::time::Duration) -> Result<()> {
|
||||
self.transport.shutdown(timeout).await
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -2,4 +2,5 @@ pub mod clangd;
|
||||
pub mod go;
|
||||
pub mod pyright;
|
||||
pub mod rust;
|
||||
pub mod tombi;
|
||||
pub mod typescript_language_server;
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user