Skip to content
Merged
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
23 changes: 23 additions & 0 deletions crates/goose-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,27 @@ enum Command {
#[command(subcommand)]
command: TermCommand,
},

/// Launch the goose terminal UI (TUI)
#[command(
about = "Launch the goose terminal UI",
long_about = "Launch the goose terminal UI (the @aaif/goose npm package).\n\
\n\
Resolution order:\n \
1. GOOSE_TUI_SCRIPT, if set to an existing dist/tui.js\n \
2. A local checkout's ui/text/dist/tui.js (dev workflow)\n \
3. `npx --yes --package <spec> -- goose-tui` (deployed installs)\n\
\n\
Override the npm spec via GOOSE_TUI_NPM_SPEC (default: @aaif/goose@latest).\n\
Local script mode requires `node` on PATH; npx mode requires `npx` on PATH.\n\
Any extra arguments are passed through to the TUI."
)]
Tui {
/// Arguments forwarded to the TUI
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},

/// Manage local inference models
#[cfg(feature = "local-inference")]
#[command(about = "Manage local inference models", visible_alias = "lm")]
Expand Down Expand Up @@ -1252,6 +1273,7 @@ fn get_command_name(command: &Option<Command>) -> &'static str {
Some(Command::Recipe { .. }) => "recipe",
Some(Command::Plugin { .. }) => "plugin",
Some(Command::Term { .. }) => "term",
Some(Command::Tui { .. }) => "tui",
#[cfg(feature = "local-inference")]
Some(Command::LocalModels { .. }) => "local-models",
Some(Command::Completion { .. }) => "completion",
Expand Down Expand Up @@ -2087,6 +2109,7 @@ pub async fn cli() -> anyhow::Result<()> {
Some(Command::Recipe { command }) => handle_recipe_subcommand(command),
Some(Command::Plugin { command }) => handle_plugin_subcommand(command),
Some(Command::Term { command }) => handle_term_subcommand(command).await,
Some(Command::Tui { args }) => crate::commands::tui::handle_tui(args),
#[cfg(feature = "local-inference")]
Some(Command::LocalModels { command }) => handle_local_models_command(command).await,
Some(Command::Review {
Expand Down
1 change: 1 addition & 0 deletions crates/goose-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ pub mod review;
pub mod schedule;
pub mod session;
pub mod term;
pub mod tui;
pub mod update;
99 changes: 99 additions & 0 deletions crates/goose-cli/src/commands/tui.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use anyhow::{anyhow, Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;

const TUI_NPM_SPEC_ENV: &str = "GOOSE_TUI_NPM_SPEC";
const TUI_REL_PATH: &str = "ui/text/dist/tui.js";
const DEFAULT_NPM_SPEC: &str = "@aaif/goose@latest";
const NPM_BIN_NAME: &str = "goose-tui";

enum TuiSource {
LocalScript(PathBuf),
Npx(String),
}

fn find_local_script() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
let exe_dir = exe.parent().unwrap_or_else(|| Path::new("."));

let mut dir = Some(exe_dir.to_path_buf());
for _ in 0..6 {
if let Some(d) = dir.clone() {
let candidate = d.join(TUI_REL_PATH);
if candidate.is_file() {
return Some(candidate);
}
dir = d.parent().map(Path::to_path_buf);
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the use case here? if I am developing the TUI, am I likely to use this path?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I thought it could be helpful that when running cargo run --bin goose -- tui it also runs the tui from source


if let Ok(cwd) = std::env::current_dir() {
let candidate = cwd.join(TUI_REL_PATH);
if candidate.is_file() {
return Some(candidate);
}
}

None
}

fn resolve_source() -> TuiSource {
if let Some(script) = find_local_script() {
return TuiSource::LocalScript(script);
}
let spec = std::env::var(TUI_NPM_SPEC_ENV).unwrap_or_else(|_| DEFAULT_NPM_SPEC.to_string());
TuiSource::Npx(spec)
Comment on lines +41 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor GOOSE_TUI_SCRIPT override before auto-discovery

The new command advertises GOOSE_TUI_SCRIPT as the highest-priority source, but resolve_source never reads that variable and always chooses auto-discovered local script or npx. In environments that rely on pinning a specific built tui.js (e.g., CI, custom packaging, or local debugging), the override currently cannot work and users cannot force the intended script when a local checkout is present.

Useful? React with 👍 / 👎.

}

fn build_command(source: &TuiSource, args: &[String]) -> Result<Command> {
match source {
TuiSource::LocalScript(script) => {
let mut cmd = Command::new("node");
cmd.arg(script).args(args);
Ok(cmd)
}
TuiSource::Npx(spec) => {
let mut cmd = Command::new("npx");
cmd.arg("--yes")
.arg("--package")
.arg(spec)
.arg("--")
.arg(NPM_BIN_NAME)
.args(args);
Ok(cmd)
}
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so this doesn't actually run the current goose binary as the source of ACP, right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TUI does. So we have some psuedo-cycles here... different commands but:

goose tui -> runs TUI -> uses goose acp (or connects to server via HTTP/WS)


pub fn handle_tui(args: Vec<String>) -> Result<()> {
let source = resolve_source();

let goose_binary = std::env::current_exe()
.context("could not determine current goose executable to expose as GOOSE_BINARY")?;

let mut cmd = build_command(&source, &args)?;
cmd.env("GOOSE_BINARY", &goose_binary);

let descriptor = match &source {
TuiSource::LocalScript(p) => format!("node {}", p.display()),
TuiSource::Npx(spec) => format!("npx --package {} -- {}", spec, NPM_BIN_NAME),
};

#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let err = cmd.exec();
Err(anyhow!("failed to exec TUI ({descriptor}): {err}"))
}

#[cfg(not(unix))]
{
let status = cmd
.status()
.with_context(|| format!("failed to run `{descriptor}`"))?;
if !status.success() {
std::process::exit(status.code().unwrap_or(1));
}
Ok(())
}
}
Loading