add nushell terminal and completion support (#8628)
Signed-off-by: Can H. Tartanoglu <gpg@rotas.mozmail.com>
This commit is contained in:
committed by
GitHub
parent
ba60b597fa
commit
80cac3626f
+100
-7
@@ -1,6 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Args, CommandFactory, Parser, Subcommand};
|
||||
use clap_complete::{generate, Shell as ClapShell};
|
||||
use clap_complete_nushell::Nushell as ClapNushell;
|
||||
use goose::agents::GoosePlatform;
|
||||
use goose::builtin_extension::register_builtin_extensions;
|
||||
use goose::config::{Config, GooseMode};
|
||||
@@ -934,7 +935,8 @@ enum Command {
|
||||
long_about = "Runs a goose session tied to your terminal window.\n\
|
||||
Each terminal maintains its own persistent session that resumes automatically.\n\n\
|
||||
Setup:\n \
|
||||
eval \"$(goose term init zsh)\" # Add to ~/.zshrc\n\n\
|
||||
eval \"$(goose term init zsh)\" # zsh/bash\n \
|
||||
let init = ($nu.cache-dir | path join \"goose-term-init.nu\"); ^goose term init nu | save --force $init; source $init\n\n\
|
||||
Usage:\n \
|
||||
goose term run \"list files in this directory\"\n \
|
||||
@goose \"create a python script\" # using alias\n \
|
||||
@@ -953,10 +955,12 @@ enum Command {
|
||||
},
|
||||
|
||||
/// Generate completions for various shells
|
||||
#[command(about = "Generate the autocompletion script for the specified shell")]
|
||||
#[command(
|
||||
about = "Generate the autocompletion script or Nushell module for the specified shell"
|
||||
)]
|
||||
Completion {
|
||||
#[arg(value_enum)]
|
||||
shell: ClapShell,
|
||||
shell: CompletionShell,
|
||||
|
||||
#[arg(long, default_value = "goose", help = "Provide a custom binary name")]
|
||||
bin_name: String,
|
||||
@@ -1016,11 +1020,16 @@ enum TermCommand {
|
||||
Setup:\n \
|
||||
echo 'eval \"$(goose term init zsh)\"' >> ~/.zshrc\n \
|
||||
source ~/.zshrc\n\n\
|
||||
Nushell:\n \
|
||||
let init = ($nu.cache-dir | path join \"goose-term-init.nu\")\n \
|
||||
^goose term init nu | save --force $init\n \
|
||||
source $init\n\n\
|
||||
With --default (anything typed that isn't a command goes to goose):\n \
|
||||
echo 'eval \"$(goose term init zsh --default)\"' >> ~/.zshrc"
|
||||
echo 'eval \"$(goose term init zsh --default)\"' >> ~/.zshrc\n \
|
||||
^goose term init nu --default | save --force $init"
|
||||
)]
|
||||
Init {
|
||||
/// Shell type (bash, zsh, fish, powershell)
|
||||
/// Shell type (bash, zsh, fish, nu, powershell)
|
||||
#[arg(value_enum)]
|
||||
shell: Shell,
|
||||
|
||||
@@ -1031,7 +1040,7 @@ enum TermCommand {
|
||||
#[arg(
|
||||
long = "default",
|
||||
help = "Make goose the default handler for unknown commands",
|
||||
long_help = "When enabled, anything you type that isn't a valid command will be sent to goose. Only supported for zsh and bash."
|
||||
long_help = "When enabled, anything you type that isn't a valid command will be sent to goose. Supported for zsh, bash, and nu."
|
||||
)]
|
||||
default: bool,
|
||||
},
|
||||
@@ -1074,6 +1083,31 @@ enum CliProviderVariant {
|
||||
Ollama,
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum CompletionShell {
|
||||
Bash,
|
||||
Elvish,
|
||||
Fish,
|
||||
#[value(alias = "pwsh")]
|
||||
Powershell,
|
||||
#[value(alias = "nushell")]
|
||||
Nu,
|
||||
Zsh,
|
||||
}
|
||||
|
||||
impl CompletionShell {
|
||||
fn generate(self, cmd: &mut clap::Command, bin_name: &str, writer: &mut dyn std::io::Write) {
|
||||
match self {
|
||||
CompletionShell::Bash => generate(ClapShell::Bash, cmd, bin_name, writer),
|
||||
CompletionShell::Elvish => generate(ClapShell::Elvish, cmd, bin_name, writer),
|
||||
CompletionShell::Fish => generate(ClapShell::Fish, cmd, bin_name, writer),
|
||||
CompletionShell::Powershell => generate(ClapShell::PowerShell, cmd, bin_name, writer),
|
||||
CompletionShell::Nu => generate(ClapNushell, cmd, bin_name, writer),
|
||||
CompletionShell::Zsh => generate(ClapShell::Zsh, cmd, bin_name, writer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InputConfig {
|
||||
pub contents: Option<String>,
|
||||
@@ -1846,7 +1880,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
||||
match cli.command {
|
||||
Some(Command::Completion { shell, bin_name }) => {
|
||||
let mut cmd = Cli::command();
|
||||
generate(shell, &mut cmd, bin_name, &mut std::io::stdout());
|
||||
shell.generate(&mut cmd, &bin_name, &mut std::io::stdout());
|
||||
Ok(())
|
||||
}
|
||||
Some(Command::Configure {}) => handle_configure().await,
|
||||
@@ -1939,3 +1973,62 @@ pub async fn cli() -> anyhow::Result<()> {
|
||||
None => handle_default_session().await,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn completion_command_accepts_nushell_alias() {
|
||||
let cli = Cli::try_parse_from(["goose", "completion", "nushell"]).expect("parse failed");
|
||||
|
||||
match cli.command {
|
||||
Some(Command::Completion {
|
||||
shell: CompletionShell::Nu,
|
||||
..
|
||||
}) => {}
|
||||
_ => panic!("expected nu completion shell"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nushell_completion_generation_emits_module() {
|
||||
let mut cmd = Cli::command();
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
CompletionShell::Nu.generate(&mut cmd, "goose", &mut buffer);
|
||||
|
||||
let script = String::from_utf8(buffer).expect("utf8");
|
||||
assert!(script.contains("module completions"));
|
||||
assert!(script.contains("export extern goose"));
|
||||
assert!(script.contains("export use completions *"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn term_init_help_mentions_nushell() {
|
||||
let mut cmd = Cli::command();
|
||||
let term = cmd.find_subcommand_mut("term").expect("term command");
|
||||
let init = term.find_subcommand_mut("init").expect("init command");
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
init.write_long_help(&mut buffer).expect("write help");
|
||||
|
||||
let help = String::from_utf8(buffer).expect("utf8");
|
||||
assert!(help.contains("goose term init nu"));
|
||||
assert!(help.contains("Supported for zsh, bash, and nu"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completion_help_lists_nu() {
|
||||
let mut cmd = Cli::command();
|
||||
let completion = cmd
|
||||
.find_subcommand_mut("completion")
|
||||
.expect("completion command");
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
completion.write_long_help(&mut buffer).expect("write help");
|
||||
|
||||
let help = String::from_utf8(buffer).expect("utf8");
|
||||
assert!(help.contains("nu"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ use crate::session::{build_session, SessionBuilderConfig};
|
||||
|
||||
use clap::ValueEnum;
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug)]
|
||||
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Shell {
|
||||
Bash,
|
||||
Zsh,
|
||||
Fish,
|
||||
#[value(alias = "nushell")]
|
||||
Nu,
|
||||
#[value(alias = "pwsh")]
|
||||
Powershell,
|
||||
}
|
||||
@@ -29,6 +31,7 @@ impl Shell {
|
||||
Shell::Bash => &BASH_CONFIG,
|
||||
Shell::Zsh => &ZSH_CONFIG,
|
||||
Shell::Fish => &FISH_CONFIG,
|
||||
Shell::Nu => &NU_CONFIG,
|
||||
Shell::Powershell => &POWERSHELL_CONFIG,
|
||||
}
|
||||
}
|
||||
@@ -97,6 +100,42 @@ end"#,
|
||||
command_not_found: None,
|
||||
};
|
||||
|
||||
static NU_CONFIG: ShellConfig = ShellConfig {
|
||||
script_template: r#"$env.AGENT_SESSION_ID = "{session_id}"
|
||||
def --wrapped @goose [...args] { run-external "{goose_bin}" "term" "run" ...$args }
|
||||
def --wrapped @g [...args] { run-external "{goose_bin}" "term" "run" ...$args }
|
||||
|
||||
if (($env | get -o GOOSE_NU_PREEXEC_INSTALLED | default false) != true) {
|
||||
$env.GOOSE_NU_PREEXEC_INSTALLED = true
|
||||
$env.config.hooks.pre_execution = (
|
||||
$env.config.hooks.pre_execution
|
||||
| append {||
|
||||
let line = (commandline | str trim)
|
||||
if ($line | is-empty) {
|
||||
return
|
||||
}
|
||||
if ($line =~ '^goose term(\s|$)') {
|
||||
return
|
||||
}
|
||||
if ($line =~ '^(@goose|@g)(\s|$)') {
|
||||
return
|
||||
}
|
||||
job spawn { run-external "{goose_bin}" "term" "log" $line | complete | ignore } | ignore
|
||||
}
|
||||
)
|
||||
}
|
||||
{command_not_found_handler}"#,
|
||||
command_not_found: Some(
|
||||
r#"
|
||||
$env.config.hooks.command_not_found = {|command_name|
|
||||
let prompt = (try { commandline | str trim } catch { $command_name })
|
||||
print $"🪿 Command '($command_name)' not found. Asking goose..."
|
||||
run-external "{goose_bin}" "term" "run" $prompt | complete | ignore
|
||||
null
|
||||
}"#,
|
||||
),
|
||||
};
|
||||
|
||||
static POWERSHELL_CONFIG: ShellConfig = ShellConfig {
|
||||
script_template: r#"$env:AGENT_SESSION_ID = "{session_id}"
|
||||
function @goose {{ & '{goose_bin}' term run @args }}
|
||||
@@ -113,12 +152,34 @@ Set-PSReadLineKeyHandler -Chord Enter -ScriptBlock {{
|
||||
command_not_found: None,
|
||||
};
|
||||
|
||||
fn render_term_init_script(
|
||||
shell: Shell,
|
||||
session_id: &str,
|
||||
goose_bin: &str,
|
||||
with_command_not_found: bool,
|
||||
) -> String {
|
||||
let config = shell.config();
|
||||
let command_not_found_handler = if with_command_not_found {
|
||||
config
|
||||
.command_not_found
|
||||
.map(|handler| handler.replace("{goose_bin}", goose_bin))
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
config
|
||||
.script_template
|
||||
.replace("{session_id}", session_id)
|
||||
.replace("{goose_bin}", goose_bin)
|
||||
.replace("{command_not_found_handler}", &command_not_found_handler)
|
||||
}
|
||||
|
||||
pub async fn handle_term_init(
|
||||
shell: Shell,
|
||||
name: Option<String>,
|
||||
with_command_not_found: bool,
|
||||
) -> Result<()> {
|
||||
let config = shell.config();
|
||||
let session_manager = SessionManager::instance();
|
||||
|
||||
let working_dir = std::env::current_dir()?;
|
||||
@@ -159,28 +220,18 @@ pub async fn handle_term_init(
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|_| "goose".to_string());
|
||||
|
||||
let command_not_found_handler = if with_command_not_found {
|
||||
config
|
||||
.command_not_found
|
||||
.map(|s| s.replace("{goose_bin}", &goose_bin))
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let script = config
|
||||
.script_template
|
||||
.replace("{session_id}", &session.id)
|
||||
.replace("{goose_bin}", &goose_bin)
|
||||
.replace("{command_not_found_handler}", &command_not_found_handler);
|
||||
|
||||
println!("{}", script);
|
||||
println!(
|
||||
"{}",
|
||||
render_term_init_script(shell, &session.id, &goose_bin, with_command_not_found)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_term_log(command: String) -> Result<()> {
|
||||
let session_id = std::env::var("AGENT_SESSION_ID").map_err(|_| {
|
||||
anyhow!("AGENT_SESSION_ID not set. Run 'eval \"$(goose term init <shell>)\"' first.")
|
||||
anyhow!(
|
||||
"AGENT_SESSION_ID not set. Initialize terminal integration with `goose term init <shell>` and reload your shell first."
|
||||
)
|
||||
})?;
|
||||
|
||||
let message = Message::new(
|
||||
@@ -202,9 +253,8 @@ pub async fn handle_term_run(prompt: Vec<String>) -> Result<()> {
|
||||
let session_id = std::env::var("AGENT_SESSION_ID").map_err(|_| {
|
||||
anyhow!(
|
||||
"AGENT_SESSION_ID not set.\n\n\
|
||||
Add to your shell config (~/.zshrc or ~/.bashrc):\n \
|
||||
eval \"$(goose term init zsh)\"\n\n\
|
||||
Then restart your terminal or run: source ~/.zshrc"
|
||||
Initialize terminal integration with `goose term init <shell>` in your shell profile, \
|
||||
then restart or reload that shell."
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -317,3 +367,37 @@ pub async fn handle_term_info() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn render_term_init_script_includes_nushell_hooks() {
|
||||
let script = render_term_init_script(Shell::Nu, "session-123", "/tmp/goose", false);
|
||||
|
||||
assert!(script.contains("$env.AGENT_SESSION_ID = \"session-123\""));
|
||||
assert!(script.contains("def --wrapped @goose [...args]"));
|
||||
assert!(script.contains("def --wrapped @g [...args]"));
|
||||
assert!(script.contains("GOOSE_NU_PREEXEC_INSTALLED"));
|
||||
assert!(script.contains("$env.config.hooks.pre_execution"));
|
||||
assert!(script.contains("job spawn { run-external \"/tmp/goose\" \"term\" \"log\" $line | complete | ignore } | ignore"));
|
||||
assert!(!script.contains("command_not_found = {|command_name|"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_term_init_script_includes_nushell_default_handler() {
|
||||
let script = render_term_init_script(Shell::Nu, "session-123", "/tmp/goose", true);
|
||||
|
||||
assert!(script.contains("$env.config.hooks.command_not_found = {|command_name|"));
|
||||
assert!(script
|
||||
.contains("run-external \"/tmp/goose\" \"term\" \"run\" $prompt | complete | ignore"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_term_init_script_skips_unsupported_default_handler() {
|
||||
let script = render_term_init_script(Shell::Fish, "session-123", "/tmp/goose", true);
|
||||
|
||||
assert!(!script.contains("command_not_found"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user