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
14 changes: 9 additions & 5 deletions crates/goat-agent/src/ask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,20 @@ pub(crate) fn ask_call_display(input: &str) -> ToolDisplay {
questions: Vec<AskQuestion>,
}
let Ok(args) = serde_json::from_str::<Input>(input) else {
return goat_tool::display::generic(input);
return goat_tool::display::generic_named(ASK_TOOL_NAME, input);
};
let Some(first) = args.questions.first() else {
return goat_tool::display::generic(input);
return goat_tool::display::generic_named(ASK_TOOL_NAME, input);
};
let primary = goat_tool::display::flatten(&first.question);
let q = goat_tool::display::flatten(&first.question);
if args.questions.len() > 1 {
ToolDisplay::with_detail(primary, format!("+{} more", args.questions.len() - 1))
let more = format!("+{} more", args.questions.len() - 1);
ToolDisplay::primary(goat_tool::display::call_sig(
ASK_TOOL_NAME,
&[q.as_str(), more.as_str()],
))
} else {
ToolDisplay::primary(primary)
ToolDisplay::primary(goat_tool::display::call_sig(ASK_TOOL_NAME, &[q.as_str()]))
}
}

Expand Down
8 changes: 6 additions & 2 deletions crates/goat-agent/src/delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,13 @@ pub(crate) fn agent_call_display(input: &str) -> ToolDisplay {
}
match serde_json::from_str::<Input>(input) {
Ok(args) => {
ToolDisplay::with_detail(args.agent_type, goat_tool::display::flatten(&args.prompt))
let prompt = goat_tool::display::flatten(&args.prompt);
ToolDisplay::primary(goat_tool::display::call_sig(
AGENT_TOOL_NAME,
&[args.agent_type.as_str(), prompt.as_str()],
))
}
Err(_) => goat_tool::display::generic(input),
Err(_) => goat_tool::display::generic_named(AGENT_TOOL_NAME, input),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/goat-agent/src/tools_exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub(crate) fn call_display(tools: &ToolRegistry, name: &str, input: &str) -> Too
ASK_TOOL_NAME => ask_call_display(input),
WEB_SEARCH_TOOL_NAME => web_search_display(input),
_ => tools.get(name).map_or_else(
|| goat_tool::display::generic(input),
|| goat_tool::display::generic_named(name, input),
|tool| tool.display_input(input),
),
}
Expand Down
7 changes: 5 additions & 2 deletions crates/goat-agent/src/websearch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ pub(crate) fn web_search_display(input: &str) -> ToolDisplay {
query: String,
}
match serde_json::from_str::<Input>(input) {
Ok(args) => ToolDisplay::primary(goat_tool::display::flatten(&args.query)),
Err(_) => goat_tool::display::generic(input),
Ok(args) => ToolDisplay::primary(goat_tool::display::call_sig(
WEB_SEARCH_TOOL_NAME,
&[args.query.as_str()],
)),
Err(_) => goat_tool::display::generic_named(WEB_SEARCH_TOOL_NAME, input),
}
}

Expand Down
15 changes: 3 additions & 12 deletions crates/goat-command-settings/src/effort.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use goat_command::{
ChoiceSpec, Command, CommandEffect, CommandInvocation, CommandShape, ParameterSpec,
ParameterValue,
Command, CommandEffect, CommandInvocation, CommandShape, ParameterSpec, ParameterValue,
};

pub struct Effort;
Expand All @@ -19,20 +18,12 @@ impl Command for Effort {
name: "level".to_owned(),
description: "reasoning effort level".to_owned(),
required: false,
value: ParameterValue::Choice(
["off", "low", "medium", "high", "xhigh", "max"]
.into_iter()
.map(|value| ChoiceSpec {
value: value.to_owned(),
description: None,
})
.collect(),
),
value: ParameterValue::Word,
}])
}

fn run(&self, invocation: CommandInvocation) -> CommandEffect {
if let Some(level) = invocation.choice("level") {
if let Some(level) = invocation.text("level") {
CommandEffect::SelectEffort(level.to_ascii_lowercase())
} else {
CommandEffect::OpenEffortPicker
Expand Down
2 changes: 1 addition & 1 deletion crates/goat-commands/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ mod tests {
let CommandShape::Parameters(parameters) = effort.shape else {
panic!("expected parameters");
};
assert!(matches!(parameters[0].value, ParameterValue::Choice(_)));
assert!(matches!(parameters[0].value, ParameterValue::Word));
}

#[test]
Expand Down
5 changes: 4 additions & 1 deletion crates/goat-tool-fs/src/tools/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ impl Tool for EditTool {

fn display_input(&self, input: &str) -> ToolDisplay {
match serde_json::from_str::<Input>(input) {
Ok(args) => ToolDisplay::primary(display::flatten(&args.path)),
Ok(args) => ToolDisplay::primary(display::call_sig(
"Edit",
&[display::flatten(&args.path).as_str()],
)),
Err(_) => display::generic(input),
}
}
Expand Down
9 changes: 6 additions & 3 deletions crates/goat-tool-fs/src/tools/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ impl Tool for ReadTool {

fn display_input(&self, input: &str) -> ToolDisplay {
match serde_json::from_str::<Input>(input) {
Ok(args) => ToolDisplay::primary(display::flatten(&args.path)),
Ok(args) => ToolDisplay::primary(display::call_sig(
"Read",
&[display::flatten(&args.path).as_str()],
)),
Err(_) => display::generic(input),
}
}
Expand Down Expand Up @@ -118,9 +121,9 @@ mod tests {
}

#[test]
fn display_shows_path_without_range() {
fn display_shows_call_signature() {
let display = ReadTool.display_input(r#"{"path":"a.txt","offset":120,"limit":50}"#);
assert_eq!(display.primary, "a.txt");
assert_eq!(display.primary, "Read(a.txt)");
assert_eq!(display.detail, None);
}

Expand Down
5 changes: 4 additions & 1 deletion crates/goat-tool-fs/src/tools/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ impl Tool for WriteTool {

fn display_input(&self, input: &str) -> ToolDisplay {
match serde_json::from_str::<Input>(input) {
Ok(args) => ToolDisplay::primary(display::flatten(&args.path)),
Ok(args) => ToolDisplay::primary(display::call_sig(
"Write",
&[display::flatten(&args.path).as_str()],
)),
Err(_) => display::generic(input),
}
}
Expand Down
43 changes: 38 additions & 5 deletions crates/goat-tool-search/src/glob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ impl Tool for GlobTool {
return goat_tool::display::generic(input);
};
let pattern = goat_tool::display::flatten(&args.pattern);
match args.path.filter(|p| !p.is_empty() && p != ".") {
Some(path) => goat_protocol::ToolDisplay::with_detail(pattern, path),
None => goat_protocol::ToolDisplay::primary(pattern),
}
let sig = match args.path.filter(|p| !p.is_empty() && p != ".") {
Some(path) => goat_tool::display::call_sig("Glob", &[pattern.as_str(), path.as_str()]),
None => goat_tool::display::call_sig("Glob", &[pattern.as_str()]),
};
goat_protocol::ToolDisplay::primary(sig)
}

fn run<'a>(&'a self, input: &'a str, ctx: &'a ToolContext) -> ToolFuture<'a> {
Expand Down Expand Up @@ -85,6 +86,7 @@ fn walk(
let mut found = Vec::new();
let mut builder = WalkBuilder::new(root);
builder.require_git(false);
builder.hidden(false);
let blocked_for_walk = blocked.to_vec();
builder.filter_entry(move |entry| !blocked_path(&blocked_for_walk, entry.path()));
for entry in builder.build() {
Expand Down Expand Up @@ -153,7 +155,7 @@ mod tests {
fn display_omits_trivial_scope() {
use goat_tool::Tool;
let display = GlobTool.display_input(r#"{"pattern":"*.rs","path":"."}"#);
assert_eq!(display.primary, "*.rs");
assert_eq!(display.primary, "Glob(*.rs)");
assert_eq!(display.detail, None);
}

Expand Down Expand Up @@ -197,4 +199,35 @@ mod tests {
Err(goat_tool::ToolError::PathBlocked { .. })
));
}

#[tokio::test]
async fn lists_dot_github_in_real_repo_layout() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".github/workflows")).unwrap();
std::fs::write(dir.path().join(".github/workflows/ci.yml"), "").unwrap();
let ctx = ctx(dir.path());
for pat in ["**/.github/**", ".github/**/*", ".github/workflows/*.yml"] {
let out = GlobTool
.run(&format!(r#"{{"pattern":"{pat}"}}"#), &ctx)
.await
.unwrap_or_else(|e| panic!("{pat}: {e:?}"));
let text = out.as_text().unwrap();
assert!(
text.contains("ci.yml") || text.contains(".github"),
"{pat} got: {text}"
);
}
}

#[test]
fn github_style_patterns_parse() {
use ignore::overrides::OverrideBuilder;
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let root = root.canonicalize().unwrap();
for pat in ["**/.github/**", ".github/**/*", "**/*depend*"] {
let mut o = OverrideBuilder::new(&root);
o.add(pat).unwrap_or_else(|e| panic!("add {pat}: {e:?}"));
o.build().unwrap_or_else(|e| panic!("build {pat}: {e:?}"));
}
}
}
17 changes: 6 additions & 11 deletions crates/goat-tool-search/src/grep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,13 @@ impl Tool for GrepTool {
let Ok(args) = serde_json::from_str::<Input>(input) else {
return goat_tool::display::generic(input);
};
let scope: Vec<String> = [
args.path.filter(|p| !p.is_empty() && p != "."),
args.glob.filter(|g| !g.is_empty() && g != "*"),
]
.into_iter()
.flatten()
.collect();
let pattern = goat_tool::display::flatten(&args.pattern);
if scope.is_empty() {
goat_protocol::ToolDisplay::primary(pattern)
} else {
goat_protocol::ToolDisplay::with_detail(pattern, scope.join(" · "))
let mut params = vec![pattern];
if let Some(path) = args.path.filter(|p| !p.is_empty() && p != ".") {
params.push(path);
}
let refs: Vec<&str> = params.iter().map(String::as_str).collect();
goat_protocol::ToolDisplay::primary(goat_tool::display::call_sig("Grep", &refs))
}

fn run<'a>(&'a self, input: &'a str, ctx: &'a ToolContext) -> ToolFuture<'a> {
Expand Down Expand Up @@ -116,6 +110,7 @@ fn search(
.build()?;
let mut builder = WalkBuilder::new(root);
builder.require_git(false);
builder.hidden(false);
let blocked_for_walk = blocked.to_vec();
builder.filter_entry(move |entry| !blocked_path(&blocked_for_walk, entry.path()));
let matcher = match glob {
Expand Down
5 changes: 4 additions & 1 deletion crates/goat-tool-shell/src/bash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ impl Tool for BashTool {

fn display_input(&self, input: &str) -> ToolDisplay {
match serde_json::from_str::<Input>(input) {
Ok(args) => ToolDisplay::primary(display::flatten(&args.command)),
Ok(args) => ToolDisplay::primary(display::call_sig(
"Bash",
&[display::flatten(&args.command).as_str()],
)),
Err(_) => display::generic(input),
}
}
Expand Down
5 changes: 4 additions & 1 deletion crates/goat-tool-web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ impl Tool for WebFetchTool {

fn display_input(&self, input: &str) -> ToolDisplay {
match serde_json::from_str::<Input>(input) {
Ok(args) => ToolDisplay::primary(display::flatten(&args.url)),
Ok(args) => ToolDisplay::primary(display::call_sig(
"WebFetch",
&[display::flatten(&args.url).as_str()],
)),
Err(_) => display::generic(input),
}
}
Expand Down
67 changes: 59 additions & 8 deletions crates/goat-tool/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ pub fn flatten(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}

pub fn format_arg(s: &str) -> String {
let needs =
s.is_empty() || s.chars().any(char::is_whitespace) || s.contains('"') || s.contains('\'');
if needs {
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
} else {
s.to_owned()
}
}

pub fn call_sig(name: &str, args: &[&str]) -> String {
if args.is_empty() {
format!("{name}()")
} else {
let inner: Vec<String> = args.iter().map(|a| format_arg(a)).collect();
format!("{name}({})", inner.join(", "))
}
}

const PRIORITY_KEYS: [&str; 8] = [
"path",
"file_path",
Expand All @@ -21,6 +41,10 @@ const PRIORITY_KEYS: [&str; 8] = [
];

pub fn generic(input: &str) -> ToolDisplay {
generic_named("", input)
}

pub fn generic_named(tool_name: &str, input: &str) -> ToolDisplay {
let Ok(Value::Object(map)) = serde_json::from_str::<Value>(input) else {
return raw(input);
};
Expand All @@ -43,15 +67,21 @@ pub fn generic(input: &str) -> ToolDisplay {
parts.push(text);
}
}
let mut iter = parts.into_iter();
let Some(primary) = iter.next() else {
if parts.is_empty() {
return raw(input);
};
let rest: Vec<String> = iter.collect();
if rest.is_empty() {
ToolDisplay::primary(primary)
}
let refs: Vec<&str> = parts.iter().map(String::as_str).collect();
if tool_name.is_empty() {
let mut iter = parts.into_iter();
let primary = iter.next().unwrap_or_default();
let rest: Vec<String> = iter.collect();
if rest.is_empty() {
ToolDisplay::primary(primary)
} else {
ToolDisplay::with_detail(primary, rest.join(", "))
}
} else {
ToolDisplay::with_detail(primary, rest.join(" · "))
ToolDisplay::primary(call_sig(tool_name, &refs))
}
}

Expand All @@ -68,7 +98,28 @@ fn scalar_text(value: &Value) -> Option<String> {
mod tests {
use goat_protocol::ToolDisplay;

use super::generic;
use super::{call_sig, format_arg, generic, generic_named};

#[test]
fn generic_named_builds_call_sig() {
let got = generic_named("Glob", r#"{"pattern":"**/symbols*"}"#);
assert_eq!(got.primary, "Glob(**/symbols*)");
}

#[test]
fn format_arg_quotes_spaces() {
assert_eq!(format_arg("a b"), "\"a b\"");
assert_eq!(format_arg("path/to"), "path/to");
}

#[test]
fn call_sig_joins_args() {
assert_eq!(call_sig("Read", &["a.txt"]), "Read(a.txt)");
assert_eq!(
call_sig("Read", &["/Users/jmo", "10", "20"]),
"Read(/Users/jmo, 10, 20)"
);
}

#[test]
fn generic_prefers_priority_keys() {
Expand Down
14 changes: 9 additions & 5 deletions crates/goat-tui/src/app/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,11 +347,15 @@ impl App {
}
}
KeyCode::Enter => {
if let Overlay::Effort(picker) = &self.overlay
&& let EffortOutcome::Selected(effort) = picker.choose()
{
self.overlay = Overlay::None;
return self.apply_effort(effort);
if let Overlay::Effort(picker) = &self.overlay {
if picker.is_empty() {
self.overlay = Overlay::None;
return Vec::new();
}
if let EffortOutcome::Selected(effort) = picker.choose() {
self.overlay = Overlay::None;
return self.apply_effort(effort);
}
}
}
_ => {}
Expand Down
Loading
Loading