From 25beee6500052c20c2b2507ba02d3b41e22d36c3 Mon Sep 17 00:00:00 2001 From: KingParmenides Date: Sun, 10 May 2026 23:19:39 -0600 Subject: [PATCH 1/3] Improve natural language install intent handling --- wezterm/src/cli/install_intent.rs | 472 ++++++++++++++++++++++++++++++ wezterm/src/cli/mod.rs | 1 + wezterm/src/cli/shortcuts.rs | 74 ++++- 3 files changed, 545 insertions(+), 2 deletions(-) create mode 100644 wezterm/src/cli/install_intent.rs diff --git a/wezterm/src/cli/install_intent.rs b/wezterm/src/cli/install_intent.rs new file mode 100644 index 000000000..733aa3852 --- /dev/null +++ b/wezterm/src/cli/install_intent.rs @@ -0,0 +1,472 @@ +/* +Copyright (c) 2026 AI Venture Holdings LLC +Licensed under the Business Source License 1.1 +You may not use this file except in compliance with the License. +*/ +//! Natural language install intent matching for demo-safe offline operation. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InstallPlanStatus { + Ready, + NeedsClarification, + Unknown, +} + +#[derive(Debug, Clone)] +pub struct InstallPlan { + pub status: InstallPlanStatus, + pub topic: String, + pub packages: Vec, + pub alternatives: Vec, + pub reasoning: String, + pub confidence: f32, +} + +impl InstallPlan { + pub fn apt_command(&self) -> Option { + if self.status != InstallPlanStatus::Ready || self.packages.is_empty() { + return None; + } + + Some(format!("sudo apt install {}", self.packages.join(" "))) + } + + pub fn summary_lines(&self) -> Vec { + let mut lines = vec![ + format!("I understood you want to {}.", self.topic), + format!("Reasoning: {}", self.reasoning), + format!("Confidence: {:.0}%", self.confidence * 100.0), + ]; + + if !self.packages.is_empty() { + lines.push(format!("Packages: {}", self.packages.join(", "))); + } + + if !self.alternatives.is_empty() { + lines.push(format!("Alternatives: {}", self.alternatives.join(", "))); + } + + lines + } +} + +struct Candidate { + topic: &'static str, + packages: &'static [&'static str], + alternatives: &'static [&'static str], + reasoning: &'static str, + confidence: f32, +} + +pub fn resolve_install_intent(query: &str) -> InstallPlan { + let tokens = tokenize(query); + let normalized = tokens.join(" "); + + if tokens.is_empty() { + return clarification_plan("install software", "No package or goal was provided.", 0.2); + } + + if is_ambiguous_request(&tokens) { + return clarification_plan( + "install software", + "The request names a broad goal but not enough detail to choose packages safely.", + 0.42, + ); + } + + let mut candidates = Vec::new(); + + if let Some(candidate) = docker_kubernetes_candidate(&tokens, &normalized) { + candidates.push(candidate); + } + if let Some(candidate) = machine_learning_candidate(&tokens, &normalized) { + candidates.push(candidate); + } + if let Some(candidate) = python_dev_candidate(&tokens, &normalized) { + candidates.push(candidate); + } + if let Some(candidate) = web_server_candidate(&tokens, &normalized) { + candidates.push(candidate); + } + if let Some(candidate) = docker_candidate(&tokens) { + candidates.push(candidate); + } + + if let Some(best) = candidates + .into_iter() + .max_by(|left, right| left.confidence.total_cmp(&right.confidence)) + { + if best.confidence >= 0.68 { + return InstallPlan { + status: InstallPlanStatus::Ready, + topic: best.topic.to_string(), + packages: best.packages.iter().map(|pkg| pkg.to_string()).collect(), + alternatives: best + .alternatives + .iter() + .map(|alt| alt.to_string()) + .collect(), + reasoning: best.reasoning.to_string(), + confidence: best.confidence, + }; + } + + return InstallPlan { + status: InstallPlanStatus::NeedsClarification, + topic: best.topic.to_string(), + packages: Vec::new(), + alternatives: best + .alternatives + .iter() + .map(|alt| alt.to_string()) + .collect(), + reasoning: format!( + "{} I am not confident enough to install packages without confirmation.", + best.reasoning + ), + confidence: best.confidence, + }; + } + + InstallPlan { + status: InstallPlanStatus::Unknown, + topic: "install software".to_string(), + packages: Vec::new(), + alternatives: vec![ + "machine learning tools".to_string(), + "web server".to_string(), + "python development environment".to_string(), + "docker and kubernetes".to_string(), + ], + reasoning: "I could not map the request to a known install profile or safe package set." + .to_string(), + confidence: 0.18, + } +} + +fn machine_learning_candidate(tokens: &[String], normalized: &str) -> Option { + let mut confidence: f32 = 0.0; + + if normalized.contains("machine learning") { + confidence = confidence.max(0.94); + } + if normalized.contains("data science") { + confidence = confidence.max(0.9); + } + if has_token(tokens, "ml") || has_token(tokens, "ai") { + confidence = confidence.max(0.74); + } + if has_all(tokens, &["python"]) && has_any(tokens, &["numpy", "pandas", "scipy", "sklearn"]) { + confidence = confidence.max(0.86); + } + if has_any(tokens, &["tensorflow", "pytorch", "torch"]) { + confidence = confidence.max(0.8); + } + + (confidence > 0.0).then_some(Candidate { + topic: "install a machine learning Python toolkit", + packages: &[ + "python3", + "python3-pip", + "python3-venv", + "python3-numpy", + "python3-scipy", + "python3-pandas", + "python3-sklearn", + ], + alternatives: &[ + "Use pip inside a virtual environment for TensorFlow or PyTorch", + "Install Jupyter with pip if notebooks are needed", + ], + reasoning: "The request mentions machine learning or data science, so I selected the common Python scientific stack available from apt.", + confidence, + }) +} + +fn python_dev_candidate(tokens: &[String], normalized: &str) -> Option { + let mut confidence: f32 = 0.0; + + if normalized.contains("python development") || normalized.contains("python dev") { + confidence = confidence.max(0.96); + } + if has_all(tokens, &["python"]) + && has_any( + tokens, + &["development", "dev", "environment", "env", "setup"], + ) + { + confidence = confidence.max(0.9); + } + if has_all(tokens, &["pyton"]) && has_any(tokens, &["dev", "env"]) { + confidence = confidence.max(0.82); + } + + (confidence > 0.0).then_some(Candidate { + topic: "set up a Python development environment", + packages: &[ + "python3", + "python3-pip", + "python3-venv", + "python3-dev", + "build-essential", + ], + alternatives: &[ + "Add python3-poetry for Poetry projects", + "Add pipx for isolated Python command-line tools", + ], + reasoning: "The request names Python plus a development environment, so I selected the interpreter, pip, venv, headers, and build tools.", + confidence, + }) +} + +fn web_server_candidate(tokens: &[String], normalized: &str) -> Option { + let mut confidence: f32 = 0.0; + + if normalized.contains("web server") || normalized.contains("http server") { + confidence = confidence.max(0.82); + } + if has_any(tokens, &["nginx", "ngnix"]) { + confidence = confidence.max(0.95); + } + if has_all(tokens, &["web", "server"]) { + confidence = confidence.max(0.8); + } + + (confidence > 0.0).then_some(Candidate { + topic: "install a web server", + packages: &["nginx"], + alternatives: &["apache2", "caddy", "lighttpd"], + reasoning: "The request asks for a web or HTTP server. I selected nginx as the default lightweight web server and listed common alternatives.", + confidence, + }) +} + +fn docker_kubernetes_candidate(tokens: &[String], _normalized: &str) -> Option { + if has_any(tokens, &["docker", "dockr"]) + && has_any(tokens, &["kubernetes", "kubernets", "kubectl", "k8s"]) + { + return Some(Candidate { + topic: "install Docker and Kubernetes command-line tooling", + packages: &["docker.io", "kubernetes-client"], + alternatives: &[ + "Use the upstream Docker repository for the newest Docker Engine", + "Use kind or minikube for a local Kubernetes cluster", + ], + reasoning: "The request mentions Docker and Kubernetes, so I selected the distro Docker package plus the apt package that provides kubectl.", + confidence: 0.94, + }); + } + + None +} + +fn docker_candidate(tokens: &[String]) -> Option { + if has_any(tokens, &["docker", "dockr"]) { + return Some(Candidate { + topic: "install Docker", + packages: &["docker.io"], + alternatives: &["podman", "containerd"], + reasoning: "The request mentions Docker, so I selected the distro Docker package and listed compatible container alternatives.", + confidence: 0.88, + }); + } + + None +} + +fn clarification_plan(topic: &str, reasoning: &str, confidence: f32) -> InstallPlan { + InstallPlan { + status: InstallPlanStatus::NeedsClarification, + topic: topic.to_string(), + packages: Vec::new(), + alternatives: vec![ + "machine learning tools".to_string(), + "web server".to_string(), + "python development environment".to_string(), + "docker and kubernetes".to_string(), + ], + reasoning: reasoning.to_string(), + confidence, + } +} + +fn is_ambiguous_request(tokens: &[String]) -> bool { + let meaningful: Vec<&String> = tokens.iter().filter(|token| !is_stopword(token)).collect(); + + if meaningful.is_empty() { + return true; + } + + if meaningful.len() == 1 + && matches!( + meaningful[0].as_str(), + "something" | "stuff" | "tools" | "software" + ) + { + return true; + } + + has_any(tokens, &["server"]) + && !has_any( + tokens, + &["web", "http", "nginx", "apache", "database", "ssh"], + ) +} + +fn tokenize(query: &str) -> Vec { + query + .to_lowercase() + .split(|ch: char| !ch.is_ascii_alphanumeric() && ch != '-' && ch != '_') + .filter(|word| !word.is_empty()) + .map(|word| word.to_string()) + .collect() +} + +fn has_all(tokens: &[String], needles: &[&str]) -> bool { + needles.iter().all(|needle| has_token(tokens, needle)) +} + +fn has_any(tokens: &[String], needles: &[&str]) -> bool { + needles.iter().any(|needle| has_token(tokens, needle)) +} + +fn has_token(tokens: &[String], needle: &str) -> bool { + tokens.iter().any(|token| token_matches(token, needle)) +} + +fn token_matches(token: &str, needle: &str) -> bool { + if token == needle { + return true; + } + + if token.len() <= 2 || needle.len() <= 2 { + return false; + } + + let distance = levenshtein(token, needle); + let limit = if needle.len() <= 5 { 1 } else { 2 }; + distance <= limit +} + +fn levenshtein(left: &str, right: &str) -> usize { + let mut costs: Vec = (0..=right.len()).collect(); + + for (i, left_char) in left.chars().enumerate() { + let mut previous = i; + costs[0] = i + 1; + + for (j, right_char) in right.chars().enumerate() { + let insert = costs[j + 1] + 1; + let delete = costs[j] + 1; + let replace = previous + usize::from(left_char != right_char); + previous = costs[j + 1]; + costs[j + 1] = insert.min(delete).min(replace); + } + } + + costs[right.len()] +} + +fn is_stopword(token: &str) -> bool { + matches!( + token, + "a" | "an" + | "and" + | "for" + | "i" + | "install" + | "instal" + | "need" + | "please" + | "set" + | "setup" + | "the" + | "to" + | "up" + | "want" + | "with" + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_ready(query: &str, expected_package: &str) -> InstallPlan { + let plan = resolve_install_intent(query); + assert_eq!( + plan.status, + InstallPlanStatus::Ready, + "{}: {:?}", + query, + plan + ); + assert!( + plan.packages + .iter() + .any(|package| package == expected_package), + "{}: expected package {}, got {:?}", + query, + expected_package, + plan.packages + ); + assert!(plan.confidence >= 0.68, "{}: {:?}", query, plan); + plan + } + + #[test] + fn handles_required_natural_language_install_cases() { + assert_ready("install something for machine learning", "python3-sklearn"); + assert_ready("I need a web server", "nginx"); + assert_ready("set up python development environment", "python3-venv"); + assert_ready("install docker and kubernetes", "kubernetes-client"); + } + + #[test] + fn handles_typos_in_demo_requests() { + assert_ready("instal dockr and kubernets", "docker.io"); + assert_ready("install pyton dev env", "python3-pip"); + assert_ready("install ngnix web serber", "nginx"); + } + + #[test] + fn handles_additional_natural_language_cases() { + assert_ready("give me a data science toolkit", "python3-pandas"); + assert_ready("please install docker", "docker.io"); + assert_ready("I want an http server", "nginx"); + } + + #[test] + fn handles_ambiguous_requests_gracefully() { + let plan = resolve_install_intent("install something"); + assert_eq!(plan.status, InstallPlanStatus::NeedsClarification); + assert!(plan.packages.is_empty()); + assert!(!plan.alternatives.is_empty()); + + let server_plan = resolve_install_intent("I need a server"); + assert_eq!(server_plan.status, InstallPlanStatus::NeedsClarification); + assert!(server_plan.reasoning.contains("not enough detail")); + } + + #[test] + fn handles_unknown_requests_gracefully() { + let plan = resolve_install_intent("install obscure thingamabob"); + assert_eq!(plan.status, InstallPlanStatus::Unknown); + assert!(plan.packages.is_empty()); + assert!(!plan.alternatives.is_empty()); + } + + #[test] + fn exposes_reasoning_confidence_and_command() { + let plan = resolve_install_intent("install docker and kubernetes"); + let lines = plan.summary_lines().join("\n"); + + assert!(lines.contains("I understood you want")); + assert!(lines.contains("Reasoning:")); + assert!(lines.contains("Confidence:")); + assert_eq!( + plan.apt_command(), + Some("sudo apt install docker.io kubernetes-client".to_string()) + ); + } +} diff --git a/wezterm/src/cli/mod.rs b/wezterm/src/cli/mod.rs index b9c4eeecf..ad9d8a64f 100644 --- a/wezterm/src/cli/mod.rs +++ b/wezterm/src/cli/mod.rs @@ -29,6 +29,7 @@ pub mod ask; pub mod ask_context; pub mod ask_patterns; pub mod daemon; +pub mod install_intent; pub mod license; pub mod model_utils; pub mod new; diff --git a/wezterm/src/cli/shortcuts.rs b/wezterm/src/cli/shortcuts.rs index 96fc3fdc7..0a7dd26d7 100644 --- a/wezterm/src/cli/shortcuts.rs +++ b/wezterm/src/cli/shortcuts.rs @@ -14,8 +14,11 @@ You may not use this file except in compliance with the License. use anyhow::Result; use clap::Parser; +use std::io::{self, Write}; +use std::process::Command; use super::ask::AskCommand; +use super::install_intent::{resolve_install_intent, InstallPlan, InstallPlanStatus}; /// Install packages or software using natural language #[derive(Debug, Parser, Clone)] @@ -39,7 +42,13 @@ pub struct InstallCommand { impl InstallCommand { pub fn run(&self) -> Result<()> { - let query = format!("install {}", self.description.join(" ")); + let query = self.description.join(" "); + + if handle_install_intent(&query, self.auto_confirm, true)? { + return Ok(()); + } + + let query = format!("install {}", query); let ask = AskCommand { query: vec![query], @@ -76,7 +85,13 @@ pub struct SetupCommand { impl SetupCommand { pub fn run(&self) -> Result<()> { - let query = format!("setup {}", self.description.join(" ")); + let query = self.description.join(" "); + + if handle_install_intent(&query, self.auto_confirm, false)? { + return Ok(()); + } + + let query = format!("setup {}", query); let ask = AskCommand { query: vec![query], @@ -91,6 +106,61 @@ impl SetupCommand { } } +fn handle_install_intent( + description: &str, + auto_confirm: bool, + handle_unknown: bool, +) -> Result { + let plan = resolve_install_intent(description); + + match plan.status { + InstallPlanStatus::Ready => { + print_install_plan(&plan); + if let Some(command) = plan.apt_command() { + println!("Command: {}", command); + + if auto_confirm || confirm_install()? { + let status = Command::new("sh").arg("-c").arg(&command).status()?; + + if !status.success() { + eprintln!("Command failed with exit code: {:?}", status.code()); + } + } else { + eprintln!("Cancelled."); + } + } + Ok(true) + } + InstallPlanStatus::NeedsClarification => { + print_install_plan(&plan); + println!("Please clarify what you want installed before I run a command."); + Ok(true) + } + InstallPlanStatus::Unknown if handle_unknown => { + print_install_plan(&plan); + println!("I do not know a safe package set for that request yet."); + Ok(true) + } + InstallPlanStatus::Unknown => Ok(false), + } +} + +fn print_install_plan(plan: &InstallPlan) { + for line in plan.summary_lines() { + println!("{}", line); + } +} + +fn confirm_install() -> Result { + eprint!("Install these packages now? [y/N] "); + io::stderr().flush()?; + + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + + Ok(input.trim().eq_ignore_ascii_case("y")) +} + /// Ask questions about the system #[derive(Debug, Parser, Clone)] pub struct WhatCommand { From 7ce4f61fe710fbc5a2c7c00e2500a22005eb8afe Mon Sep 17 00:00:00 2001 From: KingParmenides Date: Mon, 11 May 2026 07:45:42 -0600 Subject: [PATCH 2/3] Refine install intent apt command handling --- wezterm/src/cli/install_intent.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/wezterm/src/cli/install_intent.rs b/wezterm/src/cli/install_intent.rs index 733aa3852..d24db0295 100644 --- a/wezterm/src/cli/install_intent.rs +++ b/wezterm/src/cli/install_intent.rs @@ -23,12 +23,17 @@ pub struct InstallPlan { } impl InstallPlan { - pub fn apt_command(&self) -> Option { + pub fn apt_command(&self, auto_confirm: bool) -> Option { if self.status != InstallPlanStatus::Ready || self.packages.is_empty() { return None; } - Some(format!("sudo apt install {}", self.packages.join(" "))) + let yes_flag = if auto_confirm { " -y" } else { "" }; + Some(format!( + "sudo apt install{} {}", + yes_flag, + self.packages.join(" ") + )) } pub fn summary_lines(&self) -> Vec { @@ -96,6 +101,8 @@ pub fn resolve_install_intent(query: &str) -> InstallPlan { .into_iter() .max_by(|left, right| left.confidence.total_cmp(&right.confidence)) { + // Keep the ready threshold high enough that demo-safe package installs + // only run for direct profile matches or clear fuzzy matches. if best.confidence >= 0.68 { return InstallPlan { status: InstallPlanStatus::Ready, @@ -121,7 +128,7 @@ pub fn resolve_install_intent(query: &str) -> InstallPlan { .map(|alt| alt.to_string()) .collect(), reasoning: format!( - "{} I am not confident enough to install packages without confirmation.", + "{} I need more detail before choosing packages safely.", best.reasoning ), confidence: best.confidence, @@ -465,8 +472,12 @@ mod tests { assert!(lines.contains("Reasoning:")); assert!(lines.contains("Confidence:")); assert_eq!( - plan.apt_command(), + plan.apt_command(false), Some("sudo apt install docker.io kubernetes-client".to_string()) ); + assert_eq!( + plan.apt_command(true), + Some("sudo apt install -y docker.io kubernetes-client".to_string()) + ); } } From 0285320c716fcf486f7291bc15c09cd8d17efaa0 Mon Sep 17 00:00:00 2001 From: KingParmenides Date: Mon, 11 May 2026 07:45:56 -0600 Subject: [PATCH 3/3] Use safe apt execution for install intents --- wezterm/src/cli/shortcuts.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/wezterm/src/cli/shortcuts.rs b/wezterm/src/cli/shortcuts.rs index 0a7dd26d7..dc82fb98a 100644 --- a/wezterm/src/cli/shortcuts.rs +++ b/wezterm/src/cli/shortcuts.rs @@ -44,7 +44,7 @@ impl InstallCommand { pub fn run(&self) -> Result<()> { let query = self.description.join(" "); - if handle_install_intent(&query, self.auto_confirm, true)? { + if handle_install_intent(&query, self.auto_confirm, false)? { return Ok(()); } @@ -116,11 +116,18 @@ fn handle_install_intent( match plan.status { InstallPlanStatus::Ready => { print_install_plan(&plan); - if let Some(command) = plan.apt_command() { + if let Some(command) = plan.apt_command(auto_confirm) { println!("Command: {}", command); if auto_confirm || confirm_install()? { - let status = Command::new("sh").arg("-c").arg(&command).status()?; + let mut command = Command::new("sudo"); + command.arg("apt").arg("install"); + if auto_confirm { + command.arg("-y"); + } + command.args(&plan.packages); + + let status = command.status()?; if !status.success() { eprintln!("Command failed with exit code: {:?}", status.code());