Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions rust/crates/commands/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,13 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
argument_hint: Some("[list|add|remove]"),
resume_supported: true,
},
SlashCommandSpec {
name: "lsp",
aliases: &[],
summary: "Show or manage LSP server status",
argument_hint: Some("[status|start|stop|list]"),
resume_supported: true,
},
SlashCommandSpec {
name: "team",
aliases: &[],
Expand Down Expand Up @@ -1180,6 +1187,10 @@ pub enum SlashCommand {
count: Option<String>,
},
Unknown(String),
Lsp {
action: Option<String>,
target: Option<String>,
},
Team {
action: Option<String>,
},
Expand Down Expand Up @@ -1280,6 +1291,7 @@ impl SlashCommand {
Self::Tag { .. } => "/tag",
Self::OutputStyle { .. } => "/output-style",
Self::AddDir { .. } => "/add-dir",
Self::Lsp { .. } => "/lsp",
Self::Team { .. } => "/team",
Self::Sandbox => "/sandbox",
Self::Mcp { .. } => "/mcp",
Expand Down Expand Up @@ -1495,6 +1507,10 @@ pub fn validate_slash_command_input(
"history" => SlashCommand::History {
count: optional_single_arg(command, &args, "[count]")?,
},
"lsp" => SlashCommand::Lsp {
action: args.first().map(|s| (*s).to_string()),
target: args.get(1).map(|s| (*s).to_string()),
},
other => SlashCommand::Unknown(other.to_string()),
}))
}
Expand Down Expand Up @@ -4623,6 +4639,7 @@ pub fn handle_slash_command(
| SlashCommand::OutputStyle { .. }
| SlashCommand::AddDir { .. }
| SlashCommand::History { .. }
| SlashCommand::Lsp { .. }
| SlashCommand::Team { .. }
| SlashCommand::Unknown(_) => None,
}
Expand Down
83 changes: 82 additions & 1 deletion rust/crates/runtime/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,16 @@ pub struct RuntimePluginConfig {
max_output_tokens: Option<u32>,
}

/// Per-language LSP server configuration supplied by the user in settings.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LspServerConfig {
pub command: String,
pub args: Vec<String>,
pub enabled: bool,
}

/// Structured feature configuration consumed by runtime subsystems.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeFeatureConfig {
hooks: RuntimeHookConfig,
plugins: RuntimePluginConfig,
Expand All @@ -95,6 +103,28 @@ pub struct RuntimeFeatureConfig {
sandbox: SandboxConfig,
provider_fallbacks: ProviderFallbackConfig,
trusted_roots: Vec<String>,
lsp_auto_start: bool,
lsp: BTreeMap<String, LspServerConfig>,
}

impl Default for RuntimeFeatureConfig {
fn default() -> Self {
Self {
hooks: RuntimeHookConfig::default(),
plugins: RuntimePluginConfig::default(),
mcp: McpConfigCollection::default(),
oauth: None,
model: None,
aliases: BTreeMap::new(),
permission_mode: None,
permission_rules: RuntimePermissionRuleConfig::default(),
sandbox: SandboxConfig::default(),
provider_fallbacks: ProviderFallbackConfig::default(),
trusted_roots: Vec::new(),
lsp_auto_start: true,
lsp: BTreeMap::new(),
}
}
}

/// Ordered chain of fallback model identifiers used when the primary
Expand Down Expand Up @@ -353,6 +383,12 @@ impl ConfigLoader {
sandbox: parse_optional_sandbox_config(&merged_value)?,
provider_fallbacks: parse_optional_provider_fallbacks(&merged_value)?,
trusted_roots: parse_optional_trusted_roots(&merged_value)?,
lsp_auto_start: merged_value
.as_object()
.and_then(|o| o.get("lspAutoStart"))
.and_then(|v| v.as_bool())
.unwrap_or(true),
lsp: parse_optional_lsp_config(&merged_value)?,
};

Ok(RuntimeConfig {
Expand Down Expand Up @@ -410,6 +446,12 @@ impl ConfigLoader {
sandbox: parse_optional_sandbox_config(&merged_value)?,
provider_fallbacks: parse_optional_provider_fallbacks(&merged_value)?,
trusted_roots: parse_optional_trusted_roots(&merged_value)?,
lsp_auto_start: merged_value
.as_object()
.and_then(|o| o.get("lspAutoStart"))
.and_then(|v| v.as_bool())
.unwrap_or(true),
lsp: parse_optional_lsp_config(&merged_value)?,
};

let config = RuntimeConfig {
Expand Down Expand Up @@ -596,6 +638,16 @@ impl RuntimeFeatureConfig {
pub fn trusted_roots_with_overrides(&self, per_call_roots: &[String]) -> Vec<String> {
merge_trusted_roots(self.trusted_roots(), per_call_roots)
}

#[must_use]
pub fn lsp(&self) -> &BTreeMap<String, LspServerConfig> {
&self.lsp
}

#[must_use]
pub fn lsp_auto_start(&self) -> bool {
self.lsp_auto_start
}
}

fn merge_trusted_roots(config_roots: &[String], per_call_roots: &[String]) -> Vec<String> {
Expand Down Expand Up @@ -1162,6 +1214,35 @@ fn parse_optional_trusted_roots(root: &JsonValue) -> Result<Vec<String>, ConfigE
)
}

fn parse_optional_lsp_config(
root: &JsonValue,
) -> Result<BTreeMap<String, LspServerConfig>, ConfigError> {
let Some(lsp_value) = root.as_object().and_then(|object| object.get("lsp")) else {
return Ok(BTreeMap::new());
};
let lsp_object = expect_object(lsp_value, "merged settings.lsp")?;
let mut result = BTreeMap::new();
for (language, value) in lsp_object {
let entry = expect_object(value, &format!("merged settings.lsp.{language}"))?;
let command =
expect_string(entry, "command", &format!("merged settings.lsp.{language}"))?.to_owned();
let args =
optional_string_array(entry, "args", &format!("merged settings.lsp.{language}"))?
.unwrap_or_default();
let enabled = optional_bool(entry, "enabled", &format!("merged settings.lsp.{language}"))?
.unwrap_or(true);
result.insert(
language.clone(),
LspServerConfig {
command,
args,
enabled,
},
);
}
Ok(result)
}

fn parse_filesystem_mode_label(value: &str) -> Result<FilesystemIsolationMode, ConfigError> {
match value {
"off" => Ok(FilesystemIsolationMode::Off),
Expand Down
11 changes: 9 additions & 2 deletions rust/crates/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ mod hooks;
mod json;
mod lane_events;
pub mod lsp_client;
pub mod lsp_discovery;
pub mod lsp_process;
pub mod lsp_transport;
mod mcp;
mod mcp_client;
pub mod mcp_lifecycle_hardened;
Expand Down Expand Up @@ -66,8 +69,8 @@ pub use compact::{
};
pub use config::{
suppress_config_warnings_for_json_mode, ConfigEntry, ConfigError, ConfigLoader, ConfigSource,
McpConfigCollection, McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig,
McpSdkServerConfig, McpServerConfig, McpStdioServerConfig, McpTransport,
LspServerConfig, McpConfigCollection, McpManagedProxyServerConfig, McpOAuthConfig,
McpRemoteServerConfig, McpSdkServerConfig, McpServerConfig, McpStdioServerConfig, McpTransport,
McpWebSocketServerConfig, OAuthConfig, ProviderFallbackConfig, ResolvedPermissionMode,
RuntimeConfig, RuntimeFeatureConfig, RuntimeHookConfig, RuntimePermissionRuleConfig,
RuntimePluginConfig, ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
Expand Down Expand Up @@ -98,6 +101,10 @@ pub use lane_events::{
LaneFailureClass, LaneOwnership, SessionIdentity, ShipMergeMethod, ShipProvenance,
WatcherAction,
};
pub use lsp_discovery::{
command_exists_on_path, discover_available_servers, find_server_for_file, known_lsp_servers,
LspServerDescriptor,
};
pub use mcp::{
mcp_server_signature, mcp_tool_name, mcp_tool_prefix, normalize_name_for_mcp,
scoped_mcp_config_hash, unwrap_ccr_proxy_url,
Expand Down
Loading
Loading